admin/src/Oms.Application/Services/Operations/InboundRecordService.cs
zzh 82df5d1e79 feat(order): add order edit APIs, inbound lookup, and fee output
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>
2026-06-26 17:30:41 +08:00

192 lines
8.1 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 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 };
}
}