2026-09-27·by Sijie Wang#standmeet#architecture#design#events

code-structure

代码结构:事务显式传到用例

上级:events

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

总线大部分就是"接口后面的 infra"(queue-behind-ports),没太多可说的;真正要定的是三件事:事务怎么到达用例、订阅方如何零胶水注册、副作用端口搬到哪里。

事务怎么到达用例

原来事务是在 repo 内部开的,大多数写入是单条语句 autocommit,用例手里没有事务,所以显式的 Record 没法和业务写入进同一个事务。触发器捕获的行事件(two-sources-of-events)不受影响,语义事件受影响。

事务是显式参数,不进 ctx。 ctx 里已经有 14 处 WithValue;再放事务就是给它加一种隐式依赖——同一个 repo 调用进不进事务,取决于调用方 ctx 里恰好装了什么——这正是 ctx 变神类的路。

  • pgstore.InTx(ctx, db, fn):在 db 上开事务,db 是一个 Beginner:连接池,或一个已打开的事务(那就是一个 savepoint)。fn 返回 nil 就提交,否则回滚;fn 里 panic 会回滚后再抛出。
  • pgstore.Nested(q, pool):被 With(q) 绑定的 repo 需要自己开事务时用它:q 是事务就在里面开 savepoint,否则在连接池上开一个新事务。
  • With(tx):每个 repo、Recorder、Jobs 都有,返回绑定到该事务的副本。原有方法签名不动;谁在事务里由编译器检查。
  • ctx 只装取消与截止时间:本计划不增加任何 ctx key。Record 要的 owner 等信息显式传参,不从 ctx 读。
  • 门禁 check-tx-only-via-pgstore.sh:.Begin(、BeginTx(、BeginFunc( 只能出现在 internal/infra/pgstore。现有的 11 处 Begin 都已改完(计划时估的是约 20 处)(no-bypass-by-structure)。
  • UT:fn 返回错误或 panic 时业务、事件、任务一起回滚;With(tx) 的副本不影响原对象;不经 With 的调用行为不变(events-test-plan)。

订阅方零胶水

各域把事件类型(event-model)、订阅和任务种类都声明成数据(和 dispatcher op 同样形状),经自己的 facade 暴露。组合根(cmd/server/wire/periodic.go)每个来源一行把它们收集起来,不关心任务具体做什么。唯一的专门接线是把 embed 范围函数(EmbedAdmits,来自 access 域)交给 owner 域的 webhook 扇出。

副作用端口搬了家

OutboundSender 原来在 cmd/server/port,后台 supplier 调用在 plugin/adapters。现在邮件端口是 internal/infra/sideeffect/mail;后台 supplier 调用是 internal/infra/sideeffect/supplier 里持久的 supplier.invoke 任务。错误表、lint 豁免、测试钩子都跟着搬了。只有 subscriber 包、infra 和 cmd/server 可以 import 它们。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →