609 lines
26 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|