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(); private ISalesStoreOrderRepository SalesStoreOrderRepository => LazyServiceProvider.GetRequiredService(); /// /// 工作台订单统计概览。 /// [HttpGet("/order/workplace-stats")] public async Task 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, }, }; } /// /// 工作台订单趋势(新增 vs 发货)。 /// [HttpGet("/order/workplace-trend")] public async Task 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(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, }; } /// /// 工作台费用趋势(运费支出 vs 单票均价)。 /// [HttpGet("/order/workplace-spend-trend")] public async Task 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(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, }; } /// /// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。 /// [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( CustomerRepository, CurrentUser, null); var filter = new OrderListFilter { CustomerId = customerId }; await ApplyInternalOrderDataScopeAsync(filter); return filter; } private static List BuildTrendBuckets(string dimension, int count) { var today = DateTime.Today; var buckets = new List(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); }