Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/WorkflowProcedureAdapters.cs
T
2026-08-14 14:28:28 +08:00

778 lines
30 KiB
C#

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<string, object> payload);
BusinessWriteResult Write(
string workflow,
string action,
string moduleCode,
CommandExecutionContext context,
IDictionary<string, object> payload,
string idempotencyKey,
string inputFingerprint);
}
/// <summary>
/// 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.
/// </summary>
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<string, object>
{
{ "supplierCode", supplierCode },
{ "invoiceNumber", invoiceNumber }
}), "purchase.invoice_exists");
return ProcedureRows.Boolean(row, "exists");
}
public IList<PurchaseSupplierCandidate> ResolveSuppliers(
string reference,
string taxId,
CommandExecutionContext context)
{
DataTable table = ReadCandidates(
"resolve_supplier",
context,
new Dictionary<string, object>
{
{ "reference", reference },
{ "taxId", taxId }
});
List<PurchaseSupplierCandidate> result =
new List<PurchaseSupplierCandidate>();
HashSet<string> codes = new HashSet<string>(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<PurchaseCurrencyCandidate> ResolveCurrencies(
string reference,
CommandExecutionContext context)
{
DataTable table = ReadCandidates(
"resolve_currency",
context,
new Dictionary<string, object> { { "reference", reference } });
List<PurchaseCurrencyCandidate> result =
new List<PurchaseCurrencyCandidate>();
HashSet<string> codes = new HashSet<string>(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<PurchaseMaterialCandidate> ResolveMaterials(
PurchaseInvoiceIntentLine line,
string supplierCode,
CommandExecutionContext context)
{
if (line == null) throw new ArgumentNullException("line");
DataTable table = ReadCandidates(
"resolve_material",
context,
new Dictionary<string, object>
{
{ "lineId", line.LineId },
{ "reference", line.MaterialReference },
{ "specification", line.Specification },
{ "unit", line.Unit },
{ "supplierCode", supplierCode }
});
List<PurchaseMaterialCandidate> result =
new List<PurchaseMaterialCandidate>();
HashSet<string> codes = new HashSet<string>(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<PurchaseSourceLine> 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<PurchaseSourceLine> result = new List<PurchaseSourceLine>();
HashSet<string> identities = new HashSet<string>(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<string, object> 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<string, object> { { "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<string, object>
{
{ "flowTypeCode", flowTypeCode }
}),
"leave.flow_type_enabled");
return ProcedureRows.Boolean(row, "enabled");
}
public IList<LeaveTypeCandidate> ResolveLeaveTypes(
string query,
CommandExecutionContext context)
{
DataTable table = Read(
"resolve_type",
context,
new Dictionary<string, object> { { "query", query } });
if (table == null || table.Rows.Count > 10)
throw ProcedureRows.Protocol(
"leave.resolve_type",
"结果表不能为空且候选不能超过 10 行。");
List<LeaveTypeCandidate> result = new List<LeaveTypeCandidate>();
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<LeaveFlowTypeCandidate> ResolveLeaveFlowTypes(
string employeeId,
decimal calculatedHours,
string query,
CommandExecutionContext context)
{
DataTable table = Read(
"resolve_flow_type",
context,
new Dictionary<string, object>
{
{ "employeeId", employeeId },
{ "calculatedHours", calculatedHours },
{ "query", query }
});
if (table == null || table.Rows.Count > 20)
throw ProcedureRows.Protocol(
"leave.resolve_flow_type",
"结果表不能为空且候选不能超过 20 行。");
List<LeaveFlowTypeCandidate> result =
new List<LeaveFlowTypeCandidate>();
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<string, object>
{
{ "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<string, object> { { "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<string, object> { { "recordId", recordId } },
idempotencyKey,
inputFingerprint);
}
private DataRow LeaveContext(CommandExecutionContext context)
{
return ProcedureRows.Single(
Read("context", context, new Dictionary<string, object>()),
"leave.context");
}
private DataTable Read(
string action,
CommandExecutionContext context,
IDictionary<string, object> payload)
{
if (context == null) throw new ArgumentNullException("context");
return _gateway.Read(Workflow, action, ModuleCode, context, payload);
}
private BusinessWriteResult Write(
string action,
CommandExecutionContext context,
IDictionary<string, object> 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<string, object> TimePayload(
string employeeId,
DateTime startLocal,
DateTime endLocal)
{
return new Dictionary<string, object>
{
{ "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<string, object> From(object value)
{
if (value == null) throw new ArgumentNullException("value");
lock (Serializer)
{
return JObject.FromObject(value, Serializer)
.ToObject<Dictionary<string, object>>();
}
}
}
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<DataColumn>().FirstOrDefault(item =>
item.ColumnName.Equals(name, StringComparison.OrdinalIgnoreCase));
if (column == null || row[column] == DBNull.Value)
throw Protocol(name, "结果缺少字段。");
return row[column];
}
}
}