|
|
@@ -45,10 +45,11 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
{
|
|
|
var kw = q.Keyword!.Trim();
|
|
|
// 直接匹配 BOM 主表存储的父/子项物料编码文本(ParentItem/ComponentItem)。
|
|
|
- // legacy BOM 行的 ParentMaterialId/ComponentMaterialId 多为 NULL(未解析到 ItemMaster.Id),
|
|
|
- // 原"keyword→ItemMaster.Id→Contains(ParentMaterialId)"关联会漏掉几乎全部历史数据,
|
|
|
- // 且依赖 CompanyRefId/FactoryRefId(查询区已不再提供)导致整段过滤被跳过。
|
|
|
- // 改为按父/子项编码文本模糊匹配,对 NULL-FK 行同样生效,对应"父级物料编码"查询语义。
|
|
|
+ // 原"keyword→ItemMaster.Id→Contains(ParentMaterialId)"关联依赖 CompanyRefId/FactoryRefId
|
|
|
+ // (查询区已不再提供)导致整段过滤被跳过,且对未解析到 ItemMaster.Id 的行直接漏掉。
|
|
|
+ // 改为按父/子项编码文本模糊匹配:不依赖 FK 是否解析成功,对应"父级物料编码"查询语义。
|
|
|
+ // 注:当初写下本注释时 legacy 行的 Parent/ComponentMaterialId 大量为 NULL,该描述已过期
|
|
|
+ // ——2026-09-02 实测全表 0 条 NULL/0(797 租户 57199 行全部已解析)。文本匹配的选型仍然成立。
|
|
|
query = query.Where(m => m.ParentItem.Contains(kw) || m.ComponentItem.Contains(kw));
|
|
|
}
|
|
|
|
|
|
@@ -57,16 +58,29 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
if (list.Count == 0)
|
|
|
return Ok(new { total, page, pageSize, list });
|
|
|
|
|
|
- var factoryId = q.FactoryRefId ?? list[0].FactoryRefId;
|
|
|
- var companyId = q.CompanyRefId ?? list[0].CompanyRefId;
|
|
|
-
|
|
|
- var phantomComponents = await _masterRep.ScopedTo(tenantId)
|
|
|
- .Where(x => x.FactoryRefId == factoryId && x.CompanyRefId == companyId
|
|
|
+ // 作用域必须按行隔离,**不得**用 list[0] 的公司/工厂代表整页。
|
|
|
+ // 一个租户可以同时存在多组 (公司, 工厂):实测 tenant 838257186181189 的 BOM 同时有
|
|
|
+ // 雪花 scope 与 legacy 1000/8010 两组。原实现用首行 scope 计算整页的虚拟件集合与工序聚合,
|
|
|
+ // 跨组分页时会把 A 工厂的数据算到 B 工厂的行上(前结构 / 工序两列出错)。
|
|
|
+ // 这里改为:先取本页出现的 scope 组合,批量取数一次,再按行各取自己的桶——不做 per-row 查询,无 N+1。
|
|
|
+ var scopeCompanyIds = list.Select(x => x.CompanyRefId).Distinct().ToList();
|
|
|
+ var scopeFactoryIds = list.Select(x => x.FactoryRefId).Distinct().ToList();
|
|
|
+
|
|
|
+ // 两个 IN 是 scope 组合的**超集**(可能多取到本页不存在的公司×工厂交叉组合);
|
|
|
+ // 下面按行真实的 (公司, 工厂) 分桶,多余的桶不会被任何行命中,结果不受影响。
|
|
|
+ var phantomRows = await _masterRep.ScopedTo(tenantId)
|
|
|
+ .Where(x => scopeCompanyIds.Contains(x.CompanyRefId) && scopeFactoryIds.Contains(x.FactoryRefId)
|
|
|
&& x.StructureType != null && x.StructureType.ToUpper() == "X")
|
|
|
- .Select(x => x.ComponentMaterialId)
|
|
|
- .Distinct()
|
|
|
+ .Select(x => new { x.CompanyRefId, x.FactoryRefId, x.ComponentMaterialId })
|
|
|
.ToListAsync();
|
|
|
- var phantomSet = phantomComponents.ToHashSet();
|
|
|
+ var phantomByScope = new Dictionary<(long CompanyRefId, long FactoryRefId), HashSet<long>>();
|
|
|
+ foreach (var p in phantomRows)
|
|
|
+ {
|
|
|
+ var key = (p.CompanyRefId, p.FactoryRefId);
|
|
|
+ if (!phantomByScope.TryGetValue(key, out var set))
|
|
|
+ phantomByScope[key] = set = new HashSet<long>();
|
|
|
+ set.Add(p.ComponentMaterialId);
|
|
|
+ }
|
|
|
|
|
|
var materialIds = list.SelectMany(m => new[] { m.ParentMaterialId, m.ComponentMaterialId }).Distinct().ToList();
|
|
|
var materials = await _materialRep.ScopedTo(tenantId)
|
|
|
@@ -76,12 +90,15 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
|
|
|
var pids = list.Select(x => x.ParentMaterialId).Distinct().ToList();
|
|
|
var cids = list.Select(x => x.ComponentMaterialId).Distinct().ToList();
|
|
|
+ // 工序覆盖同样按工厂隔离:聚合键带上 FactoryRefId,避免不同工厂下同一 (父件, 子件)
|
|
|
+ // 的工序号被串到一起(原实现只按首行 factory 过滤,跨工厂分页时必然串用)。
|
|
|
var allOps = await _opRep.ScopedTo(tenantId)
|
|
|
- .Where(o => o.FactoryRefId == factoryId && pids.Contains(o.ParentMaterialId) && cids.Contains(o.ComponentMaterialId))
|
|
|
+ .Where(o => scopeFactoryIds.Contains(o.FactoryRefId)
|
|
|
+ && pids.Contains(o.ParentMaterialId) && cids.Contains(o.ComponentMaterialId))
|
|
|
.OrderBy(o => o.Op)
|
|
|
.ToListAsync();
|
|
|
var aggMap = allOps
|
|
|
- .GroupBy(o => (o.ParentMaterialId, o.ComponentMaterialId))
|
|
|
+ .GroupBy(o => (o.FactoryRefId, o.ParentMaterialId, o.ComponentMaterialId))
|
|
|
.ToDictionary(
|
|
|
g => g.Key,
|
|
|
g => string.Join(",", g.Select(x => x.Op).Distinct().OrderBy(x => x)));
|
|
|
@@ -90,12 +107,15 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
{
|
|
|
matMap.TryGetValue(m.ParentMaterialId, out var pm);
|
|
|
matMap.TryGetValue(m.ComponentMaterialId, out var cm);
|
|
|
- var preStructureType = phantomSet.Contains(m.ParentMaterialId) ? "X" : "";
|
|
|
+ // 每行只看**自己所属工厂**的虚拟件集合与工序覆盖,不跨 scope 取数。
|
|
|
+ var parentIsPhantom = phantomByScope.TryGetValue((m.CompanyRefId, m.FactoryRefId), out var rowPhantoms)
|
|
|
+ && rowPhantoms.Contains(m.ParentMaterialId);
|
|
|
+ var preStructureType = parentIsPhantom ? "X" : "";
|
|
|
var structureTypeLabel = string.Equals(m.StructureType, "X", StringComparison.OrdinalIgnoreCase) ? "是" : "";
|
|
|
- var useAgg = phantomSet.Contains(m.ParentMaterialId)
|
|
|
+ var useAgg = parentIsPhantom
|
|
|
&& !string.Equals(m.StructureType, "X", StringComparison.OrdinalIgnoreCase);
|
|
|
var opDisplay = useAgg
|
|
|
- ? (aggMap.TryGetValue((m.ParentMaterialId, m.ComponentMaterialId), out var s) ? s : "")
|
|
|
+ ? (aggMap.TryGetValue((m.FactoryRefId, m.ParentMaterialId, m.ComponentMaterialId), out var s) ? s : "")
|
|
|
: (m.LineOp.HasValue ? m.LineOp.Value.ToString() : "");
|
|
|
|
|
|
return new
|
|
|
@@ -381,7 +401,8 @@ public class AdoS0ProductStructuresController : ControllerBase
|
|
|
/// 依据已证实的业务关系:ProductStructureOp 是「某顶层成品展开 BOM 时,对某条
|
|
|
/// (ParentItem → ComponentItem) 结构行的工序号覆盖」,其 ProductItem 恒为该父件在
|
|
|
/// BOM 中的祖先根件(实测 808/808 命中)。这里沿 ComponentItem→ParentItem 逐层上溯,
|
|
|
- /// 取「不再作为任何行 ComponentItem 出现」的顶层件作为候选。
|
|
|
+ /// 把**沿途每一层的祖先**都收进候选集(不是只取「不再作为任何行 ComponentItem 出现」的顶层件)——
|
|
|
+ /// 这与下面「候选集是上限而非实际值」的定位一致:服务端只做合法性收敛,不替用户挑成品。
|
|
|
///
|
|
|
/// 注意:候选集是**上限**而非实际值——历史数据里同一父件的 7 个祖先只有 4 个有覆盖行,
|
|
|
/// 说明具体选哪些是人工决定,服务端只做合法性收敛,不自动派生。
|