package eventloop import ( "context" "fmt" "log/slog" "time" "bj_power_mes/constants" "bj_power_mes/ent" "bj_power_mes/ent/equipmentslot" "bj_power_mes/ent/job" "bj_power_mes/ent/pallet" "bj_power_mes/ent/workorder" ) // RecoverOnStartup 启动时执行保守人工确认恢复。 // 先暂停所有工单再校验一致性——避免启动时未经验证的状态被调度器消费。 // 1. 将所有 IN_PROGRESS 的 work_order 改为 PAUSED // 2. 校验 temp_slot_no 一致性(重复/越界) // 3. 校验 equipment_slot 与 job 位置一致性 // 4. 不一致项创建 manual_action 等待人工确认 func (d *DBState) RecoverOnStartup(ctx context.Context) (recoveredJobs int, pendingRecoveryOrderIDs []int, manualActions []*ent.ManualAction, err error) { // 逐个检查 IN_PROGRESS/PAUSING 订单:有 ON_EQUIPMENT 活跃工件的保持原状等待人工确认 activeOrders, _ := d.client.WorkOrder.Query(). Where(workorder.StatusIn(constants.WorkOrderStatus_InProgress, constants.WorkOrderStatus_Pausing)). All(ctx) var pendingOrderIDs []int for _, o := range activeOrders { onEquipCount, _ := d.client.Job.Query(). Where( job.WorkOrderIdEQ(o.ID), job.StatusNotIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped), job.PositionTypeEQ(constants.PositionType_OnEquipment), ). Count(ctx) if onEquipCount > 0 { // 在设备中的工件需要人工确认,保持 IN_PROGRESS pendingOrderIDs = append(pendingOrderIDs, o.ID) slog.Info("recovery: order kept IN_PROGRESS for position confirmation", "orderId", o.ID) } else { // 没有设备中工件,安全切换为 PAUSED _, _ = d.client.WorkOrder.UpdateOneID(o.ID). SetStatus(constants.WorkOrderStatus_Paused). Save(ctx) } } jobs, err := d.GetActiveJobs(ctx) if err != nil { return 0, nil, nil, err } // 构建 jobID→orderID 映射,用于 ManualAction 关联工单 jobOrder := make(map[int]int, len(jobs)) for _, j := range jobs { jobOrder[j.ID] = j.WorkOrderId } createMA := func(jobID int, desc string) { ma, _ := d.CreateManualAction(ctx, constants.ManualActionType_RecoveryRequired, jobID, jobOrder[jobID], desc) if ma != nil { manualActions = append(manualActions, ma) } } capacity := d.TempSlotCapacity(ctx) slotUsers := make(map[int][]int) for _, j := range jobs { if j.TempSlotNo != nil { slot := *j.TempSlotNo if slot < 1 || slot > capacity { createMA(j.ID, fmt.Sprintf("temp_slot_no %d out of range [1,%d]", slot, capacity)) continue } slotUsers[slot] = append(slotUsers[slot], j.ID) } } for slot, users := range slotUsers { if len(users) > 1 { slog.Warn("recovery: duplicate temp slot", "slot", slot, "jobs", users) for _, jID := range users { createMA(jID, fmt.Sprintf("duplicate temp_slot_no %d", slot)) } } } for _, j := range jobs { if j.PositionType == constants.PositionType_OnEquipment { equipID, slotNo := parsePositionRef(j.PositionRefId) if equipID == 0 { continue } slot, slotErr := d.client.EquipmentSlot.Query(). Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)). First(ctx) if slotErr != nil { slog.Warn("recovery: slot not found", "equip", equipID, "slot", slotNo) createMA(j.ID, fmt.Sprintf("equipment slot %d:%d not found", equipID, slotNo)) continue } cjid := 0 if slot.CurrentJobId != nil { cjid = *slot.CurrentJobId } if cjid != j.ID { createMA(j.ID, fmt.Sprintf("equipment_slot %d:%d has job %d, expected %d", equipID, slotNo, cjid, j.ID)) } } } // Pallet 状态推导 pallets, pErr := d.client.Pallet.Query(). Where(pallet.StatusIn(constants.PalletStatus_PendingProcess, constants.PalletStatus_Processing)). All(ctx) if pErr != nil { slog.Warn("recovery: query pallets failed", "error", pErr) } else { for _, p := range pallets { palletPred := job.HasPalletWith(pallet.IDEQ(p.ID)) activeCount, cErr := d.client.Job.Query(). Where( palletPred, job.StatusNotIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped), ). Count(ctx) if cErr != nil { continue } if activeCount == 0 { // 全部终态 → COMPLETED totalJobs, _ := d.client.Job.Query().Where(job.HasPalletWith(pallet.IDEQ(p.ID))).Count(ctx) scrappedCount, _ := d.client.Job.Query(). Where(job.HasPalletWith(pallet.IDEQ(p.ID)), job.StatusEQ(constants.JobStatus_Scrapped)). Count(ctx) now := time.Now() _, err := d.client.Pallet.UpdateOneID(p.ID). SetStatus(constants.PalletStatus_Completed). SetPassQty(totalJobs - scrappedCount). SetNgQty(scrappedCount). SetCompletedAt(now). Save(ctx) if err != nil { slog.Warn("recovery: complete pallet failed", "palletId", p.ID, "error", err) } else { slog.Info("recovery: pallet completed", "palletId", p.ID) } } // 存在非终态 Job → 保持不变,跟随 Job 恢复流程 } } return len(jobs), pendingOrderIDs, manualActions, nil }