From d20c10a2f2038a89c63d090bd0d339f8411a447b Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 28 Oct 2026 15:07:40 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=85=AC=E5=BC=8F=E6=94=AF=E6=8C=81=E6=9C=89=E5=80=BC?= =?UTF-8?q?/=E4=B8=BA=E7=A9=BA=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Orders/Issues/ChineseFormulaEvaluator.cs | 70 +++++++++++++++++-- .../Issues/OrderFormulaFieldResolver.cs | 30 +++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs b/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs index 0017e30..b4dd466 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; @@ -306,6 +350,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 +392,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(); 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) From b87414aafd35929454dff9552d91794cd24dbef6 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 28 Oct 2026 15:40:14 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E8=87=AA=E5=8A=A8=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=85=AC=E5=BC=8F=E4=BF=AE=E5=A4=8D=E6=8B=AC=E5=8F=B7=E5=B8=83?= =?UTF-8?q?=E5=B0=94=E7=BB=84=E4=B8=8E=20||/&&=20=E7=9F=AD=E8=B7=AF?= =?UTF-8?q?=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Orders/Issues/ChineseFormulaEvaluator.cs | 29 +++++++- src/Oms.Tests/ChineseFormulaEvaluatorTests.cs | 73 +++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/Oms.Tests/ChineseFormulaEvaluatorTests.cs 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", + "美国"); + } +}