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
This commit is contained in:
+164
-13
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from jose import JWTError, jwt
|
||||
from sqlalchemy import func
|
||||
@@ -8,8 +10,12 @@ 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=["管理后台"])
|
||||
|
||||
@@ -39,21 +45,48 @@ def get_current_admin_user(
|
||||
return user
|
||||
|
||||
|
||||
def joke_to_response(joke: Joke) -> JokeResponse:
|
||||
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,
|
||||
type_id=joke.type_id,
|
||||
crowd_id=joke.crowd_id,
|
||||
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_name=joke.type.name if joke.type else None,
|
||||
crowd_name=joke.crowd.name if joke.crowd else None,
|
||||
type_names=type_names,
|
||||
crowd_names=crowd_names,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,6 +100,8 @@ def admin_list_jokes(
|
||||
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)
|
||||
@@ -74,7 +109,7 @@ def admin_list_jokes(
|
||||
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) for j in jokes],
|
||||
items=[joke_to_response(j, db) for j in jokes],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
@@ -88,11 +123,29 @@ def admin_create_joke(
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""创建笑话"""
|
||||
db_joke = Joke(**joke.model_dump())
|
||||
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)
|
||||
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)
|
||||
@@ -108,10 +161,12 @@ def admin_update_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)
|
||||
return joke_to_response(db_joke, db)
|
||||
|
||||
|
||||
@router.delete("/jokes/{joke_id}")
|
||||
@@ -152,12 +207,14 @@ def admin_stats(
|
||||
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,
|
||||
}
|
||||
@@ -209,8 +266,11 @@ def admin_delete_type(
|
||||
db_type = db.query(JokeType).filter(JokeType.id == type_id).first()
|
||||
if not db_type:
|
||||
raise HTTPException(status_code=404, detail="类型不存在")
|
||||
# Check if there are jokes using this type
|
||||
joke_count = db.query(Joke).filter(Joke.type_id == type_id).count()
|
||||
# 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)
|
||||
@@ -263,10 +323,101 @@ def admin_delete_crowd(
|
||||
db_crowd = db.query(JokeCrowd).filter(JokeCrowd.id == crowd_id).first()
|
||||
if not db_crowd:
|
||||
raise HTTPException(status_code=404, detail="人群分类不存在")
|
||||
# Check if there are jokes using this crowd
|
||||
joke_count = db.query(Joke).filter(Joke.crowd_id == crowd_id).count()
|
||||
# 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": "删除成功"}
|
||||
Reference in New Issue
Block a user