Files
zszq-trs/YLErpDAL/Modules/RiskEngine/RuleConditionExpressionBuilder.cs
T
2026-08-06 13:09:27 +08:00

350 lines
16 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;
using Newtonsoft.Json.Linq;
using System.Globalization;
using System.Text.RegularExpressions;
using YLErp.Commons;
using YLErp.DBModels;
using YLErp.Modules.RiskEngine.Dto;
namespace YLErp.Modules.RiskEngine
{
/// <summary>
/// ConditionJson 契约校验和简洁 RuleExpr 生成器。
/// </summary>
public static class RuleConditionExpressionBuilder
{
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"
};
private static readonly string[] VariableReferenceFields =
{
"VariableId", "ThresholdVariableId", "LowerThresholdVariableId", "UpperThresholdVariableId"
};
public static List<RuleCondition> DeserializeConditions(string conditionJson)
{
try
{
var conditions = JsonConvert.DeserializeObject<List<RuleCondition>>(conditionJson);
if (conditions == null || conditions.Count == 0)
throw new ServiceException("公式条件列表不能为空");
return conditions;
}
catch (ServiceException)
{
throw;
}
catch
{
throw new ServiceException("公式表达式 JSON 格式不合法");
}
}
public static IReadOnlyCollection<long> GetReferencedVariableIds(IEnumerable<RuleCondition> conditions)
{
return conditions
.SelectMany(condition => condition == null
? Array.Empty<long?>()
: new long?[]
{
condition.VariableId,
condition.ThresholdVariableId,
condition.LowerThresholdVariableId,
condition.UpperThresholdVariableId
})
.Where(id => id.HasValue && id.Value > 0)
.Select(id => id.Value)
.Distinct()
.ToList();
}
public static bool IsVariableReferenced(string conditionJson, long variableId)
{
if (string.IsNullOrWhiteSpace(conditionJson) || variableId <= 0)
return false;
var idText = variableId.ToString(CultureInfo.InvariantCulture);
return VariableReferenceFields.Any(field =>
Regex.IsMatch(conditionJson, $@"""{field}""\s*:\s*{idText}(?=[,\s}}\]])", RegexOptions.CultureInvariant));
}
public static string Build(
IReadOnlyList<RuleCondition> conditions,
IReadOnlyDictionary<long, glms_risk_variable> variables)
{
if (conditions == null || conditions.Count == 0)
throw new ServiceException("公式条件列表不能为空");
var expressions = conditions
.Select((condition, index) => BuildConditionExpression(condition, variables, index + 1));
return string.Join(" && ", expressions);
}
private static string BuildConditionExpression(
RuleCondition condition,
IReadOnlyDictionary<long, glms_risk_variable> variables,
int conditionIndex)
{
var label = $"条件{conditionIndex}";
if (condition == null)
throw new ServiceException($"{label}不能为空");
if (!variables.TryGetValue(condition.VariableId, out var variable))
throw new ServiceException($"{label}:变量 ID '{condition.VariableId}' 不存在");
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
throw new ServiceException($"{label}:变量'{variable.VariableName}'的取值表达式为空");
if (variable.DataType == RiskVariableDataType.Boolean)
return BuildBooleanExpression(condition, variable, label);
if (variable.DataType != RiskVariableDataType.Numeric && variable.DataType != RiskVariableDataType.Date)
throw new ServiceException($"{label}:不支持的数据类型");
if (!ComparableOperators.Contains(condition.Operator))
throw new ServiceException($"{label}:操作符 '{condition.Operator}' 不适用于{GetDataTypeName(variable.DataType)}类型变量");
var leftExpression = BuildComparableExpression(variable, label);
if (condition.Operator == "between" || condition.Operator == "notBetween")
return BuildRangeExpression(condition, variable, leftExpression, variables, label);
EnsureNoRangeFields(condition, label);
var rightExpression = BuildThresholdExpression(
condition.ThresholdType,
condition.Value,
condition.ThresholdVariableId,
variable,
variables,
label,
"阈值");
var comparisonOperator = GetComparisonOperator(condition.Operator, label);
return $"{leftExpression} {comparisonOperator} {rightExpression}";
}
private static string BuildBooleanExpression(RuleCondition condition, glms_risk_variable variable, string label)
{
if (!BooleanOperators.Contains(condition.Operator))
throw new ServiceException($"{label}:操作符 '{condition.Operator}' 不适用于布尔类型变量");
EnsureNoThresholdFields(condition, label);
return $"{variable.VariableExpr} == {(condition.Operator == "isTrue" ? "true" : "false")}";
}
private static string BuildRangeExpression(
RuleCondition condition,
glms_risk_variable conditionVariable,
string leftExpression,
IReadOnlyDictionary<long, glms_risk_variable> variables,
string label)
{
if (condition.ThresholdType != null || HasValue(condition.Value) || condition.ThresholdVariableId.HasValue)
throw new ServiceException($"{label}:区间条件不得携带普通阈值字段");
if (!condition.IncludeLower.HasValue || !condition.IncludeUpper.HasValue)
throw new ServiceException($"{label}:区间开闭配置不完整");
var lowerExpression = BuildThresholdExpression(
condition.LowerThresholdType,
condition.LowerValue,
condition.LowerThresholdVariableId,
conditionVariable,
variables,
label,
"下限");
var upperExpression = BuildThresholdExpression(
condition.UpperThresholdType,
condition.UpperValue,
condition.UpperThresholdVariableId,
conditionVariable,
variables,
label,
"上限");
ValidateFixedRangeOrder(condition, conditionVariable.DataType, label);
var lowerOperator = condition.IncludeLower.Value ? ">=" : ">";
var upperOperator = condition.IncludeUpper.Value ? "<=" : "<";
var rangeExpression = $"{leftExpression} {lowerOperator} {lowerExpression} && {leftExpression} {upperOperator} {upperExpression}";
return condition.Operator == "notBetween" ? $"!({rangeExpression})" : rangeExpression;
}
private static string BuildThresholdExpression(
string thresholdType,
object fixedValue,
long? thresholdVariableId,
glms_risk_variable conditionVariable,
IReadOnlyDictionary<long, glms_risk_variable> variables,
string label,
string thresholdLabel)
{
var dataType = conditionVariable.DataType;
if (string.Equals(thresholdType, "Fixed", StringComparison.Ordinal))
{
if (thresholdVariableId.HasValue)
throw new ServiceException($"{label}:固定{thresholdLabel}不得携带变量 ID");
return BuildFixedValueExpression(fixedValue, dataType, label, thresholdLabel);
}
if (string.Equals(thresholdType, "Variable", StringComparison.Ordinal))
{
if (HasValue(fixedValue))
throw new ServiceException($"{label}:变量{thresholdLabel}不得携带固定值");
if (!thresholdVariableId.HasValue)
throw new ServiceException($"{label}{thresholdLabel}变量 ID 不能为空");
if (!variables.TryGetValue(thresholdVariableId.Value, out var thresholdVariable))
throw new ServiceException($"{label}{thresholdLabel}变量 ID '{thresholdVariableId.Value}' 不存在");
if (thresholdVariable.DataType != dataType)
throw new ServiceException($"{label}{thresholdLabel}变量与条件变量的数据类型不一致");
if (dataType == RiskVariableDataType.Numeric && !UnitsMatch(conditionVariable.Unit, thresholdVariable.Unit))
throw new ServiceException($"{label}{thresholdLabel}变量与条件变量的单位不一致");
return BuildComparableExpression(thresholdVariable, label);
}
throw new ServiceException($"{label}{thresholdLabel}类型 '{thresholdType}' 不合法,仅支持 Fixed/Variable");
}
private static string BuildComparableExpression(glms_risk_variable variable, string label)
{
if (string.IsNullOrWhiteSpace(variable.VariableExpr))
throw new ServiceException($"{label}:变量'{variable.VariableName}'的取值表达式为空");
return variable.VariableExpr;
}
private static string BuildFixedValueExpression(
object value,
RiskVariableDataType dataType,
string label,
string thresholdLabel)
{
if (!HasValue(value))
throw new ServiceException($"{label}:固定{thresholdLabel}不能为空");
var rawValue = GetRawValue(value);
if (dataType == RiskVariableDataType.Numeric)
{
if (!decimal.TryParse(rawValue, NumberStyles.Float,
CultureInfo.InvariantCulture, out var numericValue))
{
throw new ServiceException($"{label}:数值型{thresholdLabel}必须为数字");
}
return FormatFixedDecimal(numericValue);
}
if (dataType == RiskVariableDataType.Date)
{
if (!DateTime.TryParseExact(rawValue, "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var dateValue))
{
throw new ServiceException($"{label}:日期型{thresholdLabel}必须为 yyyy-MM-dd 格式的合法日期");
}
return $"new DateTime({dateValue.Year}, {dateValue.Month}, {dateValue.Day})";
}
throw new ServiceException($"{label}:不支持的数据类型");
}
private static string FormatFixedDecimal(decimal value)
{
return value.ToString("0.############################", CultureInfo.InvariantCulture);
}
private static void ValidateFixedRangeOrder(RuleCondition condition, RiskVariableDataType dataType, string label)
{
if (condition.LowerThresholdType != "Fixed" || condition.UpperThresholdType != "Fixed")
return;
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)
{
throw new ServiceException($"{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)
{
throw new ServiceException($"{label}:下限不能晚于上限");
}
}
private static void 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)
{
throw new ServiceException($"{label}:非区间条件不得携带区间字段");
}
}
private static void 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)
{
throw new ServiceException($"{label}:布尔条件不得携带阈值字段");
}
}
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 bool UnitsMatch(string left, string right)
{
return string.Equals(left?.Trim() ?? string.Empty, right?.Trim() ?? string.Empty, StringComparison.Ordinal);
}
private static string GetComparisonOperator(string ruleOperator, string label)
{
return ruleOperator switch
{
"gt" => ">",
"lt" => "<",
"gte" => ">=",
"lte" => "<=",
"eq" => "==",
"ne" => "!=",
_ => throw new ServiceException($"{label}:不支持的操作符'{ruleOperator}'")
};
}
private static string GetDataTypeName(RiskVariableDataType dataType)
{
return dataType switch
{
RiskVariableDataType.Numeric => "数值",
RiskVariableDataType.Date => "日期",
RiskVariableDataType.Boolean => "布尔",
_ => "未知"
};
}
}
}