diff --git a/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs b/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs
index 0017e30..9ae8ed3 100644
--- a/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs
+++ b/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs
@@ -5,7 +5,9 @@ namespace Oms.Application.Services.Orders.Issues;
public static class ChineseFormulaEvaluator
{
- private const string ContainsKeyword = "包含";
+ private const string ContainsKeyword = "包含";
+ private const string HasValueKeyword = "有值";
+ private const string IsEmptyKeyword = "为空";
public static bool TryEvaluate(string? expression, OrderRuleEvaluationContext context, out bool result)
{
@@ -53,6 +55,8 @@ public static class ChineseFormulaEvaluator
LeftParen,
RightParen,
Contains,
+ HasValue,
+ IsEmpty,
End
}
@@ -208,12 +212,14 @@ public static class ChineseFormulaEvaluator
continue;
}
- _pos += label.Length;
+ var afterLabel = _pos + label.Length;
+ _pos = afterLabel;
SkipWhitespace();
if (_pos + ContainsKeyword.Length <= source.Length
&& source.AsSpan(_pos, ContainsKeyword.Length).SequenceEqual(ContainsKeyword.AsSpan()))
{
- _pos += ContainsKeyword.Length;
+ var afterContainsKeyword = _pos + ContainsKeyword.Length;
+ _pos = afterContainsKeyword;
SkipWhitespace();
if (_pos < source.Length && source[_pos] == '(')
{
@@ -221,6 +227,22 @@ public static class ChineseFormulaEvaluator
token = new Token(TokenKind.Contains, label);
return true;
}
+
+ // 「包含」后无「(」则回退,继续尝试有值()/为空()/()
+ _pos = afterLabel;
+ SkipWhitespace();
+ }
+
+ if (TryConsumeKeywordWithEmptyParens(HasValueKeyword))
+ {
+ token = new Token(TokenKind.HasValue, label);
+ return true;
+ }
+
+ if (TryConsumeKeywordWithEmptyParens(IsEmptyKeyword))
+ {
+ token = new Token(TokenKind.IsEmpty, label);
+ return true;
}
if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
@@ -230,12 +252,34 @@ public static class ChineseFormulaEvaluator
return true;
}
- _pos -= label.Length;
+ _pos = afterLabel - label.Length;
}
return false;
}
+ /// 匹配「有值()」「为空()」这类无参关键字。
+ private bool TryConsumeKeywordWithEmptyParens(string keyword)
+ {
+ var saved = _pos;
+ if (_pos + keyword.Length > source.Length
+ || !source.AsSpan(_pos, keyword.Length).SequenceEqual(keyword.AsSpan()))
+ {
+ return false;
+ }
+
+ _pos += keyword.Length;
+ SkipWhitespace();
+ if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
+ {
+ _pos += 2;
+ return true;
+ }
+
+ _pos = saved;
+ return false;
+ }
+
public int SavePosition() => _pos;
public void RestorePosition(int pos) => _pos = pos;
@@ -270,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;
@@ -282,7 +328,9 @@ public static class ChineseFormulaEvaluator
while (_current.Kind == TokenKind.And)
{
Advance();
- value = value && ParseNot();
+ // 同上:禁止 && 短路跳过右侧解析
+ var right = ParseNot();
+ value = value && right;
}
return value;
@@ -306,6 +354,11 @@ public static class ChineseFormulaEvaluator
return ParseContains();
}
+ if (_current.Kind is TokenKind.HasValue or TokenKind.IsEmpty)
+ {
+ return ParsePresence(_current.Kind == TokenKind.HasValue);
+ }
+
var left = ParseAdditive();
if (_current.Kind is TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual or TokenKind.Equal or TokenKind.NotEqual)
{
@@ -343,6 +396,19 @@ public static class ChineseFormulaEvaluator
return OrderFormulaFieldResolver.StringContains(fieldRef, _context, needle);
}
+ private bool ParsePresence(bool expectHasValue)
+ {
+ var label = _current.Text;
+ Advance();
+ if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
+ {
+ throw new FormulaParseException($"未知字段:{label}");
+ }
+
+ var hasValue = OrderFormulaFieldResolver.HasValue(fieldRef, _context);
+ return expectHasValue ? hasValue : !hasValue;
+ }
+
private decimal ParseAdditive()
{
var value = ParseMultiplicative();
@@ -448,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)
@@ -472,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;
@@ -483,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.Application/Services/Orders/Issues/OrderFormulaFieldResolver.cs b/src/Oms.Application/Services/Orders/Issues/OrderFormulaFieldResolver.cs
index 3393c17..5aceb38 100644
--- a/src/Oms.Application/Services/Orders/Issues/OrderFormulaFieldResolver.cs
+++ b/src/Oms.Application/Services/Orders/Issues/OrderFormulaFieldResolver.cs
@@ -16,9 +16,10 @@ public static class OrderFormulaFieldResolver
public static bool StringContains(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context, string needle)
{
+ // 空参数:表示「字段有值」(非 null / 非空白),用于必填校验,如 国家包含('')
if (string.IsNullOrEmpty(needle))
{
- return true;
+ return HasValue(fieldRef, context);
}
if (fieldRef.Table == "t_order_declaration")
@@ -36,6 +37,33 @@ public static class OrderFormulaFieldResolver
return ContainsValue(raw, needle);
}
+ ///
+ /// 字段有值:非 null;字符串另要求非空白。数值 0 / 布尔 false 视为有值。
+ ///
+ public static bool HasValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context) =>
+ IsPresent(GetRawValue(fieldRef, context));
+
+ ///
+ /// 字段为空:null,或字符串空白。
+ ///
+ public static bool IsEmpty(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context) =>
+ !HasValue(fieldRef, context);
+
+ private static bool IsPresent(object? value)
+ {
+ if (value == null)
+ {
+ return false;
+ }
+
+ if (value is string s)
+ {
+ return !string.IsNullOrWhiteSpace(s);
+ }
+
+ return true;
+ }
+
private static object? GetRawValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
{
if (fieldRef.Virtual)
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",
+ "美国");
+ }
+}