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 MissingRequirements() { List missing = new List(); 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(); } /// /// Optional runtime readiness contract. Startup registration still uses /// , while an actual /// command rechecks readiness against the exact request session instead of /// allowing an adapter to silently consult a mutable global ERP singleton. /// 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(); } public bool Ready { get { return Issues.Count == 0; } } public bool Registered { get; internal set; } public IList 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 missing = readiness == null ? new List { "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 missing = readiness == null ? new List { "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 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(), 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 mappings) { List 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 source = string.Equals( mapping.Scope, "detail", StringComparison.OrdinalIgnoreCase) ? inspection.DetailFields : inspection.MasterFields; List 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 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; } } }