package processor import ( "context" "sort" "sync/atomic" "bj_power_mes/constants" "bj_power_mes/ent" "bj_power_mes/ent/job" "bj_power_mes/ent/producttype" "bj_power_mes/ent/workorder" "bj_power_mes/internal/processor/actor" ) // Replenisher 暂存台补料编排器。配额计算 + 选料,物理搬运由 RobotWorker 执行。 type Replenisher struct { entClient *ent.Client tempStore *actor.TempStoreActor replenishing atomic.Bool } // NewReplenisher 创建暂存台补料编排器 func NewReplenisher(entClient *ent.Client, tempStore *actor.TempStoreActor) *Replenisher { return &Replenisher{ entClient: entClient, tempStore: tempStore, } } // IsReplenishing 返回当前是否正在执行补料操作 func (r *Replenisher) IsReplenishing() bool { return r.replenishing.Load() } // SetReplenishing 设置补料互斥标志 func (r *Replenisher) SetReplenishing(on bool) { r.replenishing.Store(on) } // --- 硬编码暂存台配额规则 --- // activeCategories 返回当前活跃工单(有 Created job 在 dock 上)的 category 列表 func (r *Replenisher) activeCategories(ctx context.Context) []string { cats, _ := r.dockCategories(ctx) return cats } // computeQuotas 根据活跃 category 组合计算配额。8 个暂存台位置按以下规则分配: // // A+B+C: 3:3:2 A+B: 4:4 A+C: 5:3 B+C: 5:3 仅一种: 8 func computeQuotas(activeCats []string) map[string]int { sort.Strings(activeCats) key := "" for i, c := range activeCats { if i > 0 { key += "+" } key += c } switch key { case "A6VM107+A6VM160+A6VM200": return map[string]int{"A6VM107": 3, "A6VM160": 3, "A6VM200": 2} case "A6VM107+A6VM160": return map[string]int{"A6VM107": 4, "A6VM160": 4} case "A6VM107+A6VM200": return map[string]int{"A6VM107": 5, "A6VM200": 3} case "A6VM160+A6VM200": return map[string]int{"A6VM160": 5, "A6VM200": 3} default: if len(activeCats) == 1 { return map[string]int{activeCats[0]: 8} } return nil } } // BuildReplenishCandidate 检查是否需要补料,返回一个 Dock 上的待补料工件。 // 触发条件:某 category 暂存台占用 ≤ quota/2。 // 返回 nil 表示无需补料或暂存台已满。 func (r *Replenisher) BuildReplenishCandidate(ctx context.Context) *ReplenishItem { if r == nil || r.entClient == nil { return nil } activeCats := r.activeCategories(ctx) if len(activeCats) == 0 { return nil } quotas := computeQuotas(activeCats) if quotas == nil { return nil } // 找出需要补料的 category // 条件:1) 未满(occ < quota) 2) 已半满或正在补料中(occ ≤ quota/2 或 replenishing) var needRefill string for cat, quota := range quotas { occ := r.categoryOccupied(ctx, cat) if occ >= quota { continue // 已满 } if !r.replenishing.Load() && occ > quota/2 { continue // 有空位但未到半满,等消耗更多再补 } needRefill = cat break } if needRefill == "" { return nil } // 从 Dock 上找该 category 的 Created 工件(仅已启动的工单) jbs, err := r.entClient.Job.Query(). Where( job.StatusEQ(constants.JobStatus_Created), job.PositionTypeEQ(constants.PositionType_OnDock), job.HasProductTypeWith(producttype.CategoryEQ(producttype.Category(needRefill))), job.HasWorkOrderWith(workorder.StatusIn( constants.WorkOrderStatus_InProgress, constants.WorkOrderStatus_Paused, )), ). Order(job.ByDockNo(), job.ByDockSlotNo()). Limit(1). All(ctx) if err != nil || len(jbs) == 0 { return nil } j := jbs[0] if j.DockNo == nil || j.DockSlotNo == nil { return nil } return &ReplenishItem{ JobID: j.ID, DockNo: *j.DockNo, DockSlotNo: *j.DockSlotNo, ProductTypeID: j.ProductTypeId, } } // dockCategories 返回 dock 上 Created 工件所属的 category 列表(去重) func (r *Replenisher) dockCategories(ctx context.Context) ([]string, error) { pts, err := r.entClient.ProductType.Query(). Where(producttype.HasJobsWith( job.StatusIn(constants.JobStatus_Created, constants.JobStatus_Suspended), job.PositionTypeEQ(constants.PositionType_OnDock), job.HasWorkOrderWith(workorder.StatusIn( constants.WorkOrderStatus_InProgress, constants.WorkOrderStatus_Paused, )), )). All(ctx) if err != nil { return nil, err } seen := make(map[string]bool) var cats []string for _, pt := range pts { cat := string(pt.Category) if !seen[cat] { seen[cat] = true cats = append(cats, cat) } } return cats, nil } // categoryOccupied 返回指定 category 当前占用暂存台的槽位数 func (r *Replenisher) categoryOccupied(ctx context.Context, category string) int { count, _ := r.entClient.Job.Query(). Where( job.StatusNotIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped), job.TempSlotNoNotNil(), job.HasProductTypeWith(producttype.CategoryEQ(producttype.Category(category))), ). Count(ctx) return count }