Files
bj_power/bj_power_mes/internal/processor/eventloop/dbstate.go
T

782 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 值为 truthybool 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_resultSAMPLING -> 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
}