Files
bj_power/bj_power_mes/internal/logic/bom_expand.go
T

180 lines
6.1 KiB
Go
Raw Normal View History

package logic
import (
"context"
"bj_power_mes/ent/bomitem"
)
// ============================================================
// BOM 展开统一核心(R1 工程债,2026-09-19
// ============================================================
//
// 背景:备料算料(expandBomToLeaf)与追溯物料组成(expandBindBom)原先各自实现了一套
// 递归展开,两套的"是否继续下钻"判定不同——
// - 备料:子项在 WMS 的品类为成品(3) 才继续展开;
// - 追溯:本地存在该编码的下级物料清单才继续展开。
// 结果是同一产品在"备料料单"和"追溯物料组成"里可能呈现不同结构,追溯断链。
//
// 此处抽出唯一递归骨架,两处共用同一份实现:深度上限、防环、用量倍率相乘、日志口径
// 完全一致;下钻判定与 BOM 名称策略由调用方以 resolver/参数注入,保证业务口径不变。
// bomNode BOM 展开的中间态树节点(备料拍平为叶子、追溯直接展示层级)。
type bomNode struct {
MaterialCode string
MaterialName string
Spec string
Unit string
ManageMode string
UnitQty float64 // 已按路径倍率折算的单台用量
LossRate float64
ProcessCode int
Level int // 1=顶层物料,2+=下级
Children []bomNode
}
// bomMaxDepth 展开深度上限(防配置错误导致的无界递归)。
const bomMaxDepth = 10
// bomCompositeResolver 批量判定本层哪些子项属于"组合件"(需继续下钻展开其自身清单)。
// 每层只调用一次,避免逐条查 WMS/库造成 N+1。
type bomCompositeResolver func(ctx context.Context, productCode string, codes []string) map[string]bool
// expandBomTree 统一递归展开核心。
// - productCode: 当前层产品/组合件编码;
// - bomName: 物料清单名称("" 由 ListBom 决定不过滤,具体策略由调用方传入);
// - mult: 父级累计倍率;
// - path: 当前展开路径(防环,同路径重复编码视为配置错误并记日志)。
func (s *Service) expandBomTree(ctx context.Context, productCode, bomName string, mult float64, depth int,
path map[string]bool, resolve bomCompositeResolver) []bomNode {
if depth > bomMaxDepth || productCode == "" {
return nil
}
bom, err := s.ListBom(ctx, productCode, bomName)
if err != nil || len(bom) == 0 {
return nil
}
codes := make([]string, 0, len(bom))
for _, b := range bom {
if b.MaterialCode != "" {
codes = append(codes, b.MaterialCode)
}
}
var composite map[string]bool
if resolve != nil {
composite = resolve(ctx, productCode, codes)
}
out := make([]bomNode, 0, len(bom))
for _, b := range bom {
if b.MaterialCode == "" {
continue
}
if path[b.MaterialCode] {
s.ctx.EventLog.Write(ctx, "bom.expand.cycle", "", "", "product_bom", productCode,
"产品物料清单存在循环引用,已跳过",
map[string]any{"materialCode": b.MaterialCode, "depth": depth})
continue
}
unitQty := b.UnitQty * mult
node := bomNode{
MaterialCode: b.MaterialCode,
MaterialName: b.MaterialName,
Spec: b.Spec,
Unit: b.Unit,
ManageMode: b.ManageMode,
UnitQty: unitQty,
LossRate: b.LossRate,
ProcessCode: b.ProcessCode,
Level: depth,
}
if composite[b.MaterialCode] {
path[b.MaterialCode] = true
node.Children = s.expandBomTree(ctx, b.MaterialCode, bomName, unitQty, depth+1, path, resolve)
delete(path, b.MaterialCode)
// 组合件没有自身清单(或展开为空):按叶子处理,避免漏料
if len(node.Children) == 0 {
node.Children = nil
}
}
out = append(out, node)
}
return out
}
// bomTreeToLeaf 把展开树拍平为叶子清单(备料算料用):组合件递归下沉,叶子原样输出。
func bomTreeToLeaf(nodes []bomNode) []bomLeaf {
out := make([]bomLeaf, 0, len(nodes))
for _, n := range nodes {
if len(n.Children) > 0 {
out = append(out, bomTreeToLeaf(n.Children)...)
continue
}
out = append(out, bomLeaf{
materialCode: n.MaterialCode,
materialName: n.MaterialName,
unit: n.Unit,
manageMode: n.ManageMode,
unitQty: n.UnitQty,
lossRate: n.LossRate,
processCode: n.ProcessCode,
})
}
return out
}
// bomTreeToBindNodes 把展开树转为追溯展示节点(保留层级与用量)。
func bomTreeToBindNodes(nodes []bomNode) []BindNode {
out := make([]BindNode, 0, len(nodes))
for _, n := range nodes {
node := BindNode{
MaterialCode: n.MaterialCode,
MaterialName: n.MaterialName,
Spec: n.Spec,
Unit: n.Unit,
ManageMode: n.ManageMode,
Qty: n.UnitQty,
Level: n.Level,
}
if len(n.Children) > 0 {
node.Children = bomTreeToBindNodes(n.Children)
}
out = append(out, node)
}
return out
}
// wmsFinishedResolver 备料侧下钻判定:子项在 WMS 的品类为成品(3) 时继续展开。
// 客户口径:成品可作为零部件被再次加工,备料必须展开到原材料,否则产线缺料。
// WMS 不可达时返回空集(全部按叶子处理),不阻塞备料生成,并记日志。
func (s *Service) wmsFinishedResolver(ctx context.Context, productCode string, codes []string) map[string]bool {
out := map[string]bool{}
if s.ctx.Wms == nil || len(codes) == 0 {
return out
}
types, err := s.ctx.Wms.MaterialTypes(ctx, codes)
if err != nil {
s.ctx.EventLog.Write(ctx, "bom.expand.wms_skip", "", "", "product_bom", productCode,
"WMS 物料品类查询不可达,本次按叶子物料展开", map[string]any{"error": err.Error()})
return out
}
for _, c := range codes {
if types[c] == 3 {
out[c] = true
}
}
return out
}
// localBomExistsResolver 追溯侧下钻判定:本地存在该编码的下级物料清单时继续展开。
// 不依赖 WMS(追溯页在 WMS 不可达时也必须能展示完整物料组成)。
func (s *Service) localBomExistsResolver(ctx context.Context, _ string, codes []string) map[string]bool {
out := map[string]bool{}
for _, c := range codes {
if has, _ := s.ctx.EntClient.BomItem.Query().Where(bomitem.ProductCode(c)).Exist(ctx); has {
out[c] = true
}
}
return out
}