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

609 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Lskj.CommandKernel
{
public sealed class BusinessAdapterReadiness
{
public string AdapterId { get; set; }
public string AdapterVersion { get; set; }
public string EvidenceId { get; set; }
public string EvidenceSha256 { get; set; }
public string AccountBook { get; set; }
public string SubSystemId { get; set; }
public string ValidatedBy { get; set; }
public DateTime ValidatedAtUtc { get; set; }
public bool CustomerConfigurationValidated { get; set; }
public bool ParameterizedReadQueriesVerified { get; set; }
public bool TransactionalWriteVerified { get; set; }
public bool PersistentIdempotencyVerified { get; set; }
public bool PermissionRecheckVerified { get; set; }
public bool WindowsIntegrationVerified { get; set; }
public bool AcceptanceManifestVerified { get; set; }
public bool AcceptanceSignatureVerified { get; set; }
public string ActivationMode { get; set; }
public bool UatAuthorizationVerified { get; set; }
public IList<string> MissingRequirements()
{
List<string> missing = new List<string>();
if (string.IsNullOrWhiteSpace(AdapterId)) missing.Add("adapter_id");
if (string.IsNullOrWhiteSpace(AdapterVersion)) missing.Add("adapter_version");
if (string.IsNullOrWhiteSpace(EvidenceId)) missing.Add("integration_evidence");
if (!CommandInputFingerprint.IsValid(EvidenceSha256)) missing.Add("integration_evidence_hash");
if (string.IsNullOrWhiteSpace(AccountBook)) missing.Add("evidence_account_book");
if (string.IsNullOrWhiteSpace(SubSystemId)) missing.Add("evidence_subsystem");
if (string.IsNullOrWhiteSpace(ValidatedBy)) missing.Add("evidence_validator");
if (ValidatedAtUtc == DateTime.MinValue) missing.Add("evidence_validation_time");
if (string.Equals(
ActivationMode,
"customer_uat",
StringComparison.Ordinal))
{
if (!UatAuthorizationVerified)
missing.Add("uat_authorization");
return missing;
}
if (!string.IsNullOrWhiteSpace(ActivationMode)
&& !string.Equals(
ActivationMode,
"commercial",
StringComparison.Ordinal))
{
missing.Add("activation_mode");
return missing;
}
if (!CustomerConfigurationValidated) missing.Add("customer_configuration");
if (!ParameterizedReadQueriesVerified) missing.Add("parameterized_read_queries");
if (!TransactionalWriteVerified) missing.Add("transactional_write");
if (!PersistentIdempotencyVerified) missing.Add("persistent_idempotency");
if (!PermissionRecheckVerified) missing.Add("permission_recheck");
if (!WindowsIntegrationVerified) missing.Add("windows_integration");
if (!AcceptanceManifestVerified) missing.Add("acceptance_manifest");
if (!AcceptanceSignatureVerified) missing.Add("acceptance_signature");
return missing;
}
}
public interface IBusinessWorkflowAdapterReadiness
{
BusinessAdapterReadiness GetReadiness();
}
/// <summary>
/// Optional runtime readiness contract. Startup registration still uses
/// <see cref="IBusinessWorkflowAdapterReadiness"/>, while an actual
/// command rechecks readiness against the exact request session instead of
/// allowing an adapter to silently consult a mutable global ERP singleton.
/// </summary>
public interface IContextualBusinessWorkflowAdapterReadiness
{
BusinessAdapterReadiness GetReadiness(CommandExecutionContext context);
}
public sealed class AdapterPreflightIssue
{
public string Code { get; set; }
public string Message { get; set; }
public string Field { get; set; }
}
public sealed class AdapterPreflightResult
{
public AdapterPreflightResult()
{
Issues = new List<AdapterPreflightIssue>();
}
public bool Ready
{
get { return Issues.Count == 0; }
}
public bool Registered { get; internal set; }
public IList<AdapterPreflightIssue> Issues { get; private set; }
internal void Add(string code, string message, string field)
{
Issues.Add(new AdapterPreflightIssue
{
Code = code,
Message = message,
Field = field
});
}
}
public sealed class PurchaseWorkflowFieldMap
{
public string ModuleCode { get; set; }
public string SupplierCode { get; set; }
public string InvoiceNumber { get; set; }
public string InvoiceDate { get; set; }
public string CurrencyCode { get; set; }
public string MaterialCode { get; set; }
public string Unit { get; set; }
public string Quantity { get; set; }
public string UnitPrice { get; set; }
public string TaxRate { get; set; }
public string ExchangeRate { get; set; }
public string LineAmount { get; set; }
public string SourceOrderId { get; set; }
public string SourceLineId { get; set; }
}
public sealed class LeaveWorkflowFieldMap
{
public string ModuleCode { get; set; }
public string EmployeeId { get; set; }
public string LeaveTypeCode { get; set; }
public string FlowTypeCode { get; set; }
public string StartLocal { get; set; }
public string EndLocal { get; set; }
public string RequestedHours { get; set; }
public string Reason { get; set; }
}
public static class BusinessAdapterRegistrationGate
{
private static readonly Regex SafeIdentifier = new Regex(
@"^[A-Za-z_][A-Za-z0-9_]{0,127}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public static AdapterPreflightResult RegisterPurchase(
CommandRegistry registry,
IPurchaseInvoiceWorkflowAdapter adapter,
PurchaseInvoiceMatchOptions options,
PurchaseWorkflowFieldMap fields,
ModuleInspection inspection)
{
if (registry == null) throw new ArgumentNullException("registry");
AdapterPreflightResult result = ValidatePurchase(adapter, fields, inspection);
ValidateCommercialPurchaseOptions(options, result);
if (!result.Ready) return result;
IPurchaseInvoiceIntentResolver resolver =
adapter as IPurchaseInvoiceIntentResolver;
if (resolver == null)
{
result.Add(
"purchase_intent_resolver_missing",
"采购适配器缺少供应商、币种和物料主数据解析能力。",
null);
return result;
}
if (registry.Resolve("purchase.invoice.resolve") != null
|| registry.Resolve("purchase.invoice.create") != null)
{
result.Add("command_already_registered", "采购发票解析或创建命令已经注册。", null);
return result;
}
IPurchaseResolutionProofService proofs =
HmacPurchaseResolutionProofService.Create(new SystemClock());
registry.RegisterMany(new ICommandHandler[]
{
new PurchaseInvoiceIntentResolveCommandHandler(adapter, resolver, proofs),
new PurchaseInvoiceCreateCommandHandler(adapter, options, proofs)
});
result.Registered = true;
return result;
}
public static AdapterPreflightResult RegisterLeave(
CommandRegistry registry,
ILeaveWorkflowAdapter adapter,
LeaveValidationOptions options,
LeaveWorkflowFieldMap fields,
ModuleInspection inspection)
{
if (registry == null) throw new ArgumentNullException("registry");
AdapterPreflightResult result = ValidateLeave(adapter, fields, inspection);
if (!result.Ready) return result;
ILeaveIntentResolver resolver = adapter as ILeaveIntentResolver;
if (resolver == null)
{
result.Add(
"leave_intent_resolver_missing",
"请假适配器缺少自然语言假别与员工日历解析能力。",
null);
return result;
}
if (registry.Resolve("hr.leave.resolve") != null
|| registry.Resolve("hr.leave.create") != null
|| registry.Resolve("hr.leave.submit") != null)
{
result.Add("command_already_registered", "请假解析、创建或提交命令已经注册。", null);
return result;
}
ILeaveResolutionProofService proofs =
HmacLeaveResolutionProofService.Create(new SystemClock());
registry.RegisterMany(new ICommandHandler[]
{
new LeaveIntentResolveCommandHandler(adapter, resolver, options, proofs),
new LeaveCreateCommandHandler(adapter, options, proofs),
new LeaveSubmitCommandHandler(adapter)
});
result.Registered = true;
return result;
}
public static AdapterPreflightResult ValidatePurchase(
IPurchaseInvoiceWorkflowAdapter adapter,
PurchaseWorkflowFieldMap fields,
ModuleInspection inspection)
{
AdapterPreflightResult result = ValidateAdapterReadiness(adapter);
if (adapter != null && !(adapter is IPurchaseInvoiceIntentResolver))
{
result.Add(
"purchase_intent_resolver_missing",
"采购适配器必须实现供应商、币种和物料主数据解析契约。",
null);
}
Append(result, ValidatePurchaseFieldMap(fields, inspection));
if (fields != null && adapter != null
&& !string.Equals(
fields.ModuleCode,
adapter.ModuleCode,
StringComparison.Ordinal))
{
result.Add("module_code_mismatch", "字段映射模块编号与适配器模块编号不一致。", "moduleCode");
}
return result;
}
public static AdapterPreflightResult ValidatePurchaseFieldMap(
PurchaseWorkflowFieldMap fields,
ModuleInspection inspection)
{
AdapterPreflightResult result = ValidateInspection(inspection, "bill");
if (fields == null)
{
result.Add("field_map_missing", "采购字段映射不能为空。", null);
return result;
}
ValidateInspectedModuleCode(fields.ModuleCode, inspection, result);
ValidateFields(
inspection,
result,
new[]
{
Mapping("supplierCode", "master", fields.SupplierCode),
Mapping("invoiceNumber", "master", fields.InvoiceNumber),
Mapping("invoiceDate", "master", fields.InvoiceDate),
Mapping("currencyCode", "master", fields.CurrencyCode),
Mapping("materialCode", "detail", fields.MaterialCode),
Mapping("unit", "detail", fields.Unit),
Mapping("quantity", "detail", fields.Quantity),
Mapping("unitPrice", "detail", fields.UnitPrice),
Mapping("taxRate", "detail", fields.TaxRate),
Mapping("exchangeRate", "detail", fields.ExchangeRate),
Mapping("lineAmount", "detail", fields.LineAmount),
Mapping("sourceOrderId", "detail", fields.SourceOrderId),
Mapping("sourceLineId", "detail", fields.SourceLineId)
});
return result;
}
public static AdapterPreflightResult ValidateLeave(
ILeaveWorkflowAdapter adapter,
LeaveWorkflowFieldMap fields,
ModuleInspection inspection)
{
AdapterPreflightResult result = ValidateAdapterReadiness(adapter);
if (adapter != null && !(adapter is ILeaveIntentResolver))
{
result.Add(
"leave_intent_resolver_missing",
"请假适配器必须实现自然语言假别与员工日历解析契约。",
null);
}
Append(result, ValidateLeaveFieldMap(fields, inspection));
if (fields != null && adapter != null
&& !string.Equals(
fields.ModuleCode,
adapter.ModuleCode,
StringComparison.Ordinal))
{
result.Add("module_code_mismatch", "字段映射模块编号与适配器模块编号不一致。", "moduleCode");
}
return result;
}
public static AdapterPreflightResult ValidateLeaveFieldMap(
LeaveWorkflowFieldMap fields,
ModuleInspection inspection)
{
AdapterPreflightResult result = ValidateInspection(inspection, null);
if (inspection != null
&& !string.Equals(inspection.Kind, "bill", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(inspection.Kind, "base", StringComparison.OrdinalIgnoreCase))
{
result.Add("module_kind_unsupported", "请假模块必须是单据或基础档案模块。", "moduleCode");
}
if (fields == null)
{
result.Add("field_map_missing", "请假字段映射不能为空。", null);
return result;
}
ValidateInspectedModuleCode(fields.ModuleCode, inspection, result);
ValidateFields(
inspection,
result,
new[]
{
Mapping("employeeId", "master", fields.EmployeeId),
Mapping("leaveTypeCode", "master", fields.LeaveTypeCode),
Mapping("flowTypeCode", "master", fields.FlowTypeCode),
Mapping("startLocal", "master", fields.StartLocal),
Mapping("endLocal", "master", fields.EndLocal),
Mapping("requestedHours", "master", fields.RequestedHours),
Mapping("reason", "master", fields.Reason)
});
return result;
}
public static void EnsureRuntimeReady(IBusinessWorkflowAdapterReadiness adapter)
{
EnsureRuntimeReady(adapter, null);
}
public static void EnsureRuntimeReady(
IBusinessWorkflowAdapterReadiness adapter,
CommandExecutionContext context)
{
if (adapter == null)
throw new CommandKernelException(
"adapter_readiness_missing",
"业务适配器没有实现商用就绪检查。",
6);
BusinessAdapterReadiness readiness;
try
{
IContextualBusinessWorkflowAdapterReadiness contextual =
adapter as IContextualBusinessWorkflowAdapterReadiness;
readiness = context != null && contextual != null
? contextual.GetReadiness(context)
: adapter.GetReadiness();
}
catch (CommandKernelException error)
{
if (IsSafeCustomerProfileRuntimeCode(error.Code))
throw new CommandKernelException(
error.Code,
"客户画像在线目录复核未通过,写命令保持禁用。",
6);
throw new CommandKernelException(
"adapter_readiness_failed",
"业务适配器就绪检查失败,写命令保持禁用。",
6);
}
catch
{
throw new CommandKernelException(
"adapter_readiness_failed",
"业务适配器就绪检查失败,写命令保持禁用。",
6);
}
IList<string> missing = readiness == null
? new List<string> { "readiness_result" }
: readiness.MissingRequirements();
if (context != null && readiness != null
&& (!string.Equals(
readiness.AccountBook,
context.AccountBook,
StringComparison.Ordinal)
|| !string.Equals(
readiness.SubSystemId,
context.SubSystemId,
StringComparison.Ordinal)))
missing.Add("readiness_scope");
if (missing.Count > 0)
{
throw new CommandKernelException(
"adapter_not_ready",
"业务适配器未满足商用注册条件:" + string.Join(",", missing.ToArray()),
6);
}
}
private static bool IsSafeCustomerProfileRuntimeCode(string code)
{
return code == "profile_runtime_hash_mismatch"
|| code == "profile_runtime_contract_invalid"
|| code == "profile_workflow_activation_blocked"
|| code == "profile_runtime_metadata_contract_invalid"
|| code == "profile_runtime_metadata_unavailable"
|| code == "profile_runtime_metadata_changed"
|| code == "profile_critical_catalog_contract_changed"
|| code == "runtime_configuration_changed"
|| code == "runtime_configuration_unavailable";
}
private static AdapterPreflightResult ValidateAdapterReadiness(
IBusinessWorkflowAdapterReadiness adapter)
{
AdapterPreflightResult result = new AdapterPreflightResult();
if (adapter == null)
{
result.Add("adapter_missing", "业务适配器不能为空。", null);
}
else
{
try
{
BusinessAdapterReadiness readiness = adapter.GetReadiness();
IList<string> missing = readiness == null
? new List<string> { "readiness_result" }
: readiness.MissingRequirements();
foreach (string item in missing)
result.Add("adapter_requirement_missing", "业务适配器缺少商用证据:" + item, item);
}
catch (CommandKernelException error)
{
result.Add(
string.IsNullOrWhiteSpace(error.Code)
? "adapter_readiness_failed"
: error.Code,
"业务适配器就绪检查返回了安全错误代码,写命令保持禁用。",
null);
}
catch
{
result.Add("adapter_readiness_failed", "业务适配器就绪检查失败。", null);
}
}
return result;
}
private static AdapterPreflightResult ValidateInspection(
ModuleInspection inspection,
string expectedKind)
{
AdapterPreflightResult result = new AdapterPreflightResult();
if (inspection == null)
{
result.Add("module_inspection_missing", "缺少模块元数据检查结果。", "moduleCode");
}
else if (!string.IsNullOrWhiteSpace(expectedKind)
&& !string.Equals(inspection.Kind, expectedKind, StringComparison.OrdinalIgnoreCase))
{
result.Add("module_kind_mismatch", "业务适配器与低代码模块类型不匹配。", "moduleCode");
}
if (inspection != null)
{
try
{
IDictionary<string, object> diagnosis = ModuleInspector.Diagnose(inspection);
object healthy;
if (!diagnosis.TryGetValue("healthy", out healthy) || !Convert.ToBoolean(healthy))
{
result.Add(
"module_configuration_unhealthy",
"低代码模块配置诊断未通过;请先运行 modules diagnose 修复错误。",
"moduleCode");
}
}
catch
{
result.Add(
"module_diagnosis_failed",
"无法完成低代码模块配置诊断,写命令保持禁用。",
"moduleCode");
}
}
return result;
}
private static void ValidateCommercialPurchaseOptions(
PurchaseInvoiceMatchOptions options,
AdapterPreflightResult result)
{
if (options == null
|| options.LineAmountMode == InvoiceLineAmountMode.None)
{
result.Add(
"purchase_amount_validation_required",
"采购写入必须明确含税或不含税金额模式,禁止关闭财务一致性复核。",
"lineAmountMode");
return;
}
try
{
PurchaseInvoiceMatcher.Match(
new PurchaseInvoiceDraft(),
new List<PurchaseSourceLine>(),
options);
}
catch
{
result.Add(
"purchase_match_options_invalid",
"采购金额、税额或匹配容差配置无效。",
"matchOptions");
}
}
private static void ValidateInspectedModuleCode(
string configured,
ModuleInspection inspection,
AdapterPreflightResult result)
{
if (string.IsNullOrWhiteSpace(configured)
|| inspection == null
|| !string.Equals(
configured,
inspection.ModuleCode,
StringComparison.Ordinal))
result.Add("module_code_mismatch", "字段映射模块编号与已检查模块编号不一致。", "moduleCode");
}
private static void Append(AdapterPreflightResult target, AdapterPreflightResult source)
{
if (target == null || source == null) return;
foreach (AdapterPreflightIssue issue in source.Issues)
target.Issues.Add(issue);
}
private static void ValidateFields(
ModuleInspection inspection,
AdapterPreflightResult result,
IEnumerable<FieldMapping> mappings)
{
List<FieldMapping> values = mappings.ToList();
foreach (FieldMapping mapping in values)
{
if (string.IsNullOrWhiteSpace(mapping.FieldName)
|| !SafeIdentifier.IsMatch(mapping.FieldName))
{
result.Add("unsafe_field_mapping", "字段映射必须是安全的精确数据库字段名。", mapping.SemanticName);
continue;
}
if (inspection == null) continue;
IEnumerable<FieldSpec> source = string.Equals(
mapping.Scope, "detail", StringComparison.OrdinalIgnoreCase)
? inspection.DetailFields : inspection.MasterFields;
List<FieldSpec> exact = source.Where(item => item != null
&& string.Equals(
item.Name,
mapping.FieldName,
StringComparison.OrdinalIgnoreCase)).ToList();
if (exact.Count == 0)
result.Add("mapped_field_not_found", "低代码元数据中不存在映射字段。", mapping.SemanticName);
else if (exact.Count > 1)
result.Add("mapped_field_ambiguous", "低代码元数据中存在重复映射字段。", mapping.SemanticName);
else if (!exact[0].Exposed)
result.Add("mapped_field_not_exposed", "映射字段未在当前低代码界面和字段权限中暴露。", mapping.SemanticName);
}
foreach (IGrouping<string, FieldMapping> duplicate in values
.Where(item => !string.IsNullOrWhiteSpace(item.FieldName))
.GroupBy(item => item.Scope + ":" + item.FieldName, StringComparer.OrdinalIgnoreCase)
.Where(group => group.Count() > 1))
{
result.Add("mapped_field_reused", "多个业务含义映射到了同一个字段。", duplicate.Key);
}
}
private static FieldMapping Mapping(string semanticName, string scope, string fieldName)
{
return new FieldMapping
{
SemanticName = semanticName,
Scope = scope,
FieldName = fieldName == null ? null : fieldName.Trim()
};
}
private sealed class FieldMapping
{
public string SemanticName;
public string Scope;
public string FieldName;
}
}
}