后端 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>
63 lines
2.2 KiB
Python
63 lines
2.2 KiB
Python
"""统一响应模型。
|
||
|
||
所有接口响应体形如 `{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
|