using System.Text.RegularExpressions; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.DataPlatform; /// /// 标准层「当前 active 源 FULL Replace」局部工具(S5/S6/S7 列表双源 Phase 1 用)。 /// /// 用途:切源/FULL 刷新时,保证 std = 当前 active source 的完整结果集,消除旧源独有业务键残留。 /// 语义:先由调用方 PullAll 成功灌 stg,再在单事务内 DELETE 当前 tenant(+可选业务条件) 的 std, /// 随后执行「仅当前 source_system」的 INSERT(由调用方以委托传入其对象专属 transform SQL)。 /// /// 安全边界: /// - 仅本工具删除 std;不 TRUNCATE 整表、不跨 tenant、不跨业务类型(extraWhere 由调用方精确限定)。 /// - DELETE + INSERT 同一本库事务;任一失败回滚,旧 std 保持不变。 /// - 必须在 Pull 成功之后调用;Pull 失败应让异常在调用方先抛出,绝不先删 std。 /// - 非数据平台核心(不改 Factory/Executor/Writer),仅一层薄事务封装。 /// internal static class MdpStdFullReplace { // ⚠️ 适用边界(Phase 2 防御性复核确认):本工具的 DELETE 仅按 tenant_id(+可选 extraWhere) 限定, // 因此**仅适用于 tenant 独占的业务专表 std**(如 mdp_std_purchase_receipt / _production_receipt / _ipqc_inspection, // 实测各仅其转换服务写入)。若某 std 与其它业务共享,必须由调用方传入精确 extraWhere 业务分区条件,否则会误删他业务行。 private static readonly Regex IdentifierRe = new(@"^[A-Za-z0-9_]+$", RegexOptions.Compiled); /// /// 单事务内 FULL Replace 当前 tenant 的 std:先 DELETE,再执行 source 过滤的 INSERT 委托。 /// /// 调用方自身注入的 ISqlSugarClient(同一 scope,事务覆盖委托内的写入)。 /// 标准层表名(标识符白名单校验)。 /// 精确隔离的租户 id。 /// 可选附加删除条件(如业务类型列),已由调用方保证安全,不含用户输入。 /// 仅写入当前 source_system 结果的 INSERT 委托,返回写入行数。 public static async Task ReplaceAsync( ISqlSugarClient db, string stdTable, long tenantId, string? extraWhere, Func> insertScopedAsync, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(stdTable) || !IdentifierRe.IsMatch(stdTable)) throw new InvalidOperationException($"非法 std 表名:{stdTable}"); cancellationToken.ThrowIfCancellationRequested(); var where = string.IsNullOrWhiteSpace(extraWhere) ? "tenant_id=@__tenant" : $"tenant_id=@__tenant AND {extraWhere}"; await db.Ado.BeginTranAsync(); try { await db.Ado.ExecuteCommandAsync( $"DELETE FROM `{stdTable}` WHERE {where}", new SugarParameter("@__tenant", tenantId)); var rows = await insertScopedAsync(); await db.Ado.CommitTranAsync(); return rows; } catch { await db.Ado.RollbackTranAsync(); throw; } } }