Files
zszq-trs/YLErpDAL/Modules/RiskEngine/StructuredRuleExecutor.cs
T

462 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
};
}
}
}