- jokes.py: Public joke listing, detail, and random endpoints - categories.py: Type and crowd listing endpoints - auth.py: Login endpoint with JWT token generation - admin.py: Full CRUD for jokes, types, crowds with JWT protection - Fix request body schemas to use Create DTOs instead of Response models Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
272 lines
9.1 KiB
Python
272 lines
9.1 KiB
Python
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.schemas.category import JokeCrowdCreate, JokeCrowdResponse, JokeTypeCreate, JokeTypeResponse
|
|
from app.schemas.joke import JokeCreate, JokeResponse, JokeUpdate, PaginatedJokeResponse
|
|
|
|
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 joke_to_response(joke: Joke) -> JokeResponse:
|
|
"""Convert Joke model to JokeResponse schema."""
|
|
return JokeResponse(
|
|
id=joke.id,
|
|
title=joke.title,
|
|
content=joke.content,
|
|
type_id=joke.type_id,
|
|
crowd_id=joke.crowd_id,
|
|
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,
|
|
)
|
|
|
|
|
|
# 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),
|
|
):
|
|
"""获取所有笑话(支持状态筛选)"""
|
|
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) 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),
|
|
):
|
|
"""创建笑话"""
|
|
db_joke = Joke(**joke.model_dump())
|
|
db.add(db_joke)
|
|
db.commit()
|
|
db.refresh(db_joke)
|
|
return joke_to_response(db_joke)
|
|
|
|
|
|
@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():
|
|
setattr(db_joke, key, value)
|
|
db.commit()
|
|
db.refresh(db_joke)
|
|
return joke_to_response(db_joke)
|
|
|
|
|
|
@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()
|
|
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,
|
|
"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 if there are jokes using this type
|
|
joke_count = db.query(Joke).filter(Joke.type_id == type_id).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 if there are jokes using this crowd
|
|
joke_count = db.query(Joke).filter(Joke.crowd_id == crowd_id).count()
|
|
if joke_count > 0:
|
|
raise HTTPException(status_code=400, detail=f"有 {joke_count} 条笑话使用此人群,无法删除")
|
|
db.delete(db_crowd)
|
|
db.commit()
|
|
return {"message": "删除成功"} |