package logic import ( "context" "errors" "strconv" "time" "bj_power_mes/ent" "bj_power_mes/ent/associationtrace" "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" ) 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"` } type OnlineReq struct { Sn string `json:"sn"` OrderNo string `json:"orderNo"` WorkOrderId int `json:"workOrderId"` ProcessSeq string `json:"processSeq"` } // OnlineWorkpiece 工件进线登记 func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator string) error { if req.Sn == "" { return errors.New("sn 不能为空") } seq := req.ProcessSeq if seq == "" { seq = FullProcessSeq } 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 if req.ProcessSeq == "" && wo.ProcessSeq != "" { seq = wo.ProcessSeq } } } seq = NormalizeProcessSeq(seq) err := s.ctx.EntClient.Workpiece.Create(). SetSn(req.Sn).SetOrderNo(req.OrderNo).SetWorkOrderId(req.WorkOrderId). SetProcessSeq(seq).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),随报工原子提交 } // 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("工件未进线登记") } // 阶段1:装机绑定(随报工提交)→ 落库后强校验齐套 stationNo := "" if req.StationNo > 0 { stationNo = itoa(req.StationNo) } if _, err := s.applyBinds(ctx, req.Sn, wp.OrderNo, req.ProcessCode, stationNo, operator, req.Binds); err != nil { return err } if miss, err := s.validateStationBinds(ctx, req.Sn, req.ProcessCode); err != nil { return err } else if miss != "" { return errors.New(miss) } now := time.Now() proc, err := s.ctx.EntClient.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(now).SetEndedAt(now).Save(ctx) if err != nil { return err } allOK := true for _, st := range req.Steps { if ok, e := s.writeStepData(ctx, req.Sn, proc.ID, st, operator); e == nil && !ok { allOK = false } } if bad, _ := s.ctx.EntClient.StepData.Query(). Where(stepdata.ProcessId(proc.ID), stepdata.IsOK(false)).Exist(ctx); bad { allOK = false } result := "OK" if !allOK { result = "NG" } _, _ = s.ctx.EntClient.WorkpieceProcess.UpdateOneID(proc.ID).SetResult(result).Save(ctx) _, _ = s.ctx.EntClient.Workpiece.UpdateOneID(wp.ID). SetCurrentProcess(req.ProcessCode).SetStatus("PROCESSING").Save(ctx) 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 } // writeStepData 写一条步骤考核数据,按标准自动判定是否合格 func (s *Service) writeStepData(ctx context.Context, sn string, processId int, st StepDataReq, operator string) (bool, error) { ok := true crits, _ := s.ctx.EntClient.StepCriterion.Query().Where(stepcriterion.StepId(st.StepId)).All(ctx) 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 := s.ctx.EntClient.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, 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 { _, err = s.ctx.EntClient.AssociationTrace.Create(). SetFinishedSn(req.Sn).SetOrderNo(wp.OrderNo).SetProcessCode(wp.ProcessSeq). 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) s.notifyDashboard() return nil } // 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 } 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) }