编写后端
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user