package eventloop import ( "context" "fmt" "log/slog" "strings" "time" "bj_power_mes/constants" "bj_power_mes/ent" "bj_power_mes/ent/equipment" "bj_power_mes/ent/equipmentslot" "bj_power_mes/ent/equipmenttype" "bj_power_mes/ent/job" "bj_power_mes/ent/recipestep" ) // DoneSlotInfo 完成槽位信息 type DoneSlotInfo struct { JobID int SlotNo int } // DBState ent 原子条件更新封装,仅由 event loop 使用 type DBState struct { client *ent.Client } func NewDBState(client *ent.Client) *DBState { return &DBState{client: client} } // 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 { // 合并而非替换 context:先读取现有 context,再写入 patch 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 } // FinishJob 工件终态。 // 使用事务确保 job 状态切换 + work_order 计数累加原子执行。 func (d *DBState) FinishJob(ctx context.Context, jobID, workOrderID int, terminalStatus constants.JobStatus) error { tx, err := d.client.Tx(ctx) if err != nil { return err } defer tx.Rollback() _, err = tx.Job.UpdateOneID(jobID). Where(job.StatusNotIn( constants.JobStatus_Completed, constants.JobStatus_Scrapped, )). SetStatus(terminalStatus). SetCompletedAt(time.Now()). ClearTempSlotNo(). AddVersion(1). Save(ctx) if err != nil { slog.Error("finish job failed", "jobID", jobID, "error", err) return fmt.Errorf("finish job %d: %w", jobID, err) } if terminalStatus == constants.JobStatus_Completed { _, err = tx.WorkOrder.UpdateOneID(workOrderID).AddFinishedNum(1).Save(ctx) if err != nil { return fmt.Errorf("finish job %d: add finishedNum: %w", jobID, err) } } else if terminalStatus == constants.JobStatus_Scrapped { _, err = tx.WorkOrder.UpdateOneID(workOrderID).AddFailNum(1).Save(ctx) if err != nil { slog.Error("finish job: add failNum failed", "jobID", jobID, "error", err) return fmt.Errorf("finish job %d: add failNum: %w", jobID, err) } // 报废工件工人取出,清空位置 _, err = tx.Job.UpdateOneID(jobID). SetPositionType(""). SetPositionRefId(""). Save(ctx) if err != nil { slog.Error("finish job: clear position failed", "jobID", jobID, "error", err) return fmt.Errorf("finish job %d: clear position: %w", jobID, err) } } return tx.Commit() } // SetEquipmentSlot 更新设备槽位状态,管理 OccupiedAt 生命周期: // - Occupied: 设置 OccupiedAt=now(记录占用时间) // - Done: 保留 OccupiedAt(工件仍在设备上) // - Empty: 清除 OccupiedAt + CurrentJobId(设备已释放) func (d *DBState) SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expectedStatus, nextStatus constants.SlotStatus, jobID int) error { update := d.client.EquipmentSlot.Update(). Where( equipmentslot.EquipmentIdEQ(equipmentID), equipmentslot.SlotNoEQ(slotNo), equipmentslot.StatusEQ(expectedStatus), ). SetStatus(nextStatus) switch nextStatus { case constants.SlotStatus_Occupied: update = update.SetCurrentJobId(jobID).SetOccupiedAt(time.Now()) case constants.SlotStatus_Done: update = update.SetCurrentJobId(jobID) case constants.SlotStatus_Empty: update = update.ClearCurrentJobId().ClearOccupiedAt() } n, err := update.Save(ctx) if err != nil { return fmt.Errorf("set slot %d-%d: %w", equipmentID, slotNo, err) } if n == 0 { return fmt.Errorf("set slot %d-%d: expected %s", equipmentID, slotNo, expectedStatus) } return nil } // SetJobProcessing 设置 job 为 PROCESSING 状态并记录设备位置 func (d *DBState) SetJobProcessing(ctx context.Context, jobID, equipmentID, slotNo int) error { _, err := d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_Processing). SetPositionType(constants.PositionType_OnEquipment). SetPositionRefId(fmt.Sprintf("%d:%d", equipmentID, slotNo)). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set job %d processing: %w", jobID, err) } return nil } // SetJobOnEquipment 仅更新 job 位置到指定设备槽位,不修改状态/版本(用于设备内槽位转移场景) func (d *DBState) SetJobOnEquipment(ctx context.Context, jobID, equipmentID, slotNo int) error { _, err := d.client.Job.UpdateOneID(jobID). SetPositionType(constants.PositionType_OnEquipment). SetPositionRefId(fmt.Sprintf("%d:%d", equipmentID, slotNo)). Save(ctx) if err != nil { return fmt.Errorf("set job %d on equipment %d slot %d: %w", jobID, equipmentID, slotNo, err) } return nil } // SetJobWaitingUnload 设置 job 为 WAITING_UNLOAD 状态 func (d *DBState) SetJobWaitingUnload(ctx context.Context, jobID int) error { _, err := d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_WaitingUnload). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set job %d waiting unload: %w", jobID, err) } return nil } // GetActiveJobs 获取所有非终态 job func (d *DBState) GetActiveJobs(ctx context.Context) ([]*ent.Job, error) { return d.client.Job.Query(). Where(job.StatusNotIn( constants.JobStatus_Completed, constants.JobStatus_Scrapped, )).Order(job.ByID()). All(ctx) } // AdvanceStep 推进到下一步:先合并 context → 匹配 nextStepBranches → 否则 nextStepDefault → 否则线性推进。 // 推进后若下一步是 Decision/Judge 则链式内联处理(纯规则评估,无需硬件动作), // 直至遇到需要等待或 Worker 动作的步骤。返回 true 表示已是最后一步(job 已完成)。 func (d *DBState) AdvanceStep(ctx context.Context, jobID int, contextUpdates map[string]any, posType constants.PositionType, posRefID string) (bool, error) { maxIter := 100 // 安全上限,防止 Decision 步骤形成循环 for i := 0; i < maxIter; i++ { jb, err := d.client.Job.Get(ctx, jobID) if err != nil { return false, fmt.Errorf("advance step: job %d not found: %w", jobID, err) } // 合并上下文更新(分支匹配前合并,让 contextUpdates 中的键可触发分支) 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 { // 查询不到下一步 → 确认为最后一步则完成 job maxStep, maxErr := d.GetRecipeMaxStepIndex(ctx, jb.RecipeId) if maxErr != nil { return false, fmt.Errorf("advance step: get recipe max for job %d: %w", jobID, maxErr) } if step.StepIndex >= maxStep { 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) } 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 // 最后一步:先写位置再 FinishJob,确保终态位置正确 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:纯规则评估 + 条件跳步)。 // signal_true 类型需实时读 PLC,不适合内联,退回给 trySchedule → handleDecisionCandidate 处理。 _, stepType, _, _, _, err := d.GetRecipeStepByID(ctx, jb.RecipeId, nextStepID) if err != nil || (stepType != constants.StepType_Decision && stepType != constants.StepType_Judge) { return false, err } if isSignalTrueDecision(d, ctx, jb.RecipeId, nextStepID) { return false, nil } // 计算 Decision 分支键,作为下一次迭代的 contextUpdates contextUpdates = evaluateDecisionForJob(d, ctx, jb.RecipeId, nextStepID, ctxMap, jb.WorkpieceNo) posType, posRefID = "", "" } return false, fmt.Errorf("advance step: exceeded max iterations for job %d, possible decision step cycle", jobID) } // evaluateDecisionForJob 读 processingParams 并调用 evaluateDecision 原地修改 ctxMap 副本, // 返回新增的键值对作为下一轮 AdvanceStep 迭代的 contextUpdates。 func evaluateDecisionForJob(d *DBState, ctx context.Context, recipeID int, stepID string, ctxMap map[string]any, workpieceNo string) map[string]any { 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, nil) result := make(map[string]any) for k, v := range clone { if _, ok := ctxMap[k]; !ok { result[k] = v } } return result } // isSignalTrueDecision 检查 Decision 步骤是否使用 signal_true 类型(需实时读 PLC,不能内联处理) func isSignalTrueDecision(d *DBState, ctx context.Context, recipeID int, stepID string) bool { step, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)). First(ctx) if err != nil || step.ProcessingParams == nil { return false } dt, _ := step.ProcessingParams["decisionType"].(string) return dt == "signal_true" } // GetRecipeStep 获取工艺路线中指定步骤的信息 func (d *DBState) GetRecipeStep(ctx context.Context, recipeID, stepIndex int) (stepType constants.StepType, resourceType string, toolType string, stepName string, stepId string, err error) { step, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)). First(ctx) if err != nil { return "", "", "", "", "", fmt.Errorf("get recipe %d step %d: %w", recipeID, stepIndex, err) } return step.StepType, step.ResourceType, step.ToolType, step.StepName, step.StepId, nil } // GetStepProcessingParams 返回工序的 processingParams(用于 Decision 步骤的通用判定规则) func (d *DBState) GetStepProcessingParams(ctx context.Context, recipeID, stepIndex int) (map[string]any, error) { step, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)). First(ctx) if err != nil { return nil, fmt.Errorf("get recipe %d step %d processing params: %w", recipeID, stepIndex, err) } if step.ProcessingParams == nil { return nil, nil } return step.ProcessingParams, nil } // GetStepProcessingParamsByID 根据 stepId 返回工序的 processingParams 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 } // 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 } // 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 } // 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 } // GetStepIndexByStepID 根据 stepId 查找步骤的 stepIndex func (d *DBState) GetStepIndexByStepID(ctx context.Context, recipeID int, stepID string) (int, error) { step, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)). First(ctx) if err != nil { return -1, fmt.Errorf("get step index by stepId %s: %w", stepID, err) } return step.StepIndex, nil } // 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 } // isTruthy 判断 context 值为 truthy(bool true、非空字符串、非零数字) func isTruthy(v any) bool { switch tv := v.(type) { case bool: return tv case string: return tv != "" case int: return tv != 0 case float64: return tv != 0 default: return v != nil } } // GetRecipeMaxStepIndex 获取工艺路线的最大步骤索引 func (d *DBState) GetRecipeMaxStepIndex(ctx context.Context, recipeID int) (int, error) { steps, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID)). All(ctx) if err != nil { return 0, fmt.Errorf("get recipe %d steps: %w", recipeID, err) } maxIdx := 0 for _, s := range steps { if s.StepIndex > maxIdx { maxIdx = s.StepIndex } } return maxIdx, nil } // TempSlotCapacity 从 DB 查询暂存台槽位数,默认返回 8 func (d *DBState) TempSlotCapacity(ctx context.Context) int { equip, err := d.client.Equipment.Query(). Where(equipment.HasEquipmentTypeWith(equipmenttype.CodeEQ(constants.EquipmentTypeCode_TempStore))). First(ctx) if err != nil || equip == nil { return 8 } if equip.SlotCount > 0 { return equip.SlotCount } return 8 } // parsePositionRef 解析 "equipmentID:slotNo" 格式 func parsePositionRef(ref string) (int, int) { equipStr, slotStr, ok := strings.Cut(ref, ":") if !ok { return 0, 0 } var equipID, slotNo int fmt.Sscanf(equipStr, "%d", &equipID) fmt.Sscanf(slotStr, "%d", &slotNo) return equipID, slotNo } // RaiseAlarm 创建报警 func (d *DBState) RaiseAlarm(ctx context.Context, alarmCode, message string, level constants.AlarmLevel, equipmentID, jobID int, source string) error { _, err := d.client.Alarm.Create(). SetAlarmCode(alarmCode). SetAlarmMessage(message). SetLevel(level). SetNillableEquipmentId(&equipmentID). SetNillableJobId(&jobID). SetSource(source). Save(ctx) return err } // FindDoneJobOnMachine 查找设备上状态为 DONE 的槽位对应的 jobID func (d *DBState) FindDoneJobOnMachine(ctx context.Context, machineID int) (jobID int, slotNo int, found bool) { slot, err := d.client.EquipmentSlot.Query(). Where( equipmentslot.EquipmentIdEQ(machineID), equipmentslot.StatusEQ(constants.SlotStatus_Done), ). First(ctx) if err != nil || slot == nil { return 0, 0, false } return slotJobID(slot), slot.SlotNo, true } // FindAllDoneJobsOnMachine 查找设备上所有 DONE 状态的槽位 func (d *DBState) FindAllDoneJobsOnMachine(ctx context.Context, machineID int) []DoneSlotInfo { slots, _ := d.client.EquipmentSlot.Query(). Where( equipmentslot.EquipmentIdEQ(machineID), equipmentslot.StatusEQ(constants.SlotStatus_Done), ). All(ctx) var result []DoneSlotInfo for _, s := range slots { result = append(result, DoneSlotInfo{ JobID: slotJobID(s), SlotNo: s.SlotNo, }) } return result } // CreateManualAction 创建人工恢复动作 func (d *DBState) CreateManualAction(ctx context.Context, actionType constants.ManualActionType, jobID, orderID int, description string) (*ent.ManualAction, error) { c := d.client.ManualAction.Create(). SetActionType(actionType). SetJobId(jobID). SetContext(map[string]any{"description": description}). SetStatus(constants.ManualActionStatus_PENDING) if orderID > 0 { c = c.SetOrderId(orderID) } ma, err := c.Save(ctx) return ma, err } // SetJobTempSlot 设置 job.temp_slot_no func (d *DBState) SetJobTempSlot(ctx context.Context, jobID, slotNo int) error { _, err := d.client.Job.UpdateOneID(jobID). SetNillableTempSlotNo(&slotNo). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set job %d temp slot %d: %w", jobID, slotNo, err) } return nil } // ClearJobTempSlot 清除 job.temp_slot_no func (d *DBState) ClearJobTempSlot(ctx context.Context, jobID int) error { _, err := d.client.Job.UpdateOneID(jobID). ClearTempSlotNo(). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("clear job %d temp slot: %w", jobID, err) } return nil } // SetCncStartedAt 记录 CNC 加工开始时间(仅首次写入,已有值则跳过) func (d *DBState) SetCncStartedAt(ctx context.Context, jobID int) error { n, err := d.client.Job.Update(). Where(job.ID(jobID), job.CncStartedAtIsNil()). SetCncStartedAt(time.Now()). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set cnc started at for job %d: %w", jobID, err) } if n == 0 { slog.Warn("cnc started at already set, skipping", "jobId", jobID) } return nil } // SetCncCompletedAt 记录 CNC 加工完成时间(仅首次写入,且要求 cncStartedAt 已设置) func (d *DBState) SetCncCompletedAt(ctx context.Context, jobID int) error { n, err := d.client.Job.Update(). Where(job.ID(jobID), job.CncStartedAtNotNil()). SetCncCompletedAt(time.Now()). AddVersion(1). Save(ctx) if err != nil { return fmt.Errorf("set cnc completed at for job %d: %w", jobID, err) } if n == 0 { slog.Warn("cnc completed at skipped (already set or cncStartedAt missing)", "jobId", jobID) } return nil } // FixJobPosition 恢复工件位置到暂存台:释放 equipment_slot(如果占用)+ 设置位置/步骤/状态 func (d *DBState) FixJobPosition(ctx context.Context, jobID int, tempSlotNo int, stepID string, positionType string) error { jb, err := d.client.Job.Get(ctx, jobID) if err != nil { return fmt.Errorf("fix position: get job %d: %w", jobID, err) } switch positionType { case "ON_EQUIPMENT_DONE": equipID, slotNo := parsePositionRef(jb.PositionRefId) if equipID > 0 && slotNo > 0 { _, _ = d.client.EquipmentSlot.Update(). Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)). SetStatus(constants.SlotStatus_Done). Save(ctx) } _, err = d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_WaitingUnload). SetPositionType(constants.PositionType_OnEquipment). SetPositionRefId(jb.PositionRefId). SetNillableTempSlotNo(&tempSlotNo). SetCurrentStepId(stepID). AddVersion(1). Save(ctx) case "ON_EQUIPMENT_PROCESSING": equipID, slotNo := parsePositionRef(jb.PositionRefId) if equipID > 0 && slotNo > 0 { _, _ = d.client.EquipmentSlot.Update(). Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)). SetStatus(constants.SlotStatus_Occupied). Save(ctx) } _, err = d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_Processing). SetPositionType(constants.PositionType_OnEquipment). SetPositionRefId(jb.PositionRefId). SetNillableTempSlotNo(&tempSlotNo). SetCurrentStepId(stepID). AddVersion(1). Save(ctx) default: // ON_BUFFER: release equipment slot first if needed if jb.PositionType == constants.PositionType_OnEquipment { equipID, slotNo := parsePositionRef(jb.PositionRefId) if equipID > 0 && slotNo > 0 { // recovery: force release slot regardless of current status _, _ = d.client.EquipmentSlot.Update(). Where( equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo), ). SetStatus(constants.SlotStatus_Empty). ClearCurrentJobId(). ClearOccupiedAt(). Save(ctx) } } _, err = d.client.Job.UpdateOneID(jobID). SetStatus(constants.JobStatus_OnBuffer). SetPositionType(constants.PositionType_OnBuffer). SetPositionRefId(fmt.Sprintf("%d", tempSlotNo)). SetNillableTempSlotNo(&tempSlotNo). SetCurrentStepId(stepID). AddVersion(1). Save(ctx) } if err != nil { return fmt.Errorf("fix position: update job %d: %w", jobID, err) } return nil } // ScrapRecoveryJob 报废现场找不到的恢复工件:释放 equipment_slot(若占用)+ 终态 Scrapped + 工单 failNum +1。 func (d *DBState) ScrapRecoveryJob(ctx context.Context, jobID int) error { jb, err := d.client.Job.Get(ctx, jobID) if err != nil { return fmt.Errorf("scrap recovery: get job %d: %w", jobID, err) } if jb.Status == constants.JobStatus_Scrapped || jb.Status == constants.JobStatus_Completed { return fmt.Errorf("scrap recovery: job %d already terminal: %s", jobID, jb.Status) } // 释放设备槽位(若曾在设备上) if jb.PositionType == constants.PositionType_OnEquipment { if equipID, slotNo := parsePositionRef(jb.PositionRefId); equipID > 0 && slotNo > 0 { _, _ = d.client.EquipmentSlot.Update(). Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)). SetStatus(constants.SlotStatus_Empty). ClearCurrentJobId(). ClearOccupiedAt(). Save(ctx) } } if err := d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Scrapped); err != nil { return fmt.Errorf("scrap recovery: finish job %d: %w", jobID, err) } return nil } func (d *DBState) IsCNCEquipment(ctx context.Context, equipmentID int) bool { equip, err := d.client.Equipment.Query(). Where(equipment.ID(equipmentID)). WithEquipmentType(). Only(ctx) if err != nil || equip == nil { return false } et, err := equip.Edges.EquipmentTypeOrErr() if err != nil || et == nil { return false } return et.Code == constants.EquipmentTypeCode_CNC } // SetQCResult 写入检测结果到 job 的独立字段(按设备类型区分)。 // INSPECTION -> inspection_result,SAMPLING -> sampling_result;其余设备忽略(防御)。 // 与 context.inspectionPass 双写并存,本字段为合格率统计的一等数据源。 func (d *DBState) SetQCResult(ctx context.Context, jobID int, machineType string, pass bool) error { result := constants.QCResult_Pass if !pass { result = constants.QCResult_Fail } update := d.client.Job.UpdateOneID(jobID) switch constants.EquipmentTypeCode(machineType) { case constants.EquipmentTypeCode_Inspection: update = update.SetInspectionResult(result) case constants.EquipmentTypeCode_Sampling: update = update.SetSamplingResult(result) default: return nil } if _, err := update.Save(ctx); err != nil { return fmt.Errorf("set qc result: job %d: %w", jobID, err) } return nil } // GetFirstStepID 返回配方第一个步骤的 stepId func (d *DBState) GetFirstStepID(ctx context.Context, recipeID int) (string, error) { step, err := d.client.RecipeStep.Query(). Where(recipestep.RecipeIdEQ(recipeID)). Order(recipestep.ByStepIndex()). First(ctx) if err != nil { return "", fmt.Errorf("get first step for recipe %d: %w", recipeID, err) } return step.StepId, nil }