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

17 KiB
Raw Blame History

微服务四步迁移与第二阶段并行开发计划

1. 文档目的

本文档回答两个问题:

  1. 当前 Gin 单体如何平滑演进到“同仓库、多 Go module、分别启动”的微服务形态。
  2. 第二阶段 to5.1 主动调度闭环应在什么时间点介入,才能与底座迁移并行推进,避免互相干扰。

本轮迁移的核心原则是:先拆运行边界,再拆服务边界,最后再拆数据所有权

也就是说,第一轮不追求一步到位变成完整微服务,而是先把现在所有能力都挤在 cmd.Start() 里的问题解决掉,让 API、Worker、通知、主动调度后续可以各走各的启动链路。


2. 最终目标形态

项目保持一个 GitHub 仓库,但后端逐步演进为多个可独立构建、独立启动、独立部署的 Go module。

推荐最终目录形态如下:

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 内的多启动入口。

建议目录:

backend/
  go.mod
  cmd/
    api/
      main.go
    worker/
      main.go
    all/
      main.go
  internal/app/                    # 或 cmd/app承载启动装配函数

启动角色:

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

后台监控/事件触发
  -> 读取四象限任务
  -> 读取课表/日程空闲时间
  -> 生成局部调整建议
  -> 附带 Reasoning
  -> 写入对比预览
  -> 发布 notification.feishu.requested
  -> 飞书提醒用户回到系统确认
  -> 用户按变更项确认
  -> 确认后应用

建议新增事件契约:

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 内的飞书通知,演进为第一个真正服务化模块。

推荐目录:

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 状态。

建议新增通知记录表或等价存储:

notification_records
  id
  user_id
  channel
  biz_type
  biz_id
  idempotency_key
  status
  retry_count
  last_error
  requested_at
  sent_at

飞书发送链路:

notification.feishu.requested
  -> 写入/读取 notification_records
  -> 幂等判断
  -> 调用飞书 provider
  -> 记录 sent / failed / retry

这一阶段完成后,飞书挂了不会影响 Gin API也不会影响主动调度生成预览。

说明:第二步允许先做简版飞书 webhook是为了尽快跑通“后台主动发现 -> 飞书触达 -> 用户回系统确认”的产品闭环。第三步再把通知投递模型、失败补偿、幂等记录独立出来,避免第一轮就被完整 notification 平台拖慢。

第四步:逐步拆主动调度、日程、任务服务

目标:在业务边界稳定后,再拆核心服务。

推荐顺序:

active-scheduler
  -> schedule
  -> task

active-scheduler 的前提:

  1. 主动调度输入输出 DTO 稳定。
  2. Reasoning 结构稳定。
  3. 预览生成和确认协议稳定。
  4. 调度触发事件稳定。

schedule 的前提:

  1. 正式日程应用命令契约稳定。
  2. 已明确 schedule_eventsschedulestask_items.embedded_time 的一致性边界。
  3. 有 apply id / idempotency key。
  4. 有冲突失败回执和回滚策略。

task 的前提:

  1. 四象限任务池语义稳定。
  2. DDL、完成状态、优先级平移规则稳定。
  3. 任务表所有权明确。

在这些前提未满足之前,不建议把 scheduletask 强行拆成独立服务,否则容易变成分布式单体。


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 字段:区分 webfeishu、未来移动端等入口。
  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. 移动端替代方案。

推荐预埋事件:

agent.channel.message.received
agent.channel.reply.requested

飞书主动通知仍然走:

notification.feishu.requested

也就是说,通知通道聊天通道 从事件名和 DTO 上就要分开,避免下周期把飞书消息接入时污染 notification 逻辑。

介入点 C主动调度 MVP 稳定后,扩充更多主动触发源

此时可以扩展:

  1. DDL 临近插入课表空余时间。
  2. 任务未完成监控。
  3. 用户反馈很累后的局部微调。
  4. 提前完成后的碎片任务建议。
  5. 休息/发呆选项。

所有触发源都应该进入同一个主动调度入口:

active_schedule.triggered

不要每个触发源各写一套调度逻辑。

介入点 D飞书通知模型稳定后拆 notification 独立 module

当飞书通知从“能发 webhook”升级为“有幂等、有记录、有失败补偿”后再把它从 backend 中迁到:

backend/services/notification

迁移时要保持:

  1. 主动调度 worker 只发布 notification.feishu.requested,不直接依赖飞书 SDK 或 webhook 细节。
  2. notification 服务负责通知记录、幂等、重试、provider 调用。
  3. 飞书聊天链路如果下周期启动,应单独走 agent.channel.* 事件,不混入 notification 投递模型。

介入点 E主动调度协议稳定后再拆 active-scheduler 独立 module

当主动调度闭环稳定后,再把它从 backend 中迁到:

backend/services/active-scheduler

迁移时要保持:

  1. API 仍只发布事件和查询预览。
  2. active-scheduler 只负责建议生成和预览。
  3. 正式日程应用仍通过稳定命令或事件进入 schedule 域。

4.1 为下周期飞书 Agent 聊天链路预埋

如果后续暂缓移动端开发,把飞书作为轻量移动入口,那么本周期接飞书时要提前避免两个误区:

  1. 不要把飞书通知写成只会发送固定文案的死逻辑。
  2. 不要把飞书聊天直接塞进 notification handler。

建议从本周期就区分三层:

notification
  负责主动通知,例如“我发现你的今晚安排可能过载,已生成一版建议”。

channel adapter
  负责不同渠道的入站/出站消息适配,例如 web、feishu、未来移动端。

agent conversation
  负责真正的 agent 多轮对话、上下文、工具调用、确认卡片。

飞书作为聊天入口时,推荐链路是:

飞书回调
  -> feishu channel adapter
  -> 校验签名/解密/去重
  -> 映射系统 user_id 与 conversation_id
  -> 发布 agent.channel.message.received
  -> agent worker / agent service 处理
  -> 发布 agent.channel.reply.requested
  -> feishu channel adapter 发送回复

本周期最小预埋:

  1. 配置层预留 feishu.enabledfeishu.webhookfeishu.appIDfeishu.appSecret 等字段位置。
  2. DTO 层预留 ChannelExternalUserIDExternalConversationIDMessageIDIdempotencyKey
  3. 事件层预留 agent.channel.message.receivedagent.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 上。

建议先稳定以下结构:

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 服务拆出去。