S8ClaimEligibility.cs 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. using Admin.NET.Plugin.AiDOP.Const.S8;
  2. using Admin.NET.Plugin.AiDOP.Entity.S8;
  3. using Admin.NET.Plugin.AiDOP.Service.S8;
  4. namespace Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  5. /// <summary>认领资格判定结果。拒绝时带原因码,让「没权限」与「不负责这条规则」可分辨。</summary>
  6. public sealed record S8ClaimEligibilityResult(bool Allowed, string? ReasonCode, string? Message)
  7. {
  8. public static S8ClaimEligibilityResult Pass() => new(true, null, null);
  9. public static S8ClaimEligibilityResult Deny(string reason, string message) => new(false, reason, message);
  10. }
  11. /// <summary>认领被拒的原因码。</summary>
  12. public static class S8ClaimDenyReason
  13. {
  14. /// <summary>账号没有「认领」这个动作的权限(角色维度)。</summary>
  15. public const string NoActionPermission = "claim_no_action_permission";
  16. /// <summary>账号不在这条规则的处理账号池里(责任维度)。</summary>
  17. public const string NotInHandlerPool = "claim_not_in_handler_pool";
  18. }
  19. /// <summary>
  20. /// S8-HANDLER-POOL-1:「这个账号现在能不能认领这条异常」的<b>唯一</b>判定实现。
  21. ///
  22. /// <para><b>三个正交维度,缺一不可</b>:
  23. /// <list type="number">
  24. /// <item><b>动作权限</b>:角色是否被授予 <c>EXCEPTION_CLAIM</c>(由 <see cref="IS8ExceptionActionAuthorizer"/> 回答);</item>
  25. /// <item><b>规则责任</b>:账号是否在该异常来源规则的处理池里(本类回答);</item>
  26. /// <item><b>状态机</b>:单据当前状态是否允许认领 + 是否已被人抢走
  27. /// (由 <c>S8TaskFlowService</c> 的原子 UPDATE 回答,本类不重复实现)。</item>
  28. /// </list>
  29. /// 把三者混在一处判断是本仓已经踩过的坑:一旦某一维度被"顺手"放宽,
  30. /// 另外两维看起来仍然正常,缺陷只在特定组合下出现。</para>
  31. ///
  32. /// <para><b>人工提报的异常没有来源规则</b>(<c>SourceRuleCode</c> 为空)。
  33. /// 这类单据不受责任池约束 —— 池是"谁负责这条规则",而它根本不来自任何规则。
  34. /// 若在这里一刀切要求入池,人工提报的异常将永远无人可认领。</para>
  35. /// </summary>
  36. public interface IS8ClaimEligibility
  37. {
  38. Task<S8ClaimEligibilityResult> CheckAsync(long tenantId, long userId, AdoS8Exception exception);
  39. }
  40. /// <summary>正式实现。生产代码里只有这一份;接口只为测试留 seam。</summary>
  41. public sealed class S8ClaimEligibility : IS8ClaimEligibility, ITransient
  42. {
  43. private readonly IS8ExceptionActionAuthorizer _authorizer;
  44. private readonly IS8RuleHandlerPoolReader _handlerPool;
  45. public S8ClaimEligibility(
  46. IS8ExceptionActionAuthorizer authorizer,
  47. IS8RuleHandlerPoolReader handlerPool)
  48. {
  49. _authorizer = authorizer;
  50. _handlerPool = handlerPool;
  51. }
  52. public async Task<S8ClaimEligibilityResult> CheckAsync(long tenantId, long userId, AdoS8Exception exception)
  53. {
  54. // ① 动作权限。Controller 上的 [S8ExceptionAction] 已经拦过一次,
  55. // 这里再判一次是因为本方法也被"我能不能认领"的前端探测接口复用 ——
  56. // 两处都必须给出同一个答案,否则按钮显示可点、点了却 403。
  57. var auth = await _authorizer.AuthorizeAsync(tenantId, userId, S8ExceptionActionCode.Claim);
  58. if (!auth.Allowed)
  59. return S8ClaimEligibilityResult.Deny(
  60. S8ClaimDenyReason.NoActionPermission, "没有权限执行该操作");
  61. // ② 规则责任。人工提报无来源规则,不受池约束(见接口注释)。
  62. var ruleCode = exception.SourceRuleCode;
  63. if (string.IsNullOrWhiteSpace(ruleCode))
  64. return S8ClaimEligibilityResult.Pass();
  65. if (!await _handlerPool.IsMemberAsync(tenantId, ruleCode!, userId))
  66. return S8ClaimEligibilityResult.Deny(
  67. S8ClaimDenyReason.NotInHandlerPool,
  68. "你不是该规则的处理人员,无法认领。如需处理请联系管理员将你加入该规则的处理账号池。");
  69. return S8ClaimEligibilityResult.Pass();
  70. }
  71. }