19 KiB
系统设计方案第一阶段对齐重构 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: 写失败测试,锁定新状态语义
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 层断言测试或编译断言
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.gotask包含id/jobId/type/status/fromPos/toPos/assignedRobot/startedTime/completedTime/resultjob_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: 写失败测试,锁定旧字段到新模型的映射
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
- 提供从 ent.Job 到新领域
-
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: 写失败测试,锁定新接口签名与行为
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.goStation改为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 行为
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]StationBuildProductionLine统一产出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 状态机
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: 改写失败测试为文档状态
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: 写失败测试或编译断言,锁定新编排输出语义
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.goOrderProcessorInterface对外只暴露文档语义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: 写失败测试,覆盖旧数据兼容读取
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对外不再暴露槽位操作 APIJob/Task/RecipeStep术语与文档一致JobProcessor仍可作为运行入口- 没有引入 Redis SSOT、事件重放或三层调度的半成品实现
风险点
StationRegistry从 int ID 切到 string ID 会波及 processor 与恢复逻辑- 处理:先落适配层,再统一替换调用点。
currentStepIndex到currentStepId的切换容易引入 recipe 跳转错误- 处理:在
recipe_loader中集中转换,先保留stepIndex做排序。
- 处理:在
RobotTask与领域 Task 拆分后可能出现状态不同步- 处理:所有状态推进统一经由
TaskRecord方法,不允许 dispatcher 直接写裸字段。
- 处理:所有状态推进统一经由
- 终态释放暂存位的规则可能和现有补料测试冲突
- 处理:先改测试,确保只有
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?