|
|
@@ -238,16 +238,35 @@ public class ProductDesignService : IDynamicApiController, ITransient
|
|
|
if (string.IsNullOrWhiteSpace(itemNum))
|
|
|
return new BomAndRoutingOutput();
|
|
|
|
|
|
+ // 🔴 租户硬门:本方法的两段裸 SQL 走 _db.Ado.SqlQueryAsync,**完全绕过** SqlSugar 的
|
|
|
+ // QueryFilter(后者只作用于 Queryable<T>),必须自己带 tenant_id。
|
|
|
+ // 且本控制器是 [AllowAnonymous],匿名请求下 SqlSugarSetup 的 ITenantIdFilter
|
|
|
+ // 根本不会注册(它要求 JWT 里有 TenantId claim),连兜底都没有。
|
|
|
+ // tenantId 为 0(匿名/无 claim)时下面所有谓词都匹配不到行 —— fail-closed,符合预期。
|
|
|
+ var tenantId = _userManager.TenantId;
|
|
|
+
|
|
|
// 查询图纸设计周期
|
|
|
var drawingDesignCycle = await _db.Queryable<AdoS0ItemMaster>()
|
|
|
- .Where(x => x.ItemNum == itemNum && x.IsActive == true)
|
|
|
+ .Where(x => x.TenantId == tenantId && x.ItemNum == itemNum && x.IsActive == true)
|
|
|
.Select(x => x.DrawingDesign)
|
|
|
.FirstAsync();
|
|
|
|
|
|
+ // 🔴 七处 tenant_id 谓词缺一不可(2026-09-08 实测,逐条见下)。
|
|
|
+ //
|
|
|
+ // **只在最外层 WHERE 过滤是无效的**,有三层原因:
|
|
|
+ // ① 语法上不成立 —— CTE 的列清单里根本没有 tenant_id;
|
|
|
+ // ② 即便把 tenant_id 加进 CTE 列再只在外层过滤,仍会残留"血统不明的自家行" ——
|
|
|
+ // 实测料号 3124C0015:外层过滤版比正确版多出 15/15/150 行,这些行自身的
|
|
|
+ // tenant_id 等于查询租户、外层删不掉,但它们是靠"经过外租户的边"才被走到的节点
|
|
|
+ // (ItemMaster 有 663 个跨租户重码,递归中间层一旦串到别人的图上就会这样回落);
|
|
|
+ // ③ 最外层的 LEFT JOIN ItemMaster 本身就是独立放大器 —— 实测 452 行 CTE 经它膨胀到 1120 行。
|
|
|
+ //
|
|
|
+ // ⚠️ ④⑤⑦ 三处**必须放在 ON 而不是 WHERE**:放 WHERE 会把无匹配的 NULL 行滤掉,
|
|
|
+ // LEFT JOIN 退化成 INNER JOIN,没有物料主数据/产线的 BOM 行会整行消失。
|
|
|
var bomSql = @"
|
|
|
WITH RECURSIVE temp(ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed) AS (
|
|
|
SELECT ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed
|
|
|
- FROM ProductStructureMaster WHERE ParentItem=@itemNum
|
|
|
+ FROM ProductStructureMaster WHERE ParentItem=@itemNum AND tenant_id=@tenantId
|
|
|
UNION ALL
|
|
|
SELECT c.ParentItem,c.ComponentItem,c.op,
|
|
|
CAST(c.qty+(c.qty*(c.Scrap+c.QtyExchd)/100) AS DECIMAL(15,8)) AS qty,
|
|
|
@@ -255,21 +274,27 @@ WITH RECURSIVE temp(ParentItem,ComponentItem,op,qty,StructureType,QtyConsumed) A
|
|
|
FROM ProductStructureMaster c
|
|
|
INNER JOIN temp p ON c.ParentItem=p.ComponentItem
|
|
|
INNER JOIN ItemMaster parentIm ON c.ParentItem=parentIm.ItemNum AND parentIm.PurMfg<>'P'
|
|
|
+ AND parentIm.tenant_id=@tenantId
|
|
|
+ WHERE c.tenant_id=@tenantId
|
|
|
)
|
|
|
SELECT psm.ParentItem,psm.ComponentItem AS ItemNum,im.Descr AS ItemName,
|
|
|
CASE WHEN IFNULL(pso.Op,0)=0 THEN psm.Op ELSE pso.Op END AS Op,
|
|
|
psm.qty AS Qty,psm.StructureType,im.EMTType AS EmtType,psm.QtyConsumed
|
|
|
FROM temp psm
|
|
|
LEFT JOIN ItemMaster im ON psm.ComponentItem=im.ItemNum
|
|
|
+ AND im.tenant_id=@tenantId
|
|
|
LEFT JOIN ProductStructureOp pso ON pso.ParentItem=psm.ParentItem
|
|
|
- AND pso.ComponentItem=psm.ComponentItem AND pso.ProductItem=@itemNum";
|
|
|
+ AND pso.ComponentItem=psm.ComponentItem AND pso.ProductItem=@itemNum
|
|
|
+ AND pso.tenant_id=@tenantId";
|
|
|
|
|
|
var routingSql = @"
|
|
|
SELECT r.Descr,r.Op,r.ParentOp,CAST(r.MilestoneOp AS CHAR(5)) AS MilestoneOp,p.Line,r.RouteCode
|
|
|
-FROM RoutingOpDetail as r left join ProdLineDetail as p on r.RoutingCode=p.Part and r.Op=p.Op WHERE RoutingCode=@itemNum ORDER BY r.Op";
|
|
|
+FROM RoutingOpDetail as r left join ProdLineDetail as p on r.RoutingCode=p.Part and r.Op=p.Op
|
|
|
+ and p.tenant_id=@tenantId
|
|
|
+WHERE r.RoutingCode=@itemNum AND r.tenant_id=@tenantId ORDER BY r.Op";
|
|
|
|
|
|
- var boms = await _db.Ado.SqlQueryAsync<BomQueryRow>(bomSql, new { itemNum });
|
|
|
- var routings = await _db.Ado.SqlQueryAsync<RoutingQueryRow>(routingSql, new { itemNum });
|
|
|
+ var boms = await _db.Ado.SqlQueryAsync<BomQueryRow>(bomSql, new { itemNum, tenantId });
|
|
|
+ var routings = await _db.Ado.SqlQueryAsync<RoutingQueryRow>(routingSql, new { itemNum, tenantId });
|
|
|
|
|
|
return new BomAndRoutingOutput
|
|
|
{
|