Version: 0.9.64.dev.260503

后端:
1. 服务级 outbox 基础设施全量落地——新增 service route / service catalog / route registry,重构 outbox engine、repository、event bus 和 model,按 `event_type -> service -> table/topic/group` 统一写入与投递,保留 `agent` 兼容壳但不再依赖共享 outbox
2. Kafka 投递、消费与启动装配同步切换——更新 kafka config、consumer、envelope,接入服务级 topic 与 consumer group,并同步调整 mysql 初始化、start/main/router 装配,保证各服务 relay / consumer 独立装配
3. 业务事件处理器按服务归属重接新 bus——`active-scheduler` 触发链路,以及 `agent` / `memory` / `notification` / `task` 相关 outbox handler 统一切到新路由注册与服务目录,避免新流量回流共享表
4. 同步更新《微服务四步迁移与第二阶段并行开发计划》,把阶段 1 改成当前基线并补齐结构图、阶段快照、风险回退和多代理执行口径
This commit is contained in:
Losita
2026-05-03 20:29:00 +08:00
parent 166fb1b507
commit a6c1e5d077
28 changed files with 1631 additions and 340 deletions

View File

@@ -2,7 +2,12 @@
## 1. 文档定位
这份文档是当前后端迁移的主总纲,目标是把现状从“单体 Gin + 统一 outbox”平滑演进到“Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层”。
这份文档是当前后端迁移的主总纲,目标是把阶段 0 的“单体 Gin + 统一 outbox”基线,继续平滑演进到“Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层”。
当前进度口径:
1. 阶段 0 已完成:运行入口、事件契约、路由清单和 outbox 语义已经冻结。
2. 阶段 1 已完成:当前基线已经切成服务级 outbox 表、服务级 Kafka topic、服务级 consumer group仍在单体进程内装配多个服务级 worker后续拆微服务时再物理迁出。
本计划遵守两个硬原则:
@@ -23,13 +28,15 @@
3. `notification_records` 已经有独立状态机、幂等、重试能力。
4. 事件契约已经拆成 `active_schedule.triggered``notification.feishu.requested``schedule.apply.*` 等独立 payload。
当前 outbox 仍然偏单体
阶段 1 之后,当前 outbox 已经不是共享单表 / 单 topic / 单 group 形态
1. relay 还带着“全局扫一遍”的思维
2. consumer 还默认“一个 worker 吃全部事件”
3. 未知事件的处理策略还不适合长期多服务共用
1. `agent``task``memory``active-scheduler``notification` 已经有各自的 outbox 表
2. 每个服务都有自己的 relay worker把本服务 outbox 表投递到本服务 Kafka topic
3. Kafka 仍是共享运输层,但 topic 已按服务切开,不再把新流量写进共享 topic
4. 消费侧已经按服务 consumer group 隔离,不再用一个 worker 吃全部事件。
5. 当前仍是单体进程内多 worker 装配worker 后续会跟随对应服务一起迁出,不在阶段 1 直接拆进程。
所以现在最合适的路线不是直接把所有服务拆完,而是先把 outbox 升级成服务级基础设施,再按服务边界逐个切出去。
所以后续路线不是再补一次 outbox 基建,而是在这个阶段 1 基线上,按服务边界逐个把 gozero 服务、DAO / model / worker 和启动入口迁出去。
---
@@ -76,9 +83,21 @@ gozero 服务负责领域能力:
1. 每个服务拥有自己的 outbox。
2. 每个服务有自己的 relay worker。
3. Kafka 作为共享运输层。
3. Kafka 作为共享运输层,但业务 topic 不共享
4. 同一服务的多个实例进入同一个 consumer group做横向负载均衡。
5. topic 优先按服务/事件域划分,过渡期如果共享 topic必须有显式 `event_type -> service` 路由
5. topic 按服务/事件域划分,消费侧按服务 consumer group 隔离
6. 新增事件必须登记 `event_type -> service` 路由,再由服务 catalog 映射到对应 outbox 表、topic 和 consumer group。
7. 未知事件不能靠“写进共享表再由全局 worker 兜底”处理;必须在路由注册、死信或显式失败之间选一种清晰策略。
当前阶段 1 基线:
| 服务 | outbox 表 | Kafka topic | consumer group |
| --- | --- | --- | --- |
| `agent` | `agent_outbox_messages` | `smartflow.agent.outbox` | `smartflow-agent-outbox-consumer` |
| `task` | `task_outbox_messages` | `smartflow.task.outbox` | `smartflow-task-outbox-consumer` |
| `memory` | `memory_outbox_messages` | `smartflow.memory.outbox` | `smartflow-memory-outbox-consumer` |
| `active-scheduler` | `active_scheduler_outbox_messages` | `smartflow.active-scheduler.outbox` | `smartflow-active-scheduler-outbox-consumer` |
| `notification` | `notification_outbox_messages` | `smartflow.notification.outbox` | `smartflow-notification-outbox-consumer` |
### 3.4 共享层边界
@@ -97,8 +116,8 @@ gozero 服务负责领域能力:
| 阶段 | 目标 | 建议 commit 点 | 建议测试 |
| --- | --- | --- | --- |
| 0 | 语义冻结和基线确认 | 当前运行边界、事件契约和路由清单固定后,先做一个基线 commit | `go test ./...``api / worker / all` 启动 smoke |
| 1 | Outbox v2 基建 | 服务级 outbox 路由和 relay 逻辑打通后 commit 一次 | 全量单测 + outbox 发布/消费 smoke |
| 0 | 语义冻结和基线确认(已完成) | 阶段 0 已作为历史基线保存;后续只在契约变化时回看 | `go test ./...``api / worker / all` 启动 smoke |
| 1 | Outbox v2 基建(已完成,当前基线) | 当前已具备阶段 1 保存点:服务级 outbox 表、topic、group 和多 worker 装配已打通 | 已完成健康检查、服务级 outbox 写入/投递/消费 smoke、Kafka group lag 核对 |
| 1.5 | 先抽 llm-service | 统一模型调用、provider 路由、流式输出和审计后 commit | course / active-scheduler / memory 模型调用 smoke |
| 1.6 | 再抽 rag-service | 向量化、召回、重排、检索能力跑通后 commit | memory retrieve / rerank smoke |
| 2 | 先拆 user/auth | user 路由、JWT 签发和 token 额度治理独立后 commit | 注册/登录/刷新/登出 smoke + token quota 回归 |
@@ -113,7 +132,9 @@ gozero 服务负责领域能力:
---
### 4.2 阶段 0语义冻结和基线确认
### 4.2 阶段 0语义冻结和基线确认(已完成)
本节保留为历史回顾,用来说明阶段 0 冻结了什么,不是当前待办。
目标:
@@ -142,32 +163,69 @@ gozero 服务负责领域能力:
---
### 4.3 阶段 1Outbox v2 基建
### 4.3 阶段 1Outbox v2 基建(已完成)
目标
当前结论
1. outbox 从“单体内部事件泵”升级成“服务级事件总线能力”。
2. 让 outbox 先具备服务归属,再谈服务拆分
3. 为后面的 gozero 服务切分打地基
1. outbox 已经从“单体内部事件泵”升级成“服务级事件总线能力”。
2. 当前基线已经具备 `event_type -> service -> outbox 表 / topic / group` 的服务归属链路
3. 服务还没有物理拆成多个 gozero 进程;目前是在单体内按服务装配多个 relay worker 和 consumer worker
4. 后续拆微服务时worker 会随对应服务迁出,不再重新设计 outbox 边界。
这一步要做的事:
已完成的事:
1. 引入 outbox 归属概念,服务和 outbox 一一对应
2. relay worker 变成服务自己的后台进程
3. consumer 按服务订阅路由,不再一个 worker 什么都吃
4. 过渡期允许旧实现与新实现并行,但切流点必须清晰
5. 如果短期共享 topic必须有显式事件路由不能靠“未知事件直接 dead”来硬顶
1. 引入 outbox 服务归属概念,`agent``task``memory``active-scheduler``notification` 分别对应自己的 outbox
2. relay worker 已按服务拆开,每个 relay 只扫描本服务 outbox 表并投递到本服务 topic
3. consumer 按服务 group 隔离,每个服务只处理归属到自己的事件
4. topic 已经直接切成服务级 topic不再把“共享 topic”作为当前终态或过渡依赖
5. 事件发布入口已经通过路由和 catalog 决定服务归属,避免新事件默认回流到共享 outbox
建议提交点
当前 outbox 总线结构
1. outbox 归属和路由抽象完成后,先保存一个 commit。
2. 第一条服务级 relay 跑通后,再保存一个 commit。
```mermaid
flowchart LR
Biz["业务发布事件"] --> Router["event_type -> service 路由"]
Router --> Catalog["service -> table/topic/group"]
建议测试:
Catalog --> AgentTable["agent_outbox_messages"]
Catalog --> TaskTable["task_outbox_messages"]
Catalog --> MemoryTable["memory_outbox_messages"]
Catalog --> ActiveTable["active_scheduler_outbox_messages"]
Catalog --> NotifyTable["notification_outbox_messages"]
1. `go test ./...`
2. outbox 发布 / 投递 / 消费 smoke。
3. 未知事件不会误伤其他服务的路由验证。
AgentTable --> AgentRelay["agent relay"] --> AgentTopic["smartflow.agent.outbox"] --> AgentGroup["smartflow-agent-outbox-consumer"]
TaskTable --> TaskRelay["task relay"] --> TaskTopic["smartflow.task.outbox"] --> TaskGroup["smartflow-task-outbox-consumer"]
MemoryTable --> MemoryRelay["memory relay"] --> MemoryTopic["smartflow.memory.outbox"] --> MemoryGroup["smartflow-memory-outbox-consumer"]
ActiveTable --> ActiveRelay["active-scheduler relay"] --> ActiveTopic["smartflow.active-scheduler.outbox"] --> ActiveGroup["smartflow-active-scheduler-outbox-consumer"]
NotifyTable --> NotifyRelay["notification relay"] --> NotifyTopic["smartflow.notification.outbox"] --> NotifyGroup["smartflow-notification-outbox-consumer"]
```
当前切流点:
1. 新事件先按 `event_type` 查服务路由,再按服务 catalog 写入对应 outbox 表。
2. relay 只处理本服务表里的记录,不再全局扫描一个共享表。
3. Kafka topic 已经按服务切开consumer group 也按服务切开。
4. `agent_outbox_messages` 仍沿用历史默认表名作为 `agent` 服务表;表内旧数据是共享 outbox 时代的历史存量,不代表新流量仍回流 `agent`
仍保留的旧实现:
1. `backend/cmd/api``backend/cmd/worker``backend/cmd/all` 仍是当前启动壳,尚未拆成终态的多 gozero 进程。
2. `backend/model/outbox.go` 仍保留兼容模型;实际写入表由 outbox repository 根据服务路由选择。
3. 既有事件契约和 handler 继续保留,后续按服务迁移时再逐步移动到对应服务目录。
已完成验证:
1. 健康检查返回 200。
2. MySQL 当前只有服务级 outbox 表,没有继续依赖共享 `outbox_messages` 表。
3. Kafka 已存在 5 个服务级 topic 和 5 个服务级 consumer group。
4. 最新 smoke 中,`task.urgency.promote.requested` 写入 `task_outbox_messages`topic 为 `smartflow.task.outbox`,状态流转到 `consumed``task` group lag 为 0。
5. 对应任务优先级从 `2` 更新到 `1`,证明发布、投递、消费和业务处理链路已经闭环。
下一步:
1. 不再重复做 Outbox v2 基建。
2. 后续从阶段 1.5 / 1.6 开始,按 `llm-service``rag-service``user/auth` 等服务边界推进物理拆分。
3. 迁移任何新服务时,必须复用当前 outbox 路由、服务 catalog、relay 和 consumer group 隔离规则。
---
@@ -414,22 +472,21 @@ gozero 服务负责领域能力:
## 5. 推荐执行顺序
近期建议按这个顺序推进:
当前建议按这个顺序推进:
1. 先冻结现有语义和契约
2. 再做 Outbox v2
3. llm-service统一模型出口从各业务服务里抽出去。
4. rag-service把检索基础设施从 memory / agent 里抽出去。
5. user/auth把登录态和额度门禁从 gateway 拿出去
6. 再切 notification
7. 再切 active-scheduler
8. 然后切 schedule / task / course / task-class
9. 再切 agent / memory把聊天编排和记忆链路独立出去
10. 最后把 Gin 收口成纯 Gateway。
1. 以阶段 1 的服务级 outbox 为当前基线,不再回头做共享 outbox 方案
2. 先切 llm-service把统一模型出口从各业务服务里抽出去
3. rag-service检索基础设施从 memory / agent 里抽出去。
4. user/auth把登录态和额度门禁从 gateway 拿出去。
5. notification
6. 再切 active-scheduler
7. 然后切 schedule / task / course / task-class
8. 再切 agent / memory把聊天编排和记忆链路独立出去
9. 最后把 Gin 收口成纯 Gateway
一句话总结:
> 先把 outbox 从单体内部兜底机制变成服务级基础设施;再把 llm-service 抽成全仓统一模型出口,把 rag-service 抽成统一检索基础设施;然后把 user/auth 从 gateway 里抽出去,清掉用户表直连和额度门禁耦合;接着把 notification 切成第一条事件驱动服务线;然后让 active-scheduler、schedule、task、course、task-class 按稳定边界逐步独立;再把 agent / memory 独立出来,完成聊天编排和记忆链路的服务化;最后把 Gin 收口成真正的 Gateway。
> outbox 的服务级基础设施已经完成;接下来先把 llm-service 抽成全仓统一模型出口,把 rag-service 抽成统一检索基础设施;然后把 user/auth 从 gateway 里抽出去,清掉用户表直连和额度门禁耦合;接着把 notification 切成第一条事件驱动服务线;然后让 active-scheduler、schedule、task、course、task-class 按稳定边界逐步独立;再把 agent / memory 独立出来,完成聊天编排和记忆链路的服务化;最后把 Gin 收口成真正的 Gateway。
---
@@ -650,7 +707,7 @@ SmartFlow-Agent/
3. `agent` 不是公共能力,它应当单独成服务;`memory` 也是独立支撑服务,不应长期挂在 gateway 里。
4. `notification``active-scheduler` 都应该回到更像 seckill 的服务内单体结构,避免成为“半个框架”。
5. `llm-service` 是全仓统一模型出口;`rag-service` 是统一检索基础设施;`rag-service` 依赖 `llm-service`,不反向依赖业务服务。
6. outbox 升级成服务级基础设施,按域边界逐个切出去。
6. outbox 已经升级成服务级基础设施,后续直接在当前服务级表 / topic / group 基线上按域边界逐个切出去。
7. 后续任何服务目录调整,都要先对照下面的“典型用例”;如果这次改动说不清它属于哪个用例,就先不要动结构,只补文件或补注释。
### 6.6 `notification` / `active-scheduler` 的服务内结构
@@ -757,6 +814,8 @@ graph TD
2. `agent` 仍然是编排层,实际会通过契约调用 `user/auth``course``task-class``notification``active-scheduler``schedule``task``memory`
3. Gateway 不直接碰 `llm-service` / `rag-service`,只把请求转给对应业务服务。
4. 图里的 outbox 是“每个服务自己的 outbox 表 + 专属 relay worker”的抽象不代表所有服务共用一张表。
5. 当前阶段 1 已完成 `agent``task``memory``active-scheduler``notification` 的服务级 outbox 表、topic 和 consumer group尚未物理拆出的服务后续沿用同一模式补齐。
6. Kafka 是共享运输层,不是共享业务 topic新流量不应再默认进入单一共享 topic。
### 6.9 切对话交接卡
@@ -764,11 +823,13 @@ graph TD
1. 先读顺序:`3.2` 服务层、`4.1` 阶段总览、`4.3` Outbox v2、`4.4` llm-service、`4.5` rag-service、`4.6` user/auth、`4.7` notification、`4.8` active-scheduler、`4.10` agent / memory、`6.5` 切对话锚点、`6.7` 典型用例、`6.8` 最终关系图、`6.10` 启动方式、`6.11` 测试自动化、`6.12` 多代理执行闭环。
2. 已冻结的终态是 `Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层`
3. 已冻结的基础设施服务是 `llm-service``rag-service`,其中 `rag-service` 只依赖 `llm-service`
4. 已冻结的业务服务优先级是 `user/auth -> notification -> active-scheduler -> schedule/task/course/task-class -> agent/memory`,其中 `notification``active-scheduler` 要回到更像 seckill 的服务内单体壳
5. `shared` 只保留跨进程契约和少量跨服务底座不承载业务逻辑、DAO、模型或状态机
6. 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构
7. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构
3. 阶段 1 已完成,当前 outbox 基线是服务级表、服务级 topic、服务级 consumer groupworker 仍在单体内装配,后续随对应服务迁出
4. 已冻结的基础设施服务是 `llm-service``rag-service`,其中 `rag-service` 只依赖 `llm-service`
5. 下一轮默认从阶段 1.5 / 1.6 继续,先抽 `llm-service``rag-service`;业务服务优先级仍是 `user/auth -> notification -> active-scheduler -> schedule/task/course/task-class -> agent/memory`
6. `notification``active-scheduler` 后续要回到更像 seckill 的服务内单体壳
7. `shared` 只保留跨进程契约和少量跨服务底座不承载业务逻辑、DAO、模型或状态机
8. 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构。
9. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是 `backend/cmd/api``backend/cmd/worker``backend/cmd/all`,它们只是当前仓库的启动壳,不是终态。终态仍然是“一个服务一个独立 `main.go`”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。
### 6.10 启动方式与进程模型
@@ -884,6 +945,27 @@ graph TD
---
### 6.13 阶段 0 历史基线与阶段 1 当前基线快照
阶段 0 历史基线:
1. 阶段 0 时,仓库入口是 `backend/cmd/api``backend/cmd/worker``backend/cmd/all``backend/main.go` 只保留兼容壳,不作为终态入口。
2. 阶段 0 时Gin 路由集中在 `/api/v1/...` 下,主要覆盖 `user``task``course``task-class``schedule``agent``memory``active-schedule``notification`
3. 阶段 0 时outbox 还是共享单表 + 单 topic / 单 group 形态;阶段 0 只冻结 envelope、版本号、handler 路由和消费边界,不扩 topic。
4. 阶段 0 时,`notification``active-scheduler``memory``agent` 已有单体内服务化雏形;`user/auth` 仍停留在用户域服务层能力,尚未独立成终态服务。
5. 阶段 0 只做语义冻结和基线确认,不切服务,不搬 DAO / model不重排目录只把运行入口、事件契约、路由清单和 outbox 语义定住,作为 Outbox v2 的历史基线。
阶段 1 当前基线:
1. 当前运行入口仍是 `backend/cmd/api``backend/cmd/worker``backend/cmd/all`,但 worker 已按服务装配多条 outbox 链路。
2. 当前 outbox 已切成服务级表:`agent_outbox_messages``task_outbox_messages``memory_outbox_messages``active_scheduler_outbox_messages``notification_outbox_messages`
3. 当前 Kafka 已切成服务级 topic`smartflow.agent.outbox``smartflow.task.outbox``smartflow.memory.outbox``smartflow.active-scheduler.outbox``smartflow.notification.outbox`
4. 当前消费侧已切成服务级 consumer group`smartflow-agent-outbox-consumer``smartflow-task-outbox-consumer``smartflow-memory-outbox-consumer``smartflow-active-scheduler-outbox-consumer``smartflow-notification-outbox-consumer`
5. 当前仍是单体内多 worker 装配,不代表服务已经物理拆出;后续拆服务时,要把对应 outbox 表、relay、topic、consumer group 和 handler 一起迁到服务进程里。
6. `agent_outbox_messages` 沿用历史默认表名作为 `agent` 服务 outbox 表,历史存量不代表新事件仍使用共享 outbox。
---
## 7. 风险与回退
### 风险 1消息路由不清
@@ -896,9 +978,9 @@ graph TD
回退:
1. 先把 topic 再切细
2. 先用服务级 consumer group 隔离
3. 保留 `all` 模式兜底
1. 不回退到共享 topic先冻结新增事件补齐 `event_type -> service` 路由和 service catalog 配置
2. 如果某个服务误消费,先暂停对应服务 worker 或 consumer group,不影响其他服务 group
3. 保留 `all` 模式作为本地运行兜底,但 `all` 内部仍按服务级 worker 装配
### 风险 2relay 并行过早
@@ -926,3 +1008,17 @@ graph TD
1. 保留当前单体实现。
2. 只做接口和契约前置拆分。
3. 不提前拆 schedule/task。
### 风险 4历史 outbox 存量被误判
表现:
1. `agent_outbox_messages` 里存在共享 outbox 时代的旧数据,看起来像所有事件仍写回 `agent`
2. 后续代理误以为阶段 1 没有完成,又重复设计共享表迁移方案。
3. 清理历史数据时误删仍有业务价值的审计记录。
处理:
1. 以新事件写入表、topic 和 consumer group 为准判断当前链路,不用旧存量判断新路由。
2. 历史数据清理或归档单独立项,不和服务拆分混在同一轮做。
3. 如需清理本地测试存量,必须遵守 `6.11` 的数据库操作边界,破坏性操作单独确认。