Files
smartmate/docs/backend/微服务四步迁移与第二阶段并行开发计划.md
Losita 7d21b6516f Version: 0.9.56.dev.260429
后端:
1. 启动层完成第一轮运行边界拆分,新增 `all / api / worker` 三种进程模式:`all` 保持原单体行为,`api` 只启动 Gin 与同步业务依赖,`worker` 只启动 outbox、Kafka consumer 与 memory worker。
2. 启动装配从单个入口拆成 runtime 依赖图,配置、DB、Redis、RAG、memory、DAO、Service、Handler、newAgent 依赖统一集中构造,再按进程角色选择启动 HTTP 或后台循环。
3. outbox 事件总线补齐 dispatch / consume 分离启动能力,支持后续 relay 与 consumer 独立进程化,同时保留原组合启动语义。
4. 核心 outbox handler 注册收口为公共接线入口,统一校验依赖并复用注册顺序,避免 api / worker / all 多入口复制事件注册逻辑。

迁移说明:
5. 本轮只迁运行边界,不拆业务服务边界;旧单体入口仍保留并默认走 `all` 兼容模式,当前切流点是 API 不再消费异步事件,worker 承担后台消费与 memory 任务。
6. 补充微服务四步迁移与第二阶段并行开发计划,明确先拆 API/Worker,再接主动调度与飞书通知,后续再拆 notification、active-scheduler、schedule/task。
2026-04-29 17:44:42 +08:00

