admin/src/Oms.Application/Services/Orders/OrderService.Workplace.cs
zzh 78e06d01b9 feat: 工作台扩展代发/拣货统计与业务趋势接口
补充 Fulfill/Pickup 工作台统计 DTO 与查询实现,并扩展订单工作台聚合输出。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 10:59:10 +08:00

370 lines
13 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Shared;
using Oms.Domain.OrdersAggregate;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
namespace Oms.Application.Services.Orders;
public partial class OrderService
{
private ISalesStoreRepository SalesStoreRepository =>
LazyServiceProvider.GetRequiredService<ISalesStoreRepository>();
private ISalesStoreOrderRepository SalesStoreOrderRepository =>
LazyServiceProvider.GetRequiredService<ISalesStoreOrderRepository>();
/// <summary>
/// 工作台订单统计概览。
/// </summary>
[HttpGet("/order/workplace-stats")]
public async Task<OrderWorkplaceStatsOutput> GetWorkplaceStatsAsync()
{
var filter = await BuildScopedListFilterAsync();
var rows = await OrderRepository.GetStatusCountsByFilterAsync(filter);
var counts = rows
.Where(x => !string.Equals(x.Status, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
.ToDictionary(x => x.Status, x => x.Count, StringComparer.OrdinalIgnoreCase);
int GetCount(string status) =>
counts.TryGetValue(status, out var value) ? value : 0;
var today = DateTime.Today;
var tomorrow = today.AddDays(1);
var yesterday = today.AddDays(-1);
var todayCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, today, tomorrow);
var yesterdayCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, yesterday, today);
decimal? growth = null;
if (yesterdayCreated > 0)
{
growth = Math.Round((todayCreated - yesterdayCreated) * 100m / yesterdayCreated, 1);
}
else if (todayCreated > 0)
{
growth = 100m;
}
var shipped =
GetCount(OrderStatusCodes.PendingOnline)
+ GetCount(OrderStatusCodes.Shipping)
+ GetCount(OrderStatusCodes.DeliverySuccess)
+ GetCount(OrderStatusCodes.DeliveryFailed);
var customerId = filter.CustomerId;
var storeCount = customerId is > 0
? await SalesStoreRepository.CountAsync(x => x.CustomerId == customerId.Value)
: await SalesStoreRepository.CountAsync(_ => true);
var storeOrderPending = customerId is > 0
? await SalesStoreOrderRepository.CountAsync(x =>
x.CustomerId == customerId.Value
&& x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending)
: 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(),
Counts = counts,
TodayCreated = todayCreated,
YesterdayCreated = yesterdayCreated,
DayOverDayGrowthPercent = growth,
PendingShip = GetCount(OrderStatusCodes.PendingShip),
Shipped = shipped,
PendingPayment = GetCount(OrderStatusCodes.PendingPayment),
Problematic = GetCount(OrderStatusCodes.Problematic),
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,
},
};
}
/// <summary>
/// 工作台订单趋势(新增 vs 发货)。
/// </summary>
[HttpGet("/order/workplace-trend")]
public async Task<OrderWorkplaceTrendOutput> GetWorkplaceTrendAsync(
[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 buckets = BuildTrendBuckets(dim, count);
var points = new List<OrderWorkplaceTrendPointOutput>(buckets.Count);
foreach (var bucket in buckets)
{
var forecast = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(
filter,
bucket.Start,
bucket.EndExclusive);
var shipped = await OrderRepository.CountByFilterAndShipTimeRangeAsync(
filter,
bucket.Start,
bucket.EndExclusive);
points.Add(new OrderWorkplaceTrendPointOutput
{
Label = bucket.Label,
ForecastCount = forecast,
ShippedCount = shipped,
});
}
return new OrderWorkplaceTrendOutput
{
Dimension = dim,
Points = points,
};
}
/// <summary>
/// 工作台费用趋势(运费支出 vs 单票均价)。
/// </summary>
[HttpGet("/order/workplace-spend-trend")]
public async Task<OrderWorkplaceSpendTrendOutput> GetWorkplaceSpendTrendAsync(
[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 buckets = BuildTrendBuckets(dim, count);
var points = new List<OrderWorkplaceSpendTrendPointOutput>(buckets.Count);
foreach (var bucket in buckets)
{
var orderCount = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(
filter,
bucket.Start,
bucket.EndExclusive);
var spendAmount = await OrderRepository.SumPayableAmountByCreateTimeRangeAsync(
filter,
bucket.Start,
bucket.EndExclusive);
spendAmount = Math.Round(spendAmount, 2);
var avg = orderCount > 0
? Math.Round(spendAmount / orderCount, 2)
: 0m;
points.Add(new OrderWorkplaceSpendTrendPointOutput
{
Label = bucket.Label,
OrderCount = orderCount,
SpendAmount = spendAmount,
AvgAmount = avg,
});
}
var totalOrders = points.Sum(x => x.OrderCount);
var totalSpend = Math.Round(points.Sum(x => x.SpendAmount), 2);
var totalAvg = totalOrders > 0
? Math.Round(totalSpend / totalOrders, 2)
: 0m;
return new OrderWorkplaceSpendTrendOutput
{
Dimension = dim,
TotalSpend = totalSpend,
TotalOrders = totalOrders,
AvgAmount = totalAvg,
Points = points,
};
}
/// <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()
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var filter = new OrderListFilter { CustomerId = customerId };
await ApplyInternalOrderDataScopeAsync(filter);
return filter;
}
private static List<TrendBucket> BuildTrendBuckets(string dimension, int count)
{
var today = DateTime.Today;
var buckets = new List<TrendBucket>(count);
if (dimension == "week")
{
var currentWeekStart = GetWeekStart(today);
for (var i = count - 1; i >= 0; i--)
{
var start = currentWeekStart.AddDays(-7 * i);
var end = start.AddDays(7);
var weekNo = global::System.Globalization.ISOWeek.GetWeekOfYear(start);
buckets.Add(new TrendBucket(start, end, $"第{weekNo:D2}周"));
}
return buckets;
}
if (dimension == "month")
{
var currentMonthStart = new DateTime(today.Year, today.Month, 1);
for (var i = count - 1; i >= 0; i--)
{
var start = currentMonthStart.AddMonths(-i);
var end = start.AddMonths(1);
buckets.Add(new TrendBucket(start, end, start.ToString("yyyy-MM")));
}
return buckets;
}
for (var i = count - 1; i >= 0; i--)
{
var start = today.AddDays(-i);
var end = start.AddDays(1);
buckets.Add(new TrendBucket(start, end, start.ToString("MM-dd")));
}
return buckets;
}
private static DateTime GetWeekStart(DateTime date)
{
var day = (int)date.DayOfWeek;
var mondayOffset = day == 0 ? 6 : day - 1;
return date.Date.AddDays(-mondayOffset);
}
private sealed record TrendBucket(DateTime Start, DateTime EndExclusive, string Label);
}