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

951 lines
39 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Lskj.CommandKernel;
namespace Lskj.AgentBridge
{
public sealed class WorkflowContractProbeRequest
{
public string SchemaVersion { get; set; }
public string Workflow { get; set; }
public string ModuleCode { get; set; }
public PurchaseContractProbe Purchase { get; set; }
public LeaveContractProbe Leave { get; set; }
}
public sealed class PurchaseContractProbe
{
public PurchaseContractProbe()
{
MatchOptions = new PurchaseInvoiceMatchOptions();
}
public PurchaseInvoiceDraft Draft { get; set; }
public PurchaseInvoiceMatchOptions MatchOptions { get; set; }
}
public sealed class LeaveContractProbe
{
public string EmployeeReference { get; set; }
public string LeaveTypeText { get; set; }
public string FlowTypeText { get; set; }
public string DateExpression { get; set; }
public decimal RequestedHours { get; set; }
public string Reason { get; set; }
public string ExistingRecordId { get; set; }
}
public sealed class WorkflowContractCheck
{
public WorkflowContractCheck()
{
Metrics = new Dictionary<string, object>(StringComparer.Ordinal);
}
public string Code { get; set; }
public string Action { get; set; }
public bool Passed { get; set; }
public string ErrorCode { get; set; }
public string Message { get; set; }
public IDictionary<string, object> Metrics { get; private set; }
}
public sealed class WorkflowContractVerificationResult
{
public WorkflowContractVerificationResult()
{
SchemaVersion = "1.0";
ReadOnly = true;
RegistrationReady = false;
Checks = new List<WorkflowContractCheck>();
RequiredRemainingEvidence = new List<string>
{
"customer_configuration",
"parameterized_read_queries",
"transactional_write",
"persistent_idempotency",
"permission_recheck",
"windows_integration",
"signed_acceptance_manifest",
"scoped_v2_readiness"
};
}
public string SchemaVersion { get; set; }
public string Workflow { get; set; }
public string ModuleCode { get; set; }
public DateTime CompletedAtUtc { get; set; }
public bool ReadOnly { get; set; }
public bool Verified { get; set; }
public bool RegistrationReady { get; set; }
public IList<WorkflowContractCheck> Checks { get; private set; }
public IList<string> RequiredRemainingEvidence { get; private set; }
}
/// <summary>
/// 对客户固定白名单只读过程做行为与返回契约验证。该服务永远不调用 Write,
/// 也不会把探针中的业务值复制到结果或日志。
/// </summary>
public sealed class WorkflowContractVerifier
{
private static readonly Regex SafeModule = new Regex(
@"^[A-Za-z0-9_.:\-]{1,64}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly IWorkflowProcedureGateway _gateway;
private readonly ISystemClock _clock;
public WorkflowContractVerifier(
IWorkflowProcedureGateway gateway,
ISystemClock clock)
{
if (gateway == null) throw new ArgumentNullException("gateway");
if (clock == null) throw new ArgumentNullException("clock");
_gateway = gateway;
_clock = clock;
}
public WorkflowContractVerificationResult Verify(
WorkflowContractProbeRequest request,
CommandExecutionContext context)
{
ValidateRequest(request, context);
WorkflowContractVerificationResult result =
new WorkflowContractVerificationResult
{
Workflow = request.Workflow,
ModuleCode = request.ModuleCode,
CompletedAtUtc = _clock.UtcNow
};
if (request.Workflow == "purchase")
VerifyPurchase(request, context, result);
else
VerifyLeave(request, context, result);
result.Verified = result.Checks.Count > 0
&& result.Checks.All(item => item.Passed);
return result;
}
private void VerifyPurchase(
WorkflowContractProbeRequest request,
CommandExecutionContext context,
WorkflowContractVerificationResult result)
{
ProcedurePurchaseWorkflowAdapter adapter =
new ProcedurePurchaseWorkflowAdapter(request.ModuleCode, _gateway);
PurchaseInvoiceDraft draft = request.Purchase.Draft;
ProbeOutcome<IList<PurchaseSupplierCandidate>> suppliers = Probe(
result,
"purchase_resolve_supplier_contract",
"purchase.resolve_supplier",
delegate
{
return adapter.ResolveSuppliers(
draft.SupplierCode,
null,
context);
});
if (suppliers.Success)
{
IList<PurchaseSupplierCandidate> candidates = suppliers.Value
?? new List<PurchaseSupplierCandidate>();
bool unique = candidates.Count == 1
&& ValidSupplierCandidate(candidates[0])
&& string.Equals(
candidates[0].Code,
draft.SupplierCode,
StringComparison.OrdinalIgnoreCase);
Add(
result,
"purchase_probe_supplier_is_unique",
"purchase.resolve_supplier",
unique,
unique ? null : "probe_supplier_not_unique",
unique
? "验收探针供应商编码被唯一解析。"
: "验收探针供应商没有唯一解析回相同 ERP 编码。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
}
ProbeOutcome<IList<PurchaseCurrencyCandidate>> currencies = Probe(
result,
"purchase_resolve_currency_contract",
"purchase.resolve_currency",
delegate
{
return adapter.ResolveCurrencies(draft.CurrencyCode, context);
});
if (currencies.Success)
{
IList<PurchaseCurrencyCandidate> candidates = currencies.Value
?? new List<PurchaseCurrencyCandidate>();
bool unique = candidates.Count == 1
&& ValidCurrencyCandidate(candidates[0])
&& string.Equals(
candidates[0].Code,
draft.CurrencyCode,
StringComparison.OrdinalIgnoreCase);
Add(
result,
"purchase_probe_currency_is_unique",
"purchase.resolve_currency",
unique,
unique ? null : "probe_currency_not_unique",
unique
? "验收探针币种编码被唯一解析。"
: "验收探针币种没有唯一解析回相同 ERP 编码。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
}
ProbeOutcome<List<IList<PurchaseMaterialCandidate>>> materials = Probe(
result,
"purchase_resolve_material_contract",
"purchase.resolve_material",
delegate
{
List<IList<PurchaseMaterialCandidate>> values =
new List<IList<PurchaseMaterialCandidate>>();
foreach (PurchaseInvoiceLine line in draft.Lines)
{
values.Add(adapter.ResolveMaterials(
new PurchaseInvoiceIntentLine
{
LineId = line.LineId,
MaterialReference = line.MaterialCode,
SourceOrderHint = line.SourceOrderHint,
Quantity = line.Quantity,
UnitPrice = line.UnitPrice,
TaxRate = line.TaxRate,
LineAmount = line.LineAmount
},
draft.SupplierCode,
context));
}
return values;
});
if (materials.Success)
{
int uniqueCount = 0;
IList<IList<PurchaseMaterialCandidate>> candidateGroups =
materials.Value ?? new List<IList<PurchaseMaterialCandidate>>();
for (int index = 0; index < candidateGroups.Count
&& index < draft.Lines.Count; index += 1)
{
IList<PurchaseMaterialCandidate> candidates =
candidateGroups[index] ?? new List<PurchaseMaterialCandidate>();
if (candidates.Count == 1
&& ValidMaterialCandidate(candidates[0])
&& string.Equals(
candidates[0].Code,
draft.Lines[index].MaterialCode,
StringComparison.OrdinalIgnoreCase))
uniqueCount += 1;
}
bool allUnique = candidateGroups.Count == draft.Lines.Count
&& uniqueCount == draft.Lines.Count;
Add(
result,
"purchase_probe_materials_are_unique",
"purchase.resolve_material",
allUnique,
allUnique ? null : "probe_material_not_unique",
allUnique
? "验收探针所有物料编码均被唯一解析。"
: "验收探针存在没有唯一解析回相同 ERP 编码的物料。",
new Dictionary<string, object>
{
{ "lineCount", draft.Lines.Count },
{ "uniqueMaterialCount", uniqueCount }
});
}
ProbeOutcome<bool> invoiceExists = Probe(
result,
"purchase_invoice_exists_contract",
"purchase.invoice_exists",
delegate
{
return adapter.InvoiceNumberExists(
draft.SupplierCode,
draft.InvoiceNumber,
context);
});
if (invoiceExists.Success)
Add(
result,
"purchase_probe_invoice_must_be_new",
"purchase.invoice_exists",
!invoiceExists.Value,
invoiceExists.Value ? "probe_invoice_already_exists" : null,
invoiceExists.Value
? "验收探针发票号已经存在,请换用不会落库的新探针号码。"
: "验收探针发票号当前不存在。",
new Dictionary<string, object>
{
{ "invoiceExists", invoiceExists.Value }
});
ProbeOutcome<IList<PurchaseSourceLine>> sources = Probe(
result,
"purchase_open_sources_contract",
"purchase.open_sources",
delegate { return adapter.QueryOpenSourceLines(draft, context); });
if (!sources.Success) return;
int sourceCount = sources.Value == null ? 0 : sources.Value.Count;
Add(
result,
"purchase_probe_has_open_source",
"purchase.open_sources",
sourceCount > 0,
sourceCount > 0 ? null : "probe_source_not_found",
sourceCount > 0
? "验收探针查询到开放采购来源。"
: "验收探针没有查询到开放采购来源。",
new Dictionary<string, object> { { "sourceCount", sourceCount } });
ProbeOutcome<PurchaseInvoiceMatchPlan> match = Probe(
result,
"purchase_deterministic_match_contract",
"purchase.source_match",
delegate
{
return PurchaseInvoiceMatcher.Match(
draft,
sources.Value ?? new List<PurchaseSourceLine>(),
request.Purchase.MatchOptions ?? new PurchaseInvoiceMatchOptions());
});
if (!match.Success) return;
int exact = match.Value.Lines.Count(item =>
item.Status == InvoiceLineMatchStatus.Exact);
int ambiguous = match.Value.Lines.Count(item =>
item.Status == InvoiceLineMatchStatus.Ambiguous);
Add(
result,
"purchase_probe_match_is_unique",
"purchase.source_match",
match.Value.Executable,
match.Value.Executable ? null : "probe_source_match_not_unique",
match.Value.Executable
? "验收探针所有明细均唯一匹配。"
: "验收探针存在未匹配、歧义或来源数量冲突。",
new Dictionary<string, object>
{
{ "invoiceLineCount", draft.Lines.Count },
{ "exactMatchCount", exact },
{ "ambiguousMatchCount", ambiguous }
});
}
private void VerifyLeave(
WorkflowContractProbeRequest request,
CommandExecutionContext context,
WorkflowContractVerificationResult result)
{
ProcedureLeaveWorkflowAdapter adapter =
new ProcedureLeaveWorkflowAdapter(request.ModuleCode, _gateway);
LeaveContractProbe probe = request.Leave;
ProbeOutcome<string> employee = Probe(
result,
"leave_context_employee_contract",
"leave.context",
delegate { return adapter.GetCurrentEmployeeId(context); });
ProbeOutcome<DateTime> nowLocal = Probe(
result,
"leave_context_time_contract",
"leave.context",
delegate { return adapter.GetCurrentLocalTime(context); });
if (employee.Success)
{
bool current = IsCurrentEmployee(
probe.EmployeeReference,
employee.Value);
Add(
result,
"leave_probe_uses_current_employee",
"leave.context",
current,
current ? null : "probe_employee_not_current",
current
? "验收探针绑定当前 ERP 员工。"
: "验收探针员工表达不是当前 ERP 员工。",
null);
}
LeaveDateExpressionResult date = nowLocal.Success
? LeaveDateExpressionParser.Parse(probe.DateExpression, nowLocal.Value)
: new LeaveDateExpressionResult
{
Valid = false,
IssueCode = "leave_context_unavailable",
Message = "当前 ERP 本地时间不可用。"
};
Add(
result,
"leave_probe_date_expression",
"leave.date_expression",
date.Valid,
date.Valid ? null : date.IssueCode,
date.Valid
? "自然语言日期和时段已确定性解析。"
: "自然语言日期或时段无法确定性解析。",
date.Valid
? new Dictionary<string, object>
{
{ "dayPart", date.DayPart.ToString().ToLowerInvariant() }
}
: null);
ProbeOutcome<IList<LeaveTypeCandidate>> types = Probe(
result,
"leave_resolve_type_contract",
"leave.resolve_type",
delegate { return adapter.ResolveLeaveTypes(probe.LeaveTypeText, context); });
LeaveTypeCandidate selectedType = null;
if (types.Success)
{
IList<LeaveTypeCandidate> candidates = types.Value
?? new List<LeaveTypeCandidate>();
bool validCandidates = candidates.Count <= 10
&& candidates.All(ValidCandidate)
&& candidates.Select(item => item.Code)
.Distinct(StringComparer.OrdinalIgnoreCase).Count()
== candidates.Count;
Add(
result,
"leave_type_candidates_well_formed",
"leave.resolve_type",
validCandidates,
validCandidates ? null : "leave_type_candidate_invalid",
validCandidates
? "假别候选结构有效。"
: "假别候选为空字段、重复或格式无效。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
bool unique = validCandidates && candidates.Count == 1;
Add(
result,
"leave_probe_type_is_unique",
"leave.resolve_type",
unique,
unique ? null : "probe_leave_type_not_unique",
unique
? "验收探针唯一解析为一个已配置假别候选。"
: "验收探针假别没有唯一解析。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
if (unique) selectedType = candidates[0];
}
if (selectedType != null)
{
ProbeOutcome<bool> enabled = Probe(
result,
"leave_type_enabled_contract",
"leave.type_enabled",
delegate
{
return adapter.IsLeaveTypeEnabled(selectedType.Code, context);
});
if (enabled.Success)
Add(
result,
"leave_probe_type_is_enabled",
"leave.type_enabled",
enabled.Value,
enabled.Value ? null : "probe_leave_type_disabled",
enabled.Value
? "验收探针假别对当前员工可用。"
: "验收探针假别已停用或对当前员工不可用。",
null);
}
LeaveCalendarRange range = null;
if (employee.Success && date.Valid)
{
ProbeOutcome<LeaveCalendarRange> calendar = Probe(
result,
"leave_calendar_range_contract",
"leave.resolve_calendar_range",
delegate
{
return adapter.ResolveCalendarRange(
employee.Value,
date.LocalDate,
date.DayPart,
context);
});
if (calendar.Success)
{
range = calendar.Value;
bool validRange = ValidRange(range, date.LocalDate);
Add(
result,
"leave_probe_calendar_is_available",
"leave.resolve_calendar_range",
validRange,
validRange ? null : "probe_calendar_range_unavailable",
validRange
? "员工日历返回可申请的明确本地时间范围。"
: "员工日历没有返回可申请且结构有效的时间范围。",
range == null ? null : new Dictionary<string, object>
{
{ "available", range.Available },
{ "hours", range.Hours },
{ "hasTimeZone", !string.IsNullOrWhiteSpace(range.TimeZoneId) }
});
if (!validRange) range = null;
}
}
LeaveFlowTypeCandidate selectedFlowType = null;
if (employee.Success && range != null)
{
ProbeOutcome<IList<LeaveFlowTypeCandidate>> flowTypes = Probe(
result,
"leave_resolve_flow_type_contract",
"leave.resolve_flow_type",
delegate
{
return adapter.ResolveLeaveFlowTypes(
employee.Value,
range.Hours,
probe.FlowTypeText,
context);
});
if (flowTypes.Success)
{
IList<LeaveFlowTypeCandidate> candidates = flowTypes.Value
?? new List<LeaveFlowTypeCandidate>();
bool validCandidates = candidates.Count <= 20
&& candidates.All(ValidFlowTypeCandidate)
&& candidates.Select(item => item.Code)
.Distinct(StringComparer.OrdinalIgnoreCase).Count()
== candidates.Count;
Add(
result,
"leave_flow_type_candidates_well_formed",
"leave.resolve_flow_type",
validCandidates,
validCandidates ? null : "leave_flow_type_candidate_invalid",
validCandidates
? "流转类别候选结构有效。"
: "流转类别候选为空字段、重复或格式无效。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
bool unique = validCandidates && candidates.Count == 1;
Add(
result,
"leave_probe_flow_type_is_unique",
"leave.resolve_flow_type",
unique,
unique ? null : "probe_leave_flow_type_not_unique",
unique
? "验收探针唯一解析为一个已配置流转类别。"
: "验收探针流转类别没有唯一解析。",
new Dictionary<string, object>
{
{ "candidateCount", candidates.Count }
});
if (unique) selectedFlowType = candidates[0];
}
}
if (selectedFlowType != null)
{
ProbeOutcome<bool> enabled = Probe(
result,
"leave_flow_type_enabled_contract",
"leave.flow_type_enabled",
delegate
{
return adapter.IsLeaveFlowTypeEnabled(
selectedFlowType.Code,
context);
});
if (enabled.Success)
Add(
result,
"leave_probe_flow_type_is_enabled",
"leave.flow_type_enabled",
enabled.Value,
enabled.Value ? null : "probe_leave_flow_type_disabled",
enabled.Value
? "验收探针流转类别属于当前请假模块且已启用。"
: "验收探针流转类别已停用或不属于当前请假模块。",
null);
}
if (employee.Success && range != null)
{
ProbeOutcome<decimal> hours = Probe(
result,
"leave_calculate_hours_contract",
"leave.calculate_hours",
delegate
{
return adapter.CalculateHours(
employee.Value,
range.StartLocal,
range.EndLocal,
context);
});
if (hours.Success)
{
bool same = Math.Abs(hours.Value - range.Hours) <= 0.01m;
bool requestedMatches = probe.RequestedHours <= 0
|| Math.Abs(probe.RequestedHours - hours.Value) <= 0.01m;
Add(
result,
"leave_probe_hours_are_consistent",
"leave.calculate_hours",
same && requestedMatches,
same && requestedMatches ? null : "probe_leave_hours_mismatch",
same && requestedMatches
? "日历范围、核算工时和探针工时一致。"
: "日历范围、核算工时或探针工时不一致。",
new Dictionary<string, object>
{
{ "calendarHours", range.Hours },
{ "calculatedHours", hours.Value }
});
}
ProbeOutcome<bool> conflict = Probe(
result,
"leave_has_conflict_contract",
"leave.has_conflict",
delegate
{
return adapter.HasConflict(
employee.Value,
range.StartLocal,
range.EndLocal,
context);
});
if (conflict.Success)
Add(
result,
"leave_probe_has_no_conflict",
"leave.has_conflict",
!conflict.Value,
conflict.Value ? "probe_leave_conflict" : null,
conflict.Value
? "验收探针时间段存在请假冲突。"
: "验收探针时间段没有请假冲突。",
new Dictionary<string, object>
{
{ "hasConflict", conflict.Value }
});
}
ProbeOutcome<SubmitEligibility> submit = Probe(
result,
"leave_can_submit_contract",
"leave.can_submit",
delegate
{
string reason;
bool allowed = adapter.CanSubmitLeave(
probe.ExistingRecordId,
context,
out reason);
return new SubmitEligibility
{
Allowed = allowed,
HasReason = !string.IsNullOrWhiteSpace(reason)
};
});
if (submit.Success)
Add(
result,
"leave_probe_record_is_submittable",
"leave.can_submit",
submit.Value.Allowed,
submit.Value.Allowed ? null : "probe_leave_record_not_submittable",
submit.Value.Allowed
? "验收草稿当前可以进入审批流。"
: "验收草稿当前不可提交,请准备可提交的测试草稿。",
new Dictionary<string, object>
{
{ "canSubmit", submit.Value.Allowed },
{ "hasReason", submit.Value.HasReason }
});
}
private static ProbeOutcome<T> Probe<T>(
WorkflowContractVerificationResult result,
string code,
string action,
Func<T> operation)
{
try
{
T value = operation();
Add(result, code, action, true, null, "只读过程返回契约有效。", null);
return new ProbeOutcome<T> { Success = true, Value = value };
}
catch (CommandKernelException error)
{
Add(
result,
code,
action,
false,
error.Code,
"只读过程未通过契约验证。",
null);
}
catch
{
Add(
result,
code,
action,
false,
"adapter_contract_unexpected_error",
"只读过程验证发生未分类错误。",
null);
}
return new ProbeOutcome<T>();
}
private static void Add(
WorkflowContractVerificationResult result,
string code,
string action,
bool passed,
string errorCode,
string message,
IDictionary<string, object> metrics)
{
WorkflowContractCheck check = new WorkflowContractCheck
{
Code = code,
Action = action,
Passed = passed,
ErrorCode = errorCode,
Message = message
};
if (metrics != null)
{
foreach (KeyValuePair<string, object> item in metrics)
check.Metrics[item.Key] = item.Value;
}
result.Checks.Add(check);
}
private static void ValidateRequest(
WorkflowContractProbeRequest request,
CommandExecutionContext context)
{
if (request == null
|| request.SchemaVersion != "1.0"
|| (request.Workflow != "purchase" && request.Workflow != "leave")
|| string.IsNullOrWhiteSpace(request.ModuleCode)
|| !SafeModule.IsMatch(request.ModuleCode)
|| context == null
|| string.IsNullOrWhiteSpace(context.UserId)
|| string.IsNullOrWhiteSpace(context.AccountBook)
|| string.IsNullOrWhiteSpace(context.SubSystemId))
throw Invalid("探针版本、工作流、模块或 ERP 会话范围无效。");
if (request.Workflow == "purchase")
{
if (request.Purchase == null || request.Leave != null)
throw Invalid("采购探针必须且只能包含 purchase。");
ValidatePurchaseProbe(request.Purchase);
}
else
{
if (request.Leave == null || request.Purchase != null)
throw Invalid("请假探针必须且只能包含 leave。");
ValidateLeaveProbe(request.Leave);
}
}
private static void ValidatePurchaseProbe(PurchaseContractProbe probe)
{
PurchaseInvoiceDraft draft = probe == null ? null : probe.Draft;
if (draft == null
|| !SafeText(draft.SupplierCode, 128)
|| !SafeText(draft.CurrencyCode, 32)
|| !SafeText(draft.InvoiceNumber, 128)
|| draft.InvoiceDate.Kind != DateTimeKind.Unspecified
|| draft.InvoiceDate.TimeOfDay != TimeSpan.Zero
|| draft.InvoiceDate < new DateTime(1900, 1, 1)
|| draft.InvoiceDate > new DateTime(2100, 12, 31)
|| draft.TotalWithoutTax < 0
|| draft.TaxAmount < 0
|| draft.TotalWithTax <= 0
|| draft.Lines == null
|| draft.Lines.Count == 0
|| draft.Lines.Count > 200)
throw Invalid("采购探针主信息或明细数量无效。");
foreach (PurchaseInvoiceLine line in draft.Lines)
{
if (line == null
|| !SafeText(line.LineId, 128)
|| !SafeText(line.MaterialCode, 128)
|| (!string.IsNullOrWhiteSpace(line.SourceOrderHint)
&& !SafeText(line.SourceOrderHint, 128))
|| line.Quantity <= 0
|| line.UnitPrice < 0
|| line.TaxRate < 0
|| line.TaxRate > 1
|| line.TaxAmount < 0
|| line.LineAmount < 0)
throw Invalid("采购探针明细字段或数值无效。");
}
if (draft.Lines.Select(item => item.LineId.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase).Count() != draft.Lines.Count)
throw Invalid("采购探针明细行 ID 必须唯一。");
if (probe.MatchOptions == null
|| probe.MatchOptions.LineAmountMode == InvoiceLineAmountMode.None)
throw Invalid("采购探针必须明确含税或不含税金额模式。");
try
{
PurchaseInvoiceMatcher.Match(
draft,
new List<PurchaseSourceLine>(),
probe.MatchOptions ?? new PurchaseInvoiceMatchOptions());
}
catch
{
throw Invalid("采购探针匹配容差无效。");
}
}
private static void ValidateLeaveProbe(LeaveContractProbe probe)
{
if (probe == null
|| (!string.IsNullOrWhiteSpace(probe.EmployeeReference)
&& !SafeText(probe.EmployeeReference, 64))
|| !SafeText(probe.LeaveTypeText, 128)
|| !SafeText(probe.FlowTypeText, 128)
|| !SafeText(probe.DateExpression, 64)
|| !SafeText(probe.Reason, 500)
|| !SafeText(probe.ExistingRecordId, 128)
|| probe.RequestedHours < 0
|| probe.RequestedHours > 24)
throw Invalid("请假探针字段或工时无效。");
}
private static bool ValidCandidate(LeaveTypeCandidate item)
{
return item != null
&& SafeCode(item.Code, 64)
&& SafeText(item.Name, 128);
}
private static bool ValidFlowTypeCandidate(LeaveFlowTypeCandidate item)
{
return item != null
&& SafeCode(item.Code, 64)
&& SafeText(item.Name, 128);
}
private static bool ValidSupplierCandidate(PurchaseSupplierCandidate item)
{
return item != null
&& SafeCode(item.Code, 128)
&& SafeText(item.Name, 256)
&& (string.IsNullOrWhiteSpace(item.TaxId)
|| SafeText(item.TaxId, 64));
}
private static bool ValidCurrencyCandidate(PurchaseCurrencyCandidate item)
{
return item != null
&& SafeCode(item.Code, 32)
&& SafeText(item.Name, 128);
}
private static bool ValidMaterialCandidate(PurchaseMaterialCandidate item)
{
return item != null
&& SafeCode(item.Code, 128)
&& SafeText(item.Name, 256)
&& (string.IsNullOrWhiteSpace(item.Specification)
|| SafeText(item.Specification, 256))
&& (string.IsNullOrWhiteSpace(item.Unit)
|| SafeText(item.Unit, 64));
}
private static bool ValidRange(LeaveCalendarRange range, DateTime date)
{
return range != null
&& range.Available
&& SafeCode(range.ReasonCode, 64)
&& range.StartLocal.Date == date.Date
&& range.EndLocal > range.StartLocal
&& range.EndLocal <= date.Date.AddDays(2)
&& range.Hours > 0
&& range.Hours <= 24
&& SafeText(range.TimeZoneId, 128);
}
private static bool IsCurrentEmployee(string reference, string employeeId)
{
if (!SafeCode(employeeId, 64)) return false;
string value = (reference ?? string.Empty).Trim();
return value.Length == 0
|| value == "我"
|| value == "本人"
|| value == "自己"
|| value.Equals(employeeId.Trim(), StringComparison.OrdinalIgnoreCase);
}
private static bool SafeCode(string value, int maximum)
{
if (!SafeText(value, maximum)) return false;
foreach (char item in value.Trim())
{
if (!char.IsLetterOrDigit(item)
&& item != '_' && item != '-' && item != '.' && item != ':')
return false;
}
return true;
}
private static bool SafeText(string value, int maximum)
{
if (string.IsNullOrWhiteSpace(value)
|| value.Trim().Length > maximum)
return false;
return !value.Any(char.IsControl);
}
private static CommandKernelException Invalid(string message)
{
return new CommandKernelException(
"adapter_contract_probe_invalid",
message,
2);
}
private sealed class ProbeOutcome<T>
{
public bool Success;
public T Value;
}
private sealed class SubmitEligibility
{
public bool Allowed;
public bool HasReason;
}
}
}