feat(init): 添加完整的认证和业务路由模块
- 实现微信登录、手机号绑定、token验证和登出功能 - 添加打卡系统,支持正常、迟到、请假等状态记录 - 集成课程投票功能,支持多课程选择和评分评论 - 开发反馈系统,支持图文反馈提交和管理员处理 - 构建首页页面模块,动态展示学习资源和个人信息 - 设计计划管理系统,支持周/月/年计划制定和跟踪 - 实现小组计划统计和历史记录查询功能 - 完善用户认证会话管理和数据库交互逻辑
This commit is contained in:
109
routes/feedback.py
Normal file
109
routes/feedback.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .. import config
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/feedback", tags=["feedback"])
|
||||
|
||||
|
||||
def _image_dir():
|
||||
return os.path.join(config.FEEDBACK_DIR, "images")
|
||||
|
||||
|
||||
def _ensure_dirs():
|
||||
os.makedirs(config.FEEDBACK_DIR, exist_ok=True)
|
||||
os.makedirs(_image_dir(), exist_ok=True)
|
||||
|
||||
|
||||
def _load_items():
|
||||
items = []
|
||||
for fn in os.listdir(config.FEEDBACK_DIR):
|
||||
if fn.endswith(".json"):
|
||||
path = os.path.join(config.FEEDBACK_DIR, fn)
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
items.append(json.load(f))
|
||||
except Exception:
|
||||
logger.exception("读取反馈数据文件失败: %s", path)
|
||||
return items
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_feedback():
|
||||
items = _load_items()
|
||||
unsolved = sorted([x for x in items if x.get("status") == "unsolved"], key=lambda x: x["created_at"], reverse=True)
|
||||
solved = sorted([x for x in items if x.get("status") != "unsolved"], key=lambda x: x["created_at"], reverse=True)
|
||||
return unsolved + solved
|
||||
|
||||
|
||||
class FeedbackRequest(BaseModel):
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
image: str = ""
|
||||
|
||||
|
||||
@router.post("")
|
||||
def submit_feedback(req: FeedbackRequest):
|
||||
_ensure_dirs()
|
||||
fid = os.urandom(4).hex()
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
item = {
|
||||
"id": fid,
|
||||
"name": req.name.strip() or "匿名",
|
||||
"description": req.description.strip(),
|
||||
"status": "unsolved",
|
||||
"created_at": now,
|
||||
}
|
||||
img = req.image
|
||||
if img.startswith("data:image/"):
|
||||
try:
|
||||
fmt = re.sub(r"[^a-zA-Z0-9]", "", img.split(";")[0].split("/")[1])
|
||||
with open(os.path.join(_image_dir(), fid + "." + fmt), "wb") as f:
|
||||
f.write(base64.b64decode(img.split(",")[1]))
|
||||
item["image"] = "/api/v1/feedback/file/" + fid + "." + fmt
|
||||
except Exception:
|
||||
logger.exception("保存反馈图片失败: %s", fid)
|
||||
with open(os.path.join(config.FEEDBACK_DIR, fid + ".json"), "w", encoding="utf-8") as f:
|
||||
json.dump(item, f, ensure_ascii=False)
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/file/{name}")
|
||||
def get_file(name: str):
|
||||
safe_name = os.path.basename(name)
|
||||
path = os.path.join(_image_dir(), safe_name)
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
ext = os.path.splitext(safe_name)[1].lstrip(".").lower()
|
||||
media = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", "gif": "image/gif", "webp": "image/webp"}.get(ext, "application/octet-stream")
|
||||
return FileResponse(path, media_type=media)
|
||||
|
||||
|
||||
@router.post("/resolve")
|
||||
def resolve(id: str = "", token: str = ""):
|
||||
safe_id = re.sub(r"[^a-zA-Z0-9]", "", id)
|
||||
if token != config.ADMIN_KEY:
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
if not safe_id:
|
||||
raise HTTPException(status_code=404, detail="未找到")
|
||||
path = os.path.join(config.FEEDBACK_DIR, safe_id + ".json")
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404, detail="未找到")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
item = json.load(f)
|
||||
item["status"] = "solved"
|
||||
item["resolved_at"] = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(item, f, ensure_ascii=False)
|
||||
return {"ok": True, "id": safe_id}
|
||||
Reference in New Issue
Block a user