Files
lserp_cs_6.0/插件库/Lskj.CommandKernel/CommandDispatcher.cs
T
2026-08-14 14:28:28 +08:00

366 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Lskj.CommandKernel
{
public sealed class SystemClock : ISystemClock
{
public DateTime UtcNow
{
get { return DateTime.UtcNow; }
}
}
public sealed class AllowAllCommandAuthorizer : ICommandAuthorizer
{
public CommandAuthorizationDecision Authorize(
CommandDescriptor descriptor,
CommandExecutionContext context,
bool execution)
{
return CommandAuthorizationDecision.Allow();
}
}
public sealed class NullCommandAuditSink : ICommandAuditSink
{
public void Planned(CommandDescriptor descriptor, CommandPlan plan, CommandExecutionContext context)
{
}
public void Completed(
CommandDescriptor descriptor,
CommandPlan plan,
CommandResult result,
CommandExecutionContext context)
{
}
public void Failed(
CommandDescriptor descriptor,
CommandPlan plan,
Exception exception,
CommandExecutionContext context)
{
}
}
public sealed class CommandDispatcher
{
private static readonly Regex SafeIdempotencyKey = new Regex(
"^[A-Za-z0-9_.:-]{8,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly CommandRegistry _registry;
private readonly ICommandAuditSink _audit;
private readonly IIdempotencyStore _idempotency;
private readonly IConfirmationValidator _confirmation;
private readonly ICommandAuthorizer _authorizer;
private readonly ISystemClock _clock;
public CommandDispatcher(
CommandRegistry registry,
ICommandAuditSink audit,
IIdempotencyStore idempotency,
IConfirmationValidator confirmation,
ICommandAuthorizer authorizer,
ISystemClock clock)
{
if (registry == null) throw new ArgumentNullException("registry");
if (audit == null) throw new ArgumentNullException("audit");
if (idempotency == null) throw new ArgumentNullException("idempotency");
if (confirmation == null) throw new ArgumentNullException("confirmation");
if (authorizer == null) throw new ArgumentNullException("authorizer");
if (clock == null) throw new ArgumentNullException("clock");
_registry = registry;
_audit = audit;
_idempotency = idempotency;
_confirmation = confirmation;
_authorizer = authorizer;
_clock = clock;
}
public CommandPlan Plan(
string commandName,
IDictionary<string, object> input,
CommandExecutionContext context)
{
ValidateContext(context);
ICommandHandler handler = Resolve(commandName);
CommandPlan plan = null;
try
{
EnsureAuthorized(handler.Descriptor, context, false);
CommandInputSchemaValidator.Validate(
handler.Descriptor.InputSchema,
input ?? new Dictionary<string, object>());
string inputFingerprint = CommandInputFingerprint.Create(
handler.Descriptor.Name,
input ?? new Dictionary<string, object>());
plan = handler.Plan(input ?? new Dictionary<string, object>(), context);
if (plan == null)
throw Error("invalid_plan", "命令处理器没有返回执行计划。");
plan.CommandName = handler.Descriptor.Name;
plan.CommandVersion = handler.Descriptor.Version;
plan.Risk = handler.Descriptor.Risk;
plan.OwnerClientSessionId = context.ClientSessionId;
plan.OwnerUserId = context.UserId;
plan.OwnerUserName = context.UserName;
plan.OwnerAccountBook = context.AccountBook;
plan.OwnerSubSystemId = context.SubSystemId;
plan.OwnerDatabaseScopeFingerprint =
context.DatabaseScopeFingerprint;
plan.CorrelationId = context.CorrelationId;
plan.InputFingerprint = inputFingerprint;
// 计划有效期属于调度器安全边界,不能由处理器或本机墙钟另行决定。
// 统一使用注入时钟也让桥协议、确认令牌和过期判断共享同一时间源。
plan.CreatedAtUtc = _clock.UtcNow;
plan.ExpiresAtUtc = plan.CreatedAtUtc.AddMinutes(10);
_audit.Planned(handler.Descriptor, plan, context);
return plan;
}
catch (Exception ex)
{
SafeAuditFailure(handler.Descriptor, plan, ex, context);
throw;
}
}
public CommandResult Execute(CommandPlan plan, CommandExecutionContext context)
{
ValidateExecutionPreconditions(plan, context);
ICommandHandler handler = Resolve(plan.CommandName);
CommandDescriptor descriptor = handler.Descriptor;
bool idempotencyClaimed = false;
try
{
if (descriptor.RequiresIdempotencyKey)
{
if (string.IsNullOrWhiteSpace(context.IdempotencyKey))
throw Error("idempotency_key_required", "写操作必须提供幂等键。");
context.IdempotencyKey = context.IdempotencyKey.Trim();
if (!SafeIdempotencyKey.IsMatch(context.IdempotencyKey))
throw Error(
"idempotency_key_invalid",
"幂等键只允许 8-128 位字母、数字、点、冒号、下划线和连字符。");
IdempotencyClaim claim = _idempotency.Claim(
descriptor.Name,
context.IdempotencyKey,
context,
plan.InputFingerprint);
if (claim == null)
throw Error("idempotency_store_error", "幂等存储没有返回有效状态。");
if (claim.State == IdempotencyClaimState.Replay)
{
CommandResult replay = claim.Result;
if (replay == null)
throw Error("idempotency_store_error", "幂等存储的历史结果无效。");
replay.Replayed = true;
_audit.Completed(descriptor, plan, replay, context);
return replay;
}
if (claim.State == IdempotencyClaimState.InProgress)
throw Error("idempotency_in_progress", "相同业务请求正在执行,请稍后查询结果,禁止重复提交。");
if (claim.State == IdempotencyClaimState.Conflict)
throw Error("idempotency_key_conflict", "同一幂等键已经用于不同业务内容,请生成新的幂等键。");
idempotencyClaimed = true;
}
if (descriptor.RequiresConfirmation)
{
string failureReason;
if (!_confirmation.Validate(plan, context, out failureReason))
throw Error("confirmation_required", failureReason);
}
CommandResult result = handler.Execute(plan, context);
if (result == null)
throw Error("invalid_result", "命令处理器没有返回执行结果。");
if (idempotencyClaimed)
_idempotency.Complete(
descriptor.Name,
context.IdempotencyKey,
context,
plan.InputFingerprint,
result);
_audit.Completed(descriptor, plan, result, context);
return result;
}
catch (Exception ex)
{
if (idempotencyClaimed)
{
try
{
_idempotency.Abandon(
descriptor.Name,
context.IdempotencyKey,
context,
plan.InputFingerprint);
}
catch { }
}
SafeAuditFailure(descriptor, plan, ex, context);
throw;
}
}
public void ValidateExecutionPreconditions(
CommandPlan plan,
CommandExecutionContext context)
{
ValidateContext(context);
ValidatePlanOwnership(plan, context);
ICommandHandler handler = Resolve(plan.CommandName);
CommandDescriptor descriptor = handler.Descriptor;
ValidatePlanDescriptor(plan, descriptor);
EnsureAuthorized(descriptor, context, true);
if (!plan.Valid)
throw Error("plan_invalid", "执行计划校验未通过,不能执行。");
if (_clock.UtcNow > plan.ExpiresAtUtc)
throw Error("plan_expired", "执行计划已过期,请重新生成预览。");
}
public CommandAuthorizationDecision CheckAuthorization(
CommandDescriptor descriptor,
CommandExecutionContext context,
bool execution)
{
ValidateContext(context);
if (descriptor == null)
throw Error("command_descriptor_required", "命令描述不能为空。");
CommandAuthorizationDecision decision = _authorizer.Authorize(descriptor, context, execution);
if (decision != null) return decision;
return CommandAuthorizationDecision.Deny(
"command_access_denied",
"当前 ERP 用户没有执行该命令的权限。");
}
private ICommandHandler Resolve(string commandName)
{
ICommandHandler handler = _registry.Resolve(commandName);
if (handler == null)
throw Error("command_not_found", "未注册命令:" + (commandName ?? string.Empty));
return handler;
}
private static void ValidateContext(CommandExecutionContext context)
{
if (context == null) throw new ArgumentNullException("context");
if (string.IsNullOrWhiteSpace(context.CorrelationId))
throw Error("correlation_id_required", "请求必须包含关联 ID。");
if (!ValidScopeValue(context.UserId)
|| !ValidScopeValue(context.UserName)
|| !ValidScopeValue(context.AccountBook)
|| !ValidScopeValue(context.SubSystemId)
|| !CommandInputFingerprint.IsValid(
context.DatabaseScopeFingerprint))
{
throw Error(
"erp_session_required",
"未检测到完整 ERP 登录作用域(用户编号、用户名、账套、子系统和数据库)。");
}
if (string.IsNullOrWhiteSpace(context.ClientSessionId))
throw Error("client_session_required", "未检测到桌宠客户端会话。");
}
private static bool ValidScopeValue(string value)
{
if (string.IsNullOrWhiteSpace(value)
|| value.Length > 256
|| !string.Equals(value, value.Trim(), StringComparison.Ordinal))
return false;
foreach (char item in value)
{
if (char.IsControl(item)) return false;
}
return true;
}
private static void ValidatePlanOwnership(CommandPlan plan, CommandExecutionContext context)
{
if (plan == null) throw Error("plan_required", "执行计划不能为空。");
if (!string.Equals(
plan.CorrelationId ?? string.Empty,
context.CorrelationId ?? string.Empty,
StringComparison.Ordinal))
{
throw Error(
"plan_correlation_mismatch",
"计划关联 ID 与当前执行请求不一致,请重新生成预览。");
}
if (!Same(plan.OwnerClientSessionId, context.ClientSessionId)
|| !Same(plan.OwnerUserId, context.UserId)
|| !Same(plan.OwnerUserName, context.UserName)
|| !Same(plan.OwnerAccountBook, context.AccountBook)
|| !Same(plan.OwnerSubSystemId, context.SubSystemId)
|| !Same(
plan.OwnerDatabaseScopeFingerprint,
context.DatabaseScopeFingerprint))
{
throw Error("plan_owner_mismatch", "计划所属 ERP 会话与当前登录会话不一致。");
}
}
private static void ValidatePlanDescriptor(CommandPlan plan, CommandDescriptor descriptor)
{
if (!Same(plan.CommandName, descriptor.Name)
|| !Same(plan.CommandVersion, descriptor.Version)
|| plan.Risk != descriptor.Risk
|| !CommandInputFingerprint.IsValid(plan.InputFingerprint))
{
throw Error("plan_tampered", "执行计划与已注册命令不一致。");
}
}
private void EnsureAuthorized(
CommandDescriptor descriptor,
CommandExecutionContext context,
bool execution)
{
CommandAuthorizationDecision decision = CheckAuthorization(descriptor, context, execution);
if (decision == null || !decision.Allowed)
{
string code = decision == null || string.IsNullOrWhiteSpace(decision.Code)
? "command_access_denied"
: decision.Code;
string message = decision == null || string.IsNullOrWhiteSpace(decision.Message)
? "当前 ERP 用户没有执行该命令的权限。"
: decision.Message;
throw Error(code, message);
}
}
private void SafeAuditFailure(
CommandDescriptor descriptor,
CommandPlan plan,
Exception exception,
CommandExecutionContext context)
{
try
{
_audit.Failed(descriptor, plan, exception, context);
}
catch
{
// 保留原始业务异常;审计后端自身失败应由其监控通道报警。
}
}
private static bool Same(string left, string right)
{
return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.OrdinalIgnoreCase);
}
private static CommandKernelException Error(string code, string message)
{
return new CommandKernelException(code, message, 6);
}
}
}