Files
zszq-trs/YLErpDAL/Modules/RiskEngine/StructuredRuleExecutor.cs
T
ruisu 8fb3c4392c feat:风控触发时显示应用ID、
新老风控组合校验:
老风控Error/Warning,新风控需审批——点击特批即两边都放行
老风控Error/Warning,新风控禁止——不展示
老风控Error/Warning——展示特批,且只特批老风控。
新增风控流程日志
2026-08-05 14:39:01 +08:00

812 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using YLErp.Commons;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine.Dto;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// 结构化规则执行器。
/// 用于执行现有 ConditionJson:变量取值、按 DataType 转换并比较阈值;复杂业务计算放在变量表达式中完成。
/// </summary>
public static class StructuredRuleExecutor
{
/// <summary>
/// 变量表达式编译缓存。Key 包含变量 ID、版本、更新时间和表达式哈希,避免变量变更后复用旧委托。
/// </summary>
private static readonly ConcurrentDictionary<string, Func<RiskContext, object>> VariableCompiledCache = new ConcurrentDictionary<string, Func<RiskContext, object>>();
/// <summary>
/// 按变量 ID 增量移除变量表达式编译缓存。
/// </summary>
public static void RemoveVariableCompiledCache(long variableId)
{
var keyPrefix = variableId + ":";
// 缓存 Key 以变量 ID 开头,变量修改/删除时只清理该变量相关的编译结果。
foreach (var key in VariableCompiledCache.Keys.Where(k => k.StartsWith(keyPrefix, StringComparison.Ordinal)))
{
VariableCompiledCache.TryRemove(key, out _);
}
}
/// <summary>
/// 按变量 ID 集合批量移除变量表达式编译缓存。
/// </summary>
public static void RemoveVariableCompiledCache(IEnumerable<long> variableIds)
{
if (variableIds == null)
return;
var idSet = new HashSet<long>(variableIds.Distinct());
if (idSet.Count == 0)
return;
// 批量移除时只扫描一次缓存 Key,避免变量数较多时重复遍历整个缓存。
foreach (var key in VariableCompiledCache.Keys)
{
var separatorIndex = key.IndexOf(':');
if (separatorIndex <= 0)
continue;
if (long.TryParse(key.Substring(0, separatorIndex), NumberStyles.None, CultureInfo.InvariantCulture, out var variableId) && idSet.Contains(variableId))
{
VariableCompiledCache.TryRemove(key, out _);
}
}
}
/// <summary>
/// 清空全部变量表达式编译缓存。仅用于风控缓存全量刷新场景。
/// </summary>
public static void ClearVariableCompiledCache()
{
VariableCompiledCache.Clear();
}
/// <summary>
/// 数值和日期类型支持的比较操作符。
/// </summary>
private static readonly HashSet<string> ComparableOperators = new HashSet<string>(StringComparer.Ordinal)
{
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
};
/// <summary>
/// 布尔类型支持的判断操作符。
/// </summary>
private static readonly HashSet<string> BooleanOperators = new HashSet<string>(StringComparer.Ordinal)
{
"isTrue", "isFalse"
};
/// <summary>
/// 执行一组结构化条件。当前规则条件之间按 AND 关系处理,任一条件不满足则整条规则不触发。
/// </summary>
public static StructuredRuleExecuteResult Execute(
IReadOnlyList<RuleCondition> conditions,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context)
{
try
{
if (conditions == null || conditions.Count == 0)
return StructuredRuleExecuteResult.Fail("公式条件列表不能为空");
if (variables == null)
return StructuredRuleExecuteResult.Fail("变量列表不能为空");
var triggerMessages = new List<string>();
for (var i = 0; i < conditions.Count; i++)
{
var conditionResult = ExecuteCondition(conditions[i], variables, context, i + 1);
if (!conditionResult.Success)
return conditionResult;
// 结构化规则当前按 AND 执行,任一条件不命中即可短路返回,避免后续变量表达式继续查库。
if (!conditionResult.Triggered)
return StructuredRuleExecuteResult.Ok(false);
if (!string.IsNullOrWhiteSpace(conditionResult.Message))
triggerMessages.Add(conditionResult.Message);
}
// 所有条件都命中时,合并每个条件的实际值/阈值说明,供最终风控结果展示。
return StructuredRuleExecuteResult.Ok(true, string.Join("", triggerMessages));
}
catch (Exception ex)
{
return StructuredRuleExecuteResult.Fail(ex.Message);
}
}
/// <summary>
/// 执行单个结构化条件,负责按变量数据类型分派到布尔、普通比较或区间比较逻辑。
/// </summary>
private static StructuredRuleExecuteResult ExecuteCondition(
RuleCondition condition,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
int conditionIndex)
{
var label = $"条件{conditionIndex}";
if (condition == null)
return StructuredRuleExecuteResult.Fail($"{label}不能为空");
if (!variables.TryGetValue(condition.VariableId, out var variable))
return StructuredRuleExecuteResult.Fail($"{label}:变量 ID '{condition.VariableId}' 不存在");
// 布尔变量没有阈值概念,只能走 isTrue/isFalse 分支,避免被后续数值/日期比较逻辑误处理。
if (variable.DataType == RiskVariableDataType.Boolean)
return ExecuteBooleanCondition(condition, variable, context, label);
if (variable.DataType != RiskVariableDataType.Numeric && variable.DataType != RiskVariableDataType.Date)
return StructuredRuleExecuteResult.Fail($"{label}:不支持的数据类型");
if (!ComparableOperators.Contains(condition.Operator))
return StructuredRuleExecuteResult.Fail($"{label}:操作符 '{condition.Operator}' 不适用于{GetDataTypeName(variable.DataType)}类型变量");
// 左侧条件变量统一先执行并转换成声明的数据类型;后续普通比较和区间比较都复用这个结果。
var left = GetVariableValue(variable, variable.DataType, context, label, "条件变量");
if (!left.Success)
return StructuredRuleExecuteResult.Fail(left.ErrorMessage);
// 区间操作符使用下限/上限字段,不能继续走普通阈值字段。
if (condition.Operator == "between" || condition.Operator == "notBetween")
return ExecuteRangeCondition(condition, variable, left, variables, context, label);
var rangeFieldCheck = EnsureNoRangeFields(condition, label);
if (!rangeFieldCheck.Success)
return rangeFieldCheck;
var right = GetThresholdValue(
condition.ThresholdType,
condition.Value,
condition.ThresholdVariableId,
variable,
variables,
context,
label,
"阈值");
if (!right.Success)
return StructuredRuleExecuteResult.Fail(right.ErrorMessage);
var compareResult = Compare(left.Value, right.Value, variable.DataType, condition.Operator, label);
if (!compareResult.Success)
return compareResult;
return StructuredRuleExecuteResult.Ok(
compareResult.Triggered,
// 未命中时不构造提示,减少字符串拼接开销;只有真正触发才展示实际值和阈值。
compareResult.Triggered ? BuildCompareMessage(variable, left, right, condition.Operator) : null);
}
/// <summary>
/// 执行布尔条件。布尔变量只允许 isTrue/isFalse,不允许配置固定值、变量阈值或区间字段。
/// </summary>
private static StructuredRuleExecuteResult ExecuteBooleanCondition(
RuleCondition condition,
glms_risk_variable variable,
RiskContext context,
string label)
{
if (!BooleanOperators.Contains(condition.Operator))
return StructuredRuleExecuteResult.Fail($"{label}:操作符 '{condition.Operator}' 不适用于布尔类型变量");
var thresholdFieldCheck = EnsureNoThresholdFields(condition, label);
if (!thresholdFieldCheck.Success)
return thresholdFieldCheck;
var value = GetVariableValue(variable, RiskVariableDataType.Boolean, context, label, "条件变量");
if (!value.Success)
return StructuredRuleExecuteResult.Fail(value.ErrorMessage);
var expected = condition.Operator == "isTrue";
var triggered = (bool)value.Value == expected;
return StructuredRuleExecuteResult.Ok(
triggered,
triggered ? BuildBooleanMessage(variable, value, expected) : null);
}
/// <summary>
/// 执行区间条件。下限和上限可以分别来自固定值或变量,并根据开闭区间转换为对应比较操作符。
/// </summary>
private static StructuredRuleExecuteResult ExecuteRangeCondition(
RuleCondition condition,
glms_risk_variable variable,
ValueExecuteResult left,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
string label)
{
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue)
return StructuredRuleExecuteResult.Fail($"{label}:区间条件不得携带普通阈值字段");
if (!condition.IncludeLower.HasValue || !condition.IncludeUpper.HasValue)
return StructuredRuleExecuteResult.Fail($"{label}:区间开闭配置不完整");
// 固定上下限可以提前比较顺序;变量上下限依赖运行时查询结果,留到后面按实际值比较。
var fixedRangeCheck = ValidateFixedRangeOrder(condition, variable.DataType, label);
if (!fixedRangeCheck.Success)
return fixedRangeCheck;
var lower = GetThresholdValue(condition.LowerThresholdType, condition.LowerValue, condition.LowerThresholdVariableId, variable, variables, context, label, "下限");
if (!lower.Success)
return StructuredRuleExecuteResult.Fail(lower.ErrorMessage);
var upper = GetThresholdValue(condition.UpperThresholdType, condition.UpperValue, condition.UpperThresholdVariableId, variable, variables, context, label, "上限");
if (!upper.Success)
return StructuredRuleExecuteResult.Fail(upper.ErrorMessage);
// 将区间开闭配置转换为普通比较:闭区间使用 >=/<=,开区间使用 >/<。
var lowerOperator = condition.IncludeLower == true ? "gte" : "gt";
var upperOperator = condition.IncludeUpper == true ? "lte" : "lt";
var lowerCompare = Compare(left.Value, lower.Value, variable.DataType, lowerOperator, label);
if (!lowerCompare.Success)
return lowerCompare;
var upperCompare = Compare(left.Value, upper.Value, variable.DataType, upperOperator, label);
if (!upperCompare.Success)
return upperCompare;
var inRange = lowerCompare.Triggered && upperCompare.Triggered;
// between 要求落在区间内,notBetween 则取反;提示同样只在命中时构造。
var triggered = condition.Operator == "notBetween" ? !inRange : inRange;
return StructuredRuleExecuteResult.Ok(
triggered,
triggered ? BuildRangeMessage(variable, left, lower, upper, condition.IncludeLower == true, condition.IncludeUpper == true, condition.Operator) : null);
}
/// <summary>
/// 获取阈值。固定阈值直接转换;变量阈值先校验类型和单位,再执行阈值变量表达式。
/// </summary>
private static ValueExecuteResult GetThresholdValue(
string thresholdType,
object fixedValue,
long? thresholdVariableId,
glms_risk_variable conditionVariable,
IReadOnlyDictionary<long, glms_risk_variable> variables,
RiskContext context,
string label,
string thresholdLabel)
{
if (string.Equals(thresholdType, "Fixed", StringComparison.Ordinal))
{
if (thresholdVariableId.HasValue)
return ValueExecuteResult.Fail($"{label}:固定{thresholdLabel}不得携带变量 ID");
var fixedResult = ConvertValue(fixedValue, conditionVariable.DataType, label, $"固定{thresholdLabel}");
if (!fixedResult.Success)
return ValueExecuteResult.Fail($"{fixedResult.ErrorMessage},原始值:{FormatRawValue(fixedValue)}");
return fixedResult;
}
if (string.Equals(thresholdType, "Variable", StringComparison.Ordinal))
{
if (HasValue(fixedValue))
return ValueExecuteResult.Fail($"{label}:变量{thresholdLabel}不得携带固定值");
if (!thresholdVariableId.HasValue)
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量 ID 不能为空");
if (!variables.TryGetValue(thresholdVariableId.Value, out var thresholdVariable))
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量 ID '{thresholdVariableId.Value}' 不存在");
if (thresholdVariable.DataType != conditionVariable.DataType)
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量与条件变量的数据类型不一致");
// 数值变量允许配置单位;变量阈值必须与条件变量单位一致,避免“金额”和“比例”等不同口径被直接比较。
if (conditionVariable.DataType == RiskVariableDataType.Numeric && !UnitsMatch(conditionVariable.Unit, thresholdVariable.Unit))
return ValueExecuteResult.Fail($"{label}{thresholdLabel}变量与条件变量的单位不一致");
return GetVariableValue(thresholdVariable, conditionVariable.DataType, context, label, thresholdLabel);
}
return ValueExecuteResult.Fail($"{label}{thresholdLabel}类型 '{thresholdType}' 不合法,仅支持 Fixed/Variable");
}
/// <summary>
/// 执行变量取值表达式,并将表达式返回值转换为变量声明的数据类型。
/// </summary>
private static ValueExecuteResult GetVariableValue(
glms_risk_variable variable,
RiskVariableDataType dataType,
RiskContext context,
string label,
string valueLabel)
{
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})的取值表达式为空");
var compiled = GetCompiledVariableExpression(variable, out var errorMessage);
if (compiled == null)
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})编译失败:{errorMessage}");
object rawValue;
try
{
rawValue = compiled(context);
}
catch (Exception ex)
{
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id})执行失败:{ex.Message}");
}
// 变量表达式可选择返回 RiskVariableValueDetailValue 参与比较,Message 只用于命中说明。
// 普通变量表达式仍返回原始值,不需要修改历史变量配置。
var detail = rawValue as RiskVariableValueDetail;
var actualRawValue = detail?.Value ?? rawValue;
var converted = ConvertValue(actualRawValue, dataType, label, valueLabel);
if (!converted.Success)
return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id}){converted.ErrorMessage},原始值:{FormatRawValue(actualRawValue)}");
converted.DetailMessage = detail?.Message;
converted.DetailItems = detail?.Items;
return converted;
}
/// <summary>
/// 获取变量表达式的编译委托。缓存 Key 绑定变量版本信息,变量内容变化后会自然生成新 Key。
/// </summary>
private static Func<RiskContext, object> GetCompiledVariableExpression(glms_risk_variable variable, out string errorMessage)
{
var cacheKey = $"{variable.id}:{variable.Version}:{variable.UpdateDate?.Ticks ?? 0}:{StringComparer.Ordinal.GetHashCode(variable.VariableExpr ?? string.Empty)}";
if (VariableCompiledCache.TryGetValue(cacheKey, out var compiled))
{
errorMessage = null;
return compiled;
}
// 变量表达式只负责取原始值,类型转换统一在结构化执行器中处理;这里缓存编译结果,避免每次执行重复 Roslyn 编译。
compiled = RuleCompiler.CompileValueExpression(variable.VariableExpr, out errorMessage);
if (compiled != null)
VariableCompiledCache[cacheKey] = compiled;
return compiled;
}
/// <summary>
/// 按变量数据类型统一转换值。日期固定值只接受 yyyy-MM-dd,避免不同区域格式导致比较结果不一致。
/// </summary>
private static ValueExecuteResult ConvertValue(object value, RiskVariableDataType dataType, string label, string valueLabel)
{
if (!HasValue(value))
return ValueExecuteResult.Fail($"{valueLabel}为空");
if (dataType == RiskVariableDataType.Numeric)
{
if (value is decimal decimalValue)
return ValueExecuteResult.Ok(decimalValue);
// 数据库字段可能返回 double/int/string/JValue,统一按 InvariantCulture 转 decimal,避免服务器区域设置影响小数点解析。
if (decimal.TryParse(GetRawValue(value), NumberStyles.Float, CultureInfo.InvariantCulture, out var numericValue))
return ValueExecuteResult.Ok(numericValue);
return ValueExecuteResult.Fail($"{valueLabel}必须为数字");
}
if (dataType == RiskVariableDataType.Date)
{
if (value is DateTime dateValue)
return ValueExecuteResult.Ok(dateValue.Date);
if (DateTime.TryParseExact(GetRawValue(value), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var fixedDate))
return ValueExecuteResult.Ok(fixedDate.Date);
return ValueExecuteResult.Fail($"{valueLabel}必须为 yyyy-MM-dd 格式的合法日期");
}
if (dataType == RiskVariableDataType.Boolean)
{
if (value is bool boolValue)
return ValueExecuteResult.Ok(boolValue);
if (bool.TryParse(GetRawValue(value), out var parsedBool))
return ValueExecuteResult.Ok(parsedBool);
return ValueExecuteResult.Fail($"{valueLabel}必须为布尔值");
}
return ValueExecuteResult.Fail($"{label}:不支持的数据类型");
}
/// <summary>
/// 比较两个已转换为同一数据类型的值,并应用结构化规则操作符。
/// </summary>
private static StructuredRuleExecuteResult Compare(object left, object right, RiskVariableDataType dataType, string ruleOperator, string label)
{
// 前置 ConvertValue 已保证左右值类型一致,这里只做最终大小关系计算。
var compare = dataType switch
{
RiskVariableDataType.Numeric => ((decimal)left).CompareTo((decimal)right),
RiskVariableDataType.Date => ((DateTime)left).CompareTo((DateTime)right),
_ => throw new ServiceException($"{label}:不支持的数据类型")
};
var result = ruleOperator switch
{
"gt" => compare > 0,
"lt" => compare < 0,
"gte" => compare >= 0,
"lte" => compare <= 0,
"eq" => compare == 0,
"ne" => compare != 0,
_ => throw new ServiceException($"{label}:不支持的操作符'{ruleOperator}'")
};
return StructuredRuleExecuteResult.Ok(result);
}
/// <summary>
/// 构建普通比较条件的命中说明。变量表达式如果返回 RiskVariableValueDetail,会优先带出自定义明细。
/// </summary>
private static string BuildCompareMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult right, string ruleOperator)
{
var parts = new List<string>();
parts.Add($"触发判断:{variable.VariableName}为{FormatDisplayValue(left.Value)},阈值为{FormatDisplayValue(right.Value)},比较关系:{GetOperatorName(ruleOperator)}");
AddCompareDetails(parts, variable, left, right, ruleOperator);
AddDetail(parts, right.DetailMessage);
return string.Join("", parts);
}
/// <summary>
/// 按当前规则阈值重新判断变量返回的逐条明细,并将明细拆分为超过阈值和未超过阈值两组。
/// 该逻辑只在规则已经命中且需要构造提示时执行,不参与规则主判断;未提供逐条明细时保留原始明细文本。
/// </summary>
private static void AddCompareDetails(List<string> parts, glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult right, string ruleOperator)
{
var detailItems = left.DetailItems;
if (detailItems == null || detailItems.Count == 0)
{
AddDetail(parts, left.DetailMessage);
return;
}
AddDetail(parts, left.DetailMessage);
var triggeredItems = new List<string>(detailItems.Count);
var untriggeredItems = new List<string>();
foreach (var detailItem in detailItems)
{
if (detailItem == null || string.IsNullOrWhiteSpace(detailItem.Message))
continue;
if (!TryEvaluateDetailTrigger(detailItem.Value, right.Value, variable.DataType, ruleOperator, out var triggered))
continue;
if (triggered)
triggeredItems.Add(detailItem.Message);
else
untriggeredItems.Add(detailItem.Message);
}
if (triggeredItems.Count > 0)
parts.Add($"{variable.VariableName}超过阈值的记录:" + string.Join("", triggeredItems));
if (untriggeredItems.Count > 0)
parts.Add($"{variable.VariableName}未超过阈值的其他偏离记录:" + string.Join("", untriggeredItems));
}
/// <summary>
/// 复用普通比较条件的类型转换口径,判断单条变量明细是否满足当前条件。
/// 为减少命中提示阶段的额外开销,这里只做一次明细值转换,并直接用 CompareTo 完成比较。
/// 返回 false 表示明细值无法转换或操作符不适用于当前类型,此类异常明细不影响主规则结果。
/// </summary>
private static bool TryEvaluateDetailTrigger(object detailValue, object thresholdValue, RiskVariableDataType dataType, string ruleOperator, out bool triggered)
{
triggered = false;
if (dataType != RiskVariableDataType.Numeric && dataType != RiskVariableDataType.Date)
return false;
var itemValue = ConvertValue(detailValue, dataType, "明细", "明细值");
if (!itemValue.Success)
return false;
var compare = dataType == RiskVariableDataType.Numeric
? ((decimal)itemValue.Value).CompareTo((decimal)thresholdValue)
: ((DateTime)itemValue.Value).CompareTo((DateTime)thresholdValue);
triggered = ruleOperator switch
{
"gt" => compare > 0,
"lt" => compare < 0,
"gte" => compare >= 0,
"lte" => compare <= 0,
"eq" => compare == 0,
"ne" => compare != 0,
_ => false
};
return ruleOperator is "gt" or "lt" or "gte" or "lte" or "eq" or "ne";
}
/// <summary>
/// 构建区间条件的命中说明,包含实际值、上下限和区间开闭配置。
/// </summary>
private static string BuildRangeMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult lower, ValueExecuteResult upper, bool includeLower, bool includeUpper, string ruleOperator)
{
var parts = new List<string>();
AddDetail(parts, left.DetailMessage);
parts.Add($"{variable.VariableName}为{FormatDisplayValue(left.Value)}");
AddDetail(parts, lower.DetailMessage);
AddDetail(parts, upper.DetailMessage);
parts.Add($"阈值区间为{(includeLower ? "[" : "(")}{FormatDisplayValue(lower.Value)}, {FormatDisplayValue(upper.Value)}{(includeUpper ? "]" : ")")}");
parts.Add(ruleOperator == "notBetween" ? "要求不在区间内" : "要求在区间内");
return string.Join("", parts);
}
/// <summary>
/// 构建布尔条件的命中说明。
/// </summary>
private static string BuildBooleanMessage(glms_risk_variable variable, ValueExecuteResult value, bool expected)
{
var parts = new List<string>();
AddDetail(parts, value.DetailMessage);
parts.Add($"{variable.VariableName}为{FormatDisplayValue(value.Value)}");
parts.Add($"期望为{FormatDisplayValue(expected)}");
return string.Join("", parts);
}
/// <summary>
/// 添加变量表达式返回的自定义明细,避免空明细污染最终提示。
/// </summary>
private static void AddDetail(List<string> parts, string detailMessage)
{
if (!string.IsNullOrWhiteSpace(detailMessage))
parts.Add(detailMessage.Trim());
}
/// <summary>
/// 校验非区间条件没有携带区间字段,避免前端残留字段影响规则语义。
/// </summary>
private static StructuredRuleExecuteResult EnsureNoRangeFields(RuleCondition condition, string label)
{
if (condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
{
return StructuredRuleExecuteResult.Fail($"{label}:非区间条件不得携带区间字段");
}
return StructuredRuleExecuteResult.Ok(false);
}
/// <summary>
/// 校验布尔条件没有携带阈值或区间字段,布尔判断只由变量值和 isTrue/isFalse 决定。
/// </summary>
private static StructuredRuleExecuteResult EnsureNoThresholdFields(RuleCondition condition, string label)
{
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue ||
condition.LowerThresholdType != null || HasValue(condition.LowerValue) || condition.LowerThresholdVariableId.HasValue ||
condition.UpperThresholdType != null || HasValue(condition.UpperValue) || condition.UpperThresholdVariableId.HasValue ||
condition.IncludeLower.HasValue || condition.IncludeUpper.HasValue)
{
return StructuredRuleExecuteResult.Fail($"{label}:布尔条件不得携带阈值字段");
}
return StructuredRuleExecuteResult.Ok(false);
}
/// <summary>
/// 当区间上下限都是固定值时,提前校验下限不能大于上限;变量上下限留到运行时按实际值比较。
/// </summary>
private static StructuredRuleExecuteResult ValidateFixedRangeOrder(RuleCondition condition, RiskVariableDataType dataType, string label)
{
if (condition.LowerThresholdType != "Fixed" || condition.UpperThresholdType != "Fixed")
return StructuredRuleExecuteResult.Ok(false);
var lower = GetRawValue(condition.LowerValue);
var upper = GetRawValue(condition.UpperValue);
if (dataType == RiskVariableDataType.Numeric &&
decimal.TryParse(lower, NumberStyles.Float, CultureInfo.InvariantCulture, out var lowerNumber) &&
decimal.TryParse(upper, NumberStyles.Float, CultureInfo.InvariantCulture, out var upperNumber) &&
lowerNumber > upperNumber)
{
return StructuredRuleExecuteResult.Fail($"{label}:下限不能大于上限");
}
if (dataType == RiskVariableDataType.Date &&
DateTime.TryParseExact(lower, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var lowerDate) &&
DateTime.TryParseExact(upper, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var upperDate) &&
lowerDate > upperDate)
{
return StructuredRuleExecuteResult.Fail($"{label}:下限不能晚于上限");
}
return StructuredRuleExecuteResult.Ok(false);
}
/// <summary>
/// 判断普通对象或 JObject/JValue 字段是否有有效值,空字符串、null、undefined 都视为无值。
/// </summary>
private static bool HasValue(object value)
{
if (value == null) return false;
if (value is JValue jsonValue)
{
if (jsonValue.Type == JTokenType.Null || jsonValue.Type == JTokenType.Undefined) return false;
if (jsonValue.Type == JTokenType.String) return !string.IsNullOrWhiteSpace(jsonValue.Value<string>());
return true;
}
return value is not string text || !string.IsNullOrWhiteSpace(text);
}
/// <summary>
/// 使用固定区域性取出原始字符串,保证数字和日期解析不受服务器区域设置影响。
/// </summary>
private static string GetRawValue(object value)
{
return value is JValue jsonValue
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
: Convert.ToString(value, CultureInfo.InvariantCulture);
}
/// <summary>
/// 格式化错误信息中的原始值,便于区分 null、空字符串和普通值。
/// </summary>
private static string FormatRawValue(object value)
{
if (value == null)
return "<null>";
var rawValue = GetRawValue(value);
return string.IsNullOrWhiteSpace(rawValue) ? "<empty>" : rawValue;
}
/// <summary>
/// 格式化命中说明中的值,日期只展示日期部分,布尔值转为中文。
/// </summary>
private static string FormatDisplayValue(object value)
{
if (value == null)
return "<null>";
if (value is DateTime dateValue)
return dateValue.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
if (value is bool boolValue)
return boolValue ? "是" : "否";
if (value is decimal decimalValue)
return decimalValue.ToString("0.#########", CultureInfo.InvariantCulture);
if (value is double doubleValue)
return doubleValue.ToString("0.#########", CultureInfo.InvariantCulture);
if (value is float floatValue)
return floatValue.ToString("0.#########", CultureInfo.InvariantCulture);
return Convert.ToString(value, CultureInfo.InvariantCulture);
}
/// <summary>
/// 获取操作符中文说明,用于正常命中时解释实际值与阈值的关系。
/// </summary>
private static string GetOperatorName(string ruleOperator)
{
return ruleOperator switch
{
"gt" => "大于阈值",
"lt" => "小于阈值",
"gte" => "大于等于阈值",
"lte" => "小于等于阈值",
"eq" => "等于阈值",
"ne" => "不等于阈值",
_ => ruleOperator
};
}
/// <summary>
/// 比较数值变量单位。空单位按空字符串处理,前后空格不参与比较。
/// </summary>
private static bool UnitsMatch(string left, string right)
{
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
}
/// <summary>
/// 获取数据类型中文名称,用于拼接用户可读的错误信息。
/// </summary>
private static string GetDataTypeName(RiskVariableDataType dataType)
{
return dataType switch
{
RiskVariableDataType.Numeric => "数值",
RiskVariableDataType.Date => "日期",
RiskVariableDataType.Boolean => "布尔",
_ => "未知"
};
}
}
/// <summary>
/// 结构化规则执行结果。Success 表示执行过程是否成功,Triggered 表示规则条件是否命中。
/// </summary>
public class StructuredRuleExecuteResult
{
public bool Success { get; set; }
public bool Triggered { get; set; }
public string ErrorMessage { get; set; }
/// <summary>
/// 正常命中时的可读说明,用于展示实际值、阈值和变量表达式返回的计算明细。
/// </summary>
public string Message { get; set; }
public static StructuredRuleExecuteResult Ok(bool triggered, string message = null)
{
return new StructuredRuleExecuteResult
{
Success = true,
Triggered = triggered,
Message = message
};
}
public static StructuredRuleExecuteResult Fail(string errorMessage)
{
return new StructuredRuleExecuteResult
{
Success = false,
Triggered = false,
ErrorMessage = errorMessage
};
}
}
/// <summary>
/// 变量表达式可选返回模型。Value 参与规则比较,Message 用于正常命中时展示计算明细。
/// 仅保证金等需要逐条判断的变量应返回 Items,普通单交易变量只需返回 Value 和当前取值来源说明。
/// </summary>
public class RiskVariableValueDetail
{
public RiskVariableValueDetail(object value, string message)
: this(value, message, null)
{
}
public RiskVariableValueDetail(object value, string message, IEnumerable<RiskVariableValueDetailItem> items)
{
Value = value;
Message = message;
Items = items?.ToList() ?? new List<RiskVariableValueDetailItem>();
}
public object Value { get; }
public string Message { get; }
public IReadOnlyList<RiskVariableValueDetailItem> Items { get; }
}
/// <summary>
/// 需要遍历的变量所返回的单条计算明细。Value 用于按规则阈值复判该条明细是否命中,Message 用于展示该条记录。
/// </summary>
public class RiskVariableValueDetailItem
{
public RiskVariableValueDetailItem(object value, string message)
{
Value = value;
Message = message;
}
public object Value { get; }
public string Message { get; }
}
/// <summary>
/// 单个值取值或转换结果,用于在内部传递转换后的值和错误信息。
/// </summary>
internal class ValueExecuteResult
{
public bool Success { get; set; }
public object Value { get; set; }
/// <summary>
/// 变量表达式返回的计算明细,仅用于正常命中提示,不参与比较。
/// </summary>
public string DetailMessage { get; set; }
/// <summary>
/// 变量表达式返回的逐条计算明细,仅用于命中提示阶段按当前阈值拆分展示。
/// </summary>
public IReadOnlyList<RiskVariableValueDetailItem> DetailItems { get; set; }
public string ErrorMessage { get; set; }
public static ValueExecuteResult Ok(object value)
{
return new ValueExecuteResult
{
Success = true,
Value = value
};
}
public static ValueExecuteResult Fail(string errorMessage)
{
return new ValueExecuteResult
{
Success = false,
ErrorMessage = errorMessage
};
}
}
}