S8RuleHandlerPoolService.cs 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  5. using Microsoft.Extensions.Logging;
  6. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  7. /// <summary>人员池成员(展示用)。</summary>
  8. public sealed class S8HandlerUserDto
  9. {
  10. public long UserId { get; set; }
  11. public string Account { get; set; } = string.Empty;
  12. public string DisplayName { get; set; } = string.Empty;
  13. /// <summary>该账号当前是否仍可用(同租户 + 启用)。false = 历史配置已失效,前端需显式提示。</summary>
  14. public bool Valid { get; set; }
  15. }
  16. /// <summary>
  17. /// 责任池的<b>读侧</b> seam。就绪门禁、认领资格、可见性收敛、以及后续的通知收件人解析
  18. /// 都只需要读 —— 让它们依赖这个窄接口而不是整个服务,既能在单测里替身,
  19. /// 也让「谁会写这张表」在类型层面收敛到配置入口一处。
  20. /// </summary>
  21. public interface IS8RuleHandlerPoolReader
  22. {
  23. /// <summary>池成员账号 Id(原样返回,含可能已失效的历史成员)。</summary>
  24. Task<IReadOnlyList<long>> GetMemberIdsAsync(long tenantId, string ruleCode);
  25. /// <summary>池成员(含展示信息与有效性标记)。失效成员照样返回、只标 <c>Valid=false</c>。</summary>
  26. Task<List<S8HandlerUserDto>> GetMembersAsync(long tenantId, string ruleCode);
  27. /// <summary>该账号是否属于这条规则的责任池(停用 / 跨租户成员一律不算数)。</summary>
  28. Task<bool> IsMemberAsync(long tenantId, string ruleCode, long userId);
  29. /// <summary>该账号在本租户负责的全部规则编码(供可见性过滤使用)。</summary>
  30. Task<IReadOnlyList<string>> GetRuleCodesForUserAsync(long tenantId, long userId);
  31. /// <summary>批量取多条规则的池成员(避免规则列表页 N+1)。</summary>
  32. Task<IReadOnlyDictionary<string, List<long>>> GetPoolsAsync(long tenantId, IEnumerable<string> ruleCodes);
  33. }
  34. /// <summary>
  35. /// S8-HANDLER-POOL-1:规则处理账号池的<b>唯一</b>读写实现。
  36. ///
  37. /// <para>它回答的是「哪些账号负责这条规则」,与「哪个角色能做认领这类动作」
  38. /// (<see cref="IS8ExceptionActionAuthorizer"/>)是两个正交问题。
  39. /// 最终允许 = 动作权限 ∩ 规则责任 ∩ 状态机。</para>
  40. ///
  41. /// <para><b>成员合法性复用 <see cref="IS8UserScopeValidator"/></b>,不另写一套判据:
  42. /// 池成员与认领人必须用同一把尺子,否则会出现「配得进池、认领时又被拒」这种
  43. /// 管理员完全无法自查的状态。</para>
  44. /// </summary>
  45. public class S8RuleHandlerPoolService : IS8RuleHandlerPoolReader, ITransient
  46. {
  47. private readonly SqlSugarRepository<AdoS8RuleHandlerUser> _rep;
  48. private readonly IS8UserScopeValidator _userScope;
  49. private readonly UserManager _userManager;
  50. private readonly ILogger<S8RuleHandlerPoolService> _logger;
  51. public S8RuleHandlerPoolService(
  52. SqlSugarRepository<AdoS8RuleHandlerUser> rep,
  53. IS8UserScopeValidator userScope,
  54. UserManager userManager,
  55. ILogger<S8RuleHandlerPoolService> logger)
  56. {
  57. _rep = rep;
  58. _userScope = userScope;
  59. _userManager = userManager;
  60. _logger = logger;
  61. }
  62. // ────────────────────────────── 读 ──────────────────────────────
  63. /// <summary>池成员的账号 Id(原样返回,含可能已失效的历史成员)。</summary>
  64. public async Task<IReadOnlyList<long>> GetMemberIdsAsync(long tenantId, string ruleCode)
  65. {
  66. if (tenantId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return Array.Empty<long>();
  67. return await _rep.AsQueryable().ClearFilter()
  68. .Where(x => x.TenantId == tenantId && x.RuleCode == ruleCode)
  69. .Select(x => x.UserId)
  70. .ToListAsync();
  71. }
  72. /// <summary>
  73. /// 池成员(含展示信息与有效性标记)。
  74. ///
  75. /// <para><b>失效成员照样返回、只标 <c>Valid=false</c></b>,不静默过滤:
  76. /// 账号被停用或移出租户之后,管理员必须能在页面上看见"这里有一个人已经不算数了",
  77. /// 而不是眼看着池子无声地少了一个人。</para>
  78. /// </summary>
  79. public async Task<List<S8HandlerUserDto>> GetMembersAsync(long tenantId, string ruleCode)
  80. {
  81. var ids = await GetMemberIdsAsync(tenantId, ruleCode);
  82. if (ids.Count == 0) return new List<S8HandlerUserDto>();
  83. var valid = (await _userScope.ValidateUsersAsync(tenantId, ids)).ToDictionary(u => u.UserId);
  84. return ids.Distinct().Select(id => valid.TryGetValue(id, out var u)
  85. ? new S8HandlerUserDto { UserId = id, Account = u.Account, DisplayName = u.DisplayName, Valid = true }
  86. : new S8HandlerUserDto { UserId = id, Account = string.Empty, DisplayName = "账号已停用或不在本租户", Valid = false })
  87. .ToList();
  88. }
  89. /// <summary>
  90. /// 该账号是否属于这条规则的责任池。<b>停用 / 跨租户成员一律不算数</b> ——
  91. /// 判据与认领时完全同源,避免"配置里有他、运行时又不认"。
  92. /// </summary>
  93. public async Task<bool> IsMemberAsync(long tenantId, string ruleCode, long userId)
  94. {
  95. if (tenantId <= 0 || userId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return false;
  96. var exists = await _rep.AsQueryable().ClearFilter()
  97. .Where(x => x.TenantId == tenantId && x.RuleCode == ruleCode && x.UserId == userId)
  98. .AnyAsync();
  99. if (!exists) return false;
  100. return await _userScope.FindActiveUserAsync(tenantId, userId) != null;
  101. }
  102. /// <summary>该账号在本租户负责的全部规则编码(供可见性过滤使用)。</summary>
  103. public async Task<IReadOnlyList<string>> GetRuleCodesForUserAsync(long tenantId, long userId)
  104. {
  105. if (tenantId <= 0 || userId <= 0) return Array.Empty<string>();
  106. return await _rep.AsQueryable().ClearFilter()
  107. .Where(x => x.TenantId == tenantId && x.UserId == userId)
  108. .Select(x => x.RuleCode)
  109. .Distinct()
  110. .ToListAsync();
  111. }
  112. /// <summary>批量取多条规则的池成员(避免规则列表页 N+1)。</summary>
  113. public async Task<IReadOnlyDictionary<string, List<long>>> GetPoolsAsync(long tenantId, IEnumerable<string> ruleCodes)
  114. {
  115. var codes = ruleCodes.Where(c => !string.IsNullOrWhiteSpace(c)).Distinct().ToList();
  116. if (tenantId <= 0 || codes.Count == 0)
  117. return new Dictionary<string, List<long>>(StringComparer.OrdinalIgnoreCase);
  118. var rows = await _rep.AsQueryable().ClearFilter()
  119. .Where(x => x.TenantId == tenantId && codes.Contains(x.RuleCode))
  120. .Select(x => new { x.RuleCode, x.UserId })
  121. .ToListAsync();
  122. return rows.GroupBy(x => x.RuleCode, StringComparer.OrdinalIgnoreCase)
  123. .ToDictionary(g => g.Key, g => g.Select(x => x.UserId).Distinct().ToList(), StringComparer.OrdinalIgnoreCase);
  124. }
  125. // ────────────────────────────── 写 ──────────────────────────────
  126. /// <summary>
  127. /// 全量替换某条规则的处理账号池。
  128. ///
  129. /// <para>成员必须是本租户的启用账号 —— 跨租户与不存在共用同一文案,不泄漏存在性。
  130. /// 判据来自 <see cref="IS8UserScopeValidator"/>,与认领 / 转派 / 复检完全同源。</para>
  131. /// </summary>
  132. public async Task SetMembersAsync(S8TrustedScope scope, string ruleCode, IEnumerable<long>? userIds)
  133. {
  134. if (string.IsNullOrWhiteSpace(ruleCode)) throw new S8BizException("规则编码不能为空");
  135. var wanted = (userIds ?? Enumerable.Empty<long>()).Where(x => x > 0).Distinct().ToList();
  136. if (wanted.Count > 0)
  137. {
  138. var validated = await _userScope.ValidateUsersAsync(scope.TenantId, wanted);
  139. var validIds = validated.Select(u => u.UserId).ToHashSet();
  140. if (wanted.Any(id => !validIds.Contains(id)))
  141. throw new S8BizException("所选账号不存在或已停用");
  142. }
  143. var existing = await _rep.AsQueryable().ClearFilter()
  144. .Where(x => x.TenantId == scope.TenantId && x.RuleCode == ruleCode)
  145. .ToListAsync();
  146. var existingIds = existing.Select(x => x.UserId).ToHashSet();
  147. var toRemove = existing.Where(x => !wanted.Contains(x.UserId)).Select(x => x.Id).ToList();
  148. var toAdd = wanted.Where(id => !existingIds.Contains(id)).ToList();
  149. if (toRemove.Count > 0)
  150. await _rep.AsDeleteable().Where(x => toRemove.Contains(x.Id)).ExecuteCommandAsync();
  151. if (toAdd.Count > 0)
  152. {
  153. var now = DateTime.Now;
  154. await _rep.AsInsertable(toAdd.Select(id => new AdoS8RuleHandlerUser
  155. {
  156. TenantId = scope.TenantId,
  157. RuleCode = ruleCode,
  158. UserId = id,
  159. CreatedAt = now,
  160. CreatedBy = _userManager.Account
  161. }).ToList()).ExecuteCommandAsync();
  162. }
  163. _logger.LogInformation(
  164. "s8_rule_handler_pool_updated tenant={Tenant} rule={Rule} added={Added} removed={Removed} total={Total}",
  165. scope.TenantId, ruleCode, toAdd.Count, toRemove.Count, wanted.Count);
  166. }
  167. }