package logic import ( "context" "errors" "fmt" "time" "bj_power_mes/ent" "bj_power_mes/ent/inspectionrecord" "bj_power_mes/ent/materialqtyreport" "bj_power_mes/ent/workorder" ) // ============ 工位终端业务操作(P0-3 / 数量不符 / 退库 / 工序间检验记录) ============ // EmergencyCall 应急呼叫:落预警消息(type=emergency_call),预警中心实时展示 + pad 顶部提示。 // 纯软件方案(客户 L614 已定:发消息给班组长,不依赖安灯硬件)。 func (s *Service) EmergencyCall(ctx context.Context, stationNo int, orderNo, remark, operator string) error { if stationNo <= 0 { return errors.New("缺少工位号") } content := fmt.Sprintf("工位 %d 发起应急呼叫", stationNo) if orderNo != "" { content += "(工单 " + orderNo + ")" } if remark != "" { content += ":" + remark } _, err := s.ctx.EntClient.Alert.Create(). SetRuleId(0). SetType("emergency_call"). SetTitle("应急呼叫"). SetContent(content). SetRefType("station"). SetRefId(fmt.Sprintf("%d", stationNo)). SetReceiver(""). SetStatus("UNREAD"). Save(ctx) if err != nil { return err } s.ctx.EventLog.Write(ctx, "station.emergency", orderNo, operator, "alert", fmt.Sprintf("%d", stationNo), "工位应急呼叫", map[string]any{"stationNo": stationNo, "remark": remark}) s.notifyDashboard() return nil } // QtyReportReq 数量不符上报请求(工位收到料数量与下发数量不一致) type QtyReportReq struct { StationNo int `json:"stationNo"` OrderNo string `json:"orderNo"` Sn string `json:"sn"` MaterialCode string `json:"materialCode"` MaterialName string `json:"materialName"` PlanQty int `json:"planQty"` ActualQty int `json:"actualQty"` Cause string `json:"cause"` Operator string `json:"operator"` } // CreateQtyReport 数量不符上报:落 material_qty_report + 预警中心消息(type=qty_diff)。 func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error { if req.StationNo <= 0 || req.MaterialCode == "" { return errors.New("工位号与物料编码必填") } diff := req.PlanQty - req.ActualQty _, err := s.ctx.EntClient.MaterialQtyReport.Create(). SetStationNo(req.StationNo). SetOrderNo(req.OrderNo). SetSn(req.Sn). SetMaterialCode(req.MaterialCode). SetMaterialName(req.MaterialName). SetPlanQty(req.PlanQty). SetActualQty(req.ActualQty). SetDiffQty(diff). SetCause(req.Cause). SetStatus("PENDING"). SetOperator(req.Operator). Save(ctx) if err != nil { return err } content := fmt.Sprintf("工位 %d 数量不符:物料 %s 应发 %d / 实收 %d / 差异 %d。原因:%s", req.StationNo, req.MaterialCode, req.PlanQty, req.ActualQty, diff, req.Cause) _, _ = s.ctx.EntClient.Alert.Create(). SetRuleId(0). SetType("qty_diff"). SetTitle("数量不符上报"). SetContent(content). SetRefType("material_qty_report"). SetRefId(req.MaterialCode). SetReceiver(""). SetStatus("UNREAD"). Save(ctx) s.ctx.EventLog.Write(ctx, "station.qty_report", req.OrderNo, req.Operator, "material_qty_report", req.MaterialCode, "数量不符上报", map[string]any{"stationNo": req.StationNo, "diff": diff}) s.notifyDashboard() return nil } // ListQtyReports 数量不符上报列表 func (s *Service) ListQtyReports(ctx context.Context, status, orderNo, materialCode string) ([]*ent.MaterialQtyReport, error) { q := s.ctx.EntClient.MaterialQtyReport.Query() if status != "" { q = q.Where(materialqtyreport.Status(status)) } if orderNo != "" { q = q.Where(materialqtyreport.OrderNoContainsFold(orderNo)) } if materialCode != "" { q = q.Where(materialqtyreport.MaterialCodeContainsFold(materialCode)) } return q.Order(ent.Desc(materialqtyreport.FieldCreatedAt), ent.Desc(materialqtyreport.FieldID)).All(ctx) } // StationReturnMaterial 工位退料回库房:落事件日志 + 调 WMS 预建待确认退库单。 // 退库单在 WMS 侧由库管确认收货后库存加回(链路封闭,问题记录 L491)。 func (s *Service) StationReturnMaterial(ctx context.Context, orderNo string, stationNo int, sn, materialCode, materialName, spec, reason, operator, unit string, qty int) error { if stationNo <= 0 || materialCode == "" || qty <= 0 { return errors.New("工位号、物料编码、退库数量必填") } s.ctx.EventLog.Write(ctx, "station.return_material", orderNo, operator, "return_order", materialCode, "工位退料回库房", map[string]any{"stationNo": stationNo, "qty": qty, "reason": reason}) // 调 WMS 内部 API 预建退库单(失败降级:记录日志但不阻断工位操作) if s.ctx.Wms != nil { if err := s.ctx.Wms.CreateReturnOrder(ctx, orderNo, sn, materialCode, materialName, spec, reason, operator, stationNo, qty, unit); err != nil { return fmt.Errorf("通知 WMS 建退库单失败:%w", err) } } return nil } // ============ 一键生成工序间检验记录(P0-4) ============ // InterProcessInspection 工序间检验记录汇总项 type InterProcessInspection struct { OrderNo string `json:"orderNo"` StationNo int `json:"stationNo"` StepId int `json:"stepId"` StepName string `json:"stepName"` Sn string `json:"sn"` Result string `json:"result"` Measured string `json:"measuredValue"` Photo string `json:"photo"` Operator string `json:"operator"` CreatedAt time.Time `json:"createdAt"` } // GenerateInterProcessInspection 汇总某工单 PROCESS 类巡检记录(按步骤维度)生成工序间检验记录。 // 返回结构化列表,前端/H5 据此渲染打印(PDF/Excel)。 func (s *Service) GenerateInterProcessInspection(ctx context.Context, orderNo string) ([]*InterProcessInspection, error) { if orderNo == "" { return nil, errors.New("工单号必填") } if _, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).First(ctx); err != nil { return nil, errors.New("工单不存在:" + orderNo) } rows, err := s.ctx.EntClient.InspectionRecord.Query(). Where( inspectionrecord.Category("PROCESS"), inspectionrecord.OrderNo(orderNo), ). Order(ent.Desc(inspectionrecord.FieldCreatedAt), ent.Desc(inspectionrecord.FieldID)). All(ctx) if err != nil { return nil, err } out := make([]*InterProcessInspection, 0, len(rows)) for _, r := range rows { out = append(out, &InterProcessInspection{ OrderNo: r.OrderNo, StationNo: atoiSafe(r.StationNo), StepId: r.StepId, StepName: r.StepName, Sn: r.Sn, Result: r.Result, Measured: r.MeasuredValue, Photo: r.Photo, Operator: r.Operator, CreatedAt: r.CreatedAt, }) } return out, nil }