using System.Diagnostics;
using Admin.NET.Plugin.AiDOP.FinishedWarehouse;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.Infrastructure;
///
/// S7-FQC-TENANT-AUTHORITY-1:S7 成品检验(FQC)三类审批角色的**租户本地化**权威基线。
///
/// 修复的缺陷(与 S6 同一 SYSTEMIC TENANT CLONE DEFECT,2026-09-14 审计确认):
/// - ROLE_S7_FQC_INSPECTOR / _SUPERVISOR / _QE 三个 Code **全库各只有一行、固定挂默认租户**
/// (对照 ROLE_S6_IPQC_* 已各有 10 个租户副本);
/// - 各租户用户靠**跨租户 SysUserRole 绑定**借用(审计实测 9 条跨租户绑定,其中 4 条被 Running 实例依赖);
/// - 流程定义又把默认租户的**物理 RoleId**(1329915020002 / 1329915020003)写死进 FlowJson,
/// 于是「谁是成品检验主管」跨租户共享,可用性挂在一条无约束、无审计、可被单行 SQL 删除的手工绑定上。
///
/// 目标模型(与 S6 及平台主流 ROLE_QC / SMART_OPS_* 一致):
/// 每个租户各有自己的三个 S7 角色,Code 相同、RoleId 不同、成员与角色同租户;
/// FlowJson 以 **RoleCode** 表达审批权威,由 FlowEngineService 的 Code 分支
/// (r.TenantId == effectiveTenantId && r.Status == Enable && codes.Contains(r.Code))
/// 在目标租户内重新解析。
///
/// 为什么新建文件而不是把 泛化:后者是 S6 两条链的在用权威基线,
/// 改它等于把 S6 拖进本批变更面。本文件结构与其逐一对应(RoleSpecs / EnsureTenantLocalRoles /
/// CopyLegacyCrossTenantMembership / ResolveLocalRoleId / GetActivatableTenantIds / ResolveTenantIds),
/// 便于将来两者一并抽公共基类——那是独立的重构批次,不在本批。
///
/// 边界:
/// - 默认租户 的那三行**本身就是它的 tenant-local 角色**,
/// 沿用 的既有固定 Id,不重建、不改动(历史 FlowJson 仍按该 Id 判定)。
/// - membership 是 **COPY 不是 MOVE**:既有跨租户绑定原样保留,供 7 条 Running 实例与尚未迁移的
/// legacy 快照继续使用;清理属后续独立批次(必须排在 Active Snapshot Migration 之后)。
/// - 本文件**不得**出现任何具体租户 Id 或 UAT 账号——只按 SysTenant 注册表通用处理。
/// - 本文件**不触碰**任何 ApprovalFlowInstance / FlowJsonSnapshot:历史实例迁移不在此发生。
///
internal static class S7TenantAuthoritySeed
{
/// 三类 S7 角色:Code / 名称 / 排序号 / 默认租户既有固定 Id。名称沿用既有 S7 角色语义。
private static readonly (string Code, string Name, int OrderNo, long LegacyDefaultTenantRoleId)[] RoleSpecs =
{
(FqcInspBillFlowConst.RoleInspectorCode, "成品检验员", 820, FqcInspBillFlowConst.RoleInspectorId),
(FqcInspBillFlowConst.RoleSupervisorCode, "成品检验主管", 821, FqcInspBillFlowConst.RoleSupervisorId),
(FqcInspBillFlowConst.RoleQeCode, "成品检验QE", 822, FqcInspBillFlowConst.RoleQeId),
};
/// 三个 S7 RoleCode(供流程种子与 Readiness 判定共用)。
public static IReadOnlyList RoleCodes { get; } =
RoleSpecs.Select(x => x.Code).ToList();
///
/// 幂等地建立租户本地角色并复制既有成员语义。重复调用无副作用。
///
public static void EnsureTenantAuthority(ISqlSugarClient db)
{
try
{
EnsureTenantLocalRoles(db);
CopyLegacyCrossTenantMembership(db);
}
catch (Exception ex)
{
Trace.TraceWarning("S7 tenant authority seed: " + ex);
}
}
///
/// 为 SysTenant 注册表中的每个租户建立三个 S7 角色。查重键是 (TenantId, Code)——
/// 原实现(FqcInspBillFlowSeed.EnsureRole)用全局 ClearFilter().Any(x => x.Code == code),
/// 一旦默认租户已有该 Code 就再也不会为其它租户建,这正是「全库单例」的成因。
///
private static void EnsureTenantLocalRoles(ISqlSugarClient db)
{
var now = DateTime.Now;
foreach (var tenantId in ResolveTenantIds(db))
{
foreach (var spec in RoleSpecs)
{
var exists = db.Queryable()
.ClearFilter()
.Any(r => r.TenantId == tenantId && r.Code == spec.Code);
if (exists) continue;
// 默认租户沿用既有固定 Id(历史 FlowJson 仍按该 Id 判定,不能换号);
// 其余租户一律走雪花号,遵循 SysRole 的 tenant-local 身份语义。
var roleId = tenantId == SqlSugarConst.DefaultTenantId
? spec.LegacyDefaultTenantRoleId
: YitIdHelper.NextId();
db.Insertable(new SysRole
{
Id = roleId,
TenantId = tenantId,
Name = spec.Name,
Code = spec.Code,
OrderNo = spec.OrderNo,
DataScope = DataScopeEnum.Self,
Status = StatusEnum.Enable,
Remark = "S7 成品检验流程角色(租户本地)",
CreateTime = now,
}).ExecuteCommand();
}
}
}
///
/// 把「用户在 A 租户、却绑着默认租户 S7 角色」的既有授权事实,等价复制成该用户自己租户内的本地绑定。
/// 只复制**已经存在**的授权事实,不新增任何业务成员、不猜谁该当主管;
/// legacy 绑定保留不动(COPY 不是 MOVE,7 条 Running 仍依赖它)。
///
private static void CopyLegacyCrossTenantMembership(ISqlSugarClient db)
{
foreach (var spec in RoleSpecs)
{
var memberIds = db.Queryable()
.ClearFilter()
.Where(ur => ur.RoleId == spec.LegacyDefaultTenantRoleId)
.Select(ur => ur.UserId)
.ToList();
if (memberIds.Count == 0) continue;
var crossTenantMembers = db.Queryable()
.ClearFilter()
.Where(u => memberIds.Contains(u.Id) && u.TenantId != SqlSugarConst.DefaultTenantId)
.ToList();
foreach (var user in crossTenantMembers)
{
if (user.TenantId is not > 0) continue; // 无租户归属的用户不产生本地绑定
var localRoleId = ResolveLocalRoleId(db, user.TenantId.Value, spec.Code);
if (localRoleId is not > 0) continue;
// SysUserRole 无 (UserId, RoleId) 唯一约束,防重只能靠这里自己做。
var bound = db.Queryable()
.ClearFilter()
.Any(x => x.UserId == user.Id && x.RoleId == localRoleId.Value);
if (bound) continue;
db.Insertable(new SysUserRole
{
Id = YitIdHelper.NextId(),
UserId = user.Id,
RoleId = localRoleId.Value,
}).ExecuteCommand();
}
}
}
/// 按 (TenantId, Code) 取租户本地角色 Id;不存在返回 null。
public static long? ResolveLocalRoleId(ISqlSugarClient db, long tenantId, string code)
{
return db.Queryable()
.ClearFilter()
.Where(r => r.TenantId == tenantId && r.Code == code && r.Status == StatusEnum.Enable)
.Select(r => r.Id)
.First();
}
///
/// Activation Gate:只有三个 S7 角色在该租户内**都存在本地角色且各至少 1 名同租户成员**时,
/// 才允许为该租户发布 RoleCode 版流程定义。
///
/// 这道门禁的意义:不得把一个「目前还靠 legacy 跨租户 RoleId 正常工作」的租户,迁成 0 审批人。
/// 成员不齐的租户维持原状(继续命中 legacy 定义),等它齐了下次启动自动补发。
///
public static List GetActivatableTenantIds(ISqlSugarClient db)
{
var ready = new List();
foreach (var tenantId in ResolveTenantIds(db))
{
var allReady = true;
foreach (var spec in RoleSpecs)
{
var roleId = ResolveLocalRoleId(db, tenantId, spec.Code);
if (roleId is not > 0) { allReady = false; break; }
var memberIds = db.Queryable()
.ClearFilter()
.Where(ur => ur.RoleId == roleId.Value)
.Select(ur => ur.UserId)
.ToList();
if (memberIds.Count == 0) { allReady = false; break; }
// 成员必须与角色同租户——审批人解析最终按 SysUser.TenantId 过滤,
// 只数绑定行会把「有绑定但没有同租户成员」误判为 ready。
var sameTenantMembers = db.Queryable()
.ClearFilter()
.Count(u => memberIds.Contains(u.Id) && u.TenantId == tenantId);
if (sameTenantMembers == 0) { allReady = false; break; }
}
if (allReady) ready.Add(tenantId);
}
return ready;
}
///
/// 租户清单一律取自项目正式注册表 SysTenant(含默认租户),不自行构造、不按 SysOrg 数量机械展开
/// (按 Org 播种正是 S7 原实现产生 57 条定义、且多条定义租户与业务租户错配的成因)。
///
private static List ResolveTenantIds(ISqlSugarClient db)
{
var ids = db.Queryable()
.ClearFilter()
.Where(x => x.Id > 1)
.Select(x => x.Id)
.ToList();
if (!ids.Contains(SqlSugarConst.DefaultTenantId))
ids.Add(SqlSugarConst.DefaultTenantId);
return ids.Distinct().ToList();
}
}