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:
liuwenxinandClaude Code committed 2026-10-09 18:46:36 +08:00
commit 273ebf4657
101 files changed
+8725

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
"""统一响应模型。
所有接口响应体形如 `{code, message, data}`(PROJECT_STANDARDS 第 3 节)。
分页响应字段为 `list/total/pageNum/pageSize`(同规范第 14 节)。
字段名采用驼峰(规范约定)而 Python 属性用蛇形(语言惯例),
两者通过 pydantic 的 alias 映射,序列化时输出驼峰。
"""
from pydantic import BaseModel, ConfigDict, Field
from app.core.constants import CODE_SUCCESS
class ApiResponse[T](BaseModel):
"""统一响应包装。泛型参数为 `data` 的类型。"""
code: int = Field(default=CODE_SUCCESS, description="状态码,与 HTTP 状态码一致")
message: str = Field(default="success", description="提示信息")
data: T | None = Field(default=None, description="业务数据")
def ok[T](data: T | None = None, message: str = "success") -> ApiResponse[T]:
"""构造成功响应。
统一出口便于日后调整成功码或附加字段时只改一处。
"""
return ApiResponse(code=CODE_SUCCESS, message=message, data=data)
class PageResult[T](BaseModel):
"""分页结果。
Python 侧使用蛇形属性名,序列化后输出规范要求的驼峰字段名。
"""
model_config = ConfigDict(populate_by_name=True)
records: list[T] = Field(
default_factory=list, alias="list", serialization_alias="list", description="当前页数据"
)
total: int = Field(default=0, description="总记录数")
page_num: int = Field(
default=1, alias="pageNum", serialization_alias="pageNum", description="当前页码"
)
page_size: int = Field(
default=10, alias="pageSize", serialization_alias="pageSize", description="每页条数"
)
class PageQuery(BaseModel):
"""分页查询入参基类。业务查询参数继承本类扩展。"""
model_config = ConfigDict(populate_by_name=True)
page_num: int = Field(default=1, ge=1, alias="pageNum", description="页码,从 1 开始")
page_size: int = Field(default=10, ge=1, le=200, alias="pageSize", description="每页条数")
@property
def offset(self) -> int:
"""转换为 SQL OFFSET。"""
return (self.page_num - 1) * self.page_size