using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure; using System.Text.RegularExpressions; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// S8 数据源配置服务。 /// /// S8-LEGACY-RUNTIME-RETIREMENT-1:本服务已退化为只读。 /// /// 产品裁决:S8 = 数据中台**消费层**,不是数据接入层。 /// /// Data Platform → Governed Dataset → S8 Provider → Canonical Contract → Watch Rule /// /// 物理数据源(连接串 / 主机 / 端口 / 凭据)的治理权归数据平台,不由租户 S8 管理员维护。 /// /// 为什么不能只隐藏 UIado_s8_data_source.endpoint 存的是**含凭据的真实连接串**, /// 而 API 仍可被直接调用。写能力留着就等于「入口看不见但仍可写库、仍可让 S8 发起对外连接」。 /// 故在服务层(写入的唯一收敛点)统一拒绝。 /// /// 为什么连接测试也退役TestAsync 虽只写 last_check_*,但它会用库里的 endpoint /// **真实建立外部数据库连接** —— 那正是本批要终止的「S8 维护物理连接」能力, /// 比单纯的行更新更需要关掉。随之移除 S8SqlSugarScopeFactory 依赖: /// 本服务从此**在结构上**不具备发起外连的能力,而不只是"约定不用"。 /// /// 保留的部分: /// · —— 存量 Legacy 规则仍绑着这些行,排障与历史核对需要能看到(endpoint 仍脱敏); /// · 写方法签名 —— 硬约束,S8TenantIsolationContractTests 用反射断言带 /// S8TrustedScope 的写入口存在、且无 scope 的旧重载不存在; /// · ado_s8_data_source 表 / S8DataSourceRowLoader / S8LegacySqlDataProvider —— /// 本批只做 Runtime Retirement,物理清理留待后续独立批次。 /// public class S8DataSourceService : ITransient { private readonly SqlSugarRepository _rep; public S8DataSourceService(SqlSugarRepository rep) { _rep = rep; } /// 数据源写入口退役后的统一文案。与 Rule 侧退役文案分开:二者是两条不同的产品裁决。 public const string RetiredMessage = "S8 已切换为数据中台 Dataset 消费模式,物理数据源由数据平台治理," + "S8 侧不再提供数据源的新增 / 修改 / 删除 / 连接测试。"; /// /// 只读列出本作用域的数据源。endpoint 中的 Pwd / Password 一律脱敏后返回。 /// public async Task> ListAsync(long tenantId, long factoryId) { var rows = await _rep.AsQueryable() .Where(x => x.TenantId == tenantId && x.FactoryId == factoryId) .ToListAsync(); foreach (var r in rows) r.Endpoint = MaskSecret(r.Endpoint); return rows; } // ================================================================================ // 写入口:全部退役。 // // ⚠️ 抛出发生在**任何 DB 访问之前**:不做 LoadScopedAsync、不做重复性查询。 // 这既保证零 DB 触碰,也保证任意 id(含越权 id)一律 410 而非 404 // ——「这个能力没了」优先于「这条记录不属于你」, // 避免用越权探测反推他租户资源是否存在。 // ================================================================================ public Task CreateAsync(AdoS8DataSource body, S8TrustedScope scope) => throw new S8WriteRetiredException(RetiredMessage); public Task UpdateAsync(long id, AdoS8DataSource body, S8TrustedScope scope) => throw new S8WriteRetiredException(RetiredMessage); public Task DeleteAsync(long id, S8TrustedScope scope) => throw new S8WriteRetiredException(RetiredMessage); public Task TestAsync(long id, S8TrustedScope scope) => throw new S8WriteRetiredException(RetiredMessage); // BUG-13:endpoint 中的 Pwd=xxx / Password=xxx(大小写不敏感)替换为 ******,保留其它字段。 private static readonly Regex SecretPattern = new( @"(?i)(Pwd|Password)\s*=\s*([^;]*)", RegexOptions.Compiled); private static string? MaskSecret(string? endpoint) { if (string.IsNullOrWhiteSpace(endpoint)) return endpoint; return SecretPattern.Replace(endpoint, m => $"{m.Groups[1].Value}=******"); } }