Files
bwstudio fde86b90d2 feat: 新增点踩功能,列表卡片和详情页支持 👍/👎 投票
- 后端: 新增 dislike_count 字段(模型/Schema/数据库迁移)
- 后端: 新增 POST /api/jokes/{id}/dislike 端点
- 后端: 管理后台统计新增 total_dislikes
- 前端: 新增 useVote 组合函数(localStorage 持久化防重复)
- 前端: JokeCard 列表卡片新增 👍/👎 可点击投票按钮
- 前端: 详情页 ❤️ 改为 👍 赞一下 / 👎 踩一脚 双按钮
2026-06-11 19:17:12 +08:00

504 lines
17 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
from app.schemas.user import UserCreate, UserResponse
from app.routers.auth import hash_password
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,
ai_score=joke.ai_score,
ai_level=joke.ai_level,
type_ids=ids,
crowd_ids=crowd_ids,
status=joke.status,
view_count=joke.view_count,
like_count=joke.like_count,
dislike_count=joke.dislike_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,
keyword: str = None,
ai_level: 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)
if keyword:
keyword_filter = f"%{keyword}%"
query = query.filter(
(Joke.title.like(keyword_filter)) | (Joke.content.like(keyword_filter))
)
if ai_level:
query = query.filter(Joke.ai_level == ai_level)
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
total_dislikes = db.query(Joke).with_entities(func.sum(Joke.dislike_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,
"total_dislikes": total_dislikes,
}
# 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": "删除成功"}
# ============================================================
# 用户管理
# ============================================================
@router.get("/users", response_model=list[UserResponse])
def admin_list_users(
db: Session = Depends(get_db),
current_user: AdminUser = Depends(get_current_admin_user),
):
"""获取所有用户"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可操作")
return db.query(AdminUser).order_by(AdminUser.created_at.desc()).all()
@router.post("/users", response_model=UserResponse)
def admin_create_user(
user: UserCreate,
db: Session = Depends(get_db),
current_user: AdminUser = Depends(get_current_admin_user),
):
"""创建用户"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可操作")
# 检查用户名是否已存在
existing = db.query(AdminUser).filter(AdminUser.username == user.username).first()
if existing:
raise HTTPException(status_code=400, detail="用户名已存在")
db_user = AdminUser(
username=user.username,
password_hash=hash_password(user.password),
role=user.role,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.delete("/users/{user_id}")
def admin_delete_user(
user_id: int,
db: Session = Depends(get_db),
current_user: AdminUser = Depends(get_current_admin_user),
):
"""删除用户"""
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="仅管理员可操作")
db_user = db.query(AdminUser).filter(AdminUser.id == user_id).first()
if not db_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 不允许删除自己
if db_user.id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除自己")
db.delete(db_user)
db.commit()
return {"message": "删除成功"}