from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status from pydantic import BaseModel, Field from sqlalchemy.orm import Session from app.core.deps import require_member from app.db import get_db from app.models.content import AdminFile, read_upload_limit from app.models.expert import create_question, member_question, member_questions from app.models.member import get_member_by_id from app.models.uploaded_case import create_uploaded_case from app.services.storage import ( make_object_key, private_object_url, remove_stored_object, safe_filename, upload_object, ) router = APIRouter(dependencies=[Depends(require_member)]) class QuestionWrite(BaseModel): title: str = "" body: str = "" attachmentFileIds: list[int] = Field(default_factory=list) def _member_id(principal: dict) -> int: try: return int(principal["sub"]) except (TypeError, ValueError, KeyError) as exc: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效") from exc def _active_member(db: Session, principal: dict): member = get_member_by_id(db, _member_id(principal)) if member is None or member.status != "active": raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已失效") return member def _content_type(value: str | None) -> str: raw = (value or "").split(";", 1)[0].strip().lower() return raw.replace("\r", "").replace("\n", "")[:128] def _read_limited(upload: UploadFile, limit: int) -> bytes: chunks = bytearray() while True: piece = upload.file.read(65536) if not piece: break if len(chunks) + len(piece) > limit: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="文件超过大小上限") chunks.extend(piece) return bytes(chunks) @router.get("/me") def member_me( principal: dict = Depends(require_member), db: Session = Depends(get_db), ) -> dict: member = _active_member(db, principal) return { "id": member.id, "email": member.email, "tokenType": principal["typ"], } @router.post("/files") def upload_private_file( file: UploadFile = File(...), principal: dict = Depends(require_member), db: Session = Depends(get_db), ): member = _active_member(db, principal) limit = read_upload_limit(db) if limit is None: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="未配置上传大小上限") data = _read_limited(file, limit) filename = safe_filename(file.filename) content_type = _content_type(file.content_type) object_key = make_object_key(filename) upload_object("private", object_key, data, content_type) row = AdminFile( bucket="private", object_key=object_key, filename=filename, content_type=content_type, size=len(data), created_by=member.id, ) db.add(row) try: db.commit() db.refresh(row) except Exception: db.rollback() try: remove_stored_object("private", object_key) except HTTPException: pass raise url, expires_at = private_object_url(row.object_key) return { "id": row.id, "bucket": row.bucket, "filename": row.filename, "contentType": row.content_type, "size": row.size, "url": url, "expiresAt": expires_at.isoformat(), } @router.get("/expert-questions") def list_my_questions( principal: dict = Depends(require_member), db: Session = Depends(get_db), ) -> dict: member = _active_member(db, principal) return member_questions(db, member.id) @router.get("/expert-questions/{question_id}") def read_my_question( question_id: int, principal: dict = Depends(require_member), db: Session = Depends(get_db), ) -> dict: member = _active_member(db, principal) body = member_question(db, member.id, question_id) if body is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="提问不存在") return body @router.post("/expert-questions") def ask_question( body: QuestionWrite, principal: dict = Depends(require_member), db: Session = Depends(get_db), ) -> dict: member = _active_member(db, principal) try: question_id = create_question( db, member_id=member.id, title=body.title, body=body.body, attachment_file_ids=body.attachmentFileIds, ) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return {"id": question_id} @router.post("/uploaded-cases") def submit_uploaded_case( body: dict, principal: dict = Depends(require_member), db: Session = Depends(get_db), ) -> dict: member = _active_member(db, principal) try: row = create_uploaded_case(db, member, body) except ValueError as exc: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc return {"id": row.id, "message": "提交成功"}