Files
bj_power/bj_power_mes/internal/logic/bom_expand.go
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

180 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}