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}