diff --git a/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs b/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs index b4dd466..9ae8ed3 100644 --- a/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs +++ b/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs @@ -314,7 +314,9 @@ public static class ChineseFormulaEvaluator while (_current.Kind == TokenKind.Or) { Advance(); - value = value || ParseAnd(); + // 必须先解析右侧,禁止用 || 短路跳过,否则剩余 token 会让 ExpectEnd 失败 + var right = ParseAnd(); + value = value || right; } return value; @@ -326,7 +328,9 @@ public static class ChineseFormulaEvaluator while (_current.Kind == TokenKind.And) { Advance(); - value = value && ParseNot(); + // 同上:禁止 && 短路跳过右侧解析 + var right = ParseNot(); + value = value && right; } return value; @@ -510,8 +514,18 @@ public static class ChineseFormulaEvaluator _ => false }; + /// + /// 括号内是否应按布尔表达式解析。 + /// 注意:调用前已 Advance 掉 '(',第一个内部 token 在 _current;词法位置在其后。 + /// 旧逻辑只向后扫逻辑运算符,导致括号内的包含/有值/为空/比较被误判为数值式而解析失败。 + /// private bool LooksLikeBooleanGroup() { + if (IsBooleanAtomKind(_current.Kind)) + { + return true; + } + var savedPos = _lexer.SavePosition(); var depth = 1; while (depth > 0) @@ -534,7 +548,7 @@ public static class ChineseFormulaEvaluator continue; } - if (depth == 1 && token.Kind is TokenKind.And or TokenKind.Or) + if (depth == 1 && IsBooleanGroupIndicator(token.Kind)) { _lexer.RestorePosition(savedPos); return true; @@ -545,6 +559,15 @@ public static class ChineseFormulaEvaluator return false; } + private static bool IsBooleanAtomKind(TokenKind kind) => + kind is TokenKind.Contains or TokenKind.HasValue or TokenKind.IsEmpty or TokenKind.Not; + + private static bool IsBooleanGroupIndicator(TokenKind kind) => + kind is TokenKind.And or TokenKind.Or + or TokenKind.Contains or TokenKind.HasValue or TokenKind.IsEmpty or TokenKind.Not + or TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual + or TokenKind.Equal or TokenKind.NotEqual; + private void Advance() => _current = _lexer.Next(); } } diff --git a/src/Oms.Tests/ChineseFormulaEvaluatorTests.cs b/src/Oms.Tests/ChineseFormulaEvaluatorTests.cs new file mode 100644 index 0000000..2a9390e --- /dev/null +++ b/src/Oms.Tests/ChineseFormulaEvaluatorTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using Oms.Application.Dtos.Orders; +using Oms.Application.Services.Orders.Issues; +using Oms.Domain.CustomersAggregate.Entities; +using Xunit; + +namespace Oms.Tests; + +public class ChineseFormulaEvaluatorTests +{ + [Theory] + [InlineData("产品标识编码为空()", true)] + [InlineData("产品标识编码有值()", false)] + [InlineData("!产品标识编码有值()", true)] + [InlineData("销售产品包含('US02-5DAY')", true)] + [InlineData("销售产品包含('US02-5DAY')&&产品标识编码为空()", true)] + [InlineData("(销售产品包含('US02-5DAY'))&&产品标识编码为空()", true)] + [InlineData("销售产品包含('US02-5DAY')&&(产品标识编码为空())", true)] + [InlineData("产品标识编码为空()||非标产品标识编码为空()", true)] + [InlineData("(销售产品包含('US02-5DAY')||销售产品包含('OTHER'))&&(产品标识编码为空()||非标产品标识编码为空())", true)] + [InlineData("(销售产品包含('3PE快递美国专线-5日达')||销售产品包含('US02-5DAY'))&&(产品标识编码为空()||非标产品标识编码为空())", true)] + [InlineData("(实重()>1)&&产品标识编码为空()", true)] + public void TryEvaluate_EmptyProductIds_ShouldMatch(string expression, bool expected) + { + var context = BuildPreCommitContext(productCode: "US02-5DAY", productId: "", nonStdProductId: "", actualWeight: 1.2m); + + var ok = ChineseFormulaEvaluator.TryEvaluate(expression, context, out var matched); + + ok.Should().BeTrue($"expression should parse: {expression}"); + matched.Should().Be(expected); + } + + [Fact] + public void TryEvaluate_FilledProductIds_ShouldNotMatchEmptyCheck() + { + var context = BuildPreCommitContext(productCode: "US02-5DAY", productId: "P1", nonStdProductId: "N1", actualWeight: 1.2m); + var expression = + "(销售产品包含('US02-5DAY'))&&(产品标识编码为空()||非标产品标识编码为空())"; + + var ok = ChineseFormulaEvaluator.TryEvaluate(expression, context, out var matched); + + ok.Should().BeTrue(); + matched.Should().BeFalse(); + } + + private static OrderRuleEvaluationContext BuildPreCommitContext( + string productCode, + string? productId, + string? nonStdProductId, + decimal actualWeight) + { + var input = new OrderParcelCreateInput + { + ProductCode = productCode, + ActualWeight = actualWeight, + Items = + [ + new OrderParcelItemInput + { + CnName = "手机壳", + ProductId = productId, + NonStdProductId = nonStdProductId + } + ] + }; + + return OrderRuleEvaluationContext.FromPreCommit( + input, + new Customer { CustomerName = "test" }, + "US", + "美国"); + } +}