针对
app/modules/<域>/业务开发。框架已强制的(dishka DI / import-linter 分层 / outbox-inbox 自动化 / TaskResult 抽象 / FastStream 自动埋点)不在此列。
- 异步优先:IO 一律 async,阻塞调用放线程池
- 引包先评估:支持 async + 活跃维护 + 最新稳定版,不满足换选型
- 不过度设计:只做当前必要的;无 2+ 同类项不预留目录
- 密钥:
SecretStr+.env(本地)/ Secrets Manager(生产),不硬编码
- 跨模块调用经
public.py:from app.modules.<域>.public import X,禁止直 import 内部 - 三入口(HTTP/consumer/scheduler)只做协议翻译,业务逻辑只写一份落
service.py - 对外 HTTP 调用收敛到
app/integrations/<provider>/,客户端轻薄,不自动重试 - DI 不挂业务 Provider:service / router 直接
FromDishka[session_factory / EventRegistry / ...]取 infrastructure 资源 - Worker consumer / Scheduler cron 需外部 client 时,handler 声明
*, integrations: Integrations,内部integrations.get(<Client>)取;多实例拆类(<A>Client/<B>Client),不用 (type, name) 二元 key;不在 handler 内自建 httpx client
- service 接收
session_factory(不是 session);入口层不管事务 - 写表(单/多统一):
async with sf() as s, s.begin(): ...;只读:async with sf() as s: ... - Consumer 长任务只在写 DB 步骤开 session
- 只捕获自定义异常(
AppError体系)做业务处理;不吞异常、不给默认值、不捕获Exception兜底 - 事务回滚后必须重抛;其他异常自然向上抛由全局处理器兜底
- 契约收口在
events.py:每事件一个 Pydantic 类,routing_key 只出现一次 - 发布:
await events.X.publish(session, payload)(同事务 outbox);schema 演进只增字段 + 带message_version
- routing_key 格式
<domain>.<entity>.<action>(如example.widget.requested),action 可按订阅粒度继续细分;不带 app_name 前缀;domain 对应app/modules/<domain>/目录名 - queue 名 当前
== routing_key(引擎RabbitQueue(routing_key)直接绑定);多 app 共享 vhost 撞车时再分离为<appname>.<domain>.<entity>.<action>(延后)
- 主键
BIGINT GENERATED BY DEFAULT AS IDENTITY(业务自然唯一用联合主键);时间戳TIMESTAMPTZ(不用TIMESTAMP);JSON 用JSONB - 状态/枚举
VARCHAR(N)存字符串(如pending/dead),不用魔法 INT,可选值在 COMMENT 列全 - 索引 按查询模式建,优先部分索引,命名
idx_<表>_<语义>;默认值 有业务含义的写进 DDL(DEFAULT) - COMMENT 全字段覆盖:每张表配
COMMENT ON TABLE,每个字段(含id/created_at/updated_at等通用列)都配COMMENT ON COLUMN。注释只描述"字段装的是什么"(语义、数据来源、枚举值域),不带架构方案文案(实现要点、用途、不变量、设计决策等) updated_at手动刷新:框架不自动刷新updated_at(model 不挂onupdate、不建触发器)。每个 UPDATE 写路径必须显式带updated_at=func.now()(见app/modules/example/repository.py)。
- 不提供跨库事务(XA / two-phase commit)。
session.begin()只保证单库原子 - 跨库一致性使用 outbox 最终一致,但须理解边界:
- outbox 表在主 PG,只保证"主库事务内 outbox 落地 → 消息必达"
- 业务库写 + 主库 outbox 写本身跨库,无法单事务原子
- 正确姿势:业务库写 → 消费侧幂等 + inbox 去重承接。不要指望业务库写与 outbox 写原子绑定
- 业务模块统一 structlog:
log = structlog.get_logger(__name__),上下文走 kwargs(log.info("module.event", key=value),kwargs 直接进 event_dict) - 不要 stdlib
logging.getLogger+extra={...}:extra 经 structlogProcessorFormatter不渲染,字段全部丢失 - 事件名
<模块>.<动作>(点号分隔,如example.created、outbox.dead) - 例外:
app/infrastructure/observability/logging.py配置 root logger / handler 时仍用 stdlib API(那是 stdlib 入口)