377 lines
13 KiB
C#
377 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Lskj.CommandKernel
|
|
{
|
|
public enum CommandRisk
|
|
{
|
|
Read = 0,
|
|
Navigate = 1,
|
|
Draft = 2,
|
|
Write = 3,
|
|
Critical = 4
|
|
}
|
|
|
|
public sealed class CommandDescriptor
|
|
{
|
|
public string Name { get; set; }
|
|
public string Version { get; set; }
|
|
public string Description { get; set; }
|
|
public string SchemaVersion { get; set; }
|
|
public IDictionary<string, object> InputSchema { get; set; }
|
|
public string RequiredPermission { get; set; }
|
|
public CommandRisk Risk { get; set; }
|
|
public bool RequiresConfirmation { get; set; }
|
|
public bool RequiresIdempotencyKey { get; set; }
|
|
}
|
|
|
|
public sealed class CommandExecutionContext
|
|
{
|
|
public CommandExecutionContext()
|
|
{
|
|
CorrelationId = Guid.NewGuid().ToString("N");
|
|
}
|
|
|
|
public string CorrelationId { get; set; }
|
|
public string UserId { get; set; }
|
|
public string UserName { get; set; }
|
|
public string AccountBook { get; set; }
|
|
public string SubSystemId { get; set; }
|
|
// 由受信任的 ERP 登录运行时根据实际数据库提供者、服务器和库名
|
|
// 计算;模型和命令输入均不得提供或覆盖该值。
|
|
public string DatabaseScopeFingerprint { get; set; }
|
|
public string IdempotencyKey { get; set; }
|
|
public string ConfirmationToken { get; set; }
|
|
public string ClientSessionId { get; set; }
|
|
// These fields are populated only by the in-process bridge after a
|
|
// customer-UAT grant has been verified. The lease is deliberately
|
|
// never projected or audited; guarded workflow gateways revalidate it
|
|
// immediately before every read/write procedure call.
|
|
public string UatAuthorizationIdSha256 { get; set; }
|
|
public string UatCaseCode { get; set; }
|
|
public string UatCommandName { get; set; }
|
|
public string UatExecutionLease { get; set; }
|
|
}
|
|
|
|
public sealed class CommandPlan
|
|
{
|
|
private readonly Dictionary<string, object> _serverData;
|
|
|
|
public CommandPlan()
|
|
{
|
|
PlanId = Guid.NewGuid().ToString("N");
|
|
CreatedAtUtc = DateTime.UtcNow;
|
|
ExpiresAtUtc = CreatedAtUtc.AddMinutes(10);
|
|
Data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
Warnings = new List<string>();
|
|
_serverData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public string PlanId { get; set; }
|
|
public string CommandName { get; set; }
|
|
public string CommandVersion { get; set; }
|
|
public string ModuleCode { get; set; }
|
|
public string OwnerClientSessionId { get; set; }
|
|
public string OwnerUserId { get; set; }
|
|
public string OwnerUserName { get; set; }
|
|
public string OwnerAccountBook { get; set; }
|
|
public string OwnerSubSystemId { get; set; }
|
|
public string OwnerDatabaseScopeFingerprint { get; set; }
|
|
public string CorrelationId { get; set; }
|
|
public string InputFingerprint { get; set; }
|
|
public CommandRisk Risk { get; set; }
|
|
public DateTime CreatedAtUtc { get; set; }
|
|
public DateTime ExpiresAtUtc { get; set; }
|
|
public bool Valid { get; set; }
|
|
public IDictionary<string, object> Data { get; private set; }
|
|
public IList<string> Warnings { get; private set; }
|
|
|
|
public void SetServerData(string key, object value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
throw new ArgumentException("服务端计划数据键不能为空。", "key");
|
|
_serverData[key] = value;
|
|
}
|
|
|
|
public T GetServerData<T>(string key)
|
|
{
|
|
object value;
|
|
if (string.IsNullOrWhiteSpace(key) || !_serverData.TryGetValue(key, out value))
|
|
throw new CommandKernelException("plan_state_missing", "执行计划缺少服务端状态,请重新生成预览。", 6);
|
|
if (!(value is T))
|
|
throw new CommandKernelException("plan_state_invalid", "执行计划服务端状态类型无效,请重新生成预览。", 6);
|
|
return (T)value;
|
|
}
|
|
|
|
public bool TryGetServerData<T>(string key, out T value)
|
|
{
|
|
object stored;
|
|
if (!string.IsNullOrWhiteSpace(key)
|
|
&& _serverData.TryGetValue(key, out stored)
|
|
&& stored is T)
|
|
{
|
|
value = (T)stored;
|
|
return true;
|
|
}
|
|
value = default(T);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public sealed class CommandResult
|
|
{
|
|
public CommandResult()
|
|
{
|
|
Data = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public bool Success { get; set; }
|
|
public string Code { get; set; }
|
|
public string Message { get; set; }
|
|
public string RecordId { get; set; }
|
|
public bool Replayed { get; set; }
|
|
public string TransactionEvidenceId { get; set; }
|
|
public string BusinessAuditId { get; set; }
|
|
public IDictionary<string, object> Data { get; private set; }
|
|
}
|
|
|
|
public interface ICommandHandler
|
|
{
|
|
CommandDescriptor Descriptor { get; }
|
|
CommandPlan Plan(IDictionary<string, object> input, CommandExecutionContext context);
|
|
CommandResult Execute(CommandPlan plan, CommandExecutionContext context);
|
|
}
|
|
|
|
public sealed class CommandFollowupRequest
|
|
{
|
|
public CommandFollowupRequest()
|
|
{
|
|
Input = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public string CommandName { get; set; }
|
|
public IDictionary<string, object> Input { get; private set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 写命令成功后可生成一个新的、仍需独立预览和确认的后续计划。
|
|
/// 返回值只描述下一条命令,绝不能在此接口内执行副作用。
|
|
/// </summary>
|
|
public interface ICommandExecutionFollowupProvider
|
|
{
|
|
bool TryCreateFollowup(
|
|
CommandPlan completedPlan,
|
|
CommandResult completedResult,
|
|
CommandExecutionContext context,
|
|
out CommandFollowupRequest followup);
|
|
}
|
|
|
|
public interface ICommandAuditSink
|
|
{
|
|
void Planned(CommandDescriptor descriptor, CommandPlan plan, CommandExecutionContext context);
|
|
void Completed(CommandDescriptor descriptor, CommandPlan plan, CommandResult result, CommandExecutionContext context);
|
|
void Failed(CommandDescriptor descriptor, CommandPlan plan, Exception exception, CommandExecutionContext context);
|
|
}
|
|
|
|
public enum IdempotencyClaimState
|
|
{
|
|
Acquired = 0,
|
|
Replay = 1,
|
|
InProgress = 2,
|
|
Conflict = 3
|
|
}
|
|
|
|
public sealed class IdempotencyClaim
|
|
{
|
|
public IdempotencyClaimState State { get; set; }
|
|
public CommandResult Result { get; set; }
|
|
}
|
|
|
|
public interface IIdempotencyStore
|
|
{
|
|
IdempotencyClaim Claim(
|
|
string commandName,
|
|
string idempotencyKey,
|
|
CommandExecutionContext context,
|
|
string inputFingerprint);
|
|
void Complete(
|
|
string commandName,
|
|
string idempotencyKey,
|
|
CommandExecutionContext context,
|
|
string inputFingerprint,
|
|
CommandResult result);
|
|
void Abandon(
|
|
string commandName,
|
|
string idempotencyKey,
|
|
CommandExecutionContext context,
|
|
string inputFingerprint);
|
|
}
|
|
|
|
public interface IConfirmationValidator
|
|
{
|
|
bool Validate(CommandPlan plan, CommandExecutionContext context, out string failureReason);
|
|
}
|
|
|
|
public interface IConfirmationTokenIssuer
|
|
{
|
|
string Issue(CommandPlan plan, CommandExecutionContext context, TimeSpan lifetime);
|
|
}
|
|
|
|
public sealed class CommandAuthorizationDecision
|
|
{
|
|
public static CommandAuthorizationDecision Allow()
|
|
{
|
|
return new CommandAuthorizationDecision { Allowed = true };
|
|
}
|
|
|
|
public static CommandAuthorizationDecision Deny(string code, string message)
|
|
{
|
|
return new CommandAuthorizationDecision
|
|
{
|
|
Allowed = false,
|
|
Code = code,
|
|
Message = message
|
|
};
|
|
}
|
|
|
|
public bool Allowed { get; set; }
|
|
public string Code { get; set; }
|
|
public string Message { get; set; }
|
|
}
|
|
|
|
public interface ICommandAuthorizer
|
|
{
|
|
CommandAuthorizationDecision Authorize(
|
|
CommandDescriptor descriptor,
|
|
CommandExecutionContext context,
|
|
bool execution);
|
|
}
|
|
|
|
public interface ISystemClock
|
|
{
|
|
DateTime UtcNow { get; }
|
|
}
|
|
|
|
public sealed class CommandRegistry
|
|
{
|
|
private readonly object _syncRoot = new object();
|
|
private readonly Dictionary<string, ICommandHandler> _handlers =
|
|
new Dictionary<string, ICommandHandler>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
public void Register(ICommandHandler handler)
|
|
{
|
|
RegisterMany(new[] { handler });
|
|
}
|
|
|
|
public void RegisterMany(IEnumerable<ICommandHandler> handlers)
|
|
{
|
|
if (handlers == null) throw new ArgumentNullException("handlers");
|
|
List<ICommandHandler> items = handlers.ToList();
|
|
if (items.Count == 0 || items.Any(handler => handler == null))
|
|
{
|
|
throw new ArgumentException("命令处理器不能为空。", "handlers");
|
|
}
|
|
List<ICommandHandler> registrations = items.Select(handler =>
|
|
(ICommandHandler)new RegisteredCommandHandler(
|
|
handler,
|
|
CommandDescriptorContract.ValidateAndClone(handler.Descriptor)))
|
|
.ToList();
|
|
if (registrations.GroupBy(
|
|
handler => handler.Descriptor.Name,
|
|
StringComparer.OrdinalIgnoreCase)
|
|
.Any(group => group.Count() > 1))
|
|
{
|
|
throw new InvalidOperationException("批量注册包含重复命令。");
|
|
}
|
|
|
|
lock (_syncRoot)
|
|
{
|
|
foreach (ICommandHandler handler in registrations)
|
|
{
|
|
if (_handlers.ContainsKey(handler.Descriptor.Name))
|
|
throw new InvalidOperationException("命令已注册:" + handler.Descriptor.Name);
|
|
}
|
|
foreach (ICommandHandler handler in registrations)
|
|
_handlers.Add(handler.Descriptor.Name, handler);
|
|
}
|
|
}
|
|
|
|
public ICommandHandler Resolve(string commandName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(commandName)) return null;
|
|
lock (_syncRoot)
|
|
{
|
|
ICommandHandler handler;
|
|
return _handlers.TryGetValue(commandName, out handler) ? handler : null;
|
|
}
|
|
}
|
|
|
|
public IList<CommandDescriptor> List()
|
|
{
|
|
lock (_syncRoot)
|
|
{
|
|
List<CommandDescriptor> result = new List<CommandDescriptor>();
|
|
foreach (ICommandHandler handler in _handlers.Values)
|
|
result.Add(handler.Descriptor);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private sealed class RegisteredCommandHandler :
|
|
ICommandHandler,
|
|
ICommandExecutionFollowupProvider
|
|
{
|
|
private readonly ICommandHandler _inner;
|
|
private readonly CommandDescriptor _descriptor;
|
|
|
|
public RegisteredCommandHandler(
|
|
ICommandHandler inner,
|
|
CommandDescriptor descriptor)
|
|
{
|
|
_inner = inner;
|
|
_descriptor = descriptor;
|
|
}
|
|
|
|
public CommandDescriptor Descriptor
|
|
{
|
|
get { return CommandDescriptorContract.Clone(_descriptor); }
|
|
}
|
|
|
|
public CommandPlan Plan(
|
|
IDictionary<string, object> input,
|
|
CommandExecutionContext context)
|
|
{
|
|
return _inner.Plan(input, context);
|
|
}
|
|
|
|
public CommandResult Execute(
|
|
CommandPlan plan,
|
|
CommandExecutionContext context)
|
|
{
|
|
return _inner.Execute(plan, context);
|
|
}
|
|
|
|
public bool TryCreateFollowup(
|
|
CommandPlan completedPlan,
|
|
CommandResult completedResult,
|
|
CommandExecutionContext context,
|
|
out CommandFollowupRequest followup)
|
|
{
|
|
ICommandExecutionFollowupProvider provider =
|
|
_inner as ICommandExecutionFollowupProvider;
|
|
if (provider == null)
|
|
{
|
|
followup = null;
|
|
return false;
|
|
}
|
|
return provider.TryCreateFollowup(
|
|
completedPlan,
|
|
completedResult,
|
|
context,
|
|
out followup);
|
|
}
|
|
}
|
|
}
|
|
}
|