127 lines
5.0 KiB
Python
127 lines
5.0 KiB
Python
"""
|
||||
|
|
评论路由。
|
|||
|
|
|
|||
|
|
接口(统一响应格式 {success, data, message}):
|
|||
|
|
- POST /api/comment/add 发表评论或回复(仅好友/博主;content 最长 2000 字;parent_id 指定回复对象)
|
|||
|
|
- GET /api/comment/list 评论列表(可见性与文章一致;按时间升序,返回 parent_id 供前端分组展示)
|
|||
|
|
|
|||
|
|
权限:
|
|||
|
|
- 游客不可评论;visitor 角色不可评论(需先成为好友)。
|
|||
|
|
- 好友文章仅好友/博主可查看评论。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|||
|
|
from sqlalchemy.orm import Session
|
|||
|
|
|
|||
|
|
from ..auth import get_current_user
|
|||
|
|
from ..database import get_db
|
|||
|
|
from ..models import (
|
|||
|
|
ROLE_BLOGGER,
|
|||
|
|
ROLE_FRIEND,
|
|||
|
|
VISIBILITY_FRIEND,
|
|||
|
|
Article,
|
|||
|
|
Comment,
|
|||
|
|
User,
|
|||
|
|
)
|
|||
|
|
from ..schemas import CommentCreate, UnifiedResponse
|
|||
|
|
from ..security import action_limiter, get_client_ip
|
|||
|
|
from .deps import can_read_friend_article, get_optional_user
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/comment", tags=["comment"])
|
|||
|
|
|
|||
|
|
# 评论内容长度限制(可通过 .env 的 COMMENT_MAX_LENGTH 调整,默认 2000)
|
|||
|
|
COMMENT_MAX_LENGTH = int(os.getenv("COMMENT_MAX_LENGTH") or "2000")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/add", response_model=UnifiedResponse)
|
|||
|
|
def add_comment(
|
|||
|
|
payload: CommentCreate,
|
|||
|
|
request: Request,
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
) -> UnifiedResponse:
|
|||
|
|
"""发表评论或回复:仅好友或博主可评论,回复时 parent_id 必须属于同一篇文章。"""
|
|||
|
|
# 写操作限流(IP 维度):防止好友账号刷屏
|
|||
|
|
if action_limiter.is_blocked(get_client_ip(request)):
|
|||
|
|
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="操作过于频繁,请稍后再试")
|
|||
|
|
if current_user.role not in (ROLE_FRIEND, ROLE_BLOGGER):
|
|||
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="只有好友或博主可以评论")
|
|||
|
|
article = db.query(Article).filter(Article.id == payload.article_id).first()
|
|||
|
|
if article is None:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文章不存在")
|
|||
|
|
|
|||
|
|
content = payload.content.strip()
|
|||
|
|
if not content:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="评论内容不能为空")
|
|||
|
|
if len(content) > COMMENT_MAX_LENGTH:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"评论内容不能超过 {COMMENT_MAX_LENGTH} 字")
|
|||
|
|
|
|||
|
|
# 回复校验:父评论必须存在且属于同一篇文章,且不允许回复自身(无自身场景,防脏数据)
|
|||
|
|
parent_id = payload.parent_id
|
|||
|
|
if parent_id is not None:
|
|||
|
|
parent = db.query(Comment).filter(Comment.id == parent_id).first()
|
|||
|
|
if parent is None:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复的评论不存在")
|
|||
|
|
if parent.article_id != article.id:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="回复的评论不属于该文章")
|
|||
|
|
|
|||
|
|
comment = Comment(
|
|||
|
|
article_id=payload.article_id,
|
|||
|
|
user_id=current_user.id,
|
|||
|
|
parent_id=parent_id,
|
|||
|
|
content=content,
|
|||
|
|
)
|
|||
|
|
db.add(comment)
|
|||
|
|
db.commit()
|
|||
|
|
db.refresh(comment)
|
|||
|
|
action_limiter.hit(get_client_ip(request))
|
|||
|
|
|
|||
|
|
data = {
|
|||
|
|
"id": comment.id,
|
|||
|
|
"article_id": comment.article_id,
|
|||
|
|
"user_id": comment.user_id,
|
|||
|
|
"parent_id": comment.parent_id,
|
|||
|
|
"username": current_user.username,
|
|||
|
|
"avatar": current_user.avatar,
|
|||
|
|
"content": comment.content,
|
|||
|
|
"created_time": comment.created_time,
|
|||
|
|
}
|
|||
|
|
return UnifiedResponse(success=True, data=data, message="评论成功")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/list", response_model=UnifiedResponse)
|
|||
|
|
def list_comments(
|
|||
|
|
article_id: int,
|
|||
|
|
current_user: Optional[User] = Depends(get_optional_user),
|
|||
|
|
db: Session = Depends(get_db),
|
|||
|
|
) -> UnifiedResponse:
|
|||
|
|
"""评论列表:公开文章所有人可见,好友文章仅好友/博主可见。"""
|
|||
|
|
article = db.query(Article).filter(Article.id == article_id).first()
|
|||
|
|
if article is None:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="文章不存在")
|
|||
|
|
if article.visibility == VISIBILITY_FRIEND and not can_read_friend_article(current_user):
|
|||
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="无权查看该文章评论")
|
|||
|
|
|
|||
|
|
comments = (
|
|||
|
|
db.query(Comment)
|
|||
|
|
.filter(Comment.article_id == article_id)
|
|||
|
|
.order_by(Comment.created_time.asc(), Comment.id.asc())
|
|||
|
|
.all()
|
|||
|
|
)
|
|||
|
|
data = [
|
|||
|
|
{
|
|||
|
|
"id": c.id,
|
|||
|
|
"article_id": c.article_id,
|
|||
|
|
"user_id": c.user_id,
|
|||
|
|
"parent_id": c.parent_id,
|
|||
|
|
"username": c.user.username if c.user else None,
|
|||
|
|
"avatar": c.user.avatar if c.user else None,
|
|||
|
|
"content": c.content,
|
|||
|
|
"created_time": c.created_time,
|
|||
|
|
}
|
|||
|
|
for c in comments
|
|||
|
|
]
|
|||
|
|
return UnifiedResponse(success=True, data=data, message="获取成功")
|