782 lines
26 KiB
Go
782 lines
26 KiB
Go
package eventloop
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"log/slog"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"bj_power_mes/constants"
|
|||
|
|
"bj_power_mes/ent"
|
|||
|
|
"bj_power_mes/ent/equipment"
|
|||
|
|
"bj_power_mes/ent/equipmentslot"
|
|||
|
|
"bj_power_mes/ent/equipmenttype"
|
|||
|
|
"bj_power_mes/ent/job"
|
|||
|
|
"bj_power_mes/ent/recipestep"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// DoneSlotInfo 完成槽位信息
|
|||
|
|
type DoneSlotInfo struct {
|
|||
|
|
JobID int
|
|||
|
|
SlotNo int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DBState ent 原子条件更新封装,仅由 event loop 使用
|
|||
|
|
type DBState struct {
|
|||
|
|
client *ent.Client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewDBState(client *ent.Client) *DBState {
|
|||
|
|
return &DBState{client: client}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CompleteStep 完成当前步骤,推进到下一步 stepId
|
|||
|
|
func (d *DBState) CompleteStep(ctx context.Context, jobID int, nextStepID string, contextPatch map[string]any) error {
|
|||
|
|
update := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetCurrentStepId(nextStepID).
|
|||
|
|
AddVersion(1)
|
|||
|
|
if contextPatch != nil {
|
|||
|
|
// 合并而非替换 context:先读取现有 context,再写入 patch
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("complete step: get job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
ctxMap := jb.Context
|
|||
|
|
if ctxMap == nil {
|
|||
|
|
ctxMap = make(map[string]any)
|
|||
|
|
}
|
|||
|
|
for k, v := range contextPatch {
|
|||
|
|
ctxMap[k] = v
|
|||
|
|
}
|
|||
|
|
update = update.SetContext(ctxMap)
|
|||
|
|
}
|
|||
|
|
_, err := update.Save(ctx)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FinishJob 工件终态。
|
|||
|
|
// 使用事务确保 job 状态切换 + work_order 计数累加原子执行。
|
|||
|
|
func (d *DBState) FinishJob(ctx context.Context, jobID, workOrderID int, terminalStatus constants.JobStatus) error {
|
|||
|
|
tx, err := d.client.Tx(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
defer tx.Rollback()
|
|||
|
|
|
|||
|
|
_, err = tx.Job.UpdateOneID(jobID).
|
|||
|
|
Where(job.StatusNotIn(
|
|||
|
|
constants.JobStatus_Completed,
|
|||
|
|
constants.JobStatus_Scrapped,
|
|||
|
|
)).
|
|||
|
|
SetStatus(terminalStatus).
|
|||
|
|
SetCompletedAt(time.Now()).
|
|||
|
|
ClearTempSlotNo().
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("finish job failed", "jobID", jobID, "error", err)
|
|||
|
|
return fmt.Errorf("finish job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if terminalStatus == constants.JobStatus_Completed {
|
|||
|
|
_, err = tx.WorkOrder.UpdateOneID(workOrderID).AddFinishedNum(1).Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("finish job %d: add finishedNum: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
} else if terminalStatus == constants.JobStatus_Scrapped {
|
|||
|
|
_, err = tx.WorkOrder.UpdateOneID(workOrderID).AddFailNum(1).Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("finish job: add failNum failed", "jobID", jobID, "error", err)
|
|||
|
|
return fmt.Errorf("finish job %d: add failNum: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
// 报废工件工人取出,清空位置
|
|||
|
|
_, err = tx.Job.UpdateOneID(jobID).
|
|||
|
|
SetPositionType("").
|
|||
|
|
SetPositionRefId("").
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("finish job: clear position failed", "jobID", jobID, "error", err)
|
|||
|
|
return fmt.Errorf("finish job %d: clear position: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return tx.Commit()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetEquipmentSlot 更新设备槽位状态,管理 OccupiedAt 生命周期:
|
|||
|
|
// - Occupied: 设置 OccupiedAt=now(记录占用时间)
|
|||
|
|
// - Done: 保留 OccupiedAt(工件仍在设备上)
|
|||
|
|
// - Empty: 清除 OccupiedAt + CurrentJobId(设备已释放)
|
|||
|
|
func (d *DBState) SetEquipmentSlot(ctx context.Context, equipmentID, slotNo int, expectedStatus, nextStatus constants.SlotStatus, jobID int) error {
|
|||
|
|
update := d.client.EquipmentSlot.Update().
|
|||
|
|
Where(
|
|||
|
|
equipmentslot.EquipmentIdEQ(equipmentID),
|
|||
|
|
equipmentslot.SlotNoEQ(slotNo),
|
|||
|
|
equipmentslot.StatusEQ(expectedStatus),
|
|||
|
|
).
|
|||
|
|
SetStatus(nextStatus)
|
|||
|
|
|
|||
|
|
switch nextStatus {
|
|||
|
|
case constants.SlotStatus_Occupied:
|
|||
|
|
update = update.SetCurrentJobId(jobID).SetOccupiedAt(time.Now())
|
|||
|
|
case constants.SlotStatus_Done:
|
|||
|
|
update = update.SetCurrentJobId(jobID)
|
|||
|
|
case constants.SlotStatus_Empty:
|
|||
|
|
update = update.ClearCurrentJobId().ClearOccupiedAt()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
n, err := update.Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set slot %d-%d: %w", equipmentID, slotNo, err)
|
|||
|
|
}
|
|||
|
|
if n == 0 {
|
|||
|
|
return fmt.Errorf("set slot %d-%d: expected %s", equipmentID, slotNo, expectedStatus)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetJobProcessing 设置 job 为 PROCESSING 状态并记录设备位置
|
|||
|
|
func (d *DBState) SetJobProcessing(ctx context.Context, jobID, equipmentID, slotNo int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_Processing).
|
|||
|
|
SetPositionType(constants.PositionType_OnEquipment).
|
|||
|
|
SetPositionRefId(fmt.Sprintf("%d:%d", equipmentID, slotNo)).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set job %d processing: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetJobOnEquipment 仅更新 job 位置到指定设备槽位,不修改状态/版本(用于设备内槽位转移场景)
|
|||
|
|
func (d *DBState) SetJobOnEquipment(ctx context.Context, jobID, equipmentID, slotNo int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetPositionType(constants.PositionType_OnEquipment).
|
|||
|
|
SetPositionRefId(fmt.Sprintf("%d:%d", equipmentID, slotNo)).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set job %d on equipment %d slot %d: %w", jobID, equipmentID, slotNo, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetJobWaitingUnload 设置 job 为 WAITING_UNLOAD 状态
|
|||
|
|
func (d *DBState) SetJobWaitingUnload(ctx context.Context, jobID int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_WaitingUnload).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set job %d waiting unload: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetActiveJobs 获取所有非终态 job
|
|||
|
|
func (d *DBState) GetActiveJobs(ctx context.Context) ([]*ent.Job, error) {
|
|||
|
|
return d.client.Job.Query().
|
|||
|
|
Where(job.StatusNotIn(
|
|||
|
|
constants.JobStatus_Completed,
|
|||
|
|
constants.JobStatus_Scrapped,
|
|||
|
|
)).Order(job.ByID()).
|
|||
|
|
All(ctx)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdvanceStep 推进到下一步:先合并 context → 匹配 nextStepBranches → 否则 nextStepDefault → 否则线性推进。
|
|||
|
|
// 推进后若下一步是 Decision/Judge 则链式内联处理(纯规则评估,无需硬件动作),
|
|||
|
|
// 直至遇到需要等待或 Worker 动作的步骤。返回 true 表示已是最后一步(job 已完成)。
|
|||
|
|
func (d *DBState) AdvanceStep(ctx context.Context, jobID int, contextUpdates map[string]any, posType constants.PositionType, posRefID string) (bool, error) {
|
|||
|
|
maxIter := 100 // 安全上限,防止 Decision 步骤形成循环
|
|||
|
|
for i := 0; i < maxIter; i++ {
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: job %d not found: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 合并上下文更新(分支匹配前合并,让 contextUpdates 中的键可触发分支)
|
|||
|
|
ctxMap := jb.Context
|
|||
|
|
if ctxMap == nil {
|
|||
|
|
ctxMap = make(map[string]any)
|
|||
|
|
}
|
|||
|
|
for k, v := range contextUpdates {
|
|||
|
|
ctxMap[k] = v
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确定下一步 stepId
|
|||
|
|
var nextStepID string
|
|||
|
|
currentStepID := jb.CurrentStepId
|
|||
|
|
|
|||
|
|
// 1. 优先 nextStepBranches(条件跳步)
|
|||
|
|
if branchID, matched := d.resolveBranch(ctx, jb.RecipeId, currentStepID, ctxMap); matched {
|
|||
|
|
nextStepID = branchID
|
|||
|
|
} else {
|
|||
|
|
// 2. 其次 nextStepDefault(指定跳步)
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(jb.RecipeId), recipestep.StepIdEQ(currentStepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: query current step %s: %w", currentStepID, err)
|
|||
|
|
}
|
|||
|
|
if step.NextStepDefault != "" {
|
|||
|
|
nextStepID = step.NextStepDefault
|
|||
|
|
} else {
|
|||
|
|
// 3. 线性推进:当前 stepIndex + 1 → 找对应 stepId
|
|||
|
|
nextID, err := d.GetStepIDByIndex(ctx, jb.RecipeId, step.StepIndex+1)
|
|||
|
|
if err != nil {
|
|||
|
|
// 查询不到下一步 → 确认为最后一步则完成 job
|
|||
|
|
maxStep, maxErr := d.GetRecipeMaxStepIndex(ctx, jb.RecipeId)
|
|||
|
|
if maxErr != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: get recipe max for job %d: %w", jobID, maxErr)
|
|||
|
|
}
|
|||
|
|
if step.StepIndex >= maxStep {
|
|||
|
|
if posType != "" || posRefID != "" {
|
|||
|
|
upd := d.client.Job.UpdateOneID(jobID).AddVersion(1)
|
|||
|
|
if posType != "" {
|
|||
|
|
upd = upd.SetPositionType(posType)
|
|||
|
|
}
|
|||
|
|
if posRefID != "" {
|
|||
|
|
upd = upd.SetPositionRefId(posRefID)
|
|||
|
|
}
|
|||
|
|
if _, err := upd.Save(ctx); err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true, d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Completed)
|
|||
|
|
}
|
|||
|
|
return false, fmt.Errorf("advance step: no next step after %s: %w", currentStepID, err)
|
|||
|
|
}
|
|||
|
|
nextStepID = nextID
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 是否最后一步:查 nextStepID 对应的 stepIndex 与 maxStepIndex 比较
|
|||
|
|
nextIdx, err := d.GetStepIndexByStepID(ctx, jb.RecipeId, nextStepID)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: get index for %s: %w", nextStepID, err)
|
|||
|
|
}
|
|||
|
|
maxStep, err := d.GetRecipeMaxStepIndex(ctx, jb.RecipeId)
|
|||
|
|
if err != nil {
|
|||
|
|
return false, fmt.Errorf("advance step: get recipe max for job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
isLast := nextIdx > maxStep
|
|||
|
|
|
|||
|
|
// 最后一步:先写位置再 FinishJob,确保终态位置正确
|
|||
|
|
if isLast {
|
|||
|
|
if posType != "" || posRefID != "" {
|
|||
|
|
upd := d.client.Job.UpdateOneID(jobID).AddVersion(1)
|
|||
|
|
if posType != "" {
|
|||
|
|
upd = upd.SetPositionType(posType)
|
|||
|
|
}
|
|||
|
|
if posRefID != "" {
|
|||
|
|
upd = upd.SetPositionRefId(posRefID)
|
|||
|
|
}
|
|||
|
|
if _, err := upd.Save(ctx); err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true, d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Completed)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
update := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetCurrentStepId(nextStepID).
|
|||
|
|
SetContext(ctxMap).
|
|||
|
|
AddVersion(1)
|
|||
|
|
|
|||
|
|
if posType != "" {
|
|||
|
|
update = update.SetPositionType(posType)
|
|||
|
|
}
|
|||
|
|
if posRefID != "" {
|
|||
|
|
update = update.SetPositionRefId(posRefID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if _, err = update.Save(ctx); err != nil {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查下一步是否可内联处理(Decision/Judge:纯规则评估 + 条件跳步)。
|
|||
|
|
// signal_true 类型需实时读 PLC,不适合内联,退回给 trySchedule → handleDecisionCandidate 处理。
|
|||
|
|
_, stepType, _, _, _, err := d.GetRecipeStepByID(ctx, jb.RecipeId, nextStepID)
|
|||
|
|
if err != nil || (stepType != constants.StepType_Decision && stepType != constants.StepType_Judge) {
|
|||
|
|
return false, err
|
|||
|
|
}
|
|||
|
|
if isSignalTrueDecision(d, ctx, jb.RecipeId, nextStepID) {
|
|||
|
|
return false, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 计算 Decision 分支键,作为下一次迭代的 contextUpdates
|
|||
|
|
contextUpdates = evaluateDecisionForJob(d, ctx, jb.RecipeId, nextStepID, ctxMap, jb.WorkpieceNo)
|
|||
|
|
posType, posRefID = "", ""
|
|||
|
|
}
|
|||
|
|
return false, fmt.Errorf("advance step: exceeded max iterations for job %d, possible decision step cycle", jobID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// evaluateDecisionForJob 读 processingParams 并调用 evaluateDecision 原地修改 ctxMap 副本,
|
|||
|
|
// 返回新增的键值对作为下一轮 AdvanceStep 迭代的 contextUpdates。
|
|||
|
|
func evaluateDecisionForJob(d *DBState, ctx context.Context, recipeID int, stepID string, ctxMap map[string]any, workpieceNo string) map[string]any {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
params := step.ProcessingParams
|
|||
|
|
clone := make(map[string]any, len(ctxMap)+2)
|
|||
|
|
for k, v := range ctxMap {
|
|||
|
|
clone[k] = v
|
|||
|
|
}
|
|||
|
|
evaluateDecision(params, clone, workpieceNo, nil)
|
|||
|
|
result := make(map[string]any)
|
|||
|
|
for k, v := range clone {
|
|||
|
|
if _, ok := ctxMap[k]; !ok {
|
|||
|
|
result[k] = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// isSignalTrueDecision 检查 Decision 步骤是否使用 signal_true 类型(需实时读 PLC,不能内联处理)
|
|||
|
|
func isSignalTrueDecision(d *DBState, ctx context.Context, recipeID int, stepID string) bool {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil || step.ProcessingParams == nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
dt, _ := step.ProcessingParams["decisionType"].(string)
|
|||
|
|
return dt == "signal_true"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetRecipeStep 获取工艺路线中指定步骤的信息
|
|||
|
|
func (d *DBState) GetRecipeStep(ctx context.Context, recipeID, stepIndex int) (stepType constants.StepType, resourceType string, toolType string, stepName string, stepId string, err error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", "", "", "", "", fmt.Errorf("get recipe %d step %d: %w", recipeID, stepIndex, err)
|
|||
|
|
}
|
|||
|
|
return step.StepType, step.ResourceType, step.ToolType, step.StepName, step.StepId, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStepProcessingParams 返回工序的 processingParams(用于 Decision 步骤的通用判定规则)
|
|||
|
|
func (d *DBState) GetStepProcessingParams(ctx context.Context, recipeID, stepIndex int) (map[string]any, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("get recipe %d step %d processing params: %w", recipeID, stepIndex, err)
|
|||
|
|
}
|
|||
|
|
if step.ProcessingParams == nil {
|
|||
|
|
return nil, nil
|
|||
|
|
}
|
|||
|
|
return step.ProcessingParams, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStepProcessingParamsByID 根据 stepId 返回工序的 processingParams
|
|||
|
|
func (d *DBState) GetStepProcessingParamsByID(ctx context.Context, recipeID int, stepID string) (map[string]any, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("get recipe %d step %s processing params: %w", recipeID, stepID, err)
|
|||
|
|
}
|
|||
|
|
if step.ProcessingParams == nil {
|
|||
|
|
return nil, nil
|
|||
|
|
}
|
|||
|
|
return step.ProcessingParams, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetRecipeStepByID 根据 stepId 获取工艺路线中指定步骤的信息
|
|||
|
|
func (d *DBState) GetRecipeStepByID(ctx context.Context, recipeID int, stepID string) (stepIndex int, stepType constants.StepType, resourceType string, toolType string, stepName string, err error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, "", "", "", "", fmt.Errorf("get recipe %d step %s: %w", recipeID, stepID, err)
|
|||
|
|
}
|
|||
|
|
return step.StepIndex, step.StepType, step.ResourceType, step.ToolType, step.StepName, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStepIDByIndex 根据 stepIndex 查找 stepId
|
|||
|
|
func (d *DBState) GetStepIDByIndex(ctx context.Context, recipeID, stepIndex int) (string, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("get stepId by index %d: %w", stepIndex, err)
|
|||
|
|
}
|
|||
|
|
return step.StepId, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// getSortedStepIDs 返回按 stepIndex 排序的 stepId 列表
|
|||
|
|
func (d *DBState) getSortedStepIDs(ctx context.Context, recipeID int) ([]string, error) {
|
|||
|
|
steps, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID)).
|
|||
|
|
Order(recipestep.ByStepIndex()).
|
|||
|
|
All(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("get sorted step IDs for recipe %d: %w", recipeID, err)
|
|||
|
|
}
|
|||
|
|
ids := make([]string, len(steps))
|
|||
|
|
for i, s := range steps {
|
|||
|
|
ids[i] = s.StepId
|
|||
|
|
}
|
|||
|
|
return ids, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetStepIndexByStepID 根据 stepId 查找步骤的 stepIndex
|
|||
|
|
func (d *DBState) GetStepIndexByStepID(ctx context.Context, recipeID int, stepID string) (int, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return -1, fmt.Errorf("get step index by stepId %s: %w", stepID, err)
|
|||
|
|
}
|
|||
|
|
return step.StepIndex, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// resolveBranch 检查 nextStepBranches:遍历分支键,若在 ctxMap 中存在且 truthy,返回目标 stepId。
|
|||
|
|
// 返回 (stepId, true) 表示匹配成功,("", false) 表示无匹配。
|
|||
|
|
func (d *DBState) resolveBranch(ctx context.Context, recipeID int, currentStepID string, ctxMap map[string]any) (string, bool) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(currentStepID)).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil || step.NextStepBranches == nil {
|
|||
|
|
return "", false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for key, val := range step.NextStepBranches {
|
|||
|
|
if cv, ok := ctxMap[key]; ok && isTruthy(cv) {
|
|||
|
|
stepID, ok := val.(string)
|
|||
|
|
if !ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
return stepID, true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "", false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// isTruthy 判断 context 值为 truthy(bool true、非空字符串、非零数字)
|
|||
|
|
func isTruthy(v any) bool {
|
|||
|
|
switch tv := v.(type) {
|
|||
|
|
case bool:
|
|||
|
|
return tv
|
|||
|
|
case string:
|
|||
|
|
return tv != ""
|
|||
|
|
case int:
|
|||
|
|
return tv != 0
|
|||
|
|
case float64:
|
|||
|
|
return tv != 0
|
|||
|
|
default:
|
|||
|
|
return v != nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetRecipeMaxStepIndex 获取工艺路线的最大步骤索引
|
|||
|
|
func (d *DBState) GetRecipeMaxStepIndex(ctx context.Context, recipeID int) (int, error) {
|
|||
|
|
steps, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID)).
|
|||
|
|
All(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return 0, fmt.Errorf("get recipe %d steps: %w", recipeID, err)
|
|||
|
|
}
|
|||
|
|
maxIdx := 0
|
|||
|
|
for _, s := range steps {
|
|||
|
|
if s.StepIndex > maxIdx {
|
|||
|
|
maxIdx = s.StepIndex
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return maxIdx, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TempSlotCapacity 从 DB 查询暂存台槽位数,默认返回 8
|
|||
|
|
func (d *DBState) TempSlotCapacity(ctx context.Context) int {
|
|||
|
|
equip, err := d.client.Equipment.Query().
|
|||
|
|
Where(equipment.HasEquipmentTypeWith(equipmenttype.CodeEQ(constants.EquipmentTypeCode_TempStore))).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil || equip == nil {
|
|||
|
|
return 8
|
|||
|
|
}
|
|||
|
|
if equip.SlotCount > 0 {
|
|||
|
|
return equip.SlotCount
|
|||
|
|
}
|
|||
|
|
return 8
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// parsePositionRef 解析 "equipmentID:slotNo" 格式
|
|||
|
|
func parsePositionRef(ref string) (int, int) {
|
|||
|
|
equipStr, slotStr, ok := strings.Cut(ref, ":")
|
|||
|
|
if !ok {
|
|||
|
|
return 0, 0
|
|||
|
|
}
|
|||
|
|
var equipID, slotNo int
|
|||
|
|
fmt.Sscanf(equipStr, "%d", &equipID)
|
|||
|
|
fmt.Sscanf(slotStr, "%d", &slotNo)
|
|||
|
|
return equipID, slotNo
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RaiseAlarm 创建报警
|
|||
|
|
func (d *DBState) RaiseAlarm(ctx context.Context, alarmCode, message string, level constants.AlarmLevel, equipmentID, jobID int, source string) error {
|
|||
|
|
_, err := d.client.Alarm.Create().
|
|||
|
|
SetAlarmCode(alarmCode).
|
|||
|
|
SetAlarmMessage(message).
|
|||
|
|
SetLevel(level).
|
|||
|
|
SetNillableEquipmentId(&equipmentID).
|
|||
|
|
SetNillableJobId(&jobID).
|
|||
|
|
SetSource(source).
|
|||
|
|
Save(ctx)
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FindDoneJobOnMachine 查找设备上状态为 DONE 的槽位对应的 jobID
|
|||
|
|
func (d *DBState) FindDoneJobOnMachine(ctx context.Context, machineID int) (jobID int, slotNo int, found bool) {
|
|||
|
|
slot, err := d.client.EquipmentSlot.Query().
|
|||
|
|
Where(
|
|||
|
|
equipmentslot.EquipmentIdEQ(machineID),
|
|||
|
|
equipmentslot.StatusEQ(constants.SlotStatus_Done),
|
|||
|
|
).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil || slot == nil {
|
|||
|
|
return 0, 0, false
|
|||
|
|
}
|
|||
|
|
return slotJobID(slot), slot.SlotNo, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FindAllDoneJobsOnMachine 查找设备上所有 DONE 状态的槽位
|
|||
|
|
func (d *DBState) FindAllDoneJobsOnMachine(ctx context.Context, machineID int) []DoneSlotInfo {
|
|||
|
|
slots, _ := d.client.EquipmentSlot.Query().
|
|||
|
|
Where(
|
|||
|
|
equipmentslot.EquipmentIdEQ(machineID),
|
|||
|
|
equipmentslot.StatusEQ(constants.SlotStatus_Done),
|
|||
|
|
).
|
|||
|
|
All(ctx)
|
|||
|
|
var result []DoneSlotInfo
|
|||
|
|
for _, s := range slots {
|
|||
|
|
result = append(result, DoneSlotInfo{
|
|||
|
|
JobID: slotJobID(s),
|
|||
|
|
SlotNo: s.SlotNo,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// CreateManualAction 创建人工恢复动作
|
|||
|
|
func (d *DBState) CreateManualAction(ctx context.Context, actionType constants.ManualActionType, jobID, orderID int, description string) (*ent.ManualAction, error) {
|
|||
|
|
c := d.client.ManualAction.Create().
|
|||
|
|
SetActionType(actionType).
|
|||
|
|
SetJobId(jobID).
|
|||
|
|
SetContext(map[string]any{"description": description}).
|
|||
|
|
SetStatus(constants.ManualActionStatus_PENDING)
|
|||
|
|
if orderID > 0 {
|
|||
|
|
c = c.SetOrderId(orderID)
|
|||
|
|
}
|
|||
|
|
ma, err := c.Save(ctx)
|
|||
|
|
return ma, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetJobTempSlot 设置 job.temp_slot_no
|
|||
|
|
func (d *DBState) SetJobTempSlot(ctx context.Context, jobID, slotNo int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetNillableTempSlotNo(&slotNo).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set job %d temp slot %d: %w", jobID, slotNo, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ClearJobTempSlot 清除 job.temp_slot_no
|
|||
|
|
func (d *DBState) ClearJobTempSlot(ctx context.Context, jobID int) error {
|
|||
|
|
_, err := d.client.Job.UpdateOneID(jobID).
|
|||
|
|
ClearTempSlotNo().
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("clear job %d temp slot: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetCncStartedAt 记录 CNC 加工开始时间(仅首次写入,已有值则跳过)
|
|||
|
|
func (d *DBState) SetCncStartedAt(ctx context.Context, jobID int) error {
|
|||
|
|
n, err := d.client.Job.Update().
|
|||
|
|
Where(job.ID(jobID), job.CncStartedAtIsNil()).
|
|||
|
|
SetCncStartedAt(time.Now()).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set cnc started at for job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
if n == 0 {
|
|||
|
|
slog.Warn("cnc started at already set, skipping", "jobId", jobID)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetCncCompletedAt 记录 CNC 加工完成时间(仅首次写入,且要求 cncStartedAt 已设置)
|
|||
|
|
func (d *DBState) SetCncCompletedAt(ctx context.Context, jobID int) error {
|
|||
|
|
n, err := d.client.Job.Update().
|
|||
|
|
Where(job.ID(jobID), job.CncStartedAtNotNil()).
|
|||
|
|
SetCncCompletedAt(time.Now()).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("set cnc completed at for job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
if n == 0 {
|
|||
|
|
slog.Warn("cnc completed at skipped (already set or cncStartedAt missing)", "jobId", jobID)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// FixJobPosition 恢复工件位置到暂存台:释放 equipment_slot(如果占用)+ 设置位置/步骤/状态
|
|||
|
|
func (d *DBState) FixJobPosition(ctx context.Context, jobID int, tempSlotNo int, stepID string, positionType string) error {
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("fix position: get job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch positionType {
|
|||
|
|
case "ON_EQUIPMENT_DONE":
|
|||
|
|
equipID, slotNo := parsePositionRef(jb.PositionRefId)
|
|||
|
|
if equipID > 0 && slotNo > 0 {
|
|||
|
|
_, _ = d.client.EquipmentSlot.Update().
|
|||
|
|
Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)).
|
|||
|
|
SetStatus(constants.SlotStatus_Done).
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
_, err = d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_WaitingUnload).
|
|||
|
|
SetPositionType(constants.PositionType_OnEquipment).
|
|||
|
|
SetPositionRefId(jb.PositionRefId).
|
|||
|
|
SetNillableTempSlotNo(&tempSlotNo).
|
|||
|
|
SetCurrentStepId(stepID).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
case "ON_EQUIPMENT_PROCESSING":
|
|||
|
|
equipID, slotNo := parsePositionRef(jb.PositionRefId)
|
|||
|
|
if equipID > 0 && slotNo > 0 {
|
|||
|
|
_, _ = d.client.EquipmentSlot.Update().
|
|||
|
|
Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)).
|
|||
|
|
SetStatus(constants.SlotStatus_Occupied).
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
_, err = d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_Processing).
|
|||
|
|
SetPositionType(constants.PositionType_OnEquipment).
|
|||
|
|
SetPositionRefId(jb.PositionRefId).
|
|||
|
|
SetNillableTempSlotNo(&tempSlotNo).
|
|||
|
|
SetCurrentStepId(stepID).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
default:
|
|||
|
|
// ON_BUFFER: release equipment slot first if needed
|
|||
|
|
if jb.PositionType == constants.PositionType_OnEquipment {
|
|||
|
|
equipID, slotNo := parsePositionRef(jb.PositionRefId)
|
|||
|
|
if equipID > 0 && slotNo > 0 {
|
|||
|
|
// recovery: force release slot regardless of current status
|
|||
|
|
_, _ = d.client.EquipmentSlot.Update().
|
|||
|
|
Where(
|
|||
|
|
equipmentslot.EquipmentIdEQ(equipID),
|
|||
|
|
equipmentslot.SlotNoEQ(slotNo),
|
|||
|
|
).
|
|||
|
|
SetStatus(constants.SlotStatus_Empty).
|
|||
|
|
ClearCurrentJobId().
|
|||
|
|
ClearOccupiedAt().
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
_, err = d.client.Job.UpdateOneID(jobID).
|
|||
|
|
SetStatus(constants.JobStatus_OnBuffer).
|
|||
|
|
SetPositionType(constants.PositionType_OnBuffer).
|
|||
|
|
SetPositionRefId(fmt.Sprintf("%d", tempSlotNo)).
|
|||
|
|
SetNillableTempSlotNo(&tempSlotNo).
|
|||
|
|
SetCurrentStepId(stepID).
|
|||
|
|
AddVersion(1).
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("fix position: update job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ScrapRecoveryJob 报废现场找不到的恢复工件:释放 equipment_slot(若占用)+ 终态 Scrapped + 工单 failNum +1。
|
|||
|
|
func (d *DBState) ScrapRecoveryJob(ctx context.Context, jobID int) error {
|
|||
|
|
jb, err := d.client.Job.Get(ctx, jobID)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("scrap recovery: get job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
if jb.Status == constants.JobStatus_Scrapped || jb.Status == constants.JobStatus_Completed {
|
|||
|
|
return fmt.Errorf("scrap recovery: job %d already terminal: %s", jobID, jb.Status)
|
|||
|
|
}
|
|||
|
|
// 释放设备槽位(若曾在设备上)
|
|||
|
|
if jb.PositionType == constants.PositionType_OnEquipment {
|
|||
|
|
if equipID, slotNo := parsePositionRef(jb.PositionRefId); equipID > 0 && slotNo > 0 {
|
|||
|
|
_, _ = d.client.EquipmentSlot.Update().
|
|||
|
|
Where(equipmentslot.EquipmentIdEQ(equipID), equipmentslot.SlotNoEQ(slotNo)).
|
|||
|
|
SetStatus(constants.SlotStatus_Empty).
|
|||
|
|
ClearCurrentJobId().
|
|||
|
|
ClearOccupiedAt().
|
|||
|
|
Save(ctx)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if err := d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Scrapped); err != nil {
|
|||
|
|
return fmt.Errorf("scrap recovery: finish job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (d *DBState) IsCNCEquipment(ctx context.Context, equipmentID int) bool {
|
|||
|
|
equip, err := d.client.Equipment.Query().
|
|||
|
|
Where(equipment.ID(equipmentID)).
|
|||
|
|
WithEquipmentType().
|
|||
|
|
Only(ctx)
|
|||
|
|
if err != nil || equip == nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
et, err := equip.Edges.EquipmentTypeOrErr()
|
|||
|
|
if err != nil || et == nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
return et.Code == constants.EquipmentTypeCode_CNC
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetQCResult 写入检测结果到 job 的独立字段(按设备类型区分)。
|
|||
|
|
// INSPECTION -> inspection_result,SAMPLING -> sampling_result;其余设备忽略(防御)。
|
|||
|
|
// 与 context.inspectionPass 双写并存,本字段为合格率统计的一等数据源。
|
|||
|
|
func (d *DBState) SetQCResult(ctx context.Context, jobID int, machineType string, pass bool) error {
|
|||
|
|
result := constants.QCResult_Pass
|
|||
|
|
if !pass {
|
|||
|
|
result = constants.QCResult_Fail
|
|||
|
|
}
|
|||
|
|
update := d.client.Job.UpdateOneID(jobID)
|
|||
|
|
switch constants.EquipmentTypeCode(machineType) {
|
|||
|
|
case constants.EquipmentTypeCode_Inspection:
|
|||
|
|
update = update.SetInspectionResult(result)
|
|||
|
|
case constants.EquipmentTypeCode_Sampling:
|
|||
|
|
update = update.SetSamplingResult(result)
|
|||
|
|
default:
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if _, err := update.Save(ctx); err != nil {
|
|||
|
|
return fmt.Errorf("set qc result: job %d: %w", jobID, err)
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetFirstStepID 返回配方第一个步骤的 stepId
|
|||
|
|
func (d *DBState) GetFirstStepID(ctx context.Context, recipeID int) (string, error) {
|
|||
|
|
step, err := d.client.RecipeStep.Query().
|
|||
|
|
Where(recipestep.RecipeIdEQ(recipeID)).
|
|||
|
|
Order(recipestep.ByStepIndex()).
|
|||
|
|
First(ctx)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("get first step for recipe %d: %w", recipeID, err)
|
|||
|
|
}
|
|||
|
|
return step.StepId, nil
|
|||
|
|
}
|