From 78e06d01b9ce78af1ac0c554762aa7c126795f48 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 28 Aug 2026 10:59:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E4=BD=9C=E5=8F=B0=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E4=BB=A3=E5=8F=91/=E6=8B=A3=E8=B4=A7=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E4=B8=8E=E4=B8=9A=E5=8A=A1=E8=B6=8B=E5=8A=BF=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充 Fulfill/Pickup 工作台统计 DTO 与查询实现,并扩展订单工作台聚合输出。 Co-authored-by: Cursor --- .../Orders/FulfillWorkplaceStatsOutput.cs | 19 ++++ .../OrderWorkplaceBusinessTrendOutput.cs | 43 +++++++ .../Dtos/Orders/OrderWorkplaceStatsOutput.cs | 10 ++ .../Dtos/Orders/PickupWorkplaceStatsOutput.cs | 19 ++++ .../Services/Orders/OrderService.StockBind.cs | 40 +++++++ .../Services/Orders/OrderService.Workplace.cs | 106 ++++++++++++++++++ .../Repositories/IPickupOrderRepository.cs | 21 ++++ .../Orders/PickupOrderRepository.cs | 58 ++++++++++ 8 files changed, 316 insertions(+) create mode 100644 src/Oms.Application/Dtos/Orders/FulfillWorkplaceStatsOutput.cs create mode 100644 src/Oms.Application/Dtos/Orders/OrderWorkplaceBusinessTrendOutput.cs create mode 100644 src/Oms.Application/Dtos/Orders/PickupWorkplaceStatsOutput.cs diff --git a/src/Oms.Application/Dtos/Orders/FulfillWorkplaceStatsOutput.cs b/src/Oms.Application/Dtos/Orders/FulfillWorkplaceStatsOutput.cs new file mode 100644 index 0000000..e269cb6 --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/FulfillWorkplaceStatsOutput.cs @@ -0,0 +1,19 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 工作台代发订单统计。 +/// +public class FulfillWorkplaceStatsOutput +{ + /// 待绑定包裹/库存。 + public int PendingBind { get; set; } + + /// 国内快递在途。 + public int DomesticInTransit { get; set; } + + /// 已到仓,待仓库拣货。 + public int ArrivedPendingPick { get; set; } + + /// 已出库(拣货扣减完成)。 + public int OutboundDone { get; set; } +} diff --git a/src/Oms.Application/Dtos/Orders/OrderWorkplaceBusinessTrendOutput.cs b/src/Oms.Application/Dtos/Orders/OrderWorkplaceBusinessTrendOutput.cs new file mode 100644 index 0000000..6c05a99 --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/OrderWorkplaceBusinessTrendOutput.cs @@ -0,0 +1,43 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。 +/// +public class OrderWorkplaceBusinessTrendOutput +{ + /// + /// 维度:day / week / month。 + /// + public string Dimension { get; set; } = "day"; + + /// + /// 趋势点(按时间正序)。 + /// + public List Points { get; set; } = []; +} + +/// +/// 单个时间桶的三类业务量。 +/// +public class OrderWorkplaceBusinessTrendPointOutput +{ + /// + /// 展示标签。 + /// + public string Label { get; set; } = string.Empty; + + /// + /// 运单新增(非仓内代发)。 + /// + public int WaybillCount { get; set; } + + /// + /// 代发订单新增(仓内履约)。 + /// + public int FulfillCount { get; set; } + + /// + /// 揽收单新增。 + /// + public int PickupCount { get; set; } +} diff --git a/src/Oms.Application/Dtos/Orders/OrderWorkplaceStatsOutput.cs b/src/Oms.Application/Dtos/Orders/OrderWorkplaceStatsOutput.cs index a852b54..4e963fb 100644 --- a/src/Oms.Application/Dtos/Orders/OrderWorkplaceStatsOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderWorkplaceStatsOutput.cs @@ -64,4 +64,14 @@ public class OrderWorkplaceStatsOutput /// 店铺订单待处理数量(履约状态 pending)。 /// public int StoreOrderPending { get; set; } + + /// + /// 代发订单统计(仓内履约)。 + /// + public FulfillWorkplaceStatsOutput Fulfill { get; set; } = new(); + + /// + /// 揽收订单统计。 + /// + public PickupWorkplaceStatsOutput Pickup { get; set; } = new(); } diff --git a/src/Oms.Application/Dtos/Orders/PickupWorkplaceStatsOutput.cs b/src/Oms.Application/Dtos/Orders/PickupWorkplaceStatsOutput.cs new file mode 100644 index 0000000..4145095 --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/PickupWorkplaceStatsOutput.cs @@ -0,0 +1,19 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 工作台揽收订单统计。 +/// +public class PickupWorkplaceStatsOutput +{ + /// 待支付(有应收且未支付,不含取消/失败)。 + public int PendingPayment { get; set; } + + /// 草稿,待确认下发。 + public int PendingSchedule { get; set; } + + /// 已预约/已派单,待取件。 + public int PendingPickup { get; set; } + + /// 已取件,待到仓。 + public int PendingInbound { get; set; } +} diff --git a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs index 09a3ae2..d8f5150 100644 --- a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs +++ b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs @@ -8,6 +8,7 @@ using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Entities; +using Oms.Domain.WarehouseAggregate.Consts; using Oms.Domain.WarehouseAggregate.Entities; using Oms.Domain.WarehouseAggregate.Repositories; @@ -279,6 +280,12 @@ public partial class OrderService var order = await GetOrderOrThrowAsync(input.OrderId); var binds = await OrderStockBindRepository.GetListAsync(x => x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound); + if (binds.Count == 0) + { + throw new BusinessException("当前订单无已绑定明细,无法确认扣减"); + } + + await EnsureStockBindReadyForDeductAsync(binds); foreach (var bind in binds) { await DeductBoundLineAsync(bind); @@ -906,4 +913,37 @@ public partial class OrderService order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); } + + private async Task EnsureStockBindReadyForDeductAsync(IReadOnlyList binds) + { + var parcelIds = binds + .Where(x => x.ParcelId is > 0) + .Select(x => x.ParcelId!.Value) + .Distinct() + .ToArray(); + if (parcelIds.Length == 0) + { + return; + } + + var parcels = await StockBindParcelRepository.GetListAsync(x => parcelIds.Contains(x.Id)); + var inTransitNos = parcels + .Where(x => string.Equals( + x.LifecycleStatus, + WarehouseParcelLifecycleCodes.InTransit, + StringComparison.OrdinalIgnoreCase)) + .Select(x => x.TrackingNo?.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + if (inTransitNos.Length == 0) + { + return; + } + + var sample = inTransitNos[0]; + throw new BusinessException(inTransitNos.Length == 1 + ? $"包裹国内在途({sample}),请先到仓确认后再标记已打包" + : $"存在 {inTransitNos.Length} 个国内在途包裹,请先到仓确认后再标记已打包"); + } } diff --git a/src/Oms.Application/Services/Orders/OrderService.Workplace.cs b/src/Oms.Application/Services/Orders/OrderService.Workplace.cs index 0b05e2b..2bc46b5 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Workplace.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Workplace.cs @@ -65,6 +65,25 @@ public partial class OrderService : await SalesStoreOrderRepository.CountAsync(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending); + var fulfillFilter = new OrderListFilter + { + CustomerId = filter.CustomerId, + FulfillmentType = FulfillmentTypeCodes.Warehouse, + ScopeOrganizationIds = filter.ScopeOrganizationIds, + ScopeSecretLevels = filter.ScopeSecretLevels, + ScopeDataScope = filter.ScopeDataScope, + ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId, + ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount, + ScopeOwnerMapIds = filter.ScopeOwnerMapIds, + ScopeBizFilters = filter.ScopeBizFilters, + ScopeCustomFilters = filter.ScopeCustomFilters, + }; + var fulfillCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(fulfillFilter); + int FulfillCount(string key) => + fulfillCounts.TryGetValue(key, out var value) ? value : 0; + + var pickupCounts = await PickupOrderRepository.GetWorkplaceCountsAsync(filter.CustomerId); + return new OrderWorkplaceStatsOutput { Total = counts.Values.Sum(), @@ -79,6 +98,20 @@ public partial class OrderService Claimed = GetCount(OrderStatusCodes.Claimed), StoreCount = storeCount, StoreOrderPending = storeOrderPending, + Fulfill = new FulfillWorkplaceStatsOutput + { + PendingBind = FulfillCount(FulfillmentProgressCodes.PendingBind), + DomesticInTransit = FulfillCount(FulfillmentProgressCodes.DomesticInTransit), + ArrivedPendingPick = FulfillCount(FulfillmentProgressCodes.ArrivedPendingPick), + OutboundDone = FulfillCount(FulfillmentProgressCodes.OutboundDone), + }, + Pickup = new PickupWorkplaceStatsOutput + { + PendingPayment = pickupCounts.PendingPayment, + PendingSchedule = pickupCounts.PendingSchedule, + PendingPickup = pickupCounts.PendingPickup, + PendingInbound = pickupCounts.PendingInbound, + }, }; } @@ -198,6 +231,79 @@ public partial class OrderService }; } + /// + /// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。 + /// + [HttpGet("/order/workplace-business-trend")] + public async Task GetWorkplaceBusinessTrendAsync( + [FromQuery] string? dimension = "day", + [FromQuery] int count = 7) + { + var dim = (dimension ?? "day").Trim().ToLowerInvariant(); + if (dim is not ("day" or "week" or "month")) + { + dim = "day"; + } + + count = dim switch + { + "week" => Math.Clamp(count, 1, 12), + "month" => Math.Clamp(count, 1, 12), + _ => Math.Clamp(count, 1, 31), + }; + + var filter = await BuildScopedListFilterAsync(); + var fulfillFilter = CloneScopedFilter(filter); + fulfillFilter.FulfillmentType = FulfillmentTypeCodes.Warehouse; + + var buckets = BuildTrendBuckets(dim, count); + var points = new List(buckets.Count); + + foreach (var bucket in buckets) + { + var totalOrders = await OrderRepository.CountByFilterAndCreateTimeRangeAsync( + filter, + bucket.Start, + bucket.EndExclusive); + var fulfillOrders = await OrderRepository.CountByFilterAndCreateTimeRangeAsync( + fulfillFilter, + bucket.Start, + bucket.EndExclusive); + var pickupOrders = await PickupOrderRepository.CountByCreateTimeRangeAsync( + filter.CustomerId, + bucket.Start, + bucket.EndExclusive); + + points.Add(new OrderWorkplaceBusinessTrendPointOutput + { + Label = bucket.Label, + WaybillCount = Math.Max(0, totalOrders - fulfillOrders), + FulfillCount = fulfillOrders, + PickupCount = pickupOrders, + }); + } + + return new OrderWorkplaceBusinessTrendOutput + { + Dimension = dim, + Points = points, + }; + } + + private static OrderListFilter CloneScopedFilter(OrderListFilter filter) => + new() + { + CustomerId = filter.CustomerId, + ScopeOrganizationIds = filter.ScopeOrganizationIds, + ScopeSecretLevels = filter.ScopeSecretLevels, + ScopeDataScope = filter.ScopeDataScope, + ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId, + ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount, + ScopeOwnerMapIds = filter.ScopeOwnerMapIds, + ScopeBizFilters = filter.ScopeBizFilters, + ScopeCustomFilters = filter.ScopeCustomFilters, + }; + private async Task BuildScopedListFilterAsync() { var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( diff --git a/src/Oms.Domain/OrdersAggregate/Repositories/IPickupOrderRepository.cs b/src/Oms.Domain/OrdersAggregate/Repositories/IPickupOrderRepository.cs index 143c38d..55b9cc8 100644 --- a/src/Oms.Domain/OrdersAggregate/Repositories/IPickupOrderRepository.cs +++ b/src/Oms.Domain/OrdersAggregate/Repositories/IPickupOrderRepository.cs @@ -8,6 +8,27 @@ public interface IPickupOrderRepository : IRepository Task GetByPickupNoAsync(string pickupNo, CancellationToken cancellationToken = default); Task GetOwnedAsync(int id, int customerId, CancellationToken cancellationToken = default); + + Task GetWorkplaceCountsAsync( + int? customerId, + CancellationToken cancellationToken = default); + + Task CountByCreateTimeRangeAsync( + int? customerId, + DateTime startInclusive, + DateTime endExclusive, + CancellationToken cancellationToken = default); +} + +public sealed class PickupWorkplaceCountRow +{ + public int PendingPayment { get; set; } + + public int PendingSchedule { get; set; } + + public int PendingPickup { get; set; } + + public int PendingInbound { get; set; } } public interface IPickupOrderLineRepository : IRepository diff --git a/src/Oms.SqlSugarCore/Orders/PickupOrderRepository.cs b/src/Oms.SqlSugarCore/Orders/PickupOrderRepository.cs index 9e10219..a898e0b 100644 --- a/src/Oms.SqlSugarCore/Orders/PickupOrderRepository.cs +++ b/src/Oms.SqlSugarCore/Orders/PickupOrderRepository.cs @@ -1,4 +1,5 @@ using Fake.DependencyInjection; +using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.SqlSugarCore.Repositories; @@ -17,6 +18,63 @@ public class PickupOrderRepository : OmsSqlSugarRepository, IPickup { return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken); } + + public async Task GetWorkplaceCountsAsync( + int? customerId, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + var query = db.Queryable(); + if (customerId is > 0) + { + query = query.Where(x => x.CustomerId == customerId.Value); + } + + var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed }; + + var pendingPayment = await query + .Where(x => !terminal.Contains(x.Status)) + .Where(x => x.PayStatus != PickupPayStatusCodes.Paid) + .Where(x => x.ReceivableAmount != null && x.ReceivableAmount > 0) + .CountAsync(cancellationToken); + + var pendingSchedule = await query + .Where(x => x.Status == PickupOrderStatusCodes.Draft) + .CountAsync(cancellationToken); + + var pendingPickup = await query + .Where(x => x.Status == PickupOrderStatusCodes.Scheduled || x.Status == PickupOrderStatusCodes.Dispatched) + .CountAsync(cancellationToken); + + var pendingInbound = await query + .Where(x => x.Status == PickupOrderStatusCodes.Picked) + .CountAsync(cancellationToken); + + return new PickupWorkplaceCountRow + { + PendingPayment = pendingPayment, + PendingSchedule = pendingSchedule, + PendingPickup = pendingPickup, + PendingInbound = pendingInbound, + }; + } + + public async Task CountByCreateTimeRangeAsync( + int? customerId, + DateTime startInclusive, + DateTime endExclusive, + CancellationToken cancellationToken = default) + { + var db = await GetDbContextAsync(cancellationToken); + var query = db.Queryable() + .Where(x => x.CreateTime >= startInclusive && x.CreateTime < endExclusive); + if (customerId is > 0) + { + query = query.Where(x => x.CustomerId == customerId.Value); + } + + return await query.CountAsync(cancellationToken); + } } public class PickupOrderLineRepository : OmsSqlSugarRepository, IPickupOrderLineRepository, IScopedDependency