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