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