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";
}