180 lines
6.1 KiB
Go
180 lines
6.1 KiB
Go
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
|
|||
|
|
}
|