using System.Globalization; using Newtonsoft.Json; using Oms.Application.Shared; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; namespace Oms.Application.Services.Orders; /// /// 运单操作日志白名单快照与字段 label。 /// public static class OrderOperationLogSnapshot { public const int SummaryMaxChangeItems = 5; private static readonly Dictionary Labels = new(StringComparer.OrdinalIgnoreCase) { ["orderStatus"] = "订单状态", ["clientNo"] = "客户单号", ["trackNo"] = "跟踪号", ["channelCode"] = "服务渠道", ["productCode"] = "销售产品", ["warehouseId"] = "仓库", ["cargoAttribute"] = "货物属性", ["isCod"] = "是否COD", ["codCurrency"] = "COD币种", ["codSum"] = "COD金额", ["declaredCurrency"] = "申报币种", ["declaredSum"] = "申报金额", ["quantity"] = "包裹数量", ["receivableSubtotal"] = "应收小计", ["payableAmount"] = "应付金额", ["downstreamServiceNo"] = "服务商单号", ["allocationScheme"] = "排货方案", ["salesProductRulesVersion"] = "规则版本", ["salesProductPriceVersion"] = "价格版本", ["isPaid"] = "是否已支付", ["additionalServices"] = "附加服务", ["sender.name"] = "发件人姓名", ["sender.company"] = "发件公司", ["sender.phone"] = "发件人电话", ["sender.telePhone"] = "发件人固话", ["sender.email"] = "发件人邮箱", ["sender.country"] = "发件国家", ["sender.province"] = "发件省/州", ["sender.city"] = "发件城市", ["sender.zipCode"] = "发件邮编", ["sender.address1"] = "发件地址1", ["sender.address2"] = "发件地址2", ["sender.address3"] = "发件地址3", ["sender.houseNumber"] = "发件门牌号", ["receiver.name"] = "收件人姓名", ["receiver.company"] = "收件公司", ["receiver.phone"] = "收件人电话", ["receiver.telePhone"] = "收件人固话", ["receiver.email"] = "收件人邮箱", ["receiver.country"] = "收件国家", ["receiver.province"] = "收件省/州", ["receiver.city"] = "收件城市", ["receiver.zipCode"] = "收件邮编", ["receiver.streetNumber"] = "收件街道号", ["receiver.address1"] = "收件地址1", ["receiver.address2"] = "收件地址2", ["receiver.address3"] = "收件地址3", ["receiver.taxType"] = "收件税号类型", ["receiver.taxNo"] = "收件税号", ["receiver.certificateType"] = "收件证件类型", ["receiver.certificateNo"] = "收件证件号", ["receiver.eori"] = "收件EORI", ["receiver.vat"] = "收件VAT", ["fee.receivableTotal"] = "应收合计", ["fee.payableTotal"] = "应付合计", }; public static Dictionary CaptureOrder(Order order) { return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["orderStatus"] = Norm(order.OrderStatus), ["clientNo"] = Norm(order.ClientNo), ["trackNo"] = Norm(order.TrackNo), ["channelCode"] = Norm(order.ChannelCode), ["productCode"] = Norm(order.ProductCode), ["warehouseId"] = FormatInt(order.WarehouseId), ["cargoAttribute"] = Norm(order.CargoAttribute), ["isCod"] = FormatByte(order.IsCod), ["codCurrency"] = Norm(order.CodCurrency), ["codSum"] = FormatDecimal(order.CodSum), ["declaredCurrency"] = Norm(order.DeclaredCurrency), ["declaredSum"] = FormatDecimal(order.DeclaredSum), ["quantity"] = FormatInt(order.Quantity), ["receivableSubtotal"] = FormatDecimal(order.ReceivableSubtotal), ["payableAmount"] = FormatDecimal(order.PayableAmount), ["downstreamServiceNo"] = Norm(order.DownstreamServiceNo), ["allocationScheme"] = Norm(order.AllocationScheme), ["salesProductRulesVersion"] = Norm(order.SalesProductRulesVersion), ["salesProductPriceVersion"] = Norm(order.SalesProductPriceVersion), ["isPaid"] = FormatByte(order.IsPaid), ["additionalServices"] = FormatAdditionalServices(order.AdditionalServices), }; } public static Dictionary CaptureSender(OrderSender? sender) { if (sender == null) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["sender.name"] = Norm(sender.SenderName), ["sender.company"] = Norm(sender.Company), ["sender.phone"] = Norm(sender.Phone), ["sender.telePhone"] = Norm(sender.TelePhone), ["sender.email"] = Norm(sender.Email), ["sender.country"] = FormatInt(sender.Country), ["sender.province"] = Norm(sender.Province), ["sender.city"] = Norm(sender.City), ["sender.zipCode"] = Norm(sender.ZipCode), ["sender.address1"] = Norm(sender.StreetAddress1), ["sender.address2"] = Norm(sender.StreetAddress2), ["sender.address3"] = Norm(sender.StreetAddress3), ["sender.houseNumber"] = Norm(sender.HouseNumber), }; } public static Dictionary CaptureReceiver(OrderRecipient? recipient) { if (recipient == null) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["receiver.name"] = Norm(recipient.RecipientsName), ["receiver.company"] = Norm(recipient.CompanyName), ["receiver.phone"] = Norm(recipient.Phone), ["receiver.telePhone"] = Norm(recipient.TelePhone), ["receiver.email"] = Norm(recipient.Email), ["receiver.country"] = FormatInt(recipient.Country), ["receiver.province"] = Norm(recipient.Province), ["receiver.city"] = Norm(recipient.City), ["receiver.zipCode"] = Norm(recipient.ZipCode), ["receiver.streetNumber"] = Norm(recipient.StreetNumber), ["receiver.address1"] = Norm(recipient.StreetAddress1), ["receiver.address2"] = Norm(recipient.StreetAddress2), ["receiver.address3"] = Norm(recipient.StreetAddress3), ["receiver.taxType"] = FormatInt(recipient.TaxType), ["receiver.taxNo"] = Norm(recipient.TaxNo), ["receiver.certificateType"] = FormatInt(recipient.CertificateType), ["receiver.certificateNo"] = Norm(recipient.CertificateNo), ["receiver.eori"] = Norm(recipient.Eori), ["receiver.vat"] = Norm(recipient.Vat), }; } public static Dictionary CaptureWeights(IEnumerable weights) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var row in weights) { var type = string.IsNullOrWhiteSpace(row.Type) ? "unknown" : row.Type.Trim().ToLowerInvariant(); var prefix = $"weight.{type}"; map[$"{prefix}.actualWeight"] = FormatDecimal(row.ActualWeight); map[$"{prefix}.length"] = FormatDecimal(row.Length); map[$"{prefix}.width"] = FormatDecimal(row.Width); map[$"{prefix}.height"] = FormatDecimal(row.Height); map[$"{prefix}.chargeableWeight"] = FormatDecimal(row.ChargeableWeight); } return map; } public static Dictionary CaptureFeeTotals(IEnumerable fees) { var list = fees.ToList(); var receivable = list .Where(x => string.Equals(x.Type, OrderFeeTypeCodes.Receivable, StringComparison.OrdinalIgnoreCase)) .Sum(x => x.Amount); var payable = list .Where(x => string.Equals(x.Type, OrderFeeTypeCodes.Payable, StringComparison.OrdinalIgnoreCase)) .Sum(x => x.Amount); return new Dictionary(StringComparer.OrdinalIgnoreCase) { ["fee.receivableTotal"] = FormatDecimal(receivable), ["fee.payableTotal"] = FormatDecimal(payable), }; } public static Dictionary Merge(params IReadOnlyDictionary[] parts) { var map = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var part in parts) { foreach (var (key, value) in part) { map[key] = value; } } return map; } public static List<(string Path, string Label, string? Before, string? After)> Diff( IReadOnlyDictionary before, IReadOnlyDictionary after) { var keys = before.Keys.Union(after.Keys, StringComparer.OrdinalIgnoreCase).OrderBy(x => x).ToList(); var changes = new List<(string, string, string?, string?)>(); foreach (var key in keys) { before.TryGetValue(key, out var b); after.TryGetValue(key, out var a); b = Norm(b); a = Norm(a); if (string.Equals(b, a, StringComparison.Ordinal)) { continue; } changes.Add((key, ResolveLabel(key), b, a)); } return changes; } public static string ResolveLabel(string path) { if (Labels.TryGetValue(path, out var label)) { return label; } if (path.StartsWith("weight.", StringComparison.OrdinalIgnoreCase)) { var parts = path.Split('.'); if (parts.Length >= 3) { var typeName = parts[1] switch { "forecast" => "预报", "receive" => "收货", "ship" => "发货", "downstream" => "对账", _ => parts[1] }; var fieldName = parts[2] switch { "actualWeight" => "实重", "length" => "长", "width" => "宽", "height" => "高", "chargeableWeight" => "计费重", _ => parts[2] }; return $"{typeName}{fieldName}"; } } return path; } /// /// 拼装列表「内容」列:备注优先,再跟最多 N 条字段变更。 /// public static string BuildSummary( IReadOnlyList changes, string? remark, int maxItems = SummaryMaxChangeItems) { var parts = new List(); if (!string.IsNullOrWhiteSpace(remark)) { parts.Add(remark.Trim()); } if (changes.Count > 0) { var take = Math.Max(0, maxItems); foreach (var change in changes.Take(take)) { var label = string.IsNullOrWhiteSpace(change.Label) ? change.Path : change.Label; parts.Add($"{label}:{DisplayValue(change.Path, change.Before)}→{DisplayValue(change.Path, change.After)}"); } if (changes.Count > take) { parts.Add($"等{changes.Count}项变更"); } } return parts.Count == 0 ? "—" : string.Join(";", parts); } public readonly record struct OrderOperationLogChangeView( string Path, string Label, string? Before, string? After); private static string DisplayValue(string path, string? value) { if (string.IsNullOrWhiteSpace(value)) { return "空"; } if (string.Equals(path, "isPaid", StringComparison.OrdinalIgnoreCase) || string.Equals(path, "isCod", StringComparison.OrdinalIgnoreCase)) { return value == "1" ? "是" : "否"; } if (string.Equals(path, "orderStatus", StringComparison.OrdinalIgnoreCase)) { return OmsDocHelper.GetOrderStatusName(value) ?? value; } return value; } private static string? FormatAdditionalServices(string? json) { if (string.IsNullOrWhiteSpace(json)) { return null; } try { var list = JsonConvert.DeserializeObject>(json) ?? []; var codes = list .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); return codes.Count == 0 ? null : string.Join("、", codes); } catch { return Norm(json); } } private static string? Norm(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } return value.Trim(); } private static string? FormatDecimal(decimal? value) { if (!value.HasValue) { return null; } return value.Value.ToString("0.####", CultureInfo.InvariantCulture); } private static string? FormatInt(int? value) => value.HasValue ? value.Value.ToString(CultureInfo.InvariantCulture) : null; private static string FormatByte(byte value) => value == 0 ? "0" : "1"; }