diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..9d8928c
--- /dev/null
+++ b/backend/app/__init__.py
@@ -0,0 +1 @@
+# App module
diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..fc45964
Binary files /dev/null and b/backend/app/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
new file mode 100644
index 0000000..9c7f58e
--- /dev/null
+++ b/backend/app/api/__init__.py
@@ -0,0 +1 @@
+# API module
diff --git a/backend/app/api/__pycache__/__init__.cpython-312.pyc b/backend/app/api/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..282357f
Binary files /dev/null and b/backend/app/api/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/api/__pycache__/api.cpython-312.pyc b/backend/app/api/__pycache__/api.cpython-312.pyc
new file mode 100644
index 0000000..bbb1133
Binary files /dev/null and b/backend/app/api/__pycache__/api.cpython-312.pyc differ
diff --git a/backend/app/api/api.py b/backend/app/api/api.py
new file mode 100644
index 0000000..6252348
--- /dev/null
+++ b/backend/app/api/api.py
@@ -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)
diff --git a/backend/app/api/endpoints/__init__.py b/backend/app/api/endpoints/__init__.py
new file mode 100644
index 0000000..ab8cf4e
--- /dev/null
+++ b/backend/app/api/endpoints/__init__.py
@@ -0,0 +1 @@
+# Endpoints module
diff --git a/backend/app/api/endpoints/__pycache__/__init__.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..9924643
Binary files /dev/null and b/backend/app/api/endpoints/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc
new file mode 100644
index 0000000..553affb
Binary files /dev/null and b/backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc differ
diff --git a/backend/app/api/endpoints/__pycache__/comments.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/comments.cpython-312.pyc
new file mode 100644
index 0000000..e036b18
Binary files /dev/null and b/backend/app/api/endpoints/__pycache__/comments.cpython-312.pyc differ
diff --git a/backend/app/api/endpoints/__pycache__/posts.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/posts.cpython-312.pyc
new file mode 100644
index 0000000..318a68c
Binary files /dev/null and b/backend/app/api/endpoints/__pycache__/posts.cpython-312.pyc differ
diff --git a/backend/app/api/endpoints/__pycache__/users.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/users.cpython-312.pyc
new file mode 100644
index 0000000..9d1e4fa
Binary files /dev/null and b/backend/app/api/endpoints/__pycache__/users.cpython-312.pyc differ
diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py
new file mode 100644
index 0000000..18621f1
--- /dev/null
+++ b/backend/app/api/endpoints/auth.py
@@ -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": "登出成功"}
diff --git a/backend/app/api/endpoints/comments.py b/backend/app/api/endpoints/comments.py
new file mode 100644
index 0000000..6aa03e2
--- /dev/null
+++ b/backend/app/api/endpoints/comments.py
@@ -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}")
diff --git a/backend/app/api/endpoints/posts.py b/backend/app/api/endpoints/posts.py
new file mode 100644
index 0000000..d182447
--- /dev/null
+++ b/backend/app/api/endpoints/posts.py
@@ -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
diff --git a/backend/app/api/endpoints/users.py b/backend/app/api/endpoints/users.py
new file mode 100644
index 0000000..9e0d284
--- /dev/null
+++ b/backend/app/api/endpoints/users.py
@@ -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
diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py
new file mode 100644
index 0000000..3e83c63
--- /dev/null
+++ b/backend/app/core/__init__.py
@@ -0,0 +1 @@
+# Core module
diff --git a/backend/app/core/__pycache__/__init__.cpython-312.pyc b/backend/app/core/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..4e0132b
Binary files /dev/null and b/backend/app/core/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc
new file mode 100644
index 0000000..45b44d9
Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-312.pyc differ
diff --git a/backend/app/core/__pycache__/database.cpython-312.pyc b/backend/app/core/__pycache__/database.cpython-312.pyc
new file mode 100644
index 0000000..539a79a
Binary files /dev/null and b/backend/app/core/__pycache__/database.cpython-312.pyc differ
diff --git a/backend/app/core/__pycache__/logger.cpython-312.pyc b/backend/app/core/__pycache__/logger.cpython-312.pyc
new file mode 100644
index 0000000..6212234
Binary files /dev/null and b/backend/app/core/__pycache__/logger.cpython-312.pyc differ
diff --git a/backend/app/core/__pycache__/security.cpython-312.pyc b/backend/app/core/__pycache__/security.cpython-312.pyc
new file mode 100644
index 0000000..0adb690
Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-312.pyc differ
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index e69de29..b5ff201 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -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()
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
new file mode 100644
index 0000000..aac05cc
--- /dev/null
+++ b/backend/app/core/database.py
@@ -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")
diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py
new file mode 100644
index 0000000..91e024c
--- /dev/null
+++ b/backend/app/core/logger.py
@@ -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="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
+ 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()
diff --git a/backend/app/core/security.py b/backend/app/core/security.py
new file mode 100644
index 0000000..1dc6596
--- /dev/null
+++ b/backend/app/core/security.py
@@ -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
diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py
new file mode 100644
index 0000000..6fea1c8
--- /dev/null
+++ b/backend/app/crud/__init__.py
@@ -0,0 +1 @@
+# CRUD module
diff --git a/backend/app/crud/__pycache__/__init__.cpython-312.pyc b/backend/app/crud/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..79186f9
Binary files /dev/null and b/backend/app/crud/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/crud/__pycache__/category.cpython-312.pyc b/backend/app/crud/__pycache__/category.cpython-312.pyc
new file mode 100644
index 0000000..ba8cc07
Binary files /dev/null and b/backend/app/crud/__pycache__/category.cpython-312.pyc differ
diff --git a/backend/app/crud/__pycache__/comment.cpython-312.pyc b/backend/app/crud/__pycache__/comment.cpython-312.pyc
new file mode 100644
index 0000000..7661459
Binary files /dev/null and b/backend/app/crud/__pycache__/comment.cpython-312.pyc differ
diff --git a/backend/app/crud/__pycache__/post.cpython-312.pyc b/backend/app/crud/__pycache__/post.cpython-312.pyc
new file mode 100644
index 0000000..33e33ef
Binary files /dev/null and b/backend/app/crud/__pycache__/post.cpython-312.pyc differ
diff --git a/backend/app/crud/__pycache__/tag.cpython-312.pyc b/backend/app/crud/__pycache__/tag.cpython-312.pyc
new file mode 100644
index 0000000..3afbb15
Binary files /dev/null and b/backend/app/crud/__pycache__/tag.cpython-312.pyc differ
diff --git a/backend/app/crud/__pycache__/user.cpython-312.pyc b/backend/app/crud/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..ad533ca
Binary files /dev/null and b/backend/app/crud/__pycache__/user.cpython-312.pyc differ
diff --git a/backend/app/crud/category.py b/backend/app/crud/category.py
new file mode 100644
index 0000000..f7d50c6
--- /dev/null
+++ b/backend/app/crud/category.py
@@ -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()
diff --git a/backend/app/crud/comment.py b/backend/app/crud/comment.py
new file mode 100644
index 0000000..1a1d56a
--- /dev/null
+++ b/backend/app/crud/comment.py
@@ -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()
diff --git a/backend/app/crud/post.py b/backend/app/crud/post.py
new file mode 100644
index 0000000..fbde7e1
--- /dev/null
+++ b/backend/app/crud/post.py
@@ -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()
diff --git a/backend/app/crud/tag.py b/backend/app/crud/tag.py
new file mode 100644
index 0000000..ae49786
--- /dev/null
+++ b/backend/app/crud/tag.py
@@ -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()
diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py
new file mode 100644
index 0000000..bcfeb55
--- /dev/null
+++ b/backend/app/crud/user.py
@@ -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()
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
new file mode 100644
index 0000000..e2313c5
--- /dev/null
+++ b/backend/app/models/__init__.py
@@ -0,0 +1 @@
+# Models module
diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..a5a60ca
Binary files /dev/null and b/backend/app/models/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/models/__pycache__/category.cpython-312.pyc b/backend/app/models/__pycache__/category.cpython-312.pyc
new file mode 100644
index 0000000..9582933
Binary files /dev/null and b/backend/app/models/__pycache__/category.cpython-312.pyc differ
diff --git a/backend/app/models/__pycache__/comment.cpython-312.pyc b/backend/app/models/__pycache__/comment.cpython-312.pyc
new file mode 100644
index 0000000..df0babd
Binary files /dev/null and b/backend/app/models/__pycache__/comment.cpython-312.pyc differ
diff --git a/backend/app/models/__pycache__/post.cpython-312.pyc b/backend/app/models/__pycache__/post.cpython-312.pyc
new file mode 100644
index 0000000..f436491
Binary files /dev/null and b/backend/app/models/__pycache__/post.cpython-312.pyc differ
diff --git a/backend/app/models/__pycache__/tag.cpython-312.pyc b/backend/app/models/__pycache__/tag.cpython-312.pyc
new file mode 100644
index 0000000..308e115
Binary files /dev/null and b/backend/app/models/__pycache__/tag.cpython-312.pyc differ
diff --git a/backend/app/models/__pycache__/user.cpython-312.pyc b/backend/app/models/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..d99bef4
Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-312.pyc differ
diff --git a/backend/app/models/category.py b/backend/app/models/category.py
new file mode 100644
index 0000000..6fa6bc5
--- /dev/null
+++ b/backend/app/models/category.py
@@ -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
diff --git a/backend/app/models/comment.py b/backend/app/models/comment.py
new file mode 100644
index 0000000..908c959
--- /dev/null
+++ b/backend/app/models/comment.py
@@ -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}"
diff --git a/backend/app/models/post.py b/backend/app/models/post.py
new file mode 100644
index 0000000..5860500
--- /dev/null
+++ b/backend/app/models/post.py
@@ -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"),)
diff --git a/backend/app/models/tag.py b/backend/app/models/tag.py
new file mode 100644
index 0000000..40ef96d
--- /dev/null
+++ b/backend/app/models/tag.py
@@ -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
diff --git a/backend/app/models/user.py b/backend/app/models/user.py
new file mode 100644
index 0000000..4f0ef59
--- /dev/null
+++ b/backend/app/models/user.py
@@ -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
diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py
new file mode 100644
index 0000000..fff1fac
--- /dev/null
+++ b/backend/app/schemas/__init__.py
@@ -0,0 +1 @@
+# Schemas module
diff --git a/backend/app/schemas/__pycache__/__init__.cpython-312.pyc b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..27a4d27
Binary files /dev/null and b/backend/app/schemas/__pycache__/__init__.cpython-312.pyc differ
diff --git a/backend/app/schemas/__pycache__/auth.cpython-312.pyc b/backend/app/schemas/__pycache__/auth.cpython-312.pyc
new file mode 100644
index 0000000..124b465
Binary files /dev/null and b/backend/app/schemas/__pycache__/auth.cpython-312.pyc differ
diff --git a/backend/app/schemas/__pycache__/comment.cpython-312.pyc b/backend/app/schemas/__pycache__/comment.cpython-312.pyc
new file mode 100644
index 0000000..95fe3d5
Binary files /dev/null and b/backend/app/schemas/__pycache__/comment.cpython-312.pyc differ
diff --git a/backend/app/schemas/__pycache__/post.cpython-312.pyc b/backend/app/schemas/__pycache__/post.cpython-312.pyc
new file mode 100644
index 0000000..de5d2aa
Binary files /dev/null and b/backend/app/schemas/__pycache__/post.cpython-312.pyc differ
diff --git a/backend/app/schemas/__pycache__/user.cpython-312.pyc b/backend/app/schemas/__pycache__/user.cpython-312.pyc
new file mode 100644
index 0000000..467ec8c
Binary files /dev/null and b/backend/app/schemas/__pycache__/user.cpython-312.pyc differ
diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py
new file mode 100644
index 0000000..04d58bd
--- /dev/null
+++ b/backend/app/schemas/auth.py
@@ -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="刷新令牌")
diff --git a/backend/app/schemas/comment.py b/backend/app/schemas/comment.py
new file mode 100644
index 0000000..a5ae3fd
--- /dev/null
+++ b/backend/app/schemas/comment.py
@@ -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
diff --git a/backend/app/schemas/post.py b/backend/app/schemas/post.py
new file mode 100644
index 0000000..6887747
--- /dev/null
+++ b/backend/app/schemas/post.py
@@ -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
diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py
new file mode 100644
index 0000000..24a70c5
--- /dev/null
+++ b/backend/app/schemas/user.py
@@ -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
diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py
new file mode 100644
index 0000000..0557eb6
--- /dev/null
+++ b/backend/app/services/__init__.py
@@ -0,0 +1 @@
+# Services module
diff --git a/backend/logs/acg_blog_2026-03-28.log b/backend/logs/acg_blog_2026-03-28.log
new file mode 100644
index 0000000..e69de29
diff --git a/backend/main.py b/backend/main.py
index 9149eef..be9972a 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -1,6 +1,72 @@
+"""
+FastAPI 应用入口
+"""
+from contextlib import asynccontextmanager
from fastapi import FastAPI
-from pydantic import BaseModel
-app = FastAPI()
-@app.get("/")
-def read_root():
- return {"Hello": "World"}
\ No newline at end of file
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.core.config import settings
+from app.core.database import init_db, close_db
+from app.core.logger import app_logger
+from app.api.api import api_router
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ """应用生命周期管理"""
+ # 启动时
+ app_logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}...")
+
+ # 初始化数据库
+ await init_db()
+
+ yield
+
+ # 关闭时
+ app_logger.info("Shutting down...")
+ await close_db()
+
+
+def create_app() -> FastAPI:
+ """创建 FastAPI 应用"""
+ app = FastAPI(
+ title=settings.APP_NAME,
+ version=settings.APP_VERSION,
+ description="ACG 风格博客系统 API",
+ docs_url="/docs",
+ redoc_url="/redoc",
+ lifespan=lifespan
+ )
+
+ # 配置 CORS
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.BACKEND_CORS_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ # 注册路由
+ app.include_router(api_router)
+
+ # 健康检查
+ @app.get("/health")
+ async def health_check():
+ return {"status": "healthy", "app": settings.APP_NAME}
+
+ return app
+
+
+app = create_app()
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(
+ "main:app",
+ host="0.0.0.0",
+ port=8000,
+ reload=True,
+ log_level="info"
+ )