# 系统设计方案第一阶段对齐重构 Implementation Plan > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让工站域、编排域和数据库 schema 全部对齐 `系统设计方案.md` 的第一阶段语义,并保持现有执行内核可继续运行。 **Architecture:** 先统一领域常量和 ent schema,再用适配层收口 `station` 与 `processor` 的公开语义,最后通过迁移脚本和回归测试完成旧字段到新模型的平滑切换。第一阶段不接入 Redis SSOT、不实现事件重放,也不完整落地三层调度器,只为这些能力预埋模型与边界。 **Tech Stack:** Go 1.23.4, go-zero, ent v0.14.5, PostgreSQL, testify --- ## 文件结构 ### 必改文件 - Modify: `constants/constants.go` — 统一第一阶段使用的 `StationStatus`、`PositionType`、必要的 `TaskStatus` 与补充步骤类型。 - Modify: `schema/job.go` — `job` 主模型对齐文档,新增 `version`、`lastEventId`,降级旧位置字段为兼容字段。 - Modify: `schema/recipe_step.go` — `recipe_step` 从基于 index 的流转改为以 `step_id` 为主。 - Modify: `schema/gen.go` — 仅确认生成入口,无语义改动。 - Modify: `internal/station/interface.go` — 公共 `Station` 契约切到文档语义。 - Modify: `internal/station/base.go` — 保留槽位内部实现,但不再作为上层公共 API 依赖。 - Modify: `internal/station/registry.go` — Registry 改为 string ID / 新接口语义。 - Modify: `internal/station/cnc.go` - Modify: `internal/station/washer.go` - Modify: `internal/station/inspection.go` - Modify: `internal/station/deburr.go` - Modify: `internal/station/cleaning.go` - Modify: `internal/station/sampling.go` - Modify: `internal/station/dock.go` — 具体站点适配新接口。 - Modify: `internal/processor/interface.go` — 顶层编排接口输出新领域状态。 - Modify: `internal/processor/task.go` — 拆分领域 Task 与执行对象。 - Modify: `internal/processor/job_runtime.go` — 运行时状态与 DB 状态统一到文档语义。 - Modify: `internal/processor/job_processor.go` — 改造成第一阶段编排门面。 - Modify: `internal/processor/dispatcher.go` — 消费新的 TaskExecution 结构。 - Modify: `internal/processor/recipe_loader.go` — 读取新 `recipe_step` 字段与 `step_id` 语义。 - Modify: `internal/svc/service_context.go` — 注入与恢复逻辑适配新接口。 ### 新建文件 - Create: `schema/task.go` — Task 领域持久化模型。 - Create: `schema/job_step_instance.go` — Job 步骤尝试履历模型。 - Create: `internal/station/adapter.go` — 旧槽位站点到新 `Station` 语义的适配层。 - Create: `internal/processor/task_model.go` — 领域 Task、位置值对象、TaskStatus。 - Create: `internal/processor/job_runtime_status.go` — `JobRuntime` 内部状态与文档状态映射收口。 - Create: `internal/processor/repositories.go` — 新旧字段映射与 repository 收口点。 - Create: `internal/station/interface_test.go` — 新接口语义测试。 - Create: `internal/station/registry_test.go` — string ID registry 测试。 - Create: `internal/processor/task_test.go` — 任务状态机与领域/执行对象分离测试。 ### 现有测试要改 - Modify: `internal/processor/job_runtime_test.go` - Modify: `internal/processor/temp_slot_allocator_test.go` — 仅保留与“终态释放槽位”一致的断言。 - Modify: `internal/processor/replenisher_test.go` — 如受位置语义影响则同步更新。 --- ## Chunk 1: 常量与 Schema 对齐 ### Task 1: 统一领域常量与状态枚举 **Files:** - Modify: `constants/constants.go` - Test: `internal/processor/job_runtime_test.go` - Test: `internal/station/interface_test.go` **Depends on:** 无 - [ ] **Step 1: 写失败测试,锁定新状态语义** ```go func TestStationStatusValuesMatchPhase1Design(t *testing.T) { assert.Equal(t, []string{"IDLE", "BUSY", "WAITING", "FAULT"}, constants.StationStatus("").Values()) } func TestPositionTypeValuesIncludeDockAndSamplingCompatibility(t *testing.T) { values := constants.PositionType("").Values() assert.Contains(t, values, "ON_EQUIPMENT") assert.Contains(t, values, "ON_BUFFER") assert.Contains(t, values, "IN_HAND") } ``` - [ ] **Step 2: 运行测试确认失败** Run: `rtk go test ./internal/processor/... ./internal/station/...` Expected: FAIL,提示 `StationStatus` 仍为 `PROCESSING/DONE/ERROR` 或缺少新枚举。 - [ ] **Step 3: 最小化修改常量定义** - 将 `StationStatus` 改为 `IDLE/BUSY/WAITING/FAULT` - 新增 `TaskStatus` 枚举 - 如需要,补充 `PositionType_OnDock`、`PositionType_OnSampling` 作为兼容值 - 不修改与本阶段无关的常量 - [ ] **Step 4: 运行测试确认通过** Run: `rtk go test ./internal/processor/... ./internal/station/...` Expected: PASS 或仅剩 schema/编译相关失败。 - [ ] **Step 5: 记录检查点** - 非 git 仓库,跳过 commit - 在执行日志中记录“常量层已切到文档语义” ### Task 2: 重构 `job`、`recipe_step` 并新增 `task` / `job_step_instance` **Files:** - Modify: `schema/job.go` - Modify: `schema/recipe_step.go` - Create: `schema/task.go` - Create: `schema/job_step_instance.go` - Modify: `schema/gen.go` **Depends on:** Task 1 - [ ] **Step 1: 先写 schema 层断言测试或编译断言** ```go func TestJobStatusDefaultsMatchPhase1Design(t *testing.T) { assert.Equal(t, string(constants.JobStatus_Created), string(constants.JobStatus_Created)) } ``` 说明:ent schema 当前缺少直接单测入口,这一步以最小编译断言 + 生成后编译为主。 - [ ] **Step 2: 修改 `schema/job.go`** - 保留:`workOrderId/productTypeId/recipeId/currentStepId/status/positionType/positionRefId/context/priority/suspendedReason/createdTime/lastUpdated` - 新增:`version`、`lastEventId` - 旧字段 `currentStepIndex/tempSlotNo/onMachineId/onSlot/dockNo/slotNo` 保留但标注兼容用途 - [ ] **Step 3: 修改 `schema/recipe_step.go`** - 引入 `stepId` 作为业务步骤标识 - `nextStepDefault`、`nextStepBranches` 改为表达 step id - 新增 `stepTimeout` - 保留 `stepIndex` 仅用于排序 - [ ] **Step 4: 新增 `schema/task.go` 与 `schema/job_step_instance.go`** - `task` 包含 `id/jobId/type/status/fromPos/toPos/assignedRobot/startedTime/completedTime/result` - `job_step_instance` 包含 `jobId/stepId/attemptNo/status/equipmentId/startTime/endTime/result` - [ ] **Step 5: 生成 ent 代码** Run: `cd schema && rtk go generate` Expected: `ent code generated successfully!` - [ ] **Step 6: 编译确认新 schema 可生成** Run: `rtk go test ./schema/...` Expected: PASS 或输出“[no test files]”,但不能有 ent 生成/编译错误。 - [ ] **Step 7: 记录检查点** - 标记“schema 已能表达第一阶段语义” ### Task 3: 收口旧字段到新字段的迁移入口 **Files:** - Create: `internal/processor/repositories.go` - Modify: `internal/processor/recipe_loader.go` - Test: `internal/processor/task_test.go` **Depends on:** Task 2 - [ ] **Step 1: 写失败测试,锁定旧字段到新模型的映射** ```go func TestBuildPositionFromLegacyFieldsPrefersNewFields(t *testing.T) { job := legacyJobFixture() pos := buildPosition(job) assert.Equal(t, "ON_BUFFER", pos.Type) } ``` - [ ] **Step 2: 运行单测确认失败** Run: `rtk go test ./internal/processor/...` Expected: FAIL,提示缺少 `buildPosition` 或仍直接依赖旧字段。 - [ ] **Step 3: 新建 `repositories.go`** - 提供从 ent.Job 到新领域 `JobSnapshot` / `PositionRef` / `TaskRecord` 的转换 - 把 `currentStepIndex -> currentStepId`、旧位置字段 -> `positionType + positionRefId` 的兼容逻辑集中放这里 - `recipe_loader.go` 改为优先使用 `stepId` - [ ] **Step 4: 运行 processor 测试** Run: `rtk go test ./internal/processor/...` Expected: 旧失败减少,映射逻辑测试通过。 - [ ] **Step 5: 记录检查点** - 标记“新旧字段映射已集中,不允许继续散落在业务代码里” --- ## Chunk 2: 工站接口对齐 ### Task 4: 切换 `Station` 公开接口到文档语义 **Files:** - Modify: `internal/station/interface.go` - Create: `internal/station/adapter.go` - Create: `internal/station/interface_test.go` **Depends on:** Chunk 1 完成 - [ ] **Step 1: 写失败测试,锁定新接口签名与行为** ```go func TestStationAdapterExposesDocumentInterface(t *testing.T) { st := newFakeStationAdapter() assert.Equal(t, "CNC_1", st.ID()) assert.Equal(t, constants.StationStatus_Idle, st.GetStatus()) assert.True(t, st.CanAccept("A6VM160")) } ``` - [ ] **Step 2: 运行 station 测试确认失败** Run: `rtk go test ./internal/station/...` Expected: FAIL,提示 `ID()` 返回 int、`GetStatus()` 签名不匹配或 `CanAccept` 缺参。 - [ ] **Step 3: 修改 `interface.go` 并新增 `adapter.go`** - `Station` 改为 `ID() string`、`GetStatus() constants.StationStatus`、`CanAccept(jobType string) bool`、`Execute(cmd StationCommand) (string, error)` - 保留 `StationCommand` / `StationEvent`,但字段值语义切到文档语言 - 用适配层包住旧 `BaseStation` 的槽位实现 - [ ] **Step 4: 运行 station 测试确认通过** Run: `rtk go test ./internal/station/...` Expected: PASS 或仅剩 registry/具体站点适配失败。 - [ ] **Step 5: 记录检查点** - 标记“公开工站语义已与文档一致,槽位只留在内部实现” ### Task 5: 调整 Registry 与具体站点实现 **Files:** - Modify: `internal/station/registry.go` - Modify: `internal/station/base.go` - Modify: `internal/station/cnc.go` - Modify: `internal/station/washer.go` - Modify: `internal/station/inspection.go` - Modify: `internal/station/deburr.go` - Modify: `internal/station/cleaning.go` - Modify: `internal/station/sampling.go` - Modify: `internal/station/dock.go` - Create: `internal/station/registry_test.go` **Depends on:** Task 4 - [ ] **Step 1: 写失败测试,锁定 string ID registry 行为** ```go func TestRegistryGetByStringID(t *testing.T) { reg := NewStationRegistry() reg.Register(newFakeStation("CNC_1", "CNC")) st, ok := reg.Get("CNC_1") assert.True(t, ok) assert.Equal(t, "CNC", st.Type()) } ``` - [ ] **Step 2: 运行测试确认失败** Run: `rtk go test ./internal/station/...` Expected: FAIL,提示 registry 仍使用 `map[int]Station`。 - [ ] **Step 3: 改造 registry 和具体站点** - `StationRegistry` 改为 `map[string]Station` - `BuildProductionLine` 统一产出 `CNC_1`、`CNC_2` 等 string 标识 - `BaseStation` 内部继续维护槽位数组,但对外状态映射为 `IDLE/BUSY/WAITING/FAULT` - 具体站点的 `Execute` 返回 string 型位点引用 - [ ] **Step 4: 运行 station 测试** Run: `rtk go test ./internal/station/...` Expected: PASS。 - [ ] **Step 5: 运行依赖 station 的基础编译测试** Run: `rtk go test ./internal/svc/... ./internal/processor/...` Expected: 可能有 processor 适配失败,但 station 引起的签名错误应明显减少。 --- ## Chunk 3: 编排模型与执行内核适配 ### Task 6: 引入领域 Task 模型并拆分执行对象 **Files:** - Create: `internal/processor/task_model.go` - Modify: `internal/processor/task.go` - Create: `internal/processor/task_test.go` - Modify: `internal/processor/dispatcher.go` **Depends on:** Chunk 2 完成 - [ ] **Step 1: 写失败测试,锁定 Task 状态机** ```go func TestTaskStatusFlow(t *testing.T) { task := NewTaskRecord("task-1", 1001) task.MarkDispatched() task.MarkRunning() task.MarkSuccess(map[string]any{"slot": "BUFFER_1"}) assert.Equal(t, TaskStatusSuccess, task.Status) } ``` - [ ] **Step 2: 运行测试确认失败** Run: `rtk go test ./internal/processor/...` Expected: FAIL,提示缺少领域 Task 结构或 `RobotTask` 混合职责。 - [ ] **Step 3: 新建 `task_model.go` 并改 `task.go`** - 在 `task_model.go` 定义 `TaskRecord`、`PositionRef`、`TaskStatus` - `task.go` 保留 `RobotTask` / `TaskExecution`,只持有执行闭包与运行时引用 - `dispatcher.go` 改为消费执行对象,同时在回调中推进领域 Task 状态 - [ ] **Step 4: 运行 processor 测试确认通过** Run: `rtk go test ./internal/processor/...` Expected: Task 状态机测试通过。 - [ ] **Step 5: 记录检查点** - 标记“数据库/接口看 Task,dispatcher 看执行对象” ### Task 7: 统一 `JobRuntime` 的领域状态与 DB 状态 **Files:** - Create: `internal/processor/job_runtime_status.go` - Modify: `internal/processor/job_runtime.go` - Modify: `internal/processor/job_runtime_test.go` **Depends on:** Task 6 - [ ] **Step 1: 改写失败测试为文档状态** ```go func TestRuntimeStateToDomainStatus(t *testing.T) { assert.Equal(t, constants.JobStatus_OnBuffer, runtimeStateToDomainStatus(RuntimeStateOnBuffer)) assert.Equal(t, constants.JobStatus_WaitingUnload, runtimeStateToDomainStatus(RuntimeStateWaitingUnload)) } ``` - [ ] **Step 2: 运行测试确认失败** Run: `rtk go test ./internal/processor/...` Expected: FAIL,提示仍存在 `Running/Paused/Error` 这套旧状态映射。 - [ ] **Step 3: 新建 `job_runtime_status.go` 并修改 `job_runtime.go`** - 明确定义运行时内部状态与文档领域状态映射 - `Pause/Resume/Suspend/Cancel` 全部改为写文档状态 - 暂存位释放规则仅在 `COMPLETED/SCRAPPED` 时触发 - [ ] **Step 4: 运行 processor 测试** Run: `rtk go test ./internal/processor/...` Expected: 现有 `job_runtime_test.go` 与新状态测试通过。 - [ ] **Step 5: 记录检查点** - 标记“JobRuntime 已降级为执行会话对象,不再自带公开业务世界观” ### Task 8: 将 `JobProcessor` 改为第一阶段编排门面 **Files:** - Modify: `internal/processor/interface.go` - Modify: `internal/processor/job_processor.go` - Modify: `internal/svc/service_context.go` **Depends on:** Task 7 - [ ] **Step 1: 写失败测试或编译断言,锁定新编排输出语义** ```go func TestOrderProcessorUsesDocumentStatusVocabulary(t *testing.T) { // 编译期约束:接口返回值与监控结构只能使用文档状态 } ``` - [ ] **Step 2: 运行 processor / svc 测试确认失败** Run: `rtk go test ./internal/processor/... ./internal/svc/...` Expected: FAIL,提示 `StationRegistry.Get`、恢复逻辑、状态输出仍依赖旧接口。 - [ ] **Step 3: 修改 `interface.go`、`job_processor.go`、`service_context.go`** - `OrderProcessorInterface` 对外只暴露文档语义 - `JobProcessor` 内部用 repository、recipe resolver、runtime factory 的方式收口职责 - `restoreActiveOrders()` 保持现有入口,但恢复读取新 `currentStepId/position` 模型 - [ ] **Step 4: 运行核心测试** Run: `rtk go test ./internal/processor/... ./internal/svc/...` Expected: PASS 或仅剩数据迁移/fixture 不一致问题。 --- ## Chunk 4: 数据迁移、回归验证与收尾 ### Task 9: 实现一次性数据回填与兼容读取验证 **Files:** - Modify: `internal/processor/repositories.go` - Modify: `internal/processor/recipe_loader.go` - Test: `internal/processor/task_test.go` - Test: `internal/processor/replenisher_test.go` **Depends on:** Chunk 3 完成 - [ ] **Step 1: 写失败测试,覆盖旧数据兼容读取** ```go func TestLegacyJobFieldsBackfillToCurrentStepID(t *testing.T) { snap := buildJobSnapshot(legacyJobFixture()) assert.Equal(t, "30", snap.CurrentStepID) } ``` - [ ] **Step 2: 运行测试确认失败** Run: `rtk go test ./internal/processor/...` Expected: FAIL,提示仍依赖 `currentStepIndex` 或旧位置字段。 - [ ] **Step 3: 实现回填与兼容读取** - 优先读取 `currentStepId/positionType/positionRefId` - 仅当新字段为空时回退旧字段 - 将回填逻辑收口到 repository / loader,不让上层分散判断 - [ ] **Step 4: 运行 processor 测试** Run: `rtk go test ./internal/processor/...` Expected: PASS。 ### Task 10: 完成端到端回归验证 **Files:** - Modify: `internal/processor/job_runtime_test.go` - Modify: `internal/processor/replenisher_test.go` - Modify: `internal/processor/temp_slot_allocator_test.go` - Test: `internal/station/interface_test.go` - Test: `internal/station/registry_test.go` **Depends on:** Task 9 - [ ] **Step 1: 补充关键路径测试** - 工单启动进入首个文档状态 - 扫码成功进入 `ON_BUFFER` - 扫码失败进入 `SUSPENDED` - 加工完成进入 `WAITING_UNLOAD` - `COMPLETED/SCRAPPED` 释放暂存位 - Task 生命周期完整走通 - [ ] **Step 2: 运行定向测试** Run: `rtk go test ./internal/station/... ./internal/processor/...` Expected: PASS。 - [ ] **Step 3: 运行更大范围回归** Run: `rtk go test ./...` Expected: PASS;如果现有无关模块失败,需记录为已有问题并隔离确认不由本计划引入。 - [ ] **Step 4: 运行覆盖率检查** Run: `rtk go test -cover ./internal/processor/... ./internal/station/...` Expected: 输出覆盖率,新增逻辑路径有明确测试覆盖。 - [ ] **Step 5: 人工验收清单** - `Station` 对外不再暴露槽位操作 API - `Job`/`Task`/`RecipeStep` 术语与文档一致 - `JobProcessor` 仍可作为运行入口 - 没有引入 Redis SSOT、事件重放或三层调度的半成品实现 --- ## 风险点 1. **`StationRegistry` 从 int ID 切到 string ID 会波及 processor 与恢复逻辑** - 处理:先落适配层,再统一替换调用点。 2. **`currentStepIndex` 到 `currentStepId` 的切换容易引入 recipe 跳转错误** - 处理:在 `recipe_loader` 中集中转换,先保留 `stepIndex` 做排序。 3. **`RobotTask` 与领域 Task 拆分后可能出现状态不同步** - 处理:所有状态推进统一经由 `TaskRecord` 方法,不允许 dispatcher 直接写裸字段。 4. **终态释放暂存位的规则可能和现有补料测试冲突** - 处理:先改测试,确保只有 `COMPLETED/SCRAPPED` 释放。 ## 验证点 - `internal/station/...` 全量测试通过 - `internal/processor/...` 全量测试通过 - `cd schema && rtk go generate` 成功 - `rtk go test ./...` 无新增回归 - 关键状态词只出现文档语义:`IDLE/BUSY/WAITING/FAULT`、`CREATED/IN_HANDLING/...` ## 执行说明 - 当前目录不是 git 仓库,跳过“频繁 commit”步骤,改为每个任务结束记录检查点。 - 当前环境无法使用基于 worktree 的审阅代理;执行时如需审阅,采用当前会话内人工检查或普通子代理替代。 - 严格遵守项目规则:只改 `schema/*.go` 后再生成 ent;不要手改 `ent/`。 Plan complete and saved to `docs/superpowers/plans/2026-05-01-system-design-alignment-phase1.md`. Ready to execute?