package logic import ( "context" "errors" "sort" "strconv" "strings" "time" "bj_power_mes/ent" "bj_power_mes/ent/processstep" "bj_power_mes/ent/station" "bj_power_mes/ent/stepcriterion" "bj_power_mes/ent/torqueauditlog" "bj_power_mes/ent/torquerecord" "bj_power_mes/ent/workpieceprocess" ) type TorqueReq struct { Sn string `json:"sn"` WorkOrder string `json:"workOrder"` WorkOrderNo string `json:"workOrderNo"` StationNo string `json:"stationNo"` ScrewNo string `json:"screwNo"` Strain float64 `json:"strain"` // 扭矩 Torque float64 `json:"torque"` Angle float64 `json:"angle"` Result string `json:"result"` Operator string `json:"operator"` Time time.Time `json:"time"` } // ReportTorque 接收拧紧数据(工位终端/内部接口上报),入库并做扭矩 RANGE 判定写 step_data func (s *Service) ReportTorque(ctx context.Context, req TorqueReq, operator string) error { if req.Sn == "" { return errors.New("sn 不能为空") } if req.Strain == 0 { req.Strain = req.Torque } result := req.Result if result == "" { result = "OK" } workOrderNo := req.WorkOrder if workOrderNo == "" { workOrderNo = req.WorkOrderNo } if req.Time.IsZero() { req.Time = time.Now() } _, err := s.ctx.EntClient.TorqueRecord.Create(). SetWorkOrderNo(workOrderNo).SetSn(req.Sn).SetScrewNo(req.ScrewNo). SetStrain(req.Strain).SetTorque(req.Strain).SetAngle(req.Angle). SetResult(result).SetOperator(req.Operator).SetStationNo(req.StationNo). SetTime(req.Time).Save(ctx) if err != nil { return err } s.ctx.EventLog.Write(ctx, "torque.report", workOrderNo, operator, "torque_record", req.Sn, "接收拧紧数据上报", map[string]any{"strain": req.Strain, "angle": req.Angle, "result": result}) s.evaluateTorqueCriterion(ctx, req.Sn, req.StationNo, req.Strain, req.Angle, operator) s.notifyDashboard() return nil } // evaluateTorqueCriterion 依据 step_criterion RANGE 自动判定 OK/NG 写入 step_data func (s *Service) evaluateTorqueCriterion(ctx context.Context, sn, stationNo string, strain, angle float64, operator string) { q := s.ctx.EntClient.StepCriterion.Query().Where(stepcriterion.Name("扭矩")) crits, _ := q.All(ctx) ng := false for _, c := range crits { if c.Logic != "RANGE" { continue } ok := true if c.Min != nil && strain < *c.Min { ok = false } if c.Max != nil && strain > *c.Max { ok = false } _, _ = s.ctx.EntClient.StepData.Create(). SetSn(sn).SetStepId(c.StepId).SetCriterionId(c.ID). SetCriterionName(c.Name).SetCriterionLogic(c.Logic). SetValue(strain).SetIsOK(ok).SetOperator(operator).Save(ctx) if !ok { ng = true } } if ng { s.Evaluate(ctx, "torque_ng", 1, "拧紧扭矩不合格", "SN="+sn+" 工位"+stationNo, "torque_record", sn) } } // ListTorqueRecords 查询拧紧数据(四条件复合取交集:工单号/SN/日期区间/工位) func (s *Service) ListTorqueRecords(ctx context.Context, sn, workOrderNo, stationNo string, from, to time.Time) ([]*ent.TorqueRecord, error) { q := s.ctx.EntClient.TorqueRecord.Query() if sn != "" { // 大小写不敏感(2026-09-08 全项目搜索规范) q = q.Where(torquerecord.SnEqualFold(sn)) } if workOrderNo != "" { q = q.Where(torquerecord.WorkOrderNoEqualFold(workOrderNo)) } if stationNo != "" { q = q.Where(torquerecord.StationNoEqualFold(stationNo)) } if !from.IsZero() { q = q.Where(torquerecord.TimeGTE(from)) } if !to.IsZero() { q = q.Where(torquerecord.TimeLTE(to)) } return q.Order(ent.Desc(torquerecord.FieldTime)).Limit(500).All(ctx) } // requiredScrews 按工位号取该工位绑定工艺流程中 isTorque=true 的步骤数 = 应拧数量。 // 工位未绑定流程或流程无扭矩步骤时返回 0(表示无工艺基准,前端标注为「未配置」)。 func (s *Service) requiredScrews(ctx context.Context, stationNo string) int { no, err := strconv.Atoi(strings.TrimSpace(stationNo)) if err != nil || no <= 0 { return 0 } st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(no)).First(ctx) if err != nil || st.FlowId <= 0 { return 0 } cnt, _ := s.ctx.EntClient.ProcessStep.Query(). Where(processstep.FlowId(st.FlowId), processstep.IsTorque(true)).Count(ctx) return cnt } // TorqueGroupVO 拧紧一级汇总行(按 工单号+SN+工位 分组) type TorqueGroupVO struct { WorkOrderNo string `json:"workOrderNo"` Sn string `json:"sn"` StationNo string `json:"stationNo"` Required int `json:"required"` // 应拧数量(工艺流程 isTorque 步骤数) Actual int `json:"actual"` // 实际记录数 OkCount int `json:"okCount"` // 合格颗数 NgCount int `json:"ngCount"` // 不合格颗数 Satisfied bool `json:"satisfied"` // 是否满足:应拧>0 且 实拧>=应拧 且 无不合格 Audited bool `json:"audited"` // 是否已全部审核签字 Operator string `json:"operator"` // 最近操作人 LastTime time.Time `json:"lastTime"` // 最近采集时间 } // ListTorqueGroups 拧紧一级汇总列表:按 工单号+SN+工位 分组,附「应拧 vs 实拧」满足判定,后端真分页。 func (s *Service) ListTorqueGroups(ctx context.Context, sn, workOrderNo, stationNo string, from, to time.Time, page, pageSize int) (map[string]any, error) { recs, err := s.ListTorqueRecords(ctx, sn, workOrderNo, stationNo, from, to) if err != nil { return nil, err } type gkey struct{ wo, sn, st string } agg := map[gkey]*TorqueGroupVO{} reqCache := map[string]int{} for _, r := range recs { k := gkey{r.WorkOrderNo, r.Sn, r.StationNo} g, ok := agg[k] if !ok { g = &TorqueGroupVO{WorkOrderNo: r.WorkOrderNo, Sn: r.Sn, StationNo: r.StationNo} agg[k] = g } g.Actual++ if r.Result == "OK" { g.OkCount++ } else { g.NgCount++ } if r.Time.After(g.LastTime) { g.LastTime = r.Time g.Operator = r.Operator } } list := make([]*TorqueGroupVO, 0, len(agg)) for _, g := range agg { req, ok := reqCache[g.StationNo] if !ok { req = s.requiredScrews(ctx, g.StationNo) reqCache[g.StationNo] = req } g.Required = req g.Satisfied = req > 0 && g.Actual >= req && g.NgCount == 0 list = append(list, g) } // Audited 需基于分组内全部记录:任一条未审核即整组未审核 auditOK := map[gkey]bool{} for _, r := range recs { k := gkey{r.WorkOrderNo, r.Sn, r.StationNo} if _, seen := auditOK[k]; !seen { auditOK[k] = true } if r.AuditBy == "" { auditOK[k] = false } } for _, g := range list { g.Audited = auditOK[gkey{g.WorkOrderNo, g.Sn, g.StationNo}] } sort.Slice(list, func(i, j int) bool { return list[i].LastTime.After(list[j].LastTime) }) total := len(list) if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } start := (page - 1) * pageSize if start > total { start = total } end := start + pageSize if end > total { end = total } return map[string]any{"total": total, "list": list[start:end], "page": page, "pageSize": pageSize}, nil } // TorqueScrewStat 单工件单工位「应拧 vs 实拧」统计(二级明细抽屉顶部展示) func (s *Service) TorqueScrewStat(ctx context.Context, sn, stationNo string) (*TorqueGroupVO, error) { if strings.TrimSpace(sn) == "" { return nil, errors.New("sn 不能为空") } q := s.ctx.EntClient.TorqueRecord.Query().Where(torquerecord.SnEqualFold(sn)) if stationNo != "" { q = q.Where(torquerecord.StationNoEqualFold(stationNo)) } recs, _ := q.All(ctx) vo := &TorqueGroupVO{Sn: sn, StationNo: stationNo, Required: s.requiredScrews(ctx, stationNo)} allAudited := len(recs) > 0 for _, r := range recs { vo.Actual++ if r.WorkOrderNo != "" && vo.WorkOrderNo == "" { vo.WorkOrderNo = r.WorkOrderNo } if r.Result == "OK" { vo.OkCount++ } else { vo.NgCount++ } if r.AuditBy == "" { allAudited = false } if r.Time.After(vo.LastTime) { vo.LastTime = r.Time vo.Operator = r.Operator } } vo.Satisfied = vo.Required > 0 && vo.Actual >= vo.Required && vo.NgCount == 0 vo.Audited = allAudited return vo, nil } // AuditTorqueRecord 拧紧记录审核签字:标记审核人/时间并写留痕 func (s *Service) AuditTorqueRecord(ctx context.Context, recordId int, remark, operator string) error { rec, err := s.ctx.EntClient.TorqueRecord.Get(ctx, recordId) if err != nil { return errors.New("拧紧记录不存在") } if rec.AuditBy != "" { return errors.New("该记录已审核,不可重复审核") } _, err = rec.Update().SetAuditBy(operator).SetAuditAt(time.Now()).Save(ctx) if err != nil { return err } _, _ = s.ctx.EntClient.TorqueAuditLog.Create(). SetTorqueRecordId(rec.ID).SetAction("审核").SetOperator(operator). SetRemark(remark).SetOldVal(rec.AuditBy).SetNewVal(operator).Save(ctx) return nil } // ListTorqueAuditLog 查询某拧紧记录的修改留痕 func (s *Service) ListTorqueAuditLog(ctx context.Context, recordId int) ([]*ent.TorqueAuditLog, error) { return s.ctx.EntClient.TorqueAuditLog.Query(). Where(torqueauditlog.TorqueRecordId(recordId)). Order(ent.Desc(torqueauditlog.FieldCreatedAt)).All(ctx) } // 计算一块工件某工序的扭矩步骤是否已报 func (s *Service) hasWorkpieceProcess(ctx context.Context, sn string, processCode int) bool { cnt, _ := s.ctx.EntClient.WorkpieceProcess.Query(). Where(workpieceprocess.Sn(sn), workpieceprocess.ProcessCode(processCode)).Count(ctx) return cnt > 0 } // ProcessStepTemplate 下发给前端渲染的参数采集模板 type ProcessStepTemplate struct { ID int `json:"id"` Seq int `json:"seq"` Name string `json:"name"` CollectType string `json:"collectType"` // AUTO(拧紧枪自动)/MANUAL(手填)/NONE IsTorque bool `json:"isTorque"` NeedCheck bool `json:"needCheck"` Remark string `json:"remark"` Attachment []map[string]string `json:"attachment"` // 本步骤图纸/附件 [{name,url}],按工序分发到工位终端 Criteria []CriterionVO `json:"criteria"` } // CriterionVO 考核标准视图 type CriterionVO struct { ID int `json:"id"` Name string `json:"name"` Unit string `json:"unit"` Logic string `json:"logic"` // GE/LE/GT/LT/RANGE/EQUAL/NONE Target float64 `json:"target"` Min *float64 `json:"min"` Max *float64 `json:"max"` } // ListStationSteps 按工位号加载该工位绑定且「启用」的工艺流程的工序步骤(含考核标准)。 // 链路:工位号 → station.flow_id → 工艺流程(绑定关系在「关联工位」页维护,流程可绑任意工位)。 // 手动报工页:选工位 → 自动带出该工位的工序步骤。 func (s *Service) ListStationSteps(ctx context.Context, stationNo int) ([]*ProcessStepTemplate, error) { if stationNo < 1 { return []*ProcessStepTemplate{}, nil } st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx) if err != nil || st.FlowId <= 0 { // 该工位未绑定工艺流程:返回空模板(前端提示) return []*ProcessStepTemplate{}, nil } flow, err := s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId) if err != nil || flow.Status != "ACTIVE" { return []*ProcessStepTemplate{}, nil } return s.flowSteps(ctx, flow.ID), nil } // StepItemReq 一次保存的一个步骤(含其考核标准) type StepItemReq struct { ID int `json:"id"` Seq int `json:"seq"` Name string `json:"name"` CollectType string `json:"collectType"` IsTorque bool `json:"isTorque"` NeedCheck bool `json:"needCheck"` Remark string `json:"remark"` Attachment []map[string]string `json:"attachment"` // 本步骤图纸/附件 [{name,url}] Criteria []CriterionVO `json:"criteria"` }