admin/src/Oms.Application/Services/Orders/Issues/ChineseFormulaEvaluator.cs
zzh 43563b727f feat: 自动触发公式引擎与查价运输方式分组接口
实现中文公式预报前拦截、规则测试 API 及按销售产品分组返回查价运输方式选项。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 18:36:17 +08:00

489 lines
14 KiB
C#

using System.Globalization;
using System.Text;
namespace Oms.Application.Services.Orders.Issues;
public static class ChineseFormulaEvaluator
{
private const string ContainsKeyword = "包含";
public static bool TryEvaluate(string? expression, OrderRuleEvaluationContext context, out bool result)
{
result = false;
var text = expression?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
result = true;
return true;
}
try
{
var parser = new Parser(text, context);
result = parser.ParseExpression();
parser.ExpectEnd();
return true;
}
catch (FormulaParseException)
{
return false;
}
}
private sealed class FormulaParseException(string message) : Exception(message);
private enum TokenKind
{
Number,
String,
Label,
And,
Or,
Not,
Plus,
Minus,
Multiply,
Divide,
Greater,
GreaterEqual,
Less,
LessEqual,
Equal,
NotEqual,
LeftParen,
RightParen,
Contains,
End
}
private readonly record struct Token(TokenKind Kind, string Text, decimal Number = 0);
private sealed class Lexer(string source)
{
private int _pos;
public Token Next()
{
SkipWhitespace();
if (_pos >= source.Length)
{
return new Token(TokenKind.End, string.Empty);
}
var ch = source[_pos];
if (ch is >= '0' and <= '9' || (ch == '.' && _pos + 1 < source.Length && source[_pos + 1] is >= '0' and <= '9'))
{
return ReadNumber();
}
if (ch == '\'')
{
return ReadString();
}
if (TryReadOperator(out var opToken))
{
return opToken;
}
if (TryReadLabel(out var labelToken))
{
return labelToken;
}
throw new FormulaParseException($"无法识别的字符:{source[_pos]}");
}
public Token Peek()
{
var saved = _pos;
var token = Next();
_pos = saved;
return token;
}
private void SkipWhitespace()
{
while (_pos < source.Length && char.IsWhiteSpace(source[_pos]))
{
_pos++;
}
}
private Token ReadNumber()
{
var start = _pos;
while (_pos < source.Length && (char.IsDigit(source[_pos]) || source[_pos] == '.'))
{
_pos++;
}
var text = source[start.._pos];
if (!decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
{
throw new FormulaParseException($"无效数字:{text}");
}
return new Token(TokenKind.Number, text, number);
}
private Token ReadString()
{
_pos++;
var sb = new StringBuilder();
while (_pos < source.Length)
{
var ch = source[_pos++];
if (ch == '\'')
{
if (_pos < source.Length && source[_pos] == '\'')
{
sb.Append('\'');
_pos++;
continue;
}
return new Token(TokenKind.String, sb.ToString());
}
sb.Append(ch);
}
throw new FormulaParseException("字符串未闭合");
}
private bool TryReadOperator(out Token token)
{
token = default;
if (_pos + 1 < source.Length)
{
var two = source.Substring(_pos, 2);
token = two switch
{
"&&" => new Token(TokenKind.And, two),
"||" => new Token(TokenKind.Or, two),
">=" => new Token(TokenKind.GreaterEqual, two),
"<=" => new Token(TokenKind.LessEqual, two),
"==" => new Token(TokenKind.Equal, two),
"!=" => new Token(TokenKind.NotEqual, two),
_ => default
};
if (token.Kind != default)
{
_pos += 2;
return true;
}
}
var ch = source[_pos];
token = ch switch
{
'+' => new Token(TokenKind.Plus, "+"),
'-' => new Token(TokenKind.Minus, "-"),
'*' => new Token(TokenKind.Multiply, "*"),
'/' => new Token(TokenKind.Divide, "/"),
'>' => new Token(TokenKind.Greater, ">"),
'<' => new Token(TokenKind.Less, "<"),
'!' => new Token(TokenKind.Not, "!"),
'(' => new Token(TokenKind.LeftParen, "("),
')' => new Token(TokenKind.RightParen, ")"),
_ => default
};
if (token.Kind == default)
{
return false;
}
_pos++;
return true;
}
private bool TryReadLabel(out Token token)
{
token = default;
foreach (var label in AutoTriggerFormulaMeta.LabelsByLengthDesc)
{
if (!source.AsSpan(_pos).StartsWith(label, StringComparison.Ordinal))
{
continue;
}
_pos += label.Length;
SkipWhitespace();
if (_pos + ContainsKeyword.Length <= source.Length
&& source.AsSpan(_pos, ContainsKeyword.Length).SequenceEqual(ContainsKeyword.AsSpan()))
{
_pos += ContainsKeyword.Length;
SkipWhitespace();
if (_pos < source.Length && source[_pos] == '(')
{
_pos++;
token = new Token(TokenKind.Contains, label);
return true;
}
}
if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
{
_pos += 2;
token = new Token(TokenKind.Label, label);
return true;
}
_pos -= label.Length;
}
return false;
}
public int SavePosition() => _pos;
public void RestorePosition(int pos) => _pos = pos;
}
private sealed class Parser
{
private readonly Lexer _lexer;
private readonly OrderRuleEvaluationContext _context;
private Token _current;
public Parser(string source, OrderRuleEvaluationContext context)
{
_lexer = new Lexer(source);
_context = context;
_current = _lexer.Next();
}
public bool ParseExpression() => ParseOr();
public void ExpectEnd()
{
if (_current.Kind != TokenKind.End)
{
throw new FormulaParseException($"公式未预期结束:{_current.Text}");
}
}
private bool ParseOr()
{
var value = ParseAnd();
while (_current.Kind == TokenKind.Or)
{
Advance();
value = value || ParseAnd();
}
return value;
}
private bool ParseAnd()
{
var value = ParseNot();
while (_current.Kind == TokenKind.And)
{
Advance();
value = value && ParseNot();
}
return value;
}
private bool ParseNot()
{
if (_current.Kind == TokenKind.Not)
{
Advance();
return !ParseNot();
}
return ParseComparison();
}
private bool ParseComparison()
{
if (_current.Kind is TokenKind.Contains)
{
return ParseContains();
}
var left = ParseAdditive();
if (_current.Kind is TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual or TokenKind.Equal or TokenKind.NotEqual)
{
var op = _current.Kind;
Advance();
var right = ParseAdditive();
return Compare(left, op, right);
}
return left != 0;
}
private bool ParseContains()
{
var label = _current.Text;
Advance();
if (_current.Kind != TokenKind.String)
{
throw new FormulaParseException("包含函数缺少字符串参数");
}
var needle = _current.Text;
Advance();
if (_current.Kind != TokenKind.RightParen)
{
throw new FormulaParseException("包含函数缺少右括号");
}
Advance();
if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
{
throw new FormulaParseException($"未知字段:{label}");
}
return OrderFormulaFieldResolver.StringContains(fieldRef, _context, needle);
}
private decimal ParseAdditive()
{
var value = ParseMultiplicative();
while (_current.Kind is TokenKind.Plus or TokenKind.Minus)
{
var op = _current.Kind;
Advance();
var right = ParseMultiplicative();
value = op == TokenKind.Plus ? value + right : value - right;
}
return value;
}
private decimal ParseMultiplicative()
{
var value = ParseUnary();
while (_current.Kind is TokenKind.Multiply or TokenKind.Divide)
{
var op = _current.Kind;
Advance();
var right = ParseUnary();
value = op == TokenKind.Multiply ? value * right : right == 0 ? 0 : value / right;
}
return value;
}
private decimal ParseUnary()
{
if (_current.Kind == TokenKind.Minus)
{
Advance();
return -ParseUnary();
}
return ParsePrimary();
}
private decimal ParsePrimary()
{
switch (_current.Kind)
{
case TokenKind.Number:
{
var number = _current.Number;
Advance();
return number;
}
case TokenKind.Label:
{
var label = _current.Text;
Advance();
if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
{
throw new FormulaParseException($"未知字段:{label}");
}
return OrderFormulaFieldResolver.GetNumericValue(fieldRef, _context);
}
case TokenKind.LeftParen:
{
Advance();
decimal value;
if (LooksLikeBooleanGroup())
{
var inside = ParseOr();
if (_current.Kind != TokenKind.RightParen)
{
throw new FormulaParseException("缺少右括号");
}
Advance();
value = inside ? 1 : 0;
}
else
{
value = ParseAdditive();
if (_current.Kind != TokenKind.RightParen)
{
throw new FormulaParseException("缺少右括号");
}
Advance();
}
return value;
}
default:
throw new FormulaParseException($"无法解析:{_current.Text}");
}
}
private static bool Compare(decimal left, TokenKind op, decimal right) =>
op switch
{
TokenKind.Greater => left > right,
TokenKind.GreaterEqual => left >= right,
TokenKind.Less => left < right,
TokenKind.LessEqual => left <= right,
TokenKind.Equal => left == right,
TokenKind.NotEqual => left != right,
_ => false
};
private bool LooksLikeBooleanGroup()
{
var savedPos = _lexer.SavePosition();
var depth = 1;
while (depth > 0)
{
var token = _lexer.Next();
if (token.Kind == TokenKind.End)
{
break;
}
if (token.Kind == TokenKind.LeftParen)
{
depth++;
continue;
}
if (token.Kind == TokenKind.RightParen)
{
depth--;
continue;
}
if (depth == 1 && token.Kind is TokenKind.And or TokenKind.Or)
{
_lexer.RestorePosition(savedPos);
return true;
}
}
_lexer.RestorePosition(savedPos);
return false;
}
private void Advance() => _current = _lexer.Next();
}
}