diff --git a/docs/backend/微服务四步迁移与第二阶段并行开发计划.md b/docs/backend/微服务四步迁移与第二阶段并行开发计划.md index 62f6ac1..a6200fb 100644 --- a/docs/backend/微服务四步迁移与第二阶段并行开发计划.md +++ b/docs/backend/微服务四步迁移与第二阶段并行开发计划.md @@ -8,6 +8,9 @@ 1. 阶段 0 已完成:运行入口、事件契约、路由清单和 outbox 语义已经冻结。 2. 阶段 1 已完成:当前基线已经切成服务级 outbox 表、服务级 Kafka topic、服务级 consumer group;仍在单体进程内装配多个服务级 worker,后续拆微服务时再物理迁出。 +3. 阶段 1.5 / 1.6 已完成:`backend/services/llm` 和 `backend/services/rag` 已经是当前 canonical 入口,`backend/infra/llm` 和 `backend/infra/rag` 的 `.go` 旧实现已删除。 +4. 阶段 2 已完成:`user/auth` 已经从 Gin 单体抽成 `cmd/userauth` + `services/userauth` 的 go-zero zrpc 服务边界,gateway 只保留 user HTTP 入口、鉴权、额度门禁和轻量转发。 +5. 下一轮从阶段 3 开始,默认目标是拆 `notification`;不要再把 outbox、llm-service、rag-service 或 user/auth 当成未完成待办。 本计划遵守两个硬原则: @@ -38,7 +41,9 @@ 阶段 1.5 / 1.6 也已经先落地完毕:`backend/services/llm` 和 `backend/services/rag` 已经成为当前 canonical 入口,`backend/infra/llm` 和 `backend/infra/rag` 的 `.go` 旧实现已删除,仅保留迁移说明文档。当前仍然是单体进程内多 worker 装配,llm / rag 先完成服务化收口,还没有进入 gozero 进程拆分。 -所以后续路线不是再补一次 outbox 基建,而是在这个阶段 1 基线上,按服务边界逐个把 gozero 服务、DAO / model / worker 和启动入口迁出去。 +阶段 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 和启动入口迁出去。 --- @@ -53,6 +58,7 @@ Gin Gateway 只做边缘层职责: 3. 请求编排。 4. SSE / 流式返回。 5. 前端所需的轻量组合逻辑。 +6. API 层错误响应适配。迁移期可以继续复用 `backend/respond`,等全部服务边界稳定后再整体收进 gateway/shared。 网关不再承担这些职责: @@ -60,6 +66,13 @@ Gin Gateway 只做边缘层职责: 2. 直接写核心业务表。 3. 直接消费所有后台事件。 4. 直接维护服务内部重试与投递状态。 +5. 直接维护用户黑名单、JWT 签发、token 额度账本这类 user/auth 内部状态。 + +当前阶段 2 切流点: + +1. `/api/v1/user/*` 由 `backend/gateway/userapi` 承载 HTTP 入口,核心能力通过 `backend/gateway/userauth` 调 `cmd/userauth` zrpc。 +2. `gateway/middleware` 的 JWT 鉴权和 token quota guard 只调 `userauth`,不直接读写 `users`、Redis 黑名单或额度缓存。 +3. zrpc client 放在 gateway/service 调用侧目录,不放进 `cmd`。`cmd` 只负责进程入口和装配,不承载跨服务 client 语义。 ### 3.2 服务层 @@ -82,6 +95,8 @@ gozero 服务负责领域能力: > 说明:`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 client 在 `backend/gateway/userauth`。 ### 3.3 事件层 @@ -111,6 +126,7 @@ gozero 服务负责领域能力: 4. 不要把 `dao`、`model`、`service`、`handler` 这类服务私有代码塞进 `shared`;它们应该跟随各自服务归属。 5. `infra` 也不应该是一个大公共篮子:像 `kafka`、`outbox` 这类跨服务底座可以放到 `shared/infra`;`llm-service`、`rag-service` 这类模型与检索能力要单独成基础设施服务,不要塞进 `shared`;`prompt`、`tooling` 这类强业务依赖的适配器则应跟着具体服务走。 6. 换句话说,`shared` 是“跨进程契约层 + 少量跨服务底座”,不是“公共业务层”。 +7. 阶段 2 已经新增 `backend/shared/contracts/userauth` 和 `backend/shared/ports`,只承载跨层契约和端口接口;user/auth 的 JWT、DAO、额度治理、黑名单实现不进入 `shared`。 --- @@ -124,8 +140,8 @@ gozero 服务负责领域能力: | 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 | user 路由、JWT 签发和 token 额度治理独立后 commit | 注册/登录/刷新/登出 smoke + token quota 回归 | -| 3 | 再拆 notification | notification 服务能独立消费和重试后 commit | notification E2E smoke + worker-only smoke | +| 2 | 先拆 user/auth(已完成) | 已完成,阶段 2 样板 commit 点:userauth zrpc、gateway userapi、JWT/黑名单/额度治理、启动与迁移边界已收口 | 已完成注册/登录/刷新/并发 refresh/登出/鉴权/token quota smoke | +| 3 | 再拆 notification(下一阶段) | notification 服务能独立消费和重试后 commit | notification E2E smoke + worker-only smoke | | 4 | 再拆 active-scheduler | 预览生成和确认链路通过 gozero 服务跑通后 commit | dry-run / preview / 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 | @@ -228,112 +244,104 @@ flowchart LR 下一步: 1. 不再重复做 Outbox v2 基建。 -2. 后续从阶段 1.5 / 1.6 开始,按 `llm-service`、`rag-service`、`user/auth` 等服务边界推进物理拆分。 +2. 阶段 1.5 / 1.6 和阶段 2 已完成,后续从阶段 3 `notification` 开始推进。 3. 迁移任何新服务时,必须复用当前 outbox 路由、服务 catalog、relay 和 consumer group 隔离规则。 --- ### 4.4 阶段 1.5:先抽 llm-service(已完成) -目标: +当前结论: -1. 把全仓统一模型出口先从各业务服务里抽出来。 -2. 让 `course`、`active-scheduler`、`memory`、`agent` 对模型调用的依赖先收口到统一服务。 -3. 先把模型 provider 路由、流式输出、限流、审计这些共性收束起来,避免每个服务各写一份。 - -当前状态: - -1. 代码已经落到 `backend/services/llm`。 +1. `backend/services/llm` 已经是当前 canonical 入口。 2. `backend/infra/llm` 的 `.go` 旧实现已删除,仅保留迁移说明。 -3. 仍由 `backend/cmd/start.go` 在同一进程内装配,尚未引入 gozero 独立服务进程。 +3. llm 相关调用仍由 `backend/cmd/start.go` 在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。 +4. 这一段已经不是待办,不要再安排新的 llm 抽离任务。 -这一步要做的事: +已完成的事: -1. 把当前分散在业务服务里的模型调用入口改成统一调用 `llm-service`。 -2. 把 provider 路由、重试、流式转发、审计日志收进 `llm-service`。 -3. 先保留旧调用路径的兼容适配,但新逻辑必须优先走 `llm-service`。 -4. 让 `course`、`active-scheduler`、`memory`、`agent` 的模型使用方式先统一,后面再看是否继续做更细的协议抽象。 -5. `llm-service` 只负责模型出口,不负责业务 prompt 状态机、工具编排或领域决策。 +1. 把全仓统一模型出口从各业务服务里收口到 `backend/services/llm`。 +2. 把 provider 路由、流式输出、限流、审计这些共性统一起来。 +3. 让 `course`、`active-scheduler`、`memory`、`agent` 的模型使用方式先统一,后面再看是否继续做更细的协议抽象。 +4. `llm-service` 只负责模型出口,不负责业务 prompt 状态机、工具编排或领域决策。 -建议提交点: +已完成验证: -1. `llm-service` 可以独立启动时先 commit。 -2. 第一批业务服务完成切换并验证稳定后再 commit。 - -建议测试: - -1. `llm-service` 单独启动 smoke。 -2. `course` 调模型 smoke。 -3. `active-scheduler` 调模型 smoke。 -4. `memory` / `agent` 调模型 smoke。 -5. 流式输出、重试和审计回归 smoke。 +1. `go test ./...` 通过。 +2. 业务服务调模型链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧 `backend/infra/llm`。 --- ### 4.5 阶段 1.6:再抽 rag-service(已完成) -目标: +当前结论: -1. 把统一检索基础设施从 `memory` / `agent` 里抽出来。 -2. 让向量化、召回、重排、向量库读写先进入独立服务。 -3. 明确 `rag-service` 只能依赖 `llm-service` 做 embedding / rerank,不反向依赖业务服务。 - -当前状态: - -1. 代码已经落到 `backend/services/rag`。 +1. `backend/services/rag` 已经是当前 canonical 入口。 2. `backend/infra/rag` 的 `.go` 旧实现已删除,仅保留迁移说明。 -3. 仍由 `backend/cmd/start.go` 在同一进程内装配,尚未引入 gozero 独立服务进程。 +3. rag 相关调用仍由 `backend/cmd/start.go` 在同一进程内装配,还没有拆成 gozero 独立进程,但服务内边界已经收口。 +4. 这一段已经不是待办,不要再安排新的 rag 抽离任务。 -这一步要做的事: +已完成的事: -1. 把当前分散在 `memory`、`agent` 里的检索逻辑改成统一调用 `rag-service`。 -2. 把 chunk、embed、rerank、retrieve、store 这些能力收进 `rag-service`。 -3. `rag-service` 通过 `llm-service` 获取 embedding 或 rerank 能力,不直接接业务模型出口。 -4. 先保留旧检索链路的兼容适配,但新链路必须优先走 `rag-service`。 -5. 让 `memory` 退回成记忆管理和编排支撑服务,不再自己持有完整检索基础设施。 +1. 把统一检索基础设施从 `memory` / `agent` 里收口到 `backend/services/rag`。 +2. 把向量化、召回、重排、向量库读写统一起来。 +3. 明确 `rag-service` 只依赖 `llm-service` 做 embedding / rerank,不反向依赖业务服务。 +4. 让 `memory` 退回成记忆管理和编排支撑服务,不再自己持有完整检索基础设施。 -建议提交点: +已完成验证: -1. `rag-service` 可以独立启动时先 commit。 -2. `memory` / `agent` 切到 `rag-service` 后稳定,再 commit。 - -建议测试: - -1. `rag-service` 单独启动 smoke。 -2. `memory retrieve` smoke。 -3. `memory rerank` smoke。 -4. `agent` 检索调用 smoke。 -5. `rag-service -> llm-service` 依赖链路 smoke。 +1. `go test ./...` 通过。 +2. 业务服务检索链路已可用,后续直接沿用这一 canonical 入口,不再回退到旧 `backend/infra/rag`。 --- -### 4.6 阶段 2:先拆 user/auth +### 4.6 阶段 2:先拆 user/auth(已完成) -目标: +当前结论: -1. 把用户入口、登录态签发和 token 额度治理从 Gin 单体里拆出来。 -2. 让 gateway 不再直读 `users` 表,先把“用户域”变成独立服务边界。 -3. 为后面所有需要鉴权和额度检查的服务提供稳定入口。 +1. `user/auth` 已经从 Gin 单体拆成 go-zero zrpc 独立服务边界。 +2. gateway 不再直读 `users` 表,也不再维护 JWT 签发、refresh 轮转、黑名单和 token 额度账本。 +3. 阶段 2 是后续服务迁移的样板:服务端拥有自己的 `dao/model/sv/internal/rpc`,gateway 只保留 HTTP 边缘入口和 zrpc client。 +4. `cmd/all` 不再内嵌 userauth;完整本地运行需要同时启动 `cmd/all` 与 `cmd/userauth`。 -这一步要做的事: +已完成的事: -1. 把 `/user/register`、`/user/login`、`/user/refresh-token`、`/user/logout` 迁出当前单体。 -2. 把 JWT 签发、刷新、黑名单和 token 额度判断收进 `user/auth` 服务。 -3. gateway 只保留 access token 校验、路由转发和轻量编排,不再直接碰用户表。 -4. `agent/chat` 相关的 token quota 门禁同步改成调用 `user/auth` 能力,避免网关继续直连 `users` 表。 -5. 过渡期允许 gateway 保留原 `/user` 路由壳,但业务实现必须已经落到新服务。 +1. 新增 `backend/cmd/userauth/main.go` 作为 userauth 独立进程入口。 +2. 新增 `backend/services/userauth/**`,承载注册、登录、刷新 token、登出、JWT 签发/校验、黑名单、token 额度治理和 token 记账幂等。 +3. 新增 `backend/gateway/userapi/**`,承载 `/api/v1/user/register`、`/api/v1/user/login`、`/api/v1/user/refresh-token`、`/api/v1/user/logout` 的 HTTP handler。 +4. 新增 `backend/gateway/userauth/**`,承载 gateway 侧 zrpc client 和 gRPC 错误反解。 +5. 新增 `backend/gateway/middleware/**`,把 JWT 鉴权和 token quota guard 改成调用 userauth,不再直接碰 users 表或 Redis 黑名单细节。 +6. 新增 `backend/shared/contracts/userauth` 与 `backend/shared/ports`,只放跨层契约和端口接口。 +7. 拆分 MySQL / Redis 初始化和 AutoMigrate 边界:`cmd/all` 走 `ConnectCoreDB` / `InitCoreRedis`,只迁单体残留域;`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。 +8. 清退旧 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`。 -建议提交点: +关键修复: -1. `user/auth` 服务可以独立启动时,先 commit。 -2. 登录、刷新、登出与 token quota 完整切流后,再 commit。 +1. refresh token 并发重放:使用 Redis `SET NX` 抢占旧 refresh JTI,保证并发刷新时只有一个请求能签出新 token。 +2. token 记账幂等:新增 `user_token_usage_adjustments`,和 `users.token_usage` 在同一个 MySQL 事务内提交,避免 outbox 重试或并发重放重复记账。 +3. 额度边界:`token_usage >= token_limit` 即拒绝后续高消耗入口,不再多放行一次。 +4. RPC 错误:服务间错误通过 go-zero / gRPC `error` 返回,gateway client 反解成项目内 `respond.Response`;非业务内部错误不再原样透给前端。 -建议测试: +当前切流点: -1. `user/auth` 服务单独启动 smoke。 -2. 注册 / 登录 / 刷新 / 登出 smoke。 -3. token quota 门禁回归 smoke。 -4. 旧网关壳停掉 user 直连后,`agent/chat` 仍能正常鉴权与限额校验。 +1. 前端仍访问 `/api/v1/user/*`。 +2. gateway 的 user handler 只做参数绑定、调用 userauth client、复用 `respond` 写回前端。 +3. gateway 鉴权和 quota guard 只依赖 `ports.UserAuthClient`,不直接依赖 userauth DAO/model。 +4. `agent/chat` 的 token quota 门禁已经通过 userauth 服务判断;会话完成后的 token 记账由 agent 事件处理链路调用 userauth `AdjustTokenUsage`。 + +已完成验证: + +1. `go test ./...` 通过,并已按规则清理 `.gocache`。 +2. 注册、重复注册、登录、鉴权接口、刷新 token、旧 refresh 复用拒绝、登出、登出后 access/refresh 拒绝都已通过真实 smoke。 +3. 并发 refresh smoke:5 个并发请求只有 1 个成功,其余请求被拒绝。 +4. token quota smoke:临时把 smoke 用户 `token_usage` 打到 `token_limit` 后,`/agent/chat` 返回 `40051 token usage exceeds limit`。 +5. 通过 `docker exec` 确认 `users` 落库、Redis 黑名单存在、`user_token_usage_adjustments` 已由 `cmd/userauth` 迁移出来。 + +遗留约定: + +1. `respond` 暂时继续放在 `backend/respond` 复用;等全部阶段收尾后再整体收进 gateway/shared。 +2. `cmd/all` 单独启动不再覆盖 user/auth 完整能力,后续 smoke 必须同时确认 userauth 已启动。 +3. 不要再把 user/auth 当成后续待办;阶段 3 的接手人应从 notification 开始。 --- @@ -490,21 +498,20 @@ flowchart LR 当前建议按这个顺序推进: -注:阶段 1.5 / 1.6 已完成,当前实际推进可从阶段 2 开始。 +注:阶段 1.5 / 1.6 / 2 已完成,当前实际推进从阶段 3 `notification` 开始。 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。 +2. 保持 `backend/services/llm` 和 `backend/services/rag` 为 canonical 入口,不再把它们写成待办。 +3. 保持 `backend/services/userauth` + `cmd/userauth` 为阶段 2 样板,不再回头恢复 Gin 单体 user/auth。 +4. 下一步切 notification。 +5. 再切 active-scheduler。 +6. 然后切 schedule / task / course / task-class。 +7. 再切 agent / memory,把聊天编排和记忆链路独立出去。 +8. 最后把 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 样板服务都已经完成;下一轮从 notification 开始,把通知投递和重试切成独立服务;然后让 active-scheduler、schedule、task、course、task-class 按稳定边界逐步独立;再把 agent / memory 独立出来,完成聊天编排和记忆链路的服务化;最后把 Gin 收口成真正的 Gateway。 --- @@ -540,19 +547,19 @@ SmartFlow-Agent/ │ │ │ └── main.go │ │ ├── api/ │ │ ├── middleware/ -│ │ ├── routers/ -│ │ └── auth/ +│ │ ├── router/ +│ │ ├── userapi/ +│ │ └── userauth/ │ ├── services/ -│ │ ├── user-auth/ -│ │ │ ├── start.go -│ │ │ ├── handler.go +│ │ ├── userauth/ │ │ │ ├── sv/ │ │ │ ├── dao/ │ │ │ ├── model/ -│ │ │ └── internal/ -│ │ │ ├── token/ -│ │ │ ├── quota/ -│ │ │ └── session/ +│ │ │ ├── internal/ +│ │ │ │ └── auth/ +│ │ │ └── rpc/ +│ │ │ ├── pb/ +│ │ │ └── userauth.proto │ │ ├── course/ │ │ │ ├── start.go │ │ │ ├── handler.go @@ -687,11 +694,13 @@ SmartFlow-Agent/ > > 当前目录到目标目录的映射: > -> 1. `backend/service/*.go` 这批现有业务逻辑,后面要分别迁到各自服务根目录下的 `sv/`。 -> 2. `backend/service/agentsvc/*` 和 `backend/newAgent/*`,后面要收束到 `backend/services/agent/sv/` + `internal/{prompt,graph,stream,tool,session,router}`。 -> 3. `backend/notification/*`,后面要收束到 `backend/services/notification/`,其中 `runner/provider/dedupe/channel_service` 归入 `internal/notification/`。 -> 4. `backend/active_scheduler/*`,后面要收束到 `backend/services/active-scheduler/`,其中 `graph/selection/feedbacklocate/apply/job` 归入 `internal/`。 -> 5. `backend/memory/*`,后面要收束到 `backend/services/memory/`;当前 `memory/service/*` 只是迁移过渡态,终态还是按 `sv/` 或 `internal/` 拆开。 +> 1. `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` 不再作为活跃实现。 +> 2. `backend/gateway/userapi/*` 是 user HTTP 入口,`backend/gateway/userauth/*` 是 userauth zrpc client,二者都属于 gateway 边缘层。 +> 3. `backend/service/*.go` 这批现有业务逻辑,后面要分别迁到各自服务根目录下的 `sv/`。 +> 4. `backend/service/agentsvc/*` 和 `backend/newAgent/*`,后面要收束到 `backend/services/agent/sv/` + `internal/{prompt,graph,stream,tool,session,router}`。 +> 5. `backend/notification/*`,下一阶段要收束到 `backend/services/notification/`,其中 `runner/provider/dedupe/channel_service` 归入 `sv/` 或 `internal/notification/`。 +> 6. `backend/active_scheduler/*`,后面要收束到 `backend/services/active-scheduler/`,其中 `graph/selection/feedbacklocate/apply/job` 归入 `internal/`。 +> 7. `backend/memory/*`,后面要收束到 `backend/services/memory/`;当前 `memory/service/*` 只是迁移过渡态,终态还是按 `sv/` 或 `internal/` 拆开。 > > 说明 4:`shared` 先保留 `events` 和少量跨服务底座型 `infra`。以后如果真的出现跨服务 DTO / 枚举 / 常量,再新增 `contracts` 一类目录,但不要把 `dao`、`model`、`sv`、`handler` 这类服务私有层塞进去。 @@ -700,6 +709,8 @@ SmartFlow-Agent/ > 说明 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`。gateway 侧 zrpc client 放在 `backend/gateway/userauth/`,进程入口放在 `backend/cmd/userauth/`;不要把 rpc client 放进 `cmd`。 ### 6.3 哪些可以不用变 @@ -716,17 +727,19 @@ SmartFlow-Agent/ 4. 当前单体里的共享启动方式 `api / worker / all`,后面会拆成“gateway 进程 + 服务进程 + worker 进程”的组合。 5. 任何依赖 `users` 表直读、核心表直写的网关路径,都要迁到对应服务里。 6. 不再把服务私有的 `dao` / `model` / `sv` / `handler` 误放进 `shared`,避免它变成新的单体公共层。 -7. 当前 `backend/infra/llm` 和 `backend/infra/rag` 只是迁移过渡态,后面分别收束到 `backend/services/llm` 和 `backend/services/rag`。 +7. `backend/infra/llm` 和 `backend/infra/rag` 的 `.go` 旧实现已经删除;不要再把它们写成“后面分别收束”的待办。当前 canonical 入口就是 `backend/services/llm` 和 `backend/services/rag`。 +8. user/auth 已经迁到 `backend/services/userauth`,后续不要重新恢复 `backend/api/user.go`、`backend/service/user.go`、`backend/dao/user.go` 等旧单体入口。 ### 6.5 换对话时要先记住的锚点 1. `shared` 不是公共业务层,只是跨服务契约层。 -2. 第一批 gozero 域服务是 `user/auth`、`course`、`task-class`、`notification`、`active-scheduler`、`schedule`、`task`,后面再切 `agent` / `memory`。 +2. 第一批 gozero 样板服务 `user/auth` 已完成;下一批默认从 `notification` 开始,再到 `active-scheduler`、`schedule/task/course/task-class`,后面再切 `agent` / `memory`。 3. `agent` 不是公共能力,它应当单独成服务;`memory` 也是独立支撑服务,不应长期挂在 gateway 里。 4. `notification` 和 `active-scheduler` 都应该回到更像 seckill 的服务内单体结构,避免成为“半个框架”。 -5. `llm-service` 是全仓统一模型出口;`rag-service` 是统一检索基础设施;`rag-service` 依赖 `llm-service`,不反向依赖业务服务。 +5. `llm-service` 是全仓统一模型出口;`rag-service` 是统一检索基础设施;它们已经完成服务内 canonical 入口迁移,不是阶段 3 待办。 6. outbox 已经升级成服务级基础设施,后续直接在当前服务级表 / topic / group 基线上按域边界逐个切出去。 -7. 后续任何服务目录调整,都要先对照下面的“典型用例”;如果这次改动说不清它属于哪个用例,就先不要动结构,只补文件或补注释。 +7. gateway 只保留边缘转发、鉴权和轻量组合,不直接碰各服务核心业务表。 +8. 后续任何服务目录调整,都要先对照下面的“典型用例”;如果这次改动说不清它属于哪个用例,就先不要动结构,只补文件或补注释。 ### 6.6 `notification` / `active-scheduler` 的服务内结构 @@ -740,7 +753,7 @@ SmartFlow-Agent/ | 服务 | 典型用例 | 结构收束建议 | 不允许的改法 | | --- | --- | --- | --- | -| `user/auth` | 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁 | `handler.go` / `sv/` / `dao/` / `model/` / `internal/auth/`;认证状态机和额度判断留在服务内 | 不要把 gateway 鉴权逻辑和别的领域规则混进来 | +| `user/auth` | 注册、登录、刷新、登出、JWT 签发、黑名单、token 额度门禁、token 记账幂等 | 已完成:`cmd/userauth` + `services/userauth/{sv,dao,model,internal/auth,rpc}`;gateway 侧是 `gateway/userapi` + `gateway/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 | @@ -833,30 +846,32 @@ graph TD 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. 当前阶段 2 已完成 `user/auth` 物理拆分;gateway 到 userauth 的调用已经通过 zrpc client,不再通过本地 DAO/service。 +7. Kafka 是共享运输层,不是共享业务 topic;新流量不应再默认进入单一共享 topic。 ### 6.9 切对话交接卡 这段是给下一位代理直接接手用的,不需要再反复问大方向。 -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` 多代理执行闭环。 +1. 先读顺序:`3.2` 服务层、`4.1` 阶段总览、`4.3` Outbox v2、`4.6` user/auth(已完成)、`4.7` notification、`4.8` active-scheduler、`4.9` schedule / task / course / task-class、`4.10` agent / memory、`6.5` 切对话锚点、`6.7` 典型用例、`6.8` 最终关系图、`6.10` 启动方式、`6.11` 测试自动化、`6.12` 多代理执行闭环。 2. 已冻结的终态是 `Gin Gateway + gozero 服务群 + 服务级 outbox + Kafka 共享运输层`。 3. 阶段 1 已完成,当前 outbox 基线是服务级表、服务级 topic、服务级 consumer group;worker 仍在单体内装配,后续随对应服务迁出。 -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`。 +4. 阶段 2 已完成,`user/auth` 已经是样板服务,不要再把它当成下一轮待办。 +5. 下一轮默认从阶段 3 `notification` 开始;`llm-service`、`rag-service` 也已完成,不要重新当成待办。 6. `notification` 和 `active-scheduler` 后续要回到更像 seckill 的服务内单体壳。 7. `shared` 只保留跨进程契约和少量跨服务底座,不承载业务逻辑、DAO、模型或状态机。 8. 如果后续要改目录,必须先回答“这个文件属于哪一个典型用例”,回答不清楚就先别动结构。 -9. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是 `backend/cmd/api`、`backend/cmd/worker`、`backend/cmd/all`,它们只是当前仓库的启动壳,不是终态。终态仍然是“一个服务一个独立 `main.go`”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。 +9. 当前文档已经可以作为切对话基线;后续代理默认按本文件推进。现阶段的迁移基线入口是 `backend/cmd/api`、`backend/cmd/worker`、`backend/cmd/all`,它们只是当前仓库的启动壳,不是终态。`backend/cmd/userauth` 是阶段 2 的独立服务入口。终态仍然是“一个服务一个独立 `main.go`”,只在出现新的契约风险、边界变化或业务语义变化时再重新讨论架构。 ### 6.10 启动方式与进程模型 1. 终态里每个 gozero 服务都应当是独立进程:一个服务一个 `main.go`,一份配置,一组日志,一套端口和资源连接。 -2. 目录上可以继续采用 `backend/cmd//main.go` 作为可执行入口,`backend/services//` 负责 `start.go`、`handler.go`、`sv/`、`dao/`、`model/`、`internal/`。 +2. 目录上可以继续采用 `backend/cmd//main.go` 作为可执行入口,`backend/services//` 负责 `sv/`、`dao/`、`model/`、`internal/`、`rpc/`;gateway 自己的 HTTP 适配和 zrpc client 放在 `backend/gateway/...`,不要把 rpc client 放进 `cmd`。 3. 本地开发为了方便,可以保留 `backend/cmd/all`、`make dev` 或类似聚合启动器,但它只负责拉起多个独立进程,不在同一个 Go 进程里把所有服务 `startXXX()` 混着跑。 4. `go startxxx()` 这种“一个进程里同时起多个服务”的方式只适合作为过渡调试壳,不作为最终部署形态。 5. 如果某些服务需要联动启动,应通过脚本、Makefile、docker compose 或开发编排器去启动多个二进制,而不是把进程边界打穿。 6. 带 worker 的服务可以继续保留多入口角色,例如 `api` / `worker` / `all`,但它们仍然是同一服务的不同可执行角色,不是把多个服务硬塞进一个进程。 +7. MySQL / Redis 容器的启动归 `docker compose` 或运维层;Go 服务只负责在自己的进程里建立连接、做自己的 AutoMigrate 和连通性检查。 ### 6.11 测试自动化与 smoke 权限边界 @@ -864,7 +879,7 @@ graph TD 1. **注册测试账号**:允许通过本地或测试环境接口创建临时 smoke 账号,用于登录、鉴权、token quota、agent chat、主动调度等链路验证。 2. **curl 调接口**:允许用 `curl` 调本地 `gateway` 或本地服务接口,验证 HTTP 状态码、响应结构、业务状态流转和错误码。 -3. **docker exec 查 MySQL**:允许通过 `docker exec` 进入本地 MySQL 容器执行只读查询,核对用户、任务、日程、outbox、notification_records、memory_jobs 等表里的状态。 +3. **docker exec 查 MySQL / Redis**:允许通过 `docker exec` 进入本地 MySQL 或 Redis 容器执行查询,核对用户、任务、日程、outbox、notification_records、memory_jobs、黑名单键、额度快照等状态。 默认测试账号规则: @@ -881,13 +896,14 @@ graph TD 4. 用 `docker exec` + `SELECT` / `SHOW` 查询本地 MySQL 状态。 5. 查看 outbox 是否写入、是否投递、是否被对应服务消费。 6. 查看 notification、schedule、task、memory 等关键表状态是否符合预期。 +7. 用 `docker exec` 查询本地 Redis 的黑名单、额度快照、幂等键等迁移相关状态。 默认不允许的 smoke 操作: 1. 不访问生产环境接口。 2. 不使用真实用户账号或真实第三方 webhook 做自动化验证。 3. 不直接执行 `DROP`、`TRUNCATE`、`DELETE`、`UPDATE`、`ALTER` 这类破坏性或批量数据库操作。 -4. 不直接清空 outbox、任务、日程、用户、通知、记忆等业务表。 +4. 不直接清空 outbox、任务、日程、用户、通知、记忆等业务表,也不随意清 Redis 业务键,除非是临时 smoke 数据且已经明确回收。 5. 不把 token、密码、API key、webhook 地址等敏感值写进文档、提交信息或最终报告。 如果 smoke 需要清理测试数据,优先使用专门测试库、测试前缀、接口级清理能力或一次性测试环境;直接改库清理必须单独确认。 @@ -897,10 +913,10 @@ graph TD 1. 启动了哪些进程或服务。 2. 注册/登录使用的是哪类测试账号前缀,不输出密码。 3. curl 调用了哪些接口、返回了哪些关键状态码。 -4. docker exec 查询了哪些表、验证了哪些关键字段。 +4. docker exec 查询了哪些表或 Redis 键、验证了哪些关键字段。 5. 哪些检查通过,哪些失败,失败时保留可复现命令或最小上下文。 -如果本轮还执行了 `go test`,测试结束后必须清理项目根目录下的 `.gocache`,保持仓库整洁。 +如果本轮还执行了 `go test ./...`,测试结束后必须清理工作区里的 `.gocache`,保持仓库整洁。 ### 6.12 多代理并行执行闭环 @@ -908,21 +924,22 @@ graph TD 子代理统一配置: -1. 子代理默认统一使用 `gpt-5.4`,reasoning effort 统一使用 `xhigh`。 +1. 如果需要开子代理,主代理必须在工具调用里显式指定 `model=gpt-5.4`、`reasoning_effort=xhigh`,不能依赖默认配置。 2. 除非用户明确指定其他模型,否则迁移实现、代码评审和真实 smoke 子代理都按这个配置开。 3. 主代理给子代理的任务必须写清楚目标、文件范围、禁止改动范围、验收标准和最终输出格式。 整体闭环: -1. **主代理先定边界**:读取本计划和当前代码,明确本轮只处理一个阶段、一个能力域或一类公共件。 -2. **主代理拆分任务**:把任务拆成互不冲突的子任务,并给每个子代理指定明确文件范围、责任边界和验收标准。 +1. **主代理先定边界**:读取本计划和当前代码,明确本轮只处理一个阶段、一个能力域或一类公共件;不要过度翻文件,只读当前判断和实施必需的文件。 +2. **主代理拆分任务**:把任务拆成互不冲突的子任务,并给每个子代理指定明确文件范围、责任边界和验收标准;关键阻塞任务由主代理自己做,不甩给子代理。 3. **子代理并行推进**:多个子代理可以并行实现不同服务、不同适配层或不同测试补强,但禁止同时修改同一批核心文件。 4. **主代理耐心等待**:主代理必须等并行推进的子代理返回完整结果后,再进入统一收口;等待期间可以做不冲突的上下文整理,但不能重复实现、覆盖或抢跑子代理负责的任务。 5. **主代理统一收口**:主代理负责合并实现、处理接口命名、依赖注入、配置、启动入口和跨服务契约一致性。 -6. **子代理并行 code review + 真实测试**:实现收口后,再开子代理分别做代码评审和真实 smoke;code review 聚焦 bug、边界、回归风险,真实测试按 `6.11` 执行。 -7. **主代理再次等待**:主代理必须等 review 子代理和测试子代理都给出结论后,再判断是否修复、回退或扩大测试面。 -8. **主代理修主要问题**:主代理根据评审和 smoke 结果修复阻塞问题、高风险问题和明确回归。 -9. **主代理最终复核**:复跑关键测试,汇总本轮迁了什么、旧实现保留在哪里、切流点在哪里、下一轮建议继续处理什么。 +6. **实现收口先停下**:涉及独立服务、启动入口、配置或迁移边界时,主代理实现收口后先停下来,让用户重启相关服务。 +7. **子代理并行 code review + 真实测试**:用户确认服务已重启后,再开子代理分别做代码评审和真实 smoke;code review 聚焦 bug、边界、回归风险,真实测试按 `6.11` 执行。 +8. **主代理再次等待**:主代理必须等 review 子代理和测试子代理都给出结论后,再判断是否修复、回退或扩大测试面。 +9. **主代理修主要问题**:主代理根据评审和 smoke 结果修复阻塞问题、高风险问题和明确回归。 +10. **主代理最终复核**:复跑关键测试,汇总本轮迁了什么、旧实现保留在哪里、切流点在哪里、下一轮建议继续处理什么。 子代理拆分原则: @@ -963,7 +980,26 @@ graph TD --- -### 6.13 阶段 0 历史基线与阶段 1 当前基线快照 +### 6.13 后续接手硬规则 + +这段用于避免后续代理重复踩阶段 2 已经纠偏过的问题。 + +1. 阶段 3 起步默认是 `notification`,不是 outbox、llm-service、rag-service 或 user/auth。 +2. 主代理负责 leader:先读必要文档和代码,拆任务,关键阻塞任务自己做;子代理只能承担并行、明确、非阻塞的侧翼任务。 +3. 如果确实有会影响切分方向的不确定点,先总结成拍板点问用户;文档已经写清楚的内容不要重复问。 +4. 查库一律用 `docker exec`。MySQL / Redis 都按这个规则走;不直接用本机客户端绕过容器。 +5. 跑完 `go test ./...` 后必须清理工作区 `.gocache`。 +6. 不擅自回滚、覆盖、删除用户或其他代理的无关改动。 +7. 不主动 `git commit` / `git branch`,除非用户明确要求。 +8. 服务间错误传递优先使用 go-zero / gRPC 内置 `error`,调用侧保持 `res, err :=` 风格;API 层对前端错误继续复用 `respond`,后续总收尾再考虑迁到 gateway/shared。 +9. MySQL / Redis 容器启动归 `docker compose` 或运维层;Go 服务只负责自己的连接初始化、AutoMigrate 和运行时依赖。 +10. gateway 只做边缘转发、鉴权和轻量组合;不要把核心业务表、服务内部 Redis key、JWT 签发、额度账本放回 gateway。 +11. 新服务开发可以和后续迁移并行,但必须独立目录、端口、配置和契约,不能污染正在迁移的服务边界。 +12. 每一轮结构迁移最终都必须说明:本轮迁了什么、旧实现还保留什么、当前切流点在哪里、下一轮建议迁什么。 + +--- + +### 6.14 阶段 0 历史基线与阶段 1/2 当前基线快照 阶段 0 历史基线: @@ -982,6 +1018,21 @@ graph TD 5. 当前仍是单体内多 worker 装配,不代表服务已经物理拆出;后续拆服务时,要把对应 outbox 表、relay、topic、consumer group 和 handler 一起迁到服务进程里。 6. `agent_outbox_messages` 沿用历史默认表名作为 `agent` 服务 outbox 表,历史存量不代表新事件仍使用共享 outbox。 +阶段 1.5 / 1.6 当前基线: + +1. `backend/services/llm` 是当前统一模型出口,`backend/infra/llm` 的 `.go` 旧实现已删除。 +2. `backend/services/rag` 是当前统一检索基础设施入口,`backend/infra/rag` 的 `.go` 旧实现已删除。 +3. 两者仍在单体进程内由 `cmd/start.go` 装配,不代表已经拆成 gozero 独立进程;但服务内 canonical 入口已经完成,后续不要再回头重迁。 + +阶段 2 当前基线: + +1. `backend/cmd/userauth/main.go` 是 userauth 独立进程入口。 +2. `backend/services/userauth` 拥有 user/auth 核心业务、DAO、模型、JWT、黑名单、额度治理、zrpc server 和 token 记账幂等表。 +3. `backend/gateway/userapi` 是 HTTP user 入口,`backend/gateway/userauth` 是 zrpc client,`backend/gateway/middleware` 只调 userauth 做鉴权和额度门禁。 +4. `backend/shared/contracts/userauth` 和 `backend/shared/ports` 只承载跨层契约,不承载服务私有业务实现。 +5. `cmd/all` 不再迁 `users`,`cmd/userauth` 自己迁 `users` 和 `user_token_usage_adjustments`。 +6. 完整本地 smoke 需要同时启动 `cmd/all` 和 `cmd/userauth`。 + --- ## 7. 风险与回退 @@ -1040,3 +1091,17 @@ graph TD 1. 以新事件写入表、topic 和 consumer group 为准判断当前链路,不用旧存量判断新路由。 2. 历史数据清理或归档单独立项,不和服务拆分混在同一轮做。 3. 如需清理本地测试存量,必须遵守 `6.11` 的数据库操作边界,破坏性操作单独确认。 + +### 风险 5:阶段 2 样板被误回退 + +表现: + +1. 后续代理看到旧日志、旧文档或历史文件名,误以为 user/auth 还在 Gin 单体里。 +2. 新增 gateway handler 时又直接 import userauth DAO/model、直接读写 `users` 或 Redis 黑名单。 +3. 把 zrpc client 放进 `cmd`,让进程入口承担跨服务调用语义。 + +处理: + +1. 以当前编译入口和路由装配为准:`gateway/userapi` + `gateway/userauth` + `services/userauth` 是阶段 2 当前样板。 +2. 任何 user/auth 新能力先放进 `services/userauth`,gateway 只做 HTTP 适配和 respond 响应。 +3. 如果 user/auth 调用失败,先查 `cmd/userauth` 是否启动、zrpc endpoint 是否正确、服务内 MySQL/Redis 是否可连,不要把逻辑搬回 gateway。