350 lines
16 KiB
C#
350 lines
16 KiB
C#
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 => "布尔",
|
||
_ => "未知"
|
||
};
|
||
}
|
||
}
|
||
}
|