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 => "布尔", _ => "未知" }; } } }