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
{
///
/// ConditionJson 契约校验和简洁 RuleExpr 生成器。
///
public static class RuleConditionExpressionBuilder
{
private static readonly HashSet ComparableOperators = new HashSet(StringComparer.Ordinal)
{
"gt", "lt", "gte", "lte", "eq", "ne", "between", "notBetween"
};
private static readonly HashSet BooleanOperators = new HashSet(StringComparer.Ordinal)
{
"isTrue", "isFalse"
};
private static readonly string[] VariableReferenceFields =
{
"VariableId", "ThresholdVariableId", "LowerThresholdVariableId", "UpperThresholdVariableId"
};
public static List DeserializeConditions(string conditionJson)
{
try
{
var conditions = JsonConvert.DeserializeObject>(conditionJson);
if (conditions == null || conditions.Count == 0)
throw new ServiceException("公式条件列表不能为空");
return conditions;
}
catch (ServiceException)
{
throw;
}
catch
{
throw new ServiceException("公式表达式 JSON 格式不合法");
}
}
public static IReadOnlyCollection GetReferencedVariableIds(IEnumerable conditions)
{
return conditions
.SelectMany(condition => condition == null
? Array.Empty()
: 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 conditions,
IReadOnlyDictionary 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 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 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 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());
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 => "布尔",
_ => "未知"
};
}
}
}