273 lines
12 KiB
C#
273 lines
12 KiB
C#
using System.Data;
|
||
using Fake.Application;
|
||
using Fake.UnitOfWork;
|
||
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;
|
||
|
||
/// <summary>
|
||
/// 出库记录 API 服务。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Operation")]
|
||
public class OutboundRecordService : ApplicationService
|
||
{
|
||
private IOutboundRecordRepository OutboundRecordRepository => LazyServiceProvider.GetRequiredService<IOutboundRecordRepository>();
|
||
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||
private AutoTriggerRuntimeService AutoTriggerRuntimeService =>
|
||
LazyServiceProvider.GetRequiredService<AutoTriggerRuntimeService>();
|
||
|
||
/// <summary>
|
||
/// 按单号查询订单(出库扫描展示销售产品、目的国、服务渠道,只读)。
|
||
/// </summary>
|
||
[HttpGet("/operation/outbound/order-lookup")]
|
||
public async Task<OutboundOrderLookupOutput> 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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增出库记录。
|
||
/// </summary>
|
||
/// <param name="input">入参:出库记录创建参数。</param>
|
||
/// <returns>出库记录结果。</returns>
|
||
[HttpPost("/operation/outbound/create")]
|
||
[UnitOfWork(IsTransactional = true, IsolationLevel = IsolationLevel.ReadCommitted)]
|
||
public async Task<OutboundRecordOutput> 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<int>();
|
||
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);
|
||
|
||
// 自动触发走 Outbox,避免拉长出库扫码 RT
|
||
await AutoTriggerRuntimeService.EnqueueFireAsync(order.Id, AutoTriggerTiming.Outbound, operatorId);
|
||
|
||
return ObjectMapper.Map<OutboundRecord, OutboundRecordOutput>(savedEntity);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询最近出库记录列表。
|
||
/// </summary>
|
||
/// <param name="limit">入参:返回记录条数上限。</param>
|
||
/// <param name="warehouseId">入参:warehouseId。</param>
|
||
/// <returns>出库记录列表结果。</returns>
|
||
[HttpGet("/operation/outbound/recent-list")]
|
||
public async Task<OmsItemsResult<OutboundRecordOutput>> 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<string, bool> { [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<OutboundRecordOutput>();
|
||
foreach (var entity in entities)
|
||
{
|
||
var output = ObjectMapper.Map<OutboundRecord, OutboundRecordOutput>(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<OutboundRecordOutput> { 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<string> 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;
|
||
}
|
||
}
|