using Admin.NET.Core;
using Admin.NET.Plugin.AiDOP.Const.S8;
using Admin.NET.Plugin.AiDOP.Entity.S8;
using Microsoft.Extensions.Logging;
namespace Admin.NET.Plugin.AiDOP.Service.S8;
/// 供给结果。分项计数,运维要能看出「这次到底做了什么」。
public sealed class S8ActionRoleProvisioningResult
{
public int TenantCount { get; set; }
public int ActionCount { get; set; }
/// 新建的授权行数。
public int CreatedCount { get; set; }
/// 已有授权、本次跳过的租户数(只对完全没有任何授权行的租户做首次供给)。
public int SkippedTenantCount { get; set; }
/// 推导后仍然一个角色都没有的 (租户, 动作) 数 —— 这些动作保持关闭,需人工配置。
public int UnconfiguredCount { get; set; }
}
///
/// S8-ACTION-PERMISSION-1:把「本租户原有的 S8 权限语义」一次性投射成动作授权矩阵的初值。
///
/// 只做首次供给:一个租户只要已经存在任意一行授权,本服务就整体跳过它。
/// 理由是这张表随后由管理员在页面上维护 —— 每次启动都去"补齐默认"会把管理员
/// 刻意取消的授权悄悄加回来,那是比缺权限更难发现的故障(页面显示没配,实际有权)。
///
/// 跨租户角色不带入:推导链是
/// SysRole(TenantId == 目标租户) → SysRoleMenu → SysMenu(Btn).Permission。
/// 真库里 UATAdminA 经别家租户的 ROLE_S6_IPQC_SUPERVISOR 持有
/// s8:exception:assign,该角色因租户不符不会进入本租户的转派授权 ——
/// 迁移后它失去转派能力属预期修复结果。
///
/// 推不出角色的动作留空(如上述租户的「转派」),保持 fail closed。
/// 不给兜底角色:那等于把刚堵上的越权换个形式放回去。
///
public class S8ExceptionActionRoleProvisioningService : ITransient
{
private readonly SqlSugarRepository _rep;
private readonly ILogger _logger;
public S8ExceptionActionRoleProvisioningService(
SqlSugarRepository rep,
ILogger logger)
{
_rep = rep;
_logger = logger;
}
public async Task SyncAsync(CancellationToken ct = default)
{
// 「租户有效」沿用本仓既有口径(Status = Enable),与 S8RuleProvisioningService 一致。
var tenantIds = await _rep.Context.Queryable()
.Where(t => t.Status == StatusEnum.Enable)
.Select(t => t.Id)
.ToListAsync(ct);
var result = new S8ActionRoleProvisioningResult
{
TenantCount = tenantIds.Count,
ActionCount = S8ExceptionActionCatalog.All.Count
};
if (tenantIds.Count == 0) return result;
var tenantsWithRows = (await _rep.AsQueryable().ClearFilter()
.Where(x => tenantIds.Contains(x.TenantId))
.Select(x => x.TenantId)
.Distinct()
.ToListAsync(ct)).ToHashSet();
var pending = tenantIds.Where(t => !tenantsWithRows.Contains(t)).ToList();
result.SkippedTenantCount = tenantIds.Count - pending.Count;
if (pending.Count == 0)
{
_logger.LogInformation(
"s8_action_role_provisioning_skipped tenants={Tenants}(全部已有授权配置)", tenantIds.Count);
return result;
}
// 一次取回待供给租户的 (租户, 角色, 能力码) 三元组。角色租户在 join 条件里就已经限死。
var grants = await _rep.Context.Queryable().ClearFilter()
.InnerJoin((r, rm) => r.Id == rm.RoleId)
.InnerJoin((r, rm, m) => rm.MenuId == m.Id)
.Where((r, rm, m) => r.TenantId != null && pending.Contains(r.TenantId.Value)
&& m.Type == MenuTypeEnum.Btn
&& m.Permission != null
&& m.Permission.StartsWith("s8:"))
.Select((r, rm, m) => new { TenantId = r.TenantId!.Value, RoleId = r.Id, m.Permission })
.ToListAsync(ct);
var now = DateTime.Now;
var inserts = new List();
foreach (var tenantId in pending)
{
var tenantGrants = grants.Where(g => g.TenantId == tenantId).ToList();
foreach (var action in S8ExceptionActionCatalog.All)
{
var codes = new List();
if (!string.IsNullOrWhiteSpace(action.LegacyPermissionCode)) codes.Add(action.LegacyPermissionCode!);
codes.AddRange(action.LegacyPermissionAliases);
// 旧体系里没有对应能力码的动作(ViewAll)无从推导 —— 留空,由管理员显式配置。
if (codes.Count == 0) { result.UnconfiguredCount++; continue; }
var roleIds = tenantGrants
.Where(g => codes.Contains(g.Permission!, StringComparer.OrdinalIgnoreCase))
.Select(g => g.RoleId)
.Distinct()
.ToList();
if (roleIds.Count == 0) { result.UnconfiguredCount++; continue; }
inserts.AddRange(roleIds.Select(rid => new AdoS8ExceptionActionRole
{
TenantId = tenantId,
ActionCode = action.Code,
RoleId = rid,
CreatedAt = now,
CreatedBy = "PROVISIONING"
}));
}
}
if (inserts.Count > 0)
{
await _rep.AsInsertable(inserts).ExecuteCommandAsync();
result.CreatedCount = inserts.Count;
}
_logger.LogInformation(
"s8_action_role_provisioning_done tenants={Tenants} provisioned={Pending} skipped={Skipped} "
+ "actions={Actions} created={Created} unconfigured={Unconfigured}",
result.TenantCount, pending.Count, result.SkippedTenantCount,
result.ActionCount, result.CreatedCount, result.UnconfiguredCount);
if (result.UnconfiguredCount > 0)
_logger.LogWarning(
"s8_action_role_unconfigured count={Count};这些 (租户, 动作) 推导不出任何本租户角色,"
+ "按 fail-closed 保持关闭,需管理员在「异常操作权限」页面显式配置",
result.UnconfiguredCount);
return result;
}
}