后端风格
后端代码的强制约定清单,可直接当 PR review checklist。涵盖响应、Schema、API 路径、CRUD、权限、异常、缓存、命名八块。
不是软建议
违反任意一条都视为"待修复"。需破例时请在 PR 中给出明确理由并指向架构决策。
1. 响应
- ✅ 必须用
Success/SuccessExtra/Fail(来自app.utils) - ❌ 不要返回裸 dict、
JSONResponse(...)、{"code": "0000", ...}字面量 - ❌ 不要手工拼 snake_case 响应——
SchemaBase/to_dict()都自动 camelCase - ✅ 每个不同的失败场景用唯一业务码(在
app/core/code.py末尾追加),少用Code.FAIL兜底 - ✅ 业务异常用
raise BizError(code, msg)(穿透任意层),仅在 api 层用return Fail(...)
python
# ✅
return Success(data=await user.to_dict())
return SuccessExtra(data={"records": records}, total=total, current=obj_in.current, size=obj_in.size)
raise BizError(code=Code.HR_INVALID_TRANSITION, msg="不允许的状态流转")
# ❌
return {"code": "0000", "data": {...}}
return JSONResponse({"code": Code.FAIL, "msg": "失败"})2. Schema
- ✅ 业务 schema 必须继承
SchemaBase(自动 snake_case ↔ camelCase) - ✅ 分页搜索 schema 必须继承
PageQueryBase - ✅ 资源 ID 字段类型用
SqidId,FastAPI 路径参数用SqidPath - ✅ 整型字段按数据库范围用
Int16 / Int32 / Int64 - ✅ Update schema 用
make_optional(XxxCreate, "XxxUpdate")派生,避免冗余字段定义 - ❌ Schema 中不要
from pydantic import BaseModel as ...——业务一律from app.utils import SchemaBase - ✅ Schema 校验器中要返回业务码时用
SchemaValidationError(code, msg),不要ValueError
3. API 路径与方法
- ✅ 列表:
POST /resources/search,body 继承PageQueryBase - ✅ 单条:
GET / PATCH / DELETE /resources/{id}(id 是 sqid) - ✅ 创建:
POST /resources - ✅ 批量删除:
DELETE /resources,body 用CommonIds - ✅ 多词路径用 kebab-case(
/batch-offline、/user-routes) - ✅ 资源名一律 复数
- ❌ 路径不带尾斜杠
- ❌ 不要用
GET /resources?xxx=...&yyy=...实现复杂搜索——一律走POST /search
完整约定见 API 约定。
4. 路由层(CRUD)
- ✅ 标准 6 路由用
CRUDRouter生成,禁止手写 - ✅ 自定义某条路由用
@crud.override("create"),不要在 router 上重新声明同路径 - ✅ 按钮权限挂在
action_dependencies上,对@override路由也生效 - ✅ 标准 6 路由之外的端点直接挂在
crud.router上 - ❌ 不要直接操作
router.routes.append(...)绕过_OrderedRouter排序 - ❌ Controller / Service 中不要 import
fastapi.Request/Response——HTTP 相关只在 api 层 - ❌
@crud.override内禁止出现in_transaction/request.app.state.redis/ 跨模型写 / 事件 / 审计——必须下沉到services/,api 层只做参数转发与响应封装 - ❌ 当某资源 override 数 ≥ 3,或该资源是聚合根(带状态、带副作用),应改为显式
@router.post(...)+services/,不要硬塞CRUDRouter(CRUDRouter 只服务贫血资源;详见 CRUDRouter / 适用边界)
5. 分层职责
| 层 | 必须做 | 必须不做 |
|---|---|---|
api/ | URL 接线、依赖、Success/Fail | 业务规则、跨模型、事务 |
services/ | 事务、跨模型、缓存、状态机、事件 | 触发 HTTP(Request/Response) |
controllers/ | 单模型 CRUD(继承 CRUDBase)、build_search | 跨模型编排、事务、缓存、事件派发、外部 IO |
models/ | 表字段、索引、关系、Mixin | 业务校验逻辑 |
schemas/ | DTO 与字段级校验 | 跨资源逻辑 |
6. 权限
- ✅ 写接口(POST/PATCH/DELETE)必须挂按钮权限(
require_buttons或action_dependencies) - ✅ 业务角色种子必须显式声明
data_scope(不要依赖模型默认all) - ✅ 涉及行级权限的列表接口必须
@override("list")拼build_scope_filter - ❌ 不要靠"前端隐藏按钮"做安全——后端必须有对应校验
- ❌ 不要在业务里直接判
role_code == "R_HR_ADMIN"——用has_role_code/has_button_code
7. 模型
- ✅ 模型一律继承
BaseModel + AuditMixin(持久化的) - ✅ 文件头
# pyright: reportIncompatibleVariableOverride=false(Tortoise + Pyright 已知误报) - ✅ 字段加
description="..."(CLI 生成 schema 时会用作 i18n 中文名,截断到第一个句号) - ✅ 类的 docstring 写中文资源名(
"""部门"""),用作 API summary 前缀 - ✅
Meta.table用biz_<module>_<entity>前缀(系统模型在app/system/models/下用语义化表名) - ✅ 每个
ForeignKeyField/OneToOneField上方显式声明<name>_id: int(或int | None)类型注解——见下方 - ❌ 不要在
models.py写业务逻辑——字段级校验放 schema、跨模型编排 / 事务 / 事件 / 缓存放 service
外键访问规范
Tortoise 的 FK 字段在运行时会自动派生一个 <name>_id 属性(同步访问、零查询,对应 DB 列);<name> 本身是异步懒加载的关系对象。两者不是一回事:
python
# models.py
class Employee(BaseModel, AuditMixin):
# ✅ 显式声明 _id 类型注解,让 pyright / IDE 能看到该属性
user_id: int | None
user: fields.ForeignKeyNullableRelation = fields.ForeignKeyField(
"app_system.User", null=True, on_delete=fields.SET_NULL, related_name="employee",
)
department_id: int
department: fields.ForeignKeyRelation["Department"] = fields.ForeignKeyField(
"app_system.Department", related_name="employees",
)使用时按需求选择:
| 需求 | 正确写法 | 错误写法 |
|---|---|---|
| 只要 ID(比较、日志、传给另一个模型的 FK 字段) | emp.department_id | (await emp.department).id — 多一次查询 |
要关系对象的字段(dept.name) | 先 prefetch_related("department") 再读 emp.department.name | emp.department.name — 懒加载,循环中触发 N+1 |
| 单对象场景、无批量 | dept = await emp.department | — |
| M2M / 反向关系 | prefetch_related("by_role_menus") 后遍历;或显式 .all() | 直接 for m in role.by_role_menus — RelatedManager 不是可迭代对象 |
创建 / 更新时不要把未 prefetch 的关系对象当值传
python
# ❌ 如果 other.department 没有 prefetch,这一行会触发查询;
# 更糟的是 Tortoise 老版本会把 coroutine 对象赋值给 FK,写入时抛 TypeError
new_emp = await Employee.create(department=other.department)
# ✅ 总是传 _id
new_emp = await Employee.create(department_id=other.department_id)这就是为什么 _id: int 注解是强制的——把"用 ID 安全路径"放到开发者眼前,IDE 自动补全也会优先提示。
8. 业务模块边界
- ✅ 业务模块 import 入口统一走
app.utils - ✅ 跨业务模块联动用 事件总线(
emit/on) - ❌ 业务模块不得反向 import
app.system.*(除了ensure_menu/ensure_role等 system 主动暴露的 service) - ❌ 业务模块不得互相 import(
app.business.crm.*不能from app.business.hr import ...)
9. 命名
- 文件 / 目录:
snake_case - 类:
PascalCase - 函数 / 方法:
snake_case - 常量:
UPPER_SNAKE_CASE - API 路径:
kebab-case - Schema 字段(Python 内部):
snake_case;HTTP 边界(前端可见):camelCase - 角色编码:
R_<UPPER>/ 按钮编码:B_<MODULE>_<RESOURCE>_<ACTION>/ 路由名:module_subpage
详见 命名规范。
10. 类型注解 / 格式化 / 静态检查
- ✅ 所有函数都加类型注解
- ✅ 提交前跑:
bash
make fmt # ruff check --fix + format
make typecheck # basedpyright
make test # pytest
make check # 上面三条一次跑完- ✅ 行宽 200,双引号,自动排序 import
- ✅ basedpyright standard 模式必须通过
11. 异常处理
- ✅ 用
BizError/SchemaValidationError抛业务错 - ❌ 不要
raise HTTPException(旧别名仅为兼容存在,新代码用BizError) - ❌ 不要 catch
Exception后吞掉——全局处理器会记录日志 - ✅ Service 里需要事务 + 失败补偿用
in_transaction(get_db_conn(Model)) - ❌ 不要硬编码连接名(
"conn_system"),用get_db_conn(Model)
12. 缓存
- ✅ 业务自有的小热点(统计 / 选项)按"读 → miss → 查 → 写带 TTL"模式手写
- ✅ 数据变更时主动失效对应 key(业务模块的
cache_utils.py) - ❌ 不要给带分页 / 多参数的接口加全局
@cache(...)装饰器 - ✅ 业务 key 命名:
<module>_<resource>:<scope>(如dict_options:tag_category)
详见 缓存。
13. 日志与监控
- ✅ 关键业务节点 / 权限拒绝 / 异常分支用
radar_log - ✅ 高频度调试日志直接
log.debug(...),不要全部上 radar - ❌ 不要
print(...)
14. 提交前必跑
bash
make check-all # 后端 + 前端所有质量检查包括:ruff fix + format、basedpyright、pytest、eslint + oxlint、vue-tsc。
相关
- 架构 — 总览
- 开发指南 — 用 CLI 创建模块的端到端流程
- API 约定 / 响应码
- HR 模块(最佳实践全集)