feat:支持变量为复杂表达式。
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
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
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, Func<RiskContext, object>> VariableCompiledCache = new ConcurrentDictionary<string, Func<RiskContext, object>>();
|
||||
|
||||
private static readonly HashSet<string> ComparableOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> BooleanOperators = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"isTrue", "isFalse"
|
||||
};
|
||||
|
||||
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("变量列表不能为空");
|
||||
|
||||
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<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}' 不存在");
|
||||
|
||||
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<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(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<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");
|
||||
}
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
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<string>());
|
||||
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 "<null>";
|
||||
var rawValue = GetRawValue(value);
|
||||
return string.IsNullOrWhiteSpace(rawValue) ? "<empty>" : 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user