PayrollManagementSystem/backend/app/core/config.py
liuwenxin 273ebf4657 chore: 初始化项目结构与登录鉴权模块
后端 FastAPI 六层结构(api/services/repositories/models/schemas/core),
统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存
db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、
登出黑名单(TTL=令牌剩余有效期)、登录日志。

前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。

工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。

测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-09 18:46:36 +08:00

104 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""应用配置。
从 `backend/.env` 读取配置项,字段名与 `.env` 中的变量名一一对应
(pydantic-settings 默认大小写不敏感,`app_secret_key` ←→ `APP_SECRET_KEY`)。
设计取舍:
- `APP_SECRET_KEY` 无默认值 —— 缺失时启动即失败。空密钥签发 JWT 属安全问题
而非可用性问题,必须 fail-fast,不能容忍"后端起得来但令牌可被伪造"。
- 数据库/Redis 连接不在此处建立 —— 依赖不可用时仍应能启动应用、查看接口文档。
"""
from functools import lru_cache
from pathlib import Path
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy import URL
# backend/ 目录:本文件位于 backend/app/core/config.py,向上三级
BACKEND_DIR = Path(__file__).resolve().parents[2]
ENV_FILE = BACKEND_DIR / ".env"
class Settings(BaseSettings):
"""应用配置项。"""
model_config = SettingsConfigDict(
env_file=str(ENV_FILE),
# Windows 默认编码为 cp936,不显式指定会导致中文值解析出错
env_file_encoding="utf-8",
case_sensitive=False,
# .env 中可能含本服务用不到的变量(如 docker 编排专用项),忽略而非报错
extra="ignore",
)
# ===== 应用 =====
app_env: str = "dev"
app_secret_key: str = Field(description="JWT 签名密钥,至少 32 字符")
# ===== 鉴权 =====
# 规范第 13 节:Token 有效期 ≤ 2 小时
jwt_expire_minutes: int = Field(default=120, ge=1, le=120)
# 规范第 6 节:验证码 300s
captcha_expire_seconds: int = Field(default=300, ge=60, le=300)
login_fail_limit: int = Field(default=5, ge=1)
login_lock_minutes: int = Field(default=15, ge=1)
# ===== MySQL =====
mysql_host: str = "127.0.0.1"
mysql_port: int = 3306
mysql_database: str = "payroll"
mysql_user: str = "payroll"
mysql_password: str = ""
# ===== Redis =====
redis_host: str = "127.0.0.1"
redis_port: int = 6379
redis_password: str = ""
redis_db: int = 0
@field_validator("app_secret_key")
@classmethod
def _check_secret_key(cls, v: str) -> str:
"""校验密钥强度:过短的密钥可被暴力破解,直接拒绝启动。"""
if len(v) < 32:
raise ValueError("APP_SECRET_KEY 长度不足 32 字符,请在 backend/.env 中重新生成")
return v
@property
def database_url(self) -> URL:
"""数据库连接 URL。运行期与 Alembic 迁移共用同一份。
用 SQLAlchemy 的 URL.create 而非手工拼接字符串 —— 密码含 @ : / 等
特殊字符时手工拼接会产生语法错误。
驱动为同步的 pymysql:本项目并发极低,异步无收益;而绩效核算与
pandas 报表都是同步阻塞代码,同步方案不会与之冲突。
"""
return URL.create(
drivername="mysql+pymysql",
username=self.mysql_user,
password=self.mysql_password,
host=self.mysql_host,
port=self.mysql_port,
database=self.mysql_database,
query={"charset": "utf8mb4"},
)
@property
def redis_url(self) -> str:
"""Redis 连接 URL。密码为空时不拼接认证段。"""
auth = f":{self.redis_password}@" if self.redis_password else ""
return f"redis://{auth}{self.redis_host}:{self.redis_port}/{self.redis_db}"
@lru_cache
def get_settings() -> Settings:
"""获取配置单例。
进程内只解析一次 .env;测试中如需覆盖,调用方应使用
`get_settings.cache_clear()` 配合环境变量注入。
"""
return Settings() # type: ignore[call-arg]