using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; using Lskj.CommandKernel; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Lskj.AgentBridge { public interface IWorkflowProcedureGateway { BusinessAdapterReadiness GetReadiness(string workflow, string moduleCode); DataTable Read( string workflow, string action, string moduleCode, CommandExecutionContext context, IDictionary payload); BusinessWriteResult Write( string workflow, string action, string moduleCode, CommandExecutionContext context, IDictionary payload, string idempotencyKey, string inputFingerprint); } /// /// Optional gateway extension used by live command rechecks. Keeping it /// separate preserves compatibility with offline contract probes and old /// customer gateways while allowing production gateways to bind readiness /// to the exact command session. /// public interface IContextualWorkflowProcedureGateway { BusinessAdapterReadiness GetReadiness( string workflow, string moduleCode, CommandExecutionContext context); } public sealed class ProcedurePurchaseWorkflowAdapter : IPurchaseInvoiceWorkflowAdapter, IPurchaseInvoiceIntentResolver, IContextualBusinessWorkflowAdapterReadiness { private const string Workflow = "purchase"; private readonly IWorkflowProcedureGateway _gateway; private readonly IBusinessAdapterReadinessAttestor _readinessAttestor; public ProcedurePurchaseWorkflowAdapter(string moduleCode, IWorkflowProcedureGateway gateway) : this(moduleCode, gateway, null) { } public ProcedurePurchaseWorkflowAdapter( string moduleCode, IWorkflowProcedureGateway gateway, IBusinessAdapterReadinessAttestor readinessAttestor) { if (string.IsNullOrWhiteSpace(moduleCode)) throw new ArgumentException("采购模块编号不能为空。", "moduleCode"); if (gateway == null) throw new ArgumentNullException("gateway"); ModuleCode = moduleCode.Trim(); _gateway = gateway; _readinessAttestor = readinessAttestor; } public string ModuleCode { get; private set; } public BusinessAdapterReadiness GetReadiness() { return GetReadiness(null); } public BusinessAdapterReadiness GetReadiness(CommandExecutionContext context) { if (context == null) return GetReadinessWithoutContext(); IBusinessAdapterReadinessOverride readinessOverride = _readinessAttestor as IBusinessAdapterReadinessOverride; if (readinessOverride != null) { IContextualBusinessAdapterReadinessOverride contextualOverride = _readinessAttestor as IContextualBusinessAdapterReadinessOverride; if (contextualOverride == null) throw ContextualReadinessRequired(); return contextualOverride.GetReadiness(Workflow, ModuleCode, context); } IContextualWorkflowProcedureGateway contextualGateway = _gateway as IContextualWorkflowProcedureGateway; if (contextualGateway == null) throw ContextualReadinessRequired(); BusinessAdapterReadiness readiness = contextualGateway.GetReadiness( Workflow, ModuleCode, context); return _readinessAttestor == null ? readiness : _readinessAttestor.Attest(Workflow, ModuleCode, readiness); } private BusinessAdapterReadiness GetReadinessWithoutContext() { IBusinessAdapterReadinessOverride readinessOverride = _readinessAttestor as IBusinessAdapterReadinessOverride; if (readinessOverride != null) return readinessOverride.GetReadiness(Workflow, ModuleCode); BusinessAdapterReadiness readiness = _gateway.GetReadiness(Workflow, ModuleCode); return _readinessAttestor == null ? readiness : _readinessAttestor.Attest(Workflow, ModuleCode, readiness); } private static CommandKernelException ContextualReadinessRequired() { return new CommandKernelException( "adapter_readiness_context_required", "运行时业务适配器就绪复核必须绑定显式 ERP 会话。", 6); } public bool InvoiceNumberExists( string supplierCode, string invoiceNumber, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(_gateway.Read( Workflow, "invoice_exists", ModuleCode, RequireContext(context), new Dictionary { { "supplierCode", supplierCode }, { "invoiceNumber", invoiceNumber } }), "purchase.invoice_exists"); return ProcedureRows.Boolean(row, "exists"); } public IList ResolveSuppliers( string reference, string taxId, CommandExecutionContext context) { DataTable table = ReadCandidates( "resolve_supplier", context, new Dictionary { { "reference", reference }, { "taxId", taxId } }); List result = new List(); HashSet codes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DataRow row in table.Rows) { PurchaseSupplierCandidate candidate = new PurchaseSupplierCandidate { Code = ProcedureRows.RequiredString(row, "supplier_code"), Name = ProcedureRows.RequiredString(row, "supplier_name"), TaxId = ProcedureRows.OptionalString(row, "supplier_tax_id") }; if (!codes.Add(candidate.Code)) throw ProcedureRows.Protocol("purchase.resolve_supplier", "供应商候选编码重复。"); result.Add(candidate); } return result; } public IList ResolveCurrencies( string reference, CommandExecutionContext context) { DataTable table = ReadCandidates( "resolve_currency", context, new Dictionary { { "reference", reference } }); List result = new List(); HashSet codes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DataRow row in table.Rows) { PurchaseCurrencyCandidate candidate = new PurchaseCurrencyCandidate { Code = ProcedureRows.RequiredString(row, "currency_code"), Name = ProcedureRows.RequiredString(row, "currency_name") }; if (!codes.Add(candidate.Code)) throw ProcedureRows.Protocol("purchase.resolve_currency", "币种候选编码重复。"); result.Add(candidate); } return result; } public IList ResolveMaterials( PurchaseInvoiceIntentLine line, string supplierCode, CommandExecutionContext context) { if (line == null) throw new ArgumentNullException("line"); DataTable table = ReadCandidates( "resolve_material", context, new Dictionary { { "lineId", line.LineId }, { "reference", line.MaterialReference }, { "specification", line.Specification }, { "unit", line.Unit }, { "supplierCode", supplierCode } }); List result = new List(); HashSet codes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DataRow row in table.Rows) { PurchaseMaterialCandidate candidate = new PurchaseMaterialCandidate { Code = ProcedureRows.RequiredString(row, "material_code"), Name = ProcedureRows.RequiredString(row, "material_name"), Specification = ProcedureRows.OptionalString(row, "specification"), Unit = ProcedureRows.RequiredString(row, "unit") }; if (!codes.Add(candidate.Code)) throw ProcedureRows.Protocol("purchase.resolve_material", "物料候选编码重复。"); result.Add(candidate); } return result; } public IList QueryOpenSourceLines( PurchaseInvoiceDraft draft, CommandExecutionContext context) { if (draft == null) throw new ArgumentNullException("draft"); DataTable table = _gateway.Read( Workflow, "open_sources", ModuleCode, RequireContext(context), WorkflowProcedurePayload.From(draft)); if (table == null || table.Rows.Count > 10000) throw ProcedureRows.Protocol("purchase.open_sources", "来源结果为空或超过 10000 行。"); List result = new List(); HashSet identities = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DataRow row in table.Rows) { PurchaseSourceLine line = new PurchaseSourceLine { SourceOrderId = ProcedureRows.RequiredString(row, "source_order_id"), SourceOrderNumber = ProcedureRows.RequiredString(row, "source_order_number"), SourceLineId = ProcedureRows.RequiredString(row, "source_line_id"), SupplierCode = ProcedureRows.RequiredString(row, "supplier_code"), CurrencyCode = ProcedureRows.RequiredString(row, "currency_code"), MaterialCode = ProcedureRows.RequiredString(row, "material_code"), Unit = ProcedureRows.RequiredString(row, "unit"), RemainingQuantity = ProcedureRows.Decimal(row, "remaining_quantity"), UnitPrice = ProcedureRows.Decimal(row, "unit_price"), TaxRate = ProcedureRows.Decimal(row, "tax_rate"), ExchangeRate = ProcedureRows.Decimal(row, "exchange_rate"), Closed = ProcedureRows.Boolean(row, "closed") }; if (line.RemainingQuantity < 0 || line.UnitPrice < 0 || line.TaxRate < 0 || line.TaxRate > 1 || line.ExchangeRate <= 0) throw ProcedureRows.Protocol( "purchase.open_sources", "来源数量、单价、税率或汇率越界。"); if (!identities.Add(line.UniqueKey)) throw ProcedureRows.Protocol("purchase.open_sources", "来源行标识重复。"); result.Add(line); } return result; } public BusinessWriteResult CreatePurchaseDocument( PurchaseInvoiceCreateRequest request, CommandExecutionContext context, string idempotencyKey, string inputFingerprint) { if (request == null || request.Draft == null) throw new ArgumentNullException("request"); return _gateway.Write( Workflow, "create_document", ModuleCode, RequireContext(context), WorkflowProcedurePayload.From(request), idempotencyKey, inputFingerprint); } private static CommandExecutionContext RequireContext(CommandExecutionContext context) { if (context == null) throw new ArgumentNullException("context"); return context; } private DataTable ReadCandidates( string action, CommandExecutionContext context, IDictionary payload) { DataTable table = _gateway.Read( Workflow, action, ModuleCode, RequireContext(context), payload); if (table == null || table.Rows.Count > 20) throw ProcedureRows.Protocol( "purchase." + action, "候选结果为空或超过 20 行。"); return table; } } public sealed class ProcedureLeaveWorkflowAdapter : ILeaveWorkflowAdapter, IContextualLeaveValidationProvider, ILeaveIntentResolver, IContextualBusinessWorkflowAdapterReadiness { private const string Workflow = "leave"; private readonly IWorkflowProcedureGateway _gateway; private readonly IBusinessAdapterReadinessAttestor _readinessAttestor; public ProcedureLeaveWorkflowAdapter(string moduleCode, IWorkflowProcedureGateway gateway) : this(moduleCode, gateway, null) { } public ProcedureLeaveWorkflowAdapter( string moduleCode, IWorkflowProcedureGateway gateway, IBusinessAdapterReadinessAttestor readinessAttestor) { if (string.IsNullOrWhiteSpace(moduleCode)) throw new ArgumentException("请假模块编号不能为空。", "moduleCode"); if (gateway == null) throw new ArgumentNullException("gateway"); ModuleCode = moduleCode.Trim(); _gateway = gateway; _readinessAttestor = readinessAttestor; } public string ModuleCode { get; private set; } public BusinessAdapterReadiness GetReadiness() { return GetReadiness(null); } public BusinessAdapterReadiness GetReadiness(CommandExecutionContext context) { if (context == null) return GetReadinessWithoutContext(); IBusinessAdapterReadinessOverride readinessOverride = _readinessAttestor as IBusinessAdapterReadinessOverride; if (readinessOverride != null) { IContextualBusinessAdapterReadinessOverride contextualOverride = _readinessAttestor as IContextualBusinessAdapterReadinessOverride; if (contextualOverride == null) throw ContextualReadinessRequired(); return contextualOverride.GetReadiness(Workflow, ModuleCode, context); } IContextualWorkflowProcedureGateway contextualGateway = _gateway as IContextualWorkflowProcedureGateway; if (contextualGateway == null) throw ContextualReadinessRequired(); BusinessAdapterReadiness readiness = contextualGateway.GetReadiness( Workflow, ModuleCode, context); return _readinessAttestor == null ? readiness : _readinessAttestor.Attest(Workflow, ModuleCode, readiness); } private BusinessAdapterReadiness GetReadinessWithoutContext() { IBusinessAdapterReadinessOverride readinessOverride = _readinessAttestor as IBusinessAdapterReadinessOverride; if (readinessOverride != null) return readinessOverride.GetReadiness(Workflow, ModuleCode); BusinessAdapterReadiness readiness = _gateway.GetReadiness(Workflow, ModuleCode); return _readinessAttestor == null ? readiness : _readinessAttestor.Attest(Workflow, ModuleCode, readiness); } private static CommandKernelException ContextualReadinessRequired() { return new CommandKernelException( "adapter_readiness_context_required", "运行时业务适配器就绪复核必须绑定显式 ERP 会话。", 6); } public string GetCurrentEmployeeId(CommandExecutionContext context) { return ProcedureRows.RequiredString(LeaveContext(context), "current_employee_id"); } public bool CanApplyForOthers(CommandExecutionContext context) { return ProcedureRows.Boolean(LeaveContext(context), "can_apply_for_others"); } public bool IsLeaveTypeEnabled(string leaveTypeCode, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(Read( "type_enabled", context, new Dictionary { { "leaveTypeCode", leaveTypeCode } }), "leave.type_enabled"); return ProcedureRows.Boolean(row, "enabled"); } public bool IsLeaveFlowTypeEnabled( string flowTypeCode, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(Read( "flow_type_enabled", context, new Dictionary { { "flowTypeCode", flowTypeCode } }), "leave.flow_type_enabled"); return ProcedureRows.Boolean(row, "enabled"); } public IList ResolveLeaveTypes( string query, CommandExecutionContext context) { DataTable table = Read( "resolve_type", context, new Dictionary { { "query", query } }); if (table == null || table.Rows.Count > 10) throw ProcedureRows.Protocol( "leave.resolve_type", "结果表不能为空且候选不能超过 10 行。"); List result = new List(); foreach (DataRow row in table.Rows) { result.Add(new LeaveTypeCandidate { Code = ProcedureRows.RequiredString(row, "leave_type_code"), Name = ProcedureRows.RequiredString(row, "leave_type_name") }); } return result; } public IList ResolveLeaveFlowTypes( string employeeId, decimal calculatedHours, string query, CommandExecutionContext context) { DataTable table = Read( "resolve_flow_type", context, new Dictionary { { "employeeId", employeeId }, { "calculatedHours", calculatedHours }, { "query", query } }); if (table == null || table.Rows.Count > 20) throw ProcedureRows.Protocol( "leave.resolve_flow_type", "结果表不能为空且候选不能超过 20 行。"); List result = new List(); foreach (DataRow row in table.Rows) { result.Add(new LeaveFlowTypeCandidate { Code = ProcedureRows.RequiredString(row, "flow_type_code"), Name = ProcedureRows.RequiredString(row, "flow_type_name") }); } return result; } public LeaveCalendarRange ResolveCalendarRange( string employeeId, DateTime localDate, LeaveDayPart dayPart, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(Read( "resolve_calendar_range", context, new Dictionary { { "employeeId", employeeId }, { "localDate", localDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) }, { "dayPart", DayPartName(dayPart) } }), "leave.resolve_calendar_range"); bool available = ProcedureRows.Boolean(row, "available"); LeaveCalendarRange result = new LeaveCalendarRange { Available = available, ReasonCode = ProcedureRows.RequiredString(row, "reason_code") }; if (!available) return result; result.StartLocal = ProcedureRows.DateTime(row, "start_local"); result.EndLocal = ProcedureRows.DateTime(row, "end_local"); result.Hours = ProcedureRows.Decimal(row, "hours"); result.TimeZoneId = ProcedureRows.RequiredString(row, "time_zone_id"); return result; } public DateTime GetCurrentLocalTime(CommandExecutionContext context) { return ProcedureRows.DateTime(LeaveContext(context), "now_local"); } public decimal CalculateHours( string employeeId, DateTime startLocal, DateTime endLocal, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(Read( "calculate_hours", context, TimePayload(employeeId, startLocal, endLocal)), "leave.calculate_hours"); decimal hours = ProcedureRows.Decimal(row, "hours"); if (hours < 0) throw ProcedureRows.Protocol("leave.calculate_hours", "工时不能小于零。"); return hours; } public bool HasConflict( string employeeId, DateTime startLocal, DateTime endLocal, CommandExecutionContext context) { DataRow row = ProcedureRows.Single(Read( "has_conflict", context, TimePayload(employeeId, startLocal, endLocal)), "leave.has_conflict"); return ProcedureRows.Boolean(row, "has_conflict"); } decimal IWorkingTimeCalculator.CalculateHours( string employeeId, DateTime startLocal, DateTime endLocal) { throw new CommandKernelException( "execution_context_required", "请假工时查询必须携带 ERP 执行上下文。", 6); } bool ILeaveConflictProvider.HasConflict( string employeeId, DateTime startLocal, DateTime endLocal) { throw new CommandKernelException( "execution_context_required", "请假冲突查询必须携带 ERP 执行上下文。", 6); } public BusinessWriteResult CreateLeaveDraft( LeaveRequestDraft draft, CommandExecutionContext context, string idempotencyKey, string inputFingerprint) { if (draft == null) throw new ArgumentNullException("draft"); return Write( "create_draft", context, WorkflowProcedurePayload.From(draft), idempotencyKey, inputFingerprint); } public bool CanSubmitLeave( string recordId, CommandExecutionContext context, out string reason) { DataRow row = ProcedureRows.Single(Read( "can_submit", context, new Dictionary { { "recordId", recordId } }), "leave.can_submit"); reason = ProcedureRows.OptionalString(row, "reason"); return ProcedureRows.Boolean(row, "can_submit"); } public BusinessWriteResult SubmitLeave( string recordId, CommandExecutionContext context, string idempotencyKey, string inputFingerprint) { return Write( "submit", context, new Dictionary { { "recordId", recordId } }, idempotencyKey, inputFingerprint); } private DataRow LeaveContext(CommandExecutionContext context) { return ProcedureRows.Single( Read("context", context, new Dictionary()), "leave.context"); } private DataTable Read( string action, CommandExecutionContext context, IDictionary payload) { if (context == null) throw new ArgumentNullException("context"); return _gateway.Read(Workflow, action, ModuleCode, context, payload); } private BusinessWriteResult Write( string action, CommandExecutionContext context, IDictionary payload, string idempotencyKey, string inputFingerprint) { if (context == null) throw new ArgumentNullException("context"); return _gateway.Write( Workflow, action, ModuleCode, context, payload, idempotencyKey, inputFingerprint); } private static IDictionary TimePayload( string employeeId, DateTime startLocal, DateTime endLocal) { return new Dictionary { { "employeeId", employeeId }, { "startLocal", startLocal }, { "endLocal", endLocal } }; } private static string DayPartName(LeaveDayPart dayPart) { switch (dayPart) { case LeaveDayPart.Morning: return "morning"; case LeaveDayPart.Afternoon: return "afternoon"; case LeaveDayPart.FullDay: return "full_day"; default: throw ProcedureRows.Protocol( "leave.resolve_calendar_range", "dayPart 无效。"); } } } internal static class WorkflowProcedurePayload { private static readonly JsonSerializer Serializer = JsonSerializer.Create( new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), NullValueHandling = NullValueHandling.Ignore, TypeNameHandling = TypeNameHandling.None, MaxDepth = 64 }); public static IDictionary From(object value) { if (value == null) throw new ArgumentNullException("value"); lock (Serializer) { return JObject.FromObject(value, Serializer) .ToObject>(); } } } internal static class ProcedureRows { public static DataRow Single(DataTable table, string operation) { if (table == null || table.Rows.Count != 1) throw Protocol(operation, "结果必须恰好包含一行。"); return table.Rows[0]; } public static string RequiredString(DataRow row, string name) { string value = OptionalString(row, name); if (string.IsNullOrWhiteSpace(value)) throw Protocol(name, "缺少必填字符串字段。"); return value.Trim(); } public static string OptionalString(DataRow row, string name) { object value = Value(row, name); return value == null || value == DBNull.Value ? string.Empty : Convert.ToString(value); } public static decimal Decimal(DataRow row, string name) { try { return Convert.ToDecimal(Value(row, name), CultureInfo.InvariantCulture); } catch { throw Protocol(name, "字段不是有效 decimal。"); } } public static bool Boolean(DataRow row, string name) { object value = Value(row, name); if (value is bool) return (bool)value; string text = Convert.ToString(value, CultureInfo.InvariantCulture); if (text == "1" || text.Equals("true", StringComparison.OrdinalIgnoreCase)) return true; if (text == "0" || text.Equals("false", StringComparison.OrdinalIgnoreCase)) return false; throw Protocol(name, "字段不是有效 bool。"); } public static DateTime DateTime(DataRow row, string name) { try { return Convert.ToDateTime(Value(row, name), CultureInfo.InvariantCulture); } catch { throw Protocol(name, "字段不是有效 DateTime。"); } } public static CommandKernelException Protocol(string operation, string message) { return new CommandKernelException( "adapter_protocol_error", "业务过程契约无效(" + operation + "):" + message, 6); } private static object Value(DataRow row, string name) { if (row == null || row.Table == null) throw Protocol(name, "结果行为空。"); DataColumn column = row.Table.Columns.Cast().FirstOrDefault(item => item.ColumnName.Equals(name, StringComparison.OrdinalIgnoreCase)); if (column == null || row[column] == DBNull.Value) throw Protocol(name, "结果缺少字段。"); return row[column]; } } }