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

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));
}
}
}