feat: 工作台扩展代发/拣货统计与业务趋势接口
补充 Fulfill/Pickup 工作台统计 DTO 与查询实现,并扩展订单工作台聚合输出。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
ce1349572d
commit
78e06d01b9
@ -0,0 +1,19 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作台代发订单统计。
|
||||||
|
/// </summary>
|
||||||
|
public class FulfillWorkplaceStatsOutput
|
||||||
|
{
|
||||||
|
/// <summary>待绑定包裹/库存。</summary>
|
||||||
|
public int PendingBind { get; set; }
|
||||||
|
|
||||||
|
/// <summary>国内快递在途。</summary>
|
||||||
|
public int DomesticInTransit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已到仓,待仓库拣货。</summary>
|
||||||
|
public int ArrivedPendingPick { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已出库(拣货扣减完成)。</summary>
|
||||||
|
public int OutboundDone { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderWorkplaceBusinessTrendOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 维度:day / week / month。
|
||||||
|
/// </summary>
|
||||||
|
public string Dimension { get; set; } = "day";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 趋势点(按时间正序)。
|
||||||
|
/// </summary>
|
||||||
|
public List<OrderWorkplaceBusinessTrendPointOutput> Points { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个时间桶的三类业务量。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderWorkplaceBusinessTrendPointOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 展示标签。
|
||||||
|
/// </summary>
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运单新增(非仓内代发)。
|
||||||
|
/// </summary>
|
||||||
|
public int WaybillCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 代发订单新增(仓内履约)。
|
||||||
|
/// </summary>
|
||||||
|
public int FulfillCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 揽收单新增。
|
||||||
|
/// </summary>
|
||||||
|
public int PickupCount { get; set; }
|
||||||
|
}
|
||||||
@ -64,4 +64,14 @@ public class OrderWorkplaceStatsOutput
|
|||||||
/// 店铺订单待处理数量(履约状态 pending)。
|
/// 店铺订单待处理数量(履约状态 pending)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int StoreOrderPending { get; set; }
|
public int StoreOrderPending { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 代发订单统计(仓内履约)。
|
||||||
|
/// </summary>
|
||||||
|
public FulfillWorkplaceStatsOutput Fulfill { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 揽收订单统计。
|
||||||
|
/// </summary>
|
||||||
|
public PickupWorkplaceStatsOutput Pickup { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作台揽收订单统计。
|
||||||
|
/// </summary>
|
||||||
|
public class PickupWorkplaceStatsOutput
|
||||||
|
{
|
||||||
|
/// <summary>待支付(有应收且未支付,不含取消/失败)。</summary>
|
||||||
|
public int PendingPayment { get; set; }
|
||||||
|
|
||||||
|
/// <summary>草稿,待确认下发。</summary>
|
||||||
|
public int PendingSchedule { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已预约/已派单,待取件。</summary>
|
||||||
|
public int PendingPickup { get; set; }
|
||||||
|
|
||||||
|
/// <summary>已取件,待到仓。</summary>
|
||||||
|
public int PendingInbound { get; set; }
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ using Oms.Domain.OrdersAggregate.Consts;
|
|||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.OrdersAggregate.Repositories;
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.WarehouseAggregate.Consts;
|
||||||
using Oms.Domain.WarehouseAggregate.Entities;
|
using Oms.Domain.WarehouseAggregate.Entities;
|
||||||
using Oms.Domain.WarehouseAggregate.Repositories;
|
using Oms.Domain.WarehouseAggregate.Repositories;
|
||||||
|
|
||||||
@ -279,6 +280,12 @@ public partial class OrderService
|
|||||||
var order = await GetOrderOrThrowAsync(input.OrderId);
|
var order = await GetOrderOrThrowAsync(input.OrderId);
|
||||||
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
var binds = await OrderStockBindRepository.GetListAsync(x =>
|
||||||
x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound);
|
x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound);
|
||||||
|
if (binds.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("当前订单无已绑定明细,无法确认扣减");
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureStockBindReadyForDeductAsync(binds);
|
||||||
foreach (var bind in binds)
|
foreach (var bind in binds)
|
||||||
{
|
{
|
||||||
await DeductBoundLineAsync(bind);
|
await DeductBoundLineAsync(bind);
|
||||||
@ -906,4 +913,37 @@ public partial class OrderService
|
|||||||
order.UpdateTime = DateTime.Now;
|
order.UpdateTime = DateTime.Now;
|
||||||
await OrderRepository.UpdateAsync(order);
|
await OrderRepository.UpdateAsync(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnsureStockBindReadyForDeductAsync(IReadOnlyList<OrderStockBind> 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} 个国内在途包裹,请先到仓确认后再标记已打包");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,25 @@ public partial class OrderService
|
|||||||
: await SalesStoreOrderRepository.CountAsync(x =>
|
: await SalesStoreOrderRepository.CountAsync(x =>
|
||||||
x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending);
|
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
|
return new OrderWorkplaceStatsOutput
|
||||||
{
|
{
|
||||||
Total = counts.Values.Sum(),
|
Total = counts.Values.Sum(),
|
||||||
@ -79,6 +98,20 @@ public partial class OrderService
|
|||||||
Claimed = GetCount(OrderStatusCodes.Claimed),
|
Claimed = GetCount(OrderStatusCodes.Claimed),
|
||||||
StoreCount = storeCount,
|
StoreCount = storeCount,
|
||||||
StoreOrderPending = storeOrderPending,
|
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
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/order/workplace-business-trend")]
|
||||||
|
public async Task<OrderWorkplaceBusinessTrendOutput> 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<OrderWorkplaceBusinessTrendPointOutput>(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<OrderListFilter> BuildScopedListFilterAsync()
|
private async Task<OrderListFilter> BuildScopedListFilterAsync()
|
||||||
{
|
{
|
||||||
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
|
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
|
||||||
|
|||||||
@ -8,6 +8,27 @@ public interface IPickupOrderRepository : IRepository<PickupOrder>
|
|||||||
Task<PickupOrder?> GetByPickupNoAsync(string pickupNo, CancellationToken cancellationToken = default);
|
Task<PickupOrder?> GetByPickupNoAsync(string pickupNo, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<PickupOrder?> GetOwnedAsync(int id, int customerId, CancellationToken cancellationToken = default);
|
Task<PickupOrder?> GetOwnedAsync(int id, int customerId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<PickupWorkplaceCountRow> GetWorkplaceCountsAsync(
|
||||||
|
int? customerId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> 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<PickupOrderLine>
|
public interface IPickupOrderLineRepository : IRepository<PickupOrderLine>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Fake.DependencyInjection;
|
using Fake.DependencyInjection;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.OrdersAggregate.Repositories;
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
using Oms.SqlSugarCore.Repositories;
|
using Oms.SqlSugarCore.Repositories;
|
||||||
@ -17,6 +18,63 @@ public class PickupOrderRepository : OmsSqlSugarRepository<PickupOrder>, IPickup
|
|||||||
{
|
{
|
||||||
return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken);
|
return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PickupWorkplaceCountRow> GetWorkplaceCountsAsync(
|
||||||
|
int? customerId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var query = db.Queryable<PickupOrder>();
|
||||||
|
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<int> CountByCreateTimeRangeAsync(
|
||||||
|
int? customerId,
|
||||||
|
DateTime startInclusive,
|
||||||
|
DateTime endExclusive,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var query = db.Queryable<PickupOrder>()
|
||||||
|
.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<PickupOrderLine>, IPickupOrderLineRepository, IScopedDependency
|
public class PickupOrderLineRepository : OmsSqlSugarRepository<PickupOrderLine>, IPickupOrderLineRepository, IScopedDependency
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user