feat: 运单发货后状态变更接口
Some checks failed
Build and Deploy / deploy (push) Failing after 45s

新增 ChangePostShipStatus,限制发货后四态白名单,妥投成功仅可进入不可改出。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-26 11:46:33 +08:00
parent a69e69d5b6
commit 3c85b7325f
3 changed files with 125 additions and 0 deletions

View File

@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 出库后状态校正入参:仅允许在待上网 / 运输中 / 派送成功 / 派送失败之间变更;派送成功只进不出。
/// </summary>
public class OrderChangePostShipStatusInput
{
/// <summary>
/// 订单主键。
/// </summary>
[Required]
public int Id { get; set; }
/// <summary>
/// 目标订单状态(pendingOnline / shipping / deliverySuccess / deliveryFailed)。
/// </summary>
[Required]
[StringLength(32)]
public string TargetStatus { get; set; } = string.Empty;
/// <summary>
/// 变更备注(必填,落操作日志)。
/// </summary>
[Required]
[StringLength(512)]
public string Remark { get; set; } = string.Empty;
}

View File

@ -237,7 +237,101 @@ public partial class OrderService : ApplicationService
order.OrderStatus = OrderStatusCodes.DeliverySuccess; order.OrderStatus = OrderStatusCodes.DeliverySuccess;
order.SignTime = DateTime.Now; order.SignTime = DateTime.Now;
await UpdateOrderAsync(order); await UpdateOrderAsync(order);
await ApplyDeliverySuccessMarketingAsync(order);
}
/// <summary>
/// 出库后状态校正:仅待上网 / 运输中 / 派送成功 / 派送失败互改;派送成功只进不出。
/// 目标为派送成功时同步 SignTime 并触发引荐分成 / 首签券(与 mark-delivery-success 一致)。
/// </summary>
/// <param name="input">订单 Id、目标状态、必填备注。</param>
[HttpPost("/order/change-post-ship-status")]
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
public async Task ChangePostShipStatusAsync([FromBody] OrderChangePostShipStatusInput input)
{
var target = NormalizePostShipStatus(input.TargetStatus);
if (target == null)
{
throw new BusinessException("目标状态仅支持:待上网、运输中、派送成功、派送失败");
}
var remark = input.Remark?.Trim();
if (string.IsNullOrWhiteSpace(remark))
{
throw new BusinessException("请填写变更备注");
}
var order = await GetOrderOrThrowAsync(input.Id);
var fromStatus = order.OrderStatus?.Trim() ?? string.Empty;
if (!IsPostShipChangeableStatus(fromStatus))
{
throw new BusinessException(
$"订单状态为「{OmsDocHelper.GetOrderStatusName(fromStatus)}」,仅出库后状态可校正");
}
if (string.Equals(fromStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException("派送成功不可回退");
}
if (string.Equals(fromStatus, target, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException("目标状态与当前状态相同");
}
var beforeSnap = await CaptureOrderAuditSnapshotAsync(order);
order.OrderStatus = target;
if (string.Equals(target, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
{
order.SignTime = DateTime.Now;
}
order.UpdateTime = DateTime.Now;
await UpdateOrderAsync(order);
if (string.Equals(target, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
{
order = await GetOrderOrThrowAsync(order.Id);
await ApplyDeliverySuccessMarketingAsync(order);
}
order = await GetOrderOrThrowAsync(order.Id);
var afterSnap = await CaptureOrderAuditSnapshotAsync(order);
var fromName = OmsDocHelper.GetOrderStatusName(fromStatus) ?? fromStatus;
var toName = OmsDocHelper.GetOrderStatusName(target) ?? target;
await OperationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.ChangePostShipStatus,
beforeSnap,
afterSnap,
$"{fromName} → {toName};{remark}");
}
private static readonly HashSet<string> PostShipChangeableStatuses = new(StringComparer.OrdinalIgnoreCase)
{
OrderStatusCodes.PendingOnline,
OrderStatusCodes.Shipping,
OrderStatusCodes.DeliverySuccess,
OrderStatusCodes.DeliveryFailed
};
private static bool IsPostShipChangeableStatus(string? status) =>
!string.IsNullOrWhiteSpace(status) && PostShipChangeableStatuses.Contains(status.Trim());
private static string? NormalizePostShipStatus(string? raw)
{
var value = raw?.Trim();
if (string.IsNullOrWhiteSpace(value) || !PostShipChangeableStatuses.Contains(value))
{
return null;
}
return PostShipChangeableStatuses.First(x =>
string.Equals(x, value, StringComparison.OrdinalIgnoreCase));
}
private async Task ApplyDeliverySuccessMarketingAsync(Order order)
{
var commissionManager = LazyServiceProvider.GetRequiredService<ReferralCommissionManager>(); var commissionManager = LazyServiceProvider.GetRequiredService<ReferralCommissionManager>();
await commissionManager.OnOrderDeliverySuccessAsync(order.Id); await commissionManager.OnOrderDeliverySuccessAsync(order.Id);

View File

@ -17,6 +17,7 @@ public static class OrderOperationActionCodes
public const string Print = "print"; public const string Print = "print";
public const string Inbound = "inbound"; public const string Inbound = "inbound";
public const string Outbound = "outbound"; public const string Outbound = "outbound";
public const string ChangePostShipStatus = "changePostShipStatus";
public static string GetActionName(string action) => action switch public static string GetActionName(string action) => action switch
{ {
@ -35,6 +36,7 @@ public static class OrderOperationActionCodes
Print => "打印面单", Print => "打印面单",
Inbound => "入库", Inbound => "入库",
Outbound => "出库", Outbound => "出库",
ChangePostShipStatus => "修改出库后状态",
_ => action _ => action
}; };
} }