Compare commits
No commits in common. "8961fa9307afe0f7d8eba6f95d8f1bdad976643f" and "ae78834d7c0763f280b9b88eb6a2db3be3d566c9" have entirely different histories.
8961fa9307
...
ae78834d7c
@ -1,29 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
namespace Oms.Application.Dtos.Orders;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查价表单下拉选项(国家 / 发货仓 / 货物属性),可供访客匿名加载。
|
|
||||||
/// </summary>
|
|
||||||
public class OrderPriceCheckFormOptionsOutput
|
|
||||||
{
|
|
||||||
/// <summary>目的国家列表。</summary>
|
|
||||||
public List<OrderPriceCheckCountryOptionOutput> Countries { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>发货仓列表(仅 id + 名称,不含地址电话)。</summary>
|
|
||||||
public List<OrderPriceCheckWarehouseOptionOutput> Warehouses { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>货物属性(字典 cargo_attribute)。</summary>
|
|
||||||
public List<OrderPriceCheckCargoOptionOutput> CargoAttributes { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>查价目的国家选项。</summary>
|
|
||||||
public class OrderPriceCheckCountryOptionOutput
|
|
||||||
{
|
|
||||||
/// <summary>ISO 二字码。</summary>
|
|
||||||
public string CountryCode { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>中文名。</summary>
|
|
||||||
public string ChineseName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>英文名。</summary>
|
|
||||||
public string EnglishName { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>查价发货仓选项。</summary>
|
|
||||||
public class OrderPriceCheckWarehouseOptionOutput
|
|
||||||
{
|
|
||||||
/// <summary>仓库主键。</summary>
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
/// <summary>仓库名称。</summary>
|
|
||||||
public string WarehouseName { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>查价货物属性选项。</summary>
|
|
||||||
public class OrderPriceCheckCargoOptionOutput
|
|
||||||
{
|
|
||||||
/// <summary>字典编码。</summary>
|
|
||||||
public string DictCode { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>字典名称。</summary>
|
|
||||||
public string DictName { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@ -19,60 +19,6 @@ public partial class OrderService
|
|||||||
|
|
||||||
private sealed record PriceCheckCargoTarget(string Code, string DisplayName, string CargoAttribute);
|
private sealed record PriceCheckCargoTarget(string Code, string DisplayName, string CargoAttribute);
|
||||||
|
|
||||||
private const string CargoAttributeDictType = "cargo_attribute";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 组装查价表单下拉:国家、发货仓名称、货物属性字典。
|
|
||||||
/// </summary>
|
|
||||||
private async Task<OrderPriceCheckFormOptionsOutput> BuildPriceCheckFormOptionsAsync()
|
|
||||||
{
|
|
||||||
var countries = (await CountryRepository.GetListAsync())
|
|
||||||
.OrderBy(x => x.CountryCode)
|
|
||||||
.ThenBy(x => x.EnglishName)
|
|
||||||
.Select(x => new OrderPriceCheckCountryOptionOutput
|
|
||||||
{
|
|
||||||
CountryCode = (x.CountryCode ?? string.Empty).Trim().ToUpperInvariant(),
|
|
||||||
ChineseName = (x.ChineseName ?? string.Empty).Trim(),
|
|
||||||
EnglishName = (x.EnglishName ?? string.Empty).Trim(),
|
|
||||||
})
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.CountryCode))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var warehouses = (await WarehouseAddressRepository.GetListAsync())
|
|
||||||
.OrderBy(x => x.Sort == null)
|
|
||||||
.ThenBy(x => x.Sort)
|
|
||||||
.ThenBy(x => x.Id)
|
|
||||||
.Select(x => new OrderPriceCheckWarehouseOptionOutput
|
|
||||||
{
|
|
||||||
Id = x.Id,
|
|
||||||
WarehouseName = string.IsNullOrWhiteSpace(x.WarehouseName)
|
|
||||||
? x.Id.ToString()
|
|
||||||
: x.WarehouseName.Trim(),
|
|
||||||
})
|
|
||||||
.Where(x => x.Id > 0)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var cargoAttributes = (await DictRepository.GetListAsync(x => x.DictType == CargoAttributeDictType))
|
|
||||||
.OrderBy(x => x.DictSort)
|
|
||||||
.ThenBy(x => x.DictCode)
|
|
||||||
.Select(x => new OrderPriceCheckCargoOptionOutput
|
|
||||||
{
|
|
||||||
DictCode = (x.DictCode ?? string.Empty).Trim(),
|
|
||||||
DictName = string.IsNullOrWhiteSpace(x.DictName)
|
|
||||||
? (x.DictCode ?? string.Empty).Trim()
|
|
||||||
: x.DictName.Trim(),
|
|
||||||
})
|
|
||||||
.Where(x => !string.IsNullOrWhiteSpace(x.DictCode))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return new OrderPriceCheckFormOptionsOutput
|
|
||||||
{
|
|
||||||
Countries = countries,
|
|
||||||
Warehouses = warehouses,
|
|
||||||
CargoAttributes = cargoAttributes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<OrderPriceCheckShippingOptionsOutput> BuildPriceCheckShippingOptionsAsync()
|
private async Task<OrderPriceCheckShippingOptionsOutput> BuildPriceCheckShippingOptionsAsync()
|
||||||
{
|
{
|
||||||
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
|
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ using Fake.Application;
|
|||||||
using Fake.Domain.Exceptions;
|
using Fake.Domain.Exceptions;
|
||||||
using Fake.ObjectStorage;
|
using Fake.ObjectStorage;
|
||||||
using Fake.UnitOfWork;
|
using Fake.UnitOfWork;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using MiniExcelLibs;
|
using MiniExcelLibs;
|
||||||
@ -237,101 +236,7 @@ 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);
|
||||||
|
|
||||||
@ -668,14 +573,6 @@ public partial class OrderService : ApplicationService
|
|||||||
public Task<OrderPriceCheckShippingOptionsOutput> GetPriceCheckShippingOptionsAsync() =>
|
public Task<OrderPriceCheckShippingOptionsOutput> GetPriceCheckShippingOptionsAsync() =>
|
||||||
BuildPriceCheckShippingOptionsAsync();
|
BuildPriceCheckShippingOptionsAsync();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查价表单下拉(国家 / 发货仓 / 货物属性)。匿名可读,供 H5 访客先填条件再登录查价。
|
|
||||||
/// </summary>
|
|
||||||
[AllowAnonymous]
|
|
||||||
[HttpGet("/order/price-check/form-options")]
|
|
||||||
public Task<OrderPriceCheckFormOptionsOutput> GetPriceCheckFormOptionsAsync() =>
|
|
||||||
BuildPriceCheckFormOptionsAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行订单运费试算。
|
/// 执行订单运费试算。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -128,7 +128,7 @@ public partial class WarehouseService
|
|||||||
var resolvedCustomerId = await ResolveListCustomerIdAsync(customerId);
|
var resolvedCustomerId = await ResolveListCustomerIdAsync(customerId);
|
||||||
var notice = await GetInboundNoticeOrThrowAsync(resolvedCustomerId, id);
|
var notice = await GetInboundNoticeOrThrowAsync(resolvedCustomerId, id);
|
||||||
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
|
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
|
||||||
boxes = OrderInboundBoxes(boxes);
|
boxes = boxes.OrderBy(x => x.BoxSeqNo).ThenBy(x => x.Id).ToList();
|
||||||
var boxIds = boxes.Select(x => x.Id).ToArray();
|
var boxIds = boxes.Select(x => x.Id).ToArray();
|
||||||
var lines = boxIds.Length == 0
|
var lines = boxIds.Length == 0
|
||||||
? []
|
? []
|
||||||
@ -367,8 +367,7 @@ public partial class WarehouseService
|
|||||||
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
|
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
|
||||||
var notice = await GetInboundNoticeOrThrowAsync(customerId, input.Id);
|
var notice = await GetInboundNoticeOrThrowAsync(customerId, input.Id);
|
||||||
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
|
var boxes = await InboundBoxRepository.GetListAsync(x => x.InboundNoticeId == notice.Id);
|
||||||
// BoxSeqNo 为字符串,必须按数值排,否则 10 会排在 2 前面导致箱唛乱序
|
boxes = boxes.OrderBy(x => x.BoxSeqNo).ThenBy(x => x.Id).ToList();
|
||||||
boxes = OrderInboundBoxes(boxes);
|
|
||||||
if (boxes.Count == 0)
|
if (boxes.Count == 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("该预报没有箱子,无法打印箱唛");
|
throw new BusinessException("该预报没有箱子,无法打印箱唛");
|
||||||
@ -550,23 +549,4 @@ public partial class WarehouseService
|
|||||||
BinLocationId = line.BinLocationId
|
BinLocationId = line.BinLocationId
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 按箱序号数值升序;无法解析的序号排最后,同序号再按 Id。
|
|
||||||
/// </summary>
|
|
||||||
private static List<CInboundBox> OrderInboundBoxes(IEnumerable<CInboundBox> boxes) =>
|
|
||||||
boxes
|
|
||||||
.OrderBy(x => ParseBoxSeqNo(x.BoxSeqNo))
|
|
||||||
.ThenBy(x => x.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private static int ParseBoxSeqNo(string? boxSeqNo)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(boxSeqNo))
|
|
||||||
{
|
|
||||||
return int.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return int.TryParse(boxSeqNo.Trim(), out var n) && n > 0 ? n : int.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ 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
|
||||||
{
|
{
|
||||||
@ -36,7 +35,6 @@ public static class OrderOperationActionCodes
|
|||||||
Print => "打印面单",
|
Print => "打印面单",
|
||||||
Inbound => "入库",
|
Inbound => "入库",
|
||||||
Outbound => "出库",
|
Outbound => "出库",
|
||||||
ChangePostShipStatus => "修改出库后状态",
|
|
||||||
_ => action
|
_ => action
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user