feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user