S8RuleReadinessGate.cs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  3. /// <summary>就绪门禁的失败原因码。与 <see cref="DataAccess.S8DatasetReasonCode"/> 分开:那一族说的是「数据集能不能取数」。</summary>
  4. public static class S8RuleReadinessReasonCode
  5. {
  6. /// <summary>规则声明为启用必需的运行参数尚未配置。</summary>
  7. public const string RequiredParameterMissing = "rule_required_parameter_missing";
  8. /// <summary>已配置的部门不存在于当前租户(或已停用)。</summary>
  9. public const string DepartmentNotInTenant = "rule_department_not_in_tenant";
  10. }
  11. /// <summary>就绪判定结果。<see cref="Ok"/> 为 true 时另外两项为 null。</summary>
  12. public sealed class S8RuleReadinessCheck
  13. {
  14. private S8RuleReadinessCheck(bool ok, string? reasonCode, string? message)
  15. {
  16. Ok = ok;
  17. ReasonCode = reasonCode;
  18. Message = message;
  19. }
  20. public bool Ok { get; }
  21. public string? ReasonCode { get; }
  22. public string? Message { get; }
  23. public static S8RuleReadinessCheck Pass() => new(true, null, null);
  24. public static S8RuleReadinessCheck Fail(string reasonCode, string message) => new(false, reasonCode, message);
  25. }
  26. /// <summary>
  27. /// S8-RULE-READINESS-1:规则「能不能真的跑」的统一门禁。
  28. ///
  29. /// <para><b>要解决的问题</b>:Rule 01 的判定语义里没有部门 —— <c>dwd_supplier_delivery</c>
  30. /// 根本没有部门列,命中行也就带不出部门。于是建单时部门只能来自运行参数
  31. /// <c>defaultOccurrenceDeptId</c> / <c>defaultResponsibleDeptId</c>。没配这两项时,
  32. /// 规则会「启用成功、调度成功、一条异常都建不出来」:
  33. /// <c>tick_rule_done ... status=SUCCESS hits=28 created=0 failed=1</c>,
  34. /// 页面显示「已启用 / 最近结果:成功」。这是最难发现的一类失败 —— 每一层都报成功。</para>
  35. ///
  36. /// <para><b>三条真实执行入口必须共用本门禁</b>:<c>EnableAsync</c> · <c>RunNowAsync</c> ·
  37. /// <c>S8WatchSchedulerService.RunSingleRuleAsync</c>。少接一条就等于留了旁路:
  38. /// 启用被挡但 RunNow 能跑、或启用时合法而部门事后被停用、调度器照样进 evaluator 再 create_failed。
  39. /// Scheduler 侧必须<b>在 evaluator 之前</b> fail-fast,不进入异常创建。</para>
  40. ///
  41. /// <para><b>Preview 刻意不接</b>:预演只做只读试算、不建任何异常,
  42. /// 部门缺失不影响它的正确性。管理员本来就该「先预演看命中,再配部门,再启用」。</para>
  43. ///
  44. /// <para><b>不是全局规则</b>:是否必需由 <see cref="Definitions.S8RuleParameterPolicy.RequiresDepartmentDefaultsForEnable"/>
  45. /// 逐规则声明,默认 <c>false</c>。将来某条规则的数据集自带部门列,它就不需要这两个参数。
  46. /// 在 S8 全局写死「所有规则必须配部门」会把那类规则一起误伤。</para>
  47. ///
  48. /// <para><b>参数不重新解析</b>:只消费调用方传入的 <see cref="S8EffectiveRule"/>
  49. /// (其 <c>Parameters</c> 已由 <see cref="S8RuleRuntimeParameters.Resolve"/> 解析)。
  50. /// 门禁自己再解一次 <c>params_json</c> 就会出现「门禁看到的值」与「evaluator 用的值」不同源,
  51. /// 那是另一种形式的静默背离。</para>
  52. /// </summary>
  53. public sealed class S8RuleReadinessGate : ITransient
  54. {
  55. private readonly IS8DepartmentScopeValidator _deptValidator;
  56. public S8RuleReadinessGate(IS8DepartmentScopeValidator deptValidator) => _deptValidator = deptValidator;
  57. /// <summary>按生效形态判定就绪。不抛异常,由调用方决定失败语义(400 / run-result)。</summary>
  58. public async Task<S8RuleReadinessCheck> CheckAsync(S8EffectiveRule effective, long tenantId)
  59. {
  60. var policy = effective.Definition.Parameters;
  61. // 未声明「启用必需部门」的规则不受本门禁约束。
  62. if (policy is not { RequiresDepartmentDefaultsForEnable: true })
  63. return S8RuleReadinessCheck.Pass();
  64. var occurrence = effective.Parameters.DefaultOccurrenceDeptId;
  65. var responsible = effective.Parameters.DefaultResponsibleDeptId;
  66. // ① 未配置 —— 与「配了但非法」分开报,管理员需要知道到底是哪一种。
  67. if (!occurrence.HasValue)
  68. return S8RuleReadinessCheck.Fail(
  69. S8RuleReadinessReasonCode.RequiredParameterMissing,
  70. "启用失败:请先配置默认发生部门。");
  71. if (!responsible.HasValue)
  72. return S8RuleReadinessCheck.Fail(
  73. S8RuleReadinessReasonCode.RequiredParameterMissing,
  74. "启用失败:请先配置默认责任部门。");
  75. // ② 配了但不属当前租户(或已停用)。判据与运行时建单同源,见 S8DepartmentScopeValidator。
  76. if (!await _deptValidator.ExistsInTenantAsync(occurrence, tenantId))
  77. return S8RuleReadinessCheck.Fail(
  78. S8RuleReadinessReasonCode.DepartmentNotInTenant,
  79. "启用失败:默认发生部门不属于当前租户。");
  80. if (!await _deptValidator.ExistsInTenantAsync(responsible, tenantId))
  81. return S8RuleReadinessCheck.Fail(
  82. S8RuleReadinessReasonCode.DepartmentNotInTenant,
  83. "启用失败:默认责任部门不属于当前租户。");
  84. return S8RuleReadinessCheck.Pass();
  85. }
  86. /// <summary>启用 / 立即执行路径用这一个:不就绪即抛 <see cref="S8BizException"/>(→ 400)。</summary>
  87. public async Task EnsureReadyAsync(S8EffectiveRule effective, long tenantId)
  88. {
  89. var result = await CheckAsync(effective, tenantId);
  90. if (!result.Ok)
  91. throw new S8BizException($"[{result.ReasonCode}] {result.Message}");
  92. }
  93. }