S8RuleResponsibilityPoolService.cs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. using Admin.NET.Core;
  2. using Admin.NET.Plugin.AiDOP.Const.S8;
  3. using Admin.NET.Plugin.AiDOP.Entity.S8;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure;
  5. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  6. using Microsoft.Extensions.Logging;
  7. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  8. /// <summary>
  9. /// 三种责任池的<b>统一</b>读侧 seam。
  10. ///
  11. /// <para>就绪门禁、复核人候选、升级收件人、通知解析都只需要读。让它们依赖这一个窄接口
  12. /// 而不是"处理池一个接口、复核池另一个接口",是为了让<b>存储差异只存在一处</b>:
  13. /// HANDLER 在存量表、REVIEWER/ESCALATION 在新表,调用方一律不需要知道。</para>
  14. /// </summary>
  15. public interface IS8RuleResponsibilityReader
  16. {
  17. /// <summary>池成员账号 Id(原样返回,含可能已失效的历史成员)。</summary>
  18. Task<IReadOnlyList<long>> GetMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType);
  19. /// <summary>池成员(含展示信息与有效性标记)。失效成员照样返回、只标 <c>Valid=false</c>。</summary>
  20. Task<List<S8HandlerUserDto>> GetMembersAsync(long tenantId, string ruleCode, string responsibilityType);
  21. /// <summary>
  22. /// 当前<b>真正可用</b>的成员账号 Id(同租户 + 启用)。
  23. /// <para>运行期解析(升级通知、复核候选)一律用这个,不用 <see cref="GetMemberIdsAsync"/> ——
  24. /// 把已停用的人算进收件人,会得到一次"发送成功但没人收到"的静默失败。</para>
  25. /// </summary>
  26. Task<IReadOnlyList<long>> GetValidMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType);
  27. /// <summary>该账号是否属于这条规则的某类责任池(停用 / 跨租户成员一律不算数)。</summary>
  28. Task<bool> IsMemberAsync(long tenantId, string ruleCode, string responsibilityType, long userId);
  29. }
  30. /// <summary>
  31. /// S8-RESPONSIBILITY-POOL-1:复核池 / 升级池的唯一读写实现,并作为三种责任池的统一入口。
  32. ///
  33. /// <para><b>HANDLER 内部路由到存量实现</b>(<see cref="IS8RuleHandlerPoolReader"/>),
  34. /// 本服务不复制第二份处理池逻辑 —— 两份人员判据迟早不一致,而不一致的那一刻没有任何报错。</para>
  35. ///
  36. /// <para><b>成员合法性一律复用 <see cref="IS8UserScopeValidator"/></b>:
  37. /// 池成员、认领人、复核人、升级收件人必须用同一把尺子,否则会出现
  38. /// 「配得进池、运行时又被拒」这种管理员完全无法自查的状态。</para>
  39. /// </summary>
  40. public class S8RuleResponsibilityPoolService : IS8RuleResponsibilityReader, ITransient
  41. {
  42. private readonly SqlSugarRepository<AdoS8RuleResponsibilityUser> _rep;
  43. private readonly IS8RuleHandlerPoolReader _handlerReader;
  44. private readonly S8RuleHandlerPoolService _handlerWriter;
  45. private readonly IS8UserScopeValidator _userScope;
  46. private readonly UserManager _userManager;
  47. private readonly ILogger<S8RuleResponsibilityPoolService> _logger;
  48. public S8RuleResponsibilityPoolService(
  49. SqlSugarRepository<AdoS8RuleResponsibilityUser> rep,
  50. IS8RuleHandlerPoolReader handlerReader,
  51. S8RuleHandlerPoolService handlerWriter,
  52. IS8UserScopeValidator userScope,
  53. UserManager userManager,
  54. ILogger<S8RuleResponsibilityPoolService> logger)
  55. {
  56. _rep = rep;
  57. _handlerReader = handlerReader;
  58. _handlerWriter = handlerWriter;
  59. _userScope = userScope;
  60. _userManager = userManager;
  61. _logger = logger;
  62. }
  63. // ────────────────────────────── 读 ──────────────────────────────
  64. public async Task<IReadOnlyList<long>> GetMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType)
  65. {
  66. if (tenantId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return Array.Empty<long>();
  67. if (IsHandler(responsibilityType))
  68. return await _handlerReader.GetMemberIdsAsync(tenantId, ruleCode);
  69. if (!S8ResponsibilityCatalog.IsStoredInResponsibilityTable(responsibilityType))
  70. return Array.Empty<long>();
  71. return await _rep.AsQueryable().ClearFilter()
  72. .Where(x => x.TenantId == tenantId
  73. && x.RuleCode == ruleCode
  74. && x.ResponsibilityType == responsibilityType)
  75. .Select(x => x.UserId)
  76. .ToListAsync();
  77. }
  78. public async Task<List<S8HandlerUserDto>> GetMembersAsync(long tenantId, string ruleCode, string responsibilityType)
  79. {
  80. if (IsHandler(responsibilityType))
  81. return await _handlerReader.GetMembersAsync(tenantId, ruleCode);
  82. var ids = await GetMemberIdsAsync(tenantId, ruleCode, responsibilityType);
  83. if (ids.Count == 0) return new List<S8HandlerUserDto>();
  84. var valid = (await _userScope.ValidateUsersAsync(tenantId, ids)).ToDictionary(u => u.UserId);
  85. // 失效成员照样返回、只标 Valid=false,不静默过滤:账号被停用或移出租户之后,
  86. // 管理员必须能在页面上看见"这里有一个人已经不算数了",而不是眼看着池子无声地少一个人。
  87. return ids.Distinct().Select(id => valid.TryGetValue(id, out var u)
  88. ? new S8HandlerUserDto { UserId = id, Account = u.Account, DisplayName = u.DisplayName, Valid = true }
  89. : new S8HandlerUserDto { UserId = id, Account = string.Empty, DisplayName = "账号已停用或不在本租户", Valid = false })
  90. .ToList();
  91. }
  92. public async Task<IReadOnlyList<long>> GetValidMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType)
  93. {
  94. var members = await GetMembersAsync(tenantId, ruleCode, responsibilityType);
  95. return members.Where(m => m.Valid).Select(m => m.UserId).ToList();
  96. }
  97. public async Task<bool> IsMemberAsync(long tenantId, string ruleCode, string responsibilityType, long userId)
  98. {
  99. if (IsHandler(responsibilityType))
  100. return await _handlerReader.IsMemberAsync(tenantId, ruleCode, userId);
  101. if (tenantId <= 0 || userId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return false;
  102. if (!S8ResponsibilityCatalog.IsStoredInResponsibilityTable(responsibilityType)) return false;
  103. var exists = await _rep.AsQueryable().ClearFilter()
  104. .Where(x => x.TenantId == tenantId
  105. && x.RuleCode == ruleCode
  106. && x.ResponsibilityType == responsibilityType
  107. && x.UserId == userId)
  108. .AnyAsync();
  109. if (!exists) return false;
  110. // 表里有行 ≠ 现在还算数。判据与运行时完全同源。
  111. return await _userScope.FindActiveUserAsync(tenantId, userId) != null;
  112. }
  113. // ────────────────────────────── 写 ──────────────────────────────
  114. /// <summary>
  115. /// 全量替换某条规则某一类责任池的成员。
  116. ///
  117. /// <para>成员必须是本租户的启用账号 —— <b>跨租户与不存在共用同一文案</b>,不泄漏存在性。</para>
  118. /// </summary>
  119. public async Task SetMembersAsync(S8TrustedScope scope, string ruleCode, string responsibilityType, IEnumerable<long>? userIds)
  120. {
  121. if (string.IsNullOrWhiteSpace(ruleCode)) throw new S8BizException("规则编码不能为空");
  122. if (!S8ResponsibilityCatalog.IsKnown(responsibilityType))
  123. throw new S8BizException($"未知的责任类型:{responsibilityType}");
  124. // 处理池仍由存量服务写,保持唯一写入口,不在这里复制一份写逻辑。
  125. if (IsHandler(responsibilityType))
  126. {
  127. await _handlerWriter.SetMembersAsync(scope, ruleCode, userIds);
  128. return;
  129. }
  130. var wanted = (userIds ?? Enumerable.Empty<long>()).Where(x => x > 0).Distinct().ToList();
  131. if (wanted.Count > 0)
  132. {
  133. var validated = await _userScope.ValidateUsersAsync(scope.TenantId, wanted);
  134. var validIds = validated.Select(u => u.UserId).ToHashSet();
  135. if (wanted.Any(id => !validIds.Contains(id)))
  136. throw new S8BizException("所选账号不存在或已停用");
  137. }
  138. var existing = await _rep.AsQueryable().ClearFilter()
  139. .Where(x => x.TenantId == scope.TenantId
  140. && x.RuleCode == ruleCode
  141. && x.ResponsibilityType == responsibilityType)
  142. .ToListAsync();
  143. var existingIds = existing.Select(x => x.UserId).ToHashSet();
  144. var toRemove = existing.Where(x => !wanted.Contains(x.UserId)).Select(x => x.Id).ToList();
  145. var toAdd = wanted.Where(id => !existingIds.Contains(id)).ToList();
  146. if (toRemove.Count > 0)
  147. await _rep.AsDeleteable().Where(x => toRemove.Contains(x.Id)).ExecuteCommandAsync();
  148. if (toAdd.Count > 0)
  149. {
  150. var now = DateTime.Now;
  151. await _rep.AsInsertable(toAdd.Select(id => new AdoS8RuleResponsibilityUser
  152. {
  153. TenantId = scope.TenantId,
  154. RuleCode = ruleCode,
  155. ResponsibilityType = responsibilityType,
  156. UserId = id,
  157. CreatedAt = now,
  158. CreatedBy = _userManager.Account
  159. }).ToList()).ExecuteCommandAsync();
  160. }
  161. _logger.LogInformation(
  162. "s8_rule_responsibility_pool_updated tenant={Tenant} rule={Rule} type={Type} added={Added} removed={Removed} total={Total}",
  163. scope.TenantId, ruleCode, responsibilityType, toAdd.Count, toRemove.Count, wanted.Count);
  164. }
  165. private static bool IsHandler(string? type) =>
  166. string.Equals(type, S8ResponsibilityType.Handler, StringComparison.OrdinalIgnoreCase);
  167. }