编写后端
This commit is contained in:
1
backend/app/api/endpoints/__init__.py
Normal file
1
backend/app/api/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Endpoints module
|
||||
BIN
backend/app/api/endpoints/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/endpoints/__pycache__/comments.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/comments.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/endpoints/__pycache__/posts.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/posts.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/endpoints/__pycache__/users.cpython-312.pyc
Normal file
BIN
backend/app/api/endpoints/__pycache__/users.cpython-312.pyc
Normal file
Binary file not shown.
109
backend/app/api/endpoints/auth.py
Normal file
109
backend/app/api/endpoints/auth.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
认证 API 接口
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from app.schemas.auth import LoginRequest, RegisterRequest, TokenResponse, RefreshTokenRequest
|
||||
from app.schemas.user import UserPublic
|
||||
from app.crud.user import user_crud
|
||||
from app.core.security import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
decode_token,
|
||||
)
|
||||
from app.core.logger import app_logger
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserPublic, status_code=status.HTTP_201_CREATED)
|
||||
async def register(request: RegisterRequest):
|
||||
"""用户注册"""
|
||||
# 检查用户名是否存在
|
||||
if await user_crud.get_by_username(request.username):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
# 检查邮箱是否存在
|
||||
if await user_crud.get_by_email(request.email):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="邮箱已被注册"
|
||||
)
|
||||
|
||||
# 创建用户
|
||||
user = await user_crud.create(
|
||||
username=request.username,
|
||||
email=request.email,
|
||||
password=request.password
|
||||
)
|
||||
|
||||
app_logger.info(f"New user registered: {user.username}")
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(login_data: LoginRequest):
|
||||
"""用户登录"""
|
||||
username_or_email = login_data.username
|
||||
password = login_data.password
|
||||
|
||||
# 验证用户
|
||||
user = await user_crud.authenticate(username_or_email, password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="用户已被禁用"
|
||||
)
|
||||
|
||||
# 生成令牌
|
||||
token_data = {"sub": str(user.id), "username": user.username}
|
||||
access_token = create_access_token(token_data)
|
||||
refresh_token = create_refresh_token(token_data)
|
||||
|
||||
app_logger.info(f"User logged in: {user.username}")
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse)
|
||||
async def refresh_token(request: RefreshTokenRequest):
|
||||
"""刷新令牌"""
|
||||
payload = decode_token(request.refresh_token)
|
||||
|
||||
if payload.get("type") != "refresh":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的刷新令牌"
|
||||
)
|
||||
|
||||
user_id = payload.get("sub")
|
||||
username = payload.get("username")
|
||||
|
||||
# 生成新令牌
|
||||
token_data = {"sub": user_id, "username": username}
|
||||
access_token = create_access_token(token_data)
|
||||
refresh_token = create_refresh_token(token_data)
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout():
|
||||
"""用户登出(前端清除令牌即可)"""
|
||||
return {"message": "登出成功"}
|
||||
140
backend/app/api/endpoints/comments.py
Normal file
140
backend/app/api/endpoints/comments.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
评论 API 接口
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, status, Depends, Query
|
||||
from app.schemas.comment import (
|
||||
CommentCreate,
|
||||
CommentUpdate,
|
||||
CommentResponse,
|
||||
CommentListResponse
|
||||
)
|
||||
from app.crud.comment import comment_crud
|
||||
from app.crud.post import post_crud
|
||||
from app.core.security import get_current_user_id
|
||||
from app.core.logger import app_logger
|
||||
|
||||
router = APIRouter(prefix="/comments", tags=["评论"])
|
||||
|
||||
|
||||
@router.get("/post/{post_id}", response_model=CommentListResponse)
|
||||
async def get_post_comments(
|
||||
post_id: str,
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
|
||||
approved_only: bool = Query(True, description="仅显示已审核评论")
|
||||
):
|
||||
"""获取文章的所有评论"""
|
||||
# 检查文章是否存在
|
||||
post = await post_crud.get_by_id(post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文章不存在"
|
||||
)
|
||||
|
||||
comments, total = await comment_crud.get_by_post(
|
||||
post_id=post_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
approved_only=approved_only
|
||||
)
|
||||
|
||||
return CommentListResponse(
|
||||
items=comments,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_comment(
|
||||
comment_data: CommentCreate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""创建评论"""
|
||||
# 检查文章是否存在
|
||||
post = await post_crud.get_by_id(comment_data.post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文章不存在"
|
||||
)
|
||||
|
||||
# 如果是回复,检查父评论是否存在
|
||||
if comment_data.parent_id:
|
||||
parent = await comment_crud.get_by_id(comment_data.parent_id)
|
||||
if not parent:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="父评论不存在"
|
||||
)
|
||||
if str(parent.post_id) != comment_data.post_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="父评论不属于该文章"
|
||||
)
|
||||
|
||||
comment = await comment_crud.create(
|
||||
content=comment_data.content,
|
||||
author_id=user_id,
|
||||
post_id=comment_data.post_id,
|
||||
parent_id=comment_data.parent_id
|
||||
)
|
||||
|
||||
app_logger.info(f"Comment created on post {comment_data.post_id} by user {user_id}")
|
||||
return comment
|
||||
|
||||
|
||||
@router.put("/{comment_id}", response_model=CommentResponse)
|
||||
async def update_comment(
|
||||
comment_id: str,
|
||||
comment_data: CommentUpdate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""更新评论"""
|
||||
comment = await comment_crud.get_by_id(comment_id)
|
||||
if not comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="评论不存在"
|
||||
)
|
||||
|
||||
# 检查权限(只能修改自己的评论)
|
||||
if str(comment.author_id) != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限修改此评论"
|
||||
)
|
||||
|
||||
updated_comment = await comment_crud.update(
|
||||
comment_id,
|
||||
**comment_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
|
||||
app_logger.info(f"Comment updated: {comment_id}")
|
||||
return updated_comment
|
||||
|
||||
|
||||
@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_comment(
|
||||
comment_id: str,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""删除评论"""
|
||||
comment = await comment_crud.get_by_id(comment_id)
|
||||
if not comment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="评论不存在"
|
||||
)
|
||||
|
||||
# 检查权限(只能删除自己的评论)
|
||||
if str(comment.author_id) != user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限删除此评论"
|
||||
)
|
||||
|
||||
await comment_crud.delete(comment_id)
|
||||
app_logger.info(f"Comment deleted: {comment_id}")
|
||||
288
backend/app/api/endpoints/posts.py
Normal file
288
backend/app/api/endpoints/posts.py
Normal file
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
文章 API 接口
|
||||
"""
|
||||
import math
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, HTTPException, status, Depends, Query
|
||||
from app.schemas.post import (
|
||||
PostCreate,
|
||||
PostUpdate,
|
||||
PostResponse,
|
||||
PostListResponse,
|
||||
TagCreate,
|
||||
TagResponse,
|
||||
CategoryCreate,
|
||||
CategoryResponse
|
||||
)
|
||||
from app.schemas.post import AuthorResponse
|
||||
from app.crud.post import post_crud
|
||||
from app.crud.category import category_crud
|
||||
from app.crud.tag import tag_crud
|
||||
from app.crud.user import user_crud
|
||||
from app.core.security import get_current_user_id
|
||||
from app.core.logger import app_logger
|
||||
|
||||
router = APIRouter(prefix="/posts", tags=["文章"])
|
||||
category_router = APIRouter(prefix="/categories", tags=["分类"])
|
||||
tag_router = APIRouter(prefix="/tags", tags=["标签"])
|
||||
|
||||
|
||||
# ==================== 文章接口 ====================
|
||||
|
||||
@router.get("", response_model=PostListResponse)
|
||||
async def get_posts(
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(10, ge=1, le=100, description="每页数量"),
|
||||
status: Optional[str] = Query("published", description="文章状态"),
|
||||
category_id: Optional[str] = Query(None, description="分类ID"),
|
||||
tag_id: Optional[str] = Query(None, description="标签ID")
|
||||
):
|
||||
"""获取文章列表"""
|
||||
posts, total = await post_crud.get_all(
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
status=status,
|
||||
category_id=category_id,
|
||||
tag_id=tag_id
|
||||
)
|
||||
|
||||
return PostListResponse(
|
||||
items=posts,
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total_pages=math.ceil(total / page_size) if total > 0 else 0
|
||||
)
|
||||
|
||||
|
||||
@router.get("/hot", response_model=list[PostResponse])
|
||||
async def get_hot_posts(limit: int = Query(10, ge=1, le=50)):
|
||||
"""获取热门文章"""
|
||||
posts = await post_crud.get_hot_posts(limit=limit)
|
||||
return posts
|
||||
|
||||
|
||||
@router.get("/recent", response_model=list[PostResponse])
|
||||
async def get_recent_posts(limit: int = Query(10, ge=1, le=50)):
|
||||
"""获取最新文章"""
|
||||
posts = await post_crud.get_recent_posts(limit=limit)
|
||||
return posts
|
||||
|
||||
|
||||
@router.get("/{post_id}", response_model=PostResponse)
|
||||
async def get_post(post_id: str):
|
||||
"""获取文章详情"""
|
||||
post = await post_crud.get_by_id(post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文章不存在"
|
||||
)
|
||||
|
||||
# 增加浏览量
|
||||
await post_crud.increment_view_count(post_id)
|
||||
|
||||
return post
|
||||
|
||||
|
||||
@router.post("", response_model=PostResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_post(
|
||||
post_data: PostCreate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""创建文章"""
|
||||
# 检查 slug 是否已存在
|
||||
if await post_crud.get_by_slug(post_data.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="URL别名已存在"
|
||||
)
|
||||
|
||||
post = await post_crud.create(
|
||||
author_id=user_id,
|
||||
**post_data.model_dump()
|
||||
)
|
||||
|
||||
app_logger.info(f"Post created: {post.title} by user {user_id}")
|
||||
return post
|
||||
|
||||
|
||||
@router.put("/{post_id}", response_model=PostResponse)
|
||||
async def update_post(
|
||||
post_id: str,
|
||||
post_data: PostUpdate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""更新文章"""
|
||||
post = await post_crud.get_by_id(post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文章不存在"
|
||||
)
|
||||
|
||||
# 检查权限
|
||||
if str(post.author_id) != user_id:
|
||||
is_admin = await user_crud.is_superuser(user_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限修改此文章"
|
||||
)
|
||||
|
||||
updated_post = await post_crud.update(
|
||||
post_id,
|
||||
**post_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
|
||||
app_logger.info(f"Post updated: {updated_post.title}")
|
||||
return updated_post
|
||||
|
||||
|
||||
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_post(
|
||||
post_id: str,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""删除文章"""
|
||||
post = await post_crud.get_by_id(post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="文章不存在"
|
||||
)
|
||||
|
||||
# 检查权限
|
||||
if str(post.author_id) != user_id:
|
||||
is_admin = await user_crud.is_superuser(user_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="无权限删除此文章"
|
||||
)
|
||||
|
||||
await post_crud.delete(post_id)
|
||||
app_logger.info(f"Post deleted: {post_id}")
|
||||
|
||||
|
||||
# ==================== 分类接口 ====================
|
||||
|
||||
@category_router.get("", response_model=list[CategoryResponse])
|
||||
async def get_categories():
|
||||
"""获取所有分类"""
|
||||
return await category_crud.get_all()
|
||||
|
||||
|
||||
@category_router.get("/{category_id}", response_model=CategoryResponse)
|
||||
async def get_category(category_id: str):
|
||||
"""获取分类详情"""
|
||||
category = await category_crud.get_by_id(category_id)
|
||||
if not category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="分类不存在"
|
||||
)
|
||||
return category
|
||||
|
||||
|
||||
@category_router.post("", response_model=CategoryResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_category(
|
||||
category_data: CategoryCreate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""创建分类"""
|
||||
# 检查权限
|
||||
is_admin = await user_crud.is_superuser(user_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
# 检查 slug 是否已存在
|
||||
if await category_crud.get_by_slug(category_data.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="URL别名已存在"
|
||||
)
|
||||
|
||||
category = await category_crud.create(**category_data.model_dump())
|
||||
app_logger.info(f"Category created: {category.name}")
|
||||
return category
|
||||
|
||||
|
||||
@category_router.put("/{category_id}", response_model=CategoryResponse)
|
||||
async def update_category(
|
||||
category_id: str,
|
||||
category_data: CategoryCreate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""更新分类"""
|
||||
is_admin = await user_crud.is_superuser(user_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
category = await category_crud.update(category_id, **category_data.model_dump())
|
||||
if not category:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="分类不存在"
|
||||
)
|
||||
return category
|
||||
|
||||
|
||||
@category_router.delete("/{category_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_category(
|
||||
category_id: str,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""删除分类"""
|
||||
is_admin = await user_crud.is_superuser(user_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="需要管理员权限"
|
||||
)
|
||||
|
||||
await category_crud.delete(category_id)
|
||||
|
||||
|
||||
# ==================== 标签接口 ====================
|
||||
|
||||
@tag_router.get("", response_model=list[TagResponse])
|
||||
async def get_tags():
|
||||
"""获取所有标签"""
|
||||
return await tag_crud.get_all()
|
||||
|
||||
|
||||
@tag_router.get("/{tag_id}", response_model=TagResponse)
|
||||
async def get_tag(tag_id: str):
|
||||
"""获取标签详情"""
|
||||
tag = await tag_crud.get_by_id(tag_id)
|
||||
if not tag:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="标签不存在"
|
||||
)
|
||||
return tag
|
||||
|
||||
|
||||
@tag_router.post("", response_model=TagResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_tag(
|
||||
tag_data: TagCreate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""创建标签"""
|
||||
# 检查 slug 是否已存在
|
||||
if await tag_crud.get_by_slug(tag_data.slug):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="URL别名已存在"
|
||||
)
|
||||
|
||||
tag = await tag_crud.create(**tag_data.model_dump())
|
||||
app_logger.info(f"Tag created: {tag.name}")
|
||||
return tag
|
||||
50
backend/app/api/endpoints/users.py
Normal file
50
backend/app/api/endpoints/users.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
用户 API 接口
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
from app.schemas.user import UserPublic, UserUpdate
|
||||
from app.crud.user import user_crud
|
||||
from app.core.security import get_current_user_id
|
||||
from app.core.logger import app_logger
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["用户"])
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserPublic)
|
||||
async def get_current_user(user_id: str = Depends(get_current_user_id)):
|
||||
"""获取当前用户信息"""
|
||||
user = await user_crud.get_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserPublic)
|
||||
async def update_current_user(
|
||||
user_update: UserUpdate,
|
||||
user_id: str = Depends(get_current_user_id)
|
||||
):
|
||||
"""更新当前用户信息"""
|
||||
user = await user_crud.update(user_id, **user_update.model_dump(exclude_unset=True))
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
app_logger.info(f"User updated: {user.username}")
|
||||
return user
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserPublic)
|
||||
async def get_user(user_id: str):
|
||||
"""获取指定用户信息"""
|
||||
user = await user_crud.get_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="用户不存在"
|
||||
)
|
||||
return user
|
||||
Reference in New Issue
Block a user