641 lines
20 KiB
Go
641 lines
20 KiB
Go
package eventloop
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"log/slog"
|
|||
|
|
"strconv"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"bj_power_mes/constants"
|
|||
|
|
"bj_power_mes/ent"
|
|||
|
|
"bj_power_mes/ent/equipmentslot"
|
|||
|
|
"bj_power_mes/internal/eventbus"
|
|||
|
|
"bj_power_mes/internal/processor/actor"
|
|||
|
|
"bj_power_mes/internal/processor/scheduler"
|
|||
|
|
|
|||
|
|
momlib "bjhardman.cn/bjhardman/mom"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// PLCReader reads PLC boolean signals
|
|||
|
|
type PLCReader interface {
|
|||
|
|
ReadBool(address string) (bool, error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DockStateReader 从 PLC 读取接驳台在位状态
|
|||
|
|
type DockStateReader interface {
|
|||
|
|
ReadDockStates(ctx context.Context) ([]bool, error)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MomBizConfig MOM 业务相关配置(与 SDK 的 MomConf 解耦,避免循环依赖 internal/config)
|
|||
|
|
//
|
|||
|
|
// 仅放 EventLoop 自己直接使用的字段。设备上传相关参数由 EquipUploadService 单独持有。
|
|||
|
|
type MomBizConfig struct {
|
|||
|
|
// InstoreType 入库类型 1:立库 2:平库
|
|||
|
|
InstoreType int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ProductionEventLoop 产线事件循环:唯一生产状态写路径,串行处理所有消息
|
|||
|
|
type ProductionEventLoop struct {
|
|||
|
|
db *DBState
|
|||
|
|
entClient *ent.Client
|
|||
|
|
|
|||
|
|
msgCh chan EventLoopMessage
|
|||
|
|
|
|||
|
|
worker HardwareWorker
|
|||
|
|
|
|||
|
|
// 运行时快照:jobID → RuntimeSnapshot
|
|||
|
|
jobRuntimes map[int]*RuntimeSnapshot
|
|||
|
|
|
|||
|
|
// 飞行锁:防止同一 job 的 Worker 并发 dispatch
|
|||
|
|
dispatching map[int]bool
|
|||
|
|
|
|||
|
|
// 工人忙标志:确保同一时刻只有一个 Worker 在执行
|
|||
|
|
workerBusy atomic.Bool
|
|||
|
|
|
|||
|
|
// 重置进行中标志:人工重置暂存台期间置位,阻止 trySchedule 发起新补料 dispatch,
|
|||
|
|
// 避免 reset 直接写 DB 与补料搬运并发。仅挡"新发起",在途 worker 由 BeginReset 拒绝。
|
|||
|
|
resetting atomic.Bool
|
|||
|
|
|
|||
|
|
// 补料器(trySchedule 末尾检查)
|
|||
|
|
replenisher Replenisher
|
|||
|
|
|
|||
|
|
// 调度器
|
|||
|
|
sched *scheduler.Scheduler
|
|||
|
|
|
|||
|
|
machineActors map[int]actor.MachineActor
|
|||
|
|
|
|||
|
|
tempStoreActor *actor.TempStoreActor
|
|||
|
|
|
|||
|
|
// 接驳台变更回调(通知前端刷新)
|
|||
|
|
onDockChange func()
|
|||
|
|
|
|||
|
|
// Job 操作
|
|||
|
|
jobOps OrderProcessorInterface
|
|||
|
|
|
|||
|
|
// 事件总线(发布 job 状态变更,供 EventLogWriter 消费)
|
|||
|
|
eventBus interface {
|
|||
|
|
Publish(context.Context, eventbus.Event) error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PLC 信号读取(nil 表示不可用)
|
|||
|
|
plc PLCReader
|
|||
|
|
|
|||
|
|
// MOM
|
|||
|
|
momClient *momlib.Mom
|
|||
|
|
momConf momlib.MomConf
|
|||
|
|
momBiz MomBizConfig
|
|||
|
|
dockReader DockStateReader
|
|||
|
|
lastDockStates []bool
|
|||
|
|
|
|||
|
|
// 抽检调度标志:同一调度周期只抽一个工件
|
|||
|
|
samplingDispatched bool
|
|||
|
|
|
|||
|
|
recoveryActive atomic.Bool
|
|||
|
|
recoveryOrderIDs []int
|
|||
|
|
|
|||
|
|
stopCh chan struct{}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RuntimeSnapshot 工件运行时快照,含调度所需的 recipe 步骤信息
|
|||
|
|
type RuntimeSnapshot struct {
|
|||
|
|
JobID int
|
|||
|
|
WorkpieceNo string
|
|||
|
|
WorkOrderID int
|
|||
|
|
ProductTypeID int
|
|||
|
|
RecipeID int
|
|||
|
|
Status string
|
|||
|
|
PositionType string
|
|||
|
|
PositionRefID string
|
|||
|
|
StepID string // 当前步骤业务ID(OP10-OP120)
|
|||
|
|
StepIndex int // 当前步骤序号(从 recipe_step 加载,供调度器排序)
|
|||
|
|
TempSlotNo int
|
|||
|
|
DockNo int // 接驳台号(Dock 卸料用)
|
|||
|
|
DockSlotNo int
|
|||
|
|
// recipe 步骤信息(从 DB 加载,供调度器使用)
|
|||
|
|
StepType constants.StepType
|
|||
|
|
ResourceType string
|
|||
|
|
ToolType string
|
|||
|
|
StepName string
|
|||
|
|
Context map[string]any
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewProductionEventLoop(
|
|||
|
|
entClient *ent.Client,
|
|||
|
|
worker HardwareWorker,
|
|||
|
|
machineActors map[int]actor.MachineActor,
|
|||
|
|
tempStoreActor *actor.TempStoreActor,
|
|||
|
|
sched *scheduler.Scheduler,
|
|||
|
|
bus interface {
|
|||
|
|
Publish(context.Context, eventbus.Event) error
|
|||
|
|
},
|
|||
|
|
jobOps OrderProcessorInterface,
|
|||
|
|
replenisher Replenisher,
|
|||
|
|
plc PLCReader,
|
|||
|
|
momClient *momlib.Mom,
|
|||
|
|
momConf momlib.MomConf,
|
|||
|
|
momBiz MomBizConfig,
|
|||
|
|
dockReader DockStateReader,
|
|||
|
|
) *ProductionEventLoop {
|
|||
|
|
if machineActors == nil {
|
|||
|
|
machineActors = make(map[int]actor.MachineActor)
|
|||
|
|
}
|
|||
|
|
return &ProductionEventLoop{
|
|||
|
|
db: NewDBState(entClient),
|
|||
|
|
entClient: entClient,
|
|||
|
|
worker: worker,
|
|||
|
|
machineActors: machineActors,
|
|||
|
|
tempStoreActor: tempStoreActor,
|
|||
|
|
sched: sched,
|
|||
|
|
eventBus: bus,
|
|||
|
|
jobOps: jobOps,
|
|||
|
|
replenisher: replenisher,
|
|||
|
|
plc: plc,
|
|||
|
|
momClient: momClient,
|
|||
|
|
momConf: momConf,
|
|||
|
|
momBiz: momBiz,
|
|||
|
|
dockReader: dockReader,
|
|||
|
|
lastDockStates: make([]bool, 0),
|
|||
|
|
msgCh: make(chan EventLoopMessage, 256),
|
|||
|
|
jobRuntimes: make(map[int]*RuntimeSnapshot),
|
|||
|
|
dispatching: make(map[int]bool),
|
|||
|
|
stopCh: make(chan struct{}),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetOnDockChange 设置接驳台变更回调(SSE 通知前端刷新)
|
|||
|
|
func (l *ProductionEventLoop) SetOnDockChange(fn func()) {
|
|||
|
|
l.onDockChange = fn
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *ProductionEventLoop) notifyDockChange() {
|
|||
|
|
if l.onDockChange != nil {
|
|||
|
|
l.onDockChange()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ReleaseTempSlots 释放暂存台 Actor 内存中指定 job 占用的槽位。
|
|||
|
|
// 用于人工重置暂存台等已直接写 DB 清空 job.temp_slot_no 的场景:
|
|||
|
|
// DB 已清空但 Actor 内存 slots 仍持有旧 jobID,不释放会让后续 AllocateSlot
|
|||
|
|
// 跳过这些槽位、最终假性判满(ErrTempSlotFull),补料停摆直到进程重启。
|
|||
|
|
// 同步调用 Actor.ReleaseSlot,幂等(job 不在槽位中仅记警告;DB temp_slot_no 重复清空无害)。
|
|||
|
|
func (l *ProductionEventLoop) ReleaseTempSlots(jobIDs []int) {
|
|||
|
|
if l.tempStoreActor == nil || len(jobIDs) == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
ctx := context.Background()
|
|||
|
|
for _, jobID := range jobIDs {
|
|||
|
|
l.tempStoreActor.ReleaseSlot(ctx, jobID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BeginReset 尝试进入重置态。返回是否可进入及不可进入的原因。
|
|||
|
|
// 在 reset 事务前调用:若有在途 worker/补料则拒绝(避免与正在执行的搬运并发),
|
|||
|
|
// 否则置 resetting 阻止后续 trySchedule 发起新补料 dispatch。必须配对调用 EndReset。
|
|||
|
|
func (l *ProductionEventLoop) BeginReset() (bool, string) {
|
|||
|
|
if l.workerBusy.Load() {
|
|||
|
|
return false, "产线正在执行搬运,请稍后重试"
|
|||
|
|
}
|
|||
|
|
if l.isReplenishing() {
|
|||
|
|
return false, "补料进行中,请稍后重试"
|
|||
|
|
}
|
|||
|
|
l.resetting.Store(true)
|
|||
|
|
return true, ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EndReset 退出重置态,恢复补料调度。
|
|||
|
|
func (l *ProductionEventLoop) EndReset() {
|
|||
|
|
l.resetting.Store(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsResetting 返回是否处于重置态(供调度桥接判断是否跳过补料)。
|
|||
|
|
func (l *ProductionEventLoop) IsResetting() bool {
|
|||
|
|
return l.resetting.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SendScheduleTick 投递立即调度事件(非阻塞)。
|
|||
|
|
// 用非阻塞发送避免在 event loop goroutine 内调用时死锁自己;
|
|||
|
|
// channel 满意味着已有待处理的调度事件,丢弃不造成功能缺失。
|
|||
|
|
func (l *ProductionEventLoop) SendScheduleTick() {
|
|||
|
|
select {
|
|||
|
|
case l.msgCh <- EventLoopMessage{
|
|||
|
|
ID: fmt.Sprintf("sched-tick-%d", time.Now().UnixNano()),
|
|||
|
|
Type: EvtScheduleTick,
|
|||
|
|
}:
|
|||
|
|
default:
|
|||
|
|
slog.Warn("event loop: schedule tick dropped, channel full")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Send 向事件循环投递消息(channel 满时阻塞)
|
|||
|
|
func (l *ProductionEventLoop) Send(msg EventLoopMessage) {
|
|||
|
|
l.msgCh <- msg
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SendSync 投递消息并等待结果
|
|||
|
|
func (l *ProductionEventLoop) SendSync(msg EventLoopMessage, timeout time.Duration) (*MessageResult, error) {
|
|||
|
|
replyCh := make(chan MessageResult, 1)
|
|||
|
|
msg.Reply = replyCh
|
|||
|
|
l.msgCh <- msg
|
|||
|
|
select {
|
|||
|
|
case result := <-replyCh:
|
|||
|
|
return &result, nil
|
|||
|
|
case <-time.After(timeout):
|
|||
|
|
return nil, fmt.Errorf("event loop timeout")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// InitActorsFromDB 启动时从 DB 恢复所有 Actor 槽位状态(设备 + 暂存台)。
|
|||
|
|
func (l *ProductionEventLoop) InitActorsFromDB(ctx context.Context) {
|
|||
|
|
if l.entClient == nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设备槽位:从 equipment_slot 表恢复
|
|||
|
|
slots, err := l.entClient.EquipmentSlot.Query().
|
|||
|
|
Where(equipmentslot.StatusIn(constants.SlotStatus_Occupied, constants.SlotStatus_Done)).
|
|||
|
|
All(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("event loop: init equipment slots from db failed", "error", err)
|
|||
|
|
} else {
|
|||
|
|
for _, s := range slots {
|
|||
|
|
if a, ok := l.machineActors[*s.EquipmentId]; ok {
|
|||
|
|
jobID := 0
|
|||
|
|
if s.CurrentJobId != nil {
|
|||
|
|
jobID = *s.CurrentJobId
|
|||
|
|
}
|
|||
|
|
ts := time.Now()
|
|||
|
|
if s.OccupiedAt != nil {
|
|||
|
|
ts = *s.OccupiedAt
|
|||
|
|
}
|
|||
|
|
a.RestoreSlot(s.SlotNo, string(s.Status), jobID, ts)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
slog.Info("event loop: equipment slots initialized", "count", len(slots))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 暂存台槽位:从 job 表恢复
|
|||
|
|
jobs, err := l.db.GetActiveJobs(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("event loop: init temp slots from db failed", "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for _, j := range jobs {
|
|||
|
|
if j.TempSlotNo != nil && *j.TempSlotNo > 0 && l.tempStoreActor != nil {
|
|||
|
|
l.tempStoreActor.RestoreSlot(*j.TempSlotNo, j.ID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
slog.Info("event loop: temp store slots initialized", "jobs", len(jobs))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Run 启动事件循环(阻塞)
|
|||
|
|
func (l *ProductionEventLoop) Run(ctx context.Context) {
|
|||
|
|
slog.Info("event loop: started")
|
|||
|
|
schedTimer := time.NewTimer(60 * time.Second)
|
|||
|
|
defer schedTimer.Stop()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case msg := <-l.msgCh:
|
|||
|
|
l.handleMessage(ctx, msg)
|
|||
|
|
// Worker 完成后重置调度定时器(worker 忙时 tick 不触发调度)
|
|||
|
|
if !l.workerBusy.Load() {
|
|||
|
|
schedTimer.Reset(60 * time.Second)
|
|||
|
|
}
|
|||
|
|
case <-schedTimer.C:
|
|||
|
|
if !l.workerBusy.Load() {
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
}
|
|||
|
|
l.tryPollMomOrders(ctx)
|
|||
|
|
schedTimer.Reset(60 * time.Second)
|
|||
|
|
case <-l.stopCh:
|
|||
|
|
slog.Info("event loop: stopped")
|
|||
|
|
return
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
slog.Info("event loop: context done")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop 停止事件循环
|
|||
|
|
func (l *ProductionEventLoop) Stop() {
|
|||
|
|
close(l.stopCh)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleMessage 消息分发
|
|||
|
|
func (l *ProductionEventLoop) handleMessage(ctx context.Context, msg EventLoopMessage) {
|
|||
|
|
slog.Debug("event loop: handling message", "type", msg.Type, "id", msg.ID)
|
|||
|
|
|
|||
|
|
switch msg.Type {
|
|||
|
|
// Command
|
|||
|
|
case CmdStartOrder:
|
|||
|
|
l.handleStartOrder(ctx, msg)
|
|||
|
|
case CmdPauseOrder:
|
|||
|
|
l.handlePauseOrder(ctx, msg)
|
|||
|
|
case CmdResumeOrder:
|
|||
|
|
l.handleResumeOrder(ctx, msg)
|
|||
|
|
case CmdCancelOrder:
|
|||
|
|
l.handleCancelOrder(ctx, msg)
|
|||
|
|
case CmdStopOrder:
|
|||
|
|
l.handleStopOrder(ctx, msg)
|
|||
|
|
case CmdSuspendJob:
|
|||
|
|
l.handleSuspendJob(ctx, msg)
|
|||
|
|
case CmdResumeJob:
|
|||
|
|
l.handleResumeJob(ctx, msg)
|
|||
|
|
case CmdReworkJob:
|
|||
|
|
l.handleReworkJob(ctx, msg)
|
|||
|
|
case CmdRestoreOrder:
|
|||
|
|
l.handleRestoreOrder(ctx, msg)
|
|||
|
|
|
|||
|
|
// ExternalEvent
|
|||
|
|
case EvtMachineDone:
|
|||
|
|
l.handleMachineDone(ctx, msg)
|
|||
|
|
case EvtStepTimeout:
|
|||
|
|
l.handleStepTimeout(ctx, msg)
|
|||
|
|
case EvtScheduleTick:
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
case EvtAgvRequestEntry:
|
|||
|
|
l.handleAgvRequestEntry(ctx, msg)
|
|||
|
|
|
|||
|
|
// WorkerResult
|
|||
|
|
case ResRobotActionSucceeded, ResRobotActionFailed,
|
|||
|
|
ResToolActionSucceeded, ResToolActionFailed,
|
|||
|
|
ResMachineStartSucceeded, ResMachineStartFailed:
|
|||
|
|
l.handleWorkerResult(ctx, msg)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
func (l *ProductionEventLoop) SetRecoveryPending(orderIDs []int) {
|
|||
|
|
l.recoveryOrderIDs = orderIDs
|
|||
|
|
l.recoveryActive.Store(len(orderIDs) > 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *ProductionEventLoop) SyncMachineSlotStatus(jobID int, posType string) {
|
|||
|
|
for _, a := range l.machineActors {
|
|||
|
|
if slot, found := a.FindSlotByJobID(jobID); found {
|
|||
|
|
switch posType {
|
|||
|
|
case "ON_EQUIPMENT_DONE":
|
|||
|
|
a.RestoreSlot(slot, string(constants.SlotStatus_Done), jobID, time.Now())
|
|||
|
|
case "ON_EQUIPMENT_PROCESSING":
|
|||
|
|
a.RestoreSlot(slot, string(constants.SlotStatus_Occupied), jobID, time.Now())
|
|||
|
|
default:
|
|||
|
|
a.ReleaseSlot(slot)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
func (l *ProductionEventLoop) IsRecoveryActive() bool {
|
|||
|
|
return l.recoveryActive.Load()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *ProductionEventLoop) OccupyTempSlot(slotNo, jobID int) {
|
|||
|
|
if l.tempStoreActor != nil {
|
|||
|
|
l.tempStoreActor.RestoreSlot(slotNo, jobID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *ProductionEventLoop) CompleteRecovery(ctx context.Context) error {
|
|||
|
|
for _, id := range l.recoveryOrderIDs {
|
|||
|
|
_, err := l.entClient.WorkOrder.UpdateOneID(id).
|
|||
|
|
SetStatus(constants.WorkOrderStatus_Paused).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("complete recovery: order %d: %w", id, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
l.recoveryOrderIDs = nil
|
|||
|
|
l.recoveryActive.Store(false)
|
|||
|
|
slog.Info("recovery: completed, orders set to PAUSED")
|
|||
|
|
l.SendScheduleTick()
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (l *ProductionEventLoop) reply(msg EventLoopMessage, success bool, errMsg string, data map[string]any) {
|
|||
|
|
if msg.Reply != nil {
|
|||
|
|
msg.Reply <- MessageResult{Success: success, Error: errMsg, Data: data}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleMachineDone 设备完成信号处理
|
|||
|
|
// handleMachineDone 处理设备完成信号(Actor 已管理槽位状态 + 写 DB,EventLoop 只需推进步骤)
|
|||
|
|
func (l *ProductionEventLoop) handleMachineDone(ctx context.Context, msg EventLoopMessage) {
|
|||
|
|
machineID := intFromPayload(msg.Payload, "machineId")
|
|||
|
|
if machineID == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if l.entClient == nil {
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测结果:Actor 已标记槽位或直接回调,EventLoop 根据 pass/fail 处理
|
|||
|
|
if inspectPass, hasInspect := msg.Payload["inspectionPass"].(bool); hasInspect {
|
|||
|
|
l.handleInspectionDone(ctx, machineID, inspectPass)
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 普通设备完成:Actor 已标记槽位 Done + 写 DB,EventLoop 推进步骤
|
|||
|
|
jobID := intFromPayload(msg.Payload, "jobId")
|
|||
|
|
if jobID == 0 {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// CNC 设备 OPERATION 步骤:记录加工完成时间
|
|||
|
|
if l.db.IsCNCEquipment(ctx, machineID) && l.isOperationStep(jobID) {
|
|||
|
|
if err := l.db.SetCncCompletedAt(ctx, jobID); err != nil {
|
|||
|
|
slog.Error("event loop: set cnc completed at failed", "jobId", jobID, "error", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 所有 OPERATION 步骤发布 MACHINE_DONE(EventLogWriter 会更新对应 MACHINE_START 的 endedAt)
|
|||
|
|
if l.isOperationStep(jobID) {
|
|||
|
|
machineDonePayload := map[string]any{
|
|||
|
|
"jobId": jobID,
|
|||
|
|
"equipmentId": machineID,
|
|||
|
|
"equipmentName": equipmentName(machineID),
|
|||
|
|
"endedAt": time.Now(),
|
|||
|
|
}
|
|||
|
|
// 补充工件编号和加工时长
|
|||
|
|
doneJob, _ := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if doneJob != nil {
|
|||
|
|
machineDonePayload["workpieceNo"] = doneJob.WorkpieceNo
|
|||
|
|
if !doneJob.CncStartedAt.IsZero() {
|
|||
|
|
machineDonePayload["startedAt"] = doneJob.CncStartedAt
|
|||
|
|
machineDonePayload["duration"] = fmt.Sprintf("%.0f秒", time.Since(doneJob.CncStartedAt).Seconds())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
l.publishEvent(ctx, eventbus.EventMachineDone, "job", strconv.Itoa(jobID), machineDonePayload)
|
|||
|
|
}
|
|||
|
|
job, _ := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if job != nil && job.Status == constants.JobStatus_Processing {
|
|||
|
|
if l.isOperationStep(jobID) {
|
|||
|
|
l.db.SetJobWaitingUnload(ctx, jobID)
|
|||
|
|
} else {
|
|||
|
|
finished, _ := l.db.AdvanceStep(ctx, jobID, nil, "", "")
|
|||
|
|
if finished {
|
|||
|
|
l.checkPalletCompletion(ctx, jobID)
|
|||
|
|
l.checkOrderCompletion(ctx, jobID)
|
|||
|
|
} else {
|
|||
|
|
l.entClient.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_WaitingUnload).
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if l.jobOps != nil {
|
|||
|
|
l.jobOps.WakeJob(jobID)
|
|||
|
|
}
|
|||
|
|
l.trySchedule(ctx)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleInspectionDone 检测结果处理(由 handleMachineDone 统一调用,slot 已设为 Done)
|
|||
|
|
func (l *ProductionEventLoop) handleInspectionDone(ctx context.Context, machineID int, pass bool) {
|
|||
|
|
slot, _ := l.entClient.EquipmentSlot.Query().
|
|||
|
|
Where(equipmentslot.EquipmentIdEQ(machineID), equipmentslot.StatusEQ(constants.SlotStatus_Done)).
|
|||
|
|
First(ctx)
|
|||
|
|
if slot == nil {
|
|||
|
|
slog.Warn("event loop: inspection done, no done slot found", "machineId", machineID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
jobID := slotJobID(slot)
|
|||
|
|
if jobID == 0 {
|
|||
|
|
slog.Warn("event loop: inspection done, no job on slot", "machineId", machineID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设备类型决定检测结果写入哪个字段(INSPECTION/SAMPLING)
|
|||
|
|
var machineType string
|
|||
|
|
if a, ok := l.machineActors[machineID]; ok {
|
|||
|
|
machineType = a.Type()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发布检测结果事件
|
|||
|
|
inspPayload := map[string]any{
|
|||
|
|
"jobId": jobID,
|
|||
|
|
"equipmentId": machineID,
|
|||
|
|
"equipmentName": equipmentName(machineID),
|
|||
|
|
"pass": pass,
|
|||
|
|
"endedAt": time.Now(),
|
|||
|
|
}
|
|||
|
|
inspJob, _ := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if inspJob != nil {
|
|||
|
|
inspPayload["workpieceNo"] = inspJob.WorkpieceNo
|
|||
|
|
}
|
|||
|
|
l.publishEvent(ctx, eventbus.EventInspectionResult, "job", strconv.Itoa(jobID), inspPayload)
|
|||
|
|
|
|||
|
|
if pass {
|
|||
|
|
if l.isOperationStep(jobID) {
|
|||
|
|
jb, err := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("event loop: inspection done, get job failed", "jobId", jobID, "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if err := l.db.CompleteStep(ctx, jobID, jb.CurrentStepId, map[string]any{"inspectionPass": true}); err != nil {
|
|||
|
|
slog.Error("event loop: inspection done, complete step failed", "jobId", jobID, "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if err := l.db.SetJobWaitingUnload(ctx, jobID); err != nil {
|
|||
|
|
slog.Error("event loop: inspection done, set waiting unload failed", "jobId", jobID, "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
finished, err := l.db.AdvanceStep(ctx, jobID, map[string]any{"inspectionPass": true}, "", "")
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("event loop: inspection done, advance step failed", "jobId", jobID, "error", err)
|
|||
|
|
} else if finished {
|
|||
|
|
l.checkPalletCompletion(ctx, jobID)
|
|||
|
|
l.checkOrderCompletion(ctx, jobID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 检测合格:写入独立结果字段(与 context.inspectionPass 双写)
|
|||
|
|
if machineType != "" {
|
|||
|
|
if err := l.db.SetQCResult(ctx, jobID, machineType, true); err != nil {
|
|||
|
|
slog.Error("event loop: set qc result(pass) failed", "jobId", jobID, "error", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
job, err := l.entClient.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("event loop: inspection done, job not found", "jobId", jobID, "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
// 状态守卫:如果 job 已不在 PROCESSING(如已被 SignalDone 通过),跳过报废
|
|||
|
|
if job.Status != constants.JobStatus_Processing {
|
|||
|
|
slog.Warn("event loop: inspection fail ignored, job already advanced",
|
|||
|
|
"jobId", jobID, "status", job.Status)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录检测结果到 job context(留作审计追溯),再报废工件
|
|||
|
|
l.db.CompleteStep(ctx, jobID, job.CurrentStepId, map[string]any{"inspectionPass": false})
|
|||
|
|
// 同步写入独立结果字段(FAIL),作为合格率统计一等数据源
|
|||
|
|
if machineType != "" {
|
|||
|
|
if err := l.db.SetQCResult(ctx, jobID, machineType, false); err != nil {
|
|||
|
|
slog.Error("event loop: set qc result(fail) failed", "jobId", jobID, "error", err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
_ = l.db.RaiseAlarm(ctx, "INSPECTION_FAIL",
|
|||
|
|
fmt.Sprintf("工件 %s 检测不合格", job.WorkpieceNo),
|
|||
|
|
constants.AlarmLevel_WARN, machineID, jobID, "event-loop")
|
|||
|
|
_ = l.db.FinishJob(ctx, jobID, job.WorkOrderId, constants.JobStatus_Scrapped)
|
|||
|
|
l.checkPalletCompletion(ctx, jobID)
|
|||
|
|
l.checkOrderCompletion(ctx, jobID)
|
|||
|
|
l.publishProductionEvent(ctx, eventbus.EventJobScrapped,
|
|||
|
|
jobID, job.WorkpieceNo, 0, "", 0, nil)
|
|||
|
|
_ = l.db.SetEquipmentSlot(ctx, machineID, slot.SlotNo, constants.SlotStatus_Done, constants.SlotStatus_Empty, 0)
|
|||
|
|
// 同步释放 actor 内存槽位(NG 不走到 UNLOAD_COMPLETE 路径)
|
|||
|
|
if a, ok := l.machineActors[machineID]; ok {
|
|||
|
|
a.ReleaseSlot(slot.SlotNo)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// isOperationStep 判断 job 当前步骤是否为 OPERATION 类型。
|
|||
|
|
// 优先查内存缓存;缓存缺失时 fallback 到 DB 查询,防止 handleMachineDone 等场景下
|
|||
|
|
// 因 RuntimeSnapshot 尚未刷新而错误地走了 AdvanceStep 分支。
|
|||
|
|
func (l *ProductionEventLoop) isOperationStep(jobID int) bool {
|
|||
|
|
if snap := l.jobRuntimes[jobID]; snap != nil {
|
|||
|
|
return snap.StepType == constants.StepType_Operation
|
|||
|
|
}
|
|||
|
|
// 缓存缺失 → 查 DB
|
|||
|
|
if l.db != nil {
|
|||
|
|
jb, err := l.entClient.Job.Get(context.Background(), jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
_, stepType, _, _, _, err := l.db.GetRecipeStepByID(context.Background(), jb.RecipeId, jb.CurrentStepId)
|
|||
|
|
if err != nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
return stepType == constants.StepType_Operation
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// slotJobID 从槽位安全获取 jobID
|
|||
|
|
func slotJobID(s *ent.EquipmentSlot) int {
|
|||
|
|
if s.CurrentJobId != nil {
|
|||
|
|
return *s.CurrentJobId
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// intFromPayload 从 payload 中提取 int 值
|
|||
|
|
func intFromPayload(p map[string]any, key string) int {
|
|||
|
|
switch v := p[key].(type) {
|
|||
|
|
case int:
|
|||
|
|
return v
|
|||
|
|
case float64:
|
|||
|
|
return int(v)
|
|||
|
|
}
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// stringFromPayload 从 payload 中提取 string 值
|
|||
|
|
func stringFromPayload(p map[string]any, key string) string {
|
|||
|
|
if v, ok := p[key].(string); ok {
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|