admin/src/Oms.Application/Services/Operations/OutboundRecordService.cs
zzh 90f6c25b04 feat(order): 问题件/理赔件标记、通知投递与自动触发
新增 mark-problematic/mark-claimed/handle-status 接口,异常事件审计表与占位符渲染;inApp/email 通知及下单/入库/出库自动触发运行时。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 16:46:52 +08:00

280 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 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;
/// <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")]
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);
try
{
var refreshed = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim());
if (refreshed != null)
{
await AutoTriggerRuntimeService.FireAsync(refreshed, AutoTriggerTiming.Outbound, operatorId);
}
}
catch
{
// 自动触发失败不影响出库主流程
}
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;
}
}