Initial commit: MyBlog full stack blog

This commit is contained in:
2026-08-22 22:28:41 +08:00
commit c61193f2af
64 changed files with 7290 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
"""
评论路由。
接口(统一响应格式 {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="获取成功")