Files
bj_power/bj_power_mes/internal/logic/bom_expand.go
T
SunYF d609ff8a9e refactor: 重构工艺工序相关命名与数据模型
1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
2026-09-22 11:32:29 +08:00

177 lines
6.0 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
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,
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,
})
}
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
}