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