Files

127 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
评论路由。
接口(统一响应格式 {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="获取成功")