830 lines
23 KiB
Markdown
830 lines
23 KiB
Markdown
# current_step_id 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:** 将 job 表的运行时步骤标识从 `currentStepIndex`(int) 迁移到 `currentStepId`(string),消除 AdvanceStep 中 stepId↔stepIndex 的冗余转换。
|
|||
|
|
|
|||
|
|
**Architecture:** job 表新增 `currentStepId` 字段,旧字段 `currentStepIndex` 保留但不写入。`AdvanceStep`/`CompleteStep` 改为写入 stepId,`resolveBranch` 直接返回 stepId(无需二次查询转换)。核心读路径新增 `GetRecipeStepByID` 用 stepId 直接查询。scheduler 层 `StepView`/`CandidateTask` 加 `StepID` 字段。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Go 1.25, ent ORM v0.14.5, PostgreSQL
|
|||
|
|
|
|||
|
|
**Spec:** `docs/superpowers/specs/2026-05-16-current-step-id-design.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## File Structure
|
|||
|
|
|
|||
|
|
| File | Action | Responsibility |
|
|||
|
|
|------|--------|---------------|
|
|||
|
|
| `schema/job.go` | Modify | 新增 `currentStepId` 字段 |
|
|||
|
|
| `internal/eventloop/dbstate.go` | Modify | 核心方法:`AdvanceStep`/`CompleteStep` 改为 stepId |
|
|||
|
|
| `internal/eventloop/loop.go` | Modify | `RuntimeSnapshot` 加 `StepID` |
|
|||
|
|
| `internal/eventloop/scheduler_bridge.go` | Modify | `buildJobViews` 传 stepId |
|
|||
|
|
| `internal/eventloop/candidate_handlers.go` | Modify | 用 stepId 查询 |
|
|||
|
|
| `internal/scheduler/types.go` | Modify | `StepView`/`CandidateTask` 加 `StepID` |
|
|||
|
|
| `internal/scheduler/generator.go` | Modify | 填充 `StepID` |
|
|||
|
|
| `internal/types/types.go` | Modify | API 类型加 `CurrentStepId` |
|
|||
|
|
| `internal/processor/job_processor.go` | Modify | `ReworkJob` 用 stepId |
|
|||
|
|
| `internal/processor/interface.go` | Modify | `ReworkJob` 签名变更 |
|
|||
|
|
| `cmd/mockrun/main.go` | Modify | `SetCurrentStepId` |
|
|||
|
|
| `frontend/src/api/temp-station/index.ts` | Modify | 类型加 `currentStepId` |
|
|||
|
|
| `frontend/src/api/dock/index.ts` | Modify | 类型加 `currentStepId` |
|
|||
|
|
| `schema/tools/migrate_step_id.go` | Create | 数据迁移 SQL |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 1: Schema + Migration
|
|||
|
|
|
|||
|
|
### Task 1.1: Add currentStepId field to job schema
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `schema/job.go:35`
|
|||
|
|
- Create: `schema/tools/migrate_step_id.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add field to schema**
|
|||
|
|
|
|||
|
|
In `schema/job.go`, after line 35 (`field.Int("currentStepIndex")...`):
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
field.String("currentStepId").MaxLen(20).Default("").Comment("当前步骤业务ID(OP10-OP120)"),
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Regenerate ent code**
|
|||
|
|
|
|||
|
|
Run: `cd schema && go generate`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Verify ent generated SetCurrentStepId/CurrentStepId**
|
|||
|
|
|
|||
|
|
Run: `rtk grep "SetCurrentStepId" ent/job/`
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Create migration SQL file**
|
|||
|
|
|
|||
|
|
Create `schema/tools/migrate_step_id.go`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package tools
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"log/slog"
|
|||
|
|
|
|||
|
|
"hougai/ent"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func MigrateCurrentStepID(ctx context.Context, client *ent.Client) error {
|
|||
|
|
rows, err := client.Job.Update().
|
|||
|
|
Where(func(s *sql.Selector) {
|
|||
|
|
s.Where(sql.ExprP("current_step_id = '' OR current_step_id IS NULL"))
|
|||
|
|
}).
|
|||
|
|
Save(ctx)
|
|||
|
|
// Use raw SQL for migration:
|
|||
|
|
// UPDATE job SET current_step_id = rs.step_id
|
|||
|
|
// FROM recipe_step rs
|
|||
|
|
// WHERE job.recipe_id = rs.recipe_id AND job.current_step_index = rs.step_index
|
|||
|
|
// AND (job.current_step_id = '' OR job.current_step_id IS NULL);
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Wait—ent doesn't support cross-table UPDATE easily. Use raw SQL via ent client:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package tools
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"database/sql"
|
|||
|
|
"fmt"
|
|||
|
|
"log/slog"
|
|||
|
|
|
|||
|
|
"hougai/ent"
|
|||
|
|
"hougai/ent/job"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// MigrateCurrentStepID 将现有 job 的 current_step_index 转换为 current_step_id
|
|||
|
|
func MigrateCurrentStepID(ctx context.Context, client *ent.Client) error {
|
|||
|
|
db := client.Job.Query().UnderlyingDB()
|
|||
|
|
|
|||
|
|
// 先检查是否有未迁移的 job
|
|||
|
|
var count int
|
|||
|
|
err := db.QueryRowContext(ctx,
|
|||
|
|
`SELECT COUNT(*) FROM job WHERE current_step_id = '' AND current_step_index > 0`,
|
|||
|
|
).Scan(&count)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("migrate currentStepId: count pending: %w", err)
|
|||
|
|
}
|
|||
|
|
if count == 0 {
|
|||
|
|
slog.Info("migrate currentStepId: nothing to migrate")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result, err := db.ExecContext(ctx, `
|
|||
|
|
UPDATE job SET current_step_id = rs.step_id
|
|||
|
|
FROM recipe_step rs
|
|||
|
|
WHERE job.recipe_id = rs.recipe_id
|
|||
|
|
AND job.current_step_index = rs.step_index
|
|||
|
|
AND job.current_step_id = ''
|
|||
|
|
`)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("migrate currentStepId: %w", err)
|
|||
|
|
}
|
|||
|
|
n, _ := result.RowsAffected()
|
|||
|
|
slog.Info("migrate currentStepId: done", "rows", n)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Wire migration into EnsureDataInitialized**
|
|||
|
|
|
|||
|
|
In `internal/svc/service_context.go`, after the existing migration calls, add:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
if err := tools.MigrateCurrentStepID(ctx, entClient); err != nil {
|
|||
|
|
return nil, fmt.Errorf("migrate currentStepId: %w", err)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add schema/job.go schema/tools/migrate_step_id.go ent/ internal/svc/service_context.go
|
|||
|
|
rtk git commit -m "feat: add currentStepId field to job schema with migration"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 2: DBState Core Methods
|
|||
|
|
|
|||
|
|
### Task 2.1: Add stepId-based query methods to DBState
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add GetRecipeStepByID method**
|
|||
|
|
|
|||
|
|
After `GetRecipeStep` (~line 268), add:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// GetRecipeStepByID 根据 stepId 获取工艺路线中指定步骤的信息
|
|||
|
|
func (d *DBState) GetRecipeStepByID(ctx context.Context, recipeID int, stepID string) (stepIndex int, stepType constants.StepType, resourceType string, toolType string, stepName string, err error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, "", "", "", "", fmt.Errorf("get recipe %d step %s: %w", recipeID, stepID, err)
|
|||
|
|
}
|
|||
|
|
return step.StepIndex, step.StepType, step.ResourceType, step.ToolType, step.StepName, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Add GetStepIDByIndex method**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// GetStepIDByIndex 根据 stepIndex 查找 stepId
|
|||
|
|
func (d *DBState) GetStepIDByIndex(ctx context.Context, recipeID, stepIndex int) (string, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("get stepId by index %d: %w", stepIndex, err)
|
|||
|
|
}
|
|||
|
|
return step.StepId, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Add getSortedStepIDs method**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// getSortedStepIDs 返回按 stepIndex 排序的 stepId 列表
|
|||
|
|
func (d *DBState) getSortedStepIDs(ctx context.Context, recipeID int) ([]string, error) {
|
|||
|
|
steps, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID)).
|
|||
|
|
Order(recipestep.ByStepIndex()).
|
|||
|
|
All(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("get sorted step IDs for recipe %d: %w", recipeID, err)
|
|||
|
|
}
|
|||
|
|
ids := make([]string, len(steps))
|
|||
|
|
for i, s := range steps {
|
|||
|
|
ids[i] = s.StepId
|
|||
|
|
}
|
|||
|
|
return ids, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/dbstate.go
|
|||
|
|
rtk git commit -m "feat: add GetRecipeStepByID, GetStepIDByIndex, getSortedStepIDs to DBState"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 2.2: Revise resolveBranch to work with stepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go:315-337`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Create resolveBranch that returns stepId**
|
|||
|
|
|
|||
|
|
Replace the existing `resolveBranch`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// resolveBranch 检查 nextStepBranches:遍历分支键,若在 ctxMap 中存在且 truthy,返回目标 stepId。
|
|||
|
|
// 返回 (stepId, true) 表示匹配成功,("", false) 表示无匹配。
|
|||
|
|
func (d *DBState) resolveBranch(ctx context.Context, recipeID int, currentStepID string, ctxMap map[string]any) (string, bool) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(currentStepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil || step.NextStepBranches == nil {
|
|||
|
|
return "", false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for key, val := range step.NextStepBranches {
|
|||
|
|
if cv, ok := ctxMap[key]; ok && isTruthy(cv) {
|
|||
|
|
stepID, ok := val.(string)
|
|||
|
|
if !ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
return stepID, true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "", false
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/dbstate.go
|
|||
|
|
rtk git commit -m "refactor: resolveBranch returns stepId directly instead of stepIndex"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 2.3: Revise AdvanceStep to use stepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go:162-239`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Rewrite AdvanceStep**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// AdvanceStep 推进到下一步:先合并 context → 匹配 nextStepBranches → 否则 nextStepDefault → 否则线性推进。
|
|||
|
|
// 推进后若下一步是 Decision/Judge 则链式内联处理。
|
|||
|
|
// 返回 true 表示已是最后一步(job 已完成)。
|
|||
|
|
func (d *DBState) AdvanceStep(ctx context.Context, jobID int, contextUpdates map[string]any, posType constants.PositionType, posRefID string) (bool, error) {
|
|||
|
|
for {
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: job %d not found: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并上下文更新
|
|||
|
|
ctxMap := jb.Context
|
|||
|
|
if ctxMap == nil {
|
|||
|
|
ctxMap = make(map[string]any)
|
|||
|
|
}
|
|||
|
|
for k, v := range contextUpdates {
|
|||
|
|
ctxMap[k] = v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确定下一步 stepId
|
|||
|
|
var nextStepID string
|
|||
|
|
currentStepID := jb.CurrentStepId
|
|||
|
|
|
|||
|
|
// 1. 优先 nextStepBranches(条件跳步)
|
|||
|
|
if branchID, matched := d.resolveBranch(ctx, jb.RecipeId, currentStepID, ctxMap); matched {
|
|||
|
|
nextStepID = branchID
|
|||
|
|
} else {
|
|||
|
|
// 2. 其次 nextStepDefault(指定跳步)
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(jb.RecipeId), recipestep.StepIdEQ(currentStepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: query current step %s: %w", currentStepID, err)
|
|||
|
|
}
|
|||
|
|
if step.NextStepDefault != "" {
|
|||
|
|
nextStepID = step.NextStepDefault
|
|||
|
|
} else {
|
|||
|
|
// 3. 线性推进:当前 stepIndex + 1 → 找对应 stepId
|
|||
|
|
nextID, err := d.GetStepIDByIndex(ctx, jb.RecipeId, step.StepIndex+1)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: no next step after %s: %w", currentStepID, err)
|
|||
|
|
}
|
|||
|
|
nextStepID = nextID
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 是否最后一步:查 nextStepID 对应的 stepIndex 与 maxStepIndex 比较
|
|||
|
|
nextIdx, err := d.GetStepIndexByStepID(ctx, jb.RecipeId, nextStepID)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: get index for %s: %w", nextStepID, err)
|
|||
|
|
}
|
|||
|
|
maxStep, err := d.GetRecipeMaxStepIndex(ctx, jb.RecipeId)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: get recipe max for job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
isLast := nextIdx > maxStep
|
|||
|
|
|
|||
|
|
if isLast {
|
|||
|
|
if posType != "" || posRefID != "" {
|
|||
|
|
upd := d.client.Job.UpdateOneID(jobID).AddVersion(1)
|
|||
|
|
if posType != "" {
|
|||
|
|
upd = upd.SetPositionType(posType)
|
|||
|
|
}
|
|||
|
|
if posRefID != "" {
|
|||
|
|
upd = upd.SetPositionRefId(posRefID)
|
|||
|
|
}
|
|||
|
|
if _, err := upd.Save(ctx); err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true, d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Completed)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetCurrentStepId(nextStepID).
|
|||
|
|
SetContext(ctxMap).
|
|||
|
|
AddVersion(1)
|
|||
|
|
|
|||
|
|
if posType != "" {
|
|||
|
|
update = update.SetPositionType(posType)
|
|||
|
|
}
|
|||
|
|
if posRefID != "" {
|
|||
|
|
update = update.SetPositionRefId(posRefID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if _, err = update.Save(ctx); err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查下一步是否可内联处理(Decision/Judge)
|
|||
|
|
_, stepType, _, _, _, err := d.GetRecipeStepByID(ctx, jb.RecipeId, nextStepID)
|
|||
|
|
if err != nil || (stepType != constants.StepType_Decision && stepType != constants.StepType_Judge) {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
contextUpdates = evaluateDecisionForJob(d, ctx, jb.RecipeId, nextStepID, ctxMap, jb.WorkpieceNo)
|
|||
|
|
posType, posRefID = "", ""
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Update evaluateDecisionForJob to accept stepId**
|
|||
|
|
|
|||
|
|
Change signature from `stepIndex int` to `stepID string`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func evaluateDecisionForJob(d *DBState, ctx context.Context, recipeID int, stepID string, ctxMap map[string]any, workpieceNo string) map[string]any {
|
|||
|
|
// Get stepIndex first since GetStepProcessingParams uses stepIndex
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
params := step.ProcessingParams
|
|||
|
|
clone := make(map[string]any, len(ctxMap)+2)
|
|||
|
|
for k, v := range ctxMap {
|
|||
|
|
clone[k] = v
|
|||
|
|
}
|
|||
|
|
evaluateDecision(params, clone, workpieceNo)
|
|||
|
|
result := make(map[string]any)
|
|||
|
|
for k, v := range clone {
|
|||
|
|
if _, ok := ctxMap[k]; !ok {
|
|||
|
|
result[k] = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/dbstate.go
|
|||
|
|
rtk git commit -m "refactor: AdvanceStep uses stepId instead of stepIndex"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 2.4: Revise CompleteStep to use stepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go:34-55`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Change CompleteStep signature**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// CompleteStep 完成当前步骤,推进到下一步 stepId
|
|||
|
|
func (d *DBState) CompleteStep(ctx context.Context, jobID int, nextStepID string, contextPatch map[string]any) error {
|
|||
|
|
update := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetCurrentStepId(nextStepID).
|
|||
|
|
AddVersion(1)
|
|||
|
|
if contextPatch != nil {
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("complete step: get job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
ctxMap := jb.Context
|
|||
|
|
if ctxMap == nil {
|
|||
|
|
ctxMap = make(map[string]any)
|
|||
|
|
}
|
|||
|
|
for k, v := range contextPatch {
|
|||
|
|
ctxMap[k] = v
|
|||
|
|
}
|
|||
|
|
update = update.SetContext(ctxMap)
|
|||
|
|
}
|
|||
|
|
_, err := update.Save(ctx)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Update callers in loop.go**
|
|||
|
|
|
|||
|
|
At `loop.go:409`:
|
|||
|
|
```go
|
|||
|
|
// Before:
|
|||
|
|
l.db.CompleteStep(ctx, jobID, jb.CurrentStepIndex, map[string]any{"inspectionPass": true})
|
|||
|
|
// After:
|
|||
|
|
l.db.CompleteStep(ctx, jobID, jb.CurrentStepId, map[string]any{"inspectionPass": true})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
At `loop.go:432`:
|
|||
|
|
```go
|
|||
|
|
// Before:
|
|||
|
|
l.db.CompleteStep(ctx, jobID, job.CurrentStepIndex, map[string]any{"inspectionPass": false})
|
|||
|
|
// After:
|
|||
|
|
l.db.CompleteStep(ctx, jobID, job.CurrentStepId, map[string]any{"inspectionPass": false})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/dbstate.go internal/eventloop/loop.go
|
|||
|
|
rtk git commit -m "refactor: CompleteStep uses stepId instead of stepIndex"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 3: Runtime Snapshot + Scheduler Bridge
|
|||
|
|
|
|||
|
|
### Task 3.1: Add StepID to RuntimeSnapshot and scheduler types
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/loop.go:65-84`
|
|||
|
|
- Modify: `internal/scheduler/types.go:25-32,86-92`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add StepID to RuntimeSnapshot**
|
|||
|
|
|
|||
|
|
In `internal/eventloop/loop.go`, add after `StepIndex`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type RuntimeSnapshot struct {
|
|||
|
|
// ... existing fields ...
|
|||
|
|
StepIndex int
|
|||
|
|
StepID string // 当前步骤业务ID(OP10-OP120)
|
|||
|
|
// ... rest ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Populate StepID in trySchedule**
|
|||
|
|
|
|||
|
|
In `scheduler_bridge.go:49-63`, after loading recipe step info:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
st, rt, tt, sn, sid, err := l.db.GetRecipeStep(ctx, j.RecipeId, j.CurrentStepIndex)
|
|||
|
|
if err == nil {
|
|||
|
|
snap.StepType = st
|
|||
|
|
snap.ResourceType = rt
|
|||
|
|
snap.ToolType = tt
|
|||
|
|
snap.StepName = sn
|
|||
|
|
snap.StepID = sid
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Also read from the job's own `CurrentStepId` field as backup:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
snap.StepID = j.CurrentStepId
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Note: `GetRecipeStep` already returns `stepId` as the 6th return value.
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Add StepID to scheduler StepView**
|
|||
|
|
|
|||
|
|
In `internal/scheduler/types.go`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type StepView struct {
|
|||
|
|
Index int
|
|||
|
|
StepID string // 业务步骤ID(OP10-OP120)
|
|||
|
|
Name string
|
|||
|
|
Type constants.StepType
|
|||
|
|
ResourceType string
|
|||
|
|
ToolType string
|
|||
|
|
TargetID int
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Add StepID to CandidateTask**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type CandidateTask struct {
|
|||
|
|
JobID int
|
|||
|
|
OrderID int
|
|||
|
|
StepIndex int
|
|||
|
|
StepID string // 业务步骤ID(OP10-OP120)
|
|||
|
|
Action RobotAction
|
|||
|
|
TargetID int
|
|||
|
|
Priority Priority
|
|||
|
|
Meta map[string]any
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Wire StepID in buildJobViews**
|
|||
|
|
|
|||
|
|
In `scheduler_bridge.go:220-227`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
CurrentStep: scheduler.StepView{
|
|||
|
|
Index: snap.StepIndex,
|
|||
|
|
StepID: snap.StepID,
|
|||
|
|
// ... rest ...
|
|||
|
|
},
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 6: Wire StepID in generator.go**
|
|||
|
|
|
|||
|
|
In `internal/scheduler/generator.go`, each `CandidateTask` creation adds `StepID: step.StepID`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Example: line 74
|
|||
|
|
CandidateTask{
|
|||
|
|
JobID: j.JobID,
|
|||
|
|
OrderID: j.OrderID,
|
|||
|
|
StepIndex: step.Index,
|
|||
|
|
StepID: step.StepID,
|
|||
|
|
Action: ...,
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 7: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/loop.go internal/eventloop/scheduler_bridge.go internal/scheduler/types.go internal/scheduler/generator.go
|
|||
|
|
rtk git commit -m "feat: add StepID to RuntimeSnapshot, StepView, CandidateTask"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 3.2: Update candidate_handlers to use stepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/candidate_handlers.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Use GetRecipeStepByID**
|
|||
|
|
|
|||
|
|
At line 64:
|
|||
|
|
```go
|
|||
|
|
// Before:
|
|||
|
|
params, err := l.db.GetStepProcessingParams(ctx, jb.RecipeId, jb.CurrentStepIndex)
|
|||
|
|
// After:
|
|||
|
|
params, err := l.db.GetStepProcessingParams(ctx, jb.RecipeId, jb.CurrentStepId)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
But `GetStepProcessingParams` takes stepIndex. Change to look up stepIndex from stepId first, or add a stepId version. Since this is the only caller outside AdvanceStep, keep it simple and add a stepId-based version:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// In candidate_handlers.go:
|
|||
|
|
// Get the step index from the recipe step for processing params lookup
|
|||
|
|
_, _, _, _, _, err := l.db.GetRecipeStepByID(ctx, jb.RecipeId, jb.CurrentStepId)
|
|||
|
|
// Actually just pass stepId and add stepId support to GetStepProcessingParams
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Simpler: add `GetStepProcessingParamsByID` to dbstate.go:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (d *DBState) GetStepProcessingParamsByID(ctx context.Context, recipeID int, stepID string) (map[string]any, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("get recipe %d step %s processing params: %w", recipeID, stepID, err)
|
|||
|
|
}
|
|||
|
|
if step.ProcessingParams == nil {
|
|||
|
|
return nil, nil
|
|||
|
|
}
|
|||
|
|
return step.ProcessingParams, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/candidate_handlers.go internal/eventloop/dbstate.go
|
|||
|
|
rtk git commit -m "refactor: candidate_handlers uses GetStepProcessingParamsByID"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 4: Processor + Handler + Types + Mockrun
|
|||
|
|
|
|||
|
|
### Task 4.1: Update API types with CurrentStepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/types/types.go:352,519,693`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add CurrentStepId to all structs**
|
|||
|
|
|
|||
|
|
Add `CurrentStepId string \`json:"currentStepId"\`` to each struct that has `CurrentStepIndex`:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Line ~352 (TempStationJobBrief or similar):
|
|||
|
|
CurrentStepIndex int `json:"currentStepIndex"` // deprecated
|
|||
|
|
CurrentStepId string `json:"currentStepId"`
|
|||
|
|
|
|||
|
|
// Line ~519:
|
|||
|
|
CurrentStepIndex int `json:"currentStepIndex"` // deprecated
|
|||
|
|
CurrentStepId string `json:"currentStepId"`
|
|||
|
|
|
|||
|
|
// Line ~693:
|
|||
|
|
CurrentStepIndex int `json:"currentStepIndex"` // deprecated
|
|||
|
|
CurrentStepId string `json:"currentStepId"`
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Regenerate types**
|
|||
|
|
|
|||
|
|
Run: `cd apis && go generate`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Update handler/logic to populate CurrentStepId**
|
|||
|
|
|
|||
|
|
In response builder logic, set `CurrentStepId` from `job.CurrentStepId`.
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/types/types.go apis/ internal/handler/ internal/logic/
|
|||
|
|
rtk git commit -m "feat: add currentStepId to API response types"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 4.2: Update ReworkJob to use stepId
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/processor/interface.go:31`
|
|||
|
|
- Modify: `internal/processor/job_processor.go:251-260`
|
|||
|
|
- Modify: `internal/logic/job/rework_job_logic.go:26`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Change ReworkJob signature**
|
|||
|
|
|
|||
|
|
In `interface.go`:
|
|||
|
|
```go
|
|||
|
|
// Before:
|
|||
|
|
ReworkJob(ctx context.Context, jobID int, targetStepIndex int) error
|
|||
|
|
// After:
|
|||
|
|
ReworkJob(ctx context.Context, jobID int, targetStepID string) error
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Update job_processor.go implementation**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (jp *JobProcessor) ReworkJob(ctx context.Context, jobID int, targetStepID string) error {
|
|||
|
|
_, err := jp.entClient.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_Processing).
|
|||
|
|
SetCurrentStepId(targetStepID).
|
|||
|
|
SetSuspendedReason("").
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("rework job %d to step %s: %w", jobID, targetStepID, err)
|
|||
|
|
}
|
|||
|
|
slog.Info("job processor: job reworked", "jobId", jobID, "targetStepId", targetStepID)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Update stub**
|
|||
|
|
|
|||
|
|
In `interface.go`:
|
|||
|
|
```go
|
|||
|
|
func (s *StubProcessor) ReworkJob(ctx context.Context, jobID int, targetStepID string) error {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 4: Update rework logic/handler**
|
|||
|
|
|
|||
|
|
The `ReworkJobReq` already has `TargetStepId string`. Update the logic to pass it directly:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (l *ReworkJobLogic) ReworkJob(req *types.ReworkJobReq) error {
|
|||
|
|
return l.svcCtx.JobProcessor.ReworkJob(l.ctx, req.ID, req.TargetStepId)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 5: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/processor/interface.go internal/processor/job_processor.go internal/logic/job/rework_job_logic.go
|
|||
|
|
rtk git commit -m "refactor: ReworkJob uses stepId instead of stepIndex"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 4.3: Update mockrun
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `cmd/mockrun/main.go:110`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Use SetCurrentStepId**
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Before:
|
|||
|
|
SetCurrentStepIndex(initialStepIdx)
|
|||
|
|
// After:
|
|||
|
|
SetCurrentStepId(initialStepID) // lookup from recipe by stepIndex
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Need to query recipe_step to get the stepId for initialStepIdx. Add a helper:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
stepID := lookupStepID(ctx, entClient, recipe.RecipeID, initialStepIdx)
|
|||
|
|
// ...
|
|||
|
|
SetCurrentStepId(stepID)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add cmd/mockrun/main.go
|
|||
|
|
rtk git commit -m "refactor: mockrun uses SetCurrentStepId"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 5: Frontend
|
|||
|
|
|
|||
|
|
### Task 5.1: Add currentStepId to frontend API types
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `frontend/src/api/temp-station/index.ts:9`
|
|||
|
|
- Modify: `frontend/src/api/dock/index.ts:9`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Add currentStepId to JobBrief**
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export interface JobBrief {
|
|||
|
|
id: number
|
|||
|
|
workOrderId: number
|
|||
|
|
workpieceNo: string
|
|||
|
|
productTypeId: number
|
|||
|
|
status: string
|
|||
|
|
currentStepIndex: number // deprecated
|
|||
|
|
currentStepId: string
|
|||
|
|
currentStepName: string
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Same change in both files.
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add frontend/src/api/temp-station/index.ts frontend/src/api/dock/index.ts
|
|||
|
|
rtk git commit -m "feat: add currentStepId to frontend API types"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Chunk 6: Deprecation Cleanup
|
|||
|
|
|
|||
|
|
### Task 6.1: Mark deprecated methods
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `internal/eventloop/dbstate.go`
|
|||
|
|
- Modify: `internal/processor/recipe_runtime.go`
|
|||
|
|
|
|||
|
|
- [ ] **Step 1: Mark GetNextStepIndex deprecated**
|
|||
|
|
|
|||
|
|
Add comment:
|
|||
|
|
```go
|
|||
|
|
// Deprecated: use AdvanceStep with stepId instead. nextStepDefault is already a stepId.
|
|||
|
|
func (d *DBState) GetNextStepIndex(...) {...}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- [ ] **Step 2: Verify no remaining callers of SetCurrentStepIndex**
|
|||
|
|
|
|||
|
|
Run: `rtk grep "SetCurrentStepIndex" --files-with-matches`
|
|||
|
|
|
|||
|
|
- [ ] **Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
rtk git add internal/eventloop/dbstate.go
|
|||
|
|
rtk git commit -m "chore: mark GetNextStepIndex deprecated"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Verification Checklist
|
|||
|
|
|
|||
|
|
- [ ] `go build ./...` passes
|
|||
|
|
- [ ] `go vet ./...` passes
|
|||
|
|
- [ ] `go test ./internal/eventloop/...` passes
|
|||
|
|
- [ ] `go test ./internal/scheduler/...` passes
|
|||
|
|
- [ ] `go test ./internal/processor/...` passes
|
|||
|
|
- [ ] `cd frontend && pnpm build` passes
|
|||
|
|
- [ ] Migration runs successfully on existing database
|
|||
|
|
- [ ] No remaining `SetCurrentStepIndex` callers (except in generated ent code)
|