using Fake.Application; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Shared; using Oms.Application.Integrations.Itfx; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.CustomersAggregate.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.Orders; /// /// 揽收单(1 揽收 : N 物流)管理。 /// [ApiExplorerSettings(GroupName = "OMS-Order")] public class PickupOrderService : ApplicationService { private static readonly HashSet BindableOrderStatuses = new(StringComparer.OrdinalIgnoreCase) { OrderStatusCodes.Draft, OrderStatusCodes.Predeclared, OrderStatusCodes.PendingAllocation, OrderStatusCodes.PendingPayment, OrderStatusCodes.PendingShip, OrderStatusCodes.PendingOnline, }; private IPickupOrderRepository PickupOrderRepository => LazyServiceProvider.GetRequiredService(); private IPickupOrderLineRepository PickupOrderLineRepository => LazyServiceProvider.GetRequiredService(); private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private ICustomerSenderAddressRepository CustomerSenderAddressRepository => LazyServiceProvider.GetRequiredService(); private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService(); private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService(); private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService(); private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService(); private IItfxForwardClient ItfxForwardClient => LazyServiceProvider.GetRequiredService(); private IItfxMetadataClient ItfxMetadataClient => LazyServiceProvider.GetRequiredService(); /// /// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。 /// [HttpPost("/order/pickup-order/create")] public async Task CreateAsync([FromBody] PickupOrderCreateInput input) { var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId); CustomerContactHelper.EnsureContactComplete(customer); if (input.SenderAddressId <= 0) { throw new BusinessException("揽收地址不能为空"); } if (input.WarehouseId <= 0) { throw new BusinessException("仓库不能为空"); } if (input.ExpectPickupTime == default) { throw new BusinessException("期望揽收时间不能为空"); } var direction = PickupDirectionCodes.NormalizeOrDefault(input.Direction); var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList(); if (orderIds.Count == 0) { throw new BusinessException("请至少选择一票物流订单"); } var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId); if (senderAddress == null) { throw new BusinessException("揽收地址不存在"); } var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType) ? CustomerAddressTypeCodes.Sender : senderAddress.AddressType; if (!CustomerAddressTypeCodes.IsPickupEligible(addressType)) { throw new BusinessException("揽收单请选择「国内揽收」类型地址"); } var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId); if (warehouse == null) { throw new BusinessException("仓库不存在"); } var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); var orderMap = orders.ToDictionary(x => x.Id); var existingLines = await PickupOrderLineRepository.GetActiveByOrderIdsAsync(orderIds); var existingByOrderId = existingLines.ToDictionary(x => x.OrderId); var rejected = new List(); var accepted = new List(); foreach (var orderId in orderIds) { if (!orderMap.TryGetValue(orderId, out var order)) { rejected.Add(new PickupOrderRejectItem { OrderId = orderId, Reason = "订单不存在" }); continue; } var reason = ValidateOrderForBind( order, customerId, input.WarehouseId, input.SenderAddressId, existingByOrderId.GetValueOrDefault(orderId)); if (reason != null) { rejected.Add(new PickupOrderRejectItem { OrderId = orderId, ServiceNo = order.ServiceNo, Reason = reason }); continue; } accepted.Add(order); } if (rejected.Count > 0) { throw new BusinessException( "部分运单不能合并揽收:" + string.Join(";", rejected.Select(x => $"{x.ServiceNo ?? x.OrderId.ToString()} — {x.Reason}"))); } if (accepted.Count == 0) { throw new BusinessException("没有可挂载的物流订单"); } string? senderCountryCode = null; if (senderAddress.Country.HasValue) { var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == senderAddress.Country.Value); senderCountryCode = country?.CountryCode; } if (!string.IsNullOrWhiteSpace(input.RequestId)) { var existedByRequest = await PickupOrderRepository.FirstOrDefaultAsync(x => x.RequestId == input.RequestId.Trim()); if (existedByRequest != null) { if (existedByRequest.CustomerId != customerId) { throw new BusinessException("幂等请求已存在"); } return await MapOutputAsync(existedByRequest, includeOrders: true); } } var weights = await OrderRepository.GetWeightsByOrderIdsAsync(accepted.Select(x => x.Id)); var weightSum = weights .GroupBy(x => x.OrderId) .Select(g => g.OrderByDescending(w => w.Type == OrderWeightTypeCodes.Receive ? 2 : 1).FirstOrDefault()) .Where(x => x != null) .Sum(x => x!.ActualWeight ?? 0); var freight = input.FreightAmount; var receivable = freight; var orgId = accepted .Select(x => x.OrganizationId) .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)); var secretLevel = accepted .Select(x => x.SecretLevel) .FirstOrDefault(x => x > 0); if (secretLevel <= 0) { secretLevel = 2; } var entity = new PickupOrder { PickupNo = await GeneratePickupNoAsync(), RequestId = NormalizeOptional(input.RequestId), CustomerId = customerId, OrganizationId = orgId, SecretLevel = secretLevel, SenderAddressId = senderAddress.Id, Direction = direction, SenderName = senderAddress.SenderName ?? string.Empty, SenderCompany = senderAddress.Company, SenderCountry = senderAddress.Country, SenderCountryCode = senderCountryCode, SenderProvince = senderAddress.Province, SenderCity = senderAddress.City, SenderZipCode = senderAddress.ZipCode, SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty, SenderStreetAddress2 = senderAddress.StreetAddress2, SenderStreetAddress3 = senderAddress.StreetAddress3, SenderHouseNumber = senderAddress.HouseNumber, SenderPhone = senderAddress.Phone, SenderTelePhone = senderAddress.TelePhone, SenderEmail = senderAddress.Email, WarehouseId = input.WarehouseId, CarrierCode = NormalizeOptional(input.CarrierCode), CarrierService = NormalizeOptional(input.CarrierService), ExpectPickupTime = input.ExpectPickupTime, Status = PickupOrderStatusCodes.Draft, ParcelCount = accepted.Count, TotalActualWeight = weightSum > 0 ? weightSum : null, Currency = "CNY", FreightAmount = freight, OtherFeeAmount = null, ReceivableAmount = receivable, PayStatus = PickupPayStatusCodes.Unpaid, PayMode = PickupPayModeCodes.Prepaid, SettlementStatus = PickupSettlementStatusCodes.None, Remark = NormalizeOptional(input.Remark) }; await PickupOrderRepository.InsertAsync(entity); var sort = 0; foreach (var order in accepted) { await PickupOrderLineRepository.InsertAsync(new PickupOrderLine { PickupOrderId = entity.Id, OrderId = order.Id, ServiceNo = order.ServiceNo, ClientNo = order.ClientNo, SortNo = sort++ }); order.PickupOrderId = entity.Id; order.InboundMethod = OrderInboundMethodCodes.DoorPickup; order.PickupTime = entity.ExpectPickupTime; order.CustomerSenderAddressId = entity.SenderAddressId; await OrderRepository.UpdateAsync(order); } return await MapOutputAsync(entity, includeOrders: true); } /// /// 追加物流单到已有揽收单。 /// [HttpPost("/order/pickup-order/add-orders")] public async Task AddOrdersAsync([FromBody] PickupOrderOrdersInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status)) { throw new BusinessException("当前揽收单状态不可追加运单"); } var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList(); if (orderIds.Count == 0) { throw new BusinessException("请至少选择一票物流订单"); } var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); var orderMap = orders.ToDictionary(x => x.Id); var existingLines = await PickupOrderLineRepository.GetActiveByOrderIdsAsync(orderIds); var existingByOrderId = existingLines.ToDictionary(x => x.OrderId); var currentLines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); var sort = currentLines.Count == 0 ? 0 : currentLines.Max(x => x.SortNo) + 1; var rejected = new List(); var accepted = new List(); foreach (var orderId in orderIds) { if (!orderMap.TryGetValue(orderId, out var order)) { rejected.Add(new PickupOrderRejectItem { OrderId = orderId, Reason = "订单不存在" }); continue; } var reason = ValidateOrderForBind( order, pickup.CustomerId, pickup.WarehouseId, pickup.SenderAddressId, existingByOrderId.GetValueOrDefault(orderId)); if (reason != null) { rejected.Add(new PickupOrderRejectItem { OrderId = orderId, ServiceNo = order.ServiceNo, Reason = reason }); continue; } accepted.Add(order); } if (rejected.Count > 0) { throw new BusinessException( "部分运单不能追加:" + string.Join(";", rejected.Select(x => $"{x.ServiceNo ?? x.OrderId.ToString()} — {x.Reason}"))); } foreach (var order in accepted) { await PickupOrderLineRepository.InsertAsync(new PickupOrderLine { PickupOrderId = pickup.Id, OrderId = order.Id, ServiceNo = order.ServiceNo, ClientNo = order.ClientNo, SortNo = sort++ }); order.PickupOrderId = pickup.Id; order.InboundMethod = OrderInboundMethodCodes.DoorPickup; order.PickupTime = pickup.ExpectPickupTime; order.CustomerSenderAddressId = pickup.SenderAddressId; await OrderRepository.UpdateAsync(order); } await RefreshParcelCountAsync(pickup); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 从揽收单移除物流单;移除后若无关联则自动取消揽收单。 /// [HttpPost("/order/pickup-order/remove-orders")] public async Task RemoveOrdersAsync([FromBody] PickupOrderOrdersInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status)) { throw new BusinessException("当前揽收单状态不可移除运单"); } var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToHashSet(); if (orderIds.Count == 0) { throw new BusinessException("请至少选择一票物流订单"); } var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); var toRemove = lines.Where(x => orderIds.Contains(x.OrderId)).ToList(); if (toRemove.Count == 0) { throw new BusinessException("所选运单不在该揽收单中"); } foreach (var line in toRemove) { await PickupOrderLineRepository.HardDeleteAsync(line); var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId); if (order != null && order.PickupOrderId == pickup.Id) { order.PickupOrderId = null; await OrderRepository.UpdateAsync(order); } } var remain = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); if (remain.Count == 0) { if (pickup.PayStatus == PickupPayStatusCodes.Paid) { throw new BusinessException("揽收单已支付且将无关联运单,请先处理退款后再取消(P1 请联系客服)"); } pickup.Status = PickupOrderStatusCodes.Cancelled; pickup.ParcelCount = 0; await PickupOrderRepository.UpdateAsync(pickup); } else { await RefreshParcelCountAsync(pickup); } return await MapOutputAsync(pickup, includeOrders: true); } /// /// 揽收单分页列表。 /// [HttpPost("/order/pickup-order/list")] public async Task> ListAsync([FromBody] PickupOrderListInput input) { var page = input.Page <= 0 ? 1 : input.Page; var pageSize = input.PageSize <= 0 ? 30 : Math.Min(input.PageSize, 200); var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var keyword = NormalizeOptional(input.Keyword); var status = NormalizeOptional(input.Status); var payStatus = NormalizeOptional(input.PayStatus); var direction = string.IsNullOrWhiteSpace(input.Direction) ? null : PickupDirectionCodes.NormalizeOrDefault(input.Direction); var all = await PickupOrderRepository.GetListAsync(x => (customerId == null || x.CustomerId == customerId.Value) && (direction == null || x.Direction == direction) && (status == null || x.Status == status) && (payStatus == null || x.PayStatus == payStatus) && (input.WarehouseId == null || input.WarehouseId <= 0 || x.WarehouseId == input.WarehouseId.Value) && (input.CreateTimeStart == null || x.CreateTime >= input.CreateTimeStart) && (input.CreateTimeEnd == null || x.CreateTime <= input.CreateTimeEnd) && (input.ExpectPickupTimeStart == null || x.ExpectPickupTime >= input.ExpectPickupTimeStart) && (input.ExpectPickupTimeEnd == null || x.ExpectPickupTime <= input.ExpectPickupTimeEnd) && (keyword == null || x.PickupNo.Contains(keyword) || (x.CarrierWaybillNo != null && x.CarrierWaybillNo.Contains(keyword)) || (x.SenderName != null && x.SenderName.Contains(keyword)))); var ordered = all.OrderByDescending(x => x.CreateTime).ThenByDescending(x => x.Id).ToList(); var total = ordered.Count; var pageItems = ordered.Skip((page - 1) * pageSize).Take(pageSize).ToList(); var outputs = new List(); foreach (var item in pageItems) { outputs.Add(await MapOutputAsync(item, includeOrders: false)); } return new OmsPageResult { Items = outputs, Total = total }; } /// /// 揽收单状态计数(Tab 徽章)。 /// [HttpGet("/order/pickup-order/status-counts")] public async Task GetStatusCountsAsync( [FromQuery] int? customerId, [FromQuery] string? direction = null) { var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, customerId); var directionFilter = string.IsNullOrWhiteSpace(direction) ? null : PickupDirectionCodes.NormalizeOrDefault(direction); var all = await PickupOrderRepository.GetListAsync(x => (resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value) && (directionFilter == null || x.Direction == directionFilter)); var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed }; var pendingPayment = all.Count(x => !terminal.Contains(x.Status) && x.PayStatus != PickupPayStatusCodes.Paid && x.ReceivableAmount is > 0); var counts = all .GroupBy(x => x.Status ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase); return new PickupOrderStatusCountsOutput { Total = all.Count, PendingPayment = pendingPayment, Counts = counts }; } /// /// 揽收单详情。 /// [HttpGet("/order/pickup-order/detail")] public async Task GetDetailAsync([FromQuery] int id) { var pickup = await GetOwnedPickupOrThrowAsync(id); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 确认对客揽收运费。 /// [HttpPost("/order/pickup-order/confirm-freight")] public async Task ConfirmFreightAsync([FromBody] PickupOrderConfirmFreightInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed) { throw new BusinessException("已取消/失败的揽收单不可确认运费"); } if (pickup.PayStatus == PickupPayStatusCodes.Paid) { throw new BusinessException("已支付不可修改运费"); } if (input.FreightAmount < 0) { throw new BusinessException("运费不能为负数"); } var other = input.OtherFeeAmount ?? 0; if (other < 0) { throw new BusinessException("其他费用不能为负数"); } pickup.FreightAmount = input.FreightAmount; pickup.OtherFeeAmount = other; pickup.ReceivableAmount = input.FreightAmount + other; if (input.CarrierPayableAmount.HasValue) { pickup.CarrierPayableAmount = input.CarrierPayableAmount; } await PickupOrderRepository.UpdateAsync(pickup); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 客户支付揽收运费(余额)。 /// [HttpPost("/order/pickup-order/pay")] public async Task PayAsync([FromBody] PickupOrderIdInput input) { var pickup = await PayCoreAsync(input.Id); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 批量支付揽收运费(逐单扣余额,部分失败不回滚已成功项)。 /// [HttpPost("/order/pickup-order/pay-batch")] public async Task PayBatchAsync([FromBody] PickupOrderIdsInput input) { var ids = (input.Ids ?? []).Where(x => x > 0).Distinct().ToList(); if (ids.Count == 0) { throw new BusinessException("请至少选择一张揽收单"); } if (ids.Count > 100) { throw new BusinessException("单次最多支付 100 张揽收单"); } var output = new PickupOrderBatchPayOutput(); foreach (var id in ids) { try { var pickup = await PayCoreAsync(id); var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0; output.SuccessCount++; output.PaidAmount += amount; output.Items.Add(new PickupOrderBatchPayItemOutput { Id = pickup.Id, PickupNo = pickup.PickupNo, Success = true, Message = "支付成功", Amount = amount }); } catch (Exception ex) { output.FailCount++; var pickupNo = await TryGetPickupNoAsync(id); output.Items.Add(new PickupOrderBatchPayItemOutput { Id = id, PickupNo = pickupNo, Success = false, Message = ex.Message }); } } output.Message = $"支付完成:成功 {output.SuccessCount},失败 {output.FailCount}"; return output; } private async Task PayCoreAsync(int id) { var pickup = await GetOwnedPickupOrThrowAsync(id); if (pickup.PayStatus == PickupPayStatusCodes.Paid) { throw new BusinessException("揽收单已支付"); } if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed) { throw new BusinessException("当前状态不可支付"); } var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0; if (amount <= 0) { throw new BusinessException("请先确认揽收运费"); } var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == pickup.CustomerId); if (customer == null) { throw new BusinessException("客户不存在"); } var balanceMutator = LazyServiceProvider.GetRequiredService(); await balanceMutator.DebitPickupPayAsync( customer, amount, pickup.Id, pickup.PickupNo, $"揽收支付 {pickup.PickupNo}"); pickup.PayStatus = PickupPayStatusCodes.Paid; pickup.PayTime = DateTime.Now; await PickupOrderRepository.UpdateAsync(pickup); return pickup; } private async Task TryGetPickupNoAsync(int id) { var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id); return pickup?.PickupNo; } /// /// 补承运商并预约(draft → scheduled)。 /// [HttpPost("/order/pickup-order/schedule")] public async Task ScheduleAsync([FromBody] PickupOrderScheduleInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (pickup.Status is not (PickupOrderStatusCodes.Draft or PickupOrderStatusCodes.Scheduled)) { throw new BusinessException("当前状态不可预约下发"); } var carrierService = NormalizeOptional(input.CarrierService); if (carrierService == null) { throw new BusinessException("承运服务不能为空"); } if (string.Equals(pickup.PayMode, PickupPayModeCodes.Prepaid, StringComparison.OrdinalIgnoreCase) && pickup.PayStatus != PickupPayStatusCodes.Paid) { throw new BusinessException("请先支付揽收运费"); } var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0; if (amount <= 0) { throw new BusinessException("请先确认揽收运费"); } pickup.CarrierService = carrierService; pickup.CarrierCode = NormalizeOptional(input.CarrierCode); var carrierWaybillNo = NormalizeOptional(input.CarrierWaybillNo); if (carrierWaybillNo != null) { pickup.CarrierWaybillNo = carrierWaybillNo; } if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default) { pickup.ExpectPickupTime = input.ExpectPickupTime.Value; } pickup.Status = PickupOrderStatusCodes.Scheduled; await PickupOrderRepository.UpdateAsync(pickup); var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); foreach (var line in lines) { var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId); if (order == null) { continue; } order.PickupTime = pickup.ExpectPickupTime; await OrderRepository.UpdateAsync(order); } return await MapOutputAsync(pickup, includeOrders: true); } /// /// 改约(未取件)。 /// [HttpPost("/order/pickup-order/reschedule")] public async Task RescheduleAsync([FromBody] PickupOrderRescheduleInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status)) { throw new BusinessException("当前状态不可改约"); } if (input.ExpectPickupTime == default) { throw new BusinessException("期望揽收时间不能为空"); } pickup.ExpectPickupTime = input.ExpectPickupTime; await PickupOrderRepository.UpdateAsync(pickup); var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); foreach (var line in lines) { var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId); if (order == null) { continue; } order.PickupTime = pickup.ExpectPickupTime; await OrderRepository.UpdateAsync(order); } return await MapOutputAsync(pickup, includeOrders: true); } /// /// 取消揽收单。 /// [HttpPost("/order/pickup-order/cancel")] public async Task CancelAsync([FromBody] PickupOrderIdInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status)) { throw new BusinessException("当前状态不可取消"); } if (pickup.PayStatus == PickupPayStatusCodes.Paid) { throw new BusinessException("已支付揽收单取消请先处理退款(P1 人工)"); } var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); foreach (var line in lines) { await PickupOrderLineRepository.HardDeleteAsync(line); var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId); if (order != null && order.PickupOrderId == pickup.Id) { order.PickupOrderId = null; await OrderRepository.UpdateAsync(order); } } pickup.Status = PickupOrderStatusCodes.Cancelled; pickup.ParcelCount = 0; await PickupOrderRepository.UpdateAsync(pickup); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 标记已取件。 /// [HttpPost("/order/pickup-order/mark-picked")] public async Task MarkPickedAsync([FromBody] PickupOrderIdInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (pickup.Status is not (PickupOrderStatusCodes.Scheduled or PickupOrderStatusCodes.Dispatched)) { throw new BusinessException("仅已预约/已派车状态可标记取件"); } pickup.Status = PickupOrderStatusCodes.Picked; pickup.ActualPickupTime = DateTime.Now; if (pickup.SettlementStatus == PickupSettlementStatusCodes.None) { pickup.SettlementStatus = PickupSettlementStatusCodes.Pending; } await PickupOrderRepository.UpdateAsync(pickup); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 标记已到仓。 /// [HttpPost("/order/pickup-order/mark-inbound")] public async Task MarkInboundAsync([FromBody] PickupOrderIdInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (pickup.Status != PickupOrderStatusCodes.Picked) { throw new BusinessException("仅已取件状态可确认到仓"); } pickup.Status = PickupOrderStatusCodes.Inbound; await PickupOrderRepository.UpdateAsync(pickup); return await MapOutputAsync(pickup, includeOrders: true); } /// /// 供小包创建成功后内部挂载(失败由调用方吞掉并回显)。 /// public async Task CreateForParcelInternalAsync(PickupOrderCreateInput input) { return await CreateAsync(input); } private async Task GetOwnedPickupOrThrowAsync(int id) { if (id <= 0) { throw new BusinessException("揽收单Id无效"); } var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id); if (pickup == null) { throw new BusinessException("揽收单不存在"); } if (OmsSessionHelper.IsCustomerLogin(CurrentUser)) { var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, null); if (pickup.CustomerId != customerId) { throw new BusinessException("揽收单不存在"); } } return pickup; } private static string? ValidateOrderForBind( Order order, int customerId, int warehouseId, int senderAddressId, PickupOrderLine? existingLine) { if (order.CustomerId != customerId) { return "不属于当前客户"; } if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase)) { return "订单已删除"; } if (!BindableOrderStatuses.Contains(order.OrderStatus ?? string.Empty)) { return $"订单状态不可挂载揽收({order.OrderStatus})"; } if (order.WarehouseId != warehouseId) { return "仓库不一致"; } if (existingLine != null) { return existingLine.PickupOrderId == order.PickupOrderId ? "已在当前揽收单中" : $"已绑定其他揽收单(Id={existingLine.PickupOrderId})"; } if (order.PickupOrderId is > 0) { return $"已绑定其他揽收单(Id={order.PickupOrderId})"; } // 已有发件地址快照时要求与揽收同址;尚未绑定时以本次选择为准 if (order.CustomerSenderAddressId is > 0 && order.CustomerSenderAddressId != senderAddressId) { return "取件地址不一致"; } return null; } private async Task RefreshParcelCountAsync(PickupOrder pickup) { var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id); pickup.ParcelCount = lines.Count; await PickupOrderRepository.UpdateAsync(pickup); } /// /// 揽收单预报下游:createOrderOne 请求体全部从 t_pickup_order 组装(不读关联物流单)。 /// [HttpPost("/order/pickup-order/downstream-forward")] public async Task DownstreamForwardAsync([FromBody] PickupOrderIdInput input) { var pickup = await GetOwnedPickupOrThrowAsync(input.Id); if (string.IsNullOrWhiteSpace(pickup.CarrierService)) { throw new BusinessException("请先选择承运服务后再预报下游"); } var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == pickup.WarehouseId); if (warehouse == null) { throw new BusinessException("仓库不存在"); } var (body, _) = await PickupOrderDownstreamCreateOneBodyBuilder.BuildAsync( pickup, warehouse, SalesProductRepository, ServiceChannelRepository); if (string.IsNullOrWhiteSpace(body.ProductCode)) { throw new BusinessException("无法解析下游 productCode,请检查承运服务对应渠道的预报编码"); } var creds = await ResolvePickupDownstreamCredentialsAsync(pickup); var forward = await ItfxForwardClient.ForwardCreateOrderOneAsync( creds.SupplierCode, creds.AppKey, creds.AppSecret, body); if (forward.Success && ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data) && data != null) { var changed = false; var waybill = !string.IsNullOrWhiteSpace(data.TrackNo) ? data.TrackNo.Trim() : data.ServiceNo?.Trim(); if (!string.IsNullOrWhiteSpace(waybill) && !string.Equals(pickup.CarrierWaybillNo, waybill, StringComparison.Ordinal)) { pickup.CarrierWaybillNo = waybill; changed = true; } if (string.IsNullOrWhiteSpace(pickup.CarrierCode) && !string.IsNullOrWhiteSpace(creds.SupplierCode)) { pickup.CarrierCode = creds.SupplierCode; changed = true; } if (changed) { await PickupOrderRepository.UpdateAsync(pickup); } } return new OrderDownstreamForwardResult { Success = forward.Success, SupplierCode = creds.SupplierCode, Url = forward.Url, RequestJson = forward.RequestJson, StatusCode = forward.StatusCode, ResponseBody = forward.ResponseBody, DownstreamServiceNo = pickup.CarrierWaybillNo, TrackNo = pickup.CarrierWaybillNo, }; } /// /// 凭证:优先揽收单 CarrierCode 作为 supplierCode;AppKey/Secret 仍按承运服务→渠道分组解析。 /// private async Task ResolvePickupDownstreamCredentialsAsync(PickupOrder pickup) { var salesProductCode = pickup.CarrierService!.Trim(); var product = await SalesProductRepository.GetByProductCodeAsync(salesProductCode); if (product == null) { throw new BusinessException($"未找到承运服务对应销售产品「{salesProductCode}」"); } var providers = await SalesProductRepository.GetAllocationProvidersAsync(product.Id); var channelId = providers .Where(x => x.ServiceChannelId is > 0) .OrderBy(x => x.SerialNumber) .Select(x => x.ServiceChannelId!.Value) .FirstOrDefault(); if (channelId <= 0) { throw new BusinessException($"销售产品「{salesProductCode}」未配置服务渠道"); } var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId); if (channel == null) { throw new BusinessException("承运服务关联的服务渠道不存在"); } if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0) { throw new BusinessException($"渠道「{channel.ChannelCode}」未关联服务商分组"); } var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == channel.GroupId.Value); if (group == null) { throw new BusinessException("服务商分组不存在或已删除"); } if (group.AppId <= 0 || group.ApiServiceProviderId <= 0) { throw new BusinessException($"服务商「{group.Name}」未配置应用或 API 服务商"); } var appInfo = await ItfxMetadataClient.GetAppAsync(group.AppId); var appKey = appInfo?.AppKey?.Trim(); var appSecret = appInfo?.AppSecret?.Trim(); if (string.IsNullOrWhiteSpace(appKey) || string.IsNullOrWhiteSpace(appSecret)) { throw new BusinessException( "无法获取应用 AppKey/AppSecret,请检查服务商应用配置或「其他配置-API中心服务」"); } var supplierCode = pickup.CarrierCode?.Trim(); if (string.IsNullOrWhiteSpace(supplierCode)) { var sp = await ItfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId); if (sp == null || string.IsNullOrWhiteSpace(sp.Code)) { throw new BusinessException("API 服务商编码(supplierCode)未配置"); } supplierCode = sp.Code.Trim(); } return new OrderDownstreamCredentials { SupplierCode = supplierCode, AppKey = appKey, AppSecret = appSecret }; } private async Task GeneratePickupNoAsync() { for (var i = 0; i < 8; i++) { var no = $"PK{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}"; var existed = await PickupOrderRepository.GetByPickupNoAsync(no); if (existed == null) { return no; } } throw new BusinessException("揽收单号生成失败"); } private async Task MapOutputAsync(PickupOrder entity, bool includeOrders) { string? warehouseName = null; var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == entity.WarehouseId); if (warehouse != null) { warehouseName = warehouse.WarehouseName; } string? customerName = null; var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == entity.CustomerId); if (customer != null) { customerName = customer.CustomerName; } var output = new PickupOrderOutput { Id = entity.Id, PickupNo = entity.PickupNo, RequestId = entity.RequestId, CustomerId = entity.CustomerId, CustomerName = customerName, OrganizationId = entity.OrganizationId, SecretLevel = entity.SecretLevel, SenderAddressId = entity.SenderAddressId, Direction = PickupDirectionCodes.NormalizeOrDefault(entity.Direction), SenderName = entity.SenderName, SenderPhone = entity.SenderPhone, SenderTelePhone = entity.SenderTelePhone, SenderEmail = entity.SenderEmail, SenderCompany = entity.SenderCompany, SenderCountry = entity.SenderCountry, SenderCountryCode = entity.SenderCountryCode, SenderProvince = entity.SenderProvince, SenderCity = entity.SenderCity, SenderZipCode = entity.SenderZipCode, SenderStreetAddress1 = entity.SenderStreetAddress1, SenderStreetAddress2 = entity.SenderStreetAddress2, SenderStreetAddress3 = entity.SenderStreetAddress3, SenderHouseNumber = entity.SenderHouseNumber, WarehouseId = entity.WarehouseId, WarehouseName = warehouseName, CarrierCode = entity.CarrierCode, CarrierService = entity.CarrierService, CarrierWaybillNo = entity.CarrierWaybillNo, ExpectPickupTime = entity.ExpectPickupTime, ActualPickupTime = entity.ActualPickupTime, Status = entity.Status, ParcelCount = entity.ParcelCount, TotalActualWeight = entity.TotalActualWeight, ChargeableWeight = entity.ChargeableWeight, Currency = entity.Currency, FreightAmount = entity.FreightAmount, OtherFeeAmount = entity.OtherFeeAmount, ReceivableAmount = entity.ReceivableAmount, PayStatus = entity.PayStatus, PayTime = entity.PayTime, PayMode = entity.PayMode, CarrierPayableAmount = entity.CarrierPayableAmount, SettlementStatus = entity.SettlementStatus, SettlementBatchNo = entity.SettlementBatchNo, Remark = entity.Remark, CreateTime = entity.CreateTime, UpdateTime = entity.UpdateTime }; if (!includeOrders) { return output; } var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(entity.Id); if (lines.Count == 0) { return output; } var orderIds = lines.Select(x => x.OrderId).ToList(); var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); var orderMap = orders.ToDictionary(x => x.Id); var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds); var weightMap = weights .GroupBy(x => x.OrderId) .ToDictionary( g => g.Key, g => g.OrderByDescending(w => w.Type == OrderWeightTypeCodes.Receive ? 2 : 1) .Select(w => w.ChargeableWeight ?? w.ActualWeight) .FirstOrDefault()); output.Orders = lines .OrderBy(x => x.SortNo) .Select(line => { orderMap.TryGetValue(line.OrderId, out var order); weightMap.TryGetValue(line.OrderId, out var cw); return new PickupOrderLineOutput { OrderId = line.OrderId, ServiceNo = order?.ServiceNo, ClientNo = order?.ClientNo, OrderStatus = order?.OrderStatus, WarehouseId = order?.WarehouseId, ChargeableWeight = cw }; }) .ToList(); return output; } private static string? NormalizeOptional(string? value) { var v = value?.Trim(); return string.IsNullOrEmpty(v) ? null : v; } }