Skip to content

后端风格

后端代码的强制约定清单,可直接当 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_buttonsaction_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.tablebiz_<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.nameprefetch_related("department") 再读 emp.department.nameemp.department.name — 懒加载,循环中触发 N+1
单对象场景、无批量dept = await emp.department
M2M / 反向关系prefetch_related("by_role_menus") 后遍历;或显式 .all()直接 for m in role.by_role_menusRelatedManager 不是可迭代对象

创建 / 更新时不要把未 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 + formatbasedpyrightpytesteslint + oxlintvue-tsc

相关

基于 MIT 协议发布