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