package logic import ( "context" "encoding/json" "errors" "strings" "time" "bj_power_mes/ent" "bj_power_mes/ent/bomitem" "bj_power_mes/ent/inspectionrecord" ) // FlexStr 兼容客户端把数字字段传成 number 或 string(前端 stationNo 用数字, // 标准 JSON 反序列化进 string 会报错 → 400 请求体解析失败) type FlexStr string func (f *FlexStr) UnmarshalJSON(b []byte) error { s := strings.TrimSpace(string(b)) s = strings.Trim(s, `"`) if s == "null" { s = "" } *f = FlexStr(s) return nil } // InspectionReq 巡检记录请求(PAD 巡检终端,块8) type InspectionReq struct { Category string `json:"category"` // CHECKIN/POINT/PROCESS/DONE/ALARM StationNo FlexStr `json:"stationNo"` Shift string `json:"shift"` // 早/中/晚 OrderNo string `json:"orderNo"` // 工单号 Sn string `json:"sn"` // 工件SN Result string `json:"result"` // OK/NG Items []string `json:"items"` // 点检项/异常类型 Photo string `json:"photo"` // 拍照文件名 Remark string `json:"remark"` // 文字描述/签字 Operator string `json:"operator"` // 操作人 // P0-4 过程巡检按步骤维度 ProcessCode int `json:"processCode"` // 所属工位号 StepId int `json:"stepId"` // 工艺步骤ID StepName string `json:"stepName"` // 工艺步骤名称 MeasuredValue string `json:"measuredValue"` // 实测值 // 质量检验(过程检/完工检/来料检)扩展字段 ReportNo string `json:"reportNo"` // 报检单号 MaterialCode string `json:"materialCode"` // 物料编码(图号) MaterialName string `json:"materialName"` // 物料名称 Manufacturer string `json:"manufacturer"` // 生产厂家 InspectionNo string `json:"inspectionNo"` // 检验单号 AttachmentIds string `json:"attachmentIds"` // 附件ID,逗号分隔 DisposalType string `json:"disposalType"` // 处置类型 退货/返修/退换 DisposalRemark string `json:"disposalRemark"` // 处置说明 } // CreateInspection 提交巡检记录;ALARM 类型同时触发看板报警(SSE + 清缓存) func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, operator string) error { if req.Category == "" { return errors.New("记录类型必填") } category := req.Category // PROCESS 为 PAD 巡检终端「过程巡检」专用;MES 车间质检「过程检」用 QC_PROCESS 隔离, // 避免同枚举导致两个列表互混、统计口径乱(P1-8)。 if !oneOf(category, "CHECKIN", "POINT", "PROCESS", "DONE", "ALARM", "INCOMING", "FINAL", "QC_PROCESS") { return errors.New("记录类型不合法") } result := req.Result if result == "" { result = "OK" } if req.Operator == "" { req.Operator = operator } create := s.ctx.EntClient.InspectionRecord.Create(). SetCategory(category). SetStationNo(string(req.StationNo)). SetShift(req.Shift). SetOrderNo(req.OrderNo). SetSn(req.Sn). SetResult(result). SetItems(req.Items). SetPhoto(req.Photo). SetRemark(req.Remark). SetOperator(req.Operator). SetProcessCode(req.ProcessCode). SetStepId(req.StepId). SetStepName(req.StepName). SetMeasuredValue(req.MeasuredValue) if req.ReportNo != "" { create.SetReportNo(req.ReportNo) } if req.MaterialCode != "" { create.SetMaterialCode(req.MaterialCode) } if req.MaterialName != "" { create.SetMaterialName(req.MaterialName) } if req.Manufacturer != "" { create.SetManufacturer(req.Manufacturer) } if req.InspectionNo != "" { create.SetInspectionNo(req.InspectionNo) } if req.AttachmentIds != "" { create.SetAttachmentIds(req.AttachmentIds) } if req.DisposalType != "" { create.SetDisposalType(req.DisposalType) } if req.DisposalRemark != "" { create.SetDisposalRemark(req.DisposalRemark) } _, err := create.Save(ctx) if err != nil { return err } s.ctx.EventLog.Write(ctx, "inspection."+category, req.OrderNo, req.Operator, "inspection_record", req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": string(req.StationNo), "result": result}) if category == "ALARM" { // 触发看板报警:SSE 推送 + 使 Redis 看板报警缓存失效 alarmJSON, _ := json.Marshal(map[string]any{ "level": "CRITICAL", "type": "inspection_alarm", "message": "巡检异常上报:" + req.Remark, "station": string(req.StationNo), "time": time.Now().Format("2006-01-02 15:04:05"), }) if s.ctx.SSE != nil { s.ctx.SSE.Publish("dashboard.alarm", string(alarmJSON)) } if s.ctx.RedisClient != nil { _, _ = s.ctx.RedisClient.Del("dashboard:alarms") } } s.notifyDashboard() return nil } // ListInspections 查询巡检记录(不分页,供内部/导出用,封顶 1000) func (s *Service) ListInspections(ctx context.Context, category, operator, from, to string) ([]*ent.InspectionRecord, error) { rows, _, err := s.ListInspectionsPaged(ctx, category, operator, from, to, "", "", "", "", "", 1, 1000) return rows, err } // ListInspectionsPaged 分页查询巡检记录,返回 {rows, total};排序 id desc 最新置顶 // orderNo/stationNo:2026-09-08 补充筛选(大小写不敏感) // materialCode/materialName/reportNo:质量检验模糊查询(Item H,每框内模糊、多框取交集) func (s *Service) ListInspectionsPaged(ctx context.Context, category, operator, from, to, orderNo, stationNo, materialCode, materialName, reportNo string, page, pageSize int) ([]*ent.InspectionRecord, int, error) { q := s.ctx.EntClient.InspectionRecord.Query() if category != "" { q = q.Where(inspectionrecord.Category(category)) } if orderNo != "" { q = q.Where(inspectionrecord.OrderNoEqualFold(orderNo)) } if stationNo != "" { q = q.Where(inspectionrecord.StationNoEqualFold(stationNo)) } if materialCode != "" { q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode)) } if materialName != "" { q = q.Where(inspectionrecord.MaterialNameContainsFold(materialName)) } if reportNo != "" { q = q.Where(inspectionrecord.ReportNoContainsFold(reportNo)) } if operator != "" { q = q.Where(inspectionrecord.Operator(operator)) } if from != "" { if f, err := time.Parse("2006-01-02", from); err == nil { q = q.Where(inspectionrecord.CreatedAtGTE(f)) } } if to != "" { if t, err := time.Parse("2006-01-02", to); err == nil { q = q.Where(inspectionrecord.CreatedAtLT(t.Add(24 * time.Hour))) } } total, err := q.Count(ctx) if err != nil { return nil, 0, err } rows, err := q.Order(ent.Desc(inspectionrecord.FieldID)). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx) return rows, total, err } func oneOf(v string, items ...string) bool { for _, it := range items { if v == it { return true } } return false } // GetInspectionRecord 按 id 查询一条检验记录(质量检验处置/不合格单用) func (s *Service) GetInspectionRecord(ctx context.Context, id int) (*ent.InspectionRecord, error) { if id <= 0 { return nil, errors.New("缺少 id") } rec, err := s.ctx.EntClient.InspectionRecord.Get(ctx, id) if err != nil { return nil, errors.New("检验记录不存在") } return rec, nil } // SetInspectionDisposal 质量检验处置:区分 退货/返修/退换,写处置说明 func (s *Service) SetInspectionDisposal(ctx context.Context, id int, disposalType, disposalRemark, operator string) error { if id <= 0 { return errors.New("缺少 id") } if !oneOf(disposalType, "退货", "返修", "退换") { return errors.New("处置类型仅支持 退货/返修/退换") } rec, err := s.ctx.EntClient.InspectionRecord.Get(ctx, id) if err != nil { return errors.New("检验记录不存在") } _, err = s.ctx.EntClient.InspectionRecord.UpdateOneID(id). SetDisposalType(disposalType).SetDisposalRemark(disposalRemark).Save(ctx) if err != nil { return err } s.ctx.EventLog.Write(ctx, "inspection.disposal", rec.OrderNo, operator, "inspection_record", rec.Sn, "质量检验处置 "+disposalType, map[string]any{"id": id, "remark": disposalRemark}) return nil } // QueryBomMaterial 按物料编码或名称/图号模糊检索 BOM 料信息(质量检验"选物料自动带出 BOM"用) func (s *Service) QueryBomMaterial(ctx context.Context, keyword string) ([]*ent.BomItem, error) { if keyword == "" { return []*ent.BomItem{}, nil } q := s.ctx.EntClient.BomItem.Query() q = q.Where( bomitem.Or( bomitem.MaterialCodeContainsFold(keyword), bomitem.MaterialNameContainsFold(keyword), ), ) return q.Order(ent.Asc(bomitem.FieldProductCode), ent.Asc(bomitem.FieldBomName), ent.Asc(bomitem.FieldID)).Limit(200).All(ctx) }