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>
This commit is contained in:
commit
273ebf4657
101 files changed
+8725
No files matched your search
@@ -0,0 +1,103 @@
|
||||
"""应用配置。
|
||||
|
||||
从 `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]
|
||||
Reference in new issue
Block a user