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;
///
/// 排货规则运行时选渠。
///
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; }
/// 候选渠道已算出的应付费用(选渠后可直接落库,避免重复算价)。
public IReadOnlyList PayableFees { get; init; } = [];
}
public sealed class FilterProvidersResult
{
public List Passed { get; init; } = [];
public List Skipped { get; init; } = [];
public int PoolCount { get; init; }
public int ConditionPassCount { get; init; }
}
///
/// 兼容旧调用:仅按客户名单过滤(无判断条件上下文时)。
///
public static List FilterProviders(
IEnumerable providers,
int? customerId) =>
FilterProvidersDetailed(providers, customerId, context: null).Passed;
///
/// 先按判断条件(或兼容客户名单)筛出可用渠道;空条件恒真。
///
public static FilterProvidersResult FilterProvidersDetailed(
IEnumerable 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();
var skipped = new List();
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 DeduplicateByChannel(
List 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 candidates) =>
SelectCandidate(scheme, candidates).Channel;
public static ChannelCandidate SelectCandidate(string? scheme, IReadOnlyList 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()
};
}
}