fix: rebuild MES as compilable go-zero+ent backend (renamed bj_power_mes), restore 3 workstation projects from pristine original, align naming; all Go projects go build clean

This commit is contained in:
SunYF
2026-08-27 10:56:41 +08:00
parent 380715f8a9
commit 371b494c54
523 changed files with 67923 additions and 24758 deletions
@@ -0,0 +1,31 @@
package manual_action
import (
"context"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type CompleteRecoveryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCompleteRecoveryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CompleteRecoveryLogic {
return &CompleteRecoveryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CompleteRecoveryLogic) CompleteRecovery() (resp *types.CompleteRecoveryReply, err error) {
if err := l.svcCtx.EventLoop.CompleteRecovery(l.ctx); err != nil {
return nil, err
}
return &types.CompleteRecoveryReply{Success: true}, nil
}
@@ -0,0 +1,124 @@
package manual_action
import (
"context"
"fmt"
"time"
"bj_power_mes/common/errorx"
"bj_power_mes/constants"
"bj_power_mes/ent"
"bj_power_mes/internal/eventbus"
"bj_power_mes/internal/processor/eventloop"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ConfirmRecoveryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 确认 L3 恢复
func NewConfirmRecoveryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ConfirmRecoveryLogic {
return &ConfirmRecoveryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ConfirmRecoveryLogic) ConfirmRecovery(req *types.ConfirmRecoveryReq) (resp *types.ConfirmRecoveryReply, err error) {
a, err := l.svcCtx.EntClient.ManualAction.Get(l.ctx, req.Id)
if ent.IsNotFound(err) {
return nil, errorx.NewDirectError("人工待办不存在")
} else if err != nil {
return nil, err
}
if a.ActionType != constants.ManualActionType_RecoveryRequired {
return nil, errorx.NewDirectError("该待办不是恢复确认")
}
if a.Status != constants.ManualActionStatus_PENDING {
return nil, errorx.NewDirectError("该待办已处理或已过期")
}
orderID := 0
if a.OrderId != nil {
orderID = *a.OrderId
}
if orderID <= 0 {
return nil, errorx.NewDirectError("恢复待办缺少工单")
}
_, err = l.svcCtx.EntClient.WorkOrder.Get(l.ctx, orderID)
if ent.IsNotFound(err) {
return nil, l.expireMissingOrderAction(req.Id, a.ActionType)
} else if err != nil {
return nil, fmt.Errorf("check work order %d: %w", orderID, err)
}
// 根据决策执行操作
switch req.Decision {
case string(constants.RecoveryDecision_RESUME):
result, err := l.svcCtx.EventLoop.SendSync(eventloop.EventLoopMessage{
Type: eventloop.CmdRestoreOrder,
Payload: map[string]any{"orderId": orderID},
}, 10*time.Second)
if err != nil {
return nil, err
}
if !result.Success {
return nil, fmt.Errorf("restore order %d failed: %s", orderID, result.Error)
}
case string(constants.RecoveryDecision_SUSPEND):
// 保持当前状态,仅更新 manual_action
case string(constants.RecoveryDecision_SCRAP):
// 标记工件报废由后续任务处理
case string(constants.RecoveryDecision_WAIT_UNLOAD):
// 等待卸载后恢复
default:
return nil, errorx.NewDirectError("不支持的恢复决策")
}
now := time.Now()
_, err = l.svcCtx.EntClient.ManualAction.UpdateOneID(req.Id).
SetStatus(constants.ManualActionStatus_RESOLVED).
SetResolvedAction(req.Decision).
SetResolvedAt(now).
Save(l.ctx)
if err != nil {
return nil, err
}
l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewManualActionResolvedEvent(
req.Id, string(a.ActionType), req.Decision,
))
return &types.ConfirmRecoveryReply{
OrderId: orderID,
Decision: req.Decision,
NewStatus: string(constants.WorkOrderStatus_InProgress),
}, nil
}
func (l *ConfirmRecoveryLogic) expireMissingOrderAction(actionID int, actionType constants.ManualActionType) error {
now := time.Now()
_, err := l.svcCtx.EntClient.ManualAction.UpdateOneID(actionID).
SetStatus(constants.ManualActionStatus_EXPIRED).
SetResolvedAction("ORDER_NOT_FOUND").
SetResolvedAt(now).
Save(l.ctx)
if err != nil {
return fmt.Errorf("expire missing-order manual action %d: %w", actionID, err)
}
l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewManualActionResolvedEvent(
actionID, string(actionType), "ORDER_NOT_FOUND",
))
return errorx.NewDirectError("关联工单不存在,待办已过期")
}
@@ -0,0 +1,93 @@
package manual_action
import (
"context"
"fmt"
"bj_power_mes/constants"
entjob "bj_power_mes/ent/job"
"bj_power_mes/internal/processor/eventloop"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"log/slog"
)
type FixPositionLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewFixPositionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FixPositionLogic {
return &FixPositionLogic{ctx: ctx, svcCtx: svcCtx}
}
func (l *FixPositionLogic) FixPosition(req *types.FixPositionReq) (*types.FixPositionReply, error) {
// SCRAP:现场找不到工件,直接报废(清位置 + 工单 failNum +1),不走暂存台校验
if req.PositionType == "SCRAP" {
return l.scrapJob(req.JobId)
}
// 读取 job 自身的位置/工步作为默认值
job, err := l.svcCtx.EntClient.Job.Get(l.ctx, req.JobId)
if err != nil {
return nil, fmt.Errorf("get job: %w", err)
}
tempSlotNo := req.TempSlotNo
if tempSlotNo == 0 {
if job.TempSlotNo == nil || *job.TempSlotNo == 0 {
return nil, fmt.Errorf("job %d has no temp slot, must specify tempSlotNo", req.JobId)
}
tempSlotNo = *job.TempSlotNo
}
stepId := req.StepId
if stepId == "" {
stepId = job.CurrentStepId
}
// 校验:暂存台槽位范围
dbState := eventloop.NewDBState(l.svcCtx.EntClient)
capacity := dbState.TempSlotCapacity(l.ctx)
if tempSlotNo < 1 || tempSlotNo > capacity {
return nil, fmt.Errorf("temp slot %d out of range [1,%d]", tempSlotNo, capacity)
}
// 校验:暂存台槽位未被占用
occupied, err := l.svcCtx.EntClient.Job.Query().
Where(
entjob.TempSlotNoEQ(tempSlotNo),
entjob.StatusNEQ(constants.JobStatus_Completed),
entjob.StatusNEQ(constants.JobStatus_Scrapped),
entjob.IDNEQ(req.JobId),
).
Count(l.ctx)
if err != nil {
return nil, fmt.Errorf("check temp slot: %w", err)
}
if occupied > 0 {
return nil, fmt.Errorf("temp slot %d is occupied", tempSlotNo)
}
// 执行修复:释放 equipment_slot + 更新 job 位置/步骤
if err := dbState.FixJobPosition(l.ctx, req.JobId, tempSlotNo, stepId, req.PositionType); err != nil {
return nil, fmt.Errorf("fix position: %w", err)
}
// release MachineActor in-memory slot (only when moving OFF equipment)
if req.PositionType != "ON_EQUIPMENT_DONE" && req.PositionType != "ON_EQUIPMENT_PROCESSING" {
l.svcCtx.EventLoop.SyncMachineSlotStatus(req.JobId, req.PositionType)
}
slog.Info("fix position: job recovered", "jobId", req.JobId, "tempSlotNo", tempSlotNo, "stepId", stepId, "positionType", req.PositionType)
return &types.FixPositionReply{JobId: req.JobId}, nil
}
// scrapJob 报废现场找不到的工件:释放设备槽位 + 终态 Scrapped + 工单 failNum +1 + 同步 Actor 内存。
func (l *FixPositionLogic) scrapJob(jobID int) (*types.FixPositionReply, error) {
dbState := eventloop.NewDBState(l.svcCtx.EntClient)
if err := dbState.ScrapRecoveryJob(l.ctx, jobID); err != nil {
return nil, fmt.Errorf("scrap job %d: %w", jobID, err)
}
// 同步 MachineActor 内存槽位(SCRAP 走 default 分支 ReleaseSlot
l.svcCtx.EventLoop.SyncMachineSlotStatus(jobID, "SCRAP")
slog.Info("fix position: job scrapped (not found on site)", "jobId", jobID)
return &types.FixPositionReply{JobId: jobID}, nil
}
@@ -0,0 +1,123 @@
package manual_action
import (
"context"
"fmt"
"time"
"bj_power_mes/constants"
"bj_power_mes/ent"
"bj_power_mes/ent/job"
"bj_power_mes/ent/manualaction"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryManualActionsLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 查询人工交互列表
func NewQueryManualActionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryManualActionsLogic {
return &QueryManualActionsLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryManualActionsLogic) QueryManualActions(req *types.QueryManualActionsReq) (resp *types.QueryManualActionsReply, err error) {
q := l.svcCtx.EntClient.ManualAction.Query()
if req.ActionType != "" {
q.Where(manualaction.ActionTypeEQ(constants.ManualActionType(req.ActionType)))
}
if req.Resolved != nil {
// Resolved=true 表示已完成(status=RESOLVED)
if *req.Resolved {
q.Where(manualaction.StatusEQ("RESOLVED"))
} else {
q.Where(manualaction.StatusEQ("PENDING"))
}
}
if req.StartTime != nil {
q.Where(manualaction.CreatedAtGTE(time.UnixMilli(*req.StartTime)))
}
if req.EndTime != nil {
q.Where(manualaction.CreatedAtLTE(time.UnixMilli(*req.EndTime)))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, fmt.Errorf("count manual actions: %w", err)
}
q.Order(ent.Desc(manualaction.FieldCreatedAt))
offset := (req.Page - 1) * req.Limit
records, err := q.Offset(offset).Limit(req.Limit).All(l.ctx)
if err != nil {
return nil, fmt.Errorf("query manual actions: %w", err)
}
// 批量查 workpieceNojobId → workpieceNo
jobIDs := make([]int, 0)
for _, a := range records {
if a.JobId != nil && *a.JobId > 0 {
jobIDs = append(jobIDs, *a.JobId)
}
}
wpNoMap := make(map[int]string)
if len(jobIDs) > 0 {
jobs, jerr := l.svcCtx.EntClient.Job.Query().
Where(job.IDIn(jobIDs...)).
All(l.ctx)
if jerr == nil {
for _, j := range jobs {
wpNoMap[j.ID] = j.WorkpieceNo
}
}
}
items := make([]types.ManualAction, len(records))
for i, a := range records {
item := types.ManualAction{
Id: a.ID,
ActionType: string(a.ActionType),
Status: string(a.Status),
ResolvedAction: a.ResolvedAction,
ResolvedBy: a.ResolvedBy,
CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if a.OrderId != nil {
item.OrderId = *a.OrderId
}
if a.JobId != nil {
item.JobId = *a.JobId
item.WorkpieceNo = wpNoMap[*a.JobId]
}
if a.EquipmentId != nil {
item.EquipmentId = *a.EquipmentId
}
if a.Context != nil {
item.Context = a.Context
}
if a.ResolvedAt != nil {
item.ResolvedAt = a.ResolvedAt.Format("2006-01-02T15:04:05Z07:00")
}
items[i] = item
}
return &types.QueryManualActionsReply{
PageReply: types.PageReply{
Page: req.Page,
Limit: req.Limit,
Total: total,
},
Data: items,
}, nil
}
@@ -0,0 +1,166 @@
package manual_action
import (
"context"
"fmt"
"log/slog"
"bj_power_mes/constants"
"bj_power_mes/ent"
"bj_power_mes/ent/job"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type QueryRecoveryJobsLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewQueryRecoveryJobsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryRecoveryJobsLogic {
return &QueryRecoveryJobsLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryRecoveryJobsLogic) QueryRecoveryJobs() (resp *types.QueryRecoveryJobsReply, err error) {
// 查询 IN_PROGRESS 工单下,仍在设备中(ON_EQUIPMENT)的活跃工件,等待人工确认位置
jobs, err := l.svcCtx.EntClient.Job.Query().
WithWorkOrder().
Where(
job.StatusNotIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped),
job.PositionTypeIn(constants.PositionType_OnEquipment),
job.HasWorkOrderWith(workorder.StatusEQ(constants.WorkOrderStatus_InProgress)),
).
All(l.ctx)
if err != nil {
return nil, fmt.Errorf("query recovery jobs: %w", err)
}
data := make([]types.RecoveryJobInfo, 0, len(jobs))
for _, j := range jobs {
data = append(data, buildRecoveryJobInfo(j, "on_equipment")) // 在设备上需要确认有没有完成
}
// 比对 PLC TempStates 与 DB tempSlotNo,标记暂存台不一致项
data = l.appendTempMismatches(data)
return &types.QueryRecoveryJobsReply{Data: data}, nil
}
// appendTempMismatches 读取 PLC 暂存台槽位状态,与 DB 中活跃工件的 tempSlotNo 比对,
// 标记 db_onlyDB 有记录但现场无占用)/ plc_only(现场有占用但 DB 无记录)。
// robot 不可用或读取失败时降级为原列表,不阻塞恢复流程。
func (l *QueryRecoveryJobsLogic) appendTempMismatches(data []types.RecoveryJobInfo) []types.RecoveryJobInfo {
robotCtrl, ok := l.svcCtx.RobotManager.GetRobot()
if !ok {
slog.Info("recovery: robot unavailable, skip TempStates mismatch check")
return data
}
plcStates, err := robotCtrl.TempStates()
if err != nil || len(plcStates) == 0 {
slog.Warn("recovery: read TempStates failed, skip mismatch check", "error", err, "len", len(plcStates))
return data
}
// 查 DB 记录在暂存台(ON_BUFFER)且有 tempSlotNo 的活跃工件(工单 IN_PROGRESS
tempJobs, err := l.svcCtx.EntClient.Job.Query().
WithWorkOrder().
Where(
job.StatusNotIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped),
job.PositionTypeEQ(constants.PositionType_OnBuffer),
job.TempSlotNoNotNil(),
job.HasWorkOrderWith(workorder.StatusEQ(constants.WorkOrderStatus_InProgress)),
).
All(l.ctx)
if err != nil {
slog.Warn("recovery: query temp jobs failed, skip mismatch check", "error", err)
return data
}
// 已存在于列表中的 jobID → 索引,用于就地打标
existing := make(map[int]int, len(data))
for i, d := range data {
existing[d.JobID] = i
}
// slotNo → job(越界槽位跳过,RecoverOnStartup 已为越界创建 manual_action
slotToJob := make(map[int]*ent.Job, len(tempJobs))
for _, tj := range tempJobs {
if tj.TempSlotNo == nil {
continue
}
slot := *tj.TempSlotNo
if slot < 1 || slot > len(plcStates) {
continue
}
slotToJob[slot] = tj
}
// db_onlyDB 有 tempSlotNo 但 PLC 该槽位无占用
for slot, tj := range slotToJob {
if !plcStates[slot-1] {
if idx, ok := existing[tj.ID]; ok {
data[idx].Mismatch = "db_only"
} else {
item := buildRecoveryJobInfo(tj, "db_only")
data = append(data, item)
}
}
}
// plc_onlyPLC 有占用但 DB 无对应 tempSlotNo 记录(现场"幽灵"工件)
for i, occupied := range plcStates {
slot := i + 1
if !occupied {
continue
}
if _, ok := slotToJob[slot]; ok {
continue
}
data = append(data, types.RecoveryJobInfo{
JobID: 0,
TempSlotNo: slot,
Status: string(constants.JobStatus_OnBuffer),
Mismatch: "plc_only",
})
}
return data
}
func buildRecoveryJobInfo(j *ent.Job, mismatch string) types.RecoveryJobInfo {
workOrderNo := ""
if j.Edges.WorkOrder != nil {
workOrderNo = j.Edges.WorkOrder.WorkOrderNo
}
return types.RecoveryJobInfo{
JobID: j.ID,
WorkpieceNo: j.WorkpieceNo,
WorkOrderID: j.WorkOrderId,
WorkOrderNo: workOrderNo,
Status: string(j.Status),
PositionType: string(j.PositionType),
PositionRefID: j.PositionRefId,
TempSlotNo: derefInt(j.TempSlotNo),
CurrentStepID: j.CurrentStepId,
CurrentStepName: "",
DockNo: derefInt(j.DockNo),
DockSlotNo: derefInt(j.DockSlotNo),
ProductTypeID: j.ProductTypeId,
Mismatch: mismatch,
}
}
func derefInt(v *int) int {
if v == nil {
return 0
}
return *v
}
@@ -0,0 +1,66 @@
package manual_action
import (
"context"
"time"
"bj_power_mes/constants"
"bj_power_mes/ent"
"bj_power_mes/internal/eventbus"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ResolveScanFailedLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 处理扫码失败
func NewResolveScanFailedLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResolveScanFailedLogic {
return &ResolveScanFailedLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResolveScanFailedLogic) ResolveScanFailed(req *types.ResolveScanFailedReq) (resp *types.ResolveScanFailedReply, err error) {
a, err := l.svcCtx.EntClient.ManualAction.Get(l.ctx, req.Id)
if ent.IsNotFound(err) {
return nil, err
} else if err != nil {
return nil, err
}
if a.ActionType != constants.ManualActionType_ScanFailed {
return nil, err
}
if a.Status != constants.ManualActionStatus_PENDING {
return nil, err
}
now := time.Now()
_, err = l.svcCtx.EntClient.ManualAction.UpdateOneID(req.Id).
SetStatus(constants.ManualActionStatus_RESOLVED).
SetResolvedAction(req.Action).
SetResolvedAt(now).
Save(l.ctx)
if err != nil {
return nil, err
}
l.svcCtx.EventBus.Publish(l.ctx, eventbus.NewManualActionResolvedEvent(
req.Id, string(a.ActionType), req.Action,
))
return &types.ResolveScanFailedReply{
Id: req.Id,
Status: string(constants.ManualActionStatus_RESOLVED),
ResolvedAction: req.Action,
ResolvedAt: now.UnixMilli(),
}, nil
}