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
|
|||
|
|
}
|