package logic import ( "context" "errors" "fmt" "math" "sort" "strings" "bj_power_mes/ent" "bj_power_mes/ent/bomitem" "bj_power_mes/ent/workpiece" "bj_power_mes/ent/workpiecebind" "bj_power_mes/ent/workpieceprocess" ) // RemoveBind 撤销一条绑定(仅允许未报工前的误扫纠正;报工后不可撤销) func (s *Service) RemoveBind(ctx context.Context, sn string, processCode int, materialCode, bindValue, operator string) error { if sn == "" || materialCode == "" || bindValue == "" { return errors.New("sn/materialCode/bindValue 必填") } // 该工序已报工(存在实绩)则不允许撤销 done, err := s.ctx.EntClient.WorkpieceProcess.Query(). Where(workpieceprocess.Sn(sn), workpieceprocess.ProcessCode(processCode)).Exist(ctx) if err != nil { return err } if done { return errors.New("该工序已报工,不能撤销装配绑定") } n, err := s.ctx.EntClient.WorkpieceBind.Delete(). Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode), workpiecebind.MaterialCode(materialCode), workpiecebind.BindValue(bindValue)).Exec(ctx) if err != nil { return err } if n == 0 { return errors.New("未找到该绑定记录") } s.ctx.EventLog.Write(ctx, "workpiece.bind.remove", "", operator, "workpiece_bind", sn, "撤销装配绑定", map[string]any{"processCode": processCode, "materialCode": materialCode, "bindValue": bindValue}) return nil } // BindWorkpiece 独立绑定接口:工人在工位扫一件绑一件(报工前的逐条扫码/防错), // 与报工携带 binds 等价,落库后立即返回结果供前端即时反馈(SN 重复/料不匹配当场报错)。 func (s *Service) BindWorkpiece(ctx context.Context, sn, orderNo string, processCode int, stationNo string, operator string, items []BindItemReq) (map[string]any, error) { bound, err := s.applyBinds(ctx, sn, orderNo, processCode, stationNo, operator, items) if err != nil { return nil, err } if bound > 0 { s.ctx.EventLog.Write(ctx, "workpiece.bind", orderNo, operator, "workpiece_bind", sn, "装机绑定物料", map[string]any{"processCode": processCode, "count": bound}) s.notifyDashboard() } // 返回最新面板,前端据此刷新齐套状态 panel, err := s.StationBindPanelData(ctx, sn, processCode) if err != nil { return map[string]any{"bound": bound}, nil } return map[string]any{"bound": bound, "panel": panel}, nil } // ---------- 装机绑定(工位装配物料追溯 + 工艺流程强校验) ---------- // BindItemReq 单条装机绑定请求(报工时随 steps 一并提交) type BindItemReq struct { MaterialCode string `json:"materialCode"` BindValue string `json:"bindValue"` // 结构件=批次号;精密件=SN } // bindTarget 由 BOM 解析出的某工序应装物料定义 type bindTarget struct { MaterialCode string MaterialName string Spec string Unit string ManageMode string // 1 批次(结构件) / 2 SN(精密件) UnitQty float64 } // workpieceProduct 解析工件所属产品编码(sn → workpiece → workOrder → productCode) func (s *Service) workpieceProduct(ctx context.Context, sn string) (*ent.Workpiece, string, error) { wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx) if err != nil { return nil, "", errors.New("工件未进线登记") } if wp.WorkOrderId <= 0 { return wp, "", errors.New("工件未关联工单") } wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId) if err != nil || wo == nil { return wp, "", errors.New("工件所属工单不存在") } return wp, wo.ProductCode, nil } // stationBindTargets 取某产品在某工序(processCode>0)要求装配的物料清单 func (s *Service) stationBindTargets(ctx context.Context, productCode string, processCode int) ([]bindTarget, error) { items, err := s.ctx.EntClient.BomItem.Query(). Where(bomitem.ProductCode(productCode), bomitem.ProcessCode(processCode)).All(ctx) if err != nil { return nil, err } out := make([]bindTarget, 0, len(items)) for _, it := range items { out = append(out, bindTarget{ MaterialCode: it.MaterialCode, MaterialName: it.MaterialName, Spec: it.Spec, Unit: it.Unit, ManageMode: it.ManageMode, UnitQty: it.UnitQty, }) } return out, nil } // applyBinds 报工时应用装机绑定(落库 + 防错校验): // ① 料必须属于该产品 BOM 且装配工序配置为本工序(processCode==当前工序); // ② 精密件 SN 全局防重:已被其他工件绑定则拒绝(防错拿/防重复装机); // ③ 同一工件同一料同一绑定值幂等(重复扫码不报错,静默去重)。 // 返回绑定成功条数。 func (s *Service) applyBinds(ctx context.Context, sn, orderNo string, processCode int, stationNo string, operator string, items []BindItemReq) (int, error) { if len(items) == 0 { return 0, nil } wp, productCode, err := s.workpieceProduct(ctx, sn) if err != nil { return 0, err } if wp.Status == "DONE" || wp.Status == "SCRAPPED" { return 0, errors.New("已完工/报废工件不能再绑定物料") } // 产品 BOM 全量索引:materialCode → target(校验料合法性 + 取物料快照) bomMap := map[string]*ent.BomItem{} allBom, _ := s.ctx.EntClient.BomItem.Query(). Where(bomitem.ProductCode(productCode)).All(ctx) for _, b := range allBom { bomMap[b.MaterialCode] = b } bound := 0 for _, it := range items { mc := strings.TrimSpace(it.MaterialCode) bv := strings.TrimSpace(it.BindValue) if mc == "" || bv == "" { return bound, errors.New("物料编码与绑定值不能为空") } bi, ok := bomMap[mc] if !ok { return bound, fmt.Errorf("物料 %s 不在该产品的物料清单(BOM)中,不能绑定", mc) } if bi.ProcessCode != processCode { return bound, fmt.Errorf("物料 %s 配置在工序%d装配,本工位(工序%d)不可绑定", mc, bi.ProcessCode, processCode) } // 同工件同料同值幂等 exists, _ := s.ctx.EntClient.WorkpieceBind.Query(). Where(workpiecebind.Sn(sn), workpiecebind.MaterialCode(mc), workpiecebind.BindValue(bv)).Exist(ctx) if exists { continue } // 精密件 SN 全局防重 if bi.ManageMode == "2" { other, _ := s.ctx.EntClient.WorkpieceBind.Query(). Where(workpiecebind.BindValue(bv), workpiecebind.BindType("SN"), workpiecebind.SnNEQ(sn)).First(ctx) if other != nil { return bound, fmt.Errorf("精密件SN %s 已绑定到工件 %s(工序%d),不能重复装机", bv, other.Sn, other.ProcessCode) } } bindType := "BATCH" if bi.ManageMode == "2" { bindType = "SN" } if err := s.ctx.EntClient.WorkpieceBind.Create(). SetSn(sn).SetOrderNo(orderNo).SetProcessCode(processCode). SetStationNo(stationNo).SetMaterialCode(mc).SetMaterialName(bi.MaterialName). SetSpec(bi.Spec).SetUnit(bi.Unit).SetManageMode(bi.ManageMode). SetBindValue(bv).SetBindType(bindType).SetOperator(operator). Exec(ctx); err != nil { return bound, err } bound++ } return bound, nil } // validateStationBinds 校验某工件在某工序的装配物料是否绑齐(工艺流程强校验核心): // 返回缺料明细(空串=齐套)。判定:精密件(SN) 绑定的不同SN数 ≥ 单台用量;结构件(批次) 至少绑定1个批次号。 // 若该产品该工序未配置任何装配物料(processCode未配置),视为不启用绑定校验,放行。 func (s *Service) validateStationBinds(ctx context.Context, sn string, processCode int) (string, error) { _, productCode, err := s.workpieceProduct(ctx, sn) if err != nil { return "", err } targets, err := s.stationBindTargets(ctx, productCode, processCode) if err != nil { return "", err } if len(targets) == 0 { return "", nil // 该工序未配置装配物料 → 不校验 } // 已绑定明细:按 物料编码 → 绑定值集合 binds, _ := s.ctx.EntClient.WorkpieceBind.Query(). Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode)).All(ctx) boundMap := map[string]map[string]bool{} for _, b := range binds { if boundMap[b.MaterialCode] == nil { boundMap[b.MaterialCode] = map[string]bool{} } boundMap[b.MaterialCode][b.BindValue] = true } missing := []string{} for _, t := range targets { set := boundMap[t.MaterialCode] boundCount := len(set) need := int(math.Ceil(t.UnitQty)) if need < 1 { need = 1 } if t.ManageMode == "2" { // 精密件 SN:需绑满单台用量 if boundCount < need { missing = append(missing, fmt.Sprintf("%s(%s):需绑定%d颗SN,已绑%d颗", t.MaterialCode, t.MaterialName, need, boundCount)) } } else { // 结构件批次:至少绑定1个批次号 if boundCount < 1 { missing = append(missing, fmt.Sprintf("%s(%s):未绑定批次号", t.MaterialCode, t.MaterialName)) } } } if len(missing) == 0 { return "", nil } return "装配物料未绑齐:" + strings.Join(missing, ";"), nil } // validateAllProcessBinds 完工兜底:校验工件工艺路线中每个工序的装配物料齐套 func (s *Service) validateAllProcessBinds(ctx context.Context, sn string) (string, error) { wp, _, err := s.workpieceProduct(ctx, sn) if err != nil { return "", err } missing := []string{} for _, code := range ParseProcessSeq(wp.ProcessSeq) { msg, err := s.validateStationBinds(ctx, sn, code) if err != nil { return "", err } if msg != "" { missing = append(missing, fmt.Sprintf("工序%d:%s", code, msg)) } } if len(missing) == 0 { return "", nil } return strings.Join(missing, ";"), nil } // ListWorkpieceBinds 查询工件的装机绑定明细 func (s *Service) ListWorkpieceBinds(ctx context.Context, sn string) ([]*ent.WorkpieceBind, error) { q := s.ctx.EntClient.WorkpieceBind.Query() if sn != "" { q = q.Where(workpiecebind.Sn(sn)) } return q.Order(ent.Asc(workpiecebind.FieldProcessCode), ent.Asc(workpiecebind.FieldID)).Limit(2000).All(ctx) } // BindTraceVO 追溯页:装机绑定明细 + 每工序齐套状态 type BindTraceVO struct { Binds []*ent.WorkpieceBind `json:"binds"` Complete bool `json:"complete"` Missing string `json:"missing"` } // BuildBindTrace 组装某工件装机绑定追溯(含全工序齐套状态,供成品档案/追溯页展示) func (s *Service) BuildBindTrace(ctx context.Context, sn string) (*BindTraceVO, error) { binds, err := s.ListWorkpieceBinds(ctx, sn) if err != nil { return nil, err } missing, err := s.validateAllProcessBinds(ctx, sn) if err != nil { return nil, err } return &BindTraceVO{Binds: binds, Complete: missing == "", Missing: missing}, nil } // BindRequiredItem 应绑清单条目(工位终端/手动报工展示引导 + 前端齐套计算) type BindRequiredItem struct { MaterialCode string `json:"materialCode"` MaterialName string `json:"materialName"` Spec string `json:"spec"` Unit string `json:"unit"` ManageMode string `json:"manageMode"` UnitQty float64 `json:"unitQty"` Need int `json:"need"` // 需绑数量 Bound []string `json:"bound"` // 已绑定的批次号/SN 列表 BoundCount int `json:"boundCount"` Complete bool `json:"complete"` } // StationBindPanel 某工件在某工序的装配绑定面板数据(应绑清单 + 已绑情况) type StationBindPanel struct { Enabled bool `json:"enabled"` // 本工序是否启用装配物料绑定 Required []BindRequiredItem `json:"required"` Complete bool `json:"complete"` Missing string `json:"missing"` StationNo int `json:"stationNo"` ProcessCode int `json:"processCode"` LastBinds []*ent.WorkpieceBind `json:"lastBinds"` // 最近10条绑定记录 StationNames map[int]string `json:"-"` } // StationBindPanelData 供工位终端/手动报工页在报工前拉取:本工件本工序该装什么、已装什么 func (s *Service) StationBindPanelData(ctx context.Context, sn string, processCode int) (*StationBindPanel, error) { _, productCode, err := s.workpieceProduct(ctx, sn) if err != nil { return nil, err } targets, err := s.stationBindTargets(ctx, productCode, processCode) if err != nil { return nil, err } panel := &StationBindPanel{Enabled: len(targets) > 0, ProcessCode: processCode, StationNo: processCode} // 全量绑定(齐套判定必须按全量统计,不能用 limit 截断,否则多料/多SN会误判缺料) allBinds, _ := s.ctx.EntClient.WorkpieceBind.Query(). Where(workpiecebind.Sn(sn), workpiecebind.ProcessCode(processCode)).All(ctx) boundMap := map[string]map[string]bool{} for _, b := range allBinds { if boundMap[b.MaterialCode] == nil { boundMap[b.MaterialCode] = map[string]bool{} } boundMap[b.MaterialCode][b.BindValue] = true } for _, t := range targets { need := int(math.Ceil(t.UnitQty)) if need < 1 { need = 1 } bound := make([]string, 0, len(boundMap[t.MaterialCode])) for v := range boundMap[t.MaterialCode] { bound = append(bound, v) } sort.Strings(bound) item := BindRequiredItem{ MaterialCode: t.MaterialCode, MaterialName: t.MaterialName, Spec: t.Spec, Unit: t.Unit, ManageMode: t.ManageMode, UnitQty: t.UnitQty, Need: need, Bound: bound, BoundCount: len(bound), } if t.ManageMode == "2" { item.Complete = len(bound) >= need } else { item.Complete = len(bound) >= 1 } panel.Required = append(panel.Required, item) } allComplete := true for _, r := range panel.Required { if !r.Complete { allComplete = false break } } panel.Complete = allComplete if !allComplete { panel.Missing, _ = s.validateStationBinds(ctx, sn, processCode) } // 最近绑定(仅展示用) sort.Slice(allBinds, func(i, j int) bool { return allBinds[i].ID > allBinds[j].ID }) if len(allBinds) > 10 { allBinds = allBinds[:10] } panel.LastBinds = allBinds return panel, nil }