using Admin.NET.Plugin.AiDOP.Const.S8; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Service.S8; namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8; /// 认领资格判定结果。拒绝时带原因码,让「没权限」与「不负责这条规则」可分辨。 public sealed record S8ClaimEligibilityResult(bool Allowed, string? ReasonCode, string? Message) { public static S8ClaimEligibilityResult Pass() => new(true, null, null); public static S8ClaimEligibilityResult Deny(string reason, string message) => new(false, reason, message); } /// 认领被拒的原因码。 public static class S8ClaimDenyReason { /// 账号没有「认领」这个动作的权限(角色维度)。 public const string NoActionPermission = "claim_no_action_permission"; /// 账号不在这条规则的处理账号池里(责任维度)。 public const string NotInHandlerPool = "claim_not_in_handler_pool"; } /// /// S8-HANDLER-POOL-1:「这个账号现在能不能认领这条异常」的唯一判定实现。 /// /// 三个正交维度,缺一不可: /// /// 动作权限:角色是否被授予 EXCEPTION_CLAIM(由 回答); /// 规则责任:账号是否在该异常来源规则的处理池里(本类回答); /// 状态机:单据当前状态是否允许认领 + 是否已被人抢走 /// (由 S8TaskFlowService 的原子 UPDATE 回答,本类不重复实现)。 /// /// 把三者混在一处判断是本仓已经踩过的坑:一旦某一维度被"顺手"放宽, /// 另外两维看起来仍然正常,缺陷只在特定组合下出现。 /// /// 人工提报的异常没有来源规则SourceRuleCode 为空)。 /// 这类单据不受责任池约束 —— 池是"谁负责这条规则",而它根本不来自任何规则。 /// 若在这里一刀切要求入池,人工提报的异常将永远无人可认领。 /// public interface IS8ClaimEligibility { Task CheckAsync(long tenantId, long userId, AdoS8Exception exception); } /// 正式实现。生产代码里只有这一份;接口只为测试留 seam。 public sealed class S8ClaimEligibility : IS8ClaimEligibility, ITransient { private readonly IS8ExceptionActionAuthorizer _authorizer; private readonly IS8RuleHandlerPoolReader _handlerPool; public S8ClaimEligibility( IS8ExceptionActionAuthorizer authorizer, IS8RuleHandlerPoolReader handlerPool) { _authorizer = authorizer; _handlerPool = handlerPool; } public async Task CheckAsync(long tenantId, long userId, AdoS8Exception exception) { // ① 动作权限。Controller 上的 [S8ExceptionAction] 已经拦过一次, // 这里再判一次是因为本方法也被"我能不能认领"的前端探测接口复用 —— // 两处都必须给出同一个答案,否则按钮显示可点、点了却 403。 var auth = await _authorizer.AuthorizeAsync(tenantId, userId, S8ExceptionActionCode.Claim); if (!auth.Allowed) return S8ClaimEligibilityResult.Deny( S8ClaimDenyReason.NoActionPermission, "没有权限执行该操作"); // ② 规则责任。人工提报无来源规则,不受池约束(见接口注释)。 var ruleCode = exception.SourceRuleCode; if (string.IsNullOrWhiteSpace(ruleCode)) return S8ClaimEligibilityResult.Pass(); if (!await _handlerPool.IsMemberAsync(tenantId, ruleCode!, userId)) return S8ClaimEligibilityResult.Deny( S8ClaimDenyReason.NotInHandlerPool, "你不是该规则的处理人员,无法认领。如需处理请联系管理员将你加入该规则的处理账号池。"); return S8ClaimEligibilityResult.Pass(); } }