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