package logic import ( "context" "errors" "fmt" "bj_power_mes/ent" "bj_power_mes/ent/bomitem" "bj_power_mes/ent/workorder" ) // 默认BOMName 同一型号多份 BOM 并存时的缺省名称(历史数据/未指定时统一回填此值) const DefaultBOMName = "默认" type BomItemReq struct { ProductCode string `json:"productCode"` BomName string `json:"bomName"` MaterialCode string `json:"materialCode"` MaterialName string `json:"materialName"` Spec string `json:"spec"` Unit string `json:"unit"` ManageMode string `json:"manageMode"` ProcessCode *int `json:"processCode"` // 装配工序 1~12;nil=不修改(兼容旧调用/移除时保留);0=显式清除 UnitQty float64 `json:"unitQty"` LossRate float64 `json:"lossRate"` RequiredQty float64 `json:"requiredQty"` SerialSns []string `json:"serialSns"` } // bomNameOrDefault 空 BOM 名回退为「默认」(兼容旧前端/旧数据) func bomNameOrDefault(name string) string { if name == "" { return DefaultBOMName } return name } // SaveBom 保存产品 BOM(覆盖式 upsert:唯一键 = 产品编码+BOM名称+物料编码)。 // 同一产品型号可并存多份 BOM(不同厂家供货/含半成品等不同料单结构),按 bomName 区分, // 工单建单时选定一份;备料/装机绑定均按工单锁定的 bomName 计算。 func (s *Service) SaveBom(ctx context.Context, productCode, bomName string, items []BomItemReq, operator string) error { bomName = bomNameOrDefault(bomName) for _, it := range items { if it.ManageMode == "" { it.ManageMode = "2" } exist, err := s.ctx.EntClient.BomItem.Query(). Where(bomitem.ProductCode(productCode), bomitem.BomName(bomName), bomitem.MaterialCode(it.MaterialCode)).First(ctx) if err == nil && exist != nil { u := s.ctx.EntClient.BomItem.UpdateOneID(exist.ID). SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit). SetManageMode(it.ManageMode).SetUnitQty(it.UnitQty).SetLossRate(it.LossRate) if it.ProcessCode != nil { u = u.SetProcessCode(*it.ProcessCode) // nil 时不覆盖既有工序配置 } if len(it.SerialSns) > 0 { u.SetSerialSns(it.SerialSns) } if _, err := u.Save(ctx); err != nil { return fmt.Errorf("更新BOM物料 %s 失败: %w", it.MaterialCode, err) } } else { pc := 0 if it.ProcessCode != nil { pc = *it.ProcessCode } b := s.ctx.EntClient.BomItem.Create(). SetProductCode(productCode).SetBomName(bomName).SetMaterialCode(it.MaterialCode). SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit). SetManageMode(it.ManageMode).SetProcessCode(pc). SetUnitQty(it.UnitQty).SetLossRate(it.LossRate) if len(it.SerialSns) > 0 { b.SetSerialSns(it.SerialSns) } if _, err := b.Save(ctx); err != nil { return fmt.Errorf("保存BOM物料 %s 失败: %w", it.MaterialCode, err) } } } s.ctx.EventLog.Write(ctx, "bom.save", "", operator, "product_bom", productCode, "保存产品BOM", map[string]any{"bomName": bomName, "count": len(items)}) return nil } // DeleteBomItem 移除 BOM 中的一条物料(按行主键)。 // 安全校验:该 (产品编码+BOM名称) 下任一工单已生成备料单时禁止移除,避免已锁定料单被改动。 func (s *Service) DeleteBomItem(ctx context.Context, id int, operator string) error { item, err := s.ctx.EntClient.BomItem.Get(ctx, id) if err != nil { return errors.New("BOM 物料不存在") } used, err := s.ctx.EntClient.WorkOrder.Query(). Where(workorder.ProductCode(item.ProductCode)).Exist(ctx) if err == nil && used { // 该型号已有工单,谨慎起见禁止直接移除 return fmt.Errorf("该物料清单已被「%s」型号的工单选用,移除物料会影响备料/追溯,请先评估相关工单", item.ProductCode) } if err := s.ctx.EntClient.BomItem.DeleteOneID(id).Exec(ctx); err != nil { return err } s.ctx.EventLog.Write(ctx, "bom.item.delete", "", operator, "product_bom", item.ProductCode, "移除BOM物料 "+item.MaterialCode, map[string]any{"bomName": item.BomName}) return nil } // ListBom 查询 BOM(可按产品编码 + BOM 名称过滤;bomName 传 "ALL" 或空且指定了产品时返回全部) func (s *Service) ListBom(ctx context.Context, productCode, bomName string) ([]*ent.BomItem, error) { q := s.ctx.EntClient.BomItem.Query() if productCode != "" { q = q.Where(bomitem.ProductCode(productCode)) } if bomName != "" && bomName != "ALL" { q = q.Where(bomitem.BomName(bomName)) } return q.Order(ent.Asc(bomitem.FieldProductCode), ent.Asc(bomitem.FieldBomName), ent.Asc(bomitem.FieldID)).All(ctx) } // BomNameInfo 型号下的一份 BOM 概要(名称 + 物料数) type BomNameInfo struct { BomName string `json:"bomName"` ItemCount int `json:"itemCount"` } // ListBomNames 列出某产品型号下并存的所有 BOM 名称(工单建单选 BOM 下拉数据源) func (s *Service) ListBomNames(ctx context.Context, productCode string) ([]*BomNameInfo, error) { if productCode == "" { return []*BomNameInfo{}, nil } items, err := s.ctx.EntClient.BomItem.Query(). Where(bomitem.ProductCode(productCode)).All(ctx) if err != nil { return nil, err } order := []string{} count := map[string]int{} for _, it := range items { if _, ok := count[it.BomName]; !ok { order = append(order, it.BomName) } count[it.BomName]++ } out := make([]*BomNameInfo, 0, len(order)) for _, name := range order { out = append(out, &BomNameInfo{BomName: name, ItemCount: count[name]}) } return out, nil }