admin/src/Oms.Application/Services/Operations/OutboundRecordService.cs
2026-10-10 16:41:28 +08:00

273 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}