558 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 微服务四步迁移与第二阶段并行开发计划
## 1. 文档目的
本文档回答两个问题:
1. 当前 Gin 单体如何平滑演进到“同仓库、多 Go module、分别启动”的微服务形态。
2. 第二阶段 `to5.1` 主动调度闭环应在什么时间点介入,才能与底座迁移并行推进,避免互相干扰。
本轮迁移的核心原则是:**先拆运行边界,再拆服务边界,最后再拆数据所有权**。
也就是说,第一轮不追求一步到位变成完整微服务,而是先把现在所有能力都挤在 `cmd.Start()` 里的问题解决掉,让 API、Worker、通知、主动调度后续可以各走各的启动链路。
---
## 2. 最终目标形态
项目保持一个 GitHub 仓库,但后端逐步演进为多个可独立构建、独立启动、独立部署的 Go module。
推荐最终目录形态如下:
```text
SmartFlow-Agent/
frontend/
package.json
src/
backend/
apps/
api/ # Gin BFF / API Gateway
go.mod
cmd/
api/
main.go
internal/
services/
notification/ # go-zero 通知服务,飞书优先落地
go.mod
cmd/
internal/
active-scheduler/ # 主动调度服务/worker
go.mod
cmd/
internal/
schedule/ # 正式日程应用服务,后期拆
go.mod
cmd/
internal/
task/ # 四象限任务服务,后期拆
go.mod
cmd/
internal/
workers/
outbox-worker/ # outbox relay + Kafka consumer
go.mod
cmd/
internal/
agent-worker/ # 后续 agent 重计算/主动优化 worker
go.mod
cmd/
internal/
shared/
proto/ # RPC 契约
events/ # Kafka 事件契约、JSON 示例、版本说明
packages/ # 极少量稳定公共库
deploy/
docs/
```
但这只是最终形态,不是第一轮要直接改成这样。
迁移期建议先保持当前 `backend/go.mod`,只在现有模块内拆出多启动入口。等第一轮稳定后,再把 `notification` 作为第一个独立 Go module 放到 `backend/services/notification`
---
## 3. 四步走总览
### 第一步:拆运行边界,但保持业务行为不变
目标:把现在的 Gin 单体拆成同 Go module 内的多启动入口。
建议目录:
```text
backend/
go.mod
cmd/
api/
main.go
worker/
main.go
all/
main.go
internal/app/ # 或 cmd/app承载启动装配函数
```
启动角色:
```text
api
启动 Gin、鉴权、SSE、查询接口、用户确认接口、事件发布能力。
迁移第一阶段仍保留 API 所需的同步 service/dao不是最终纯网关。
worker
只启动 outbox relay、Kafka consumer、事件 handler、memory worker。
all
保持当前单体行为,用于本地开发和迁移期兜底。
```
这一阶段必须保证:
1. `all` 模式行为与当前 `cmd.Start()` 一致。
2. `api` 进程保留现有同步业务依赖,但不注册业务消费者,不启动 `memoryModule.StartWorker()`
3. `worker` 进程不注册 Gin 路由,不占用 HTTP 端口。
4. 所有现有 API 路径、响应格式、缓存策略、数据库写入语义不变。
5. 只改启动装配,不改主动调度业务逻辑。
这一阶段完成后,第二阶段新功能就可以优先挂到 worker 或事件链路,而不是继续塞进 Gin 主进程。
### 第二步:建立事件契约、主动调度闭环 MVP 与飞书触达
目标:让“四象限任务池 + 课表时间轴”进入同一个调度闭环,并通过飞书主动触达用户。
关键修正:
1. 主动调度不是等用户打开聊天后才触发,而是由后台 worker 定时/事件驱动触发。
2. 飞书不是后置锦上添花,而是本周期主动出击闭环的第一版触达渠道。
3. 第一版飞书只负责通知用户“系统发现问题并生成了建议”,不承载复杂调度判断,也不直接改正式日程。
主动调度闭环 MVP
```text
后台监控/事件触发
-> 读取四象限任务
-> 读取课表/日程空闲时间
-> 生成局部调整建议
-> 附带 Reasoning
-> 写入对比预览
-> 发布 notification.feishu.requested
-> 飞书提醒用户回到系统确认
-> 用户按变更项确认
-> 确认后应用
```
建议新增事件契约:
```text
active_schedule.triggered
schedule.preview.generated
schedule.apply.requested
schedule.apply.succeeded
schedule.apply.failed
notification.feishu.requested
```
这一阶段主动调度可以仍在 `backend` 模块内实现,但要按未来服务边界写:
1. API 只负责测试触发、查询预览、用户确认和正式应用入口。
2. Worker 负责后台监控、消费触发事件、生成建议、写预览、发布飞书通知事件。
3. 正式应用仍先复用当前强一致落库链路,不拆成远程服务。
4. 飞书第一版可以先在 `backend` worker 内实现 webhook/provider后续再迁出到独立 notification 服务。
### 第三步:拆出第一个独立 Go modulenotification
目标:把第二步里先落在 `backend` worker 内的飞书通知,演进为第一个真正服务化模块。
推荐目录:
```text
backend/
services/
notification/
go.mod
cmd/
notification/
main.go
internal/
consumer/
domain/
repo/
provider/
feishu/
```
推荐技术选型:
1. 使用 go-zero 作为服务工程框架。
2. 通过 Kafka 消费 `notification.feishu.requested`
3. 若需要同步管理接口,再补 go-zero API 或 RPC。
通知服务必须有自己的投递模型,不能只依赖 outbox 的 `consumed` 状态。
建议新增通知记录表或等价存储:
```text
notification_records
id
user_id
channel
biz_type
biz_id
idempotency_key
status
retry_count
last_error
requested_at
sent_at
```
飞书发送链路:
```text
notification.feishu.requested
-> 写入/读取 notification_records
-> 幂等判断
-> 调用飞书 provider
-> 记录 sent / failed / retry
```
这一阶段完成后,飞书挂了不会影响 Gin API也不会影响主动调度生成预览。
> 说明:第二步允许先做简版飞书 webhook是为了尽快跑通“后台主动发现 -> 飞书触达 -> 用户回系统确认”的产品闭环。第三步再把通知投递模型、失败补偿、幂等记录独立出来,避免第一轮就被完整 notification 平台拖慢。
### 第四步:逐步拆主动调度、日程、任务服务
目标:在业务边界稳定后,再拆核心服务。
推荐顺序:
```text
active-scheduler
-> schedule
-> task
```
`active-scheduler` 的前提:
1. 主动调度输入输出 DTO 稳定。
2. Reasoning 结构稳定。
3. 预览生成和确认协议稳定。
4. 调度触发事件稳定。
`schedule` 的前提:
1. 正式日程应用命令契约稳定。
2. 已明确 `schedule_events``schedules``task_items.embedded_time` 的一致性边界。
3. 有 apply id / idempotency key。
4. 有冲突失败回执和回滚策略。
`task` 的前提:
1. 四象限任务池语义稳定。
2. DDL、完成状态、优先级平移规则稳定。
3. 任务表所有权明确。
在这些前提未满足之前,不建议把 `schedule``task` 强行拆成独立服务,否则容易变成分布式单体。
---
## 4. 第二阶段功能如何并行介入
第二阶段 `to5.1` 的业务目标是让四象限任务和课表联通,不再是两个孤岛。
建议按以下时间点介入。
### 介入点 A第一步完成后开始“后台主动调度 + 飞书触达”MVP
`api / worker / all` 三种启动角色跑通后,就可以开始主动调度 MVP。
这一版不要只做“用户打开聊天后触发”,而是要把后台触发作为主链路,把 API 测试接口作为调试入口。
此时你可以开发:
1. 主动调度后台触发器。
2. 主动调度 dry-run / trigger 测试接口。
3. `mock_now` 时间注入。
4. 任务未完成 / 用户反馈很累 / DDL 临近 的触发 payload。
5. 写入对比预览。
6. 发布 `notification.feishu.requested`
7. 简版飞书 webhook/provider通知用户回系统确认。
不建议此时开发:
1. 飞书内直接确认/改日程。
2. 飞书作为完整 agent 聊天入口。
3. DDL 全自动插空的复杂版本。
4. 大范围全局重排。
5. 独立 schedule/task 微服务。
原因:这一阶段的目标是验证“后台主动发现 -> 生成建议预览 -> 飞书触达 -> 用户回系统确认”的闭环,而不是一次性做完所有渠道与触发场景。
### 介入点 B飞书触达可用后预埋飞书聊天链路
当飞书 webhook 通知可用后,可以提前预埋下周期“飞书接入 agent 聊天链路”的边界。
下周期目标类似“飞书内直接和 agent 对话”,但本周期只埋以下能力:
1. `channel` 字段:区分 `web``feishu`、未来移动端等入口。
2. `external_user_id` / `open_id` 映射:把飞书用户身份映射到系统用户。
3. `external_conversation_id` 映射:把飞书会话映射到系统 `conversation_id`
4. 入站消息 DTO把飞书消息统一转换成 `AgentInboundMessage`
5. 出站消息 DTO把 agent 回复统一转换成 `AgentOutboundMessage`
6. 幂等键:飞书 message id 需要映射为入站消息幂等键,避免重复回调导致重复对话。
本周期不做:
1. 飞书内完整多轮 Agent Chat。
2. 飞书内复杂确认卡片。
3. 飞书内直接应用日程。
4. 移动端替代方案。
推荐预埋事件:
```text
agent.channel.message.received
agent.channel.reply.requested
```
飞书主动通知仍然走:
```text
notification.feishu.requested
```
也就是说,**通知通道** 和 **聊天通道** 从事件名和 DTO 上就要分开,避免下周期把飞书消息接入时污染 notification 逻辑。
### 介入点 C主动调度 MVP 稳定后,扩充更多主动触发源
此时可以扩展:
1. DDL 临近插入课表空余时间。
2. 任务未完成监控。
3. 用户反馈很累后的局部微调。
4. 提前完成后的碎片任务建议。
5. 休息/发呆选项。
所有触发源都应该进入同一个主动调度入口:
```text
active_schedule.triggered
```
不要每个触发源各写一套调度逻辑。
### 介入点 D飞书通知模型稳定后拆 notification 独立 module
当飞书通知从“能发 webhook”升级为“有幂等、有记录、有失败补偿”后再把它从 `backend` 中迁到:
```text
backend/services/notification
```
迁移时要保持:
1. 主动调度 worker 只发布 `notification.feishu.requested`,不直接依赖飞书 SDK 或 webhook 细节。
2. notification 服务负责通知记录、幂等、重试、provider 调用。
3. 飞书聊天链路如果下周期启动,应单独走 `agent.channel.*` 事件,不混入 notification 投递模型。
### 介入点 E主动调度协议稳定后再拆 active-scheduler 独立 module
当主动调度闭环稳定后,再把它从 `backend` 中迁到:
```text
backend/services/active-scheduler
```
迁移时要保持:
1. API 仍只发布事件和查询预览。
2. active-scheduler 只负责建议生成和预览。
3. 正式日程应用仍通过稳定命令或事件进入 schedule 域。
---
## 4.1 为下周期飞书 Agent 聊天链路预埋
如果后续暂缓移动端开发,把飞书作为轻量移动入口,那么本周期接飞书时要提前避免两个误区:
1. 不要把飞书通知写成只会发送固定文案的死逻辑。
2. 不要把飞书聊天直接塞进 notification handler。
建议从本周期就区分三层:
```text
notification
负责主动通知,例如“我发现你的今晚安排可能过载,已生成一版建议”。
channel adapter
负责不同渠道的入站/出站消息适配,例如 web、feishu、未来移动端。
agent conversation
负责真正的 agent 多轮对话、上下文、工具调用、确认卡片。
```
飞书作为聊天入口时,推荐链路是:
```text
飞书回调
-> feishu channel adapter
-> 校验签名/解密/去重
-> 映射系统 user_id 与 conversation_id
-> 发布 agent.channel.message.received
-> agent worker / agent service 处理
-> 发布 agent.channel.reply.requested
-> feishu channel adapter 发送回复
```
本周期最小预埋:
1. 配置层预留 `feishu.enabled``feishu.webhook``feishu.appID``feishu.appSecret` 等字段位置。
2. DTO 层预留 `Channel``ExternalUserID``ExternalConversationID``MessageID``IdempotencyKey`
3. 事件层预留 `agent.channel.message.received``agent.channel.reply.requested`
4. 数据层先不强行建全量飞书会话表;若需要,只建轻量映射表或先通过 Redis/配置映射过渡。
这样下周期即使做“飞书内直接聊 agent”也不会推翻本周期的飞书通知实现。
---
## 5. 并行开发分工建议
### Codex 优先负责
1.`cmd.Start()`,抽启动装配层。
2. 新增 `api / worker / all` 启动入口。
3. 把 outbox relay、Kafka consumer、memory worker 从 API 入口移走。
4. 为 notification 服务预留事件契约和目录位置。
5. 补迁移文档和启动说明。
### 业务开发优先负责
1. 定义主动调度 MVP 的产品语义。
2. 明确 Reasoning 展示格式。
3. 明确用户确认粒度。
4. 设计后台触发、dry-run、trigger 测试场景。
5. 梳理哪些任务可被退回任务池,哪些必须保护。
6. 明确飞书通知文案与“回系统确认”的跳转语义。
### 双方交汇点
交汇点只放在事件契约和 DTO 上。
建议先稳定以下结构:
```text
ActiveScheduleTrigger
ActiveScheduleSuggestion
ActiveScheduleChangeItem
ActiveScheduleReasoning
SchedulePreviewVersion
NotificationRequested
AgentInboundMessage
AgentOutboundMessage
```
只要这些结构稳定,底层是否已经拆成独立 module不影响你继续开发业务。
---
## 6. 每一步的验收标准
### 第一步验收:启动边界
1. `all` 模式与当前单体行为一致。
2. `api` 模式能启动 Gin并能访问现有接口。
3. `worker` 模式能启动 outbox 和 memory worker不启动 HTTP。
4. API 进程发布的 outbox 消息能被 worker 消费。
5. 停掉 worker 时API 仍可启动,只是异步能力延迟执行。
### 第二步验收:主动调度 MVP
1. worker 能后台触发主动调度。
2. 测试接口能触发同一条主动调度链路。
3. 能传入 `mock_now`
4. 能读取四象限任务和课表空余时间。
5. 能生成调整建议。
6. 每个建议都有 Reasoning。
7. 结果写入对比预览,不直接落库。
8. 能发布并发送飞书通知,提醒用户回系统确认。
### 第三步验收:飞书通知服务
1. `notification.feishu.requested` 能被独立服务消费。
2. 有通知幂等键。
3. 有通知发送记录。
4. 飞书失败可重试。
5. 飞书服务停止不影响 API 和主动调度预览生成。
### 第四步验收:核心服务化
1. active-scheduler 可独立启动。
2. schedule/task 的数据所有权逐步明确。
3. API 不再直接承担核心调度重计算。
4. 服务间通信只通过 RPC / Kafka / 明确契约完成。
---
## 7. 风险与回退策略
### 风险 1启动拆分后本地开发变复杂
回退策略:
1. 保留 `all` 模式。
2. 本地默认仍可一键启动。
3. 只有联调 worker / 飞书时才分进程启动。
### 风险 2API 不启动消费者后 outbox 堆积
回退策略:
1. 本地使用 `all` 模式。
2. 联调环境明确启动 worker。
3. 增加 outbox 堆积观测或临时查询 SQL。
### 风险 3多个 worker 重复投递
回退策略:
1. 第一阶段只启动一个 worker。
2. 后续再补 outbox claim / processing 状态。
3. 外部通知必须有业务幂等键。
### 风险 4过早拆 schedule/task 导致分布式单体
回退策略:
1. 第一阶段不拆正式日程落库。
2. 第二阶段只拆主动建议生成。
3. schedule/task 等数据所有权稳定后再拆。
---
## 8. 推荐近期执行顺序
近期建议按以下顺序推进:
1. Codex 先完成启动边界拆分:`api / worker / all`
2. 业务侧同步定义主动调度 MVP 的 DTO 与 Reasoning 格式。
3.`backend` worker 内实现后台主动触发器,并保留 dry-run / trigger 测试接口。
4. 主动调度结果先写现有对比预览。
5. 增加 `notification.feishu.requested` 事件,并实现简版飞书 webhook/provider。
6. 预留 `agent.channel.*` 事件和 channel DTO为下周期飞书聊天链路做准备。
7. 飞书通知模型稳定后,新建 `backend/services/notification`,使用 go-zero 落地独立通知服务。
8. 主动调度稳定后,再考虑拆 `active-scheduler`
一句话总结:
> 先让项目从“所有能力绑在一个 Gin 单体进程”变成“API 与 Worker 分开跑”;再让第二阶段主动调度闭环挂到 Worker 与事件契约上;最后把飞书通知作为第一个独立 Go module 服务拆出去。