From c6e765e6e2276b259be8053159fde2c18b2ca618 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 16 Oct 2026 09:48:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=AC=E5=91=8A=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E7=94=A8=20mapId=20=E8=A7=A3=E6=9E=90=E5=BD=93=E5=89=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=EF=BC=8C=E7=AE=A1=E7=90=86=E7=AB=AF=E5=8F=AF?= =?UTF-8?q?=E6=9F=A5=E5=85=A8=E9=83=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同步监控 overview 支持日周月经营指标与积压队列。 Co-authored-by: Cursor --- .../Dtos/Monitor/MonitorOverviewOutput.cs | 65 ++++++++- .../Dtos/System/NoticeListInput.cs | 5 + .../Services/Orders/OrderService.Monitor.cs | 129 +++++++++++++++--- .../Services/System/NoticeService.cs | 36 +++-- .../Shared/OmsSessionHelper.cs | 37 +++-- 5 files changed, 228 insertions(+), 44 deletions(-) diff --git a/src/Oms.Application/Dtos/Monitor/MonitorOverviewOutput.cs b/src/Oms.Application/Dtos/Monitor/MonitorOverviewOutput.cs index a8a9138..6f03ec8 100644 --- a/src/Oms.Application/Dtos/Monitor/MonitorOverviewOutput.cs +++ b/src/Oms.Application/Dtos/Monitor/MonitorOverviewOutput.cs @@ -10,12 +10,32 @@ public class MonitorOverviewOutput public MonitorCustomerStatsOutput Customer { get; set; } = new(); /// - /// 近 7 日新增订单趋势。 + /// 经营指标(订单量/营收/毛利)按日周月维度。 + /// + public MonitorOpsStatsOutput Ops { get; set; } = new(); + + /// + /// 实时积压队列(不随维度变化)。 + /// + public MonitorQueueStatsOutput Queue { get; set; } = new(); + + /// + /// 订单趋势(随 dimension 变化;兼容字段名 OrderTrend7d 在 day 时等同本字段)。 + /// + public List OrderTrend { get; set; } = []; + + /// + /// 近 7 日新增订单趋势(兼容旧前端;dimension=day 时与 OrderTrend 相同)。 /// public List OrderTrend7d { get; set; } = []; /// - /// 近 7 日营收/毛利趋势。 + /// 营收/毛利趋势(随 dimension 变化)。 + /// + public List RevenueTrend { get; set; } = []; + + /// + /// 近 7 日营收/毛利趋势(兼容旧前端;dimension=day 时与 RevenueTrend 相同)。 /// public List RevenueTrend7d { get; set; } = []; @@ -40,6 +60,47 @@ public class MonitorOverviewOutput public MonitorTrackRatesOutput TrackRates { get; set; } = new(); } +/// +/// 经营 KPI:订单量 / 营收 / 毛利(随日周月)。 +/// +public class MonitorOpsStatsOutput +{ + /// day / week / month + public string Dimension { get; set; } = "day"; + + /// 当前周期新建订单数。 + public int OrderCount { get; set; } + + /// 订单量环比增长百分比。 + public decimal? OrderCountGrowthPercent { get; set; } + + /// 当前周期应收(已付)营收。 + public decimal Revenue { get; set; } + + /// 营收环比增长百分比。 + public decimal? RevenueGrowthPercent { get; set; } + + /// 当前周期毛利 = 应收 - 应付。 + public decimal GrossProfit { get; set; } + + /// 毛利环比增长百分比。 + public decimal? GrossProfitGrowthPercent { get; set; } +} + +/// +/// 实时积压快照(不受日周月影响)。 +/// +public class MonitorQueueStatsOutput +{ + public int PendingPayment { get; set; } + + public int PendingOnline { get; set; } + + public int PendingSign { get; set; } + + public int Problematic { get; set; } +} + public class MonitorOrderStatsOutput { public int TodayCreated { get; set; } diff --git a/src/Oms.Application/Dtos/System/NoticeListInput.cs b/src/Oms.Application/Dtos/System/NoticeListInput.cs index 4707ffa..51f5af1 100644 --- a/src/Oms.Application/Dtos/System/NoticeListInput.cs +++ b/src/Oms.Application/Dtos/System/NoticeListInput.cs @@ -34,4 +34,9 @@ public class NoticeListInput /// 仅未读(相对当前登录主体)。 /// public bool? OnlyUnread { get; set; } + + /// + /// 管理端查看全部通知(仅非客户登录生效)。默认按当前主体可见范围过滤。 + /// + public bool? Manage { get; set; } } diff --git a/src/Oms.Application/Services/Orders/OrderService.Monitor.cs b/src/Oms.Application/Services/Orders/OrderService.Monitor.cs index ac443f2..f5a04fc 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Monitor.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Monitor.cs @@ -12,9 +12,12 @@ public partial class OrderService /// /// 实时监控页概览(订单/客户统计与趋势)。 /// + /// 经营指标维度:day / week / month,默认 day。 [HttpGet("/monitor/overview")] - public async Task GetMonitorOverviewAsync() + public async Task GetMonitorOverviewAsync( + [FromQuery] string? dimension = "day") { + var dim = NormalizeMonitorDimension(dimension); var filter = await BuildScopedListFilterAsync(); var today = DateTime.Today; var tomorrow = today.AddDays(1); @@ -28,21 +31,60 @@ public partial class OrderService int GetCount(string status) => counts.TryGetValue(status, out var value) ? value : 0; - var todayCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, today, tomorrow); - var yesterdayCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, yesterday, today); + var pendingPayment = GetCount(OrderStatusCodes.PendingPayment); + var pendingOnline = GetCount(OrderStatusCodes.PendingOnline); + var pendingSign = GetCount(OrderStatusCodes.Shipping); + var problematic = GetCount(OrderStatusCodes.Problematic); + + var (currentStart, currentEndExclusive, previousStart, previousEndExclusive) = + ResolveOpsPeriodRanges(dim, today, tomorrow); + + var orderCount = await OrderRepository.CountByFilterAndCreateTimeRangeAsync( + filter, currentStart, currentEndExclusive); + var previousOrderCount = await OrderRepository.CountByFilterAndCreateTimeRangeAsync( + filter, previousStart, previousEndExclusive); + + var revenue = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Receivable, currentStart, currentEndExclusive); + var previousRevenue = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Receivable, previousStart, previousEndExclusive); + + var payable = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Payable, currentStart, currentEndExclusive); + var previousPayable = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Payable, previousStart, previousEndExclusive); + var grossProfit = revenue - payable; + var previousGrossProfit = previousRevenue - previousPayable; + + // 兼容旧字段:始终提供「今日」口径 + var todayCreated = dim == "day" + ? orderCount + : await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, today, tomorrow); + var yesterdayCreated = dim == "day" + ? previousOrderCount + : await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, yesterday, today); + var todayRevenue = dim == "day" + ? revenue + : await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Receivable, today, tomorrow); + var yesterdayRevenue = dim == "day" + ? previousRevenue + : await OrderRepository.SumFeeAmountByPayTimeRangeAsync( + filter, OrderFeeTypeCodes.Receivable, yesterday, today); var monthStart = new DateTime(today.Year, today.Month, 1); var lastMonthStart = monthStart.AddMonths(-1); var thisMonthCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, monthStart, tomorrow); var lastMonthCreated = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(filter, lastMonthStart, monthStart); - var todayRevenue = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( - filter, OrderFeeTypeCodes.Receivable, today, tomorrow); - var yesterdayRevenue = await OrderRepository.SumFeeAmountByPayTimeRangeAsync( - filter, OrderFeeTypeCodes.Receivable, yesterday, today); - - var orderTrend7d = await BuildMonitorOrderTrendAsync(filter, 7); - var revenueTrend7d = await BuildMonitorRevenueTrendAsync(filter, 7); + var trendCount = dim switch + { + "week" => 8, + "month" => 6, + _ => 7, + }; + var orderTrend = await BuildMonitorOrderTrendAsync(filter, dim, trendCount); + var revenueTrend = await BuildMonitorRevenueTrendAsync(filter, dim, trendCount); var recentFilter = CloneFilterWithCreateTimeStart(filter, today.AddDays(-90)); var recentOrders = await OrderRepository.GetFilteredListAsync(recentFilter); @@ -83,13 +125,30 @@ public partial class OrderService TodayCreatedGrowthPercent = CalcGrowthPercent(todayCreated, yesterdayCreated), Total = counts.Values.Sum(), TotalMonthGrowthPercent = CalcGrowthPercent(thisMonthCreated, lastMonthCreated), - PendingPayment = GetCount(OrderStatusCodes.PendingPayment), - PendingOnline = GetCount(OrderStatusCodes.PendingOnline), - PendingSign = GetCount(OrderStatusCodes.Shipping), - Problematic = GetCount(OrderStatusCodes.Problematic), + PendingPayment = pendingPayment, + PendingOnline = pendingOnline, + PendingSign = pendingSign, + Problematic = problematic, TodayRevenue = todayRevenue, TodayRevenueGrowthPercent = CalcGrowthPercentDecimal(todayRevenue, yesterdayRevenue), }, + Ops = new MonitorOpsStatsOutput + { + Dimension = dim, + OrderCount = orderCount, + OrderCountGrowthPercent = CalcGrowthPercent(orderCount, previousOrderCount), + Revenue = revenue, + RevenueGrowthPercent = CalcGrowthPercentDecimal(revenue, previousRevenue), + GrossProfit = grossProfit, + GrossProfitGrowthPercent = CalcGrowthPercentDecimal(grossProfit, previousGrossProfit), + }, + Queue = new MonitorQueueStatsOutput + { + PendingPayment = pendingPayment, + PendingOnline = pendingOnline, + PendingSign = pendingSign, + Problematic = problematic, + }, Customer = new MonitorCustomerStatsOutput { TotalCustomers = totalCustomers, @@ -100,8 +159,10 @@ public partial class OrderService InactiveLogin7d = inactiveLogin7d, InactiveLogin7dDelta = inactiveLogin7d - inactiveLogin14d, }, - OrderTrend7d = orderTrend7d, - RevenueTrend7d = revenueTrend7d, + OrderTrend = orderTrend, + OrderTrend7d = dim == "day" ? orderTrend : await BuildMonitorOrderTrendAsync(filter, "day", 7), + RevenueTrend = revenueTrend, + RevenueTrend7d = dim == "day" ? revenueTrend : await BuildMonitorRevenueTrendAsync(filter, "day", 7), RegionOrderTrend = new MonitorRegionTrendBundleOutput { Week = new MonitorRegionTrendPeriodOutput { Labels = weekOrderLabels, Series = weekOrderSeries }, @@ -117,11 +178,42 @@ public partial class OrderService }; } + private static string NormalizeMonitorDimension(string? dimension) + { + var dim = (dimension ?? "day").Trim().ToLowerInvariant(); + return dim is "day" or "week" or "month" ? dim : "day"; + } + + /// + /// 当前周期(统计到今天结束)与上一完整同尺度周期。 + /// + private static (DateTime CurrentStart, DateTime CurrentEndExclusive, DateTime PreviousStart, DateTime PreviousEndExclusive) + ResolveOpsPeriodRanges(string dimension, DateTime today, DateTime tomorrow) + { + if (dimension == "week") + { + var weekStart = GetWeekStart(today); + var previousWeekStart = weekStart.AddDays(-7); + return (weekStart, tomorrow, previousWeekStart, weekStart); + } + + if (dimension == "month") + { + var monthStart = new DateTime(today.Year, today.Month, 1); + var previousMonthStart = monthStart.AddMonths(-1); + return (monthStart, tomorrow, previousMonthStart, monthStart); + } + + var yesterday = today.AddDays(-1); + return (today, tomorrow, yesterday, today); + } + private async Task> BuildMonitorOrderTrendAsync( OrderListFilter filter, + string dimension, int count) { - var buckets = BuildTrendBuckets("day", count); + var buckets = BuildTrendBuckets(dimension, count); var points = new List(buckets.Count); foreach (var bucket in buckets) { @@ -135,9 +227,10 @@ public partial class OrderService private async Task> BuildMonitorRevenueTrendAsync( OrderListFilter filter, + string dimension, int count) { - var buckets = BuildTrendBuckets("day", count); + var buckets = BuildTrendBuckets(dimension, count); var points = new List(buckets.Count); foreach (var bucket in buckets) { diff --git a/src/Oms.Application/Services/System/NoticeService.cs b/src/Oms.Application/Services/System/NoticeService.cs index ca82fe3..f01b26d 100644 --- a/src/Oms.Application/Services/System/NoticeService.cs +++ b/src/Oms.Application/Services/System/NoticeService.cs @@ -49,15 +49,16 @@ public class NoticeService : ApplicationService [HttpGet("/system/notice/list")] public async Task> GetListAsync([FromQuery] NoticeListInput input) { - var receiverUserId = CurrentUser.GetId(); + var receiverUserId = OmsSessionHelper.ResolveMapUserId(CurrentUser); if (receiverUserId <= 0) { return new OmsPageResult { Items = [], Total = 0 }; } - var expectedReceiverType = OmsSessionHelper.IsCustomerLogin(CurrentUser) - ? ReceiverTypeCustomer - : ReceiverTypeUser; + var isCustomer = OmsSessionHelper.IsCustomerLogin(CurrentUser); + var expectedReceiverType = isCustomer ? ReceiverTypeCustomer : ReceiverTypeUser; + // 管理端公告页:管理员可看全部;工作台/H5/未读仍按接收范围过滤 + var manageAll = input.Manage == true && !isCustomer; var myReadNoticeIds = (await NoticeReadRepository.GetListAsync(x => x.ReceiverType == expectedReceiverType && x.ReceiverId == receiverUserId)) @@ -65,11 +66,13 @@ public class NoticeService : ApplicationService .Distinct() .ToList(); - var predicate = BuildListPredicate( - input, - receiverUserId, - expectedReceiverType, - input.OnlyUnread == true ? myReadNoticeIds : null); + var predicate = manageAll + ? BuildManageListPredicate(input) + : BuildListPredicate( + input, + receiverUserId, + expectedReceiverType, + input.OnlyUnread == true ? myReadNoticeIds : null); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var notices = await NoticeRepository.GetPagedListAsync( @@ -197,7 +200,7 @@ public class NoticeService : ApplicationService var currentReceiverType = OmsSessionHelper.IsCustomerLogin(CurrentUser) ? ReceiverTypeCustomer : ReceiverTypeUser; - var currentReceiverId = CurrentUser.GetId(); + var currentReceiverId = OmsSessionHelper.ResolveMapUserId(CurrentUser); if (currentReceiverId <= 0) { throw new BusinessException("当前登录主体无效"); @@ -349,6 +352,19 @@ public class NoticeService : ApplicationService return result; } + private static Expression> BuildManageListPredicate(NoticeListInput input) + { + var noticeType = input.NoticeType?.Trim(); + var receiverType = input.ReceiverType?.Trim(); + var keyword = input.Keyword?.Trim(); + return x => + (string.IsNullOrWhiteSpace(noticeType) || x.NoticeType == noticeType) && + (string.IsNullOrWhiteSpace(receiverType) || x.ReceiverType == receiverType) && + (string.IsNullOrWhiteSpace(keyword) || + x.Title.Contains(keyword) || + x.Content.Contains(keyword)); + } + private static Expression> BuildListPredicate( NoticeListInput input, int receiverUserId, diff --git a/src/Oms.Application/Shared/OmsSessionHelper.cs b/src/Oms.Application/Shared/OmsSessionHelper.cs index 3b603fd..c5ff2b4 100644 --- a/src/Oms.Application/Shared/OmsSessionHelper.cs +++ b/src/Oms.Application/Shared/OmsSessionHelper.cs @@ -43,6 +43,27 @@ public static class OmsSessionHelper return string.Equals(role, CustomerRoleName, StringComparison.OrdinalIgnoreCase); } + /// + /// 解析当前会话的 RBAC mapId(user.userid)。 + /// JWT nameidentifier 多为 Guid,GetId<int> 常为 0,需回退读 mapId claim。 + /// + public static int ResolveMapUserId(ICurrentUser currentUser) + { + var userId = currentUser.GetId(); + if (userId > 0) + { + return userId; + } + + var mapIdRaw = currentUser.FindClaimOrNull(MapIdClaim)?.Value; + if (int.TryParse(mapIdRaw, out var mapId) && mapId > 0) + { + return mapId; + } + + return 0; + } + /// /// 客户登录时返回 mapId(对应 t_customer.UserId / 订单 CreateUserId);管理员返回 null。 /// @@ -53,20 +74,8 @@ public static class OmsSessionHelper return null; } - var userId = currentUser.GetId(); - if (userId > 0) - { - return userId; - } - - // GetId 未绑到 mapId 时,直接读 claim,避免客户会话被当成管理员(不过滤) - var mapIdRaw = currentUser.FindClaimOrNull(MapIdClaim)?.Value; - if (int.TryParse(mapIdRaw, out var mapId) && mapId > 0) - { - return mapId; - } - - return null; + var mapId = ResolveMapUserId(currentUser); + return mapId > 0 ? mapId : null; } ///