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