feat: 订单编辑支持服务商单号并完善操作日志快照
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s

开放 DownstreamServiceNo 更新,补充包裹计费相关操作码与字段快照,便于运单详情编辑留痕。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-10 16:27:49 +08:00
parent 64737284f3
commit 8df0f37165
5 changed files with 83 additions and 1 deletions

View File

@ -25,6 +25,12 @@ public class OrderUpdateInput
[StringLength(64)]
public string? TrackNo { get; set; }
/// <summary>
/// 服务商单号。
/// </summary>
[StringLength(64)]
public string? DownstreamServiceNo { get; set; }
/// <summary>
/// 渠道编码。
/// </summary>

View File

@ -37,21 +37,37 @@ public static class OrderOperationLogSnapshot
["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"] = "应付合计",
};
@ -94,13 +110,18 @@ public static class OrderOperationLogSnapshot
return new Dictionary<string, string?>(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),
};
}
@ -114,13 +135,24 @@ public static class OrderOperationLogSnapshot
return new Dictionary<string, string?>(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),
};
}

View File

@ -53,6 +53,13 @@ public partial class OrderService
order.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
}
if (input.DownstreamServiceNo != null)
{
order.DownstreamServiceNo = string.IsNullOrWhiteSpace(input.DownstreamServiceNo)
? null
: input.DownstreamServiceNo.Trim();
}
if (input.ChannelCode != null)
{
order.ChannelCode = string.IsNullOrWhiteSpace(input.ChannelCode) ? null : input.ChannelCode.Trim();
@ -138,7 +145,9 @@ public partial class OrderService
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeParties: true);
var remark = input.Items != null ? "申报信息已更新" : null;
// 前端保存基本信息时总会带 items;仅在无字段级 diff 时用备注兜底,避免掩盖真实字段变更
var fieldChanges = OrderOperationLogSnapshot.Diff(beforeSnap, afterSnap);
var remark = input.Items != null && fieldChanges.Count == 0 ? "申报信息已更新" : null;
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.EditBasic,

View File

@ -2382,6 +2382,7 @@ public partial class OrderService
try
{
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
.OrderBy(x => x.Id)
.ToList();
@ -2419,6 +2420,16 @@ public partial class OrderService
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
// 操作日志同步落库(件重尺已写完);计费/排货仍走 MQ,不把审计塞进队列
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.Inbound,
beforeSnap,
afterSnap,
FormatMeasuredDimensionsRemark("收货", length!.Value, width!.Value, height!.Value, actualWeight!.Value));
await EnqueueLifecycleTrackAppendAsync(order, SmallBagTrackLifecycleEvents.InboundTrackCode);
await EnqueueInboundBillingAsync(order);
}
@ -3016,6 +3027,7 @@ public partial class OrderService
EnsureBillingProductActive(product);
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
.OrderBy(x => x.Id)
.ToList();
@ -3038,9 +3050,28 @@ public partial class OrderService
order.UpdateTime = now;
await OrderRepository.UpdateAsync(order);
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order, includeWeights: true);
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.Outbound,
beforeSnap,
afterSnap,
FormatMeasuredDimensionsRemark("发货", length!.Value, width!.Value, height!.Value, actualWeight!.Value));
await EnqueueLifecycleTrackAppendAsync(order, SmallBagTrackLifecycleEvents.OutboundTrackCode);
}
private static string FormatMeasuredDimensionsRemark(
string stage,
decimal length,
decimal width,
decimal height,
decimal actualWeight)
{
return $"{stage} 长×宽×高×重:{length:0.####}×{width:0.####}×{height:0.####}×{actualWeight:0.####}";
}
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
{
// 预报已成功时,映射/面单/本地存储失败不得回滚订单(常见:容器 /app/wwwroot 不可写)。

View File

@ -14,6 +14,8 @@ public static class OrderOperationActionCodes
public const string PreDeclare = "preDeclare";
public const string DownstreamForward = "downstreamForward";
public const string Print = "print";
public const string Inbound = "inbound";
public const string Outbound = "outbound";
public static string GetActionName(string action) => action switch
{
@ -29,6 +31,8 @@ public static class OrderOperationActionCodes
PreDeclare => "重新预报",
DownstreamForward => "预报下游",
Print => "打印面单",
Inbound => "入库",
Outbound => "出库",
_ => action
};
}