Files
bj_power/bj_power_mes/internal/logic/workpiece.go
T
SunYF d609ff8a9e refactor: 重构工艺工序相关命名与数据模型
1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
2026-09-22 11:32:29 +08:00

601 lines
22 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 logic
import (
"context"
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/associationtrace"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/stepdata"
"bj_power_mes/ent/torquerecord"
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpiece"
"bj_power_mes/ent/workpieceprocess"
"bj_power_mes/internal/wmsclient"
)
func itoa(n int) string { return strconv.Itoa(n) }
// evalLogic 依据考核逻辑判定
func evalLogic(c *ent.StepCriterion, v float64) bool {
switch c.Logic {
case "GE":
return v >= c.Target
case "LE":
return v <= c.Target
case "GT":
return v > c.Target
case "LT":
return v < c.Target
case "RANGE":
l, r := true, true
if c.Min != nil {
l = v >= *c.Min
}
if c.Max != nil {
r = v <= *c.Max
}
return l && r
case "EQUAL":
return v == c.Target
default:
return true
}
}
type StepDataReq struct {
StepId int `json:"stepId"`
Name string `json:"name"`
Value float64 `json:"value"`
Text string `json:"text"`
// Checked:该步骤是否已完成「检测确认」。needCheck=true 的步骤必须为 true,服务端强校验,防止绕过前端。
Checked bool `json:"checked"`
}
type OnlineReq struct {
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
WorkOrderId int `json:"workOrderId"`
}
// OnlineWorkpiece 工件进线登记
// 执行路线 = 工单工艺组合(唯一路线源头),工件只需按组合逐工位报工推进
func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator string) error {
if req.Sn == "" {
return errors.New("sn 不能为空")
}
if req.WorkOrderId == 0 {
if wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx); err == nil {
req.WorkOrderId = wo.ID
}
}
err := s.ctx.EntClient.Workpiece.Create().
SetSn(req.Sn).SetOrderNo(req.OrderNo).SetWorkOrderId(req.WorkOrderId).
SetStatus("ONLINE").SetOnlineAt(time.Now()).Exec(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "workpiece.online", req.OrderNo, operator, "workpiece", req.Sn, "工件进线登记", nil)
s.notifyDashboard()
return nil
}
type ReportProcessReq struct {
Sn string `json:"sn"`
FlowId int `json:"flowId"` // 报工工艺(缺省回退工位当前绑定的工艺)
StationNo int `json:"stationNo"`
Steps []StepDataReq `json:"steps"`
Binds []BindItemReq `json:"binds"` // 装机绑定(本工序装配物料:结构件批次/电气件SN),随报工原子提交
// 作业时长采集(方案 M / P0-3):工位终端在「上料到本工位」时采 startedAt、点「报工」时采 endedAt
// 两端时间戳同源于终端时钟(ISO8601),离线重传也不失真;缺失则回退服务端当前时刻(时长=0)。
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
}
// parseClientTime 解析工位终端上报的 ISO8601 时间戳(兼容带/不带毫秒、Z/时区偏移)。
func parseClientTime(s string) (time.Time, error) {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}, errors.New("empty time")
}
layouts := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02 15:04:05"}
for _, l := range layouts {
if t, err := time.Parse(l, s); err == nil {
return t, nil
}
}
return time.Time{}, errors.New("unrecognized time format: " + s)
}
// ReportProcess 工位报工:先应用装机绑定并做强校验(齐套才放行),再写报工实绩 + 步骤考核。
// 绑料是"落到工艺"的强约束:工艺物料清单(flow_material)中要求装配的物料必须绑齐,
// 电气件按单台用量逐颗SN、结构件至少一个批次号;缺料直接拒绝报工并返回缺料清单。
// 顺序校验(工单工艺组合为唯一路线源头):报工工位必须在组合中、工艺匹配,且不可倒序(可跳过未选工位)。
func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, operator string) error {
if req.Sn == "" || req.StationNo <= 0 {
return errors.New("sn 与 stationNo 必填")
}
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
if err != nil {
return errors.New("工件未进线登记")
}
// 严格校验(业务口径已确认):仅“在制”工件可报工;DONE/REPAIR/SCRAPPED 一律拒绝。
if wp.Status != "ONLINE" && wp.Status != "PROCESSING" {
return errors.New("工件当前状态为「" + wp.Status + "」,非在制,不允许报工")
}
// 工艺解析:优先取报工传入 flowId;缺省回退工位当前绑定的工艺
flowId := req.FlowId
if flowId <= 0 {
if st, e := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx); e == nil {
flowId = st.FlowId
}
}
if flowId <= 0 {
return errors.New("无法确定报工工艺:请传 flowId 或确认工位已绑定工艺")
}
// 组合校验:报工条目必须在工单工艺组合中且工艺匹配;不可倒序(工位号即顺序号)
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
return errors.New("工件所属工单不存在")
}
items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo)
matched := false
for _, it := range items {
if it.StationNo == req.StationNo {
matched = true
if it.FlowId != flowId {
return fmt.Errorf("工位%d当前工艺与工单工艺组合不匹配", req.StationNo)
}
break
}
}
if !matched {
return fmt.Errorf("工位%d不在该工单的工艺组合中,不可报工", req.StationNo)
}
if req.StationNo <= wp.CurrentStationNo {
return errors.New("不可倒序报工:工件已报工到工位" + itoa(wp.CurrentStationNo) +
",不能在工位" + itoa(req.StationNo) + "重复/回退报工")
}
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 U3):
// 任一步失败全部回滚,杜绝"实绩已建但工件状态没推进/结果没落"的半成品脏数据;
// 校验(齐套/needCheck)也走同一 tx client,才能读到本事务内刚写入的绑定。
tx, err := s.ctx.EntClient.Tx(ctx)
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
client := tx.Client()
// 阶段1:装机绑定(随报工提交)→ 落库后强校验齐套
stationNo := ""
if req.StationNo > 0 {
stationNo = itoa(req.StationNo)
}
if _, err := s.applyBinds(ctx, client, req.Sn, wp.OrderNo, flowId, stationNo, operator, req.Binds); err != nil {
return err
}
if miss, err := s.validateStationBinds(ctx, client, req.Sn, flowId); err != nil {
return err
} else if miss != "" {
return errors.New(miss)
}
// 阶段2:检测确认(needCheck)强校验——服务端为准,前端勾选只是辅助提示。
// 本工位绑定工艺流程中标记“需要检测确认”的步骤,报工数据里必须逐条带 checked=true,否则拒绝报工。
if err := s.validateNeedCheck(ctx, client, req); err != nil {
return err
}
now := time.Now()
// 作业时长(方案 M / P0-3):startedAt=进工位(上料)时刻、endedAt=报工时刻,均由工位终端采集;
// 缺失或非法则回退到服务端当前时刻(durationSec=0),时钟回拨导致负数则时长归零。
startedAt, endedAt := now, now
if t, e := parseClientTime(req.StartedAt); e == nil {
startedAt = t
}
if t, e := parseClientTime(req.EndedAt); e == nil {
endedAt = t
}
durationSec := int(endedAt.Sub(startedAt).Seconds())
if durationSec < 0 {
durationSec = 0
endedAt = startedAt
}
proc, err := client.WorkpieceProcess.Create().
SetSn(req.Sn).SetFlowId(flowId).
SetStationNo(itoa(req.StationNo)).SetOrderNo(wp.OrderNo).
SetProcessName(s.flowNameById(ctx, flowId)).
SetStatus("DONE").SetOperator(operator).SetResult("OK").
SetStartedAt(startedAt).SetEndedAt(endedAt).SetDurationSec(durationSec).Save(ctx)
if err != nil {
return err
}
allOK := true
for _, st := range req.Steps {
ok, e := s.writeStepData(ctx, client, req.Sn, proc.ID, st, operator)
if e != nil {
return e
}
if !ok {
allOK = false
}
}
if bad, e := client.StepData.Query().
Where(stepdata.ProcessId(proc.ID), stepdata.IsOK(false)).Exist(ctx); e != nil {
return e
} else if bad {
allOK = false
}
result := "OK"
if !allOK {
result = "NG"
}
if _, err := client.WorkpieceProcess.UpdateOneID(proc.ID).SetResult(result).Save(ctx); err != nil {
return err
}
if _, err := client.Workpiece.UpdateOneID(wp.ID).
SetCurrentStationNo(req.StationNo).SetStatus("PROCESSING").Save(ctx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
committed = true
s.ctx.EventLog.Write(ctx, "workpiece.process.report", wp.OrderNo, operator, "workpiece", req.Sn,
"工位报工", map[string]any{"flowId": flowId, "stationNo": req.StationNo, "result": result})
s.notifyDashboard()
return nil
}
// validateNeedCheck 检测确认(needCheck)服务端强校验。
// 规则:本工位绑定的工艺流程中,凡标记 needCheck=true 的工艺步骤,
// 报工请求里必须存在对应 stepId 且 checked=true 的步骤数据;缺失则拒绝报工。
// 工位未绑定流程、或流程内没有 needCheck 步骤时不拦截(与现场配置保持一致)。
func (s *Service) validateNeedCheck(ctx context.Context, client *ent.Client, req ReportProcessReq) error {
if req.StationNo <= 0 {
return nil
}
st, err := client.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
if err != nil || st.FlowId <= 0 {
return nil
}
steps, err := client.ProcessStep.Query().
Where(processstep.FlowId(st.FlowId), processstep.NeedCheck(true)).All(ctx)
if err != nil || len(steps) == 0 {
return nil
}
checked := map[int]bool{}
for _, sd := range req.Steps {
if sd.Checked {
checked[sd.StepId] = true
}
}
missing := make([]string, 0, len(steps))
for _, ps := range steps {
if !checked[ps.ID] {
name := ps.Name
if name == "" {
name = "步骤" + itoa(ps.ID)
}
missing = append(missing, name)
}
}
if len(missing) > 0 {
return errors.New("以下步骤需先完成检测确认后方可报工:" + strings.Join(missing, "、"))
}
return nil
}
// writeStepData 写一条步骤考核数据,按标准自动判定是否合格
func (s *Service) writeStepData(ctx context.Context, client *ent.Client, sn string, processId int, st StepDataReq, operator string) (bool, error) {
ok := true
crits, err := client.StepCriterion.Query().Where(stepcriterion.StepId(st.StepId)).All(ctx)
if err != nil {
return false, err
}
for _, c := range crits {
if !evalLogic(c, st.Value) {
ok = false
}
}
var cname, clogic string
cid := 0
if len(crits) > 0 {
cname = crits[0].Name
clogic = crits[0].Logic
cid = crits[0].ID
}
_, err = client.StepData.Create().
SetSn(sn).SetProcessId(processId).SetStepId(st.StepId).
SetStepName(st.Name).SetCriterionId(cid).SetCriterionName(cname).
SetCriterionLogic(clogic).SetValue(st.Value).SetValueText(st.Text).
SetIsOK(ok).SetOperator(operator).Save(ctx)
return ok, err
}
// DoneReq 完工 + 成品关联追溯
type DoneReq struct {
Sn string `json:"sn"`
BatchItems []string `json:"batchItems"` // 结构件批次号
SerialItems []string `json:"serialItems"` // 电气件SN
}
// DoneWorkpiece 完工:先兜底校验全部工序装配物料齐套(工艺流程强校验),再登记成品并生成关联追溯
func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator string) error {
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
if err != nil {
return errors.New("工件不存在")
}
// 兜底强校验:工单工艺组合中每个工艺的装配物料都须绑齐(工艺未配置物料清单时自动放行)
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
return errors.New("工件所属工单不存在")
}
if miss, err := s.validateAllFlows(ctx, s.ctx.EntClient, req.Sn, wo); err != nil {
return err
} else if miss != "" {
return errors.New("完工前必须绑齐全部装配物料:" + miss)
}
now := time.Now()
_, _ = s.ctx.EntClient.Workpiece.UpdateOneID(wp.ID).
SetStatus("DONE").SetDoneAt(now).Save(ctx)
existID, _ := s.ctx.EntClient.AssociationTrace.Query().
Where(associationtrace.FinishedSn(req.Sn)).FirstID(ctx)
if existID > 0 {
_, _ = s.ctx.EntClient.AssociationTrace.UpdateOneID(existID).
SetBatchItems(req.BatchItems).SetSerialItems(req.SerialItems).SetOperator(operator).Save(ctx)
} else {
routeStr := ""
if items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, wo); len(items) > 0 {
routeStr = RouteStationsStrOfWO(items)
}
_, err = s.ctx.EntClient.AssociationTrace.Create().
SetFinishedSn(req.Sn).SetOrderNo(wp.OrderNo).SetProcessCode(routeStr).
SetBatchItems(req.BatchItems).SetSerialItems(req.SerialItems).
SetOperator(operator).Save(ctx)
if err != nil {
return err
}
}
s.bumpWorkOrderProgress(ctx, wp.OrderNo, req.Sn)
s.bumpDailyPlanCompleted(ctx, wp.OrderNo, now)
s.ctx.EventLog.Write(ctx, "workpiece.done", wp.OrderNo, operator, "workpiece", req.Sn, "完工+关联追溯", map[string]any{"batchItems": req.BatchItems, "serialItems": req.SerialItems})
// 成品完工自动回流 WMS(统一库存主表 category=3):
// 成品 SN = 工件 SN(进线登记即系统发号,全程唯一,保证对号可追溯)。
// WMS 不可达/校验失败时降级:记 eventlog 不阻塞完工,与备料 WMS 校验降级策略同模式。
s.retainFinishedToWms(ctx, wp, operator)
// 反冲扣账:物料在当道工序被消耗,完工即按整份 BOM 反冲扣 WMS 库存。
// 失败仅记 eventlog 不阻塞完工(与成品回流同降级策略)。
s.backflushWorkpiece(ctx, wp, operator)
s.notifyDashboard()
return nil
}
// backflushWorkpiece 反冲扣账:按工单产品整份 BOM 算「单件消耗(含损耗)」,
// 调 WMS 内部反冲接口 FIFO 扣库存 + 生成 backflush 出库单。
// 整份 BOM 一次性下发(完工即全部物料已消耗),WMS 不可达/校验失败仅记日志。
func (s *Service) backflushWorkpiece(ctx context.Context, wp *ent.Workpiece, operator string) {
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
s.ctx.EventLog.Write(ctx, "wms.backflush.failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲失败:工单不存在", map[string]any{"workOrderId": wp.WorkOrderId})
return
}
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(""))
if err != nil || len(bom) == 0 {
s.ctx.EventLog.Write(ctx, "wms.backflush.skip", wp.OrderNo, operator, "workpiece", wp.Sn,
"无BOM物料,跳过反冲", map[string]any{"productCode": wo.ProductCode})
return
}
items := make([]wmsclient.BackflushItem, 0, len(bom))
for _, it := range bom {
if it.MaterialCode == "" {
continue
}
// 单件消耗 = 单台用量 ×(1+损耗率),四舍五入;≤0 跳过
qty := int(math.Round(it.UnitQty * (1 + it.LossRate/100)))
if qty <= 0 {
continue
}
items = append(items, wmsclient.BackflushItem{MaterialCode: it.MaterialCode, Qty: qty})
}
if len(items) == 0 {
return
}
if wmsErr := s.ctx.Wms.Backflush(ctx, wp.OrderNo, 0, 0, operator, items); wmsErr != nil {
s.ctx.EventLog.Write(ctx, "wms.backflush.failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲扣账WMS失败(库存未扣,请人工核查)", map[string]any{"error": wmsErr.Error(), "productCode": wo.ProductCode})
return
}
s.ctx.EventLog.Write(ctx, "wms.backflush", wp.OrderNo, operator, "workpiece", wp.Sn,
"反冲扣账已下发WMS", map[string]any{"productCode": wo.ProductCode, "items": len(items)})
}
// retainFinishedToWms 完工品回流 WMS 库存。失败仅记日志(wms.finished.retain_failed),
// 不回滚完工状态——仓管可在 WMS 侧人工补录,避免 WMS 抖动卡死产线。
func (s *Service) retainFinishedToWms(ctx context.Context, wp *ent.Workpiece, operator string) {
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil || wo == nil {
s.ctx.EventLog.Write(ctx, "wms.finished.retain_failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"成品回流WMS失败:工单不存在", map[string]any{"error": "work order not found"})
return
}
if wmsErr := s.ctx.Wms.FinishedInbound(ctx, wo.ProductCode, wo.ProductName, wp.Sn, wp.OrderNo, operator); wmsErr != nil {
s.ctx.EventLog.Write(ctx, "wms.finished.retain_failed", wp.OrderNo, operator, "workpiece", wp.Sn,
"成品回流WMS失败(库存未入,请人工补录)", map[string]any{"error": wmsErr.Error(), "productCode": wo.ProductCode})
return
}
s.ctx.EventLog.Write(ctx, "wms.finished.retain", wp.OrderNo, operator, "workpiece", wp.Sn,
"成品完工已回流WMS库存", map[string]any{"productCode": wo.ProductCode})
}
// Trace 追溯查询(SN 维度):工序时间线/操作人/步骤数据/物料批次SN/拧紧数据
func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error) {
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx)
if err != nil {
return nil, errors.New("该SN无追溯数据")
}
procList, _ := s.ctx.EntClient.WorkpieceProcess.Query().
Where(workpieceprocess.Sn(sn)).Order(ent.Asc(workpieceprocess.FieldFlowId), ent.Asc(workpieceprocess.FieldID)).All(ctx)
steps, _ := s.ctx.EntClient.StepData.Query().
Where(stepdata.Sn(sn)).Order(ent.Desc(stepdata.FieldID)).All(ctx)
assoc, _ := s.ctx.EntClient.AssociationTrace.Query().
Where(associationtrace.FinishedSn(sn)).First(ctx)
torque, _ := s.ctx.EntClient.TorqueRecord.Query().
Where(torquerecord.Sn(sn)).Order(ent.Desc(torquerecord.FieldTime)).Limit(200).All(ctx)
bindVO, _ := s.BuildBindTrace(ctx, sn)
return map[string]any{
"workpiece": wp,
"processTimeline": procList,
"stepData": steps,
"associationTrace": assoc,
"torqueRecords": torque,
"bindTrace": bindVO,
}, nil
}
func (s *Service) ListWorkpieces(ctx context.Context, sn, orderNo string) ([]*ent.Workpiece, error) {
q := s.ctx.EntClient.Workpiece.Query()
if sn != "" {
q = q.Where(workpiece.SnContains(sn))
}
if orderNo != "" {
q = q.Where(workpiece.OrderNo(orderNo))
}
return q.Order(ent.Desc(workpiece.FieldID)).Limit(500).All(ctx)
}
// InProcessItem 在制品(已进线未完工)视图项。跨天工件继续停留,日排产不重复排。
type InProcessItem struct {
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
ProductCode string `json:"productCode"`
ProductName string `json:"productName"`
CurrentStation int `json:"currentStation"` // 0=上线位, 13=下线位, 其余为物理工位
CurrentStationName string `json:"currentStationName"` // 工位显示名(上线位/下线位/装配X)
Status string `json:"status"`
OnlineAt string `json:"onlineAt"`
DwellSec int64 `json:"dwellSec"` // 停留时长(秒),自进线起算
DoneSteps int `json:"doneSteps"`
TotalSteps int `json:"totalSteps"`
}
// WorkpiecesInProcess 列出停在各工位、尚未完工的在制品(ONLINE/PROCESSING/REPAIR)。
// stationNo<=0 返回全部;stationNo>0 仅返回当前停在该工位(0=上线位, 13=下线位)的工件。
// orderNo 非空时按工单号模糊过滤(客户第12条「记录查看加一项看工单号查询」)。
// 已进线未完工的工件跨天继续,日排产仅排「未开工数量」,不会重复排产。
func (s *Service) WorkpiecesInProcess(ctx context.Context, stationNo int, orderNo string) ([]InProcessItem, error) {
q := s.ctx.EntClient.Workpiece.Query().
Where(workpiece.Or(
workpiece.Status("ONLINE"),
workpiece.Status("PROCESSING"),
workpiece.Status("REPAIR"),
))
if orderNo != "" {
q = q.Where(workpiece.OrderNoContainsFold(orderNo))
}
list, err := q.Order(ent.Desc(workpiece.FieldOnlineAt)).All(ctx)
if err != nil {
return nil, err
}
out := make([]InProcessItem, 0, len(list))
woCache := map[int]*ent.WorkOrder{}
getWO := func(wp *ent.Workpiece) *ent.WorkOrder {
if wp.WorkOrderId <= 0 {
return nil
}
if wo, ok := woCache[wp.WorkOrderId]; ok {
return wo
}
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
if err != nil {
wo = nil
}
woCache[wp.WorkOrderId] = wo
return wo
}
for _, wp := range list {
// 路线 = 工单工艺组合(唯一路线源头);未配置组合则无路线(maxStation=0
items, _ := RouteStationsFromWO(ctx, s.ctx.EntClient, getWO(wp))
seq := RouteStationsOfWO(items)
maxStation := 0
for _, c := range seq {
if c > maxStation {
maxStation = c
}
}
// 推算当前物理工位:ONLINE 在 上线位(0)PROCESSING 且已到末端 → 下线位(13)
cur := 0
if wp.Status == "PROCESSING" || wp.Status == "REPAIR" {
cp := wp.CurrentStationNo
if cp <= 0 {
cp = maxStation
}
if cp >= maxStation && wp.Status == "PROCESSING" {
cur = 13
} else {
cur = cp
}
}
if stationNo > 0 && cur != stationNo {
continue
}
dwell := int64(0)
var onlineAt string
if wp.OnlineAt != nil {
dwell = int64(time.Since(*wp.OnlineAt).Seconds())
onlineAt = wp.OnlineAt.Format("2006-01-02 15:04:05")
}
done := 0
if procs, e := s.ctx.EntClient.WorkpieceProcess.Query().
Where(workpieceprocess.Sn(wp.Sn), workpieceprocess.Status("DONE")).Count(ctx); e == nil {
done = procs
}
code, name := "", ""
if wp.WorkOrderId > 0 {
if wo, e := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId); e == nil {
code, name = wo.ProductCode, wo.ProductName
}
}
if code == "" && wp.OrderNo != "" {
if wo, e := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(wp.OrderNo)).First(ctx); e == nil {
code, name = wo.ProductCode, wo.ProductName
}
}
stName := stationDisplayName("", cur)
if cur == 0 {
stName = "上线位(虚拟)"
} else if cur == 13 {
stName = "下线位(虚拟)"
}
out = append(out, InProcessItem{
Sn: wp.Sn,
OrderNo: wp.OrderNo,
ProductCode: code,
ProductName: name,
CurrentStation: cur,
CurrentStationName: stName,
Status: wp.Status,
OnlineAt: onlineAt,
DwellSec: dwell,
DoneSteps: done,
TotalSteps: len(seq),
})
}
return out, nil
}