初始提交:极码 GeekCode 全栈项目(FastAPI + Vue3)
This commit is contained in:
440
backend/routers/posts.py
Normal file
440
backend/routers/posts.py
Normal file
@@ -0,0 +1,440 @@
|
||||
"""经验知识库路由"""
|
||||
import json
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from database import get_db
|
||||
from models.user import User
|
||||
from models.post import Post
|
||||
from models.comment import Comment
|
||||
from models.like import Like, Collect
|
||||
from models.follow import Follow
|
||||
from models.notification import Notification
|
||||
from models.attachment import Attachment
|
||||
from schemas.post import (
|
||||
PostCreate, PostUpdate, PostResponse, PostListResponse,
|
||||
CommentCreate, CommentResponse,
|
||||
)
|
||||
from routers.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def _extract_cover_image(content: str) -> str:
|
||||
"""从Markdown内容中提取第一张图片作为封面"""
|
||||
if not content:
|
||||
return ""
|
||||
match = re.search(r'!\[.*?\]\((.*?)\)', content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
img_match = re.search(r'<img[^>]+src=["\']([^"\']+)["\']', content)
|
||||
if img_match:
|
||||
return img_match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def _enrich_post_with_author(post: Post, db: Session) -> dict:
|
||||
"""为帖子附加作者信息(用于信息流)"""
|
||||
author = db.query(User).filter(User.id == post.user_id).first()
|
||||
return {
|
||||
"id": post.id, "title": post.title, "content": post.content[:200],
|
||||
"category": post.category, "tags": post.tags,
|
||||
"cover_image": _extract_cover_image(post.content),
|
||||
"view_count": post.view_count, "like_count": post.like_count,
|
||||
"comment_count": post.comment_count, "collect_count": post.collect_count,
|
||||
"created_at": post.created_at, "updated_at": post.updated_at,
|
||||
"author": {
|
||||
"id": post.user_id,
|
||||
"username": author.username if author else "未知",
|
||||
"avatar": author.avatar if author else "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/feed")
|
||||
def get_feed(
|
||||
page: int = 1, page_size: int = 20,
|
||||
category: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""关注的人的帖子流"""
|
||||
following_ids = [f.following_id for f in db.query(Follow.following_id).filter(Follow.follower_id == current_user.id).all()]
|
||||
if not following_ids:
|
||||
return {"items": [], "total": 0, "page": page, "page_size": page_size}
|
||||
query = db.query(Post).filter(Post.user_id.in_(following_ids), Post.is_public == True, Post.is_draft == False)
|
||||
if category:
|
||||
query = query.filter(Post.category == category)
|
||||
total = query.count()
|
||||
posts = (
|
||||
query.order_by(Post.created_at.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size).all()
|
||||
)
|
||||
return {"items": [_enrich_post_with_author(p, db) for p in posts], "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/hot")
|
||||
def get_hot_posts(
|
||||
page: int = 1, page_size: int = 20,
|
||||
category: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""热门帖子(按热度排序)"""
|
||||
query = db.query(Post).filter(Post.is_public == True, Post.is_draft == False)
|
||||
if category:
|
||||
query = query.filter(Post.category == category)
|
||||
total = query.count()
|
||||
posts = (
|
||||
query.order_by((Post.like_count * 3 + Post.comment_count * 2 + Post.view_count).desc())
|
||||
.offset((page - 1) * page_size).limit(page_size).all()
|
||||
)
|
||||
return {"items": [_enrich_post_with_author(p, db) for p in posts], "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/latest")
|
||||
def get_latest_posts(
|
||||
page: int = 1, page_size: int = 20,
|
||||
category: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""最新帖子"""
|
||||
query = db.query(Post).filter(Post.is_public == True, Post.is_draft == False)
|
||||
if category:
|
||||
query = query.filter(Post.category == category)
|
||||
total = query.count()
|
||||
posts = (
|
||||
query.order_by(Post.created_at.desc())
|
||||
.offset((page - 1) * page_size).limit(page_size).all()
|
||||
)
|
||||
return {"items": [_enrich_post_with_author(p, db) for p in posts], "total": total, "page": page, "page_size": page_size}
|
||||
|
||||
|
||||
@router.get("/drafts")
|
||||
def get_drafts(
|
||||
page: int = 1, page_size: int = 20,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的草稿列表"""
|
||||
query = db.query(Post).filter(Post.user_id == current_user.id, Post.is_draft == True)
|
||||
total = query.count()
|
||||
posts = query.order_by(Post.updated_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
return {
|
||||
"items": [_enrich_post(p, db, current_user.id) for p in posts],
|
||||
"total": total, "page": page, "page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
def _enrich_post(post: Post, db: Session, current_user_id: int = None) -> PostResponse:
|
||||
"""填充帖子额外字段"""
|
||||
author = db.query(User).filter(User.id == post.user_id).first()
|
||||
result = PostResponse.model_validate(post)
|
||||
result.author_name = author.username if author else "未知用户"
|
||||
if current_user_id:
|
||||
result.is_liked = db.query(Like).filter(
|
||||
Like.post_id == post.id, Like.user_id == current_user_id
|
||||
).first() is not None
|
||||
result.is_collected = db.query(Collect).filter(
|
||||
Collect.post_id == post.id, Collect.user_id == current_user_id
|
||||
).first() is not None
|
||||
return result
|
||||
|
||||
|
||||
@router.get("", response_model=PostListResponse)
|
||||
def get_posts(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
category: Optional[str] = None,
|
||||
tag: Optional[str] = None,
|
||||
user_id: Optional[int] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取帖子列表"""
|
||||
query = db.query(Post).filter(
|
||||
or_(Post.is_public == True, Post.user_id == current_user.id),
|
||||
Post.is_draft == False,
|
||||
)
|
||||
if category:
|
||||
query = query.filter(Post.category == category)
|
||||
if tag:
|
||||
query = query.filter(Post.tags.contains(tag))
|
||||
if user_id:
|
||||
query = query.filter(Post.user_id == user_id)
|
||||
|
||||
total = query.count()
|
||||
posts = query.order_by(Post.created_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return PostListResponse(
|
||||
items=[_enrich_post(p, db, current_user.id) for p in posts],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=PostResponse)
|
||||
def create_post(
|
||||
data: PostCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发布经验帖"""
|
||||
post = Post(
|
||||
user_id=current_user.id,
|
||||
title=data.title,
|
||||
content=data.content,
|
||||
category=data.category,
|
||||
tags=json.dumps(data.tags, ensure_ascii=False),
|
||||
is_public=data.is_public,
|
||||
is_draft=data.is_draft,
|
||||
)
|
||||
db.add(post)
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
return _enrich_post(post, db, current_user.id)
|
||||
|
||||
|
||||
@router.get("/{post_id}", response_model=PostResponse)
|
||||
def get_post(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取帖子详情"""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||||
if not post.is_public and post.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权访问")
|
||||
|
||||
# 增加浏览量
|
||||
post.view_count += 1
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
|
||||
return _enrich_post(post, db, current_user.id)
|
||||
|
||||
|
||||
@router.put("/{post_id}", response_model=PostResponse)
|
||||
def update_post(
|
||||
post_id: int,
|
||||
data: PostUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""编辑帖子"""
|
||||
# 管理员可以编辑任意帖子,普通用户只能编辑自己的
|
||||
if current_user.is_admin:
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
else:
|
||||
post = db.query(Post).filter(Post.id == post_id, Post.user_id == current_user.id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在或无权编辑")
|
||||
|
||||
if data.title is not None:
|
||||
post.title = data.title
|
||||
if data.content is not None:
|
||||
post.content = data.content
|
||||
if data.category is not None:
|
||||
post.category = data.category
|
||||
if data.tags is not None:
|
||||
post.tags = json.dumps(data.tags, ensure_ascii=False)
|
||||
if data.is_public is not None:
|
||||
post.is_public = data.is_public
|
||||
if data.is_draft is not None:
|
||||
post.is_draft = data.is_draft
|
||||
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
return _enrich_post(post, db, current_user.id)
|
||||
|
||||
|
||||
@router.delete("/{post_id}")
|
||||
def delete_post(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除帖子"""
|
||||
post = db.query(Post).filter(Post.id == post_id, Post.user_id == current_user.id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在或无权删除")
|
||||
|
||||
db.query(Comment).filter(Comment.post_id == post_id).delete()
|
||||
db.query(Like).filter(Like.post_id == post_id).delete()
|
||||
db.query(Collect).filter(Collect.post_id == post_id).delete()
|
||||
db.query(Attachment).filter(Attachment.post_id == post_id).delete()
|
||||
db.delete(post)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.post("/{post_id}/like")
|
||||
def toggle_like(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""点赞/取消点赞"""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||||
|
||||
existing = db.query(Like).filter(
|
||||
Like.post_id == post_id, Like.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
db.delete(existing)
|
||||
post.like_count = max(0, post.like_count - 1)
|
||||
db.commit()
|
||||
return {"liked": False, "like_count": post.like_count}
|
||||
else:
|
||||
db.add(Like(post_id=post_id, user_id=current_user.id))
|
||||
post.like_count += 1
|
||||
# 通知帖子作者
|
||||
if post.user_id != current_user.id:
|
||||
db.add(Notification(
|
||||
user_id=post.user_id, type="like",
|
||||
content=f"{current_user.username} 赞了你的文章「{post.title[:30]}」",
|
||||
from_user_id=current_user.id, related_id=post_id,
|
||||
))
|
||||
db.commit()
|
||||
return {"liked": True, "like_count": post.like_count}
|
||||
|
||||
|
||||
@router.post("/{post_id}/collect")
|
||||
def toggle_collect(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""收藏/取消收藏"""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||||
|
||||
existing = db.query(Collect).filter(
|
||||
Collect.post_id == post_id, Collect.user_id == current_user.id
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
db.delete(existing)
|
||||
post.collect_count = max(0, post.collect_count - 1)
|
||||
db.commit()
|
||||
return {"collected": False, "collect_count": post.collect_count}
|
||||
else:
|
||||
db.add(Collect(post_id=post_id, user_id=current_user.id))
|
||||
post.collect_count += 1
|
||||
db.commit()
|
||||
return {"collected": True, "collect_count": post.collect_count}
|
||||
|
||||
|
||||
@router.get("/{post_id}/comments", response_model=List[CommentResponse])
|
||||
def get_comments(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取评论列表"""
|
||||
comments = (
|
||||
db.query(Comment)
|
||||
.filter(Comment.post_id == post_id)
|
||||
.order_by(Comment.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
results = []
|
||||
for c in comments:
|
||||
author = db.query(User).filter(User.id == c.user_id).first()
|
||||
r = CommentResponse.model_validate(c)
|
||||
r.author_name = author.username if author else "未知用户"
|
||||
results.append(r)
|
||||
return results
|
||||
|
||||
|
||||
@router.post("/{post_id}/comments", response_model=CommentResponse)
|
||||
def create_comment(
|
||||
post_id: int,
|
||||
data: CommentCreate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""发表评论"""
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="帖子不存在")
|
||||
|
||||
comment = Comment(
|
||||
post_id=post_id,
|
||||
user_id=current_user.id,
|
||||
content=data.content,
|
||||
)
|
||||
db.add(comment)
|
||||
post.comment_count += 1
|
||||
# 通知帖子作者
|
||||
if post.user_id != current_user.id:
|
||||
db.add(Notification(
|
||||
user_id=post.user_id, type="comment",
|
||||
content=f"{current_user.username} 评论了你的文章「{post.title[:30]}」",
|
||||
from_user_id=current_user.id, related_id=post_id,
|
||||
))
|
||||
db.commit()
|
||||
db.refresh(comment)
|
||||
|
||||
result = CommentResponse.model_validate(comment)
|
||||
result.author_name = current_user.username
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{post_id}/attachments")
|
||||
def get_attachments(
|
||||
post_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取帖子的附件列表"""
|
||||
attachments = (
|
||||
db.query(Attachment)
|
||||
.filter(Attachment.post_id == post_id)
|
||||
.order_by(Attachment.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": a.id,
|
||||
"filename": a.filename,
|
||||
"url": a.url,
|
||||
"file_size": a.file_size,
|
||||
"file_type": a.file_type,
|
||||
"created_at": a.created_at,
|
||||
}
|
||||
for a in attachments
|
||||
]
|
||||
|
||||
|
||||
@router.delete("/{post_id}/attachments/{attachment_id}")
|
||||
def delete_attachment(
|
||||
post_id: int,
|
||||
attachment_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除附件(仅作者)"""
|
||||
attachment = db.query(Attachment).filter(
|
||||
Attachment.id == attachment_id,
|
||||
Attachment.post_id == post_id,
|
||||
Attachment.user_id == current_user.id,
|
||||
).first()
|
||||
if not attachment:
|
||||
raise HTTPException(status_code=404, detail="附件不存在或无权删除")
|
||||
db.delete(attachment)
|
||||
db.commit()
|
||||
return {"message": "删除成功"}
|
||||
Reference in New Issue
Block a user