import json from fastapi import APIRouter, Depends, HTTPException, Header from jose import JWTError, jwt from sqlalchemy import func from sqlalchemy.orm import Session from app.config import JWT_ALGORITHM, JWT_SECRET_KEY from app.database import get_db from app.models.joke import Joke from app.models.category import JokeCrowd, JokeType from app.models.user import AdminUser from app.models.link import Link from app.models.feedback import Feedback from app.schemas.category import JokeCrowdCreate, JokeCrowdResponse, JokeTypeCreate, JokeTypeResponse from app.schemas.joke import JokeCreate, JokeResponse, JokeUpdate, PaginatedJokeResponse from app.schemas.link import LinkCreate, LinkResponse from app.schemas.feedback import FeedbackResponse router = APIRouter(prefix="/admin", tags=["管理后台"]) def get_current_admin_user( authorization: str = Header(None), db: Session = Depends(get_db), ) -> AdminUser: """Dependency to get current authenticated admin user.""" if not authorization: raise HTTPException(status_code=401, detail="未提供认证令牌") parts = authorization.split() if len(parts) != 2 or parts[0].lower() != "bearer": raise HTTPException(status_code=401, detail="无效的认证格式") token = parts[1] try: payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) user_id = payload.get("sub") if user_id is None: raise HTTPException(status_code=401, detail="无效的令牌") except JWTError: raise HTTPException(status_code=401, detail="无效或过期的令牌") user = db.query(AdminUser).filter(AdminUser.id == int(user_id)).first() if not user: raise HTTPException(status_code=401, detail="用户不存在") return user def _parse_ids(raw) -> list[int]: """解析 DB 中的 JSON 列表字符串为 Python list""" if not raw: return [] if isinstance(raw, list): return raw try: return json.loads(raw) except Exception: return [] def _get_type_names(db: Session, ids: list[int]) -> list[str]: rows = db.query(JokeType).filter(JokeType.id.in_(ids)).all() return [r.name for r in rows] def _get_crowd_names(db: Session, ids: list[int]) -> list[str]: rows = db.query(JokeCrowd).filter(JokeCrowd.id.in_(ids)).all() return [r.name for r in rows] def joke_to_response(joke: Joke, db: Session = None) -> JokeResponse: """Convert Joke model to JokeResponse schema.""" ids = _parse_ids(joke.type_ids) crowd_ids = _parse_ids(joke.crowd_ids) type_names = _get_type_names(db, ids) if db else [] crowd_names = _get_crowd_names(db, crowd_ids) if db else [] return JokeResponse( id=joke.id, title=joke.title, content=joke.content, polished_content=joke.polished_content, type_ids=ids, crowd_ids=crowd_ids, status=joke.status, view_count=joke.view_count, like_count=joke.like_count, created_at=joke.created_at, updated_at=joke.updated_at, type_names=type_names, crowd_names=crowd_names, ) # Admin Jokes Routes @router.get("/jokes", response_model=PaginatedJokeResponse) def admin_list_jokes( page: int = 1, page_size: int = 20, status: str = None, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """获取所有笑话(支持状态筛选)""" # 限制 page_size 防止 DoS page_size = max(1, min(page_size, 100)) query = db.query(Joke) if status: query = query.filter(Joke.status == status) total = query.count() offset = (page - 1) * page_size jokes = query.order_by(Joke.created_at.desc()).offset(offset).limit(page_size).all() return PaginatedJokeResponse( items=[joke_to_response(j, db) for j in jokes], total=total, page=page, page_size=page_size, ) @router.post("/jokes", response_model=JokeResponse) def admin_create_joke( joke: JokeCreate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """创建笑话""" data = joke.model_dump() if data.get("type_ids") is not None: data["type_ids"] = json.dumps(data["type_ids"]) if data.get("crowd_ids") is not None: data["crowd_ids"] = json.dumps(data["crowd_ids"]) db_joke = Joke(**data) db.add(db_joke) db.commit() db.refresh(db_joke) return joke_to_response(db_joke, db) @router.get("/jokes/{joke_id}", response_model=JokeResponse) def admin_get_joke( joke_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """获取单个笑话""" joke = db.query(Joke).filter(Joke.id == joke_id).first() if not joke: raise HTTPException(status_code=404, detail="笑话不存在") return joke_to_response(joke, db) @router.put("/jokes/{joke_id}", response_model=JokeResponse) def admin_update_joke( joke_id: int, joke: JokeUpdate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """更新笑话""" db_joke = db.query(Joke).filter(Joke.id == joke_id).first() if not db_joke: raise HTTPException(status_code=404, detail="笑话不存在") update_data = joke.model_dump(exclude_unset=True) for key, value in update_data.items(): if key in ("type_ids", "crowd_ids") and value is not None: value = json.dumps(value) setattr(db_joke, key, value) db.commit() db.refresh(db_joke) return joke_to_response(db_joke, db) @router.delete("/jokes/{joke_id}") def admin_delete_joke( joke_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """删除笑话""" db_joke = db.query(Joke).filter(Joke.id == joke_id).first() if not db_joke: raise HTTPException(status_code=404, detail="笑话不存在") db.delete(db_joke) db.commit() return {"message": "删除成功"} @router.put("/jokes/batch-approve") def admin_batch_approve( joke_ids: list[int], db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """批量审核通过笑话""" jokes = db.query(Joke).filter(Joke.id.in_(joke_ids)).all() for joke in jokes: joke.status = "approved" db.commit() return {"message": f"已审核通过 {len(jokes)} 条笑话"} @router.get("/stats") def admin_stats( db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """获取统计数据""" total_jokes = db.query(Joke).count() approved_jokes = db.query(Joke).filter(Joke.status == "approved").count() pending_jokes = db.query(Joke).filter(Joke.status == "pending").count() rejected_jokes = db.query(Joke).filter(Joke.status == "rejected").count() total_views = db.query(Joke).with_entities(func.sum(Joke.view_count)).scalar() or 0 total_likes = db.query(Joke).with_entities(func.sum(Joke.like_count)).scalar() or 0 return { "total_jokes": total_jokes, "approved_jokes": approved_jokes, "pending_jokes": pending_jokes, "rejected_jokes": rejected_jokes, "total_views": total_views, "total_likes": total_likes, } # Categories CRUD @router.post("/types", response_model=JokeTypeResponse) def admin_create_type( joke_type: JokeTypeCreate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """创建笑话类型""" db_type = JokeType(name=joke_type.name, icon=joke_type.icon, sort_order=joke_type.sort_order) db.add(db_type) db.commit() db.refresh(db_type) return JokeTypeResponse.model_validate(db_type) @router.put("/types/{type_id}", response_model=JokeTypeResponse) def admin_update_type( type_id: int, joke_type: JokeTypeResponse, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """更新笑话类型""" db_type = db.query(JokeType).filter(JokeType.id == type_id).first() if not db_type: raise HTTPException(status_code=404, detail="类型不存在") db_type.name = joke_type.name if joke_type.icon is not None: db_type.icon = joke_type.icon if joke_type.sort_order is not None: db_type.sort_order = joke_type.sort_order db.commit() db.refresh(db_type) return JokeTypeResponse.model_validate(db_type) @router.delete("/types/{type_id}") def admin_delete_type( type_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """删除笑话类型""" db_type = db.query(JokeType).filter(JokeType.id == type_id).first() if not db_type: raise HTTPException(status_code=404, detail="类型不存在") # Check both old single-field and new array-field associations old_count = db.query(Joke).filter(Joke.type_id == type_id).count() all_jokes = db.query(Joke.type_ids).filter(Joke.type_ids.isnot(None)).all() new_count = sum(1 for (raw,) in all_jokes if _parse_ids(raw) and type_id in _parse_ids(raw)) joke_count = old_count + new_count if joke_count > 0: raise HTTPException(status_code=400, detail=f"有 {joke_count} 条笑话使用此类型,无法删除") db.delete(db_type) db.commit() return {"message": "删除成功"} @router.post("/crowds", response_model=JokeCrowdResponse) def admin_create_crowd( crowd: JokeCrowdCreate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """创建笑话人群分类""" db_crowd = JokeCrowd(name=crowd.name, icon=crowd.icon, sort_order=crowd.sort_order) db.add(db_crowd) db.commit() db.refresh(db_crowd) return JokeCrowdResponse.model_validate(db_crowd) @router.put("/crowds/{crowd_id}", response_model=JokeCrowdResponse) def admin_update_crowd( crowd_id: int, crowd: JokeCrowdResponse, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """更新笑话人群分类""" db_crowd = db.query(JokeCrowd).filter(JokeCrowd.id == crowd_id).first() if not db_crowd: raise HTTPException(status_code=404, detail="人群分类不存在") db_crowd.name = crowd.name if crowd.icon is not None: db_crowd.icon = crowd.icon if crowd.sort_order is not None: db_crowd.sort_order = crowd.sort_order db.commit() db.refresh(db_crowd) return JokeCrowdResponse.model_validate(db_crowd) @router.delete("/crowds/{crowd_id}") def admin_delete_crowd( crowd_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """删除笑话人群分类""" db_crowd = db.query(JokeCrowd).filter(JokeCrowd.id == crowd_id).first() if not db_crowd: raise HTTPException(status_code=404, detail="人群分类不存在") # Check both old single-field and new array-field associations old_count = db.query(Joke).filter(Joke.crowd_id == crowd_id).count() all_jokes = db.query(Joke.crowd_ids).filter(Joke.crowd_ids.isnot(None)).all() new_count = sum(1 for (raw,) in all_jokes if _parse_ids(raw) and crowd_id in _parse_ids(raw)) joke_count = old_count + new_count if joke_count > 0: raise HTTPException(status_code=400, detail=f"有 {joke_count} 条笑话使用此人群,无法删除") db.delete(db_crowd) db.commit() return {"message": "删除成功"} # ============================================================ # 友情链接管理 # ============================================================ @router.get("/links", response_model=list[LinkResponse]) def admin_list_links( db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """获取所有友情链接""" return db.query(Link).order_by(Link.sort_order, Link.id).all() @router.post("/links", response_model=LinkResponse) def admin_create_link( link: LinkCreate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """创建友情链接""" db_link = Link(**link.model_dump()) db.add(db_link) db.commit() db.refresh(db_link) return db_link @router.put("/links/{link_id}", response_model=LinkResponse) def admin_update_link( link_id: int, link: LinkCreate, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """更新友情链接""" db_link = db.query(Link).filter(Link.id == link_id).first() if not db_link: raise HTTPException(status_code=404, detail="链接不存在") for key, value in link.model_dump().items(): setattr(db_link, key, value) db.commit() db.refresh(db_link) return db_link @router.delete("/links/{link_id}") def admin_delete_link( link_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """删除友情链接""" db_link = db.query(Link).filter(Link.id == link_id).first() if not db_link: raise HTTPException(status_code=404, detail="链接不存在") db.delete(db_link) db.commit() return {"message": "删除成功"} # ============================================================ # 反馈建议管理 # ============================================================ @router.get("/feedbacks", response_model=list[FeedbackResponse]) def admin_list_feedbacks( db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """获取所有反馈建议""" return db.query(Feedback).order_by(Feedback.created_at.desc()).all() @router.delete("/feedbacks/{feedback_id}") def admin_delete_feedback( feedback_id: int, db: Session = Depends(get_db), current_user: AdminUser = Depends(get_current_admin_user), ): """删除反馈""" db_feedback = db.query(Feedback).filter(Feedback.id == feedback_id).first() if not db_feedback: raise HTTPException(status_code=404, detail="反馈不存在") db.delete(db_feedback) db.commit() return {"message": "删除成功"}