- Add AdminUser role field (admin/submitter) - User management in admin panel - Public joke submission API (no auth required) - Document submission methods for external workers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
501 lines
16 KiB
Python
501 lines
16 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,
|
|
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
|
|
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": "删除成功"}
|
|
|
|
|
|
# ============================================================
|
|
# 用户管理
|
|
# ============================================================
|
|
|
|
@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": "删除成功"} |