using System.Diagnostics;
using Admin.NET.Plugin.AiDOP.Manufacturing;
using Yitter.IdGenerator;
namespace Admin.NET.Plugin.AiDOP.Infrastructure;
///
/// S6-TENANT-AUTHORITY-1:S6 过程检验(IPQC)三类审批角色的**租户本地化**权威基线,
/// 由 (IPQC_INSPECTION)与
/// (S6_PROCESS_INSPECTION)两条链共用。
///
/// 修复的缺陷(SYSTEMIC TENANT CLONE DEFECT):原先三个角色是**全库单例、固定挂默认租户**,
/// 各租户用户靠「跨租户 SysUserRole 绑定」借用;流程定义又把默认租户的**物理 RoleId**
/// 写死进 FlowJson。于是「谁是主管」这件事跨租户共享,且可用性挂在一条无约束、无审计、
/// 可被直连 SQL 单行改动的手工绑定上——该绑定一旦被删,整个租户的 N2 当场解析为 0 人。
///
/// 目标模型(与平台主流一致:ROLE_QC / SMART_OPS_* 等 11 个 Code 均为每租户一份):
/// 每个租户各有自己的 ROLE_S6_IPQC_INSPECTOR / _SUPERVISOR / _QUALITY_ENGINEER,
/// Code 相同、RoleId 不同、成员与角色同租户;FlowJson 以 **RoleCode** 表达审批权威,
/// 由 FlowEngineService 的 Code 分支在目标租户内重新解析。
///
/// 边界:
/// - 默认租户 的那三行**本身就是它的 tenant-local 角色**,
/// 沿用 的既有固定 Id,不重建、不改动。
/// - membership 迁移是 **COPY 不是 MOVE**:既有跨租户绑定原样保留,供历史实例与尚未迁移的
/// legacy 定义继续使用;清理属后续独立批次。
/// - 本文件**不得**出现任何具体租户 Id 或 UAT 账号——只按 SysTenant 注册表通用处理。
///
internal static class S6TenantAuthoritySeed
{
/// 三类 S6 角色:Code / 名称 / 排序号 / 默认租户既有固定 Id。
private static readonly (string Code, string Name, int OrderNo, long LegacyDefaultTenantRoleId)[] RoleSpecs =
{
(IpqcInspectionFlowConst.RoleInspectorCode, "过程检验员", 820, IpqcInspectionFlowConst.RoleInspectorId),
(IpqcInspectionFlowConst.RoleSupervisorCode, "过程检验主管", 821, IpqcInspectionFlowConst.RoleSupervisorId),
(IpqcInspectionFlowConst.RoleQualityEngineerCode, "过程检验质量工程师", 822, IpqcInspectionFlowConst.RoleQualityEngineerId),
};
/// 三个 S6 RoleCode(供流程种子与 Readiness 判定共用)。
public static IReadOnlyList RoleCodes { get; } =
RoleSpecs.Select(x => x.Code).ToList();
///
/// 幂等地建立租户本地角色并复制既有成员语义。两条 S6 链的种子都会调用,重复调用无副作用。
///
public static void EnsureTenantAuthority(ISqlSugarClient db)
{
try
{
EnsureTenantLocalRoles(db);
CopyLegacyCrossTenantMembership(db);
}
catch (Exception ex)
{
Trace.TraceWarning("S6 tenant authority seed: " + ex);
}
}
///
/// 为 SysTenant 注册表中的每个租户建立三个 S6 角色。查重键是 (TenantId, Code)——
/// 原实现用全局 Any(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 = "S6 过程检验流程角色(租户本地)",
CreateTime = now,
}).ExecuteCommand();
}
}
}
///
/// 把「用户在 A 租户、却绑着默认租户 S6 角色」的既有语义,等价复制成该用户自己租户内的本地绑定。
/// 只复制**已经存在**的授权事实,不新增任何业务成员;legacy 绑定保留不动(COPY 不是 MOVE)。
///
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:只有三个 S6 角色在该租户内**都存在本地角色且各至少 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(含默认租户),不自行构造。
///
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();
}
}