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; } }