Spaces:
Sleeping
Sleeping
update
Browse files- ai_edu.db +0 -0
- app.py +221 -38
- memory-bank/activeContext.md +8 -0
- memory-bank/progress.md +7 -0
- memory-bank/systemPatterns.md +4 -3
- memory-bank/techContext.md +1 -0
- page_info.json +19 -0
- requirements.txt +6 -1
- 方案.md +115 -0
ai_edu.db
CHANGED
|
Binary files a/ai_edu.db and b/ai_edu.db differ
|
|
|
app.py
CHANGED
|
@@ -1,15 +1,30 @@
|
|
| 1 |
-
# conda activate rzwl && uvicorn app:app --host 0.0.0.0 --port
|
| 2 |
-
from fastapi import FastAPI, APIRouter, HTTPException, status
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
-
from pydantic import BaseModel
|
| 5 |
import uuid
|
| 6 |
import sqlite3 # 导入 sqlite3 模块
|
| 7 |
-
from typing import Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
app = FastAPI()
|
| 10 |
|
| 11 |
DATABASE_URL = "ai_edu.db"
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def init_db():
|
| 14 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 15 |
cursor = conn.cursor()
|
|
@@ -51,6 +66,16 @@ def init_db():
|
|
| 51 |
level TEXT
|
| 52 |
)
|
| 53 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
conn.commit()
|
| 55 |
|
| 56 |
# 在应用启动时初始化数据库
|
|
@@ -88,12 +113,19 @@ class EnrollmentIndividualRequest(BaseModel):
|
|
| 88 |
position: str
|
| 89 |
email: str
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
class Course(BaseModel):
|
|
|
|
| 92 |
title: str
|
| 93 |
description: Optional[str] = None
|
| 94 |
-
price: float
|
| 95 |
-
duration_hours: Optional[int] =
|
| 96 |
level: Optional[str] = None
|
|
|
|
| 97 |
|
| 98 |
@api_router.post("/auth/register")
|
| 99 |
async def register_user(request: RegisterRequest):
|
|
@@ -101,9 +133,10 @@ async def register_user(request: RegisterRequest):
|
|
| 101 |
try:
|
| 102 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 103 |
cursor = conn.cursor()
|
|
|
|
| 104 |
cursor.execute(
|
| 105 |
"INSERT INTO users (user_id, username, password) VALUES (?, ?, ?)",
|
| 106 |
-
(user_id, request.username,
|
| 107 |
)
|
| 108 |
conn.commit()
|
| 109 |
return {
|
|
@@ -131,24 +164,30 @@ async def login_user(request: LoginRequest):
|
|
| 131 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 132 |
cursor = conn.cursor()
|
| 133 |
cursor.execute(
|
| 134 |
-
"SELECT user_id, username FROM users WHERE username = ?
|
| 135 |
-
(request.username,
|
| 136 |
)
|
| 137 |
user = cursor.fetchone()
|
| 138 |
if user:
|
| 139 |
-
user_id, username = user
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
"
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
}
|
| 147 |
-
}
|
| 148 |
else:
|
| 149 |
-
# 用户名或密码错误时,返回自定义的错误结构
|
| 150 |
return {
|
| 151 |
-
"code": 401,
|
| 152 |
"message": "用户名或密码错误",
|
| 153 |
"data": None
|
| 154 |
}
|
|
@@ -161,6 +200,90 @@ async def login_user(request: LoginRequest):
|
|
| 161 |
"data": None
|
| 162 |
}
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
@api_router.post("/enrollment/individual")
|
| 165 |
async def enroll_individual(request: EnrollmentIndividualRequest):
|
| 166 |
enrollment_id = str(uuid.uuid4())
|
|
@@ -232,31 +355,62 @@ async def get_individual_orders():
|
|
| 232 |
detail=f"获取个人报名订单失败: {str(e)}"
|
| 233 |
)
|
| 234 |
|
| 235 |
-
@api_router.post("/course/
|
| 236 |
-
async def
|
|
|
|
| 237 |
try:
|
| 238 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 239 |
cursor = conn.cursor()
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
)
|
| 244 |
-
|
| 245 |
-
course_id
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
except Exception as e:
|
| 255 |
raise HTTPException(
|
| 256 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 257 |
-
detail=f"
|
| 258 |
)
|
| 259 |
|
|
|
|
| 260 |
@api_router.get("/course/details/{course_id}")
|
| 261 |
async def get_course_details(course_id: int):
|
| 262 |
try:
|
|
@@ -266,10 +420,13 @@ async def get_course_details(course_id: int):
|
|
| 266 |
cursor.execute("SELECT * FROM courses WHERE course_id = ?", (course_id,))
|
| 267 |
course = cursor.fetchone()
|
| 268 |
if course:
|
|
|
|
|
|
|
|
|
|
| 269 |
return {
|
| 270 |
"code": 200,
|
| 271 |
"message": "获取课程详情成功",
|
| 272 |
-
"data":
|
| 273 |
}
|
| 274 |
else:
|
| 275 |
raise HTTPException(
|
|
@@ -309,6 +466,32 @@ async def delete_individual_enrollment(enrollment_id: str):
|
|
| 309 |
detail=f"删除报名数据失败: {str(e)}"
|
| 310 |
)
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
@app.get("/")
|
| 313 |
def greet_json():
|
| 314 |
return {"Hello": "World!"}
|
|
|
|
| 1 |
+
# conda activate rzwl && uvicorn app:app --host 0.0.0.0 --port 7861 --reload
|
| 2 |
+
from fastapi import FastAPI, APIRouter, HTTPException, status, File, UploadFile # 导入 File 和 UploadFile
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
import uuid
|
| 6 |
import sqlite3 # 导入 sqlite3 模块
|
| 7 |
+
from typing import Optional, List, Dict # 导入类型提示
|
| 8 |
+
import json # 导入 json 模块
|
| 9 |
+
import os # 导入 os 模块
|
| 10 |
+
import urllib.parse # 导入 urllib.parse 用于 URL 编码
|
| 11 |
+
from fastapi.staticfiles import StaticFiles # 导入 StaticFiles
|
| 12 |
+
from passlib.context import CryptContext # 导入 CryptContext
|
| 13 |
+
import httpx # 导入 httpx 用于异步 HTTP 请求
|
| 14 |
|
| 15 |
+
app = FastAPI(max_upload_size=10 * 1024 * 1024) # 设置最大上传大小为 10MB
|
| 16 |
|
| 17 |
DATABASE_URL = "ai_edu.db"
|
| 18 |
|
| 19 |
+
# 用于密码哈希的 CryptContext
|
| 20 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 21 |
+
|
| 22 |
+
def verify_password(plain_password, hashed_password):
|
| 23 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 24 |
+
|
| 25 |
+
def get_password_hash(password):
|
| 26 |
+
return pwd_context.hash(password)
|
| 27 |
+
|
| 28 |
def init_db():
|
| 29 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 30 |
cursor = conn.cursor()
|
|
|
|
| 66 |
level TEXT
|
| 67 |
)
|
| 68 |
""")
|
| 69 |
+
# Add page_info column if it doesn't exist
|
| 70 |
+
cursor.execute("""
|
| 71 |
+
PRAGMA table_info(courses);
|
| 72 |
+
""")
|
| 73 |
+
columns = cursor.fetchall()
|
| 74 |
+
column_names = [col[1] for col in columns]
|
| 75 |
+
if 'page_info' not in column_names:
|
| 76 |
+
cursor.execute("""
|
| 77 |
+
ALTER TABLE courses ADD COLUMN page_info TEXT;
|
| 78 |
+
""")
|
| 79 |
conn.commit()
|
| 80 |
|
| 81 |
# 在应用启动时初始化数据库
|
|
|
|
| 113 |
position: str
|
| 114 |
email: str
|
| 115 |
|
| 116 |
+
# WeChat Configuration (PLACEHOLDERS - REPLACE WITH ACTUAL VALUES OR ENVIRONMENT VARIABLES)
|
| 117 |
+
WECHAT_APP_ID = os.getenv("WECHAT_APP_ID", "YOUR_WECHAT_APP_ID")
|
| 118 |
+
WECHAT_APP_SECRET = os.getenv("WECHAT_APP_SECRET", "YOUR_WECHAT_APP_SECRET") # Add App Secret
|
| 119 |
+
WECHAT_REDIRECT_URI = os.getenv("WECHAT_REDIRECT_URI", "http://localhost:7861/api/auth/wechat/callback") # This should be your frontend callback URL
|
| 120 |
+
|
| 121 |
class Course(BaseModel):
|
| 122 |
+
course_id: Optional[int] = None
|
| 123 |
title: str
|
| 124 |
description: Optional[str] = None
|
| 125 |
+
price: float = 0
|
| 126 |
+
duration_hours: Optional[int] = 0
|
| 127 |
level: Optional[str] = None
|
| 128 |
+
page_info: Optional[List[Dict]] = None # 改为接收JSON对象而非字符串
|
| 129 |
|
| 130 |
@api_router.post("/auth/register")
|
| 131 |
async def register_user(request: RegisterRequest):
|
|
|
|
| 133 |
try:
|
| 134 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 135 |
cursor = conn.cursor()
|
| 136 |
+
hashed_password = get_password_hash(request.password)
|
| 137 |
cursor.execute(
|
| 138 |
"INSERT INTO users (user_id, username, password) VALUES (?, ?, ?)",
|
| 139 |
+
(user_id, request.username, hashed_password)
|
| 140 |
)
|
| 141 |
conn.commit()
|
| 142 |
return {
|
|
|
|
| 164 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 165 |
cursor = conn.cursor()
|
| 166 |
cursor.execute(
|
| 167 |
+
"SELECT user_id, username, password FROM users WHERE username = ?",
|
| 168 |
+
(request.username,)
|
| 169 |
)
|
| 170 |
user = cursor.fetchone()
|
| 171 |
if user:
|
| 172 |
+
user_id, username, hashed_password = user
|
| 173 |
+
if verify_password(request.password, hashed_password):
|
| 174 |
+
return {
|
| 175 |
+
"code": 200,
|
| 176 |
+
"message": "登录成功",
|
| 177 |
+
"data": {
|
| 178 |
+
"user_id": user_id,
|
| 179 |
+
"username": username
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
else:
|
| 183 |
+
return {
|
| 184 |
+
"code": 401,
|
| 185 |
+
"message": "用户名或密码错误",
|
| 186 |
+
"data": None
|
| 187 |
}
|
|
|
|
| 188 |
else:
|
|
|
|
| 189 |
return {
|
| 190 |
+
"code": 401,
|
| 191 |
"message": "用户名或密码错误",
|
| 192 |
"data": None
|
| 193 |
}
|
|
|
|
| 200 |
"data": None
|
| 201 |
}
|
| 202 |
|
| 203 |
+
@api_router.get("/auth/wechat/qrcode")
|
| 204 |
+
async def get_wechat_qrcode():
|
| 205 |
+
"""
|
| 206 |
+
获取微信登录二维码URL。
|
| 207 |
+
"""
|
| 208 |
+
# Generate a random state to prevent CSRF attacks
|
| 209 |
+
state = str(uuid.uuid4())
|
| 210 |
+
# In a real application, you would store this state in a session or database
|
| 211 |
+
# to verify it upon callback.
|
| 212 |
+
|
| 213 |
+
# URL-encode the redirect_uri
|
| 214 |
+
encoded_redirect_uri = urllib.parse.quote_plus(WECHAT_REDIRECT_URI)
|
| 215 |
+
|
| 216 |
+
# Construct the WeChat QR code login URL
|
| 217 |
+
qrcode_url = (
|
| 218 |
+
f"https://open.weixin.qq.com/connect/qrconnect?"
|
| 219 |
+
f"appid={WECHAT_APP_ID}&"
|
| 220 |
+
f"redirect_uri={encoded_redirect_uri}&"
|
| 221 |
+
f"response_type=code&"
|
| 222 |
+
f"scope=snsapi_login&"
|
| 223 |
+
f"state={state}#wechat_redirect"
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
return {
|
| 227 |
+
"code": 200,
|
| 228 |
+
"message": "获取微信二维码URL成功",
|
| 229 |
+
"data": {
|
| 230 |
+
"qrcode_url": qrcode_url,
|
| 231 |
+
"state": state # Return state to frontend for verification
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
@api_router.get("/auth/wechat/callback")
|
| 236 |
+
async def wechat_callback(code: str, state: str):
|
| 237 |
+
"""
|
| 238 |
+
微信登录回调接口,用于接收微信授权码并获取access_token。
|
| 239 |
+
"""
|
| 240 |
+
# In a real application, you would verify the 'state' parameter
|
| 241 |
+
# against the one stored in the user's session to prevent CSRF attacks.
|
| 242 |
+
# For this example, we'll just print it.
|
| 243 |
+
print(f"Received WeChat callback with code: {code} and state: {state}")
|
| 244 |
+
|
| 245 |
+
# Exchange code for access_token
|
| 246 |
+
token_url = (
|
| 247 |
+
f"https://api.weixin.qq.com/sns/oauth2/access_token?"
|
| 248 |
+
f"appid={WECHAT_APP_ID}&"
|
| 249 |
+
f"secret={WECHAT_APP_SECRET}&"
|
| 250 |
+
f"code={code}&"
|
| 251 |
+
f"grant_type=authorization_code"
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
async with httpx.AsyncClient() as client:
|
| 255 |
+
response = await client.get(token_url)
|
| 256 |
+
token_data = response.json()
|
| 257 |
+
|
| 258 |
+
if "errcode" in token_data:
|
| 259 |
+
raise HTTPException(
|
| 260 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
| 261 |
+
detail=f"微信授权失败: {token_data.get('errmsg', '未知错误')}"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Successfully got access_token and other info
|
| 265 |
+
access_token = token_data.get("access_token")
|
| 266 |
+
openid = token_data.get("openid")
|
| 267 |
+
unionid = token_data.get("unionid") # May not be present if scope is only snsapi_base
|
| 268 |
+
|
| 269 |
+
# In a real application, you would now use the access_token and openid/unionid
|
| 270 |
+
# to log in the user, create a new user, or fetch more user info.
|
| 271 |
+
# For example, you might fetch user info:
|
| 272 |
+
# userinfo_url = f"https://api.weixin.qq.com/sns/userinfo?access_token={access_token}&openid={openid}&lang=zh_CN"
|
| 273 |
+
# user_response = await client.get(userinfo_url)
|
| 274 |
+
# user_info = user_response.json()
|
| 275 |
+
|
| 276 |
+
return {
|
| 277 |
+
"code": 200,
|
| 278 |
+
"message": "微信登录回调成功",
|
| 279 |
+
"data": {
|
| 280 |
+
"access_token": access_token,
|
| 281 |
+
"openid": openid,
|
| 282 |
+
"unionid": unionid,
|
| 283 |
+
# "user_info": user_info # Uncomment if fetching user info
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
@api_router.post("/enrollment/individual")
|
| 288 |
async def enroll_individual(request: EnrollmentIndividualRequest):
|
| 289 |
enrollment_id = str(uuid.uuid4())
|
|
|
|
| 355 |
detail=f"获取个人报名订单失败: {str(e)}"
|
| 356 |
)
|
| 357 |
|
| 358 |
+
@api_router.post("/course/save")
|
| 359 |
+
async def save_course(course: Course):
|
| 360 |
+
print('\n\n\nsave_course')
|
| 361 |
try:
|
| 362 |
with sqlite3.connect(DATABASE_URL) as conn:
|
| 363 |
cursor = conn.cursor()
|
| 364 |
+
print('\n\n\n\n')
|
| 365 |
+
print(course)
|
| 366 |
+
# Convert page_info to JSON string before saving
|
| 367 |
+
page_info_json = json.dumps(course.page_info) if course.page_info is not None else None
|
| 368 |
+
|
| 369 |
+
if course.course_id is not None:
|
| 370 |
+
# 更新现有课程
|
| 371 |
+
cursor.execute(
|
| 372 |
+
"UPDATE courses SET title = ?, description = ?, price = ?, duration_hours = ?, level = ?, page_info = ? WHERE course_id = ?",
|
| 373 |
+
(course.title, course.description, course.price, course.duration_hours, course.level, page_info_json, course.course_id)
|
| 374 |
+
)
|
| 375 |
+
conn.commit()
|
| 376 |
+
if cursor.rowcount == 0:
|
| 377 |
+
raise HTTPException(
|
| 378 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
| 379 |
+
detail="未找到该课程进行更新"
|
| 380 |
+
)
|
| 381 |
+
return {
|
| 382 |
+
"code": 200,
|
| 383 |
+
"message": "课程更新成功",
|
| 384 |
+
"data": {
|
| 385 |
+
"course_id": course.course_id,
|
| 386 |
+
**course.dict(exclude_unset=True)
|
| 387 |
+
}
|
| 388 |
+
}
|
| 389 |
+
else:
|
| 390 |
+
# 添加新课程
|
| 391 |
+
cursor.execute(
|
| 392 |
+
"INSERT INTO courses (title, description, price, duration_hours, level, page_info) VALUES (?, ?, ?, ?, ?, ?)",
|
| 393 |
+
(course.title, course.description, course.price, course.duration_hours, course.level, page_info_json)
|
| 394 |
+
)
|
| 395 |
+
conn.commit()
|
| 396 |
+
course_id = cursor.lastrowid
|
| 397 |
+
return {
|
| 398 |
+
"code": 200,
|
| 399 |
+
"message": "课程添加成功",
|
| 400 |
+
"data": {
|
| 401 |
+
"course_id": course_id,
|
| 402 |
+
**course.dict(exclude_unset=True)
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
except HTTPException as e:
|
| 406 |
+
raise e
|
| 407 |
except Exception as e:
|
| 408 |
raise HTTPException(
|
| 409 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 410 |
+
detail=f"保存课程失败: {str(e)}"
|
| 411 |
)
|
| 412 |
|
| 413 |
+
|
| 414 |
@api_router.get("/course/details/{course_id}")
|
| 415 |
async def get_course_details(course_id: int):
|
| 416 |
try:
|
|
|
|
| 420 |
cursor.execute("SELECT * FROM courses WHERE course_id = ?", (course_id,))
|
| 421 |
course = cursor.fetchone()
|
| 422 |
if course:
|
| 423 |
+
course_dict = dict(course)
|
| 424 |
+
if course_dict['page_info']:
|
| 425 |
+
course_dict['page_info'] = json.loads(course_dict['page_info'])
|
| 426 |
return {
|
| 427 |
"code": 200,
|
| 428 |
"message": "获取课程详情成功",
|
| 429 |
+
"data": course_dict
|
| 430 |
}
|
| 431 |
else:
|
| 432 |
raise HTTPException(
|
|
|
|
| 466 |
detail=f"删除报名数据失败: {str(e)}"
|
| 467 |
)
|
| 468 |
|
| 469 |
+
@api_router.post("/upload/images")
|
| 470 |
+
async def upload_image(image: UploadFile = File(...)): # 将参数名从 file 改为 image,并恢复 File(...)
|
| 471 |
+
os.makedirs("upload/images", exist_ok=True) # 自动创建目录
|
| 472 |
+
file_location = f"upload/images/{image.filename}" # 使用 image.filename
|
| 473 |
+
try:
|
| 474 |
+
with open(file_location, "wb+") as buffer: # 改为 wb+
|
| 475 |
+
content = await image.read() # 异步读取
|
| 476 |
+
buffer.write(content)
|
| 477 |
+
return {
|
| 478 |
+
"code": 200,
|
| 479 |
+
"message": "图片上传成功", # 增加 message 字段
|
| 480 |
+
"data": {
|
| 481 |
+
"filename": image.filename, # 使用 image.filename
|
| 482 |
+
"url": f"/upload/images/{image.filename}" # 使用 image.filename
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
except Exception as e:
|
| 486 |
+
print(f"图片上传失败: {str(e)}") # 输出详细错误
|
| 487 |
+
raise HTTPException(
|
| 488 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, # 恢复 status.HTTP_500_INTERNAL_SERVER_ERROR
|
| 489 |
+
detail=f"图片上传失败: {str(e)}"
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
# 挂载静态文件目录
|
| 493 |
+
app.mount("/upload", StaticFiles(directory="upload"), name="upload")
|
| 494 |
+
|
| 495 |
@app.get("/")
|
| 496 |
def greet_json():
|
| 497 |
return {"Hello": "World!"}
|
memory-bank/activeContext.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
- 优化了登录接口的错误响应,使其返回自定义的业务错误码和信息。
|
| 9 |
- **新增了课程管理功能,包括课程表的创建和课程详情查询接口 (`/api/course/details/{course_id}`)。**
|
| 10 |
- **新增了课程添加接口 (`/api/course/add`) 用于测试和数据初始化。**
|
|
|
|
| 11 |
|
| 12 |
## 最近的更改
|
| 13 |
- `app.py`:
|
|
@@ -20,6 +21,13 @@
|
|
| 20 |
- 添加 `/api/auth/login` 接口,实现用户登录验证和错误响应。
|
| 21 |
- 添加 `CORSMiddleware` 处理跨域请求。
|
| 22 |
- **修复了 `Course` Pydantic 模型中的类型提示错误,将 `str | None` 更改为 `Optional[str]`,以兼容 Python 3.9 环境。**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
## 下一步计划
|
| 25 |
- **安全性增强**:
|
|
|
|
| 8 |
- 优化了登录接口的错误响应,使其返回自定义的业务错误码和信息。
|
| 9 |
- **新增了课程管理功能,包括课程表的创建和课程详情查询接口 (`/api/course/details/{course_id}`)。**
|
| 10 |
- **新增了课程添加接口 (`/api/course/add`) 用于测试和数据初始化。**
|
| 11 |
+
- **创建了 `upload/images` 文件夹,用于存储上传的图片文件。**
|
| 12 |
|
| 13 |
## 最近的更改
|
| 14 |
- `app.py`:
|
|
|
|
| 21 |
- 添加 `/api/auth/login` 接口,实现用户登录验证和错误响应。
|
| 22 |
- 添加 `CORSMiddleware` 处理跨域请求。
|
| 23 |
- **修复了 `Course` Pydantic 模型中的类型提示错误,将 `str | None` 更改为 `Optional[str]`,以兼容 Python 3.9 环境。**
|
| 24 |
+
- **在 `init_db()` 中为 `courses` 表添加了 `page_info` 字段(TEXT 类型,用于存储 JSON)。**
|
| 25 |
+
- **更新了 `Course` Pydantic 模型,包含 `page_info` 字段。**
|
| 26 |
+
- **修改了 `add_course` 接口,以处理 `page_info` 字段的 JSON 序列化存储。**
|
| 27 |
+
- **修改了 `get_course_details` 接口,以处理 `page_info` 字段的 JSON 反序列化。**
|
| 28 |
+
- **文件系统**:
|
| 29 |
+
- **创建了 `upload/images` 目录。**
|
| 30 |
+
- **配置了 FastAPI 以提供静态文件服务,`upload` 目录可通过 `/upload` 路径访问。**
|
| 31 |
|
| 32 |
## 下一步计划
|
| 33 |
- **安全性增强**:
|
memory-bank/progress.md
CHANGED
|
@@ -16,6 +16,13 @@
|
|
| 16 |
- 登录失败返回自定义错误码和信息。
|
| 17 |
- 优化了错误捕获和日志输出。
|
| 18 |
- **类型提示兼容性修复**: 修复了 `app.py` 中 `Course` 模型因 Python 版本导致的类型提示错误。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
## 待完成的工作
|
| 21 |
- **安全性增强**:
|
|
|
|
| 16 |
- 登录失败返回自定义错误码和信息。
|
| 17 |
- 优化了错误捕获和日志输出。
|
| 18 |
- **类型提示兼容性修复**: 修复了 `app.py` 中 `Course` 模型因 Python 版本导致的类型提示错误。
|
| 19 |
+
- **课程表字段扩展**: 在 `courses` 表中增加了 `page_info` 字段(TEXT 类型,用于存储 JSON)。
|
| 20 |
+
- **课程接口功能增强**:
|
| 21 |
+
- `Course` Pydantic 模型已更新,包含 `page_info` 字段。
|
| 22 |
+
- `add_course` 接口现在支持 `page_info` 字段的 JSON 序列化存储。
|
| 23 |
+
- `get_course_details` 接口现在支持 `page_info` 字段的 JSON 反序列化。
|
| 24 |
+
- **文件系统**:
|
| 25 |
+
- **创建了 `upload/images` 目录。**
|
| 26 |
|
| 27 |
## 待完成的工作
|
| 28 |
- **安全性增强**:
|
memory-bank/systemPatterns.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
| 5 |
- **数据库**: SQLite
|
| 6 |
- **API 路由**: 使用 `APIRouter` 进行模块化管理,所有 API 路径前缀为 `/api`。
|
| 7 |
- **CORS**: 使用 `CORSMiddleware` 处理跨域请求。
|
|
|
|
| 8 |
|
| 9 |
## 关键技术决策
|
| 10 |
- **用户 ID 生成**: 采用 `uuid.uuid4()` 生成全局唯一的用户 ID。
|
|
@@ -19,10 +20,10 @@
|
|
| 19 |
- `app.py`: 包含 FastAPI 应用实例、CORS 配置、数据库初始化逻辑、API 路由定义。
|
| 20 |
- `api_router`: 负责定义 `/api/auth/register`, `/api/auth/login`, `/api/course/add`, `/api/course/details/{course_id}` 等接口。
|
| 21 |
- `sqlite3`: 用于与 `ai_edu.db` 数据库文件进行交互。
|
| 22 |
-
- `pydantic.BaseModel`: 用于定义请求体的模型(`RegisterRequest`, `LoginRequest`)。
|
| 23 |
|
| 24 |
## 关键实现路径
|
| 25 |
- **用户注册**: 接收 `username` 和 `password` -> 生成 `user_id` (UUID) -> 插入 `users` 表 -> 返回成功响应或错误。
|
| 26 |
- **用户登录**: 接收 `username` 和 `password` -> 查询 `users` 表验证凭据 -> 返回成功响应(包含 `user_id` 和 `username`)或错误。
|
| 27 |
-
- **课程添加**: 接收课程详情 (`title`, `description`, `price`, `duration_hours`, `level`) -> 插入 `courses` 表 -> 返回成功响应。
|
| 28 |
-
- **课程详情查询**: 接收 `course_id` -> 查询 `courses` 表 -> 返回课程详情或 404 错误。
|
|
|
|
| 5 |
- **数据库**: SQLite
|
| 6 |
- **API 路由**: 使用 `APIRouter` 进行模块化管理,所有 API 路径前缀为 `/api`。
|
| 7 |
- **CORS**: 使用 `CORSMiddleware` 处理跨域请求。
|
| 8 |
+
- **文件存储**: `upload/images` 目录用于存储上传的图片文件,并通过 `/upload` 路径提供静态文件服务。
|
| 9 |
|
| 10 |
## 关键技术决策
|
| 11 |
- **用户 ID 生成**: 采用 `uuid.uuid4()` 生成全局唯一的用户 ID。
|
|
|
|
| 20 |
- `app.py`: 包含 FastAPI 应用实例、CORS 配置、数据库初始化逻辑、API 路由定义。
|
| 21 |
- `api_router`: 负责定义 `/api/auth/register`, `/api/auth/login`, `/api/course/add`, `/api/course/details/{course_id}` 等接口。
|
| 22 |
- `sqlite3`: 用于与 `ai_edu.db` 数据库文件进行交互。
|
| 23 |
+
- `pydantic.BaseModel`: 用于定义请求体的模型(`RegisterRequest`, `LoginRequest`, `Course`)。
|
| 24 |
|
| 25 |
## 关键实现路径
|
| 26 |
- **用户注册**: 接收 `username` 和 `password` -> 生成 `user_id` (UUID) -> 插入 `users` 表 -> 返回成功响应或错误。
|
| 27 |
- **用户登录**: 接收 `username` 和 `password` -> 查询 `users` 表验证凭据 -> 返回成功响应(包含 `user_id` 和 `username`)或错误。
|
| 28 |
+
- **课程添加**: 接收课程详情 (`title`, `description`, `price`, `duration_hours`, `level`, `page_info`) -> 插入 `courses` 表(`page_info` 存储为 JSON 字符串) -> 返回成功响应。
|
| 29 |
+
- **课程详情查询**: 接收 `course_id` -> 查询 `courses` 表 -> 返回课程详情(`page_info` 反序列化为 JSON 对象)或 404 错误。
|
memory-bank/techContext.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
- **Pydantic**: 数据验证和设置管理库,用于定义请求体模型
|
| 8 |
- **SQLite3**: Python 内置的轻量级数据库
|
| 9 |
- **uuid**: Python 标准库,用于生成通用唯一标识符
|
|
|
|
| 10 |
|
| 11 |
## 开发环境设置
|
| 12 |
- **Conda**: 用于管理 Python 虚拟环境 (`conda activate rzwl`)
|
|
|
|
| 7 |
- **Pydantic**: 数据验证和设置管理库,用于定义请求体模型
|
| 8 |
- **SQLite3**: Python 内置的轻量级数据库
|
| 9 |
- **uuid**: Python 标准库,用于生成通用唯一标识符
|
| 10 |
+
- **FastAPI.staticfiles**: 用于提供静态文件服务
|
| 11 |
|
| 12 |
## 开发环境设置
|
| 13 |
- **Conda**: 用于管理 Python 虚拟环境 (`conda activate rzwl`)
|
page_info.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"type": "image",
|
| 4 |
+
"url": "/upload/images/banner.jpg"
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"pictures": [
|
| 8 |
+
{
|
| 9 |
+
"url": "/upload/images/pic1.jpg"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"url": "/upload/images/pic2.jpg"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"url": "/upload/images/pic3.jpg"
|
| 16 |
+
}
|
| 17 |
+
]
|
| 18 |
+
}
|
| 19 |
+
]
|
requirements.txt
CHANGED
|
@@ -1,2 +1,7 @@
|
|
| 1 |
fastapi
|
| 2 |
-
uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
pydantic
|
| 4 |
+
python-multipart
|
| 5 |
+
passlib
|
| 6 |
+
bcrypt
|
| 7 |
+
httpx
|
方案.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI-EDU 后端 API 方案
|
| 2 |
+
|
| 3 |
+
## 1. 项目概述
|
| 4 |
+
|
| 5 |
+
**项目名称**: AI-EDU 后端 API
|
| 6 |
+
|
| 7 |
+
**项目目标**: 使用 FastAPI 实现 AI-EDU H5 应用的后端 API,提供用户认证(注册、登录)、课程管理等功能,并使用 SQLite 数据库进行数据存储。
|
| 8 |
+
|
| 9 |
+
**核心需求**:
|
| 10 |
+
- 实现用户注册接口 (`/api/auth/register`)
|
| 11 |
+
- 实现用户登录接口 (`/api/auth/login`)
|
| 12 |
+
- 实现课程添加接口 (`/api/course/add`)
|
| 13 |
+
- 实现课程详情查询接口 (`/api/course/details/{course_id}`)
|
| 14 |
+
- 使用 SQLite 数据库存储用户和课程数据
|
| 15 |
+
- 接口响应遵循统一的数据结构(code, message, data)
|
| 16 |
+
- 处理跨域请求 (CORS)
|
| 17 |
+
- 提供静态文件服务,用于存储上传的图片文件
|
| 18 |
+
|
| 19 |
+
**预期成果**: 一个稳定、可扩展的 FastAPI 后端服务,能够支持 AI-EDU H5 应用的基本用户认证和课程管理功能。
|
| 20 |
+
|
| 21 |
+
## 2. 产品背景
|
| 22 |
+
|
| 23 |
+
**项目存在的原因**: 为 AI-EDU H5 前端应用提供必要的后端支持,特别是用户认证、课程管理和数据管理功能。
|
| 24 |
+
|
| 25 |
+
**解决的问题**:
|
| 26 |
+
- 用户注册和登录:允许用户创建账户并登录到应用。
|
| 27 |
+
- 数据存储:持久化用户和课程数据,为后续功能提供基础。
|
| 28 |
+
- API 接口:提供前端与后端交互的标准接口。
|
| 29 |
+
- 静态资源服务:支持图片等静态文件的上传和访问。
|
| 30 |
+
|
| 31 |
+
**工作方式**: 前端通过 HTTP 请求调用后端 API,后端处理业务逻辑并与 SQLite 数据库交互,然后返回结构化的 JSON 响应。
|
| 32 |
+
|
| 33 |
+
**用户体验目标**:
|
| 34 |
+
- 注册和登录流程顺畅,响应及时。
|
| 35 |
+
- 错误信息清晰,便于用户理解和操作。
|
| 36 |
+
- 课程信息展示准确,加载迅速。
|
| 37 |
+
|
| 38 |
+
## 3. 系统架构与技术栈
|
| 39 |
+
|
| 40 |
+
**系统架构**:
|
| 41 |
+
- **后端框架**: FastAPI
|
| 42 |
+
- **数据库**: SQLite
|
| 43 |
+
- **ASGI 服务器**: Uvicorn
|
| 44 |
+
- **API 路由**: 使用 `APIRouter` 进行模块化管理,所有 API 路径前缀为 `/api`。
|
| 45 |
+
- **CORS**: 使用 `CORSMiddleware` 处理跨域请求。
|
| 46 |
+
- **文件存储**: `upload/images` 目录用于存储上传的图片文件,并通过 `/upload` 路径提供静态文件服务。
|
| 47 |
+
|
| 48 |
+
**关键技术决策**:
|
| 49 |
+
- **用户 ID 生成**: 采用 `uuid.uuid4()` 生成全局唯一的用户 ID。
|
| 50 |
+
- **密码存储**: 当前直接存储明文密码(**待改进:未来应使用哈希加密存储**)。
|
| 51 |
+
- **错误处理**: 统一的 `code`, `message`, `data` 响应格式。
|
| 52 |
+
- **数据库初始化**: 在 FastAPI 应用启动时通过 `@app.on_event("startup")` 钩子函数初始化 SQLite 数据库和相关表。
|
| 53 |
+
- **课程 `page_info` 存储**: 存储为 JSON 字符串,查询时反序列化。
|
| 54 |
+
|
| 55 |
+
**组件关系**:
|
| 56 |
+
- `app.py`: FastAPI 应用实例、CORS 配置、数据库初始化、API 路由定义。
|
| 57 |
+
- `api_router`: 定义用户认证和课程管理接口。
|
| 58 |
+
- `sqlite3`: 与 `ai_edu.db` 数据库文件交互。
|
| 59 |
+
- `pydantic.BaseModel`: 定义请求体模型(`RegisterRequest`, `LoginRequest`, `Course`)。
|
| 60 |
+
- `FastAPI.staticfiles`: 提供静态文件服务。
|
| 61 |
+
|
| 62 |
+
**使用技术**:
|
| 63 |
+
- Python
|
| 64 |
+
- FastAPI
|
| 65 |
+
- Uvicorn
|
| 66 |
+
- Pydantic
|
| 67 |
+
- SQLite3
|
| 68 |
+
- uuid
|
| 69 |
+
- FastAPI.staticfiles
|
| 70 |
+
|
| 71 |
+
**开发环境设置**:
|
| 72 |
+
- Conda: 用于管理 Python 虚拟环境 (`conda activate rzwl`)
|
| 73 |
+
- 依赖安装: `pip install -r requirements.txt` (包含 `fastapi`, `uvicorn[standard]`)
|
| 74 |
+
|
| 75 |
+
**运行命令**:
|
| 76 |
+
`conda activate rzwl && uvicorn app:app --host 0.0.0.0 --port 7860 --reload`
|
| 77 |
+
|
| 78 |
+
**技术约束**:
|
| 79 |
+
- SQLite 数据库适用于小型项目或开发环境,不适合高并发、大规模生产环境。
|
| 80 |
+
- 密码当前未加密存储,存在安全风险,未来需要引入密码哈希库(如 `passlib`)。
|
| 81 |
+
|
| 82 |
+
## 4. 当前进展与下一步计划
|
| 83 |
+
|
| 84 |
+
**已完成的工作**:
|
| 85 |
+
- FastAPI 应用启动和 CORS 配置。
|
| 86 |
+
- 用户注册接口 (`/api/auth/register`):实现了用户 ID 生成、数据存储到 SQLite、用户名重复处理。
|
| 87 |
+
- 用户登录接口 (`/api/auth/login`):实现了用户验证、自定义错误响应。
|
| 88 |
+
- 课程管理功能:
|
| 89 |
+
- 实现了 `courses` 表的创建。
|
| 90 |
+
- 实现了 `/api/course/add` 接口,用于添加课程,支持 `page_info` 字段的 JSON 序列化存储。
|
| 91 |
+
- 实现了 `/api/course/details/{course_id}` 接口,用于获取课程详情,支持 `page_info` 字段的 JSON 反序列化。
|
| 92 |
+
- `Course` Pydantic 模型类型提示兼容性修复。
|
| 93 |
+
- 创建了 `upload/images` 目录,并配置了 FastAPI 提供静态文件服务。
|
| 94 |
+
|
| 95 |
+
**待完成的工作**:
|
| 96 |
+
- **安全性增强**:
|
| 97 |
+
- **密码哈希**: 引入密码哈希(如 `passlib`)来加密存储用户密码,提高安全性。
|
| 98 |
+
- **会话管理/JWT**: 实现用户登录后的会话管理或 JWT 认证机制。
|
| 99 |
+
- **更多业务接口**: 根据 AI-EDU H5 的需求,逐步实现其他后端 API。
|
| 100 |
+
- **数据库抽象**: 考虑引入 ORM(如 SQLAlchemy)来更好地管理数据库操作。
|
| 101 |
+
|
| 102 |
+
**当前状态**:
|
| 103 |
+
- 基本的用户认证和课程管理功能已实现并可运行。
|
| 104 |
+
- 数据库集成已完成。
|
| 105 |
+
- 接口响应格式已统一。
|
| 106 |
+
|
| 107 |
+
**已知问题**:
|
| 108 |
+
- 密码明文存储,存在安全风���。
|
| 109 |
+
|
| 110 |
+
**项目决策演变**:
|
| 111 |
+
- 从简单的接口逐步扩展到完整的用户认证和课程管理流程。
|
| 112 |
+
- 从模拟 `user_id` 到使用 `uuid` 确保唯一性。
|
| 113 |
+
- 从默认错误到自定义业务错误响应。
|
| 114 |
+
- 从未处理 CORS 到添加 `CORSMiddleware`。
|
| 115 |
+
- 增加了课程管理功能和静态文件服务。
|