diff --git a/sql/migrate_t_customer_account_flow.sql b/sql/migrate_t_customer_account_flow.sql index 9e9078b..25f4074 100644 --- a/sql/migrate_t_customer_account_flow.sql +++ b/sql/migrate_t_customer_account_flow.sql @@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS `t_customer_account_flow` ( `Currency` varchar(8) NOT NULL DEFAULT 'CNY' COMMENT '币种', `Remark` varchar(512) DEFAULT NULL COMMENT '备注', `IsDeleted` tinyint(1) NOT NULL DEFAULT '0', + `ConcurrencyStamp` char(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', `CreateUserId` int NOT NULL DEFAULT '0', `CreateTime` datetime NOT NULL, `UpdateUserId` int DEFAULT NULL, diff --git a/sql/migrate_t_customer_account_flow_concurrency.sql b/sql/migrate_t_customer_account_flow_concurrency.sql new file mode 100644 index 0000000..74e908f --- /dev/null +++ b/sql/migrate_t_customer_account_flow_concurrency.sql @@ -0,0 +1,8 @@ +-- 已执行旧版 migrate_t_customer_account_flow.sql 的库补 ConcurrencyStamp(与 AggregateRoot 实体对齐) +-- 勿在 ALTER 中使用 DEFAULT(UUID()),复制环境会报 Error 1674 +ALTER TABLE `t_customer_account_flow` + ADD COLUMN `ConcurrencyStamp` char(36) NOT NULL DEFAULT '' COMMENT '并发戳' AFTER `IsDeleted`; + +UPDATE `t_customer_account_flow` + SET `ConcurrencyStamp` = UUID() + WHERE `ConcurrencyStamp` = ''; diff --git a/src/Oms.Application/Dtos/Operations/OutboundOrderLookupOutput.cs b/src/Oms.Application/Dtos/Operations/OutboundOrderLookupOutput.cs new file mode 100644 index 0000000..95259da --- /dev/null +++ b/src/Oms.Application/Dtos/Operations/OutboundOrderLookupOutput.cs @@ -0,0 +1,77 @@ +namespace Oms.Application.Dtos.Operations; + +/// +/// 出库扫描按单号查订单(只读展示销售产品、目的国、服务渠道)。 +/// +public class OutboundOrderLookupOutput +{ + /// + /// 订单主键。 + /// + public int OrderId { get; set; } + + /// + /// 服务单号。 + /// + public string ServiceNo { get; set; } = string.Empty; + + /// + /// 客户单号。 + /// + public string? ClientNo { get; set; } + + /// + /// 跟踪号。 + /// + public string? TrackNo { get; set; } + + /// + /// 产品编码。 + /// + public string? ProductCode { get; set; } + + /// + /// 销售产品主键。 + /// + public int? SalesProductId { get; set; } + + /// + /// 销售产品名称。 + /// + public string? SalesProductName { get; set; } + + /// + /// 目的国家编码。 + /// + public string? DestinationCountry { get; set; } + + /// + /// 目的国家名称。 + /// + public string? DestinationCountryName { get; set; } + + /// + /// 服务渠道主键。 + /// + public int? ServiceChannelId { get; set; } + + /// + /// 服务渠道编码。 + /// + public string? ChannelCode { get; set; } + + /// + /// 服务渠道展示名。 + /// + public string? ServiceChannelName { get; set; } + + /// + /// 订单状态码。 + /// + public string OrderStatus { get; set; } = string.Empty; + + /// + /// 订单状态名称。 + /// + public string? OrderStatusName { get; set; } +} diff --git a/src/Oms.Application/Services/Operations/OutboundRecordService.cs b/src/Oms.Application/Services/Operations/OutboundRecordService.cs index 4393969..2e92f1d 100644 --- a/src/Oms.Application/Services/Operations/OutboundRecordService.cs +++ b/src/Oms.Application/Services/Operations/OutboundRecordService.cs @@ -2,8 +2,12 @@ using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Operations; using Oms.Application.Dtos.Shared; +using Oms.Application.Shared; +using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.OperationsAggregate.Entities; using Oms.Domain.OperationsAggregate.Repositories; +using Oms.Domain.OrdersAggregate.Consts; +using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.SystemAggregate.Repositories; namespace Oms.Application.Services.Operations; @@ -15,7 +19,73 @@ namespace Oms.Application.Services.Operations; public class OutboundRecordService : ApplicationService { private IOutboundRecordRepository OutboundRecordRepository => LazyServiceProvider.GetRequiredService(); + private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService(); + private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService(); + private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService(); + private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService(); private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService(); + private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService(); + + /// + /// 按单号查询订单(出库扫描展示销售产品、目的国、服务渠道,只读)。 + /// + [HttpGet("/operation/outbound/order-lookup")] + public async Task LookupOrderAsync([FromQuery] string waybillNo) + { + if (string.IsNullOrWhiteSpace(waybillNo)) + { + throw new BusinessException("请扫描或输入单号"); + } + + var order = await OrderRepository.GetByWaybillNoAsync(waybillNo.Trim()); + if (order == null) + { + throw new BusinessException("未找到对应订单,请检查单号"); + } + + EnsureOutboundAllowed(order.OrderStatus); + + var output = new OutboundOrderLookupOutput + { + OrderId = order.Id, + ServiceNo = order.ServiceNo, + ClientNo = order.ClientNo, + TrackNo = order.TrackNo, + ProductCode = order.ProductCode, + DestinationCountry = order.ConsigneeCountry, + OrderStatus = order.OrderStatus, + OrderStatusName = OmsDocHelper.GetOrderStatusName(order.OrderStatus) + }; + + if (!string.IsNullOrWhiteSpace(order.ProductCode)) + { + var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); + if (product != null) + { + output.SalesProductId = product.Id; + output.SalesProductName = product.ProductName; + } + } + + if (!string.IsNullOrWhiteSpace(order.ConsigneeCountry)) + { + var country = await CountryRepository.FirstOrDefaultAsync(x => x.CountryCode == order.ConsigneeCountry); + output.DestinationCountryName = country?.ChineseName ?? country?.EnglishName ?? order.ConsigneeCountry; + } + + if (!string.IsNullOrWhiteSpace(order.ChannelCode)) + { + var channel = await ServiceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim()); + if (channel != null) + { + output.ServiceChannelId = channel.Id; + output.ChannelCode = channel.ChannelCode; + output.ServiceChannelName = await BuildServiceChannelDisplayNameAsync(channel.GroupId, channel.ChannelName); + } + } + + return output; + } /// /// 新增出库记录。 @@ -31,12 +101,25 @@ public class OutboundRecordService : ApplicationService [HttpPost("/operation/outbound/create")] public async Task CreateAsync(CreateOutboundRecordInput input) { + if (string.IsNullOrWhiteSpace(input.WaybillNo)) + { + throw new BusinessException("运单号不能为空"); + } + + var order = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim()); + if (order == null) + { + throw new BusinessException("未找到对应订单,请检查单号"); + } + + EnsureOutboundAllowed(order.OrderStatus); + var now = DateTime.Now; var operatorId = CurrentUser.GetId(); var entity = new OutboundRecord { - WaybillNo = input.WaybillNo, - WarehouseId = input.WarehouseId, + WaybillNo = input.WaybillNo.Trim(), + WarehouseId = input.WarehouseId ?? order.WarehouseId, OperateTime = now, OperatorId = operatorId }; @@ -86,4 +169,30 @@ public class OutboundRecordService : ApplicationService return new OmsItemsResult { Items = items }; } + + private static void EnsureOutboundAllowed(string orderStatus) + { + if (orderStatus is OrderStatusCodes.PendingPayment or OrderStatusCodes.Received) + { + throw new BusinessException("订单未付款,禁止出库"); + } + + if (orderStatus != OrderStatusCodes.PendingShip) + { + throw new BusinessException($"订单状态为「{OmsDocHelper.GetOrderStatusName(orderStatus)}」,不允许出库"); + } + } + + private async Task BuildServiceChannelDisplayNameAsync(int? groupId, string channelName) + { + string? groupName = null; + if (groupId.HasValue) + { + var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == groupId.Value); + groupName = group?.Name; + } + + var displayName = $"{groupName}{channelName}".Trim(); + return string.IsNullOrWhiteSpace(displayName) ? channelName : displayName; + } } diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index b52cdf3..c4b45a4 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -1142,7 +1142,7 @@ public partial class OrderService OrderFeeChargingStageCodes.Inbound, replaceExistingReceivable: true); - order.OrderStatus = OrderStatusCodes.Received; + order.OrderStatus = OrderStatusCodes.PendingPayment; order.ReceiveTime = DateTime.Now; order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index 1b794bb..7f21b87 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -119,7 +119,7 @@ public partial class OrderService : ApplicationService throw new BusinessException("订单已支付"); } - if (order.OrderStatus != OrderStatusCodes.PendingPayment) + if (order.OrderStatus is not (OrderStatusCodes.PendingPayment or OrderStatusCodes.Received)) { throw new BusinessException("当前订单状态不可支付"); }