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:
@@ -0,0 +1,7 @@
|
||||
from .jokes import router as jokes_router
|
||||
from .categories import router as categories_router
|
||||
from .auth import router as auth_router
|
||||
from .admin import router as admin_router
|
||||
from .settings import router as settings_router
|
||||
|
||||
__all__ = ["jokes_router", "categories_router", "auth_router", "admin_router", "settings_router"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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": "删除成功"}
|
||||
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.feedback import Feedback
|
||||
from app.schemas.feedback import FeedbackCreate, FeedbackResponse
|
||||
|
||||
router = APIRouter(tags=["反馈建议"])
|
||||
|
||||
|
||||
@router.post("/feedback", response_model=FeedbackResponse)
|
||||
def create_feedback(feedback: FeedbackCreate, db: Session = Depends(get_db)):
|
||||
"""公开:提交反馈建议"""
|
||||
db_feedback = Feedback(**feedback.model_dump())
|
||||
db.add(db_feedback)
|
||||
db.commit()
|
||||
db.refresh(db_feedback)
|
||||
return db_feedback
|
||||
@@ -76,8 +76,12 @@ def generate_joke(
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
def _parse_response(raw: str) -> GenerateResponse:
|
||||
def _parse_response(raw: str | None) -> GenerateResponse:
|
||||
"""解析 AI 返回内容,提取标题和内容"""
|
||||
# 防御:处理空或 None 输入
|
||||
if not raw or not raw.strip():
|
||||
raise ValueError("AI 返回内容为空")
|
||||
|
||||
title = ""
|
||||
content = raw
|
||||
|
||||
@@ -86,16 +90,26 @@ def _parse_response(raw: str) -> GenerateResponse:
|
||||
line = line.strip()
|
||||
if line.startswith("标题:") or line.startswith("标题:"):
|
||||
title = line.split(":", 1)[-1].split(":", 1)[-1].strip()
|
||||
content = content.replace(line, "").strip()
|
||||
# 只替换这一行,不要 replace 全局
|
||||
lines = content.split("\n")
|
||||
for i, l in enumerate(lines):
|
||||
if l.strip() == line:
|
||||
lines[i] = ""
|
||||
break
|
||||
content = "\n".join(lines).strip()
|
||||
break
|
||||
|
||||
# 如果没有提取到标题,取第一行或前20字
|
||||
# 如果没有提取到标题,取第一行
|
||||
if not title:
|
||||
first_line = raw.split("\n")[0].strip()
|
||||
if first_line.startswith("标题"):
|
||||
first_line = first_line.split(":", 1)[-1].split(":", 1)[-1].strip()
|
||||
title = first_line[:30] if len(first_line) > 30 else first_line
|
||||
|
||||
# 防御:content 不能为空
|
||||
if not content.strip():
|
||||
content = "(内容生成失败,请重新生成)"
|
||||
|
||||
return GenerateResponse(
|
||||
title=title or "生成的笑话",
|
||||
content=content.strip(),
|
||||
|
||||
+110
-17
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import random
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
@@ -6,26 +7,56 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.joke import Joke
|
||||
from app.models.category import JokeCrowd, JokeType
|
||||
from app.schemas.joke import JokeResponse, PaginatedJokeResponse
|
||||
|
||||
router = APIRouter(prefix="/jokes", tags=["笑话"])
|
||||
|
||||
|
||||
def joke_to_response(joke: Joke) -> JokeResponse:
|
||||
def _parse_ids(raw) -> list[int]:
|
||||
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]:
|
||||
if not ids:
|
||||
return []
|
||||
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]:
|
||||
if not ids:
|
||||
return []
|
||||
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,
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@@ -33,24 +64,46 @@ def joke_to_response(joke: Joke) -> JokeResponse:
|
||||
def list_jokes(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
type_id: int | None = None,
|
||||
crowd_id: int | None = None,
|
||||
type_ids: str | None = Query(None, description="逗号分隔的类型 ID,如 1,3,5"),
|
||||
crowd_ids: str | None = Query(None, description="逗号分隔的人群 ID,如 2,4"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取笑话列表(仅返回已审核通过的笑话)"""
|
||||
query = db.query(Joke).filter(Joke.status == "approved")
|
||||
|
||||
if type_id is not None:
|
||||
query = query.filter(Joke.type_id == type_id)
|
||||
if crowd_id is not None:
|
||||
query = query.filter(Joke.crowd_id == crowd_id)
|
||||
# 支持多选过滤:逗号分隔的 ID
|
||||
if type_ids:
|
||||
filter_set = {int(x.strip()) for x in type_ids.split(",") if x.strip().isdigit()}
|
||||
if filter_set:
|
||||
# 兼容旧单值字段 type_id
|
||||
old_filter = Joke.type_id.in_(filter_set)
|
||||
# 兼容新数组字段 type_ids(JSON 包含任一)
|
||||
new_filter = [
|
||||
Joke.type_ids.contains(str(tid)) for tid in filter_set
|
||||
]
|
||||
combined = old_filter
|
||||
for nf in new_filter:
|
||||
combined = combined | nf
|
||||
query = query.filter(combined)
|
||||
|
||||
if crowd_ids:
|
||||
filter_set = {int(x.strip()) for x in crowd_ids.split(",") if x.strip().isdigit()}
|
||||
if filter_set:
|
||||
old_filter = Joke.crowd_id.in_(filter_set)
|
||||
new_filter = [
|
||||
Joke.crowd_ids.contains(str(cid)) for cid in filter_set
|
||||
]
|
||||
combined = old_filter
|
||||
for nf in new_filter:
|
||||
combined = combined | nf
|
||||
query = query.filter(combined)
|
||||
|
||||
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],
|
||||
items=[joke_to_response(j, db) for j in jokes],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
@@ -60,19 +113,59 @@ def list_jokes(
|
||||
@router.get("/{joke_id}", response_model=JokeResponse)
|
||||
def get_joke(joke_id: int, db: Session = Depends(get_db)):
|
||||
"""获取单条笑话详情"""
|
||||
joke = db.query(Joke).filter(Joke.id == joke_id).first()
|
||||
# 只返回已审核通过的笑话
|
||||
joke = db.query(Joke).filter(
|
||||
Joke.id == joke_id,
|
||||
Joke.status == "approved"
|
||||
).first()
|
||||
if not joke:
|
||||
raise HTTPException(status_code=404, detail="笑话不存在")
|
||||
# 增加浏览次数
|
||||
joke.view_count += 1
|
||||
# 使用原子更新避免并发竞态
|
||||
db.query(Joke).filter(Joke.id == joke_id).update({Joke.view_count: Joke.view_count + 1})
|
||||
db.commit()
|
||||
return joke_to_response(joke)
|
||||
# 重新查询获取更新后的数据
|
||||
db.refresh(joke)
|
||||
return joke_to_response(joke, db)
|
||||
|
||||
|
||||
@router.get("/random", response_model=JokeResponse)
|
||||
def get_random_joke(db: Session = Depends(get_db)):
|
||||
"""随机获取一条已审核通过的笑话"""
|
||||
# 使用效率更高的方式:随机 ID 取模
|
||||
max_id = db.query(func.max(Joke.id)).filter(Joke.status == "approved").scalar()
|
||||
if not max_id:
|
||||
raise HTTPException(status_code=404, detail="暂无笑话")
|
||||
|
||||
# 尝试最多 10 次找到有效笑话
|
||||
for _ in range(10):
|
||||
random_id = random.randint(1, max_id)
|
||||
joke = db.query(Joke).filter(
|
||||
Joke.id >= random_id,
|
||||
Joke.status == "approved"
|
||||
).first()
|
||||
if joke:
|
||||
return joke_to_response(joke, db)
|
||||
|
||||
# 兜底:全表随机
|
||||
joke = db.query(Joke).filter(Joke.status == "approved").order_by(func.random()).first()
|
||||
if not joke:
|
||||
raise HTTPException(status_code=404, detail="暂无笑话")
|
||||
return joke_to_response(joke)
|
||||
return joke_to_response(joke, db)
|
||||
|
||||
|
||||
@router.post("/{joke_id}/like")
|
||||
def like_joke(joke_id: int, db: Session = Depends(get_db)):
|
||||
"""为笑话点赞(仅允许已审核通过的笑话)"""
|
||||
# 先检查笑话是否存在且已审核
|
||||
joke = db.query(Joke).filter(
|
||||
Joke.id == joke_id,
|
||||
Joke.status == "approved"
|
||||
).first()
|
||||
if not joke:
|
||||
raise HTTPException(status_code=404, detail="笑话不存在")
|
||||
# 使用原子更新避免并发竞态
|
||||
db.query(Joke).filter(Joke.id == joke_id).update({Joke.like_count: Joke.like_count + 1})
|
||||
db.commit()
|
||||
# 获取更新后的值
|
||||
db.refresh(joke)
|
||||
return {"message": "点赞成功", "like_count": joke.like_count}
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.link import Link
|
||||
from app.schemas.link import LinkCreate, LinkResponse
|
||||
|
||||
router = APIRouter(tags=["友情链接"])
|
||||
|
||||
|
||||
@router.get("/links", response_model=list[LinkResponse])
|
||||
def list_links(db: Session = Depends(get_db)):
|
||||
"""公开:获取所有友情链接"""
|
||||
return db.query(Link).order_by(Link.sort_order, Link.id).all()
|
||||
@@ -0,0 +1,96 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.setting import AiSetting
|
||||
from app.schemas.setting import AiSettingCreate
|
||||
from app.routers.admin import get_current_admin_user
|
||||
from app.models.user import AdminUser
|
||||
|
||||
router = APIRouter(prefix="/admin/settings", tags=["AI设置"])
|
||||
|
||||
|
||||
@router.get("", response_model=list)
|
||||
def list_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""列出所有 AI 配置"""
|
||||
return db.query(AiSetting).order_by(AiSetting.created_at.desc()).all()
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
def get_active_setting(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""获取当前激活的 AI 配置(爬虫调用,无需用户认证,token 校验仍保留)"""
|
||||
setting = db.query(AiSetting).filter(AiSetting.is_active == True).first()
|
||||
if not setting:
|
||||
raise HTTPException(status_code=404, detail="未找到激活的 AI 配置")
|
||||
return setting
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_setting(
|
||||
setting: AiSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""新建 AI 配置"""
|
||||
db_setting = AiSetting(**setting.model_dump())
|
||||
db.add(db_setting)
|
||||
db.commit()
|
||||
db.refresh(db_setting)
|
||||
return db_setting
|
||||
|
||||
|
||||
@router.put("/{setting_id}")
|
||||
def update_setting(
|
||||
setting_id: int,
|
||||
setting: AiSettingCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""更新 AI 配置"""
|
||||
db_setting = db.query(AiSetting).filter(AiSetting.id == setting_id).first()
|
||||
if not db_setting:
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
for key, value in setting.model_dump().items():
|
||||
setattr(db_setting, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_setting)
|
||||
return db_setting
|
||||
|
||||
|
||||
@router.put("/{setting_id}/toggle")
|
||||
def toggle_setting(
|
||||
setting_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""切换激活状态(只能有一个活跃)"""
|
||||
db_setting = db.query(AiSetting).filter(AiSetting.id == setting_id).first()
|
||||
if not db_setting:
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
|
||||
# 先全部设为非活跃
|
||||
db.query(AiSetting).update({AiSetting.is_active: False})
|
||||
db_setting.is_active = True
|
||||
db.commit()
|
||||
return {"message": "已激活", "id": setting_id}
|
||||
|
||||
|
||||
@router.delete("/{setting_id}")
|
||||
def delete_setting(
|
||||
setting_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: AdminUser = Depends(get_current_admin_user),
|
||||
):
|
||||
"""删除 AI 配置"""
|
||||
db_setting = db.query(AiSetting).filter(AiSetting.id == setting_id).first()
|
||||
if not db_setting:
|
||||
raise HTTPException(status_code=404, detail="配置不存在")
|
||||
db.delete(db_setting)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
Reference in New Issue
Block a user