using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Operations; using Oms.Application.Dtos.Shared; using Oms.Application.Services.Orders; using Oms.Application.Services.Orders.Issues; 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.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.SystemAggregate.Repositories; namespace Oms.Application.Services.Operations; /// /// 出库记录 API 服务。 /// [ApiExplorerSettings(GroupName = "OMS-Operation")] 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(); private OrderService OrderService => LazyServiceProvider.GetRequiredService(); private AutoTriggerRuntimeService AutoTriggerRuntimeService => 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); 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; } /// /// 新增出库记录。 /// /// 入参:出库记录创建参数。 /// 出库记录结果。 [HttpPost("/operation/outbound/create")] public async Task CreateAsync(CreateOutboundRecordInput input) { if (string.IsNullOrWhiteSpace(input.WaybillNo)) { throw new BusinessException("运单号不能为空"); } if (!input.Weight.HasValue || !input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue || input.Weight <= 0 || input.Length <= 0 || input.Width <= 0 || input.Height <= 0) { throw new BusinessException("发货实重与长宽高不能为空且须大于 0"); } var order = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim()); if (order == null) { throw new BusinessException("未找到对应订单,请检查单号"); } EnsureOutboundAllowed(order); int? salesProductId = input.SalesProductId; if (!salesProductId.HasValue && !string.IsNullOrWhiteSpace(order.ProductCode)) { var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); salesProductId = product?.Id; } var destinationCountry = input.DestinationCountry ?? order.ConsigneeCountry; var serviceChannelId = input.ServiceChannelId; if (!serviceChannelId.HasValue && !string.IsNullOrWhiteSpace(order.ChannelCode)) { var channel = await ServiceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim()); serviceChannelId = channel?.Id; } var now = DateTime.Now; var operatorId = CurrentUser.GetId(); var entity = new OutboundRecord { WaybillNo = input.WaybillNo.Trim(), Weight = input.Weight, Length = input.Length, Width = input.Width, Height = input.Height, SalesProductId = salesProductId, DestinationCountry = destinationCountry, ServiceChannelId = serviceChannelId, WarehouseId = input.WarehouseId ?? order.WarehouseId, OperateTime = now, OperatorId = operatorId }; await OrderService.ApplyShipBillingByWaybillAsync( input.WaybillNo.Trim(), input.Length, input.Width, input.Height, input.Weight); var savedEntity = await OutboundRecordRepository.InsertAsync(entity); try { var refreshed = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim()); if (refreshed != null) { await AutoTriggerRuntimeService.FireAsync(refreshed, AutoTriggerTiming.Outbound, operatorId); } } catch { // 自动触发失败不影响出库主流程 } return ObjectMapper.Map(savedEntity); } /// /// 查询最近出库记录列表。 /// /// 入参:返回记录条数上限。 /// 入参:warehouseId。 /// 出库记录列表结果。 [HttpGet("/operation/outbound/recent-list")] public async Task> GetRecentListAsync([FromQuery] int limit = 20, [FromQuery] int? warehouseId = null) { var hasWarehouseFilter = warehouseId.HasValue; var warehouseIdValue = warehouseId.GetValueOrDefault(); var entities = await OutboundRecordRepository.GetPagedListAsync( x => !hasWarehouseFilter || x.WarehouseId == warehouseIdValue, 1, limit <= 0 ? 20 : limit, new Dictionary { [nameof(OutboundRecord.OperateTime)] = false, [nameof(OutboundRecord.Id)] = false }); var salesProductIds = entities.Where(x => x.SalesProductId.HasValue).Select(x => x.SalesProductId!.Value).Distinct().ToArray(); var serviceChannelIds = entities.Where(x => x.ServiceChannelId.HasValue).Select(x => x.ServiceChannelId!.Value).Distinct().ToArray(); var warehouseIds = entities.Where(x => x.WarehouseId.HasValue).Select(x => x.WarehouseId!.Value).Distinct().ToArray(); var products = salesProductIds.Length == 0 ? [] : await SalesProductRepository.GetListAsync(x => salesProductIds.Contains(x.Id)); var channels = serviceChannelIds.Length == 0 ? [] : await ServiceChannelRepository.GetListAsync(x => serviceChannelIds.Contains(x.Id)); var warehouses = warehouseIds.Length == 0 ? [] : await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id)); var productMap = products.ToDictionary(x => x.Id); var channelMap = channels.ToDictionary(x => x.Id); var warehouseMap = warehouses.ToDictionary(x => x.Id); var groupIds = channels.Where(x => x.GroupId.HasValue).Select(x => x.GroupId!.Value).Distinct().ToArray(); var groups = groupIds.Length == 0 ? [] : await ServiceChannelGroupRepository.GetListAsync(x => groupIds.Contains(x.Id)); var groupMap = groups.ToDictionary(x => x.Id); var items = new List(); foreach (var entity in entities) { var output = ObjectMapper.Map(entity); output.SalesProductName = entity.SalesProductId.HasValue && productMap.TryGetValue(entity.SalesProductId.Value, out var product) ? product.ProductName : null; output.WarehouseName = entity.WarehouseId.HasValue && warehouseMap.TryGetValue(entity.WarehouseId.Value, out var warehouse) ? warehouse.WarehouseName : null; if (entity.ServiceChannelId.HasValue && channelMap.TryGetValue(entity.ServiceChannelId.Value, out var channel)) { string? groupName = null; if (channel.GroupId.HasValue && groupMap.TryGetValue(channel.GroupId.Value, out var group)) { groupName = group.Name; } var displayName = $"{groupName}{channel.ChannelName}".Trim(); output.ServiceChannelName = string.IsNullOrWhiteSpace(displayName) ? channel.ChannelName : displayName; } output.OperatorName = entity.OperatorId?.ToString(); items.Add(output); } return new OmsItemsResult { Items = items }; } private static void EnsureOutboundAllowed(Order order) { if (order.IsPaid == 0) { throw new BusinessException("订单未付款,禁止出库"); } var orderStatus = order.OrderStatus; if (orderStatus != OrderStatusCodes.PendingShip) { if (orderStatus is OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed) { throw new BusinessException("订单已出库"); } 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; } }