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 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 inputFingerprint = CommandInputFingerprint.Create( handler.Descriptor.Name, input ?? new Dictionary()); plan = handler.Plan(input ?? new Dictionary(), 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); } } }