feat: 自动触发公式引擎与查价运输方式分组接口

实现中文公式预报前拦截、规则测试 API 及按销售产品分组返回查价运输方式选项。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-09 18:36:17 +08:00
parent d3a95a6bad
commit 43563b727f
23 changed files with 1808 additions and 24 deletions

2
db.sql
View File

@ -730,6 +730,8 @@ CREATE TABLE `t_weight_rounding_info` (
CREATE TABLE `t_auto_trigger` ( CREATE TABLE `t_auto_trigger` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1', `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
`Status` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用', `Status` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用',
`RuleName` VARCHAR(128) NULL COMMENT '规则名称',
`Priority` INT NOT NULL DEFAULT 100 COMMENT '优先级,数值越小越先执行',
`Operation` VARCHAR(32) NULL COMMENT '操作:拦截/警告/问题件', `Operation` VARCHAR(32) NULL COMMENT '操作:拦截/警告/问题件',
`TriggerTiming` VARCHAR(32) NULL COMMENT '触发时机:下单/入库/出库', `TriggerTiming` VARCHAR(32) NULL COMMENT '触发时机:下单/入库/出库',
`TriggerCondition` TEXT COMMENT '触发条件', `TriggerCondition` TEXT COMMENT '触发条件',

View File

@ -0,0 +1,18 @@
-- t_auto_trigger 规则名与优先级(可重复执行)
-- 在 OMS 业务库执行:mysql -h<host> -u<user> -p<db> < migrate_t_auto_trigger_rule_meta.sql
SET @db = DATABASE();
SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_auto_trigger' AND COLUMN_NAME = 'RuleName');
SET @sql := IF(@exist = 0,
'ALTER TABLE `t_auto_trigger` ADD COLUMN `RuleName` VARCHAR(128) NULL COMMENT ''规则名称'' AFTER `Status`',
'SELECT ''skip RuleName''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_auto_trigger' AND COLUMN_NAME = 'Priority');
SET @sql := IF(@exist = 0,
'ALTER TABLE `t_auto_trigger` ADD COLUMN `Priority` INT NOT NULL DEFAULT 100 COMMENT ''优先级,数值越小越先执行'' AFTER `RuleName`',
'SELECT ''skip Priority''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,32 @@
namespace Oms.Application.Dtos.Channels;
public class AutoTriggerMetaOutput
{
public List<AutoTriggerFormulaCategoryOutput> Categories { get; set; } = [];
public List<AutoTriggerFormulaFieldOutput> Fields { get; set; } = [];
public string OversizeExampleFormula { get; set; } = string.Empty;
public int FormulaMaxLength { get; set; } = 10000;
}
public class AutoTriggerFormulaCategoryOutput
{
public string Key { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
}
public class AutoTriggerFormulaFieldOutput
{
public string Table { get; set; } = string.Empty;
public string Field { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string ValueType { get; set; } = string.Empty;
public bool Virtual { get; set; }
}

View File

@ -15,6 +15,16 @@ public class AutoTriggerOutput
/// </summary> /// </summary>
public byte Status { get; set; } public byte Status { get; set; }
/// <summary>
/// 规则名称。
/// </summary>
public string? RuleName { get; set; }
/// <summary>
/// 优先级。
/// </summary>
public int Priority { get; set; }
/// <summary> /// <summary>
/// 操作类型。 /// 操作类型。
/// </summary> /// </summary>

View File

@ -17,6 +17,16 @@ public class AutoTriggerSaveInput
/// </summary> /// </summary>
public EnableStatus Status { get; set; } public EnableStatus Status { get; set; }
/// <summary>
/// 规则名称。
/// </summary>
public string? RuleName { get; set; }
/// <summary>
/// 优先级,数值越小越先执行。
/// </summary>
public int Priority { get; set; } = 100;
/// <summary> /// <summary>
/// 操作类型。 /// 操作类型。
/// </summary> /// </summary>

View File

@ -0,0 +1,49 @@
namespace Oms.Application.Dtos.Channels;
public class AutoTriggerTestInput
{
/// <summary>
/// 公式正文(与 TriggerCondition 二选一)。
/// </summary>
public string? Expression { get; set; }
/// <summary>
/// 完整触发条件(纯公式或 JSON 包装)。
/// </summary>
public string? TriggerCondition { get; set; }
/// <summary>
/// 使用已有订单作为评估上下文。
/// </summary>
public int? OrderId { get; set; }
/// <summary>
/// 使用样例数据作为预报前评估上下文。
/// </summary>
public AutoTriggerTestSampleInput? Sample { get; set; }
}
public class AutoTriggerTestSampleInput
{
public string? ProductCode { get; set; }
public string? ChannelCode { get; set; }
public string? ConsigneeCountry { get; set; }
public string? ConsigneeCountryName { get; set; }
public int? CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? ClientNo { get; set; }
public decimal? Length { get; set; }
public decimal? Width { get; set; }
public decimal? Height { get; set; }
public decimal? ActualWeight { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace Oms.Application.Dtos.Channels;
public class AutoTriggerTestOutput
{
public bool Matched { get; set; }
public string? Expression { get; set; }
public string? ContextType { get; set; }
public string? Error { get; set; }
}

View File

@ -0,0 +1,38 @@
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 查价运输方式分组选项。
/// </summary>
public class OrderPriceCheckShippingOptionsOutput
{
public List<OrderPriceCheckShippingGroupOutput> Groups { get; set; } = [];
}
public class OrderPriceCheckShippingGroupOutput
{
/// <summary>
/// 分组编码(字典 sales_product_group.dictCode)。
/// </summary>
public string GroupCode { get; set; } = string.Empty;
/// <summary>
/// 分组名称(字典 sales_product_group.dictName)。
/// </summary>
public string GroupName { get; set; } = string.Empty;
public List<OrderPriceCheckShippingOptionOutput> Products { get; set; } = [];
}
public class OrderPriceCheckShippingOptionOutput
{
public int ProductId { get; set; }
public string ProductCode { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// 下拉展示文案。
/// </summary>
public string Label { get; set; } = string.Empty;
}

View File

@ -3,6 +3,7 @@ using Fake.Application;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Services.Orders.Issues;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
@ -16,12 +17,11 @@ namespace Oms.Application.Services.Channels;
public class AutoTriggerService : ApplicationService public class AutoTriggerService : ApplicationService
{ {
private IAutoTriggerRepository AutoTriggerRepository => LazyServiceProvider.GetRequiredService<IAutoTriggerRepository>(); private IAutoTriggerRepository AutoTriggerRepository => LazyServiceProvider.GetRequiredService<IAutoTriggerRepository>();
private OrderAutoTriggerService OrderAutoTriggerService => LazyServiceProvider.GetRequiredService<OrderAutoTriggerService>();
/// <summary> /// <summary>
/// 查询自动触发规则列表。 /// 查询自动触发规则列表。
/// </summary> /// </summary>
/// <param name="input">入参:自动触发规则列表查询条件。</param>
/// <returns>自动触发规则分页结果,包含列表数据和总记录数。</returns>
[HttpGet("/channel/auto-trigger/list")] [HttpGet("/channel/auto-trigger/list")]
public async Task<OmsPageResult<AutoTriggerOutput>> GetListAsync([FromQuery] AutoTriggerListInput input) public async Task<OmsPageResult<AutoTriggerOutput>> GetListAsync([FromQuery] AutoTriggerListInput input)
{ {
@ -32,23 +32,37 @@ public class AutoTriggerService : ApplicationService
predicate, predicate,
pageIndex, pageIndex,
pageSize, pageSize,
new Dictionary<string, bool> { [nameof(AutoTrigger.UpdateTime)] = false, [nameof(AutoTrigger.Id)] = false }); new Dictionary<string, bool> { [nameof(AutoTrigger.Priority)] = true, [nameof(AutoTrigger.Id)] = true });
var total = await AutoTriggerRepository.CountAsync(predicate); var total = await AutoTriggerRepository.CountAsync(predicate);
var items = entities.Select(MapOutput).ToList(); var items = entities.Select(MapOutput).ToList();
return new OmsPageResult<AutoTriggerOutput> { Items = items, Total = total }; return new OmsPageResult<AutoTriggerOutput> { Items = items, Total = total };
} }
/// <summary>
/// 获取公式字段元数据。
/// </summary>
[HttpGet("/channel/auto-trigger/meta")]
public Task<AutoTriggerMetaOutput> GetMetaAsync() =>
Task.FromResult(AutoTriggerFormulaMeta.BuildMetaOutput());
/// <summary>
/// 测试公式是否命中。
/// </summary>
[HttpPost("/channel/auto-trigger/test")]
public Task<AutoTriggerTestOutput> TestFormulaAsync(AutoTriggerTestInput input) =>
OrderAutoTriggerService.TestFormulaAsync(input);
/// <summary> /// <summary>
/// 新增自动触发规则。 /// 新增自动触发规则。
/// </summary> /// </summary>
/// <param name="input">入参:自动触发规则保存参数。</param>
/// <returns>无返回体,仅完成新增自动触发规则。</returns>
[HttpPost("/channel/auto-trigger/create")] [HttpPost("/channel/auto-trigger/create")]
public async Task CreateAsync(AutoTriggerSaveInput input) public async Task CreateAsync(AutoTriggerSaveInput input)
{ {
var entity = new AutoTrigger var entity = new AutoTrigger
{ {
Status = input.Status, Status = input.Status,
RuleName = NormalizeRuleName(input),
Priority = NormalizePriority(input),
Operation = input.Operation, Operation = input.Operation,
TriggerTiming = input.TriggerTiming, TriggerTiming = input.TriggerTiming,
TriggerCondition = input.TriggerCondition, TriggerCondition = input.TriggerCondition,
@ -61,13 +75,13 @@ public class AutoTriggerService : ApplicationService
/// <summary> /// <summary>
/// 更新自动触发规则。 /// 更新自动触发规则。
/// </summary> /// </summary>
/// <param name="input">入参:自动触发规则保存参数。</param>
/// <returns>无返回体,仅完成更新自动触发规则。</returns>
[HttpPost("/channel/auto-trigger/update")] [HttpPost("/channel/auto-trigger/update")]
public async Task UpdateAsync(AutoTriggerSaveInput input) public async Task UpdateAsync(AutoTriggerSaveInput input)
{ {
var entity = await GetEntityOrThrowAsync(input.Id); var entity = await GetEntityOrThrowAsync(input.Id);
entity.Status = input.Status; entity.Status = input.Status;
entity.RuleName = NormalizeRuleName(input);
entity.Priority = NormalizePriority(input);
entity.Operation = input.Operation; entity.Operation = input.Operation;
entity.TriggerTiming = input.TriggerTiming; entity.TriggerTiming = input.TriggerTiming;
entity.TriggerCondition = input.TriggerCondition; entity.TriggerCondition = input.TriggerCondition;
@ -79,8 +93,6 @@ public class AutoTriggerService : ApplicationService
/// <summary> /// <summary>
/// 删除自动触发规则。 /// 删除自动触发规则。
/// </summary> /// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体,仅完成删除自动触发规则。</returns>
[HttpPost("/channel/auto-trigger/delete")] [HttpPost("/channel/auto-trigger/delete")]
public async Task DeleteAsync(IdInput input) public async Task DeleteAsync(IdInput input)
{ {
@ -91,8 +103,6 @@ public class AutoTriggerService : ApplicationService
/// <summary> /// <summary>
/// 切换自动触发规则状态。 /// 切换自动触发规则状态。
/// </summary> /// </summary>
/// <param name="input">入参:目标主键和状态值。</param>
/// <returns>无返回体,仅完成切换自动触发规则状态。</returns>
[HttpPost("/channel/auto-trigger/toggleStatus")] [HttpPost("/channel/auto-trigger/toggleStatus")]
public async Task ToggleStatusAsync(ToggleStatusInput input) public async Task ToggleStatusAsync(ToggleStatusInput input)
{ {
@ -126,12 +136,35 @@ public class AutoTriggerService : ApplicationService
return entity; return entity;
} }
private static string? NormalizeRuleName(AutoTriggerSaveInput input)
{
if (!string.IsNullOrWhiteSpace(input.RuleName))
{
return input.RuleName.Trim();
}
return AutoTriggerConditionParser.Parse(input.TriggerCondition).RuleName;
}
private static int NormalizePriority(AutoTriggerSaveInput input)
{
if (input.Priority > 0)
{
return input.Priority;
}
var parsed = AutoTriggerConditionParser.Parse(input.TriggerCondition);
return parsed.Priority > 0 ? parsed.Priority : 100;
}
private static AutoTriggerOutput MapOutput(AutoTrigger entity) private static AutoTriggerOutput MapOutput(AutoTrigger entity)
{ {
return new AutoTriggerOutput return new AutoTriggerOutput
{ {
Id = entity.Id, Id = entity.Id,
Status = (byte)entity.Status, Status = (byte)entity.Status,
RuleName = AutoTriggerRuleResolver.GetRuleName(entity),
Priority = AutoTriggerRuleResolver.GetPriority(entity),
Operation = entity.Operation, Operation = entity.Operation,
TriggerTiming = entity.TriggerTiming, TriggerTiming = entity.TriggerTiming,
TriggerCondition = entity.TriggerCondition, TriggerCondition = entity.TriggerCondition,

View File

@ -6,7 +6,10 @@ namespace Oms.Application.Services.Orders.Issues;
public static class AutoTriggerConditionMatcher public static class AutoTriggerConditionMatcher
{ {
public static bool Matches(Order order, string? triggerCondition) public static bool Matches(Order order, string? triggerCondition) =>
Matches(OrderRuleEvaluationContext.FromOrderOnly(order), triggerCondition);
public static bool Matches(OrderRuleEvaluationContext context, string? triggerCondition)
{ {
var text = triggerCondition?.Trim(); var text = triggerCondition?.Trim();
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
@ -14,18 +17,37 @@ public static class AutoTriggerConditionMatcher
return true; return true;
} }
if (text.StartsWith('{') && text.EndsWith('}')) var parsed = AutoTriggerConditionParser.Parse(text);
if (ShouldUseFormulaEvaluator(parsed, text))
{ {
return MatchesJson(order, text); if (string.IsNullOrWhiteSpace(parsed.Expression))
{
return true;
}
if (ChineseFormulaEvaluator.TryEvaluate(parsed.Expression, context, out var result))
{
return result;
}
} }
var lowered = text.ToLowerInvariant(); if (parsed.IsLegacyJsonMatcher && text.StartsWith('{') && text.EndsWith('}'))
if (IssueTypeCodes.All.Contains(lowered))
{ {
return true; return MatchesLegacyJson(context.Order, text);
} }
return ContainsKeyword(order, text); if (context.Order != null)
{
var lowered = text.ToLowerInvariant();
if (IssueTypeCodes.All.Contains(lowered))
{
return true;
}
return ContainsKeyword(context.Order, text);
}
return false;
} }
public static string ResolveIssueType(string? triggerCondition) public static string ResolveIssueType(string? triggerCondition)
@ -80,8 +102,18 @@ public static class AutoTriggerConditionMatcher
.ToList(); .ToList();
} }
private static bool MatchesJson(Order order, string json) private static bool ShouldUseFormulaEvaluator(ParsedTriggerCondition parsed, string rawText) =>
parsed.IsFormulaVersion
|| (!string.IsNullOrWhiteSpace(parsed.Expression) && AutoTriggerConditionParser.LooksLikeFormula(parsed.Expression))
|| (!parsed.IsLegacyJsonMatcher && AutoTriggerConditionParser.LooksLikeFormula(rawText));
private static bool MatchesLegacyJson(Order? order, string json)
{ {
if (order == null)
{
return false;
}
try try
{ {
using var doc = JsonDocument.Parse(json); using var doc = JsonDocument.Parse(json);

View File

@ -0,0 +1,101 @@
using System.Text.Json;
namespace Oms.Application.Services.Orders.Issues;
public sealed record ParsedTriggerCondition(
string? Expression,
string? RuleName,
int Priority,
bool IsFormulaVersion,
bool IsLegacyJsonMatcher)
{
public static ParsedTriggerCondition Empty { get; } = new(null, null, 100, false, false);
}
public static class AutoTriggerConditionParser
{
public static ParsedTriggerCondition Parse(string? raw)
{
var text = raw?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return ParsedTriggerCondition.Empty;
}
if (!text.StartsWith('{') || !text.EndsWith('}'))
{
return new ParsedTriggerCondition(text, null, 100, LooksLikeFormula(text), false);
}
try
{
using var doc = JsonDocument.Parse(text);
var root = doc.RootElement;
if (root.TryGetProperty("version", out var versionElement))
{
var version = versionElement.GetString()?.Trim();
if (string.Equals(version, "formula", StringComparison.OrdinalIgnoreCase))
{
var expression = root.TryGetProperty("expression", out var exprElement)
? exprElement.GetString()?.Trim()
: null;
var ruleName = root.TryGetProperty("ruleName", out var nameElement)
? nameElement.GetString()?.Trim()
: null;
var priority = root.TryGetProperty("priority", out var priorityElement) && priorityElement.TryGetInt32(out var p)
? p
: 100;
return new ParsedTriggerCondition(expression, ruleName, priority, true, false);
}
}
if (root.TryGetProperty("orderStatus", out _)
|| root.TryGetProperty("productCode", out _)
|| root.TryGetProperty("channelCode", out _)
|| root.TryGetProperty("keyword", out _))
{
return new ParsedTriggerCondition(null, null, 100, false, true);
}
if (root.TryGetProperty("issueType", out _))
{
return new ParsedTriggerCondition(null, null, 100, false, true);
}
}
catch (JsonException)
{
// fall through
}
return new ParsedTriggerCondition(text, null, 100, LooksLikeFormula(text), false);
}
public static bool LooksLikeFormula(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
if (text.Contains("包含('", StringComparison.Ordinal)
|| text.Contains("&&", StringComparison.Ordinal)
|| text.Contains("||", StringComparison.Ordinal)
|| text.Contains("==", StringComparison.Ordinal)
|| text.Contains("!=", StringComparison.Ordinal)
|| text.Contains(">=", StringComparison.Ordinal)
|| text.Contains("<=", StringComparison.Ordinal))
{
return true;
}
foreach (var label in AutoTriggerFormulaMeta.LabelsByLengthDesc)
{
if (text.Contains(label, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,255 @@
namespace Oms.Application.Services.Orders.Issues;
using Oms.Application.Dtos.Channels;
public enum FormulaValueType
{
String,
Number,
Boolean,
DateTime
}
public sealed record FormulaFieldRef(string Table, string Field, bool Virtual = false);
/// <summary>
/// 中文字段名 → 库表字段映射(与前端 formulaMeta.ts 保持一致)。
/// </summary>
public static class AutoTriggerFormulaMeta
{
public static IReadOnlyDictionary<string, FormulaFieldRef> LabelToRef { get; }
public static IReadOnlyDictionary<string, FormulaValueType> LabelValueTypes { get; }
/// <summary>按长度降序排列的标签,供词法分析最长匹配。</summary>
public static IReadOnlyList<string> LabelsByLengthDesc { get; }
static AutoTriggerFormulaMeta()
{
var fields = BuildFieldDefs();
var labelToRef = new Dictionary<string, FormulaFieldRef>(StringComparer.Ordinal);
var labelValueTypes = new Dictionary<string, FormulaValueType>(StringComparer.Ordinal);
foreach (var (label, table, field, valueType, virtualField) in fields)
{
labelToRef[label] = new FormulaFieldRef(table, field, virtualField);
labelValueTypes[label] = valueType;
}
LabelToRef = labelToRef;
LabelValueTypes = labelValueTypes;
LabelsByLengthDesc = labelToRef.Keys.OrderByDescending(x => x.Length).ToList();
}
public static bool TryResolve(string label, out FormulaFieldRef? fieldRef) =>
LabelToRef.TryGetValue(label, out fieldRef);
public static AutoTriggerMetaOutput BuildMetaOutput()
{
var categories = new List<AutoTriggerFormulaCategoryOutput>
{
new() { Key = "t_order", Label = "订单" },
new() { Key = "t_order_recipient", Label = "收件人" },
new() { Key = "t_order_sender", Label = "发件人" },
new() { Key = "t_order_declaration", Label = "申报" },
new() { Key = "t_order_weight", Label = "件重尺" },
new() { Key = "t_order_fee", Label = "费用" },
new() { Key = "t_customer", Label = "客户" },
new() { Key = "_virtual", Label = "常用" },
};
var fields = LabelToRef
.Select(x =>
{
LabelValueTypes.TryGetValue(x.Key, out var valueType);
return new AutoTriggerFormulaFieldOutput
{
Label = x.Key,
Table = x.Value.Table,
Field = x.Value.Field,
Virtual = x.Value.Virtual,
ValueType = valueType switch
{
FormulaValueType.String => "string",
FormulaValueType.Number => "number",
FormulaValueType.Boolean => "boolean",
FormulaValueType.DateTime => "datetime",
_ => "string"
}
};
})
.OrderBy(x => x.Table)
.ThenBy(x => x.Label)
.ToList();
return new AutoTriggerMetaOutput
{
Categories = categories,
Fields = fields,
OversizeExampleFormula =
"(单件最大长度()>60||单件最大宽度()>46||((单件最大长度()+单件最大宽度()+单件最大高度())>150))&&国家包含('GB')&&客户包含('阿里国际站/深圳一达通')&&!销售产品包含('EX_ASP_HKDHL_YD')",
FormulaMaxLength = 10000
};
}
private static IEnumerable<(string Label, string Table, string Field, FormulaValueType ValueType, bool Virtual)> BuildFieldDefs()
{
// t_order
yield return ("请求标识", "t_order", "RequestId", FormulaValueType.String, false);
yield return ("运单号", "t_order", "ServiceNo", FormulaValueType.String, false);
yield return ("客户单号", "t_order", "ClientNo", FormulaValueType.String, false);
yield return ("跟踪号", "t_order", "TrackNo", FormulaValueType.String, false);
yield return ("销售产品", "t_order", "ProductCode", FormulaValueType.String, false);
yield return ("收货渠道", "t_order", "ChannelCode", FormulaValueType.String, false);
yield return ("下游服务商", "t_order", "DownstreamSupplierCode", FormulaValueType.String, false);
yield return ("下游AppKey", "t_order", "DownstreamAppKey", FormulaValueType.String, false);
yield return ("下游AppId", "t_order", "DownstreamAppId", FormulaValueType.Number, false);
yield return ("渠道预报编码", "t_order", "ChannelForecastCode", FormulaValueType.String, false);
yield return ("国家", "t_order", "ConsigneeCountry", FormulaValueType.String, false);
yield return ("收件国家名称", "t_order", "ConsigneeCountryName", FormulaValueType.String, false);
yield return ("货物属性", "t_order", "CargoAttribute", FormulaValueType.String, false);
yield return ("出口报关方式", "t_order", "ExportCustomsMode", FormulaValueType.String, false);
yield return ("预报失败原因", "t_order", "PredeclareFailReason", FormulaValueType.String, false);
yield return ("规则版本号", "t_order", "SalesProductRulesVersion", FormulaValueType.String, false);
yield return ("价格版本号", "t_order", "SalesProductPriceVersion", FormulaValueType.String, false);
yield return ("订单状态", "t_order", "OrderStatus", FormulaValueType.String, false);
yield return ("处理子状态", "t_order", "HandleStatus", FormulaValueType.String, false);
yield return ("问题类型", "t_order", "IssueType", FormulaValueType.String, false);
yield return ("问题标题", "t_order", "IssueTitle", FormulaValueType.String, false);
yield return ("问题内容", "t_order", "IssueContent", FormulaValueType.String, false);
yield return ("问题模板", "t_order", "IssueTemplateId", FormulaValueType.Number, false);
yield return ("问题备注", "t_order", "IssueRemark", FormulaValueType.String, false);
yield return ("问题件时间", "t_order", "IssueTime", FormulaValueType.DateTime, false);
yield return ("理赔类型", "t_order", "ClaimType", FormulaValueType.String, false);
yield return ("索赔金额", "t_order", "ClaimAmount", FormulaValueType.Number, false);
yield return ("索赔币种", "t_order", "ClaimCurrency", FormulaValueType.String, false);
yield return ("理赔原因", "t_order", "ClaimReason", FormulaValueType.String, false);
yield return ("理赔时间", "t_order", "ClaimTime", FormulaValueType.DateTime, false);
yield return ("异常前状态", "t_order", "PreviousOrderStatus", FormulaValueType.String, false);
yield return ("仓库", "t_order", "WarehouseId", FormulaValueType.Number, false);
yield return ("订单客户", "t_order", "CustomerId", FormulaValueType.Number, false);
yield return ("组织", "t_order", "OrganizationId", FormulaValueType.String, false);
yield return ("密级", "t_order", "SecretLevel", FormulaValueType.Number, false);
yield return ("发件人关联", "t_order", "SenderId", FormulaValueType.Number, false);
yield return ("收件人关联", "t_order", "RecipientId", FormulaValueType.Number, false);
yield return ("申报关联", "t_order", "DeclarationId", FormulaValueType.Number, false);
yield return ("收货时间", "t_order", "ReceiveTime", FormulaValueType.DateTime, false);
yield return ("发货时间", "t_order", "ShipTime", FormulaValueType.DateTime, false);
yield return ("支付时间", "t_order", "PayTime", FormulaValueType.DateTime, false);
yield return ("是否已支付", "t_order", "IsPaid", FormulaValueType.Boolean, false);
yield return ("签收时间", "t_order", "SignTime", FormulaValueType.DateTime, false);
yield return ("是否COD", "t_order", "IsCod", FormulaValueType.Boolean, false);
yield return ("COD币种", "t_order", "CodCurrency", FormulaValueType.String, false);
yield return ("COD金额", "t_order", "CodSum", FormulaValueType.Number, false);
yield return ("申报币种", "t_order", "DeclaredCurrency", FormulaValueType.String, false);
yield return ("申报总金额", "t_order", "DeclaredSum", FormulaValueType.Number, false);
yield return ("订单件数", "t_order", "Quantity", FormulaValueType.Number, false);
yield return ("是否拦截", "t_order", "IsIntercept", FormulaValueType.Boolean, false);
yield return ("是否偏远", "t_order", "IsRemote", FormulaValueType.Boolean, false);
// t_order_recipient
yield return ("收件人姓名", "t_order_recipient", "RecipientsName", FormulaValueType.String, false);
yield return ("收件人公司", "t_order_recipient", "CompanyName", FormulaValueType.String, false);
yield return ("收件人国家", "t_order_recipient", "Country", FormulaValueType.Number, false);
yield return ("收件人手机", "t_order_recipient", "Phone", FormulaValueType.String, false);
yield return ("收件人电话", "t_order_recipient", "TelePhone", FormulaValueType.String, false);
yield return ("收件人邮编", "t_order_recipient", "ZipCode", FormulaValueType.String, false);
yield return ("收件人省州", "t_order_recipient", "Province", FormulaValueType.String, false);
yield return ("收件人城市", "t_order_recipient", "City", FormulaValueType.String, false);
yield return ("收件人门牌号", "t_order_recipient", "StreetNumber", FormulaValueType.String, false);
yield return ("收件人地址1", "t_order_recipient", "StreetAddress1", FormulaValueType.String, false);
yield return ("收件人地址2", "t_order_recipient", "StreetAddress2", FormulaValueType.String, false);
yield return ("收件人地址3", "t_order_recipient", "StreetAddress3", FormulaValueType.String, false);
yield return ("收件人邮箱", "t_order_recipient", "Email", FormulaValueType.String, false);
yield return ("收件人税号类型", "t_order_recipient", "TaxType", FormulaValueType.Number, false);
yield return ("收件人税号", "t_order_recipient", "TaxNo", FormulaValueType.String, false);
yield return ("收件人证件类型", "t_order_recipient", "CertificateType", FormulaValueType.Number, false);
yield return ("收件人证件号", "t_order_recipient", "CertificateNo", FormulaValueType.String, false);
yield return ("收件人EORI", "t_order_recipient", "Eori", FormulaValueType.String, false);
yield return ("收件人VAT", "t_order_recipient", "Vat", FormulaValueType.String, false);
// t_order_sender
yield return ("发件人姓名", "t_order_sender", "SenderName", FormulaValueType.String, false);
yield return ("发件人公司", "t_order_sender", "Company", FormulaValueType.String, false);
yield return ("发件人国家", "t_order_sender", "Country", FormulaValueType.Number, false);
yield return ("发件人省州", "t_order_sender", "Province", FormulaValueType.String, false);
yield return ("发件人城市", "t_order_sender", "City", FormulaValueType.String, false);
yield return ("发件人邮编", "t_order_sender", "ZipCode", FormulaValueType.String, false);
yield return ("发件人地址1", "t_order_sender", "StreetAddress1", FormulaValueType.String, false);
yield return ("发件人地址2", "t_order_sender", "StreetAddress2", FormulaValueType.String, false);
yield return ("发件人地址3", "t_order_sender", "StreetAddress3", FormulaValueType.String, false);
yield return ("发件人门牌号", "t_order_sender", "HouseNumber", FormulaValueType.String, false);
yield return ("发件人手机", "t_order_sender", "Phone", FormulaValueType.String, false);
yield return ("发件人电话", "t_order_sender", "TelePhone", FormulaValueType.String, false);
yield return ("发件人邮箱", "t_order_sender", "Email", FormulaValueType.String, false);
yield return ("发件人是否默认", "t_order_sender", "IsDefault", FormulaValueType.Boolean, false);
// t_order_declaration
yield return ("中文品名", "t_order_declaration", "CnName", FormulaValueType.String, false);
yield return ("英文品名", "t_order_declaration", "EnName", FormulaValueType.String, false);
yield return ("SKU", "t_order_declaration", "Sku", FormulaValueType.String, false);
yield return ("申报单位", "t_order_declaration", "Unit", FormulaValueType.String, false);
yield return ("申报单价", "t_order_declaration", "UnitPrice", FormulaValueType.Number, false);
yield return ("申报单重", "t_order_declaration", "UnitWeight", FormulaValueType.Number, false);
yield return ("申报数量", "t_order_declaration", "Quantity", FormulaValueType.Number, false);
yield return ("申报金额", "t_order_declaration", "Amount", FormulaValueType.Number, false);
yield return ("申报总重", "t_order_declaration", "Weight", FormulaValueType.Number, false);
yield return ("HS编码", "t_order_declaration", "HsCode", FormulaValueType.String, false);
yield return ("进口HS编码", "t_order_declaration", "ImHsCode", FormulaValueType.String, false);
yield return ("出口HS编码", "t_order_declaration", "ExHsCode", FormulaValueType.String, false);
yield return ("申报类别", "t_order_declaration", "Category", FormulaValueType.String, false);
yield return ("品牌", "t_order_declaration", "Brand", FormulaValueType.String, false);
yield return ("品牌类型", "t_order_declaration", "BrandType", FormulaValueType.String, false);
yield return ("是否带磁带电", "t_order_declaration", "IsMagnetic", FormulaValueType.Boolean, false);
yield return ("配货信息", "t_order_declaration", "Distribution", FormulaValueType.String, false);
yield return ("材质", "t_order_declaration", "Material", FormulaValueType.String, false);
yield return ("英文材质", "t_order_declaration", "EnMaterial", FormulaValueType.String, false);
yield return ("用途", "t_order_declaration", "Purpose", FormulaValueType.String, false);
yield return ("申报属性", "t_order_declaration", "Attributes", FormulaValueType.String, false);
yield return ("原产国", "t_order_declaration", "OriginCountry", FormulaValueType.String, false);
yield return ("销售链接", "t_order_declaration", "SalesLink", FormulaValueType.String, false);
yield return ("型号", "t_order_declaration", "Model", FormulaValueType.String, false);
yield return ("规格", "t_order_declaration", "Specifications", FormulaValueType.String, false);
yield return ("申报备注", "t_order_declaration", "Comment", FormulaValueType.String, false);
// t_order_weight
yield return ("件重尺类型", "t_order_weight", "Type", FormulaValueType.String, false);
yield return ("长度", "t_order_weight", "Length", FormulaValueType.Number, false);
yield return ("宽度", "t_order_weight", "Width", FormulaValueType.Number, false);
yield return ("高度", "t_order_weight", "Height", FormulaValueType.Number, false);
yield return ("实重", "t_order_weight", "ActualWeight", FormulaValueType.Number, false);
yield return ("体积重", "t_order_weight", "VolumetricWeight", FormulaValueType.Number, false);
yield return ("计费重", "t_order_weight", "ChargeableWeight", FormulaValueType.Number, false);
yield return ("体积", "t_order_weight", "Cubic", FormulaValueType.Number, false);
// t_order_fee
yield return ("费用类型", "t_order_fee", "Type", FormulaValueType.String, false);
yield return ("费用项", "t_order_fee", "CostItem", FormulaValueType.String, false);
yield return ("费用金额", "t_order_fee", "Amount", FormulaValueType.Number, false);
yield return ("费用币种", "t_order_fee", "Currency", FormulaValueType.String, false);
yield return ("费用下游服务商", "t_order_fee", "DownstreamProvider", FormulaValueType.String, false);
yield return ("价格编号", "t_order_fee", "PriceNo", FormulaValueType.String, false);
yield return ("价格生效时间", "t_order_fee", "PriceEffectiveTime", FormulaValueType.DateTime, false);
yield return ("计费阶段", "t_order_fee", "ChargingStage", FormulaValueType.String, false);
// t_customer
yield return ("客户编码", "t_customer", "CustomerCode", FormulaValueType.String, false);
yield return ("客户", "t_customer", "CustomerName", FormulaValueType.String, false);
yield return ("客户邮箱", "t_customer", "Email", FormulaValueType.String, false);
yield return ("客户手机", "t_customer", "Phone", FormulaValueType.String, false);
yield return ("客户余额", "t_customer", "Balance", FormulaValueType.Number, false);
yield return ("客户总额度", "t_customer", "TotalQuota", FormulaValueType.Number, false);
yield return ("客户已用额度", "t_customer", "UsedQuota", FormulaValueType.Number, false);
yield return ("客户状态", "t_customer", "Status", FormulaValueType.String, false);
yield return ("客户备注", "t_customer", "Remark", FormulaValueType.String, false);
yield return ("销售", "t_customer", "Salesman", FormulaValueType.String, false);
yield return ("客服", "t_customer", "CustomerService", FormulaValueType.String, false);
yield return ("客户组", "t_customer", "CustomerGroup", FormulaValueType.String, false);
// virtual
yield return ("单件最大长度", "_virtual", "PieceMaxLength", FormulaValueType.Number, true);
yield return ("单件最大宽度", "_virtual", "PieceMaxWidth", FormulaValueType.Number, true);
yield return ("单件最大高度", "_virtual", "PieceMaxHeight", FormulaValueType.Number, true);
yield return ("单件长宽高之和", "_virtual", "PieceSumLwh", FormulaValueType.Number, true);
}
}

View File

@ -0,0 +1,26 @@
using Oms.Domain.ChannelsAggregate.Entities;
namespace Oms.Application.Services.Orders.Issues;
public static class AutoTriggerRuleResolver
{
public static int GetPriority(AutoTrigger trigger)
{
if (trigger.Priority > 0)
{
return trigger.Priority;
}
return AutoTriggerConditionParser.Parse(trigger.TriggerCondition).Priority;
}
public static string? GetRuleName(AutoTrigger trigger)
{
if (!string.IsNullOrWhiteSpace(trigger.RuleName))
{
return trigger.RuleName.Trim();
}
return AutoTriggerConditionParser.Parse(trigger.TriggerCondition).RuleName;
}
}

View File

@ -12,10 +12,13 @@ namespace Oms.Application.Services.Orders.Issues;
public class AutoTriggerRuntimeService( public class AutoTriggerRuntimeService(
IAutoTriggerRepository autoTriggerRepository, IAutoTriggerRepository autoTriggerRepository,
IOrderRepository orderRepository, IOrderRepository orderRepository,
OrderAutoTriggerService orderAutoTriggerService,
OrderExceptionWorkflowService exceptionWorkflowService, OrderExceptionWorkflowService exceptionWorkflowService,
OrderIssueNotificationService issueNotificationService, OrderIssueNotificationService issueNotificationService,
ILogger<AutoTriggerRuntimeService> logger) : IScopedDependency ILogger<AutoTriggerRuntimeService> logger) : IScopedDependency
{ {
private const string InterceptOperation = "拦截";
public async Task FireAsync(Order order, AutoTriggerTiming timing, int operatorUserId) public async Task FireAsync(Order order, AutoTriggerTiming timing, int operatorUserId)
{ {
if (order.Id <= 0) if (order.Id <= 0)
@ -23,15 +26,25 @@ public class AutoTriggerRuntimeService(
return; return;
} }
var context = await orderAutoTriggerService.BuildPostCommitContextAsync(order);
var triggers = await autoTriggerRepository.GetListAsync(x => x.Status == EnableStatus.Enabled); var triggers = await autoTriggerRepository.GetListAsync(x => x.Status == EnableStatus.Enabled);
foreach (var trigger in triggers) var orderedTriggers = triggers
.Where(x => AutoTriggerTimingCodes.Matches(x.TriggerTiming, timing))
.Select(x => (Trigger: x, Priority: AutoTriggerRuleResolver.GetPriority(x)))
.OrderBy(x => x.Priority)
.ThenBy(x => x.Trigger.Id)
.Select(x => x.Trigger)
.ToList();
foreach (var trigger in orderedTriggers)
{ {
if (!AutoTriggerTimingCodes.Matches(trigger.TriggerTiming, timing)) if (timing == AutoTriggerTiming.OrderCreate
&& string.Equals(trigger.Operation?.Trim(), InterceptOperation, StringComparison.Ordinal))
{ {
continue; continue;
} }
if (!AutoTriggerConditionMatcher.Matches(order, trigger.TriggerCondition)) if (!AutoTriggerConditionMatcher.Matches(context, trigger.TriggerCondition))
{ {
continue; continue;
} }
@ -64,7 +77,7 @@ public class AutoTriggerRuntimeService(
int operatorUserId) int operatorUserId)
{ {
var operation = trigger.Operation?.Trim(); var operation = trigger.Operation?.Trim();
if (string.Equals(operation, "拦截", StringComparison.Ordinal)) if (string.Equals(operation, InterceptOperation, StringComparison.Ordinal))
{ {
if (order.IsIntercept == 0) if (order.IsIntercept == 0)
{ {

View File

@ -23,7 +23,7 @@ public static class AutoTriggerTimingCodes
var text = triggerTiming.Trim(); var text = triggerTiming.Trim();
return timing switch return timing switch
{ {
AutoTriggerTiming.OrderCreate => ContainsAny(text, "下单", "order", "create", "parcel", "预报"), AutoTriggerTiming.OrderCreate => ContainsAny(text, "下单", "order", "create", "parcel", "预报", "forecast"),
AutoTriggerTiming.Inbound => ContainsAny(text, "入库", "inbound", "receive", "收货"), AutoTriggerTiming.Inbound => ContainsAny(text, "入库", "inbound", "receive", "收货"),
AutoTriggerTiming.Outbound => ContainsAny(text, "出库", "outbound", "ship", "发货"), AutoTriggerTiming.Outbound => ContainsAny(text, "出库", "outbound", "ship", "发货"),
_ => false _ => false

View File

@ -0,0 +1,488 @@
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();
}
}

View File

@ -0,0 +1,187 @@
using Fake.Application;
using Fake.DependencyInjection;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.Shared.Enums;
namespace Oms.Application.Services.Orders.Issues;
public class OrderAutoTriggerService(
IAutoTriggerRepository autoTriggerRepository,
IOrderRepository orderRepository,
ICustomerRepository customerRepository) : IScopedDependency
{
private const string InterceptOperation = "拦截";
public async Task<AutoTriggerTestOutput> TestFormulaAsync(
AutoTriggerTestInput input,
CancellationToken cancellationToken = default)
{
var expression = ResolveTestExpression(input);
if (string.IsNullOrWhiteSpace(expression))
{
throw new BusinessException("请提供公式表达式");
}
OrderRuleEvaluationContext context;
string contextType;
if (input.OrderId.HasValue && input.OrderId.Value > 0)
{
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == input.OrderId.Value, cancellationToken);
if (order == null)
{
throw new BusinessException("订单不存在");
}
context = await BuildPostCommitContextAsync(order, cancellationToken);
contextType = "order";
}
else
{
context = await BuildSampleContextAsync(input.Sample, cancellationToken);
contextType = "sample";
}
if (!ChineseFormulaEvaluator.TryEvaluate(expression, context, out var matched))
{
return new AutoTriggerTestOutput
{
Matched = false,
Expression = expression,
ContextType = contextType,
Error = "公式解析失败,请检查语法与中文字段名"
};
}
return new AutoTriggerTestOutput
{
Matched = matched,
Expression = expression,
ContextType = contextType
};
}
public async Task EvaluatePreCommitAsync(
OrderParcelCreateInput input,
Customer customer,
string consigneeCountryCode,
string? consigneeCountryName,
CancellationToken cancellationToken = default)
{
var triggers = await LoadMatchedTriggersAsync(
AutoTriggerTiming.OrderCreate,
InterceptOperation,
cancellationToken);
if (triggers.Count == 0)
{
return;
}
var context = OrderRuleEvaluationContext.FromPreCommit(input, customer, consigneeCountryCode, consigneeCountryName);
foreach (var trigger in triggers)
{
if (!AutoTriggerConditionMatcher.Matches(context, trigger.TriggerCondition))
{
continue;
}
var message = string.IsNullOrWhiteSpace(trigger.PromptMessage)
? "订单命中自动拦截规则"
: trigger.PromptMessage.Trim();
throw new BusinessException(message);
}
}
public async Task<OrderRuleEvaluationContext> BuildPostCommitContextAsync(
Order order,
CancellationToken cancellationToken = default)
{
var orderIds = new[] { order.Id };
var customer = order.CustomerId.HasValue
? await customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken)
: null;
var recipients = await orderRepository.GetRecipientsByOrderIdsAsync(orderIds, cancellationToken);
var senders = await orderRepository.GetSendersByOrderIdsAsync(orderIds, cancellationToken);
var declarations = await orderRepository.GetDeclarationsByOrderIdsAsync(orderIds, cancellationToken);
var weights = await orderRepository.GetWeightsByOrderIdsAsync(orderIds, cancellationToken);
var fees = await orderRepository.GetFeesByOrderIdsAsync(orderIds, cancellationToken);
return OrderRuleEvaluationContext.FromOrder(
order,
customer,
recipients.FirstOrDefault(x => x.OrderId == order.Id),
senders.FirstOrDefault(x => x.OrderId == order.Id),
declarations.Where(x => x.OrderId == order.Id).ToList(),
weights.Where(x => x.OrderId == order.Id).ToList(),
fees.Where(x => x.OrderId == order.Id).ToList());
}
private async Task<List<AutoTrigger>> LoadMatchedTriggersAsync(
AutoTriggerTiming timing,
string? operation,
CancellationToken cancellationToken)
{
var triggers = await autoTriggerRepository.GetEnabledListAsync(cancellationToken);
return triggers
.Where(x => AutoTriggerTimingCodes.Matches(x.TriggerTiming, timing))
.Where(x => string.IsNullOrWhiteSpace(operation)
|| string.Equals(x.Operation?.Trim(), operation, StringComparison.Ordinal))
.Select(x => (Trigger: x, Priority: AutoTriggerRuleResolver.GetPriority(x)))
.OrderBy(x => x.Priority)
.ThenBy(x => x.Trigger.Id)
.Select(x => x.Trigger)
.ToList();
}
private static string ResolveTestExpression(AutoTriggerTestInput input)
{
if (!string.IsNullOrWhiteSpace(input.Expression))
{
return input.Expression.Trim();
}
var parsed = AutoTriggerConditionParser.Parse(input.TriggerCondition);
return parsed.Expression?.Trim() ?? string.Empty;
}
private async Task<OrderRuleEvaluationContext> BuildSampleContextAsync(
AutoTriggerTestSampleInput? sample,
CancellationToken cancellationToken)
{
sample ??= new AutoTriggerTestSampleInput();
Customer? customer = null;
if (sample.CustomerId.HasValue && sample.CustomerId.Value > 0)
{
customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == sample.CustomerId.Value, cancellationToken);
}
else if (!string.IsNullOrWhiteSpace(sample.CustomerName))
{
customer = new Customer { CustomerName = sample.CustomerName.Trim() };
}
var input = new OrderParcelCreateInput
{
ClientNo = sample.ClientNo ?? "TEST-CLIENT-NO",
ProductCode = sample.ProductCode,
ChannelCode = sample.ChannelCode,
CustomerId = sample.CustomerId ?? customer?.Id,
Length = sample.Length,
Width = sample.Width,
Height = sample.Height,
ActualWeight = sample.ActualWeight,
Receiver = new OrderParcelReceiverInput()
};
return OrderRuleEvaluationContext.FromPreCommit(
input,
customer ?? new Customer { CustomerName = sample.CustomerName ?? string.Empty },
sample.ConsigneeCountry ?? string.Empty,
sample.ConsigneeCountryName);
}
}

View File

@ -0,0 +1,302 @@
using System.Globalization;
using System.Reflection;
using Oms.Application.Dtos.Orders;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Services.Orders.Issues;
public static class OrderFormulaFieldResolver
{
public static decimal GetNumericValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
{
var value = GetRawValue(fieldRef, context);
return ToDecimal(value);
}
public static bool StringContains(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context, string needle)
{
if (string.IsNullOrEmpty(needle))
{
return true;
}
if (fieldRef.Table == "t_order_declaration")
{
return context.Declarations.Any(d => ContainsValue(GetDeclarationValue(d, fieldRef.Field), needle))
|| context.CreateInput?.Items.Any(i => ContainsValue(GetDeclarationInputValue(i, fieldRef.Field), needle)) == true;
}
if (fieldRef.Table == "t_order_fee")
{
return context.Fees.Any(f => ContainsValue(GetPropertyValue(f, fieldRef.Field), needle));
}
var raw = GetRawValue(fieldRef, context);
return ContainsValue(raw, needle);
}
private static object? GetRawValue(FormulaFieldRef fieldRef, OrderRuleEvaluationContext context)
{
if (fieldRef.Virtual)
{
return ResolveVirtual(fieldRef.Field, context);
}
return fieldRef.Table switch
{
"t_order" => ResolveOrderField(fieldRef.Field, context),
"t_order_recipient" => ResolveRecipientField(fieldRef.Field, context),
"t_order_sender" => ResolveSenderField(fieldRef.Field, context),
"t_order_declaration" => ResolveDeclarationField(fieldRef.Field, context),
"t_order_weight" => ResolveWeightField(fieldRef.Field, context),
"t_order_fee" => ResolveFeeField(fieldRef.Field, context),
"t_customer" => GetPropertyValue(context.Customer, fieldRef.Field),
_ => null
};
}
private static object? ResolveVirtual(string field, OrderRuleEvaluationContext context)
{
var (length, width, height) = GetPieceDimensions(context);
return field switch
{
"PieceMaxLength" => length,
"PieceMaxWidth" => width,
"PieceMaxHeight" => height,
"PieceSumLwh" => length + width + height,
_ => null
};
}
private static (decimal Length, decimal Width, decimal Height) GetPieceDimensions(OrderRuleEvaluationContext context)
{
var forecast = GetForecastWeight(context);
if (forecast != null)
{
return (forecast.Length ?? 0, forecast.Width ?? 0, forecast.Height ?? 0);
}
var input = context.CreateInput;
if (input != null)
{
return (input.Length ?? 0, input.Width ?? 0, input.Height ?? 0);
}
return (0, 0, 0);
}
private static OrderWeight? GetForecastWeight(OrderRuleEvaluationContext context) =>
context.Weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Forecast)
?? context.Weights.FirstOrDefault();
private static object? ResolveOrderField(string field, OrderRuleEvaluationContext context)
{
if (context.Order != null)
{
return GetPropertyValue(context.Order, field);
}
var input = context.CreateInput;
if (input == null)
{
return null;
}
return field switch
{
"RequestId" => input.RequestId,
"ClientNo" => input.ClientNo,
"TrackNo" => input.TrackNo,
"ServiceNo" => input.ServiceNo,
"ProductCode" => input.ProductCode,
"ChannelCode" => input.ChannelCode,
"ConsigneeCountry" => context.ConsigneeCountryCode,
"ConsigneeCountryName" => context.ConsigneeCountryName,
"CargoAttribute" => input.CargoAttribute,
"ExportCustomsMode" => input.ExportCustomsMode,
"WarehouseId" => input.WarehouseId,
"CustomerId" => input.CustomerId,
"IsCod" => input.IsCod ? (byte)1 : (byte)0,
"CodCurrency" => input.CodCurrency,
"CodSum" => input.CodSum,
"DeclaredCurrency" => input.DeclaredCurrency,
"DeclaredSum" => input.DeclaredSum,
"Quantity" => input.Quantity,
_ => null
};
}
private static object? ResolveRecipientField(string field, OrderRuleEvaluationContext context)
{
if (context.Recipient != null)
{
return GetPropertyValue(context.Recipient, field);
}
var receiver = context.CreateInput?.Receiver;
if (receiver == null)
{
return null;
}
return field switch
{
"RecipientsName" => receiver.RecipientsName,
"CompanyName" => receiver.CompanyName,
"Phone" => receiver.Phone,
"TelePhone" => receiver.TelePhone,
"Email" => receiver.Email,
"Province" => receiver.Province,
"City" => receiver.City,
"StreetNumber" => receiver.StreetNumber,
"StreetAddress1" => receiver.StreetAddress1,
"StreetAddress2" => receiver.StreetAddress2,
"StreetAddress3" => receiver.StreetAddress3,
"ZipCode" => receiver.ZipCode,
"TaxType" => receiver.TaxType,
"TaxNo" => receiver.TaxNo,
"CertificateType" => receiver.CertificateType,
"CertificateNo" => receiver.CertificateNo,
"Eori" => receiver.Eori,
"Vat" => receiver.Vat,
_ => null
};
}
private static object? ResolveSenderField(string field, OrderRuleEvaluationContext context)
{
if (context.Sender != null)
{
return GetPropertyValue(context.Sender, field);
}
var sender = context.CreateInput?.Sender;
if (sender == null)
{
return null;
}
return field switch
{
"SenderName" => sender.SenderName,
"Company" => sender.Company,
"Phone" => sender.Phone,
"TelePhone" => sender.TelePhone,
"Email" => sender.Email,
"Province" => sender.Province,
"City" => sender.City,
"ZipCode" => sender.ZipCode,
"StreetAddress1" => sender.StreetAddress1,
"StreetAddress2" => sender.StreetAddress2,
"StreetAddress3" => sender.StreetAddress3,
"HouseNumber" => sender.HouseNumber,
_ => null
};
}
private static object? ResolveDeclarationField(string field, OrderRuleEvaluationContext context)
{
var declaration = context.Declarations.FirstOrDefault();
if (declaration != null)
{
return GetDeclarationValue(declaration, field);
}
var item = context.CreateInput?.Items.FirstOrDefault();
return item == null ? null : GetDeclarationInputValue(item, field);
}
private static object? ResolveWeightField(string field, OrderRuleEvaluationContext context)
{
var weight = GetForecastWeight(context);
if (weight != null)
{
return GetPropertyValue(weight, field);
}
var input = context.CreateInput;
if (input == null)
{
return null;
}
return field switch
{
"Length" => input.Length,
"Width" => input.Width,
"Height" => input.Height,
"ActualWeight" => input.ActualWeight,
"VolumetricWeight" => input.VolumetricWeight,
"ChargeableWeight" => input.ChargeableWeight,
"Cubic" => input.Cubic,
_ => null
};
}
private static object? ResolveFeeField(string field, OrderRuleEvaluationContext context)
{
var fee = context.Fees.FirstOrDefault();
return fee == null ? null : GetPropertyValue(fee, field);
}
private static object? GetDeclarationValue(OrderDeclaration declaration, string field) =>
field switch
{
"HsCode" => declaration.HsCode ?? declaration.ImHsCode ?? declaration.ExHsCode,
_ => GetPropertyValue(declaration, field)
};
private static object? GetDeclarationInputValue(OrderParcelItemInput item, string field) =>
field switch
{
"HsCode" => item.ImHsCode ?? item.ExHsCode,
"IsMagnetic" => item.IsMagnetic ? (byte)1 : (byte)0,
_ => GetPropertyValue(item, field)
};
private static object? GetPropertyValue(object? target, string fieldName)
{
if (target == null)
{
return null;
}
var prop = target.GetType().GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
return prop?.GetValue(target);
}
private static decimal ToDecimal(object? value)
{
if (value == null)
{
return 0;
}
return value switch
{
bool b => b ? 1m : 0m,
byte bt => bt,
sbyte sb => sb,
short s => s,
ushort us => us,
int i => i,
uint ui => ui,
long l => l,
ulong ul => ul,
float f => (decimal)f,
double d => (decimal)d,
decimal m => m,
DateTime => 0,
Enum e => Convert.ToDecimal(e, CultureInfo.InvariantCulture),
_ => decimal.TryParse(Convert.ToString(value, CultureInfo.InvariantCulture), NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed)
? parsed
: 0
};
}
private static bool ContainsValue(object? source, string needle) =>
!string.IsNullOrWhiteSpace(Convert.ToString(source, CultureInfo.InvariantCulture))
&& Convert.ToString(source, CultureInfo.InvariantCulture)!
.Contains(needle, StringComparison.OrdinalIgnoreCase);
}

View File

@ -0,0 +1,70 @@
using Oms.Application.Dtos.Orders;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Services.Orders.Issues;
public sealed class OrderRuleEvaluationContext
{
public Order? Order { get; init; }
public OrderParcelCreateInput? CreateInput { get; init; }
public Customer? Customer { get; init; }
public OrderRecipient? Recipient { get; init; }
public OrderSender? Sender { get; init; }
public IReadOnlyList<OrderDeclaration> Declarations { get; init; } = [];
public IReadOnlyList<OrderWeight> Weights { get; init; } = [];
public IReadOnlyList<OrderFee> Fees { get; init; } = [];
public string? ConsigneeCountryCode { get; init; }
public string? ConsigneeCountryName { get; init; }
public static OrderRuleEvaluationContext FromPreCommit(
OrderParcelCreateInput input,
Customer customer,
string consigneeCountryCode,
string? consigneeCountryName)
{
return new OrderRuleEvaluationContext
{
CreateInput = input,
Customer = customer,
ConsigneeCountryCode = consigneeCountryCode,
ConsigneeCountryName = consigneeCountryName
};
}
public static OrderRuleEvaluationContext FromOrder(
Order order,
Customer? customer,
OrderRecipient? recipient,
OrderSender? sender,
IReadOnlyList<OrderDeclaration>? declarations,
IReadOnlyList<OrderWeight>? weights,
IReadOnlyList<OrderFee>? fees)
{
return new OrderRuleEvaluationContext
{
Order = order,
Customer = customer,
Recipient = recipient,
Sender = sender,
Declarations = declarations ?? [],
Weights = weights ?? [],
Fees = fees ?? [],
ConsigneeCountryCode = order.ConsigneeCountry,
ConsigneeCountryName = order.ConsigneeCountryName
};
}
public static OrderRuleEvaluationContext FromOrderOnly(Order order) =>
FromOrder(order, null, null, null, null, null, null);
}

View File

@ -37,6 +37,9 @@ public partial class OrderService
private AutoTriggerRuntimeService AutoTriggerRuntimeService => private AutoTriggerRuntimeService AutoTriggerRuntimeService =>
LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>(); LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>();
private OrderAutoTriggerService OrderAutoTriggerService =>
LazyServiceProvider.GetRequiredService<OrderAutoTriggerService>();
private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync( private async Task<OrderParcelCreateOutput> CreateParcelCoreAsync(
OrderParcelCreateInput input, OrderParcelCreateInput input,
int? operatorUserId = null) int? operatorUserId = null)
@ -78,6 +81,12 @@ public partial class OrderService
throw new BusinessException("收件地址命中黑名单区域"); throw new BusinessException("收件地址命中黑名单区域");
} }
await OrderAutoTriggerService.EvaluatePreCommitAsync(
input,
sessionCustomer,
country.CountryCode,
country.ChineseName);
var serviceNo = await ResolveServiceNoAsync(); var serviceNo = await ResolveServiceNoAsync();
var order = BuildDraftOrderEntity(input, product, country.CountryCode, country.ChineseName, snapshotBinding, serviceNo); var order = BuildDraftOrderEntity(input, product, country.CountryCode, country.ChineseName, snapshotBinding, serviceNo);
order = await OrderRepository.InsertAsync(order); order = await OrderRepository.InsertAsync(order);

View File

@ -9,8 +9,94 @@ namespace Oms.Application.Services.Orders;
public partial class OrderService public partial class OrderService
{ {
private const string SalesProductGroupDictType = "sales_product_group";
private const string UngroupedProductGroupCode = "_ungrouped";
private sealed record PriceCheckCargoTarget(string Code, string DisplayName, string CargoAttribute); private sealed record PriceCheckCargoTarget(string Code, string DisplayName, string CargoAttribute);
public async Task<OrderPriceCheckShippingOptionsOutput> BuildPriceCheckShippingOptionsAsync()
{
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
var sortedProducts = products
.OrderBy(x => x.ProductName)
.ThenBy(x => x.ProductCode)
.ToList();
var dictRows = await DictRepository.GetListAsync(x => x.DictType == SalesProductGroupDictType);
var dictOrder = dictRows
.OrderBy(x => x.DictSort)
.ThenBy(x => x.DictCode)
.ToList();
var grouped = sortedProducts
.GroupBy(
x => string.IsNullOrWhiteSpace(x.ProductGroup) ? UngroupedProductGroupCode : x.ProductGroup.Trim(),
StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.ToList(), StringComparer.OrdinalIgnoreCase);
var groups = new List<OrderPriceCheckShippingGroupOutput>();
var consumedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var dict in dictOrder)
{
var code = dict.DictCode?.Trim();
if (string.IsNullOrWhiteSpace(code) || !grouped.TryGetValue(code, out var list) || list.Count == 0)
{
continue;
}
consumedCodes.Add(code);
groups.Add(new OrderPriceCheckShippingGroupOutput
{
GroupCode = code,
GroupName = string.IsNullOrWhiteSpace(dict.DictName) ? code : dict.DictName.Trim(),
Products = list.Select(MapPriceCheckShippingOption).ToList()
});
}
foreach (var pair in grouped.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
{
if (pair.Key.Equals(UngroupedProductGroupCode, StringComparison.OrdinalIgnoreCase)
|| consumedCodes.Contains(pair.Key)
|| pair.Value.Count == 0)
{
continue;
}
groups.Add(new OrderPriceCheckShippingGroupOutput
{
GroupCode = pair.Key,
GroupName = pair.Key,
Products = pair.Value.Select(MapPriceCheckShippingOption).ToList()
});
}
if (grouped.TryGetValue(UngroupedProductGroupCode, out var ungrouped) && ungrouped.Count > 0)
{
groups.Add(new OrderPriceCheckShippingGroupOutput
{
GroupCode = UngroupedProductGroupCode,
GroupName = "未分组",
Products = ungrouped.Select(MapPriceCheckShippingOption).ToList()
});
}
return new OrderPriceCheckShippingOptionsOutput { Groups = groups };
}
private static OrderPriceCheckShippingOptionOutput MapPriceCheckShippingOption(SalesProduct product)
{
var code = product.ProductCode?.Trim() ?? string.Empty;
var name = product.ProductName?.Trim() ?? string.Empty;
return new OrderPriceCheckShippingOptionOutput
{
ProductId = product.Id,
ProductCode = code,
ProductName = name,
Label = string.IsNullOrWhiteSpace(name) ? code : $"{name} {code}"
};
}
private async Task<OrderPriceCheckResultOutput> BuildPriceCheckResultAsync(OrderPriceCheckInput input) private async Task<OrderPriceCheckResultOutput> BuildPriceCheckResultAsync(OrderPriceCheckInput input)
{ {
ValidatePriceCheckInput(input); ValidatePriceCheckInput(input);

View File

@ -254,6 +254,13 @@ public partial class OrderService : ApplicationService
}; };
} }
/// <summary>
/// 查价运输方式选项(按销售产品分组)。
/// </summary>
[HttpGet("/order/price-check/shipping-options")]
public Task<OrderPriceCheckShippingOptionsOutput> GetPriceCheckShippingOptionsAsync() =>
BuildPriceCheckShippingOptionsAsync();
/// <summary> /// <summary>
/// 执行订单运费试算。 /// 执行订单运费试算。
/// </summary> /// </summary>

View File

@ -9,6 +9,10 @@ public class AutoTrigger : OmsAuditedAggregateRoot
{ {
public EnableStatus Status { get; set; } public EnableStatus Status { get; set; }
public string? RuleName { get; set; }
public int Priority { get; set; } = 100;
public string? Operation { get; set; } public string? Operation { get; set; }
public string? TriggerTiming { get; set; } public string? TriggerTiming { get; set; }