编写后端
This commit is contained in:
1
backend/app/__init__.py
Normal file
1
backend/app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# App module
|
||||
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API module
|
||||
BIN
backend/app/api/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/api.cpython-312.pyc
Normal file
BIN
backend/app/api/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
15
backend/app/api/api.py
Normal file
15
backend/app/api/api.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
API 路由汇总
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, users, posts, comments
|
||||
|
||||
api_router = APIRouter(prefix="/api/v1")
|
||||
|
||||
# 注册各模块路由
|
||||
api_router.include_router(auth.router)
|
||||
api_router.include_router(users.router)
|
||||
api_router.include_router(posts.router)
|
||||
api_router.include_router(posts.category_router)
|
||||
api_router.include_router(posts.tag_router)
|
||||
api_router.include_router(comments.router)
|
||||
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
|
||||
1
backend/app/core/__init__.py
Normal file
1
backend/app/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core module
|
||||
BIN
backend/app/core/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/database.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/database.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/logger.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Normal file
BIN
backend/app/core/__pycache__/security.cpython-312.pyc
Normal file
Binary file not shown.
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
应用配置模块
|
||||
使用 Pydantic Settings 管理环境变量
|
||||
"""
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置类"""
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME: str = "ACG Blog"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = True
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST: str = "localhost"
|
||||
DB_PORT: int = 5432
|
||||
DB_USER: str = "postgres"
|
||||
DB_PASSWORD: str = "postgres"
|
||||
DB_NAME: str = "acg_blog"
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST: str = "localhost"
|
||||
REDIS_PORT: int = 6379
|
||||
REDIS_DB: int = 0
|
||||
|
||||
# JWT 配置
|
||||
SECRET_KEY: str = "your-secret-key-change-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
|
||||
|
||||
# CORS 配置
|
||||
BACKEND_CORS_ORIGINS: list[str] = [
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000",
|
||||
]
|
||||
|
||||
@property
|
||||
def DATABASE_URL(self) -> str:
|
||||
"""生成数据库连接 URL"""
|
||||
return f"postgres://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
|
||||
@property
|
||||
def DATABASE_URL_ASYNC(self) -> str:
|
||||
"""生成异步数据库连接 URL"""
|
||||
return f"asyncpg://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""生成 Redis 连接 URL"""
|
||||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""获取配置单例"""
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
39
backend/app/core/database.py
Normal file
39
backend/app/core/database.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
数据库模块
|
||||
Tortoise-ORM 初始化配置
|
||||
"""
|
||||
from tortoise import Tortoise
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logger import app_logger
|
||||
|
||||
|
||||
async def init_db():
|
||||
"""初始化数据库连接"""
|
||||
app_logger.info("Initializing database connection...")
|
||||
|
||||
await Tortoise.init(
|
||||
db_url=settings.DATABASE_URL_ASYNC,
|
||||
modules={
|
||||
"models": [
|
||||
"app.models.user",
|
||||
"app.models.post",
|
||||
"app.models.category",
|
||||
"app.models.tag",
|
||||
"app.models.comment",
|
||||
]
|
||||
},
|
||||
use_tz=False, # 使用 UTC 时间
|
||||
timezone="Asia/Shanghai", # 使用上海时区
|
||||
)
|
||||
|
||||
# 生成 schema
|
||||
await Tortoise.generate_schemas()
|
||||
app_logger.info("Database connection established")
|
||||
|
||||
|
||||
async def close_db():
|
||||
"""关闭数据库连接"""
|
||||
app_logger.info("Closing database connection...")
|
||||
await Tortoise.close_connections()
|
||||
app_logger.info("Database connection closed")
|
||||
42
backend/app/core/logger.py
Normal file
42
backend/app/core/logger.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
日志配置模块
|
||||
使用 Loguru 实现异步日志处理
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR = Path(__file__).parent.parent.parent / "logs"
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""配置日志格式和输出"""
|
||||
# 移除默认处理器
|
||||
logger.remove()
|
||||
|
||||
# 控制台输出格式
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level="INFO",
|
||||
colorize=True,
|
||||
)
|
||||
|
||||
# 文件输出格式
|
||||
logger.add(
|
||||
LOG_DIR / "acg_blog_{time:YYYY-MM-DD}.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
level="DEBUG",
|
||||
rotation="00:00", # 每天零点轮换
|
||||
retention="30 days", # 保留30天
|
||||
compression="zip", # 压缩旧日志
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# 初始化日志
|
||||
app_logger = setup_logger()
|
||||
82
backend/app/core/security.py
Normal file
82
backend/app/core/security.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
安全模块
|
||||
包含 JWT 令牌生成、验证和密码哈希功能
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
# 密码上下文(使用 bcrypt)
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# OAuth2 方案
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""哈希密码"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire, "type": "access"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建刷新令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||
|
||||
to_encode.update({"exp": expire, "type": "refresh"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_token(token: str) -> dict:
|
||||
"""解码令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
async def get_current_user_id(token: str = Depends(oauth2_scheme)) -> str:
|
||||
"""从令牌中获取当前用户 ID"""
|
||||
payload = decode_token(token)
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user_id
|
||||
1
backend/app/crud/__init__.py
Normal file
1
backend/app/crud/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# CRUD module
|
||||
BIN
backend/app/crud/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/crud/__pycache__/category.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/category.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/crud/__pycache__/comment.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/comment.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/crud/__pycache__/post.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/post.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/crud/__pycache__/tag.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/tag.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/crud/__pycache__/user.cpython-312.pyc
Normal file
BIN
backend/app/crud/__pycache__/user.cpython-312.pyc
Normal file
Binary file not shown.
72
backend/app/crud/category.py
Normal file
72
backend/app/crud/category.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
分类 CRUD 操作
|
||||
"""
|
||||
from typing import Optional, List
|
||||
from app.models.category import Category
|
||||
|
||||
|
||||
class CategoryCRUD:
|
||||
"""分类 CRUD 操作类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(category_id: str) -> Optional[Category]:
|
||||
"""根据 ID 获取分类"""
|
||||
return await Category.filter(id=category_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_slug(slug: str) -> Optional[Category]:
|
||||
"""根据 slug 获取分类"""
|
||||
return await Category.filter(slug=slug).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> List[Category]:
|
||||
"""获取所有分类"""
|
||||
return await Category.all()
|
||||
|
||||
@staticmethod
|
||||
async def create(name: str, slug: str, description: Optional[str] = None) -> Category:
|
||||
"""创建分类"""
|
||||
category = await Category.create(
|
||||
name=name,
|
||||
slug=slug,
|
||||
description=description
|
||||
)
|
||||
return category
|
||||
|
||||
@staticmethod
|
||||
async def update(category_id: str, **kwargs) -> Optional[Category]:
|
||||
"""更新分类"""
|
||||
category = await Category.filter(id=category_id).first()
|
||||
if category:
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(category, key):
|
||||
setattr(category, key, value)
|
||||
await category.save()
|
||||
return category
|
||||
|
||||
@staticmethod
|
||||
async def delete(category_id: str) -> bool:
|
||||
"""删除分类"""
|
||||
category = await Category.filter(id=category_id).first()
|
||||
if category:
|
||||
await category.delete()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_with_post_count() -> List[dict]:
|
||||
"""获取分类及其文章数量"""
|
||||
categories = await Category.all().prefetch_related("posts")
|
||||
result = []
|
||||
for cat in categories:
|
||||
result.append({
|
||||
"id": str(cat.id),
|
||||
"name": cat.name,
|
||||
"slug": cat.slug,
|
||||
"description": cat.description,
|
||||
"post_count": len(cat.posts) if cat.posts else 0
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
category_crud = CategoryCRUD()
|
||||
91
backend/app/crud/comment.py
Normal file
91
backend/app/crud/comment.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
评论 CRUD 操作
|
||||
"""
|
||||
from typing import Optional, List, Tuple
|
||||
from app.models.comment import Comment
|
||||
from app.models.post import Post
|
||||
|
||||
|
||||
class CommentCRUD:
|
||||
"""评论 CRUD 操作类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(comment_id: str) -> Optional[Comment]:
|
||||
"""根据 ID 获取评论"""
|
||||
return await Comment.filter(id=comment_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_post(
|
||||
post_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
approved_only: bool = True
|
||||
) -> Tuple[List[Comment], int]:
|
||||
"""获取文章的所有评论(树形结构)"""
|
||||
query = Comment.filter(post_id=post_id)
|
||||
|
||||
if approved_only:
|
||||
query = query.filter(is_approved=True)
|
||||
|
||||
total = await query.count()
|
||||
comments = await query \
|
||||
.prefetch_related("author", "replies__author") \
|
||||
.filter(parent_id=None) \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size) \
|
||||
.order_by("created_at")
|
||||
|
||||
return comments, total
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
content: str,
|
||||
author_id: str,
|
||||
post_id: str,
|
||||
parent_id: Optional[str] = None,
|
||||
is_approved: bool = True
|
||||
) -> Comment:
|
||||
"""创建评论"""
|
||||
comment = await Comment.create(
|
||||
content=content,
|
||||
author_id=author_id,
|
||||
post_id=post_id,
|
||||
parent_id=parent_id,
|
||||
is_approved=is_approved
|
||||
)
|
||||
|
||||
# 更新文章的评论数
|
||||
await Post.filter(id=post_id).update(comment_count=Post.comment_count + 1)
|
||||
|
||||
return comment
|
||||
|
||||
@staticmethod
|
||||
async def update(comment_id: str, **kwargs) -> Optional[Comment]:
|
||||
"""更新评论"""
|
||||
comment = await Comment.filter(id=comment_id).first()
|
||||
if comment:
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(comment, key):
|
||||
setattr(comment, key, value)
|
||||
await comment.save()
|
||||
return comment
|
||||
|
||||
@staticmethod
|
||||
async def delete(comment_id: str) -> bool:
|
||||
"""删除评论"""
|
||||
comment = await Comment.filter(id=comment_id).first()
|
||||
if comment:
|
||||
post_id = comment.post_id
|
||||
await comment.delete()
|
||||
# 更新文章的评论数
|
||||
await Post.filter(id=post_id).update(comment_count=Post.comment_count - 1)
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def approve(comment_id: str) -> Optional[Comment]:
|
||||
"""审核通过评论"""
|
||||
return await CommentCRUD.update(comment_id, is_approved=True)
|
||||
|
||||
|
||||
comment_crud = CommentCRUD()
|
||||
163
backend/app/crud/post.py
Normal file
163
backend/app/crud/post.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
文章 CRUD 操作
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Tuple
|
||||
from app.models.post import Post, PostTag
|
||||
from app.models.user import User
|
||||
from app.models.category import Category
|
||||
from app.models.tag import Tag
|
||||
|
||||
|
||||
class PostCRUD:
|
||||
"""文章 CRUD 操作类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(post_id: str) -> Optional[Post]:
|
||||
"""根据 ID 获取文章"""
|
||||
return await Post.filter(id=post_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_slug(slug: str) -> Optional[Post]:
|
||||
"""根据 slug 获取文章"""
|
||||
return await Post.filter(slug=slug).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_all(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
status: str = "published",
|
||||
category_id: Optional[str] = None,
|
||||
tag_id: Optional[str] = None
|
||||
) -> Tuple[List[Post], int]:
|
||||
"""获取文章列表(分页)"""
|
||||
query = Post.all()
|
||||
|
||||
if status:
|
||||
query = query.filter(status=status)
|
||||
|
||||
if category_id:
|
||||
query = query.filter(category_id=category_id)
|
||||
|
||||
if tag_id:
|
||||
query = query.filter(tags__id=tag_id)
|
||||
|
||||
total = await query.count()
|
||||
posts = await query \
|
||||
.prefetch_related("author", "category", "tags") \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size) \
|
||||
.order_by("-created_at")
|
||||
|
||||
return posts, total
|
||||
|
||||
@staticmethod
|
||||
async def get_by_author(
|
||||
author_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10
|
||||
) -> Tuple[List[Post], int]:
|
||||
"""获取指定作者的文章列表"""
|
||||
query = Post.filter(author_id=author_id)
|
||||
total = await query.count()
|
||||
posts = await query \
|
||||
.prefetch_related("author", "category", "tags") \
|
||||
.offset((page - 1) * page_size) \
|
||||
.limit(page_size) \
|
||||
.order_by("-created_at")
|
||||
return posts, total
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
title: str,
|
||||
slug: str,
|
||||
content: str,
|
||||
author_id: str,
|
||||
summary: Optional[str] = None,
|
||||
cover_image: Optional[str] = None,
|
||||
category_id: Optional[str] = None,
|
||||
tag_ids: Optional[List[str]] = None,
|
||||
status: str = "draft",
|
||||
meta_title: Optional[str] = None,
|
||||
meta_description: Optional[str] = None
|
||||
) -> Post:
|
||||
"""创建文章"""
|
||||
post = await Post.create(
|
||||
title=title,
|
||||
slug=slug,
|
||||
content=content,
|
||||
author_id=author_id,
|
||||
summary=summary,
|
||||
cover_image=cover_image,
|
||||
category_id=category_id,
|
||||
status=status,
|
||||
meta_title=meta_title,
|
||||
meta_description=meta_description
|
||||
)
|
||||
|
||||
# 添加标签
|
||||
if tag_ids:
|
||||
for tag_id in tag_ids:
|
||||
await PostTag.create(post_id=post.id, tag_id=tag_id)
|
||||
|
||||
return post
|
||||
|
||||
@staticmethod
|
||||
async def update(post_id: str, **kwargs) -> Optional[Post]:
|
||||
"""更新文章"""
|
||||
post = await Post.filter(id=post_id).first()
|
||||
if not post:
|
||||
return None
|
||||
|
||||
# 处理标签更新
|
||||
if "tag_ids" in kwargs:
|
||||
tag_ids = kwargs.pop("tag_ids")
|
||||
# 删除旧标签关联
|
||||
await PostTag.filter(post_id=post_id).delete()
|
||||
# 添加新标签关联
|
||||
for tag_id in tag_ids:
|
||||
await PostTag.create(post_id=post_id, tag_id=tag_id)
|
||||
|
||||
# 处理发布状态更新
|
||||
if kwargs.get("status") == "published" and not post.published_at:
|
||||
kwargs["published_at"] = datetime.utcnow()
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(post, key):
|
||||
setattr(post, key, value)
|
||||
|
||||
await post.save()
|
||||
return post
|
||||
|
||||
@staticmethod
|
||||
async def delete(post_id: str) -> bool:
|
||||
"""删除文章"""
|
||||
post = await Post.filter(id=post_id).first()
|
||||
if post:
|
||||
await post.delete()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def increment_view_count(post_id: str) -> None:
|
||||
"""增加浏览量"""
|
||||
await Post.filter(id=post_id).update(view_count=Post.view_count + 1)
|
||||
|
||||
@staticmethod
|
||||
async def get_hot_posts(limit: int = 10) -> List[Post]:
|
||||
"""获取热门文章(按浏览量排序)"""
|
||||
return await Post.filter(status="published") \
|
||||
.prefetch_related("author", "category") \
|
||||
.order_by("-view_count") \
|
||||
.limit(limit)
|
||||
|
||||
@staticmethod
|
||||
async def get_recent_posts(limit: int = 10) -> List[Post]:
|
||||
"""获取最新文章"""
|
||||
return await Post.filter(status="published") \
|
||||
.prefetch_related("author", "category", "tags") \
|
||||
.order_by("-created_at") \
|
||||
.limit(limit)
|
||||
|
||||
|
||||
post_crud = PostCRUD()
|
||||
66
backend/app/crud/tag.py
Normal file
66
backend/app/crud/tag.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
标签 CRUD 操作
|
||||
"""
|
||||
from typing import Optional, List
|
||||
from app.models.tag import Tag
|
||||
|
||||
|
||||
class TagCRUD:
|
||||
"""标签 CRUD 操作类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(tag_id: str) -> Optional[Tag]:
|
||||
"""根据 ID 获取标签"""
|
||||
return await Tag.filter(id=tag_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_slug(slug: str) -> Optional[Tag]:
|
||||
"""根据 slug 获取标签"""
|
||||
return await Tag.filter(slug=slug).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_name(name: str) -> Optional[Tag]:
|
||||
"""根据名称获取标签"""
|
||||
return await Tag.filter(name=name).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> List[Tag]:
|
||||
"""获取所有标签"""
|
||||
return await Tag.all()
|
||||
|
||||
@staticmethod
|
||||
async def create(name: str, slug: str) -> Tag:
|
||||
"""创建标签"""
|
||||
tag = await Tag.create(name=name, slug=slug)
|
||||
return tag
|
||||
|
||||
@staticmethod
|
||||
async def update(tag_id: str, **kwargs) -> Optional[Tag]:
|
||||
"""更新标签"""
|
||||
tag = await Tag.filter(id=tag_id).first()
|
||||
if tag:
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(tag, key):
|
||||
setattr(tag, key, value)
|
||||
await tag.save()
|
||||
return tag
|
||||
|
||||
@staticmethod
|
||||
async def delete(tag_id: str) -> bool:
|
||||
"""删除标签"""
|
||||
tag = await Tag.filter(id=tag_id).first()
|
||||
if tag:
|
||||
await tag.delete()
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_or_create(name: str, slug: str) -> Tag:
|
||||
"""获取或创建标签"""
|
||||
tag = await TagCRUD.get_by_slug(slug)
|
||||
if tag:
|
||||
return tag
|
||||
return await TagCRUD.create(name, slug)
|
||||
|
||||
|
||||
tag_crud = TagCRUD()
|
||||
75
backend/app/crud/user.py
Normal file
75
backend/app/crud/user.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
用户 CRUD 操作
|
||||
"""
|
||||
from typing import Optional
|
||||
from app.models.user import User
|
||||
from app.core.security import get_password_hash, verify_password
|
||||
|
||||
|
||||
class UserCRUD:
|
||||
"""用户 CRUD 操作类"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(user_id: str) -> Optional[User]:
|
||||
"""根据 ID 获取用户"""
|
||||
return await User.filter(id=user_id).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_username(username: str) -> Optional[User]:
|
||||
"""根据用户名获取用户"""
|
||||
return await User.filter(username=username).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_email(email: str) -> Optional[User]:
|
||||
"""根据邮箱获取用户"""
|
||||
return await User.filter(email=email).first()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_username_or_email(username_or_email: str) -> Optional[User]:
|
||||
"""根据用户名或邮箱获取用户"""
|
||||
return await User.filter(
|
||||
username=username_or_email
|
||||
).first() or await User.filter(
|
||||
email=username_or_email
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
async def create(username: str, email: str, password: str) -> User:
|
||||
"""创建用户"""
|
||||
password_hash = get_password_hash(password)
|
||||
user = await User.create(
|
||||
username=username,
|
||||
email=email,
|
||||
password_hash=password_hash
|
||||
)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def update(user_id: str, **kwargs) -> Optional[User]:
|
||||
"""更新用户"""
|
||||
user = await User.filter(id=user_id).first()
|
||||
if user:
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(user, key):
|
||||
setattr(user, key, value)
|
||||
await user.save()
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def authenticate(username_or_email: str, password: str) -> Optional[User]:
|
||||
"""验证用户登录"""
|
||||
user = await UserCRUD.get_by_username_or_email(username_or_email)
|
||||
if not user:
|
||||
return None
|
||||
if not verify_password(password, user.password_hash):
|
||||
return None
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
async def is_superuser(user_id: str) -> bool:
|
||||
"""检查用户是否为超级用户"""
|
||||
user = await User.filter(id=user_id).first()
|
||||
return user.is_superuser if user else False
|
||||
|
||||
|
||||
user_crud = UserCRUD()
|
||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models module
|
||||
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/category.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/category.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/comment.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/comment.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/post.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/post.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/tag.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/tag.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Normal file
BIN
backend/app/models/__pycache__/user.cpython-312.pyc
Normal file
Binary file not shown.
22
backend/app/models/category.py
Normal file
22
backend/app/models/category.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
分类模型
|
||||
"""
|
||||
import uuid
|
||||
from tortoise import fields, models
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
"""分类模型"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
name = fields.CharField(max_length=50, unique=True, description="分类名称")
|
||||
slug = fields.CharField(max_length=50, unique=True, description="URL别名")
|
||||
description = fields.TextField(null=True, description="分类描述")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
|
||||
class Meta:
|
||||
table = "categories"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
49
backend/app/models/comment.py
Normal file
49
backend/app/models/comment.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
评论模型
|
||||
"""
|
||||
import uuid
|
||||
from tortoise import fields, models
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
"""评论模型"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
content = fields.TextField(description="评论内容")
|
||||
is_approved = fields.BooleanField(default=True, description="是否审核通过")
|
||||
|
||||
# 关联用户(评论者)
|
||||
author = fields.ForeignKeyField(
|
||||
"models.User",
|
||||
related_name="comments",
|
||||
on_delete=fields.CASCADE,
|
||||
description="评论者"
|
||||
)
|
||||
|
||||
# 关联文章
|
||||
post = fields.ForeignKeyField(
|
||||
"models.Post",
|
||||
related_name="comments",
|
||||
on_delete=fields.CASCADE,
|
||||
description="所属文章"
|
||||
)
|
||||
|
||||
# 自关联(回复)
|
||||
parent = fields.ForeignKeyField(
|
||||
"models.Comment",
|
||||
related_name="replies",
|
||||
on_delete=fields.CASCADE,
|
||||
null=True,
|
||||
description="父评论"
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
|
||||
|
||||
class Meta:
|
||||
table = "comments"
|
||||
ordering = ["created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"Comment by {self.author.username} on {self.post.title}"
|
||||
94
backend/app/models/post.py
Normal file
94
backend/app/models/post.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""
|
||||
文章模型
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from tortoise import fields, models
|
||||
|
||||
|
||||
class Post(models.Model):
|
||||
"""文章模型"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
title = fields.CharField(max_length=200, description="文章标题")
|
||||
slug = fields.CharField(max_length=200, unique=True, description="URL别名")
|
||||
content = fields.TextField(description="文章内容(Markdown)")
|
||||
summary = fields.TextField(null=True, description="文章摘要")
|
||||
cover_image = fields.CharField(max_length=500, null=True, description="封面图片URL")
|
||||
|
||||
# 关联用户(作者)
|
||||
author = fields.ForeignKeyField(
|
||||
"models.User",
|
||||
related_name="posts",
|
||||
on_delete=fields.CASCADE,
|
||||
description="作者"
|
||||
)
|
||||
|
||||
# 关联分类
|
||||
category = fields.ForeignKeyField(
|
||||
"models.Category",
|
||||
related_name="posts",
|
||||
on_delete=fields.SET_NULL,
|
||||
null=True,
|
||||
description="分类"
|
||||
)
|
||||
|
||||
# 标签(多对多)
|
||||
tags = fields.ManyToManyField(
|
||||
"models.Tag",
|
||||
related_name="posts",
|
||||
through="post_tags",
|
||||
description="标签"
|
||||
)
|
||||
|
||||
# 统计数据
|
||||
view_count = fields.IntField(default=0, description="浏览量")
|
||||
like_count = fields.IntField(default=0, description="点赞数")
|
||||
comment_count = fields.IntField(default=0, description="评论数")
|
||||
|
||||
# 状态:draft(草稿), published(已发布), archived(已归档)
|
||||
status = fields.CharField(
|
||||
max_length=20,
|
||||
default="draft",
|
||||
description="文章状态"
|
||||
)
|
||||
|
||||
# SEO
|
||||
meta_title = fields.CharField(max_length=200, null=True, description="SEO标题")
|
||||
meta_description = fields.TextField(null=True, description="SEO描述")
|
||||
|
||||
# 时间戳
|
||||
published_at = fields.DatetimeField(null=True, description="发布时间")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
|
||||
|
||||
class Meta:
|
||||
table = "posts"
|
||||
ordering = ["-created_at"]
|
||||
indexes = [
|
||||
("status", "published_at"),
|
||||
("author", "status"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class PostTag(models.Model):
|
||||
"""文章-标签关联表"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
post = fields.ForeignKeyField(
|
||||
"models.Post",
|
||||
on_delete=fields.CASCADE,
|
||||
description="文章"
|
||||
)
|
||||
tag = fields.ForeignKeyField(
|
||||
"models.Tag",
|
||||
on_delete=fields.CASCADE,
|
||||
description="标签"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
table = "post_tags"
|
||||
unique_together = (("post", "tag"),)
|
||||
21
backend/app/models/tag.py
Normal file
21
backend/app/models/tag.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
标签模型
|
||||
"""
|
||||
import uuid
|
||||
from tortoise import fields, models
|
||||
|
||||
|
||||
class Tag(models.Model):
|
||||
"""标签模型"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
name = fields.CharField(max_length=50, unique=True, description="标签名称")
|
||||
slug = fields.CharField(max_length=50, unique=True, description="URL别名")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
|
||||
class Meta:
|
||||
table = "tags"
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
28
backend/app/models/user.py
Normal file
28
backend/app/models/user.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
用户模型
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from tortoise import fields, models
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
"""用户模型"""
|
||||
|
||||
id = fields.UUIDField(pk=True, default=uuid.uuid4)
|
||||
username = fields.CharField(max_length=50, unique=True, description="用户名")
|
||||
email = fields.CharField(max_length=255, unique=True, description="邮箱")
|
||||
password_hash = fields.CharField(max_length=255, description="密码哈希")
|
||||
avatar = fields.CharField(max_length=500, null=True, description="头像URL")
|
||||
bio = fields.TextField(null=True, description="个人简介")
|
||||
is_active = fields.BooleanField(default=True, description="是否激活")
|
||||
is_superuser = fields.BooleanField(default=False, description="是否超级用户")
|
||||
created_at = fields.DatetimeField(auto_now_add=True, description="创建时间")
|
||||
updated_at = fields.DatetimeField(auto_now=True, description="更新时间")
|
||||
|
||||
class Meta:
|
||||
table = "users"
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
1
backend/app/schemas/__init__.py
Normal file
1
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Schemas module
|
||||
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/comment.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/comment.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/post.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/post.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/user.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/user.cpython-312.pyc
Normal file
Binary file not shown.
29
backend/app/schemas/auth.py
Normal file
29
backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
认证 Schema
|
||||
"""
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""登录请求 Schema"""
|
||||
username: str = Field(..., description="用户名或邮箱")
|
||||
password: str = Field(..., description="密码")
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
"""注册请求 Schema"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
email: EmailStr = Field(..., description="邮箱")
|
||||
password: str = Field(..., min_length=6, max_length=100, description="密码")
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""令牌响应 Schema"""
|
||||
access_token: str = Field(..., description="访问令牌")
|
||||
refresh_token: str = Field(..., description="刷新令牌")
|
||||
token_type: str = Field(default="bearer", description="令牌类型")
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
"""刷新令牌请求 Schema"""
|
||||
refresh_token: str = Field(..., description="刷新令牌")
|
||||
55
backend/app/schemas/comment.py
Normal file
55
backend/app/schemas/comment.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
评论 Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CommentBase(BaseModel):
|
||||
"""评论基础 Schema"""
|
||||
content: str = Field(..., min_length=1, description="评论内容")
|
||||
|
||||
|
||||
class CommentCreate(CommentBase):
|
||||
"""评论创建 Schema"""
|
||||
post_id: str = Field(..., description="文章ID")
|
||||
parent_id: Optional[str] = Field(None, description="父评论ID")
|
||||
|
||||
|
||||
class CommentUpdate(BaseModel):
|
||||
"""评论更新 Schema"""
|
||||
content: Optional[str] = Field(None, min_length=1)
|
||||
|
||||
|
||||
class CommentAuthor(BaseModel):
|
||||
"""评论作者 Schema"""
|
||||
id: str
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CommentResponse(CommentBase):
|
||||
"""评论响应 Schema"""
|
||||
id: str
|
||||
author: CommentAuthor
|
||||
post_id: str
|
||||
parent_id: Optional[str] = None
|
||||
is_approved: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
replies: List["CommentResponse"] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CommentListResponse(BaseModel):
|
||||
"""评论列表响应 Schema"""
|
||||
items: List[CommentResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
115
backend/app/schemas/post.py
Normal file
115
backend/app/schemas/post.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
文章 Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class TagBase(BaseModel):
|
||||
"""标签基础 Schema"""
|
||||
name: str = Field(..., min_length=1, max_length=50, description="标签名")
|
||||
slug: str = Field(..., min_length=1, max_length=50, description="URL别名")
|
||||
|
||||
|
||||
class TagCreate(TagBase):
|
||||
"""标签创建 Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class TagResponse(TagBase):
|
||||
"""标签响应 Schema"""
|
||||
id: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CategoryBase(BaseModel):
|
||||
"""分类基础 Schema"""
|
||||
name: str = Field(..., min_length=1, max_length=50, description="分类名")
|
||||
slug: str = Field(..., min_length=1, max_length=50, description="URL别名")
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class CategoryCreate(CategoryBase):
|
||||
"""分类创建 Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class CategoryResponse(CategoryBase):
|
||||
"""分类响应 Schema"""
|
||||
id: str
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PostBase(BaseModel):
|
||||
"""文章基础 Schema"""
|
||||
title: str = Field(..., min_length=1, max_length=200, description="标题")
|
||||
slug: str = Field(..., min_length=1, max_length=200, description="URL别名")
|
||||
content: str = Field(..., description="文章内容")
|
||||
summary: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
category_id: Optional[str] = None
|
||||
status: str = Field(default="draft", description="状态: draft/published/archived")
|
||||
|
||||
|
||||
class PostCreate(PostBase):
|
||||
"""文章创建 Schema"""
|
||||
tag_ids: Optional[List[str]] = []
|
||||
meta_title: Optional[str] = None
|
||||
meta_description: Optional[str] = None
|
||||
|
||||
|
||||
class PostUpdate(BaseModel):
|
||||
"""文章更新 Schema"""
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
slug: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
content: Optional[str] = None
|
||||
summary: Optional[str] = None
|
||||
cover_image: Optional[str] = None
|
||||
category_id: Optional[str] = None
|
||||
tag_ids: Optional[List[str]] = None
|
||||
status: Optional[str] = None
|
||||
meta_title: Optional[str] = None
|
||||
meta_description: Optional[str] = None
|
||||
|
||||
|
||||
class AuthorResponse(BaseModel):
|
||||
"""作者响应 Schema"""
|
||||
id: str
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PostResponse(PostBase):
|
||||
"""文章响应 Schema"""
|
||||
id: str
|
||||
author: AuthorResponse
|
||||
category: Optional[CategoryResponse] = None
|
||||
tags: List[TagResponse] = []
|
||||
view_count: int
|
||||
like_count: int
|
||||
comment_count: int
|
||||
published_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PostListResponse(BaseModel):
|
||||
"""文章列表响应 Schema"""
|
||||
items: List[PostResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
total_pages: int
|
||||
50
backend/app/schemas/user.py
Normal file
50
backend/app/schemas/user.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
用户 Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""用户基础 Schema"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="用户名")
|
||||
email: EmailStr = Field(..., description="邮箱")
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""用户创建 Schema"""
|
||||
password: str = Field(..., min_length=6, max_length=100, description="密码")
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
"""用户更新 Schema"""
|
||||
username: Optional[str] = Field(None, min_length=3, max_length=50)
|
||||
email: Optional[EmailStr] = None
|
||||
avatar: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
|
||||
|
||||
class UserInDB(UserBase):
|
||||
"""用户数据库 Schema"""
|
||||
id: str
|
||||
avatar: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
is_active: bool
|
||||
is_superuser: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UserPublic(UserBase):
|
||||
"""用户公开信息 Schema"""
|
||||
id: str
|
||||
avatar: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services module
|
||||
Reference in New Issue
Block a user