|
|
@@ -33,6 +33,7 @@ public class MissedPrintConsumerScopeTests
|
|
|
private const string PendingSrc = "MaterialWarehouse/PendingInspectionService.cs";
|
|
|
private const string IqcPushSrc = "MaterialWarehouse/IqcWmsResultPushService.cs";
|
|
|
private const string BalanceSrc = "MaterialWarehouse/InventoryBalanceReader.cs";
|
|
|
+ private const string StocktakeSrc = "MaterialWarehouse/StocktakeResultService.cs";
|
|
|
|
|
|
private static string ReadSource(string relativePath)
|
|
|
{
|
|
|
@@ -198,6 +199,65 @@ public class MissedPrintConsumerScopeTests
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // ===================== FUNC-S5-013 StocktakeResult =====================
|
|
|
+
|
|
|
+ [Fact]
|
|
|
+ public void F013_MissedPrint_MustFilterByTenantId()
|
|
|
+ {
|
|
|
+ var src = ReadSource(StocktakeSrc);
|
|
|
+ Assert.Contains("\"m.tenant_id = @TenantId\"", src, StringComparison.Ordinal);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 013 不得再把 Domain 当租户边界:UAT 的 48 行 Domain 为空串,
|
|
|
+ /// 而 resolver 给出 '8010',叠加后会把本租户数据整体清零(单向误杀)。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void F013_MustNotUseDomainAsTenantBoundary()
|
|
|
+ {
|
|
|
+ var offenders = CodeLines(StocktakeSrc)
|
|
|
+ .Where(l => l.Contains("m.Domain", StringComparison.Ordinal)
|
|
|
+ || l.Contains("\"@Domain\"", StringComparison.Ordinal)
|
|
|
+ || l.Contains("ResolveDomainAsync", StringComparison.Ordinal))
|
|
|
+ .ToList();
|
|
|
+ Assert.True(offenders.Count == 0, $"013 仍在按 Domain 过滤/解析:{string.Join(" | ", offenders)}");
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>013 的 ItemMaster 关联必须 tenant-aware,且不得回落到 Domain-only。</summary>
|
|
|
+ [Fact]
|
|
|
+ public void F013_ItemMasterJoin_MustBeTenantAware()
|
|
|
+ {
|
|
|
+ var src = ReadSource(StocktakeSrc);
|
|
|
+ Assert.Contains("LEFT JOIN ItemMaster i ON i.tenant_id = m.tenant_id AND i.ItemNum = m.ItemNum",
|
|
|
+ src, StringComparison.Ordinal);
|
|
|
+ Assert.DoesNotContain(CodeLines(StocktakeSrc),
|
|
|
+ l => l.Contains("ON m.Domain = i.Domain", StringComparison.Ordinal));
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// COUNT 与 SELECT 必须建立在同一业务集合上:COUNT 只查 MissedPrint,
|
|
|
+ /// SELECT 多一个 LEFT JOIN —— 该 JOIN 必须恒为 1:1,否则分页取回行数会大于 total。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public void F013_CountAndSelect_ShareSameWhereClause()
|
|
|
+ {
|
|
|
+ var src = ReadSource(StocktakeSrc);
|
|
|
+ Assert.Contains("SELECT COUNT(1) FROM MissedPrint m WHERE {whereSql}", src, StringComparison.Ordinal);
|
|
|
+ // SELECT 侧复用同一个 whereSql,不得另起条件
|
|
|
+ var select = Slice(src, "FROM MissedPrint m", "ORDER BY m.UpdateTime");
|
|
|
+ Assert.Contains("WHERE {whereSql}", select, StringComparison.Ordinal);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>013 仍须保持只读,且 IsTag=1 业务语义不得被改动。</summary>
|
|
|
+ [Fact]
|
|
|
+ public void F013_MustRemainReadOnly_AndKeepIsTagSemantics()
|
|
|
+ {
|
|
|
+ var src = ReadSource(StocktakeSrc);
|
|
|
+ Assert.Contains("\"m.IsTag = 1\"", src, StringComparison.Ordinal);
|
|
|
+ foreach (var verb in new[] { "HttpPost", "HttpPut", "HttpDelete", "HttpPatch" })
|
|
|
+ Assert.DoesNotContain(verb, src, StringComparison.Ordinal);
|
|
|
+ }
|
|
|
+
|
|
|
// ===================== 数据面(AIDOP_IT=1,只读) =====================
|
|
|
|
|
|
/// <summary>
|
|
|
@@ -303,6 +363,77 @@ public class MissedPrintConsumerScopeTests
|
|
|
Assert.True(itemByDomain > 0 && suppByDomain > 0, "Domain-only 若已唯一,本条放大论证需重做");
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// §三 dry-run 固化:013 的 Domain 条件会单向清零 UAT。
|
|
|
+ /// 当前 IsTag=1 全库 0,故放宽 IsTag 做候选集模拟(不造数据)。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public async Task Data_F013_DomainCondition_WipesOutUatOnly()
|
|
|
+ {
|
|
|
+ if (!ItEnabled) return;
|
|
|
+ var db = BuildAidopdev();
|
|
|
+ Assert.NotNull(db);
|
|
|
+
|
|
|
+ async Task<int> Cnt(long t, bool withDomain) => await db!.Ado.GetIntAsync(
|
|
|
+ withDomain
|
|
|
+ ? "SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T AND Domain='8010'"
|
|
|
+ : "SELECT COUNT(1) FROM MissedPrint WHERE tenant_id=@T",
|
|
|
+ new List<SugarParameter> { new("@T", t) });
|
|
|
+
|
|
|
+ var uatOnly = await Cnt(TenantUat, false);
|
|
|
+ var uatWithDomain = await Cnt(TenantUat, true);
|
|
|
+ var aidopOnly = await Cnt(TenantAidop, false);
|
|
|
+ var aidopWithDomain = await Cnt(TenantAidop, true);
|
|
|
+
|
|
|
+ _out.WriteLine($"UAT tenant-only={uatOnly} / tenant+Domain={uatWithDomain};" +
|
|
|
+ $"797 tenant-only={aidopOnly} / tenant+Domain={aidopWithDomain}");
|
|
|
+ Assert.True(uatOnly > 0);
|
|
|
+ Assert.Equal(0, uatWithDomain); // Domain 把 UAT 自己的数据整体清零
|
|
|
+ Assert.Equal(aidopOnly, aidopWithDomain); // 对 797 无影响 —— 单向误杀
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// §六/§七 JOIN 基数:租户内 ItemNum 唯一 → 1:1 不放大;
|
|
|
+ /// 且按租户关联能取到的物料主数据严格多于按 Domain(今日 104 → 152)。
|
|
|
+ /// </summary>
|
|
|
+ [Fact]
|
|
|
+ public async Task Data_F013_ItemMasterJoin_OneToOne_AndMoreComplete()
|
|
|
+ {
|
|
|
+ if (!ItEnabled) return;
|
|
|
+ var db = BuildAidopdev();
|
|
|
+ Assert.NotNull(db);
|
|
|
+
|
|
|
+ var dupByTenant = await db!.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM (SELECT tenant_id,ItemNum FROM ItemMaster GROUP BY tenant_id,ItemNum HAVING COUNT(*)>1) x");
|
|
|
+ var dupByDomain = await db.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM (SELECT Domain,ItemNum FROM ItemMaster GROUP BY Domain,ItemNum HAVING COUNT(*)>1) x");
|
|
|
+
|
|
|
+ var baseRows = await db.Ado.GetIntAsync("SELECT COUNT(1) FROM MissedPrint");
|
|
|
+ var newJoinRows = await db.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM MissedPrint m LEFT JOIN ItemMaster i ON i.tenant_id=m.tenant_id AND i.ItemNum=m.ItemNum");
|
|
|
+ var newWorst = await db.Ado.GetIntAsync(
|
|
|
+ """
|
|
|
+ SELECT IFNULL(MAX(c),0) FROM (
|
|
|
+ SELECT m.RecID, COUNT(i.ItemNum) c FROM MissedPrint m
|
|
|
+ LEFT JOIN ItemMaster i ON i.tenant_id=m.tenant_id AND i.ItemNum=m.ItemNum
|
|
|
+ GROUP BY m.RecID) x
|
|
|
+ """);
|
|
|
+ var oldMatched = await db.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM MissedPrint m JOIN ItemMaster i ON m.Domain=i.Domain AND m.ItemNum=i.ItemNum");
|
|
|
+ var newMatched = await db.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM MissedPrint m JOIN ItemMaster i ON i.tenant_id=m.tenant_id AND i.ItemNum=m.ItemNum");
|
|
|
+
|
|
|
+ _out.WriteLine($"重复组:按租户={dupByTenant} 按Domain={dupByDomain};" +
|
|
|
+ $"base={baseRows} newJoin={newJoinRows} 单行最大命中={newWorst};" +
|
|
|
+ $"能取到物料名:old={oldMatched} new={newMatched}");
|
|
|
+
|
|
|
+ Assert.Equal(0, dupByTenant); // 租户内唯一 → 可直接收口
|
|
|
+ Assert.True(dupByDomain > 0, "Domain-only 若已唯一,放大论证需重做");
|
|
|
+ Assert.Equal(baseRows, newJoinRows); // 不放大:SELECT 行数 == COUNT 基集
|
|
|
+ Assert.True(newWorst <= 1);
|
|
|
+ Assert.True(newMatched > oldMatched, "按租户关联应当严格覆盖更多行(UAT 原本一条都取不到)");
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>回归:MissedPrint 租户归属未被本批破坏。</summary>
|
|
|
[Fact]
|
|
|
public async Task Data_Regression_TenantSplitUnchanged()
|