Compare commits
No commits in common. "7f8d4cbe19044a9fa2b4be7203b5ccf1e8f0be13" and "46455b2ae9dc090cb0f4e81f96dfd2f25f5503bd" have entirely different histories.
7f8d4cbe19
...
46455b2ae9
@ -5,9 +5,7 @@ namespace Oms.Application.Services.Orders.Issues;
|
|||||||
|
|
||||||
public static class ChineseFormulaEvaluator
|
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)
|
public static bool TryEvaluate(string? expression, OrderRuleEvaluationContext context, out bool result)
|
||||||
{
|
{
|
||||||
@ -55,8 +53,6 @@ public static class ChineseFormulaEvaluator
|
|||||||
LeftParen,
|
LeftParen,
|
||||||
RightParen,
|
RightParen,
|
||||||
Contains,
|
Contains,
|
||||||
HasValue,
|
|
||||||
IsEmpty,
|
|
||||||
End
|
End
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,14 +208,12 @@ public static class ChineseFormulaEvaluator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var afterLabel = _pos + label.Length;
|
_pos += label.Length;
|
||||||
_pos = afterLabel;
|
|
||||||
SkipWhitespace();
|
SkipWhitespace();
|
||||||
if (_pos + ContainsKeyword.Length <= source.Length
|
if (_pos + ContainsKeyword.Length <= source.Length
|
||||||
&& source.AsSpan(_pos, ContainsKeyword.Length).SequenceEqual(ContainsKeyword.AsSpan()))
|
&& source.AsSpan(_pos, ContainsKeyword.Length).SequenceEqual(ContainsKeyword.AsSpan()))
|
||||||
{
|
{
|
||||||
var afterContainsKeyword = _pos + ContainsKeyword.Length;
|
_pos += ContainsKeyword.Length;
|
||||||
_pos = afterContainsKeyword;
|
|
||||||
SkipWhitespace();
|
SkipWhitespace();
|
||||||
if (_pos < source.Length && source[_pos] == '(')
|
if (_pos < source.Length && source[_pos] == '(')
|
||||||
{
|
{
|
||||||
@ -227,22 +221,6 @@ public static class ChineseFormulaEvaluator
|
|||||||
token = new Token(TokenKind.Contains, label);
|
token = new Token(TokenKind.Contains, label);
|
||||||
return true;
|
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()))
|
if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
|
||||||
@ -252,34 +230,12 @@ public static class ChineseFormulaEvaluator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pos = afterLabel - label.Length;
|
_pos -= label.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>匹配「有值()」「为空()」这类无参关键字。</summary>
|
|
||||||
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 int SavePosition() => _pos;
|
||||||
|
|
||||||
public void RestorePosition(int pos) => _pos = pos;
|
public void RestorePosition(int pos) => _pos = pos;
|
||||||
@ -314,9 +270,7 @@ public static class ChineseFormulaEvaluator
|
|||||||
while (_current.Kind == TokenKind.Or)
|
while (_current.Kind == TokenKind.Or)
|
||||||
{
|
{
|
||||||
Advance();
|
Advance();
|
||||||
// 必须先解析右侧,禁止用 || 短路跳过,否则剩余 token 会让 ExpectEnd 失败
|
value = value || ParseAnd();
|
||||||
var right = ParseAnd();
|
|
||||||
value = value || right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@ -328,9 +282,7 @@ public static class ChineseFormulaEvaluator
|
|||||||
while (_current.Kind == TokenKind.And)
|
while (_current.Kind == TokenKind.And)
|
||||||
{
|
{
|
||||||
Advance();
|
Advance();
|
||||||
// 同上:禁止 && 短路跳过右侧解析
|
value = value && ParseNot();
|
||||||
var right = ParseNot();
|
|
||||||
value = value && right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@ -354,11 +306,6 @@ public static class ChineseFormulaEvaluator
|
|||||||
return ParseContains();
|
return ParseContains();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_current.Kind is TokenKind.HasValue or TokenKind.IsEmpty)
|
|
||||||
{
|
|
||||||
return ParsePresence(_current.Kind == TokenKind.HasValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
var left = ParseAdditive();
|
var left = ParseAdditive();
|
||||||
if (_current.Kind is TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual or TokenKind.Equal or TokenKind.NotEqual)
|
if (_current.Kind is TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual or TokenKind.Equal or TokenKind.NotEqual)
|
||||||
{
|
{
|
||||||
@ -396,19 +343,6 @@ public static class ChineseFormulaEvaluator
|
|||||||
return OrderFormulaFieldResolver.StringContains(fieldRef, _context, needle);
|
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()
|
private decimal ParseAdditive()
|
||||||
{
|
{
|
||||||
var value = ParseMultiplicative();
|
var value = ParseMultiplicative();
|
||||||
@ -514,18 +448,8 @@ public static class ChineseFormulaEvaluator
|
|||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 括号内是否应按布尔表达式解析。
|
|
||||||
/// 注意:调用前已 Advance 掉 '(',第一个内部 token 在 _current;词法位置在其后。
|
|
||||||
/// 旧逻辑只向后扫逻辑运算符,导致括号内的包含/有值/为空/比较被误判为数值式而解析失败。
|
|
||||||
/// </summary>
|
|
||||||
private bool LooksLikeBooleanGroup()
|
private bool LooksLikeBooleanGroup()
|
||||||
{
|
{
|
||||||
if (IsBooleanAtomKind(_current.Kind))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var savedPos = _lexer.SavePosition();
|
var savedPos = _lexer.SavePosition();
|
||||||
var depth = 1;
|
var depth = 1;
|
||||||
while (depth > 0)
|
while (depth > 0)
|
||||||
@ -548,7 +472,7 @@ public static class ChineseFormulaEvaluator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (depth == 1 && IsBooleanGroupIndicator(token.Kind))
|
if (depth == 1 && token.Kind is TokenKind.And or TokenKind.Or)
|
||||||
{
|
{
|
||||||
_lexer.RestorePosition(savedPos);
|
_lexer.RestorePosition(savedPos);
|
||||||
return true;
|
return true;
|
||||||
@ -559,15 +483,6 @@ public static class ChineseFormulaEvaluator
|
|||||||
return false;
|
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();
|
private void Advance() => _current = _lexer.Next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,10 +16,9 @@ public static class OrderFormulaFieldResolver
|
|||||||
|
|
||||||
public static bool StringContains(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context, string needle)
|
public static bool StringContains(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context, string needle)
|
||||||
{
|
{
|
||||||
// 空参数:表示「字段有值」(非 null / 非空白),用于必填校验,如 国家包含('')
|
|
||||||
if (string.IsNullOrEmpty(needle))
|
if (string.IsNullOrEmpty(needle))
|
||||||
{
|
{
|
||||||
return HasValue(fieldRef, context);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldRef.Table == "t_order_declaration")
|
if (fieldRef.Table == "t_order_declaration")
|
||||||
@ -37,33 +36,6 @@ public static class OrderFormulaFieldResolver
|
|||||||
return ContainsValue(raw, needle);
|
return ContainsValue(raw, needle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段有值:非 null;字符串另要求非空白。数值 0 / 布尔 false 视为有值。
|
|
||||||
/// </summary>
|
|
||||||
public static bool HasValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context) =>
|
|
||||||
IsPresent(GetRawValue(fieldRef, context));
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 字段为空:null,或字符串空白。
|
|
||||||
/// </summary>
|
|
||||||
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)
|
private static object? GetRawValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
|
||||||
{
|
{
|
||||||
if (fieldRef.Virtual)
|
if (fieldRef.Virtual)
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
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",
|
|
||||||
"美国");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user