using SqlSugar; namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// Domain ↔ Ai-DOP tenant 解析器。 /// 直接查询 sys_tenant 系统租户表验证租户有效性,无需手动维护映射表。 /// Domain 一律按**当前租户自身主数据**解析:优先 GeneralizedCodeMaster.SystemConfig.CompanyCode, /// 缺失时回落到该租户自己的 LocationMaster 唯一 Domain;两者都不可得或不唯一 → fail closed。 /// **禁止任何硬编码 Domain 兜底**(历史上的 "8010" 兜底会把租户串到别人的 Domain)。 /// public sealed class SourceDomainTenantResolver : ITransient { private readonly ISqlSugarClient _db; public SourceDomainTenantResolver(ISqlSugarClient db) => _db = db; /// /// 根据 sourceCode + domain 解析**源归属租户**(source owner)。 /// /// 语义:一个 (source, domain) 组合物理上属于哪一个 DOP 租户的贴源落地区(stg)。 /// 唯一依据是显式登记表 ado_source_domain_tenant_map;未登记即 fail closed。 /// /// /// ⚠️ 本方法**不回答**「某条库存数据业务上属于哪个租户」——同一个 domain 可被多个租户 /// 按各自库位范围共享(如 8010 同时被 AIDOP 与 UATTEST_CHL 使用), /// 业务归属由标准层物化时按各租户 LocationMaster 库位范围投影决定。 /// /// /// 历史缺陷:本方法曾忽略 domain、恒取 SysTenant WHERE Status=1 ORDER BY Id LIMIT 1 /// (即默认租户),导致整份 domain 快照被塞进与业务无关的租户。已删除该行为。 /// /// public async Task ResolveTenantIdAsync( string sourceCode, string domain, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(sourceCode) || string.IsNullOrWhiteSpace(domain)) throw new InvalidOperationException("sourceCode/domain 不能为空"); var code = sourceCode.Trim(); var dom = domain.Trim(); var rows = await _db.Ado.SqlQueryAsync( """ SELECT DISTINCT m.tenant_id AS TenantId FROM ado_source_domain_tenant_map m JOIN SysTenant t ON t.Id = m.tenant_id AND t.Status = 1 WHERE m.source_code = @SourceCode AND m.domain = @Domain AND m.status = 1 """, new List { new("@SourceCode", code), new("@Domain", dom) }); // fail closed:既不猜第一个租户,也不回落默认租户 if (rows.Count == 0) throw new InvalidOperationException( $"源归属租户未登记:source={code}, domain={dom}(请在 ado_source_domain_tenant_map 显式登记)"); if (rows.Count > 1) throw new InvalidOperationException( $"源归属租户不唯一:source={code}, domain={dom}, tenants=[{string.Join(",", rows.Select(x => x.TenantId))}]"); if (rows[0].TenantId <= 0) throw new InvalidOperationException($"源归属租户 Id 非法:source={code}, domain={dom}"); return rows[0].TenantId; } /// /// 根据 sourceCode + tenantId 解析 domain。 /// 先在 sys_tenant 验证租户存在且启用,再**按该租户自身主数据**解析 Domain: /// ① GeneralizedCodeMaster.SystemConfig.CompanyCode(tenant 限定); /// ② 缺失时回落该租户 LocationMaster 的唯一 Domain。 /// 两者都取不到、或取到多个无法判定 → 抛异常 fail closed,绝不兜底到任何固定 Domain。 /// public async Task ResolveDomainAsync( string sourceCode, long tenantId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(sourceCode) || tenantId <= 0) throw new InvalidOperationException("sourceCode/tenantId 非法"); // 验证租户在 sys_tenant 中存在且启用 var tenantExists = await _db.Ado.GetIntAsync( "SELECT COUNT(1) FROM SysTenant WHERE Id = @TenantId AND Status = 1", new List { new("@TenantId", tenantId) }); if (tenantExists <= 0) throw new InvalidOperationException($"租户不存在或未启用:tenant={tenantId}"); // ① 主口径:当前租户自己配置的公司代码 var configured = await _db.Ado.SqlQueryAsync( """ SELECT DISTINCT Domain FROM GeneralizedCodeMaster WHERE tenant_id = @TenantId AND FldName = 'SystemConfig' AND Val = 'CompanyCode' AND IFNULL(Domain,'') <> '' """, new List { new("@TenantId", tenantId) }); var domain = PickSingleDomain(configured, tenantId, "GeneralizedCodeMaster.SystemConfig.CompanyCode"); if (domain is not null) return domain; // ② 次口径:当前租户自己的库位主数据(同样 tenant 限定,且必须唯一) var derived = await _db.Ado.SqlQueryAsync( """ SELECT DISTINCT Domain FROM LocationMaster WHERE tenant_id = @TenantId AND IFNULL(Domain,'') <> '' """, new List { new("@TenantId", tenantId) }); domain = PickSingleDomain(derived, tenantId, "LocationMaster"); if (domain is not null) return domain; // ③ fail closed:宁可查询失败,也不把租户串到别人的 Domain throw new InvalidOperationException( $"租户未配置 Domain 映射:tenant={tenantId}(GeneralizedCodeMaster.SystemConfig.CompanyCode 缺失," + "且该租户 LocationMaster 无法唯一推导 Domain)"); } /// 取唯一 Domain:0 条返回 null 交由下一口径;多条直接 fail closed,不猜。 private static string? PickSingleDomain(List rows, long tenantId, string source) { var domains = rows .Select(x => x.Domain?.Trim()) .Where(x => !string.IsNullOrEmpty(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (domains.Count == 0) return null; if (domains.Count > 1) throw new InvalidOperationException( $"租户 Domain 映射不唯一:tenant={tenantId}, source={source}, domains=[{string.Join(",", domains)}]"); return domains[0]; } private sealed class MapRow { public long TenantId { get; set; } } private sealed class DomainRow { public string? Domain { get; set; } } }