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