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
{
///
/// 结构化规则执行器。
/// 用于执行现有 ConditionJson:变量取值、按 DataType 转换并比较阈值;复杂业务计算放在变量表达式中完成。
///
public static class StructuredRuleExecutor
{
///
/// 变量表达式编译缓存。Key 包含变量 ID、版本、更新时间和表达式哈希,避免变量变更后复用旧委托。
///
private static readonly ConcurrentDictionary> VariableCompiledCache = new ConcurrentDictionary>();
///
/// 按变量 ID 增量移除变量表达式编译缓存。
///
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 _);
}
}
///
/// 按变量 ID 集合批量移除变量表达式编译缓存。
///
public static void RemoveVariableCompiledCache(IEnumerable variableIds)
{
if (variableIds == null)
return;
var idSet = new HashSet(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 _);
}
}
}
///
/// 清空全部变量表达式编译缓存。仅用于风控缓存全量刷新场景。
///
public static void ClearVariableCompiledCache()
{
VariableCompiledCache.Clear();
}
///
/// 数值和日期类型支持的比较操作符。
///
private static readonly HashSet ComparableOperators = new HashSet(StringComparer.Ordinal)
{
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
};
///
/// 布尔类型支持的判断操作符。
///
private static readonly HashSet BooleanOperators = new HashSet(StringComparer.Ordinal)
{
"isTrue", "isFalse"
};
///
/// 执行一组结构化条件。当前规则条件之间按 AND 关系处理,任一条件不满足则整条规则不触发。
///
public static StructuredRuleExecuteResult Execute(
IReadOnlyList conditions,
IReadOnlyDictionary variables,
RiskContext context)
{
try
{
if (conditions == null || conditions.Count == 0)
return StructuredRuleExecuteResult.Fail("公式条件列表不能为空");
if (variables == null)
return StructuredRuleExecuteResult.Fail("变量列表不能为空");
var triggerMessages = new List();
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);
}
}
///
/// 执行单个结构化条件,负责按变量数据类型分派到布尔、普通比较或区间比较逻辑。
///
private static StructuredRuleExecuteResult ExecuteCondition(
RuleCondition condition,
IReadOnlyDictionary 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);
}
///
/// 执行布尔条件。布尔变量只允许 isTrue/isFalse,不允许配置固定值、变量阈值或区间字段。
///
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);
}
///
/// 执行区间条件。下限和上限可以分别来自固定值或变量,并根据开闭区间转换为对应比较操作符。
///
private static StructuredRuleExecuteResult ExecuteRangeCondition(
RuleCondition condition,
glms_risk_variable variable,
ValueExecuteResult left,
IReadOnlyDictionary 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);
}
///
/// 获取阈值。固定阈值直接转换;变量阈值先校验类型和单位,再执行阈值变量表达式。
///
private static ValueExecuteResult GetThresholdValue(
string thresholdType,
object fixedValue,
long? thresholdVariableId,
glms_risk_variable conditionVariable,
IReadOnlyDictionary 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");
}
///
/// 执行变量取值表达式,并将表达式返回值转换为变量声明的数据类型。
///
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}");
}
// 变量表达式可选择返回 RiskVariableValueDetail:Value 参与比较,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;
}
///
/// 获取变量表达式的编译委托。缓存 Key 绑定变量版本信息,变量内容变化后会自然生成新 Key。
///
private static Func 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;
}
///
/// 按变量数据类型统一转换值。日期固定值只接受 yyyy-MM-dd,避免不同区域格式导致比较结果不一致。
///
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}:不支持的数据类型");
}
///
/// 比较两个已转换为同一数据类型的值,并应用结构化规则操作符。
///
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);
}
///
/// 构建普通比较条件的命中说明。变量表达式如果返回 RiskVariableValueDetail,会优先带出自定义明细。
///
private static string BuildCompareMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult right, string ruleOperator)
{
var parts = new List();
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);
}
///
/// 按当前规则阈值重新判断变量返回的逐条明细,并将明细拆分为超过阈值和未超过阈值两组。
/// 该逻辑只在规则已经命中且需要构造提示时执行,不参与规则主判断;未提供逐条明细时保留原始明细文本。
///
private static void AddCompareDetails(List 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(detailItems.Count);
var untriggeredItems = new List();
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));
}
///
/// 复用普通比较条件的类型转换口径,判断单条变量明细是否满足当前条件。
/// 为减少命中提示阶段的额外开销,这里只做一次明细值转换,并直接用 CompareTo 完成比较。
/// 返回 false 表示明细值无法转换或操作符不适用于当前类型,此类异常明细不影响主规则结果。
///
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";
}
///
/// 构建区间条件的命中说明,包含实际值、上下限和区间开闭配置。
///
private static string BuildRangeMessage(glms_risk_variable variable, ValueExecuteResult left, ValueExecuteResult lower, ValueExecuteResult upper, bool includeLower, bool includeUpper, string ruleOperator)
{
var parts = new List();
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);
}
///
/// 构建布尔条件的命中说明。
///
private static string BuildBooleanMessage(glms_risk_variable variable, ValueExecuteResult value, bool expected)
{
var parts = new List();
AddDetail(parts, value.DetailMessage);
parts.Add($"{variable.VariableName}为{FormatDisplayValue(value.Value)}");
parts.Add($"期望为{FormatDisplayValue(expected)}");
return string.Join(",", parts);
}
///
/// 添加变量表达式返回的自定义明细,避免空明细污染最终提示。
///
private static void AddDetail(List parts, string detailMessage)
{
if (!string.IsNullOrWhiteSpace(detailMessage))
parts.Add(detailMessage.Trim());
}
///
/// 校验非区间条件没有携带区间字段,避免前端残留字段影响规则语义。
///
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);
}
///
/// 校验布尔条件没有携带阈值或区间字段,布尔判断只由变量值和 isTrue/isFalse 决定。
///
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);
}
///
/// 当区间上下限都是固定值时,提前校验下限不能大于上限;变量上下限留到运行时按实际值比较。
///
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);
}
///
/// 判断普通对象或 JObject/JValue 字段是否有有效值,空字符串、null、undefined 都视为无值。
///
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());
return true;
}
return value is not string text || !string.IsNullOrWhiteSpace(text);
}
///
/// 使用固定区域性取出原始字符串,保证数字和日期解析不受服务器区域设置影响。
///
private static string GetRawValue(object value)
{
return value is JValue jsonValue
? Convert.ToString(jsonValue.Value, CultureInfo.InvariantCulture)
: Convert.ToString(value, CultureInfo.InvariantCulture);
}
///
/// 格式化错误信息中的原始值,便于区分 null、空字符串和普通值。
///
private static string FormatRawValue(object value)
{
if (value == null)
return "";
var rawValue = GetRawValue(value);
return string.IsNullOrWhiteSpace(rawValue) ? "" : rawValue;
}
///
/// 格式化命中说明中的值,日期只展示日期部分,布尔值转为中文。
///
private static string FormatDisplayValue(object value)
{
if (value == null)
return "";
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);
}
///
/// 获取操作符中文说明,用于正常命中时解释实际值与阈值的关系。
///
private static string GetOperatorName(string ruleOperator)
{
return ruleOperator switch
{
"gt" => "大于阈值",
"lt" => "小于阈值",
"gte" => "大于等于阈值",
"lte" => "小于等于阈值",
"eq" => "等于阈值",
"ne" => "不等于阈值",
_ => ruleOperator
};
}
///
/// 比较数值变量单位。空单位按空字符串处理,前后空格不参与比较。
///
private static bool UnitsMatch(string left, string right)
{
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
}
///
/// 获取数据类型中文名称,用于拼接用户可读的错误信息。
///
private static string GetDataTypeName(RiskVariableDataType dataType)
{
return dataType switch
{
RiskVariableDataType.Numeric => "数值",
RiskVariableDataType.Date => "日期",
RiskVariableDataType.Boolean => "布尔",
_ => "未知"
};
}
}
///
/// 结构化规则执行结果。Success 表示执行过程是否成功,Triggered 表示规则条件是否命中。
///
public class StructuredRuleExecuteResult
{
public bool Success { get; set; }
public bool Triggered { get; set; }
public string ErrorMessage { get; set; }
///
/// 正常命中时的可读说明,用于展示实际值、阈值和变量表达式返回的计算明细。
///
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
};
}
}
///
/// 变量表达式可选返回模型。Value 参与规则比较,Message 用于正常命中时展示计算明细。
/// 仅保证金等需要逐条判断的变量应返回 Items,普通单交易变量只需返回 Value 和当前取值来源说明。
///
public class RiskVariableValueDetail
{
public RiskVariableValueDetail(object value, string message)
: this(value, message, null)
{
}
public RiskVariableValueDetail(object value, string message, IEnumerable items)
{
Value = value;
Message = message;
Items = items?.ToList() ?? new List();
}
public object Value { get; }
public string Message { get; }
public IReadOnlyList Items { get; }
}
///
/// 需要遍历的变量所返回的单条计算明细。Value 用于按规则阈值复判该条明细是否命中,Message 用于展示该条记录。
///
public class RiskVariableValueDetailItem
{
public RiskVariableValueDetailItem(object value, string message)
{
Value = value;
Message = message;
}
public object Value { get; }
public string Message { get; }
}
///
/// 单个值取值或转换结果,用于在内部传递转换后的值和错误信息。
///
internal class ValueExecuteResult
{
public bool Success { get; set; }
public object Value { get; set; }
///
/// 变量表达式返回的计算明细,仅用于正常命中提示,不参与比较。
///
public string DetailMessage { get; set; }
///
/// 变量表达式返回的逐条计算明细,仅用于命中提示阶段按当前阈值拆分展示。
///
public IReadOnlyList 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
};
}
}
}