Files
joke/api/app/routers/admin.py
T
bwstudio ceed63fcb0 fix: resolve 10 code review issues
High priority:
- Fix concurrent race condition for view_count/like_count (atomic update)
- Add route request ID tracking to prevent race conditions
- Filter get_joke by status=approved (no pending content leak)
- Add error feedback for like button

Performance:
- Optimize random joke query (avoid full table sort)
- Limit page_size max to 100 (DoS prevention)

Medium:
- Add localStorage quota error handling
- Handle empty AI response gracefully
- Fix generate content title extraction

Low:
- Add rejected_jokes to stats API
- Update dashboard to show rejected count
2026-06-02 20:35:08 +08:00

423 lines
14 KiB
Python

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": "删除成功"}