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

782 lines
26 KiB
Go
Raw Normal View History

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
}