- 实现微信登录、手机号绑定、token验证和登出功能 - 添加打卡系统,支持正常、迟到、请假等状态记录 - 集成课程投票功能,支持多课程选择和评分评论 - 开发反馈系统,支持图文反馈提交和管理员处理 - 构建首页页面模块,动态展示学习资源和个人信息 - 设计计划管理系统,支持周/月/年计划制定和跟踪 - 实现小组计划统计和历史记录查询功能 - 完善用户认证会话管理和数据库交互逻辑
110 lines
3.5 KiB
Python
110 lines
3.5 KiB
Python
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}
|