using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
/// 就绪门禁的失败原因码。与 分开:那一族说的是「数据集能不能取数」。
public static class S8RuleReadinessReasonCode
{
/// 规则声明为启用必需的运行参数尚未配置。
public const string RequiredParameterMissing = "rule_required_parameter_missing";
/// 已配置的部门不存在于当前租户(或已停用)。
public const string DepartmentNotInTenant = "rule_department_not_in_tenant";
///
/// 规则没有任何处理账号(S8-HANDLER-POOL-1)。
/// 启用一条没人负责的规则,结果是异常源源不断地建出来、静静躺在列表里没人认领——
/// 而调度器每一轮都报成功。与"配了部门但没人处理"是同一类静默失败,必须在启用前挡住。
///
public const string HandlerPoolEmpty = "rule_handler_pool_empty";
}
/// 就绪判定结果。 为 true 时另外两项为 null。
public sealed class S8RuleReadinessCheck
{
private S8RuleReadinessCheck(bool ok, string? reasonCode, string? message)
{
Ok = ok;
ReasonCode = reasonCode;
Message = message;
}
public bool Ok { get; }
public string? ReasonCode { get; }
public string? Message { get; }
public static S8RuleReadinessCheck Pass() => new(true, null, null);
public static S8RuleReadinessCheck Fail(string reasonCode, string message) => new(false, reasonCode, message);
}
///
/// S8-RULE-READINESS-1:规则「能不能真的跑」的统一门禁。
///
/// 要解决的问题:Rule 01 的判定语义里没有部门 —— dwd_supplier_delivery
/// 根本没有部门列,命中行也就带不出部门。于是建单时部门只能来自运行参数
/// defaultOccurrenceDeptId / defaultResponsibleDeptId。没配这两项时,
/// 规则会「启用成功、调度成功、一条异常都建不出来」:
/// tick_rule_done ... status=SUCCESS hits=28 created=0 failed=1,
/// 页面显示「已启用 / 最近结果:成功」。这是最难发现的一类失败 —— 每一层都报成功。
///
/// S8-HANDLER-POOL-1 追加一条判据:规则必须至少有一个当前有效的处理账号。
/// 少了它,一条没人负责的规则会源源不断建出异常、静静躺在列表里没人认领,
/// 而调度器每一轮都报成功 —— 与"配了部门但没人处理"同属一类静默失败。
///
/// 三条真实执行入口必须共用本门禁:EnableAsync · RunNowAsync ·
/// S8WatchSchedulerService.RunSingleRuleAsync。少接一条就等于留了旁路:
/// 启用被挡但 RunNow 能跑、或启用时合法而部门事后被停用、调度器照样进 evaluator 再 create_failed。
/// Scheduler 侧必须在 evaluator 之前 fail-fast,不进入异常创建。
///
/// Preview 刻意不接:预演只做只读试算、不建任何异常,
/// 部门缺失不影响它的正确性。管理员本来就该「先预演看命中,再配部门,再启用」。
///
/// 不是全局规则:是否必需由
/// 逐规则声明,默认 false。将来某条规则的数据集自带部门列,它就不需要这两个参数。
/// 在 S8 全局写死「所有规则必须配部门」会把那类规则一起误伤。
///
/// 参数不重新解析:只消费调用方传入的
/// (其 Parameters 已由 解析)。
/// 门禁自己再解一次 params_json 就会出现「门禁看到的值」与「evaluator 用的值」不同源,
/// 那是另一种形式的静默背离。
///
public sealed class S8RuleReadinessGate : ITransient
{
private readonly IS8DepartmentScopeValidator _deptValidator;
private readonly IS8RuleHandlerPoolReader _handlerPool;
public S8RuleReadinessGate(
IS8DepartmentScopeValidator deptValidator,
IS8RuleHandlerPoolReader handlerPool)
{
_deptValidator = deptValidator;
_handlerPool = handlerPool;
}
/// 按生效形态判定就绪。不抛异常,由调用方决定失败语义(400 / run-result)。
public async Task CheckAsync(S8EffectiveRule effective, long tenantId)
{
var policy = effective.Definition.Parameters;
// 未声明「启用必需部门」的规则不受本门禁约束。
if (policy is not { RequiresDepartmentDefaultsForEnable: true })
return S8RuleReadinessCheck.Pass();
var occurrence = effective.Parameters.DefaultOccurrenceDeptId;
var responsible = effective.Parameters.DefaultResponsibleDeptId;
// ① 未配置 —— 与「配了但非法」分开报,管理员需要知道到底是哪一种。
if (!occurrence.HasValue)
return S8RuleReadinessCheck.Fail(
S8RuleReadinessReasonCode.RequiredParameterMissing,
"启用失败:请先配置默认发生部门。");
if (!responsible.HasValue)
return S8RuleReadinessCheck.Fail(
S8RuleReadinessReasonCode.RequiredParameterMissing,
"启用失败:请先配置默认责任部门。");
// ② 配了但不属当前租户(或已停用)。判据与运行时建单同源,见 S8DepartmentScopeValidator。
if (!await _deptValidator.ExistsInTenantAsync(occurrence, tenantId))
return S8RuleReadinessCheck.Fail(
S8RuleReadinessReasonCode.DepartmentNotInTenant,
"启用失败:默认发生部门不属于当前租户。");
if (!await _deptValidator.ExistsInTenantAsync(responsible, tenantId))
return S8RuleReadinessCheck.Fail(
S8RuleReadinessReasonCode.DepartmentNotInTenant,
"启用失败:默认责任部门不属于当前租户。");
// ③ S8-HANDLER-POOL-1:至少要有一个**当前有效**的处理账号。
//
// 判据走 GetMembersAsync 而不是「表里有没有行」:账号被停用或移出租户之后,
// 池子里的那一行还在,但那个人已经不能认领了。只数行数会让一条实际上没人能处理的
// 规则通过启用检查 —— 那正是本门禁存在的意义。
//
// Preview 不受此限(见类注释):预演只做只读试算,人员池与它的正确性无关,
// 管理员本来就该「先预演看命中,再配人,再启用」。
var members = await _handlerPool.GetMembersAsync(tenantId, effective.Row.RuleCode ?? string.Empty);
if (members.All(m => !m.Valid))
return S8RuleReadinessCheck.Fail(
S8RuleReadinessReasonCode.HandlerPoolEmpty,
members.Count == 0
? "启用失败:请先配置至少一名处理账号。"
: "启用失败:已配置的处理账号均已停用或不在当前租户,请重新配置。");
return S8RuleReadinessCheck.Pass();
}
/// 启用 / 立即执行路径用这一个:不就绪即抛 (→ 400)。
public async Task EnsureReadyAsync(S8EffectiveRule effective, long tenantId)
{
var result = await CheckAsync(effective, tenantId);
if (!result.Ok)
throw new S8BizException($"[{result.ReasonCode}] {result.Message}");
}
}