All checks were successful
Build and Deploy / deploy (push) Successful in 1m49s
用 MatchCondition 中文公式筛选可用渠道后再按方案选渠;失败时落完整排货过程(阶段、公式摘要、池汇总),并兼容旧 CustomerIds。 Co-authored-by: Cursor <cursoragent@cursor.com>
199 lines
6.9 KiB
C#
199 lines
6.9 KiB
C#
using Oms.Application.Dtos.Channels;
|
|
using Oms.Application.Dtos.Orders;
|
|
using Oms.Application.Services.Orders.Issues;
|
|
using Oms.Domain.ChannelsAggregate.Consts;
|
|
using Oms.Domain.ChannelsAggregate.Entities;
|
|
using Oms.Domain.OrdersAggregate.Entities;
|
|
|
|
namespace Oms.Application.Shared;
|
|
|
|
/// <summary>
|
|
/// 排货规则运行时选渠。
|
|
/// </summary>
|
|
public static class AllocationResolver
|
|
{
|
|
public const string SkipStageCondition = "condition";
|
|
public const string SkipStageCustomer = "customer";
|
|
public const string SkipStageChannel = "channel";
|
|
public const string SkipStageBuyPrice = "buyPrice";
|
|
|
|
private const int MatchConditionTraceMaxLen = 200;
|
|
|
|
public sealed class ChannelCandidate
|
|
{
|
|
public SalesProductAllocationProviderSnapshotDto Provider { get; init; } = null!;
|
|
|
|
public ServiceChannel Channel { get; init; } = null!;
|
|
|
|
public decimal TotalPayableCost { get; init; }
|
|
|
|
/// <summary>候选渠道已算出的应付费用(选渠后可直接落库,避免重复算价)。</summary>
|
|
public IReadOnlyList<OrderFee> PayableFees { get; init; } = [];
|
|
}
|
|
|
|
public sealed class FilterProvidersResult
|
|
{
|
|
public List<SalesProductAllocationProviderSnapshotDto> Passed { get; init; } = [];
|
|
|
|
public List<OrderAllocationSkippedOutput> Skipped { get; init; } = [];
|
|
|
|
public int PoolCount { get; init; }
|
|
|
|
public int ConditionPassCount { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 兼容旧调用:仅按客户名单过滤(无判断条件上下文时)。
|
|
/// </summary>
|
|
public static List<SalesProductAllocationProviderSnapshotDto> FilterProviders(
|
|
IEnumerable<SalesProductAllocationProviderSnapshotDto> providers,
|
|
int? customerId) =>
|
|
FilterProvidersDetailed(providers, customerId, context: null).Passed;
|
|
|
|
/// <summary>
|
|
/// 先按判断条件(或兼容客户名单)筛出可用渠道;空条件恒真。
|
|
/// </summary>
|
|
public static FilterProvidersResult FilterProvidersDetailed(
|
|
IEnumerable<SalesProductAllocationProviderSnapshotDto> providers,
|
|
int? customerId,
|
|
OrderRuleEvaluationContext? context)
|
|
{
|
|
var pool = providers
|
|
.Where(x => x.ServiceChannelId.HasValue && x.ServiceChannelId > 0)
|
|
.OrderBy(x => x.SerialNumber)
|
|
.ToList();
|
|
|
|
var passed = new List<SalesProductAllocationProviderSnapshotDto>();
|
|
var skipped = new List<OrderAllocationSkippedOutput>();
|
|
|
|
foreach (var provider in pool)
|
|
{
|
|
var match = EvaluateProviderMatch(provider, customerId, context);
|
|
if (match.Pass)
|
|
{
|
|
passed.Add(provider);
|
|
continue;
|
|
}
|
|
|
|
skipped.Add(new OrderAllocationSkippedOutput
|
|
{
|
|
ServiceChannelId = provider.ServiceChannelId,
|
|
ChannelCode = provider.ChannelCode,
|
|
ChannelName = provider.ServiceChannelName ?? provider.ProviderName,
|
|
Stage = match.Stage,
|
|
MatchCondition = TruncateMatchCondition(provider.MatchCondition),
|
|
Reason = match.Reason
|
|
});
|
|
}
|
|
|
|
// 同一渠道多行命中时保留权重更高、序号更小的一行
|
|
var deduped = DeduplicateByChannel(passed);
|
|
|
|
return new FilterProvidersResult
|
|
{
|
|
Passed = deduped,
|
|
Skipped = skipped,
|
|
PoolCount = pool.Count,
|
|
ConditionPassCount = passed.Count
|
|
};
|
|
}
|
|
|
|
private static List<SalesProductAllocationProviderSnapshotDto> DeduplicateByChannel(
|
|
List<SalesProductAllocationProviderSnapshotDto> passed)
|
|
{
|
|
return passed
|
|
.GroupBy(x => x.ServiceChannelId!.Value)
|
|
.Select(g => g
|
|
.OrderByDescending(x => x.Weight)
|
|
.ThenBy(x => x.SerialNumber)
|
|
.First())
|
|
.OrderBy(x => x.SerialNumber)
|
|
.ToList();
|
|
}
|
|
|
|
private readonly record struct MatchOutcome(bool Pass, string Stage, string Reason);
|
|
|
|
private static MatchOutcome EvaluateProviderMatch(
|
|
SalesProductAllocationProviderSnapshotDto provider,
|
|
int? customerId,
|
|
OrderRuleEvaluationContext? context)
|
|
{
|
|
var formula = provider.MatchCondition?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(formula)
|
|
&& !string.Equals(formula, "无", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (context == null)
|
|
{
|
|
return new MatchOutcome(false, SkipStageCondition, "判断条件缺少订单上下文");
|
|
}
|
|
|
|
if (!ChineseFormulaEvaluator.TryEvaluate(formula, context, out var hit))
|
|
{
|
|
return new MatchOutcome(false, SkipStageCondition, "判断条件公式无效");
|
|
}
|
|
|
|
if (!hit)
|
|
{
|
|
return new MatchOutcome(false, SkipStageCondition, "判断条件未命中");
|
|
}
|
|
|
|
return new MatchOutcome(true, SkipStageCondition, string.Empty);
|
|
}
|
|
|
|
// 无公式:兼容旧 CustomerIds 名单
|
|
if (provider.CustomerIds.Count > 0)
|
|
{
|
|
if (customerId.HasValue && provider.CustomerIds.Contains(customerId.Value))
|
|
{
|
|
return new MatchOutcome(true, SkipStageCustomer, string.Empty);
|
|
}
|
|
|
|
return new MatchOutcome(false, SkipStageCustomer, "客户不在名单内");
|
|
}
|
|
|
|
return new MatchOutcome(true, SkipStageCondition, string.Empty);
|
|
}
|
|
|
|
public static string TruncateMatchCondition(string? condition)
|
|
{
|
|
var text = condition?.Trim() ?? string.Empty;
|
|
if (text.Length <= MatchConditionTraceMaxLen)
|
|
{
|
|
return text;
|
|
}
|
|
|
|
return text[..MatchConditionTraceMaxLen] + "…";
|
|
}
|
|
|
|
public static ServiceChannel SelectChannel(string? scheme, IReadOnlyList<ChannelCandidate> candidates) =>
|
|
SelectCandidate(scheme, candidates).Channel;
|
|
|
|
public static ChannelCandidate SelectCandidate(string? scheme, IReadOnlyList<ChannelCandidate> candidates)
|
|
{
|
|
if (candidates.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("无可用排货候选渠道");
|
|
}
|
|
|
|
var normalizedScheme = string.IsNullOrWhiteSpace(scheme)
|
|
? AllocationSchemeCodes.CostLowest
|
|
: scheme.Trim();
|
|
|
|
return normalizedScheme switch
|
|
{
|
|
AllocationSchemeCodes.TimeFastest => candidates
|
|
.OrderBy(x => x.Channel.MinTime ?? int.MaxValue)
|
|
.ThenBy(x => x.Provider.SerialNumber)
|
|
.First(),
|
|
AllocationSchemeCodes.OverallBest => candidates
|
|
.OrderByDescending(x => x.Provider.Weight)
|
|
.ThenBy(x => x.Provider.SerialNumber)
|
|
.First(),
|
|
_ => candidates
|
|
.OrderBy(x => x.TotalPayableCost)
|
|
.ThenBy(x => x.Provider.SerialNumber)
|
|
.First()
|
|
};
|
|
}
|
|
}
|