admin/src/Oms.Application/Shared/AllocationResolver.cs
zzh e5c9364434
All checks were successful
Build and Deploy / deploy (push) Successful in 1m49s
feat: 排货规则支持判断条件公式并完善失败溯源
用 MatchCondition 中文公式筛选可用渠道后再按方案选渠;失败时落完整排货过程(阶段、公式摘要、池汇总),并兼容旧 CustomerIds。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 19:43:05 +08:00

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()
};
}
}