feat: add user role system and public joke submission API

- 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>
This commit is contained in:
bwstudio
2026-06-07 12:48:35 +08:00
co-authored by Claude Opus 4.8
parent ceed63fcb0
commit aecf1f1f15
10 changed files with 681 additions and 21 deletions
+2 -1
View File
@@ -5,10 +5,11 @@ from app.database import Base
class AdminUser(Base):
"""管理员用户表模型"""
"""用户表 - 支持角色:admin(管理员), submitter(提交者)"""
__tablename__ = "admin_users"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String(50), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
role = Column(String(20), default="submitter") # admin=管理员, submitter=提交者
created_at = Column(DateTime, default=func.now())
+79 -1
View File
@@ -16,6 +16,8 @@ from app.schemas.category import JokeCrowdCreate, JokeCrowdResponse, JokeTypeCre
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=["管理后台"])
@@ -78,6 +80,8 @@ def joke_to_response(joke: Joke, db: Session = None) -> JokeResponse:
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,
@@ -96,15 +100,24 @@ 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()
@@ -159,6 +172,7 @@ def admin_update_joke(
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:
@@ -420,4 +434,68 @@ def admin_delete_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": "删除成功"}
+62
View File
@@ -0,0 +1,62 @@
"""
公开提交流程 - 供外部用户(submitter)使用。
无后台管理权限,仅能提交笑话到 pending 状态。
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.database import get_db
from app.models.joke import Joke
router = APIRouter(tags=["笑话提交"])
class JokeSubmit(BaseModel):
"""笑话提交"""
title: str
content: str
class JokeBatchSubmit(BaseModel):
"""批量提交"""
jokes: list[JokeSubmit]
@router.post("/submit", response_model=dict)
def submit_joke(data: JokeSubmit, db: Session = Depends(get_db)):
"""公开接口:提交单条笑话(自动进入待审核状态)"""
joke = Joke(
title=data.title,
content=data.content,
status="pending",
)
db.add(joke)
db.commit()
db.refresh(joke)
return {
"success": True,
"id": joke.id,
"message": f"已提交,ID: {joke.id}"
}
@router.post("/submit/batch", response_model=dict)
def submit_jokes_batch(data: JokeBatchSubmit, db: Session = Depends(get_db)):
"""公开接口:批量提交笑话"""
added = []
for item in data.jokes:
joke = Joke(
title=item.title,
content=item.content,
status="pending",
)
db.add(joke)
added.append(item.title)
db.commit()
return {
"success": True,
"count": len(added),
"message": f"已提交 {len(added)} 条笑话"
}
+26
View File
@@ -0,0 +1,26 @@
from datetime import datetime
from pydantic import BaseModel
class UserCreate(BaseModel):
"""创建用户"""
username: str
password: str
role: str = "submitter" # admin=管理员, submitter=提交者
class UserResponse(BaseModel):
"""用户信息"""
id: int
username: str
role: str
created_at: datetime
class Config:
from_attributes = True
class UserRegister(BaseModel):
"""公开注册(仅限提交者)"""
username: str
password: str