462 lines
21 KiB
C#
462 lines
21 KiB
C#
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
|
||
};
|
||
}
|
||
}
|
||
}
|