181 lines
4.8 KiB
Go
181 lines
4.8 KiB
Go
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
|
||
}
|