281 lines
12 KiB
C#
281 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Lskj.CommandKernel;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Lskj.AgentBridge
|
|
{
|
|
public sealed class BusinessAdapterConfiguration
|
|
{
|
|
private const int MaximumBytes = 64 * 1024;
|
|
|
|
public string SchemaVersion { get; set; }
|
|
public string SourceSha256 { get; private set; }
|
|
public string CustomerProfilePath { get; set; }
|
|
public PurchaseAdapterConfiguration Purchase { get; set; }
|
|
public LeaveAdapterConfiguration Leave { get; set; }
|
|
|
|
public static BusinessAdapterConfiguration Load(string path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
throw Invalid("未配置业务适配器文件路径。");
|
|
try
|
|
{
|
|
string fullPath;
|
|
byte[] sourceBytes = ReadRegularFile(path, out fullPath);
|
|
string source = new UTF8Encoding(false, true).GetString(sourceBytes);
|
|
if (!StrictRuntimeJsonSyntax.IsStandard(source))
|
|
throw Invalid("业务适配器配置必须使用无 BOM、无注释、无尾逗号的标准 JSON。");
|
|
JObject root = JObject.Parse(
|
|
source,
|
|
new JsonLoadSettings
|
|
{
|
|
DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Error,
|
|
CommentHandling = CommentHandling.Ignore,
|
|
LineInfoHandling = LineInfoHandling.Ignore
|
|
});
|
|
EnsureOnly(root,
|
|
"schemaVersion", "customerProfilePath", "purchase", "leave");
|
|
EnsureObject(root, "purchase", "enabled", "acceptanceEvidencePath", "fields", "matchOptions");
|
|
EnsureObject(root, "leave", "enabled", "acceptanceEvidencePath", "fields", "validationOptions");
|
|
EnsureNestedOnly(root, "purchase", "fields",
|
|
"moduleCode", "supplierCode", "invoiceNumber", "invoiceDate", "currencyCode",
|
|
"materialCode", "unit", "quantity", "unitPrice", "taxRate", "exchangeRate", "lineAmount",
|
|
"sourceOrderId", "sourceLineId");
|
|
EnsureNestedOnly(root, "purchase", "matchOptions",
|
|
"quantityTolerance", "unitPriceAbsoluteTolerance", "unitPriceRelativeTolerance",
|
|
"taxRateTolerance", "lineAmountTolerance", "headerAmountTolerance",
|
|
"currencyScale", "lineAmountMode");
|
|
EnsureNestedOnly(root, "leave", "fields",
|
|
"moduleCode", "employeeId", "leaveTypeCode", "flowTypeCode",
|
|
"startLocal", "endLocal", "requestedHours", "reason");
|
|
EnsureNestedOnly(root, "leave", "validationOptions",
|
|
"allowPastStart", "maximumCalendarDays", "minimumReasonLength", "hoursTolerance");
|
|
|
|
BusinessAdapterConfiguration result = root.ToObject<BusinessAdapterConfiguration>();
|
|
if (result == null || result.SchemaVersion != "1.1")
|
|
throw Invalid("业务适配器 schemaVersion 必须为 1.1。");
|
|
if (result.Purchase == null) result.Purchase = new PurchaseAdapterConfiguration();
|
|
if (result.Leave == null) result.Leave = new LeaveAdapterConfiguration();
|
|
result.SourceSha256 = Sha256(sourceBytes);
|
|
string baseDirectory = Path.GetDirectoryName(fullPath);
|
|
if (result.Purchase.Enabled || result.Leave.Enabled)
|
|
result.CustomerProfilePath = NormalizeProfilePath(
|
|
result.CustomerProfilePath,
|
|
baseDirectory);
|
|
result.Purchase.Normalize(baseDirectory);
|
|
result.Leave.Normalize(baseDirectory);
|
|
return result;
|
|
}
|
|
catch (CommandKernelException) { throw; }
|
|
catch
|
|
{
|
|
throw Invalid("业务适配器配置不是有效的严格 UTF-8 JSON。");
|
|
}
|
|
}
|
|
|
|
private static byte[] ReadRegularFile(string path, out string fullPath)
|
|
{
|
|
fullPath = Path.GetFullPath(path);
|
|
FileInfo file = new FileInfo(fullPath);
|
|
if (!file.Exists)
|
|
throw Invalid("业务适配器配置文件不存在。");
|
|
FileAttributes attributes = file.Attributes;
|
|
if ((attributes & (FileAttributes.Directory
|
|
| FileAttributes.Device
|
|
| FileAttributes.ReparsePoint)) != 0)
|
|
{
|
|
throw Invalid("业务适配器配置必须是普通文件,不能是目录、设备或链接。");
|
|
}
|
|
|
|
using (FileStream stream = new FileStream(
|
|
fullPath,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read))
|
|
{
|
|
if (stream.Length <= 0 || stream.Length > MaximumBytes)
|
|
throw Invalid("业务适配器配置为空或超过大小上限。");
|
|
byte[] result = new byte[(int)stream.Length];
|
|
int offset = 0;
|
|
while (offset < result.Length)
|
|
{
|
|
int count = stream.Read(result, offset, result.Length - offset);
|
|
if (count <= 0)
|
|
throw Invalid("业务适配器配置读取不完整。");
|
|
offset += count;
|
|
}
|
|
if (stream.ReadByte() != -1)
|
|
throw Invalid("业务适配器配置读取期间发生变化。");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private static void EnsureObject(JObject root, string name, params string[] allowed)
|
|
{
|
|
JToken token = root[name];
|
|
if (token == null) return;
|
|
JObject value = token as JObject;
|
|
if (value == null) throw Invalid(name + " 必须是 JSON 对象。");
|
|
EnsureOnly(value, allowed);
|
|
}
|
|
|
|
private static void EnsureNestedOnly(
|
|
JObject root,
|
|
string parent,
|
|
string child,
|
|
params string[] allowed)
|
|
{
|
|
JObject parentObject = root[parent] as JObject;
|
|
if (parentObject == null || parentObject[child] == null) return;
|
|
JObject value = parentObject[child] as JObject;
|
|
if (value == null) throw Invalid(parent + "." + child + " 必须是 JSON 对象。");
|
|
EnsureOnly(value, allowed);
|
|
}
|
|
|
|
private static void EnsureOnly(JObject value, params string[] allowed)
|
|
{
|
|
HashSet<string> names = new HashSet<string>(allowed, StringComparer.Ordinal);
|
|
JProperty unknown = value.Properties().FirstOrDefault(item => !names.Contains(item.Name));
|
|
if (unknown != null)
|
|
throw Invalid("业务适配器配置包含未知字段:" + unknown.Name);
|
|
}
|
|
|
|
private static CommandKernelException Invalid(string message)
|
|
{
|
|
return new CommandKernelException("adapter_config_invalid", message, 6);
|
|
}
|
|
|
|
private static string NormalizeProfilePath(
|
|
string value,
|
|
string baseDirectory)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
throw Invalid("已启用业务适配器时必须配置 customerProfilePath。");
|
|
string candidate = value.Trim();
|
|
return Path.GetFullPath(Path.IsPathRooted(candidate)
|
|
? candidate
|
|
: Path.Combine(baseDirectory ?? string.Empty, candidate));
|
|
}
|
|
|
|
private static string Sha256(byte[] value)
|
|
{
|
|
using (SHA256 sha = SHA256.Create())
|
|
{
|
|
byte[] hash = sha.ComputeHash(value);
|
|
StringBuilder result = new StringBuilder(hash.Length * 2);
|
|
foreach (byte item in hash) result.Append(item.ToString("x2"));
|
|
return result.ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class PurchaseAdapterConfiguration
|
|
{
|
|
public PurchaseAdapterConfiguration()
|
|
{
|
|
MatchOptions = new PurchaseInvoiceMatchOptions();
|
|
}
|
|
|
|
public bool Enabled { get; set; }
|
|
public string AcceptanceEvidencePath { get; set; }
|
|
public PurchaseWorkflowFieldMap Fields { get; set; }
|
|
public PurchaseInvoiceMatchOptions MatchOptions { get; set; }
|
|
|
|
internal void Normalize(string baseDirectory)
|
|
{
|
|
if (!Enabled) return;
|
|
if (Fields == null || string.IsNullOrWhiteSpace(Fields.ModuleCode))
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"已启用的采购适配器缺少 fields.moduleCode。",
|
|
6);
|
|
AcceptanceEvidencePath = NormalizeEvidencePath(
|
|
AcceptanceEvidencePath, baseDirectory, "采购");
|
|
if (MatchOptions == null) MatchOptions = new PurchaseInvoiceMatchOptions();
|
|
PurchaseInvoiceMatcher.Match(
|
|
new PurchaseInvoiceDraft(),
|
|
new List<PurchaseSourceLine>(),
|
|
MatchOptions);
|
|
if (MatchOptions.LineAmountMode == InvoiceLineAmountMode.None)
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"采购 matchOptions 超出允许范围,且 lineAmountMode 必须明确为不含税或含税。",
|
|
6);
|
|
}
|
|
|
|
private static string NormalizeEvidencePath(
|
|
string value,
|
|
string baseDirectory,
|
|
string workflowName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"已启用的" + workflowName + "适配器缺少 acceptanceEvidencePath。",
|
|
6);
|
|
string candidate = value.Trim();
|
|
return Path.GetFullPath(Path.IsPathRooted(candidate)
|
|
? candidate
|
|
: Path.Combine(baseDirectory ?? string.Empty, candidate));
|
|
}
|
|
}
|
|
|
|
public sealed class LeaveAdapterConfiguration
|
|
{
|
|
public LeaveAdapterConfiguration()
|
|
{
|
|
ValidationOptions = new LeaveValidationOptions();
|
|
}
|
|
|
|
public bool Enabled { get; set; }
|
|
public string AcceptanceEvidencePath { get; set; }
|
|
public LeaveWorkflowFieldMap Fields { get; set; }
|
|
public LeaveValidationOptions ValidationOptions { get; set; }
|
|
|
|
internal void Normalize(string baseDirectory)
|
|
{
|
|
if (!Enabled) return;
|
|
if (Fields == null || string.IsNullOrWhiteSpace(Fields.ModuleCode))
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"已启用的请假适配器缺少 fields.moduleCode。",
|
|
6);
|
|
AcceptanceEvidencePath = NormalizeEvidencePath(
|
|
AcceptanceEvidencePath, baseDirectory, "请假");
|
|
if (ValidationOptions == null) ValidationOptions = new LeaveValidationOptions();
|
|
if (ValidationOptions.MaximumCalendarDays <= 0
|
|
|| ValidationOptions.MaximumCalendarDays > 31
|
|
|| ValidationOptions.MinimumReasonLength < 0
|
|
|| ValidationOptions.MinimumReasonLength > 500
|
|
|| ValidationOptions.HoursTolerance < 0
|
|
|| ValidationOptions.HoursTolerance > 1m)
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"请假 validationOptions 超出允许范围。",
|
|
6);
|
|
}
|
|
|
|
private static string NormalizeEvidencePath(
|
|
string value,
|
|
string baseDirectory,
|
|
string workflowName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
throw new CommandKernelException(
|
|
"adapter_config_invalid",
|
|
"已启用的" + workflowName + "适配器缺少 acceptanceEvidencePath。",
|
|
6);
|
|
string candidate = value.Trim();
|
|
return Path.GetFullPath(Path.IsPathRooted(candidate)
|
|
? candidate
|
|
: Path.Combine(baseDirectory ?? string.Empty, candidate));
|
|
}
|
|
}
|
|
}
|