diff --git a/backend/services/notificationService/service/notification_service.go b/backend/services/notificationService/service/notification_service.go index afc86cb..18cb3f4 100644 --- a/backend/services/notificationService/service/notification_service.go +++ b/backend/services/notificationService/service/notification_service.go @@ -51,24 +51,33 @@ var allowedTypes = map[string]struct{}{ // NotificationService 通知服务业务层。 type NotificationService struct { - db *gorm.DB - notifRepo *repository.NotificationRepository - statsRepo *repository.NotificationStatsRepository - device *UserDeviceService // 用于推送时拉取用户活跃 cid;若 nil 则跳过推送 - pusher push.Pusher // 推送客户端;若 nil 则跳过推送 + db *gorm.DB + notifRepo *repository.NotificationRepository + statsRepo *repository.NotificationStatsRepository + device *UserDeviceService // 用于推送时拉取用户活跃 cid;若 nil 则跳过推送 + pusher push.Pusher // 推送客户端;若 nil 则跳过推送 + rateLimiter *push.RateLimiter // P1-3:推送节流器,按 (user, star, type) 60s 滑窗,>1 改 summary 标题 } // NewNotificationService 创建 NotificationService。 // // 参数 device 与 pusher 用于在 CreateNotification 成功后触发手机通知栏推送; // 若任一为 nil,则不会触发推送(便于测试 / 关闭推送功能)。 +// +// P1-3 修复:rateLimiter 在 NewNotificationService 中初始化,pusher 为 nil 时不创建 +// (避免占用内存)。triggerPush 内部仍需 nil 检查,避免 panic。 func NewNotificationService(db *gorm.DB, device *UserDeviceService, pusher push.Pusher) *NotificationService { + var rl *push.RateLimiter + if pusher != nil { + rl = push.NewRateLimiter(60000) + } return &NotificationService{ - db: db, - notifRepo: repository.NewNotificationRepository(db), - statsRepo: repository.NewNotificationStatsRepository(db), - device: device, - pusher: pusher, + db: db, + notifRepo: repository.NewNotificationRepository(db), + statsRepo: repository.NewNotificationStatsRepository(db), + device: device, + pusher: pusher, + rateLimiter: rl, } } @@ -260,9 +269,22 @@ func (s *NotificationService) triggerPush(n *model.Notification) { go func() { cctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() + + // P1-3 fix:节流后再发送——60s 滑窗内同一 (user, star, type) 多条推送, + // 首条走原 title,后续合并为 "您有 N 条新" summary。 + // 注意只改 Title,不覆盖 Content(Content 是具体业务文案,客户端要展示)。 + title := n.Title + if s.rateLimiter != nil { + key := fmt.Sprintf("%d:%d:%s", n.UserID, n.StarID, n.Type) + mode, count := s.rateLimiter.Allow(key) + if mode == "summary" && count > 1 { + title = fmt.Sprintf("您有 %d 条新%s", count, push.TypeChinese(n.Type)) + } + } + if err := s.pusher.Send(cctx, push.Payload{ CIDs: cids, - Title: n.Title, + Title: title, Content: n.Content, Data: data, }); err != nil { diff --git a/frontend/App.vue b/frontend/App.vue index 252ed9a..8574638 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -13,7 +13,9 @@ import { clearAllSandboxTmpFiles } from '@/utils/ioPath.js' // 记录上次隐藏时间的 storage key const HIDE_TIME_KEY = "app_last_hide_time"; -// 推送事件去抖:同 type 在 PUSH_DEBOUNCE_MS 毫秒内只处理一次(避免系统重投/重复抵达) +// 推送事件去抖:同 (type, notification_id) 在 PUSH_DEBOUNCE_MS 毫秒内只处理一次 +// (避免系统重投/重复抵达)—— P1-4 修复:之前 keyed-by-type 导致 +// "不同 nid 的同 type 推送"被丢弃,现在按 (type, nid) 区分。 const PUSH_DEBOUNCE_MS = 10000; const recentByType = {}; @@ -320,13 +322,16 @@ export default { const envelope = payloadStr ? JSON.parse(payloadStr) : {}; const payload = envelope?.data || data || {}; const t = payload.type; + const nid = payload.notification_id || ""; + const debounceKey = `${t}:${nid}`; const now = Date.now(); - // V1.2.7 10s 同 type 去抖,避免系统重投导致重复 dispatch - if (t && recentByType[t] && now - recentByType[t] < PUSH_DEBOUNCE_MS) { - console.log("[push] debounce skip", t); + // 10s 同 (type, notification_id) 去抖,避免系统重投导致重复 dispatch + // (P1-4:原 key=type 会丢不同的 nid,改为组合 key) + if (debounceKey && recentByType[debounceKey] && now - recentByType[debounceKey] < PUSH_DEBOUNCE_MS) { + console.log("[push] debounce skip", debounceKey); return; } - if (t) recentByType[t] = now; + if (debounceKey) recentByType[debounceKey] = now; // 派发到 mailbox store:更新未读计数 + (若在站内信页)PREPEND_ITEM this.$store.dispatch("mailbox/applyPushPayload", { data: payload, diff --git a/frontend/manifest.json b/frontend/manifest.json index 7dfa079..b29ba6b 100644 --- a/frontend/manifest.json +++ b/frontend/manifest.json @@ -32,7 +32,7 @@ "distribute" : { /* android打包配置 */ "android" : { - "packagename" : "online.topfans.app", + "packagename" : "online.topfans.app", "permissions" : [ "", "", diff --git a/frontend/pages/castlove/lenticular/lenticular-create.vue b/frontend/pages/castlove/lenticular/lenticular-create.vue index 19d3995..8323c7e 100644 --- a/frontend/pages/castlove/lenticular/lenticular-create.vue +++ b/frontend/pages/castlove/lenticular/lenticular-create.vue @@ -1,126 +1,171 @@