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 1/2] =?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; } /// From 7a0affe2d48e3eb70e0cb70c077b146cd8ac994d Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 16 Oct 2026 09:57:25 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=85=AC=E5=91=8A=E5=B9=BF=E6=92=AD?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E6=80=A7=E6=94=B9=E4=B8=BA=E5=8F=8D=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=E5=88=A4=E5=AE=9A=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=20JSON=20=E5=88=97=E6=AF=94=E8=BE=83=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ReceiverIds 改为 text 存储并补充迁移;广播对客户/收件箱可见。 Co-authored-by: Cursor --- db.sql | 2 +- sql/migrate_notice_receiver_ids_text.sql | 25 +++++++ .../Services/System/NoticeService.cs | 69 ++++++++----------- .../SystemAggregate/Entities/Notice.cs | 4 ++ 4 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 sql/migrate_notice_receiver_ids_text.sql diff --git a/db.sql b/db.sql index 3f1111e..2661036 100644 --- a/db.sql +++ b/db.sql @@ -1220,7 +1220,7 @@ CREATE TABLE `t_notice` ( `Content` TEXT NOT NULL COMMENT '通知内容', `IsPopup` TINYINT NOT NULL DEFAULT 0 COMMENT '是否弹窗:0-否,1-是', `ReceiverType` VARCHAR(16) NOT NULL COMMENT '接收对象:user/customer', - `ReceiverIds` JSON NOT NULL COMMENT '接收对象ID列表(JSON数组)', + `ReceiverIds` TEXT NOT NULL COMMENT '接收对象ID列表(JSON数组字符串)', `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', diff --git a/sql/migrate_notice_receiver_ids_text.sql b/sql/migrate_notice_receiver_ids_text.sql new file mode 100644 index 0000000..d4bcd97 --- /dev/null +++ b/sql/migrate_notice_receiver_ids_text.sql @@ -0,0 +1,25 @@ +-- ============================================================================= +-- migrate_notice_receiver_ids_text.sql +-- t_notice.ReceiverIds:JSON → TEXT,保证广播([])与定向名单可用字符串条件查询 +-- 可重复执行 +-- ============================================================================= +SET @db := DATABASE(); + +SET @col_type := ( + SELECT DATA_TYPE + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db + AND TABLE_NAME = 't_notice' + AND COLUMN_NAME = 'ReceiverIds' + LIMIT 1 +); + +SET @sql := IF( + @col_type IS NOT NULL AND LOWER(IFNULL(@col_type, '')) = 'json', + 'ALTER TABLE `t_notice` + MODIFY COLUMN `ReceiverIds` TEXT NOT NULL COMMENT ''接收对象ID列表(JSON数组字符串)''', + 'SELECT ''t_notice.ReceiverIds already text or missing'' AS info' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/src/Oms.Application/Services/System/NoticeService.cs b/src/Oms.Application/Services/System/NoticeService.cs index f01b26d..e3238b6 100644 --- a/src/Oms.Application/Services/System/NoticeService.cs +++ b/src/Oms.Application/Services/System/NoticeService.cs @@ -65,22 +65,25 @@ public class NoticeService : ApplicationService .Select(x => x.NoticeId) .Distinct() .ToList(); + var myReadIdSet = myReadNoticeIds.ToHashSet(); - var predicate = manageAll - ? BuildManageListPredicate(input) - : BuildListPredicate( - input, - receiverUserId, - expectedReceiverType, - input.OnlyUnread == true ? myReadNoticeIds : null); + // 先按类型/关键词查,再在内存按 ReceiverIds 判可见性。 + // MySQL JSON 列与字符串 "[]" 比较会失败,导致广播对客户/收件箱不可见。 + var basePredicate = BuildManageListPredicate(input); + var candidates = await NoticeRepository.GetListAsync(basePredicate); + var visible = candidates + .Where(x => manageAll || IsVisibleToReceiver(x, receiverUserId, expectedReceiverType)) + .Where(x => input.OnlyUnread != true || !myReadIdSet.Contains(x.Id)) + .OrderByDescending(x => x.CreateTime) + .ThenByDescending(x => x.Id) + .ToList(); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); - var notices = await NoticeRepository.GetPagedListAsync( - predicate, - pageIndex, - pageSize, - new Dictionary { [nameof(Notice.CreateTime)] = false, [nameof(Notice.Id)] = false }); - var total = await NoticeRepository.CountAsync(predicate); + var total = visible.Count; + var notices = visible + .Skip(Math.Max(pageIndex - 1, 0) * pageSize) + .Take(pageSize) + .ToList(); var noticeIds = notices.Select(x => x.Id).Distinct().ToArray(); var reads = noticeIds.Length == 0 @@ -91,8 +94,6 @@ public class NoticeService : ApplicationService .ToDictionary( x => x.Key, x => (int)x.Select(item => $"{item.ReceiverType}:{item.ReceiverId}").Distinct(StringComparer.Ordinal).Count()); - var myReadIdSet = myReadNoticeIds.ToHashSet(); - var selectionIdMap = await BuildReceiverSelectionIdMapAsync(notices); return new OmsPageResult @@ -365,33 +366,19 @@ public class NoticeService : ApplicationService x.Content.Contains(keyword)); } - private static Expression> BuildListPredicate( - NoticeListInput input, - int receiverUserId, - string expectedReceiverType, - IReadOnlyCollection? excludeNoticeIds) + /// + /// 空接收人 = 全体广播(员工/客户均可见);否则须类型匹配且 mapId 在名单内。 + /// + private static bool IsVisibleToReceiver(Notice notice, int receiverUserId, string expectedReceiverType) { - var noticeType = input.NoticeType?.Trim(); - var receiverType = input.ReceiverType?.Trim(); - var keyword = input.Keyword?.Trim(); - var receiverIdToken = receiverUserId.ToString(); - var excluded = excludeNoticeIds?.Where(x => x > 0).Distinct().ToList() ?? []; - 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)) && - ( - // 空接收人列表 = 全体广播(员工/客户均可见) - x.ReceiverIds == "[]" || - (x.ReceiverType == expectedReceiverType && - (x.ReceiverIds == $"[{receiverIdToken}]" || - x.ReceiverIds.StartsWith($"[{receiverIdToken},") || - x.ReceiverIds.Contains($",{receiverIdToken},") || - x.ReceiverIds.EndsWith($",{receiverIdToken}]"))) - ) && - (excluded.Count == 0 || !excluded.Contains(x.Id)); + var receiverIds = OmsDocHelper.DeserializeJson>(notice.ReceiverIds) ?? []; + if (receiverIds.Count == 0) + { + return true; + } + + return string.Equals(notice.ReceiverType, expectedReceiverType, StringComparison.OrdinalIgnoreCase) + && receiverIds.Contains(receiverUserId); } private async Task GetNoticeOrThrowAsync(int id) diff --git a/src/Oms.Domain/SystemAggregate/Entities/Notice.cs b/src/Oms.Domain/SystemAggregate/Entities/Notice.cs index e8fb134..3b2afa6 100644 --- a/src/Oms.Domain/SystemAggregate/Entities/Notice.cs +++ b/src/Oms.Domain/SystemAggregate/Entities/Notice.cs @@ -16,5 +16,9 @@ public class Notice : OmsAuditedAggregateRoot public string ReceiverType { get; set; } = string.Empty; + /// + /// 接收人 mapId 列表 JSON 数组字符串,如 [] / [1,2]。用 text 存储,避免 MySQL JSON 列与字符串比较失败导致广播不可见。 + /// + [SugarColumn(ColumnDataType = "text")] public string ReceiverIds { get; set; } = "[]"; }