Support order detail/weight/declaration updates, receivable and payable recalculation, inbound scan order lookup, receive billing overwrite, and OrderOutput feeList for my-orders drawer. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
8.1 KiB
C#
192 lines
8.1 KiB
C#
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.Shared;
|
||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||
using Oms.Domain.OperationsAggregate.Entities;
|
||
using Oms.Domain.OperationsAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Repositories;
|
||
using Oms.Domain.SystemAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Operations;
|
||
|
||
/// <summary>
|
||
/// 入库记录 API 服务。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Operation")]
|
||
public class InboundRecordService : ApplicationService
|
||
{
|
||
private IInboundRecordRepository InboundRecordRepository => LazyServiceProvider.GetRequiredService<IInboundRecordRepository>();
|
||
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||
|
||
/// <summary>
|
||
/// 按单号查询订单(入库扫描展示销售产品、目的国,只读)。
|
||
/// </summary>
|
||
[HttpGet("/operation/inbound/order-lookup")]
|
||
public async Task<InboundOrderLookupOutput> LookupOrderAsync([FromQuery] string waybillNo)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(waybillNo))
|
||
{
|
||
throw new BusinessException("请扫描或输入单号");
|
||
}
|
||
|
||
var order = await OrderRepository.GetByWaybillNoAsync(waybillNo.Trim());
|
||
if (order == null)
|
||
{
|
||
throw new BusinessException("未找到对应订单,请检查单号");
|
||
}
|
||
|
||
var output = new InboundOrderLookupOutput
|
||
{
|
||
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;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增入库记录。
|
||
/// </summary>
|
||
/// <param name="input">入参:入库记录创建参数。</param>
|
||
/// <returns>入库记录结果。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 校验入参与关联对象、唯一性等前置条件。
|
||
/// 2. 组装目标实体及相关明细数据。
|
||
/// 3. 写入审计字段并持久化结果。
|
||
/// </remarks>
|
||
[HttpPost("/operation/inbound/create")]
|
||
public async Task<InboundRecordOutput> CreateAsync(CreateInboundRecordInput input)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.WaybillNo))
|
||
{
|
||
throw new BusinessException("运单号不能为空");
|
||
}
|
||
|
||
var order = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim());
|
||
if (order == null)
|
||
{
|
||
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");
|
||
}
|
||
|
||
int? salesProductId = null;
|
||
string? destinationCountry = order.ConsigneeCountry;
|
||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||
{
|
||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||
salesProductId = product?.Id;
|
||
}
|
||
|
||
var now = DateTime.Now;
|
||
var operatorId = CurrentUser.GetId<int>();
|
||
var entity = new InboundRecord
|
||
{
|
||
WaybillNo = input.WaybillNo.Trim(),
|
||
Weight = input.Weight,
|
||
Length = input.Length,
|
||
Width = input.Width,
|
||
Height = input.Height,
|
||
SalesProductId = salesProductId,
|
||
DestinationCountry = destinationCountry,
|
||
WarehouseId = input.WarehouseId ?? order.WarehouseId,
|
||
OperateTime = now,
|
||
OperatorId = operatorId
|
||
};
|
||
|
||
await OrderService.ApplyReceiveBillingByWaybillAsync(
|
||
input.WaybillNo.Trim(),
|
||
input.Length,
|
||
input.Width,
|
||
input.Height,
|
||
input.Weight);
|
||
|
||
var savedEntity = await InboundRecordRepository.InsertAsync(entity);
|
||
return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询最近入库记录列表。
|
||
/// </summary>
|
||
/// <param name="limit">入参:返回记录条数上限。</param>
|
||
/// <param name="warehouseId">入参:warehouseId。</param>
|
||
/// <returns>入库记录列表结果。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 根据查询条件构造筛选规则。
|
||
/// 2. 查询目标数据并补齐关联展示字段。
|
||
/// 3. 按接口约定返回列表结果。
|
||
/// </remarks>
|
||
[HttpGet("/operation/inbound/recent-list")]
|
||
public async Task<OmsItemsResult<InboundRecordOutput>> GetRecentListAsync([FromQuery] int limit = 20, [FromQuery] int? warehouseId = null)
|
||
{
|
||
var hasWarehouseFilter = warehouseId.HasValue;
|
||
var warehouseIdValue = warehouseId.GetValueOrDefault();
|
||
var entities = await InboundRecordRepository.GetPagedListAsync(
|
||
x => !hasWarehouseFilter || x.WarehouseId == warehouseIdValue,
|
||
1,
|
||
limit <= 0 ? 20 : limit,
|
||
new Dictionary<string, bool> { [nameof(InboundRecord.OperateTime)] = false, [nameof(InboundRecord.Id)] = false });
|
||
|
||
var salesProductIds = entities.Where(x => x.SalesProductId.HasValue).Select(x => x.SalesProductId!.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 warehouses = warehouseIds.Length == 0
|
||
? []
|
||
: await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
|
||
var productMap = products.ToDictionary(x => x.Id);
|
||
var warehouseMap = warehouses.ToDictionary(x => x.Id);
|
||
|
||
var items = entities.Select(entity =>
|
||
{
|
||
var output = ObjectMapper.Map<InboundRecord, InboundRecordOutput>(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;
|
||
output.OperatorName = entity.OperatorId?.ToString();
|
||
return output;
|
||
}).ToList();
|
||
|
||
return new OmsItemsResult<InboundRecordOutput> { Items = items };
|
||
}
|
||
}
|