编写后端

This commit is contained in:
2026-03-28 22:18:43 +08:00
parent f2528fbc87
commit f5d26949c4
63 changed files with 1841 additions and 5 deletions

View File

@@ -0,0 +1 @@
# Core module

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View 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")

View 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()

View 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