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 { private static readonly ConcurrentDictionary> VariableCompiledCache = new ConcurrentDictionary>(); 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" }; 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("变量列表不能为空"); for (var i = 0; i < conditions.Count; i++) { var conditionResult = ExecuteCondition(conditions[i], variables, context, i + 1); if (!conditionResult.Success) return conditionResult; if (!conditionResult.Triggered) return StructuredRuleExecuteResult.Ok(false); } return StructuredRuleExecuteResult.Ok(true); } 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}' 不存在"); 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.Value, 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); } 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"; return StructuredRuleExecuteResult.Ok((bool)value.Value == expected); } private static StructuredRuleExecuteResult ExecuteRangeCondition( RuleCondition condition, glms_risk_variable variable, object leftValue, 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(leftValue, lower.Value, variable.DataType, lowerOperator, label); if (!lowerCompare.Success) return lowerCompare; var upperCompare = Compare(leftValue, upper.Value, variable.DataType, upperOperator, label); if (!upperCompare.Success) return upperCompare; var inRange = lowerCompare.Triggered && upperCompare.Triggered; return StructuredRuleExecuteResult.Ok(condition.Operator == "notBetween" ? !inRange : inRange); } 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}"); } var converted = ConvertValue(rawValue, dataType, label, valueLabel); if (!converted.Success) return ValueExecuteResult.Fail($"{label}:变量'{variable.VariableName}'(ID:{variable.id}){converted.ErrorMessage},原始值:{FormatRawValue(rawValue)}"); return converted; } 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; } 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); 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) { 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); } 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); } 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); } 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); } private static string FormatRawValue(object value) { if (value == null) return ""; var rawValue = GetRawValue(value); return string.IsNullOrWhiteSpace(rawValue) ? "" : rawValue; } 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 => "布尔", _ => "未知" }; } } public class StructuredRuleExecuteResult { public bool Success { get; set; } public bool Triggered { get; set; } public string ErrorMessage { get; set; } public static StructuredRuleExecuteResult Ok(bool triggered) { return new StructuredRuleExecuteResult { Success = true, Triggered = triggered }; } public static StructuredRuleExecuteResult Fail(string errorMessage) { return new StructuredRuleExecuteResult { Success = false, Triggered = false, ErrorMessage = errorMessage }; } } internal class ValueExecuteResult { public bool Success { get; set; } public object Value { 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 }; } } }