Files
SunYF eeda70fc06 style(production): 优化代码格式和字段对齐
- 统一结构体字段的缩进和对齐格式
- 移除多余空行以提高代码可读性
- 标准化字段命名的一致性
2026-09-20 18:26:08 +08:00

606 lines
22 KiB
Go
Raw Permalink 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"
"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) }
func processName(code int) string {
names := map[int]string{1: "装配一", 2: "装配二", 3: "装配三", 4: "装配四", 5: "装配五", 6: "装配六", 7: "装配七", 8: "装配八", 9: "装配九", 10: "装配十", 11: "装配十一", 12: "装配十二"}
if n, ok := names[code]; ok {
return n
}
return "工位" + itoa(code)
}
// 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 工件进线登记
// 路线由「工位派工(station_process)」按日派生,工件不再冗余存储路线串
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"`
ProcessCode int `json:"processCode"`
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 工位报工:先应用装机绑定并做强校验(齐套才放行),再写报工实绩 + 步骤考核。
// 绑料是"落到工艺流程"的强约束:产品 BOM 中标记为本工序(processCode)的物料必须绑齐,
// 电气件按单台用量逐颗SN、结构件至少一个批次号;缺料直接拒绝报工并返回缺料清单。
func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, operator string) error {
if req.Sn == "" || req.ProcessCode == 0 {
return errors.New("sn 与 processCode 必填")
}
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(req.Sn)).First(ctx)
if err != nil {
return errors.New("工件未进线登记")
}
// 严格校验(业务口径已确认):仅“在制”工件可报工,且必须按工序顺序流转,禁止跳站/漏站/重复报同一站。
// 在制 = ONLINE(已进线) / PROCESSING(在工位)DONE/REPAIR/SCRAPPED 一律拒绝。
if wp.Status != "ONLINE" && wp.Status != "PROCESSING" {
return errors.New("工件当前状态为「" + wp.Status + "」,非在制,不允许报工")
}
// 顺序流转:currentProcess 语义为“已报工完成的工序”(进线时=0),本工位必须是其下一道,即 processCode == currentProcess+1。
if req.ProcessCode != wp.CurrentProcess+1 {
return errors.New("工序顺序不符:工件已完成工序" + itoa(wp.CurrentProcess) +
",应在工位" + itoa(wp.CurrentProcess+1) + "报工,当前请求工位" + itoa(req.ProcessCode))
}
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 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, req.ProcessCode, stationNo, operator, req.Binds); err != nil {
return err
}
if miss, err := s.validateStationBinds(ctx, client, req.Sn, req.ProcessCode); 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).SetProcessCode(req.ProcessCode).
SetStationNo(itoa(req.StationNo)).SetOrderNo(wp.OrderNo).
SetProcessName(processName(req.ProcessCode)).
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).
SetCurrentProcess(req.ProcessCode).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{"processCode": req.ProcessCode, "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("工件不存在")
}
// 兜底强校验:工艺路线中每个工序的装配物料都须绑齐(BOM 未配置工序时自动放行)
if miss, err := s.validateAllProcessBinds(ctx, s.ctx.EntClient, req.Sn); 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, _ := RouteStationsStr(ctx, s.ctx.EntClient, TodayStr(), "")
_, 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.FieldProcessCode)).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
}
// ListProcessRecords 报工记录列表(分页):直接查真实报工表 workpiece_process
// 手动报工与工位终端报工都在此可见,形成报工闭环(取代已废弃的 scan_record 查询)。
func (s *Service) ListProcessRecords(ctx context.Context, sn, orderNo string, stationNo, page, pageSize int) ([]map[string]any, int, error) {
q := s.ctx.EntClient.WorkpieceProcess.Query()
if sn != "" {
q = q.Where(workpieceprocess.SnEqualFold(sn))
}
if orderNo != "" {
q = q.Where(workpieceprocess.OrderNoEqualFold(orderNo))
}
if stationNo > 0 {
q = q.Where(workpieceprocess.StationNo(itoa(stationNo)))
}
total, err := q.Count(ctx)
if err != nil {
return nil, 0, err
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
rows, err := q.Order(ent.Desc(workpieceprocess.FieldID)).
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
if err != nil {
return nil, 0, err
}
list := make([]map[string]any, 0, len(rows))
for _, r := range rows {
ended := ""
if r.EndedAt != nil {
ended = r.EndedAt.Format("2006-01-02 15:04:05")
}
list = append(list, map[string]any{
"sn": r.Sn,
"orderNo": r.OrderNo,
"stationNo": r.StationNo,
"processCode": r.ProcessCode,
"processName": r.ProcessName,
"status": r.Status,
"result": r.Result,
"operator": r.Operator,
"durationSec": r.DurationSec,
"endedAt": ended,
})
}
return list, total, 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))
for _, wp := range list {
// 路线由今日工位派工(station_process)派生;未派工则无路线(maxStation=0
seq, _ := RouteStations(ctx, s.ctx.EntClient, TodayStr(), "")
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.CurrentProcess
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
}