feat: 风控引擎前后端统一操作符
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
using YLErp.Commons;
|
||||
using YLErp.DBModels;
|
||||
using YLErp.Modules.RiskEngine.Dto;
|
||||
|
||||
namespace YLErp.Modules.RiskEngine
|
||||
{
|
||||
[TestClass]
|
||||
public class RuleConditionExpressionBuilderTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void Build_NumericFixedValue_UsesNormalizedLiteralWithoutSuffix()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "gt",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "123.4500"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount > 123.45",
|
||||
expression);
|
||||
Assert.IsFalse(expression.Contains("123.45m"));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("0", "0")]
|
||||
[DataRow("-0", "0")]
|
||||
[DataRow("123.4500", "123.45")]
|
||||
[DataRow("1e3", "1000")]
|
||||
[DataRow("1e-28", "0.0000000000000000000000000001")]
|
||||
public void Build_NumericFixedValue_NormalizesLikeFrontend(string input, string expectedValue)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = input
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"));
|
||||
|
||||
Assert.AreEqual(
|
||||
$"amount == {expectedValue}",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericFixedValue_RejectsThousandsSeparator()
|
||||
{
|
||||
Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "1,000"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount")));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("gt", ">")]
|
||||
[DataRow("lt", "<")]
|
||||
[DataRow("gte", ">=")]
|
||||
[DataRow("lte", "<=")]
|
||||
[DataRow("eq", "==")]
|
||||
[DataRow("ne", "!=")]
|
||||
public void Build_ComparisonTokens_MapToCSharpOperators(string token, string expectedOperator)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = token,
|
||||
ThresholdType = "Fixed",
|
||||
Value = "0"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"));
|
||||
|
||||
StringAssert.Contains(expression, $" {expectedOperator} ");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericVariableThreshold_UsesStoredExpressions()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "lte",
|
||||
ThresholdType = "Variable",
|
||||
ThresholdVariableId = 2
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "limit.Amount", "元"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount <= limit.Amount",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericVariableThreshold_RejectsDifferentUnit()
|
||||
{
|
||||
var exception = Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "lte",
|
||||
ThresholdType = "Variable",
|
||||
ThresholdVariableId = 2
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
|
||||
|
||||
StringAssert.Contains(exception.Message, "单位不一致");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NumericRangeVariableBound_RejectsDifferentUnit()
|
||||
{
|
||||
var exception = Assert.ThrowsException<ServiceException>(() => Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "between",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "0",
|
||||
UpperThresholdType = "Variable",
|
||||
UpperThresholdVariableId = 2,
|
||||
IncludeLower = true,
|
||||
IncludeUpper = true
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount", "元"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "trade.Count", "笔")));
|
||||
|
||||
StringAssert.Contains(exception.Message, "单位不一致");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_FixedDate_UsesDateConstructorTemplate()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "eq",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "2026-07-21"
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
|
||||
|
||||
Assert.AreEqual("trade.TradeDate == new DateTime(2026, 7, 21)", expression);
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("isTrue", "trade.IsConfirmed == true")]
|
||||
[DataRow("isFalse", "trade.IsConfirmed == false")]
|
||||
public void Build_BooleanToken_UsesBooleanLiteral(string token, string expected)
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition { VariableId = 1, Operator = token },
|
||||
Variable(1, RiskVariableDataType.Boolean, "trade.IsConfirmed"));
|
||||
|
||||
Assert.AreEqual(expected, expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_Between_AllowsMixedFixedAndVariableBounds()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "between",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "-1.25",
|
||||
UpperThresholdType = "Variable",
|
||||
UpperThresholdVariableId = 2,
|
||||
IncludeLower = false,
|
||||
IncludeUpper = true
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Numeric, "trade.Amount"),
|
||||
Variable(2, RiskVariableDataType.Numeric, "limit.Upper"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"trade.Amount > -1.25 && trade.Amount <= limit.Upper",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_NotBetween_NegatesCompleteRangeExpression()
|
||||
{
|
||||
var expression = Build(
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "notBetween",
|
||||
LowerThresholdType = "Fixed",
|
||||
LowerValue = "2026-01-01",
|
||||
UpperThresholdType = "Fixed",
|
||||
UpperValue = "2026-12-31",
|
||||
IncludeLower = true,
|
||||
IncludeUpper = false
|
||||
},
|
||||
Variable(1, RiskVariableDataType.Date, "trade.TradeDate"));
|
||||
|
||||
Assert.AreEqual(
|
||||
"!(trade.TradeDate >= new DateTime(2026, 1, 1) && trade.TradeDate < new DateTime(2026, 12, 31))",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_MultipleConditions_UsesExactAndSeparator()
|
||||
{
|
||||
var conditions = new[]
|
||||
{
|
||||
new RuleCondition { VariableId = 1, Operator = "gt", ThresholdType = "Fixed", Value = "0" },
|
||||
new RuleCondition { VariableId = 2, Operator = "isTrue" }
|
||||
};
|
||||
var variables = Variables(
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount"),
|
||||
Variable(2, RiskVariableDataType.Boolean, "isValid"));
|
||||
|
||||
var expression = RuleConditionExpressionBuilder.Build(conditions, variables);
|
||||
|
||||
Assert.AreEqual(
|
||||
"amount > 0 && isValid == true",
|
||||
expression);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Build_RejectsNonRangeConditionWithRangeFields()
|
||||
{
|
||||
var condition = new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
Operator = "gt",
|
||||
ThresholdType = "Fixed",
|
||||
Value = "0",
|
||||
IncludeLower = true
|
||||
};
|
||||
|
||||
Assert.ThrowsException<ServiceException>(() => Build(
|
||||
condition,
|
||||
Variable(1, RiskVariableDataType.Numeric, "amount")));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferencedVariableIds_IgnoreNullConditionItems()
|
||||
{
|
||||
var conditions = new RuleCondition[]
|
||||
{
|
||||
null,
|
||||
new RuleCondition { VariableId = 7 }
|
||||
};
|
||||
|
||||
CollectionAssert.AreEqual(
|
||||
new long[] { 7 },
|
||||
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferencedVariableIds_IncludeAllFourFields()
|
||||
{
|
||||
var conditions = new[]
|
||||
{
|
||||
new RuleCondition
|
||||
{
|
||||
VariableId = 1,
|
||||
ThresholdVariableId = 2,
|
||||
LowerThresholdVariableId = 3,
|
||||
UpperThresholdVariableId = 4
|
||||
}
|
||||
};
|
||||
|
||||
CollectionAssert.AreEquivalent(
|
||||
new long[] { 1, 2, 3, 4 },
|
||||
RuleConditionExpressionBuilder.GetReferencedVariableIds(conditions).ToArray());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IsVariableReferenced_MatchesAllFieldsWithoutIdPrefixCollision()
|
||||
{
|
||||
const string json = "[{\"VariableId\":10,\"ThresholdVariableId\":20,\"LowerThresholdVariableId\":30,\"UpperThresholdVariableId\":40}]";
|
||||
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 10));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 20));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 30));
|
||||
Assert.IsTrue(RuleConditionExpressionBuilder.IsVariableReferenced(json, 40));
|
||||
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 1));
|
||||
Assert.IsFalse(RuleConditionExpressionBuilder.IsVariableReferenced(json, 4));
|
||||
}
|
||||
|
||||
private static string Build(RuleCondition condition, params glms_risk_variable[] variables)
|
||||
{
|
||||
return RuleConditionExpressionBuilder.Build(new[] { condition }, Variables(variables));
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<long, glms_risk_variable> Variables(params glms_risk_variable[] variables)
|
||||
{
|
||||
return variables.ToDictionary(variable => (long)variable.id);
|
||||
}
|
||||
|
||||
private static glms_risk_variable Variable(long id, RiskVariableDataType dataType, string expression, string unit = null)
|
||||
{
|
||||
return new glms_risk_variable
|
||||
{
|
||||
id = (int)id,
|
||||
VariableName = $"V{id}",
|
||||
DataType = dataType,
|
||||
VariableExpr = expression,
|
||||
Unit = unit
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ namespace YLErp
|
||||
[ModuleInitializer]
|
||||
internal static void M1()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("YLErp_UNIT_TEST_SKIP_INITIALIZATION"), "1", StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
Console.WriteLine("version--" + AppManager.Version);
|
||||
|
||||
var logger = NLog.LogManager.Setup().GetCurrentClassLogger();
|
||||
|
||||
Reference in New Issue
Block a user