using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Const.S8; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Microsoft.Extensions.Logging; namespace Admin.NET.Plugin.AiDOP.Service.S8; /// /// 三种责任池的统一读侧 seam。 /// /// 就绪门禁、复核人候选、升级收件人、通知解析都只需要读。让它们依赖这一个窄接口 /// 而不是"处理池一个接口、复核池另一个接口",是为了让存储差异只存在一处: /// HANDLER 在存量表、REVIEWER/ESCALATION 在新表,调用方一律不需要知道。 /// public interface IS8RuleResponsibilityReader { /// 池成员账号 Id(原样返回,含可能已失效的历史成员)。 Task> GetMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType); /// 池成员(含展示信息与有效性标记)。失效成员照样返回、只标 Valid=false Task> GetMembersAsync(long tenantId, string ruleCode, string responsibilityType); /// /// 当前真正可用的成员账号 Id(同租户 + 启用)。 /// 运行期解析(升级通知、复核候选)一律用这个,不用 —— /// 把已停用的人算进收件人,会得到一次"发送成功但没人收到"的静默失败。 /// Task> GetValidMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType); /// 该账号是否属于这条规则的某类责任池(停用 / 跨租户成员一律不算数)。 Task IsMemberAsync(long tenantId, string ruleCode, string responsibilityType, long userId); } /// /// S8-RESPONSIBILITY-POOL-1:复核池 / 升级池的唯一读写实现,并作为三种责任池的统一入口。 /// /// HANDLER 内部路由到存量实现), /// 本服务不复制第二份处理池逻辑 —— 两份人员判据迟早不一致,而不一致的那一刻没有任何报错。 /// /// 成员合法性一律复用 : /// 池成员、认领人、复核人、升级收件人必须用同一把尺子,否则会出现 /// 「配得进池、运行时又被拒」这种管理员完全无法自查的状态。 /// public class S8RuleResponsibilityPoolService : IS8RuleResponsibilityReader, ITransient { private readonly SqlSugarRepository _rep; private readonly IS8RuleHandlerPoolReader _handlerReader; private readonly S8RuleHandlerPoolService _handlerWriter; private readonly IS8UserScopeValidator _userScope; private readonly UserManager _userManager; private readonly ILogger _logger; public S8RuleResponsibilityPoolService( SqlSugarRepository rep, IS8RuleHandlerPoolReader handlerReader, S8RuleHandlerPoolService handlerWriter, IS8UserScopeValidator userScope, UserManager userManager, ILogger logger) { _rep = rep; _handlerReader = handlerReader; _handlerWriter = handlerWriter; _userScope = userScope; _userManager = userManager; _logger = logger; } // ────────────────────────────── 读 ────────────────────────────── public async Task> GetMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType) { if (tenantId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return Array.Empty(); if (IsHandler(responsibilityType)) return await _handlerReader.GetMemberIdsAsync(tenantId, ruleCode); if (!S8ResponsibilityCatalog.IsStoredInResponsibilityTable(responsibilityType)) return Array.Empty(); return await _rep.AsQueryable().ClearFilter() .Where(x => x.TenantId == tenantId && x.RuleCode == ruleCode && x.ResponsibilityType == responsibilityType) .Select(x => x.UserId) .ToListAsync(); } public async Task> GetMembersAsync(long tenantId, string ruleCode, string responsibilityType) { if (IsHandler(responsibilityType)) return await _handlerReader.GetMembersAsync(tenantId, ruleCode); var ids = await GetMemberIdsAsync(tenantId, ruleCode, responsibilityType); if (ids.Count == 0) return new List(); var valid = (await _userScope.ValidateUsersAsync(tenantId, ids)).ToDictionary(u => u.UserId); // 失效成员照样返回、只标 Valid=false,不静默过滤:账号被停用或移出租户之后, // 管理员必须能在页面上看见"这里有一个人已经不算数了",而不是眼看着池子无声地少一个人。 return ids.Distinct().Select(id => valid.TryGetValue(id, out var u) ? new S8HandlerUserDto { UserId = id, Account = u.Account, DisplayName = u.DisplayName, Valid = true } : new S8HandlerUserDto { UserId = id, Account = string.Empty, DisplayName = "账号已停用或不在本租户", Valid = false }) .ToList(); } public async Task> GetValidMemberIdsAsync(long tenantId, string ruleCode, string responsibilityType) { var members = await GetMembersAsync(tenantId, ruleCode, responsibilityType); return members.Where(m => m.Valid).Select(m => m.UserId).ToList(); } public async Task IsMemberAsync(long tenantId, string ruleCode, string responsibilityType, long userId) { if (IsHandler(responsibilityType)) return await _handlerReader.IsMemberAsync(tenantId, ruleCode, userId); if (tenantId <= 0 || userId <= 0 || string.IsNullOrWhiteSpace(ruleCode)) return false; if (!S8ResponsibilityCatalog.IsStoredInResponsibilityTable(responsibilityType)) return false; var exists = await _rep.AsQueryable().ClearFilter() .Where(x => x.TenantId == tenantId && x.RuleCode == ruleCode && x.ResponsibilityType == responsibilityType && x.UserId == userId) .AnyAsync(); if (!exists) return false; // 表里有行 ≠ 现在还算数。判据与运行时完全同源。 return await _userScope.FindActiveUserAsync(tenantId, userId) != null; } // ────────────────────────────── 写 ────────────────────────────── /// /// 全量替换某条规则某一类责任池的成员。 /// /// 成员必须是本租户的启用账号 —— 跨租户与不存在共用同一文案,不泄漏存在性。 /// public async Task SetMembersAsync(S8TrustedScope scope, string ruleCode, string responsibilityType, IEnumerable? userIds) { if (string.IsNullOrWhiteSpace(ruleCode)) throw new S8BizException("规则编码不能为空"); if (!S8ResponsibilityCatalog.IsKnown(responsibilityType)) throw new S8BizException($"未知的责任类型:{responsibilityType}"); // 处理池仍由存量服务写,保持唯一写入口,不在这里复制一份写逻辑。 if (IsHandler(responsibilityType)) { await _handlerWriter.SetMembersAsync(scope, ruleCode, userIds); return; } var wanted = (userIds ?? Enumerable.Empty()).Where(x => x > 0).Distinct().ToList(); if (wanted.Count > 0) { var validated = await _userScope.ValidateUsersAsync(scope.TenantId, wanted); var validIds = validated.Select(u => u.UserId).ToHashSet(); if (wanted.Any(id => !validIds.Contains(id))) throw new S8BizException("所选账号不存在或已停用"); } var existing = await _rep.AsQueryable().ClearFilter() .Where(x => x.TenantId == scope.TenantId && x.RuleCode == ruleCode && x.ResponsibilityType == responsibilityType) .ToListAsync(); var existingIds = existing.Select(x => x.UserId).ToHashSet(); var toRemove = existing.Where(x => !wanted.Contains(x.UserId)).Select(x => x.Id).ToList(); var toAdd = wanted.Where(id => !existingIds.Contains(id)).ToList(); if (toRemove.Count > 0) await _rep.AsDeleteable().Where(x => toRemove.Contains(x.Id)).ExecuteCommandAsync(); if (toAdd.Count > 0) { var now = DateTime.Now; await _rep.AsInsertable(toAdd.Select(id => new AdoS8RuleResponsibilityUser { TenantId = scope.TenantId, RuleCode = ruleCode, ResponsibilityType = responsibilityType, UserId = id, CreatedAt = now, CreatedBy = _userManager.Account }).ToList()).ExecuteCommandAsync(); } _logger.LogInformation( "s8_rule_responsibility_pool_updated tenant={Tenant} rule={Rule} type={Type} added={Added} removed={Removed} total={Total}", scope.TenantId, ruleCode, responsibilityType, toAdd.Count, toRemove.Count, wanted.Count); } private static bool IsHandler(string? type) => string.Equals(type, S8ResponsibilityType.Handler, StringComparison.OrdinalIgnoreCase); }