后端: 1.阶段 5 course 服务边界落地 - 新增 cmd/course 独立进程入口,落地 services/course dao/rpc/sv - 新增 gateway/client/course、shared/contracts/course 和 shared/ports course port - 将 /api/v1/course/* HTTP 门面切到 course zrpc,gateway 只保留鉴权、限流、幂等、文件读取和响应透传 - 保留 course 迁移期直写 schedule_events / schedules 权限,维持课程导入两个表同事务写入语义 - 为 course parse-image 补 bytes RPC 契约和 gRPC 消息大小配置,兼容课表图片上传 - 补充 course.rpc 示例配置与阶段 5 文档基线、切流点、残留依赖和 smoke 记录
79 KiB
微服务迁移与 Gin Gateway + gozero 演进计划
1. 文档定位
这份文档是当前后端迁移的主总纲,目标是把阶段 0 的“单体 Gin + 统一 outbox”基线,继续平滑演进到“Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层”。
当前进度口径:
- 阶段 0 已完成:运行入口、事件契约、路由清单和 outbox 语义已经冻结。
- 阶段 1 已完成:当前基线已经切成服务级 outbox 表、服务级 Kafka topic、服务级 consumer group;仍在单体进程内装配多个服务级 worker,后续拆微服务时再物理迁出。
- 阶段 1.5 / 1.6 已完成:
backend/services/llm和backend/services/rag已经是当前 canonical 入口,backend/infra/llm和backend/infra/rag的.go旧实现已删除。 - 阶段 2 已完成:
user/auth已经从 Gin 单体抽成cmd/userauth+services/userauth的 go-zero zrpc 服务边界,gateway 只保留 user HTTP 入口、鉴权、额度门禁和轻量转发。 - 阶段 3
notification服务化已完成实现、code review 修复和真实 smoke;不要再把 outbox、llm-service、rag-service 或 user/auth 当成未完成待办。 - 阶段 4
active-scheduler服务化已完成首轮收口:独立进程、zrpc、服务级 outbox consumer / relay / retry loop、gateway 门面和代码归属已经切到新边界。
本计划遵守两个硬原则:
-
业务语义不变。
预览、确认、通知、任务、日程的用户可见行为尽量不变,先改边界、再改归属、最后改运行形态。 -
并行迁移,不一步到位。
允许新旧实现并存,先迁移、再切流、再验证、最后删除旧实现。
2. 现状判断
当前代码已经具备迁移基础:
api / worker / all三种启动边界已经存在。- 主动调度已经跑通
trigger -> dry-run -> preview -> notification闭环。 notification_records已经有独立状态机、幂等、重试能力。- 事件契约已经拆成
active_schedule.triggered、notification.feishu.requested、schedule.apply.*等独立 payload。
阶段 1 之后,当前 outbox 已经不是共享单表 / 单 topic / 单 group 形态:
agent、task、memory、active-scheduler、notification已经有各自的 outbox 表。- 每个服务都有自己的 relay worker,把本服务 outbox 表投递到本服务 Kafka topic。
- Kafka 仍是共享运输层,但 topic 已按服务切开,不再把新流量写进共享 topic。
- 消费侧已经按服务 consumer group 隔离,不再用一个 worker 吃全部事件。
- 当前仍是单体进程内多 worker 装配;worker 后续会跟随对应服务一起迁出,不在阶段 1 直接拆进程。
阶段 1.5 / 1.6 也已经先落地完毕:backend/services/llm 和 backend/services/rag 已经成为当前 canonical 入口,backend/infra/llm 和 backend/infra/rag 的 .go 旧实现已删除,仅保留迁移说明文档。当前仍然是单体进程内多 worker 装配,llm / rag 先完成服务化收口,还没有进入 gozero 进程拆分。
阶段 2 也已经落地完毕:user/auth 已经是当前阶段的样板服务。cmd/all 不再内嵌 userauth,完整本地系统需要同时启动 cmd/all 和 cmd/userauth。cmd/all 只迁单体残留域,cmd/userauth 自己迁 users 和 user_token_usage_adjustments,MySQL / Redis client 初始化也按服务边界后置到各自服务入口。
所以后续路线不是再补一次 outbox 基建,也不是回头重抽 llm / rag / userauth,而是在当前阶段 2 基线上,继续按服务边界逐个把 gozero 服务、DAO / model / worker 和启动入口迁出去。
3. 终态形态
3.1 网关层
Gin Gateway 只做边缘层职责:
- 鉴权。
- 路由聚合。
- 请求编排。
- SSE / 流式返回。
- 前端所需的轻量组合逻辑。
- API 层错误响应适配。迁移期可以继续复用
backend/respond,等全部服务边界稳定后再整体收进 gateway/shared。
网关不再承担这些职责:
- 具体业务状态机。
- 直接写核心业务表。
- 直接消费所有后台事件。
- 直接维护服务内部重试与投递状态。
- 直接维护用户黑名单、JWT 签发、token 额度账本这类 user/auth 内部状态。
当前 gateway 切流点:
/api/v1/user/*由backend/gateway/api/userauth承载 HTTP 入口,核心能力通过backend/gateway/client/userauth调cmd/userauthzrpc。gateway/middleware的 JWT 鉴权和 token quota guard 只调userauth,不直接读写users、Redis 黑名单或额度缓存。notification、active-scheduler等跨服务 zrpc client 统一放在backend/gateway/client/<service>,HTTP 门面统一放在backend/gateway/api。- zrpc client 不放进
cmd。cmd只负责进程入口和装配,不承载跨服务 client 语义。
3.2 服务层
gozero 服务负责领域能力:
user/auth负责用户注册、登录、刷新、登出、JWT 签发、黑名单与 token 额度治理。course负责课程导入、图片解析、课表校验与课程落表。task-class负责任务类别、条目维护与批量入表。notification负责通知投递。active-scheduler负责主动调度建议生成和预览。schedule负责正式日程所有权。task负责任务池和任务状态所有权。agent负责聊天路由、计划/执行编排、工具接入、SSE 输出与会话生命周期。memory负责记忆抽取、检索、管理、用户记忆设置和异步 worker。llm-service负责统一模型调用、provider 路由、流式输出、重试、限流与审计。rag-service负责统一向量化、召回、重排、向量库读写与语料适配,依赖llm-service。
说明:
agent和memory都可以单独成服务,不应再被写成“公共能力”;其中agent更像对外对话编排服务,memory更像其支撑服务/worker 服务。说明:
llm-service先抽成全仓统一模型出口,rag-service再抽成检索基础设施服务;rag-service只能依赖llm-service,不反向依赖具体业务服务。当前状态:
llm-service/rag-service这两个边界已经先做成backend/services/*的服务内模块,调用仍由backend/cmd/start.go在同一进程内装配,不是 gozero 独立进程。当前状态:
user/auth已经完成 go-zero zrpc 独立进程拆分,是阶段 2 样板。服务端在backend/services/userauth,进程入口在backend/cmd/userauth,gateway HTTP 门面在backend/gateway/api/userauth,gateway client 在backend/gateway/client/userauth。当前状态:
notification已经完成阶段 3 拆分。服务端在backend/services/notification,进程入口在backend/cmd/notification,gateway client 在backend/gateway/client/notification,服务级 outbox consumer 和 retry loop 已随服务入口迁出。当前状态:
active-scheduler已经完成阶段 4 首轮收口。服务端在backend/services/active_scheduler,进程入口在backend/cmd/active-scheduler,gateway HTTP 门面在backend/gateway/api,gateway client 在backend/gateway/client/activescheduler。
3.3 事件层
- 每个服务拥有自己的 outbox。
- 每个服务有自己的 relay worker。
- Kafka 作为共享运输层,但业务 topic 不共享。
- 同一服务的多个实例进入同一个 consumer group,做横向负载均衡。
- topic 按服务/事件域划分,消费侧按服务 consumer group 隔离。
- 新增事件必须登记
event_type -> service路由,再由服务 catalog 映射到对应 outbox 表、topic 和 consumer group。 - 未知事件不能靠“写进共享表再由全局 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 共享层边界
shared只放跨进程、跨服务都要认识的契约,不放某个服务自己的业务逻辑。- 当前仓库里的
backend/shared/events就是典型用法:事件类型、payload、版本字段。 - 如果未来确实出现多个服务共同依赖的 DTO、枚举或错误码,可以放到
shared/contracts或shared/types,但前提是它们不依赖数据库,也不依赖某个服务的状态机。 - 不要把
dao、model、service、handler这类服务私有代码塞进shared;它们应该跟随各自服务归属。 infra也不应该是一个大公共篮子:像kafka、outbox这类跨服务底座可以放到shared/infra;llm-service、rag-service这类模型与检索能力要单独成基础设施服务,不要塞进shared;prompt、tooling这类强业务依赖的适配器则应跟着具体服务走。- 换句话说,
shared是“跨进程契约层 + 少量跨服务底座”,不是“公共业务层”。 - 阶段 2 已经新增
backend/shared/contracts/userauth和backend/shared/ports,只承载跨层契约和端口接口;user/auth 的 JWT、DAO、额度治理、黑名单实现不进入shared。 - 阶段 3 已经新增
backend/shared/contracts/notification,只承载 notification 跨层 DTO;通知通道 DAO、投递状态机、provider、重试策略和 outbox handler 都留在backend/services/notification。
4. 迁移阶段
4.1 阶段总览
| 阶段 | 目标 | 建议 commit 点 | 建议测试 |
|---|---|---|---|
| 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(已完成) | 已完成,backend/services/llm 作为当前 canonical 入口 |
go test ./... + course / active-scheduler / memory 模型调用 smoke |
| 1.6 | 再抽 rag-service(已完成) | 已完成,backend/services/rag 作为当前 canonical 入口 |
go test ./... + memory retrieve / rerank smoke |
| 2 | 先拆 user/auth(已完成) | 已完成,阶段 2 样板 commit 点:userauth zrpc、gateway userapi、JWT/黑名单/额度治理、启动与迁移边界已收口 | 已完成注册/登录/刷新/并发 refresh/登出/鉴权/token quota smoke |
| 3 | 再拆 notification(已完成) | 已完成,cmd/notification + services/notification zrpc / outbox consumer / retry loop 已收口,旧单体实现已删除;是否 commit 等用户明确要求 |
已完成 notification E2E smoke + worker-only smoke |
| 4 | 再拆 active-scheduler(已完成首轮收口) | 已完成,cmd/active-scheduler + services/active_scheduler zrpc / outbox consumer / retry loop / gateway 门面已收口;是否 commit 等用户明确要求 |
已完成 dry-run / trigger / preview ready smoke;confirm 仍建议单独做写入型 smoke |
| 5 | 再拆 schedule / task / course / task-class | 每个领域完成一次切流就 commit 一次 | schedule/task/course/task-class 回归 + 全链路 smoke |
| 6 | 再拆 agent / memory | agent 编排服务、memory 支撑服务和后台 worker 独立后 commit | agent chat / SSE / memory extract / memory retrieve smoke |
| 7 | Gin Gateway 收口 | 网关不再直接碰核心业务表后 commit | Gateway 路由、鉴权、组合逻辑 smoke |
建议习惯:每完成一个“可回退的切流点”就留一个 commit。
如果一个阶段会跨好几天,尽量拆成“骨架 commit”和“切流 commit”两次保存进度。
4.2 阶段 0:语义冻结和基线确认(已完成)
本节保留为历史回顾,用来说明阶段 0 冻结了什么,不是当前待办。
目标:
- 先不改业务语义。
- 先把当前单体里哪些接口会迁走、哪些事件会保留,列清楚。
- 先把当前
backend当成临时网关壳,而不是未来最终形态。
这一步要做的事:
- 固定对外接口语义。
- 固定事件类型和 DTO 契约。
- 固定当前
api / worker / all的启动行为。 - 固定当前 outbox、llm-service、rag-service、user/auth、course、task-class、notification、active-scheduler 的责任边界。
建议提交点:
- 文档定稿。
- 路由和契约清单定稿。
建议测试:
go test ./...api模式启动 smoke。worker模式启动 smoke。all模式启动 smoke。
4.3 阶段 1:Outbox v2 基建(已完成)
当前结论:
- outbox 已经从“单体内部事件泵”升级成“服务级事件总线能力”。
- 当前基线已经具备
event_type -> service -> outbox 表 / topic / group的服务归属链路。 - 服务还没有物理拆成多个 gozero 进程;目前是在单体内按服务装配多个 relay worker 和 consumer worker。
- 后续拆微服务时,worker 会随对应服务迁出,不再重新设计 outbox 边界。
已完成的事:
- 引入 outbox 服务归属概念,
agent、task、memory、active-scheduler、notification分别对应自己的 outbox 表。 - relay worker 已按服务拆开,每个 relay 只扫描本服务 outbox 表并投递到本服务 topic。
- consumer 已按服务 group 隔离,每个服务只处理归属到自己的事件。
- topic 已经直接切成服务级 topic,不再把“共享 topic”作为当前终态或过渡依赖。
- 事件发布入口已经通过路由和 catalog 决定服务归属,避免新事件默认回流到共享 outbox。
当前 outbox 总线结构:
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"]
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"]
当前切流点:
- 新事件先按
event_type查服务路由,再按服务 catalog 写入对应 outbox 表。 - relay 只处理本服务表里的记录,不再全局扫描一个共享表。
- Kafka topic 已经按服务切开,consumer group 也按服务切开。
agent_outbox_messages仍沿用历史默认表名作为agent服务表;表内旧数据是共享 outbox 时代的历史存量,不代表新流量仍回流agent。
仍保留的旧实现:
backend/cmd/api、backend/cmd/worker、backend/cmd/all仍是当前启动壳,尚未拆成终态的多 gozero 进程。backend/model/outbox.go仍保留兼容模型;实际写入表由 outbox repository 根据服务路由选择。- 既有事件契约和 handler 继续保留,后续按服务迁移时再逐步移动到对应服务目录。
已完成验证:
- 健康检查返回 200。
- MySQL 当前只有服务级 outbox 表,没有继续依赖共享
outbox_messages表。 - Kafka 已存在 5 个服务级 topic 和 5 个服务级 consumer group。
- 最新 smoke 中,
task.urgency.promote.requested写入task_outbox_messages,topic 为smartflow.task.outbox,状态流转到consumed,taskgroup lag 为 0。 - 对应任务优先级从
2更新到1,证明发布、投递、消费和业务处理链路已经闭环。
下一步:
- 不再重复做 Outbox v2 基建。
- 阶段 1.5 / 1.6 和阶段 2 已完成,后续从阶段 3
notification开始推进。 - 迁移任何新服务时,必须复用当前 outbox 路由、服务 catalog、relay 和 consumer group 隔离规则。
4.4 阶段 1.5:先抽 llm-service(已完成)
当前结论:
backend/services/llm已经是当前 canonical 入口。backend/infra/llm的.go旧实现已删除,仅保留迁移说明。- llm 相关调用仍由
backend/cmd/start.go在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。 - 这一段已经不是待办,不要再安排新的 llm 抽离任务。
已完成的事:
- 把全仓统一模型出口从各业务服务里收口到
backend/services/llm。 - 把 provider 路由、流式输出、限流、审计这些共性统一起来。
- 让
course、active-scheduler、memory、agent的模型使用方式先统一,后面再看是否继续做更细的协议抽象。 llm-service只负责模型出口,不负责业务 prompt 状态机、工具编排或领域决策。
已完成验证:
go test ./...通过。- 业务服务调模型链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧
backend/infra/llm。
4.5 阶段 1.6:再抽 rag-service(已完成)
当前结论:
backend/services/rag已经是当前 canonical 入口。backend/infra/rag的.go旧实现已删除,仅保留迁移说明。- rag 相关调用仍由
backend/cmd/start.go在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。 - 这一段已经不是待办,不要再安排新的 rag 抽离任务。
已完成的事:
- 把统一检索基础设施从
memory/agent里收口到backend/services/rag。 - 把向量化、召回、重排、向量库读写统一起来。
- 明确
rag-service只依赖llm-service做 embedding / rerank,不反向依赖业务服务。 - 让
memory退回成记忆管理和编排支撑服务,不再自己持有完整检索基础设施。
已完成验证:
go test ./...通过。- 业务服务检索链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧
backend/infra/rag。
4.6 阶段 2:先拆 user/auth(已完成)
当前结论:
user/auth已经从 Gin 单体拆成 go-zero zrpc 独立服务边界。- gateway 不再直读
users表,也不再维护 JWT 签发、refresh 轮转、黑名单和 token 额度账本。 - 阶段 2 是后续服务迁移的样板:服务端拥有自己的
dao/model/sv/internal/rpc,gateway 只保留 HTTP 边缘入口和 zrpc client。 cmd/all不再内嵌 userauth;完整本地运行需要同时启动cmd/all与cmd/userauth。
已完成的事:
- 新增
backend/cmd/userauth/main.go作为 userauth 独立进程入口。 - 新增
backend/services/userauth/**,承载注册、登录、刷新 token、登出、JWT 签发/校验、黑名单、token 额度治理和 token 记账幂等。 - 新增并归档到
backend/gateway/api/userauth/**,承载/api/v1/user/register、/api/v1/user/login、/api/v1/user/refresh-token、/api/v1/user/logout的 HTTP handler。 - 新增并归档到
backend/gateway/client/userauth/**,承载 gateway 侧 zrpc client 和 gRPC 错误反解。 - 新增
backend/gateway/middleware/**,把 JWT 鉴权和 token quota guard 改成调用 userauth,不再直接碰 users 表或 Redis 黑名单细节。 - 新增
backend/shared/contracts/userauth与backend/shared/ports,只放跨层契约和端口接口。 - 拆分 MySQL / Redis 初始化和 AutoMigrate 边界:
cmd/all走ConnectCoreDB/InitCoreRedis,只迁单体残留域;cmd/userauth自己迁users和user_token_usage_adjustments。 - 清退旧 Gin user/auth 活跃实现:
backend/api/user.go、backend/service/user.go、backend/dao/user.go、backend/model/user.go、backend/model/auth.go、backend/auth/jwt_handler.go、backend/auth/jwt_handler_test.go、backend/middleware/token_handler.go、backend/middleware/token_quota_guard.go、backend/routers/routers.go。
关键修复:
- refresh token 并发重放:使用 Redis
SET NX抢占旧 refresh JTI,保证并发刷新时只有一个请求能签出新 token。 - token 记账幂等:新增
user_token_usage_adjustments,和users.token_usage在同一个 MySQL 事务内提交,避免 outbox 重试或并发重放重复记账。 - 额度边界:
token_usage >= token_limit即拒绝后续高消耗入口,不再多放行一次。 - RPC 错误:服务间错误通过 go-zero / gRPC
error返回,gateway client 反解成项目内respond.Response;非业务内部错误不再原样透给前端。
当前切流点:
- 前端仍访问
/api/v1/user/*。 - gateway 的 user handler 只做参数绑定、调用 userauth client、复用
respond写回前端。 - gateway 鉴权和 quota guard 只依赖
ports.UserAuthClient,不直接依赖 userauth DAO/model。 agent/chat的 token quota 门禁已经通过 userauth 服务判断;会话完成后的 token 记账由 agent 事件处理链路调用 userauthAdjustTokenUsage。
已完成验证:
go test ./...通过,并已按规则清理.gocache。- 注册、重复注册、登录、鉴权接口、刷新 token、旧 refresh 复用拒绝、登出、登出后 access/refresh 拒绝都已通过真实 smoke。
- 并发 refresh smoke:5 个并发请求只有 1 个成功,其余请求被拒绝。
- token quota smoke:临时把 smoke 用户
token_usage打到token_limit后,/agent/chat返回40051 token usage exceeds limit。 - 通过
docker exec确认users落库、Redis 黑名单存在、user_token_usage_adjustments已由cmd/userauth迁移出来。
遗留约定:
respond暂时继续放在backend/respond复用;等全部阶段收尾后再整体收进 gateway/shared。cmd/all单独启动不再覆盖 user/auth 完整能力,后续 smoke 必须同时确认 userauth 已启动。- 不要再把 user/auth 当成后续待办;阶段 3 的接手人应从 notification 开始。
4.7 阶段 3:再拆 notification
目标:
- 把通知投递做成第一条真正独立出来的 gozero 服务。
- 把通知投递和主动调度闭环解耦。
- 验证“服务级 outbox + 服务专属 worker + 独立重试”这套新模式。
这一步要做的事:
- 把 notification 的投递记录、幂等、重试、provider 调用收进 notification 服务,并先按你熟悉的 service 内单体壳收束,避免继续长出新的顶层小包。
- 主动调度 worker 只负责发布
notification.feishu.requested。 - API 只负责通道配置、测试和轻量查询,不直接发真实通知。
- notification 服务只消费自己的通知事件。
- 这一步优先用 gozero 落地。
建议提交点:
- notification 服务可以独立启动时,先 commit。
- notification 的独立消费和重试都稳定后,再 commit。
建议测试:
- notification 服务单独启动 smoke。
- 通知事件端到端 smoke。
- 重试扫描 smoke。
- 停掉 notification 服务后,主动调度预览仍然可用的回归测试。
本轮收口状态(2026-05-04):
cmd/notification已承载 notification zrpc 启动、DB 迁移、服务级 outbox consumer 和重试扫描。backend/services/notification已收进 DAO、model、sv、rpc、飞书 provider 和 outbox handler;gateway 通过backend/gateway/client/notificationzrpc client 调用。- 主动调度侧只写入
notification.feishu.requested,publisher 侧只注册事件归属到notification,不再启动单体 notification consumer。 - 旧
backend/notification、旧 DAO/model 和旧service/events/notification_feishu.go已删除;review 发现的 sending 租约恢复和 RPC timeout 边界已修复。 - 真实 smoke 已通过:
notification_outbox_messages.id=3已从pending推进到consumed,smartflow.notification.outbox已出现outbox_id=3,对应notification_records生成并按未启用通道进入skipped。
4.8 阶段 4:再拆 active-scheduler
目标:
- 把主动调度的“生成建议”能力独立成服务。
- 让 gateway 退成边缘编排层,不再承载核心建议生成。
- 把主动调度的输入输出契约稳定下来。
这一步要做的事:
- 把
trigger -> dry-run -> preview链路迁出当前单体。 - 保留清晰的 DTO 和事件契约。
active-scheduler用 gozero 落地。- 正式确认应用先保持稳定同步语义,等契约更稳后再考虑更深的异步化。
建议提交点:
- dry-run 能独立跑通时,先 commit。
- preview / confirm 的 end-to-end 跑通后,再 commit。
建议测试:
- dry-run smoke。
- preview 生成 smoke。
- confirm / apply smoke。
mock_now和幂等回归测试。
本轮收口状态(2026-05-04):
cmd/active-scheduler已承载 active-scheduler zrpc 启动、DB 迁移、服务级 outbox consumer、relay、retry loop 和 due job scanner。backend/services/active_scheduler已收进 DAO、sv、rpc 和主动调度核心逻辑;复杂领域流程统一下沉到backend/services/active_scheduler/core,旧backend/active_scheduler活跃实现已移除。- gateway HTTP 门面已统一到
backend/gateway/api,active-scheduler、notification、userauth 的 zrpc client 已统一到backend/gateway/client/*。 - 单体
cmd/start.go不再启动 active-scheduler workflow / scanner / handler;gateway 的/api/v1/active-schedule/*只做鉴权、参数绑定、超时和 zrpc 转发。 - 迁移期仍共享主库读取 / 写入 task、schedule、agent 会话与 notification outbox 相关表;active-scheduler 启动时会显式检查这些运行时依赖表,后续阶段 5/6 再逐步切成 RPC 或 read model。
- 已完成真实 smoke:
trigger -> active_scheduler_outbox_messages -> consume -> preview ready闭环通过,当前 trigger 没有出现单体误消费导致的 dead handler。
4.9 阶段 5:再拆 schedule / task / course / task-class
当前进展(2026-05-05):
- 首刀
schedule已完成服务化:新增cmd/schedule、services/schedule/{dao,rpc,sv,core}、gateway/client/schedule、shared/contracts/schedule和shared/portsschedule port。 - gateway 的
/api/v1/schedule/*HTTP 门面已切到 schedule zrpc client;gateway 不再通过backend/service.ScheduleService直接承载 schedule HTTP 入口业务。 - active-scheduler 的 schedule facts / feedback / confirm apply 已改为调用 schedule RPC adapter;
cmd/active-scheduler启动依赖检查已移除schedule_events、schedules、task_classes、task_items。 - 第二刀
task已开始服务化:新增cmd/task、services/task/{dao,rpc,sv}、gateway/client/task、shared/contracts/task和shared/portstask port。 - gateway 的
/api/v1/task/*HTTP 门面已切到 task zrpc client;gateway 只负责鉴权、参数绑定、短超时和响应透传,不再直接调用backend/service.TaskService。 - active-scheduler 的 task facts / due job scanner 已切到 task RPC adapter;
cmd/active-scheduler启动依赖检查已移除tasks,进一步缩小 active-scheduler 对跨域主库表的直接依赖。 task.urgency.promote.requested的 handler、relay、retry loop 已迁入cmd/task;单体 outbox worker 只保留 agent / memory consumer,Agent 残留查询链路只允许 publish-only 写入task_outbox_messages,避免单体和 task 独立服务抢同一 task consumer group。- 第三刀
task-class已完成 HTTP 所有权切流:新增cmd/task-class、services/task_class/{dao,rpc,sv}、gateway/client/taskclass、shared/contracts/taskclass和shared/portstask-class port。 - gateway 的
/api/v1/task-class/*HTTP 门面已切到 task-class zrpc client;gateway 只负责鉴权、参数绑定、短超时和响应透传,不再直接调用backend/service.TaskClassService。 - task-class 本轮按主人拍板保留迁移期直写
schedule_events/schedules权限,不走 schedule RPC bridge,以保留insert into schedule/apply batch into schedule与 task item 状态更新的本地事务语义;cmd/task-class只 AutoMigratetask_classes/task_items,启动时显式检查 schedule 依赖表是否存在。 - 旧实现仍保留:
backend/service/schedule.go、backend/dao/schedule.go、backend/service/task.go、backend/dao/task.go、backend/service/task-class.go、backend/dao/task-class.go、backend/service/course*.go、backend/dao/course.go、active-scheduler 旧 Gorm apply adapter 暂时保留,用于 agent 迁移期、单体残留路径和回退。 - 当前切流点:HTTP schedule 流量进入
cmd/schedule;HTTP task 流量进入cmd/task;HTTP task-class 流量进入cmd/task-class;active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC;agent 内部仍存在直接 DAO 调用,后续按 agent/memory 阶段继续收。 - 当前残留跨域 DB 依赖:task-class 迁移期仍直接写
schedule_events/schedules;task 服务迁移期仍 best-effort 写active_schedule_jobs;active-scheduler 仍直接写 agent 会话 / timeline 和 notification outbox 相关表;agent 本地 task 查询、task-class upsert 和 schedule provider 仍保留 DAO 适配。 - 已完成验证:
go test ./...通过;避让默认端口启动完整本地服务组(HTTP18080,zrpc19081-19086)后,task-class add / list / get / insert-into-schedule / delete-item / delete-class smoke 通过,并用docker exec核对 task-class 与 schedule 相关表无残留。 - 第四刀
course已完成 HTTP 所有权切流:新增cmd/course、services/course/{dao,rpc,sv}、gateway/client/course、shared/contracts/course和shared/portscourse port。 - gateway 的
/api/v1/course/*HTTP 门面已切到 course zrpc client;gateway 只负责鉴权、限流、幂等、multipart 文件读取、短超时和响应透传,不再直接调用backend/service.CourseService。 - course 本轮保留迁移期直写
schedule_events/schedules权限,不走 schedule RPC bridge,以保留课程导入两个表同事务写入和冲突返回语义;cmd/course不 AutoMigrate schedule 表,启动时显式检查依赖表是否存在。 - 当前切流点更新:HTTP schedule / task / task-class / course 流量均进入各自独立 zrpc 服务;active-scheduler 读取 task/schedule facts 与正式写日程均走 RPC;agent 内部仍存在 task、task-class、schedule DAO 适配,后续按 agent/memory 阶段继续收。
- 当前残留跨域 DB 依赖更新:task-class 与 course 迁移期仍直接写
schedule_events/schedules;task 服务迁移期仍 best-effort 写active_schedule_jobs;active-scheduler 仍直接写 agent 会话 / timeline 和 notification outbox 相关表;agent 本地 task 查询、task-class upsert 和 schedule provider 仍保留 DAO 适配。 - 已完成验证:
go test ./...通过;避让默认端口启动完整本地服务组(HTTP18180,zrpc19181-19187)后,course validate / import smoke 通过,并用docker exec核对课程导入写入schedule_events=1、schedules=2。
目标:
- 把正式日程和任务池的所有权拆出去。
- 把课程导入和任务类别编排一起纳入计划编排域。
- 让核心数据服务真正独立。
- 让 gateway 不再直接读写这些核心表。
这一步要做的事:
schedule、task、task-class、course已先后独立;下一轮优先评估 agent 残留 DAO 适配和 memory/agent 阶段切分。- 每个领域只维护自己的写模型。
- 通过事件或明确 RPC 契约通信。
- 继续保持并行迁移,旧实现和新实现可以短期并存。
建议提交点:
- schedule 切流完成后 commit。
- task 切流完成后 commit。
- task-class 切流完成后 commit。
- course 切流完成后 commit。
建议测试:
- schedule 回归测试。
- course 回归测试。
- task-class 回归测试。
- task 回归测试。
- 全链路 smoke。
4.10 阶段 6:再拆 agent / memory
目标:
- 把聊天路由、计划/执行编排、工具接入从现有单体里独立出去。
- 把 memory 的异步抽取、检索、管理拆成独立支撑服务/worker。
- 让 agent 通过清晰的服务契约调用 user/auth、course、task-class、notification、active-scheduler、schedule、task,不再依赖大装配入口。
这一步要做的事:
- 把
newAgent里的路由、prompt、graph、tool registry、会话状态和 SSE 输出包装收进agent服务。 - 把
memory的 repo、worker、orchestrator 和审计写入收进memory服务。 - gateway 只保留鉴权、路由转发和流式透传,不再直接承担 agent 会话编排。
- 过渡期允许 agent 先通过同进程适配器访问现有能力,但切流点必须清楚。
建议提交点:
- agent 服务可以独立启动时先 commit。
- memory 的独立抽取和检索链路稳定后再 commit。
建议测试:
- agent chat smoke。
- memory extract smoke。
- memory retrieve / manage smoke。
- agent 停用旧网关直连后的回归 smoke。
4.11 阶段 7:Gin Gateway 收口
目标:
- 让当前 Gin 服务真正退化成 Gateway。
- 只保留用户入口转发、鉴权、组合和流式交互。
- 清掉剩余的核心业务直连路径。
这一步要做的事:
- 移除 gateway 里的核心领域写路径。
- 把业务能力全部迁到 gozero 服务。
- 只保留前端入口的薄编排。
- 用户入口也只做转发和响应适配,不再直接读写
users表。 - 把 gateway 当成 BFF,而不是域服务承载体。
建议提交点:
- gateway 不再直接写核心业务表时 commit。
- gateway 路由只剩边缘职责时再 commit。
建议测试:
- 网关路由 smoke。
- 鉴权 smoke。
- 前端关键路径 smoke。
5. 推荐执行顺序
当前建议按这个顺序推进:
注:阶段 1.5 / 1.6 / 2 / 3 / 4 已完成首轮收口;notification 和 active-scheduler 都不再作为“未拆服务”待办。
- 以阶段 1 的服务级 outbox 为当前基线,不再回头做共享 outbox 方案。
- 保持
backend/services/llm和backend/services/rag为 canonical 入口,不再把它们写成待办。 - 保持
backend/services/userauth+cmd/userauth为阶段 2 样板,不再回头恢复 Gin 单体 user/auth。 - 下一步进入阶段 5,优先切 schedule / task / course / task-class,逐步替换 active-scheduler 当前的跨域 DB 依赖。
- 再切 agent / memory,把聊天编排、主动调度会话复跑和记忆链路独立出去。
- 最后把 Gin 收口成纯 Gateway。
一句话总结:
outbox 的服务级基础设施、llm-service、rag-service、user/auth 样板服务和 notification 阶段 3 都已经完成;下一步让 active-scheduler、schedule、task、course、task-class 按稳定边界逐步独立;再把 agent / memory 独立出来,完成聊天编排和记忆链路的服务化;最后把 Gin 收口成真正的 Gateway。
6. 最终文件结构对齐
6.1 参考结论
对照你上一个 Kitex + Hertz 项目,这次换成 Gin Gateway + gozero 后,结构理念基本不变,服务承载方式会变。
不变的部分:
- 仍然是“入口层 + 多服务层 + 共享契约层”的三段式。
- 仍然是“一个服务一个目录”,目录内部继续按职责拆分。
- 仍然保留
dao / model / handler(or api) / utils / init这类服务内部基础分层。 - 仍然保留按领域拆目录,而不是把所有业务代码堆到一个大包里。
需要变化的部分:
Kitex的kitex_gen/.thrift/build.sh/script这套 RPC 生成结构,会被 gozero 的api/rpc/internal/etc结构替代。- 原来
client+kitex server的双模块思路,会变成gateway+services/*的多服务布局。 - 入口层会从“RPC 客户端网关”转成“HTTP Gateway + gozero 服务调用”。
- 服务内部的
start.go会收口成 gozero 风格的main + config + logic + svc,或者保留少量自定义启动壳,但不再依赖 Kitex 那套启动模板。
6.2 推荐目标树
SmartFlow-Agent/
├── frontend/
├── backend/
│ ├── gateway/
│ │ ├── cmd/
│ │ │ └── gateway/
│ │ │ └── main.go
│ │ ├── api/
│ │ ├── middleware/
│ │ ├── router/
│ │ ├── userapi/
│ │ └── userauth/
│ ├── services/
│ │ ├── userauth/
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ ├── internal/
│ │ │ │ └── auth/
│ │ │ └── rpc/
│ │ │ ├── pb/
│ │ │ └── userauth.proto
│ │ ├── course/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── parse/
│ │ │ ├── import/
│ │ │ ├── conflict/
│ │ │ └── adapter/
│ │ ├── task-class/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── convert/
│ │ │ ├── batch/
│ │ │ └── item/
│ │ ├── notification/
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ ├── internal/
│ │ │ │ └── feishu/
│ │ │ └── rpc/
│ │ │ ├── pb/
│ │ │ └── notification.proto
│ │ ├── active-scheduler/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── graph/
│ │ │ ├── selection/
│ │ │ ├── feedbacklocate/
│ │ │ ├── preview/
│ │ │ ├── apply/
│ │ │ ├── job/
│ │ │ └── trigger/
│ │ ├── schedule/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── command/
│ │ │ ├── event/
│ │ │ └── conflict/
│ │ ├── task/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── policy/
│ │ │ ├── event/
│ │ │ └── urgency/
│ │ ├── agent/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── prompt/
│ │ │ ├── graph/
│ │ │ ├── stream/
│ │ │ ├── tool/
│ │ │ ├── session/
│ │ │ └── router/
│ │ ├── memory/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── dao/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── repo/
│ │ │ ├── orchestrator/
│ │ │ ├── worker/
│ │ │ ├── observe/
│ │ │ ├── cleanup/
│ │ │ └── vectorsync/
│ │ ├── llm/
│ │ │ ├── start.go
│ │ │ ├── handler.go
│ │ │ ├── sv/
│ │ │ ├── model/
│ │ │ └── internal/
│ │ │ ├── provider/
│ │ │ ├── router/
│ │ │ ├── stream/
│ │ │ ├── quota/
│ │ │ └── audit/
│ │ └── rag/
│ │ ├── start.go
│ │ ├── handler.go
│ │ ├── sv/
│ │ ├── model/
│ │ └── internal/
│ │ ├── chunk/
│ │ ├── embed/
│ │ ├── rerank/
│ │ ├── retrieve/
│ │ ├── store/
│ │ ├── corpus/
│ │ └── observe/
│ ├── shared/
│ │ ├── events/
│ │ └── infra/
│ │ ├── kafka/
│ │ └── outbox/
│ └── main.go
└── docs/
说明 1:
sv/是本文档里唯一推荐的“服务主业务编排层”目录名;文中若出现service/,默认只表示当前仓库里的旧命名或迁移遗留,不作为终态目录名。说明 2:
dao/只负责数据库访问,handler.go只负责 HTTP 入口适配,sv/只负责业务用例编排,internal/只放服务私有子模块,禁止被别的服务直接 import。说明 3:
start.go只负责装配依赖和启动进程,不承载业务规则。当前目录到目标目录的映射:
backend/services/userauth/*已经是阶段 2 终态样板;旧backend/api/user.go、backend/service/user.go、backend/dao/user.go、backend/model/user.go、backend/model/auth.go、backend/auth/jwt_handler.go、backend/middleware/token_handler.go、backend/middleware/token_quota_guard.go、backend/routers/routers.go不再作为活跃实现。backend/gateway/api/userauth/*是 user HTTP 入口,backend/gateway/client/userauth/*是 userauth zrpc client,二者都属于 gateway 边缘层。backend/service/*.go这批现有业务逻辑,后面要分别迁到各自服务根目录下的sv/。backend/service/agentsvc/*和backend/newAgent/*,后面要收束到backend/services/agent/sv/+internal/{prompt,graph,stream,tool,session,router}。backend/services/notification/*已经是阶段 3 终态样板;backend/cmd/notification是独立进程入口,backend/gateway/client/notification是 gateway 侧 zrpc client,backend/shared/contracts/notification只放跨层契约;旧backend/notification/*、旧 DAO/model 和旧service/events/notification_feishu.go不再作为活跃实现。backend/services/active_scheduler/*已经是阶段 4 当前样板;backend/cmd/active-scheduler是独立进程入口,backend/gateway/client/activescheduler是 gateway 侧 zrpc client,backend/services/active_scheduler/core承载迁移期领域核心;旧backend/active_scheduler/*不再作为活跃实现。backend/memory/*,后面要收束到backend/services/memory/;当前memory/service/*只是迁移过渡态,终态还是按sv/或internal/拆开。说明 4:
shared先保留events和少量跨服务底座型infra。以后如果真的出现跨服务 DTO / 枚举 / 常量,再新增contracts一类目录,但不要把dao、model、sv、handler这类服务私有层塞进去。
说明 5:
notification已经按userauth同款最小手搓 zrpc 样板收口:rpc/server.go、rpc/handler.go、rpc/errors.go+rpc/pb,不是 goctl 自动脚手架;active-scheduler后续也按服务内单体壳继续收束,不要让一级目录长期长成一排小框架。说明 6:
llm-service和rag-service是独立基础设施服务,不放进shared;rag-service依赖llm-service做 embedding / rerank,不反向依赖业务服务。说明 7:目录树里如果暂时写成
backend/services/llm/和backend/services/rag/,那只是目录名写法;后文所有职责判断都以llm-service/rag-service这两个逻辑服务名为准。说明 8:阶段 2 已经采用
backend/services/userauth/作为实际目录名,不再使用user-auth。阶段 3 已经采用backend/services/notification/作为实际目录名。gateway 侧 zrpc client 放在backend/gateway/{userauth,notification}/,进程入口放在backend/cmd/{userauth,notification}/;不要把 rpc client 放进cmd。
6.3 哪些可以不用变
- 课程、任务、日程、通知这些领域模型的名字和业务语义可以保留。
middleware的鉴权、限流、幂等这类横切能力可以继续保留,最多是接入方式变化。conv、infra、inits这类能力可以复用思路,但位置要按服务边界重新归属,不默认做成全局公共层。shared里的事件契约和少量跨服务底座可以继续保留,并作为跨进程通信的稳定锚点。
6.4 哪些需要变
backend/cmd/start.go这种“大装配入口”后面要逐步拆成 gateway 启动和各服务启动。api这一层会收缩成纯 Gateway 职责,不再承载核心领域逻辑。- 当前仓库里的
backend/service目录和相关遗留入口,要继续按course、task-class、active-scheduler、schedule、task、agent、memory拆出去;user/auth和notification已完成独立服务边界,后续不要回迁到单体。 - 当前单体里的共享启动方式
api / worker / all,后面会拆成“gateway 进程 + 服务进程 + worker 进程”的组合。 - 任何依赖
users表直读、核心表直写的网关路径,都要迁到对应服务里。 - 不再把服务私有的
dao/model/sv/handler误放进shared,避免它变成新的单体公共层。 backend/infra/llm和backend/infra/rag的.go旧实现已经删除;不要再把它们写成“后面分别收束”的待办。当前 canonical 入口就是backend/services/llm和backend/services/rag。- user/auth 已经迁到
backend/services/userauth,后续不要重新恢复backend/api/user.go、backend/service/user.go、backend/dao/user.go等旧单体入口。
6.5 换对话时要先记住的锚点
shared不是公共业务层,只是跨服务契约层。- 第一批 gozero 样板服务
user/auth已完成;下一批默认从notification开始,再到active-scheduler、schedule/task/course/task-class,后面再切agent/memory。 agent不是公共能力,它应当单独成服务;memory也是独立支撑服务,不应长期挂在 gateway 里。notification和active-scheduler都应该回到更像 seckill 的服务内单体结构,避免成为“半个框架”。llm-service是全仓统一模型出口;rag-service是统一检索基础设施;它们已经完成服务内 canonical 入口迁移,不是阶段 3 待办。- outbox 已经升级成服务级基础设施,后续直接在当前服务级表 / topic / group 基线上按域边界逐个切出去。
- gateway 只保留边缘转发、鉴权和轻量组合,不直接碰各服务核心业务表。
- 后续任何服务目录调整,都要先对照下面的“典型用例”;如果这次改动说不清它属于哪个用例,就先不要动结构,只补文件或补注释。
6.6 notification / active-scheduler 的服务内结构
notification建议直接收束成更标准的服务内单体壳:外层统一成dao/、model/、sv/、handler.go、start.go,当前的runner.go、provider.go、dedupe.go、channel_service.go这类细节先保留在服务内部,但后面要逐步并入sv/或internal/notification/,不要长期挂成一串平级文件。active-scheduler已先收束成cmd/active-scheduler+services/active_scheduler/{dao,rpc,sv,core};core是迁移期领域核心承载处,后续若不再被单体 agent 会话复跑路径引用,可继续改成internal/{graph,selection,preview,feedbacklocate,apply,job,trigger}。- 这样做的目标,是让后续每个服务的阅读方式都更接近你熟悉的 seckill 风格,而不是把一个服务拆成十几个平级目录。
6.7 每个服务的典型用例
这段是给后续代理看的硬护栏:先对齐用例,再动目录;如果不是围绕对应用例扩展,不要先拆目录再补理由。
| 服务 | 典型用例 | 结构收束建议 | 不允许的改法 |
|---|---|---|---|
user/auth |
注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 | 已完成:cmd/userauth + services/userauth/{sv,dao,model,internal/auth,rpc};gateway 侧是 gateway/api/userauth + gateway/client/userauth |
不要恢复旧 Gin user/auth 实现;不要让 gateway 直连 users 表、Redis 黑名单或额度缓存;不要把 zrpc client 放进 cmd |
course |
课程导入、图片解析、课表校验、课程落表,图片解析走 llm-service |
handler.go / sv/ / dao/ / model/ / internal/{parse,import,conflict,adapter}/ |
不要把课程解析代码写成网关临时脚本 |
task-class |
任务类创建/更新、items 批量 upsert、嵌入时间同步 | handler.go / sv/ / dao/ / model/ / internal/{convert,batch,item}/ |
不要把批处理拼装沉到 handler 里,也不要让 agent 直接改库 |
notification |
消费 notification.feishu.requested、写通知记录、幂等、重试、provider 投递 |
start.go / handler.go / sv/ / dao/ / model/ / internal/{provider,runner,dedupe,channel,retry}/ |
不要把通知投递逻辑散回 worker 或 gateway |
active-scheduler |
trigger -> dry-run -> preview -> confirm、建议生成、反馈定位;候选选择走 llm-service |
已完成首轮收口:cmd/active-scheduler + services/active_scheduler/{dao,rpc,sv,core};下一步把跨域 DB 访问替成服务契约后,可把 core 进一步改为 internal/{graph,selection,preview,feedbacklocate,apply,job,trigger} |
不要把 graph/selection 散回 gateway 或旧根目录 |
schedule |
正式日程所有权、查询、删除、应用命令 | start.go / handler.go / sv/ / dao/ / model/ / internal/{command,event,conflict}/ |
不要让 gateway 直接写 schedule 表 |
task |
任务新增、完成/撤销、任务池查询、紧急性平移 | start.go / handler.go / sv/ / dao/ / model/ / internal/{policy,event,urgency}/ |
不要把 task 的状态机塞进 agent 或 schedule |
llm-service |
统一模型调用、provider 路由、流式输出、重试、限流、审计 | start.go / handler.go / sv/ / model/ / internal/{provider,router,stream,quota,audit}/ |
不要把业务 prompt、状态机、工具编排塞进模型出口 |
rag-service |
向量化、召回、重排、向量库读写、语料适配、检索 API | start.go / handler.go / sv/ / model/ / internal/{chunk,embed,rerank,retrieve,store,corpus,observe}/ |
不要让业务服务直连向量库并绕开统一检索层 |
agent |
多轮对话、SSE、工具调用、计划/执行编排、主动调度会话复跑;模型推理走 llm-service,记忆检索走 memory / rag-service |
当前过渡形态是 backend/service/agentsvc + backend/newAgent/*;终态应收束为 start.go / handler.go / sv/ / dao/ / model/ / internal/{prompt,graph,stream,tool,session,router}/ |
不要把 memory、notification、schedule 的业务实现塞进 agent,只通过契约调用 |
memory |
记忆抽取、检索、管理、worker 执行、观测埋点;抽取走 llm-service,检索走 rag-service |
start.go / handler.go / sv/ / dao/ / model/ / internal/{repo,orchestrator,worker,observe,cleanup,vectorsync}/;当前 module.go 只是过渡总门面 |
不要把 memory 变成 gateway 的辅助函数 |
shared 和 shared/infra 不在这张表里,因为它们不是业务服务,只承载跨服务契约和少量公共底座,不按某一个域服务的用例来改。
6.8 最终服务关系图
graph TD
FE["frontend"] --> GW["gateway"]
subgraph D["业务域服务"]
UA["user/auth"]
C["course"]
TC["task-class"]
N["notification"]
AS["active-scheduler"]
S["schedule"]
T["task"]
A["agent"]
M["memory"]
end
subgraph I["基础设施服务"]
L["llm-service"]
R["rag-service"]
end
subgraph B["共享底座"]
OB["各服务 outbox"]
K[(Kafka)]
end
GW --> UA
GW --> C
GW --> TC
GW --> N
GW --> AS
GW --> S
GW --> T
GW --> A
GW --> M
A --> UA
A --> C
A --> TC
A --> N
A --> AS
A --> S
A --> T
A --> M
AS --> C
AS --> TC
AS --> S
AS --> T
AS --> N
C --> L
AS --> L
A --> L
M --> L
M --> R
R --> L
UA --> OB
C --> OB
TC --> OB
N --> OB
AS --> OB
S --> OB
T --> OB
A --> OB
M --> OB
OB --> K
说明:
rag-service只依赖llm-service,不反向依赖业务服务。agent仍然是编排层,实际会通过契约调用user/auth、course、task-class、notification、active-scheduler、schedule、task和memory。- Gateway 不直接碰
llm-service/rag-service,只把请求转给对应业务服务。 - 图里的 outbox 是“每个服务自己的 outbox 表 + 专属 relay worker”的抽象,不代表所有服务共用一张表。
- 当前阶段 1 已完成
agent、task、memory、active-scheduler、notification的服务级 outbox 表、topic 和 consumer group;尚未物理拆出的服务后续沿用同一模式补齐。 - 当前阶段 2 已完成
user/auth物理拆分;gateway 到 userauth 的调用已经通过 zrpc client,不再通过本地 DAO/service。 - 当前阶段 3 已完成
notification物理拆分;gateway 到 notification 的调用已经通过 zrpc client,notification outbox consumer、relay 和 retry loop 已迁入cmd/notification启动边界。 - Kafka 是共享运输层,不是共享业务 topic;新流量不应再默认进入单一共享 topic。
6.9 切对话交接卡
这段是给下一位代理直接接手用的,不需要再反复问大方向。
- 先读顺序:
3.2服务层、4.1阶段总览、4.3Outbox v2、4.6user/auth(已完成)、4.7notification、4.8active-scheduler、4.9schedule / task / course / task-class、4.10agent / memory、6.5切对话锚点、6.7典型用例、6.8最终关系图、6.10启动方式、6.11测试自动化、6.12多代理执行闭环。 - 已冻结的终态是
Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层。 - 阶段 1 已完成,当前 outbox 基线是服务级表、服务级 topic、服务级 consumer group;worker 仍在单体内装配,后续随对应服务迁出。
- 阶段 2 已完成,
user/auth已经是样板服务,不要再把它当成下一轮待办。 - 阶段 3
notification已完成实现、code review 修复和真实 smoke;llm-service、rag-service也已完成,不要重新当成待办。 - 阶段 4
active-scheduler已完成首轮收口;后续不要再把它当成“未拆服务”,除非是在补契约测试或继续替换跨域 DB 访问。 shared只保留跨进程契约和少量跨服务底座,不承载业务逻辑、DAO、模型或状态机。- 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构。
- 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是
backend/cmd/api、backend/cmd/worker、backend/cmd/all,它们只是当前仓库的启动壳,不是终态。backend/cmd/userauth是阶段 2 的独立服务入口,backend/cmd/notification是阶段 3 的独立服务入口,backend/cmd/active-scheduler是阶段 4 的独立服务入口,backend/cmd/schedule、backend/cmd/task、backend/cmd/task-class、backend/cmd/course是阶段 5 已落地的独立服务入口。终态仍然是“一个服务一个独立main.go”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。
6.10 启动方式与进程模型
- 终态里每个 gozero 服务都应当是独立进程:一个服务一个
main.go,一份配置,一组日志,一套端口和资源连接。 - 目录上可以继续采用
backend/cmd/<service>/main.go作为可执行入口,backend/services/<service>/负责sv/、dao/、model/、internal/、rpc/;gateway 自己的 HTTP 适配和 zrpc client 放在backend/gateway/...,不要把 rpc client 放进cmd。 - 本地开发为了方便,可以保留
backend/cmd/all、make dev或类似聚合启动器,但它只负责拉起多个独立进程,不在同一个 Go 进程里把所有服务startXXX()混着跑。 go startxxx()这种“一个进程里同时起多个服务”的方式只适合作为过渡调试壳,不作为最终部署形态。- 如果某些服务需要联动启动,应通过脚本、Makefile、docker compose 或开发编排器去启动多个二进制,而不是把进程边界打穿。
- 带 worker 的服务可以继续保留多入口角色,例如
api/worker/all,但它们仍然是同一服务的不同可执行角色,不是把多个服务硬塞进一个进程。 - MySQL / Redis 容器的启动归
docker compose或运维层;Go 服务只负责在自己的进程里建立连接、做自己的 AutoMigrate 和连通性检查。 - 阶段 5 后,旧
cmd/start.go/cmd/all只是 gateway 和迁移期组合壳;本地完整 smoke 必须额外启动cmd/userauth、cmd/notification、cmd/active-scheduler、cmd/schedule、cmd/task、cmd/task-class和cmd/course。如果同机已有另一条线占用默认端口,应复制临时配置,把 HTTP / zrpc 端口整体平移后再启动服务。
6.11 测试自动化与 smoke 权限边界
后续 agent 做阶段 smoke 时,默认可以使用下面三类本地测试能力,不需要每次重新讨论测试方式:
- 注册测试账号:允许通过本地或测试环境接口创建临时 smoke 账号,用于登录、鉴权、token quota、agent chat、主动调度等链路验证。
- curl 调接口:允许用
curl调本地gateway或本地服务接口,验证 HTTP 状态码、响应结构、业务状态流转和错误码。 - docker exec 查 MySQL / Redis:允许通过
docker exec进入本地 MySQL 或 Redis 容器执行查询,核对用户、任务、日程、outbox、notification_records、memory_jobs、黑名单键、额度快照等状态。
默认测试账号规则:
- 账号必须使用明显的测试前缀,例如
smoke_agent_<timestamp>@example.test。 - 不使用真实手机号、真实邮箱、真实姓名或用户个人信息。
- 密码只用于本地 smoke,不能写入文档、提交记录或日志摘要。
- 如果接口要求昵称、用户名等字段,统一使用
smoke_agent_<timestamp>这类可识别测试值。
默认允许的 smoke 操作:
- 启动本地服务或本地 worker。
- 注册、登录、刷新 token、登出。
- 调用迁移阶段相关接口,例如课程导入、任务类维护、主动调度 dry-run / preview / confirm、通知投递、agent chat、memory retrieve。
- 用
docker exec+SELECT/SHOW查询本地 MySQL 状态。 - 查看 outbox 是否写入、是否投递、是否被对应服务消费。
- 查看 notification、schedule、task、memory 等关键表状态是否符合预期。
- 用
docker exec查询本地 Redis 的黑名单、额度快照、幂等键等迁移相关状态。
默认不允许的 smoke 操作:
- 不访问生产环境接口。
- 不使用真实用户账号或真实第三方 webhook 做自动化验证。
- 不直接执行
DROP、TRUNCATE、DELETE、UPDATE、ALTER这类破坏性或批量数据库操作。 - 不直接清空 outbox、任务、日程、用户、通知、记忆等业务表,也不随意清 Redis 业务键,除非是临时 smoke 数据且已经明确回收。
- 不把 token、密码、API key、webhook 地址等敏感值写进文档、提交信息或最终报告。
如果 smoke 需要清理测试数据,优先使用专门测试库、测试前缀、接口级清理能力或一次性测试环境;直接改库清理必须单独确认。
每次 smoke 最终汇报至少包含:
- 启动了哪些进程或服务。
- 注册/登录使用的是哪类测试账号前缀,不输出密码。
- curl 调用了哪些接口、返回了哪些关键状态码。
- docker exec 查询了哪些表或 Redis 键、验证了哪些关键字段。
- 哪些检查通过,哪些失败,失败时保留可复现命令或最小上下文。
如果本轮还执行了 go test ./...,测试结束后必须清理工作区里的 .gocache,保持仓库整洁。
6.12 多代理并行执行闭环
后续迁移任务如果范围较大,推荐按“主代理规划与收口,子代理并行推进”的方式执行。
子代理统一配置:
- 如果需要开子代理,主代理必须在工具调用里显式指定
model=gpt-5.4、reasoning_effort=xhigh,不能依赖默认配置。 - 除非用户明确指定其他模型,否则迁移实现、代码评审和真实 smoke 子代理都按这个配置开。
- 主代理给子代理的任务必须写清楚目标、文件范围、禁止改动范围、验收标准和最终输出格式。
整体闭环:
- 主代理先定边界:读取本计划和当前代码,明确本轮只处理一个阶段、一个能力域或一类公共件;不要过度翻文件,只读当前判断和实施必需的文件。
- 主代理拆分任务:把任务拆成互不冲突的子任务,并给每个子代理指定明确文件范围、责任边界和验收标准;关键阻塞任务由主代理自己做,不甩给子代理。
- 子代理并行推进:多个子代理可以并行实现不同服务、不同适配层或不同测试补强,但禁止同时修改同一批核心文件。
- 主代理耐心等待:主代理必须等并行推进的子代理返回完整结果后,再进入统一收口;等待期间可以做不冲突的上下文整理,但不能重复实现、覆盖或抢跑子代理负责的任务。
- 主代理统一收口:主代理负责合并实现、处理接口命名、依赖注入、配置、启动入口和跨服务契约一致性。
- 实现收口先停下:涉及独立服务、启动入口、配置或迁移边界时,主代理实现收口后先停下来,让用户重启相关服务。
- 子代理并行 code review + 真实测试:用户确认服务已重启后,再开子代理分别做代码评审和真实 smoke;code review 聚焦 bug、边界、回归风险,真实测试按
6.11执行。 - 主代理再次等待:主代理必须等 review 子代理和测试子代理都给出结论后,再判断是否修复、回退或扩大测试面。
- 主代理修主要问题:主代理根据评审和 smoke 结果修复阻塞问题、高风险问题和明确回归。
- 主代理最终复核:复跑关键测试,汇总本轮迁了什么、旧实现保留在哪里、切流点在哪里、下一轮建议继续处理什么。
子代理拆分原则:
- 每个子代理必须有清晰所有权,例如
services/notification、shared/events、gateway route adapter、outbox relay。 - 子代理之间的写入范围尽量不重叠;如果必须碰同一文件,由主代理自己处理。
- 子代理不能擅自回滚、删除或覆盖其他代理的改动。
- 子代理不能自行扩大阶段范围,例如本轮拆 notification 时顺手重构 active-scheduler。
- 子代理输出必须包含改了哪些文件、为什么改、还有哪些风险没有验证。
主代理收口职责:
- 统一包名、目录名、接口名和配置名。
- 统一错误处理、日志、注释语言和启动方式。
- 确认
shared没有被塞入服务私有业务逻辑。 - 确认
sv/dao/model/internal的职责没有串层。 - 确认 outbox、Kafka consumer group、worker 归属没有打乱服务边界。
并行 code review 细则:
- review 子代理只做评审,不直接改代码,除非主代理明确分派修复任务。
- review 先报 bug、回归风险、缺测试和边界污染,不写空泛风格建议。
- review 必须引用具体文件和位置,说明为什么这是迁移风险。
- review 结果交给主代理统一判断,不由多个子代理分别修同一处。
并行真实测试细则:
- 测试子代理按
6.11的权限边界执行,只使用本地或测试环境。 - 可以注册临时 smoke 账号、curl 接口、docker exec 只读查询 MySQL。
- 测试子代理不能做破坏性数据库操作,不能访问生产环境,不能使用真实用户信息。
- 测试结果必须包含命令、关键状态码、关键表查询点和通过/失败结论。
- 如果测试发现阻塞问题,主代理先修复主路径,再决定是否扩大测试面。
提交与保存进度:
- 不主动
git commit,除非用户明确要求。 - 如果用户要求提交,主代理必须先确认本轮改动范围、测试结果和未解决风险。
- 每个阶段推荐在“骨架可启动”“切流可回退”“真实 smoke 通过”这几个点分别保存进度。
6.13 后续接手硬规则
这段用于避免后续代理重复踩阶段 2 已经纠偏过的问题。
- 阶段 3
notification和阶段 4active-scheduler已完成首轮收口;后续起步默认是阶段 5schedule / task / course / task-class,不是 outbox、llm-service、rag-service、user/auth、notification 或 active-scheduler。 - 主代理负责 leader:先读必要文档和代码,拆任务,关键阻塞任务自己做;子代理只能承担并行、明确、非阻塞的侧翼任务。
- 如果确实有会影响切分方向的不确定点,先总结成拍板点问用户;文档已经写清楚的内容不要重复问。
- 查库一律用
docker exec。MySQL / Redis 都按这个规则走;不直接用本机客户端绕过容器。 - 跑完
go test ./...后必须清理工作区.gocache。 - 不擅自回滚、覆盖、删除用户或其他代理的无关改动。
- 不主动
git commit/git branch,除非用户明确要求。 - 服务间错误传递优先使用 go-zero / gRPC 内置
error,调用侧保持res, err :=风格;API 层对前端错误继续复用respond,后续总收尾再考虑迁到 gateway/shared。 - MySQL / Redis 容器启动归
docker compose或运维层;Go 服务只负责自己的连接初始化、AutoMigrate 和运行时依赖。 - gateway 只做边缘转发、鉴权和轻量组合;不要把核心业务表、服务内部 Redis key、JWT 签发、额度账本放回 gateway。
- 新服务开发可以和后续迁移并行,但必须独立目录、端口、配置和契约,不能污染正在迁移的服务边界。
- 每一轮结构迁移最终都必须说明:本轮迁了什么、旧实现还保留什么、当前切流点在哪里、下一轮建议迁什么。
6.14 阶段 0 历史基线与阶段 1/2/3 当前基线快照
阶段 0 历史基线:
- 阶段 0 时,仓库入口是
backend/cmd/api、backend/cmd/worker、backend/cmd/all;backend/main.go只保留兼容壳,不作为终态入口。 - 阶段 0 时,Gin 路由集中在
/api/v1/...下,主要覆盖user、task、course、task-class、schedule、agent、memory、active-schedule、notification。 - 阶段 0 时,outbox 还是共享单表 + 单 topic / 单 group 形态;阶段 0 只冻结 envelope、版本号、handler 路由和消费边界,不扩 topic。
- 阶段 0 时,
notification、active-scheduler、memory、agent已有单体内服务化雏形;user/auth仍停留在用户域服务层能力,尚未独立成终态服务。 - 阶段 0 只做语义冻结和基线确认,不切服务,不搬 DAO / model,不重排目录,只把运行入口、事件契约、路由清单和 outbox 语义定住,作为 Outbox v2 的历史基线。
阶段 1 当前基线:
- 当前运行入口仍是
backend/cmd/api、backend/cmd/worker、backend/cmd/all,但 worker 已按服务装配多条 outbox 链路。 - 当前 outbox 已切成服务级表:
agent_outbox_messages、task_outbox_messages、memory_outbox_messages、active_scheduler_outbox_messages、notification_outbox_messages。 - 当前 Kafka 已切成服务级 topic:
smartflow.agent.outbox、smartflow.task.outbox、smartflow.memory.outbox、smartflow.active-scheduler.outbox、smartflow.notification.outbox。 - 当前消费侧已切成服务级 consumer group:
smartflow-agent-outbox-consumer、smartflow-task-outbox-consumer、smartflow-memory-outbox-consumer、smartflow-active-scheduler-outbox-consumer、smartflow-notification-outbox-consumer。 - 当前仍是单体内多 worker 装配,不代表服务已经物理拆出;后续拆服务时,要把对应 outbox 表、relay、topic、consumer group 和 handler 一起迁到服务进程里。
agent_outbox_messages沿用历史默认表名作为agent服务 outbox 表,历史存量不代表新事件仍使用共享 outbox。
阶段 1.5 / 1.6 当前基线:
backend/services/llm是当前统一模型出口,backend/infra/llm的.go旧实现已删除。backend/services/rag是当前统一检索基础设施入口,backend/infra/rag的.go旧实现已删除。- 两者仍在单体进程内由
cmd/start.go装配,不代表已经拆成 gozero 独立进程;但服务内 canonical 入口已经完成,后续不要再回头重迁。
阶段 2 当前基线:
backend/cmd/userauth/main.go是 userauth 独立进程入口。backend/services/userauth拥有 user/auth 核心业务、DAO、模型、JWT、黑名单、额度治理、zrpc server 和 token 记账幂等表。backend/gateway/api/userauth是 HTTP user 入口,backend/gateway/client/userauth是 zrpc client,backend/gateway/middleware只调 userauth 做鉴权和额度门禁。backend/shared/contracts/userauth和backend/shared/ports只承载跨层契约,不承载服务私有业务实现。cmd/all不再迁users,cmd/userauth自己迁users和user_token_usage_adjustments。- 完整本地 smoke 需要同时启动
cmd/all和cmd/userauth。
阶段 3 当前基线:
backend/cmd/notification/main.go是 notification 独立进程入口,负责 DB 迁移、zrpc server、notification outbox consumer 和 retry loop 的统一生命周期。backend/services/notification拥有 notification 核心业务、DAO、模型、飞书 provider、幂等、投递记录状态机、重试扫描和 outbox handler。backend/gateway/client/notification是 gateway 侧 zrpc client;gateway 只保留 notification HTTP 入口、鉴权和轻量组合逻辑,不再直连 notification DAO/service。backend/shared/contracts/notification和backend/shared/ports只承载跨层契约和端口接口,不承载服务私有业务实现。- notification 内部是
userauth同款最小手搓 zrpc 框架,不使用 goctl 自动脚手架;rpc只保留NewServer供cmd/notification管理 signal、outbox consumer、retry loop 和 server 生命周期。 - 旧
backend/notification/*、旧backend/dao/notification_channel.go、旧backend/model/notification_channel.go和旧backend/service/events/notification_feishu.go已删除;若backend/notification目录壳仍存在,它不参与编译,也不作为活跃实现。 - notification outbox consumer 已迁入独立服务边界并处理
notification.feishu.requested,覆盖 payload/version 校验、dead/retry/consumed 状态推进和毒消息回退。 - 已完成真实 smoke:
notification_outbox_messages可从pending推进到consumed,Kafkasmartflow.notification.outbox可看到对应 outbox 消息,notification_records可生成幂等记录并按通道状态进入预期状态。
阶段 4 当前基线:
backend/cmd/active-scheduler/main.go是 active-scheduler 独立进程入口,负责 DB 迁移、zrpc server、active-scheduler outbox consumer、relay、retry loop 和 due job scanner 的统一生命周期。backend/services/active_scheduler拥有 active-scheduler DAO、rpc、sv 和领域核心;核心流程当前在backend/services/active_scheduler/core,旧backend/active_scheduler不再作为活跃实现存在。backend/gateway/api是 HTTP 门面统一目录,backend/gateway/client/activescheduler是 gateway 侧 zrpc client。backend/shared/contracts/activescheduler和backend/shared/ports只承载跨层契约和端口接口,不承载服务私有业务实现。cmd/all不再启动 active-scheduler workflow / scanner / handler;完整本地 smoke 需要同时启动cmd/all、cmd/userauth、cmd/notification和cmd/active-scheduler。- 阶段 4 收口时仍共享主库访问 task、schedule、agent 会话和 notification outbox 相关表;阶段 5 已先通过 schedule / task RPC 继续缩小这条共享边界。
阶段 5 当前基线:
backend/cmd/schedule/main.go是 schedule 独立进程入口,backend/cmd/task/main.go是 task 独立进程入口,backend/cmd/task-class/main.go是 task-class 独立进程入口,backend/cmd/course/main.go是 course 独立进程入口,四者各自初始化 DB / Redis / zrpc server 和所需服务内资源。backend/services/schedule拥有正式日程领域核心,backend/services/task拥有任务池读写、完成/撤销、紧急性平移和 task outbox handler,backend/services/task_class拥有任务类与任务块维护、批量排入日程等核心逻辑,backend/services/course拥有课程校验、课程导入和课表图片解析逻辑。backend/gateway/api继续作为 HTTP 门面统一目录,backend/gateway/client/schedule、backend/gateway/client/task、backend/gateway/client/taskclass与backend/gateway/client/course作为 gateway 侧 zrpc client。backend/shared/contracts/schedule、backend/shared/contracts/task、backend/shared/contracts/taskclass、backend/shared/contracts/course和backend/shared/ports只承载跨进程契约与端口接口,不放 DAO、model 或业务状态机。- active-scheduler 的 schedule facts / feedback / confirm apply 已走 schedule RPC,task facts / due job scanner 已走 task RPC;启动依赖检查不再要求
schedule_events、schedules、task_classes、task_items或tasks。 task.urgency.promote.requested的消费边界已迁入cmd/task;单体 outbox worker 不再启动 task service bus,只保留 Agent 残留路径的 publish-only 写入能力,避免迁移期重复 relay / consume。- task-class 迁移期仍直接写
schedule_events/schedules,用于保留insert/apply与 item 状态更新的本地事务语义;course 迁移期仍直接写schedule_events/schedules,用于保留课程导入两个表同事务写入;两个服务启动都只检查 schedule 依赖表,不 AutoMigrate schedule 表。 - 本阶段残留:task 服务仍 best-effort 写
active_schedule_jobs;agent 本地 task 查询、quick task 创建、task-class upsert 和 schedule provider 仍存在直接 DAO 调用;active-scheduler 旧 Gorm apply adapter 保留为迁移期残留,不作为新流量主路径。
7. 风险与回退
风险 1:消息路由不清
表现:
- 服务误吃别人的事件。
- 未知事件被错误 dead-letter。
- 多服务相互污染状态。
回退:
- 不回退到共享 topic;先冻结新增事件,补齐
event_type -> service路由和 service catalog 配置。 - 如果某个服务误消费,先暂停对应服务 worker 或 consumer group,不影响其他服务 group。
- 保留
all模式作为本地运行兜底,但all内部仍按服务级 worker 装配。
风险 2:relay 并行过早
表现:
- 重复投递。
- 状态回写打架。
- 重试窗口失真。
回退:
- 先保持每服务单 relay。
- 真要横向扩展时,再补 claim / lease。
风险 3:切服务过快
表现:
- 业务边界还没稳,服务已经拆散。
- 请求链路变长,但收益没起来。
- 调试成本先于收益暴涨。
回退:
- 保留当前单体实现。
- 只做接口和契约前置拆分。
- 不提前拆 schedule/task。
风险 4:历史 outbox 存量被误判
表现:
agent_outbox_messages里存在共享 outbox 时代的旧数据,看起来像所有事件仍写回agent。- 后续代理误以为阶段 1 没有完成,又重复设计共享表迁移方案。
- 清理历史数据时误删仍有业务价值的审计记录。
处理:
- 以新事件写入表、topic 和 consumer group 为准判断当前链路,不用旧存量判断新路由。
- 历史数据清理或归档单独立项,不和服务拆分混在同一轮做。
- 如需清理本地测试存量,必须遵守
6.11的数据库操作边界,破坏性操作单独确认。
风险 5:阶段 2 样板被误回退
表现:
- 后续代理看到旧日志、旧文档或历史文件名,误以为 user/auth 还在 Gin 单体里。
- 新增 gateway handler 时又直接 import userauth DAO/model、直接读写
users或 Redis 黑名单。 - 把 zrpc client 放进
cmd,让进程入口承担跨服务调用语义。
处理:
- 以当前编译入口和路由装配为准:
gateway/api/userauth+gateway/client/userauth+services/userauth是阶段 2 当前样板。 - 任何 user/auth 新能力先放进
services/userauth,gateway 只做 HTTP 适配和 respond 响应。 - 如果 user/auth 调用失败,先查
cmd/userauth是否启动、zrpc endpoint 是否正确、服务内 MySQL/Redis 是否可连,不要把逻辑搬回 gateway。