From 076bd5636f5e57811d3bd7b642d24183593a1c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=96=B0=E9=9B=A8?= Date: Tue, 10 Feb 2026 17:23:38 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E6=9B=B4=E6=96=B0APP-PUSH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/services/pushApi.ts | 25 ++++++++++++++++------ client/src/storage/appStorage.ts | 35 +++++++++++++++++++++++++++++-- server/celerybeat-schedule-shm | Bin 0 -> 32768 bytes server/celerybeat-schedule-wal | Bin 0 -> 107152 bytes 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 server/celerybeat-schedule-shm create mode 100644 server/celerybeat-schedule-wal diff --git a/client/src/services/pushApi.ts b/client/src/services/pushApi.ts index 84b1c3c..31bc19a 100644 --- a/client/src/services/pushApi.ts +++ b/client/src/services/pushApi.ts @@ -7,10 +7,10 @@ import { httpJson } from '../utils/http'; import { APP_ENV } from '../constants/env'; import { getDailyReminderSettings, - getLastRegisteredPushToken, + getLastRegisteredPushPayload, getOrCreateClientUserId, getUserProfileScoring, - setLastRegisteredPushToken, + setLastRegisteredPushPayload, } from '../storage/appStorage'; import type { UserProfileScoring } from '../storage/appStorage'; import { toBackendLocaleFromLanguageTag } from '../i18n/locale'; @@ -170,14 +170,27 @@ export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: bool const settings = await Notifications.getPermissionsAsync(); if (!isSystemNotificationPermissionGranted(settings.status)) return { ok: false, reason: 'permission_not_granted' }; + const clientUserId = await getOrCreateClientUserId(); const token = await getExpoPushTokenOrThrow(); + const env = toPushEnv(APP_ENV); + const appId = pickAppId(); - // 简单去重:token 未变化则不重复上报(减少网络与日志噪音) - const last = await getLastRegisteredPushToken().catch(() => null); - if (last && String(last) === String(token)) return { ok: true, reason: 'already_registered' }; + // 去重规则(更严格): + // - 只有当 token + client_user_id + env + app_id 全都一致时才跳过 + // - 避免出现“client_user_id 变化但 token 没变,导致后端绑定不更新”的问题 + const last = await getLastRegisteredPushPayload().catch(() => null); + if ( + last && + last.pushToken === token && + last.clientUserId === clientUserId && + last.env === env && + last.appId === appId + ) { + return { ok: true, reason: 'already_registered' }; + } await registerPushToken({ pushToken: token }); - await setLastRegisteredPushToken(token); + await setLastRegisteredPushPayload({ pushToken: token, clientUserId, env, appId }); return { ok: true, reason: 'registered' }; } diff --git a/client/src/storage/appStorage.ts b/client/src/storage/appStorage.ts index 84f7b2d..ea505da 100644 --- a/client/src/storage/appStorage.ts +++ b/client/src/storage/appStorage.ts @@ -18,8 +18,9 @@ const KEY_RECO_FEED_HISTORY = 'reco.feedHistory'; const KEY_UI_THEME_MODE = 'ui.theme.mode'; const KEY_UI_THEME_SUIXIN_STATE = 'ui.theme.suixin.state'; const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings'; -const KEY_PUSH_LAST_REGISTERED_TOKEN = 'push.lastRegisteredToken'; -const KEY_PUSH_LAST_REGISTERED_AT = 'push.lastRegisteredAt'; +const KEY_PUSH_LAST_REGISTERED_TOKEN = 'push.lastRegisteredToken'; // 旧:仅 token(保留兼容读取) +const KEY_PUSH_LAST_REGISTERED_AT = 'push.lastRegisteredAt'; // 旧:时间(保留兼容) +const KEY_PUSH_LAST_REGISTERED_PAYLOAD = 'push.lastRegisteredPayload'; // 新:token+client_user_id+env+app_id export type PushPromptState = 'enabled' | 'skipped' | 'unknown'; export type Reaction = 'like' | 'dislike'; @@ -81,6 +82,36 @@ export async function setLastRegisteredPushToken(token: string): Promise { await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_AT, new Date().toISOString()); } +export type LastRegisteredPushPayload = { + pushToken: string; + clientUserId: string; + env: 'dev' | 'prod'; + appId: string; + savedAt: string; // ISO8601 +}; + +export async function getLastRegisteredPushPayload(): Promise { + const raw = await AsyncStorage.getItem(KEY_PUSH_LAST_REGISTERED_PAYLOAD); + if (!raw) return null; + try { + const obj = JSON.parse(raw) as Partial; + if (!obj || typeof obj !== 'object') return null; + if (!obj.pushToken || !obj.clientUserId || !obj.env || !obj.appId || !obj.savedAt) return null; + if (obj.env !== 'dev' && obj.env !== 'prod') return null; + return obj as LastRegisteredPushPayload; + } catch { + return null; + } +} + +export async function setLastRegisteredPushPayload(payload: Omit): Promise { + const savedAt = new Date().toISOString(); + const full: LastRegisteredPushPayload = { ...payload, savedAt }; + await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_PAYLOAD, JSON.stringify(full)); + // 同时写入旧 key,便于兼容老逻辑/快速排查 + await setLastRegisteredPushToken(payload.pushToken); +} + export type RecoFeedCacheItem = { content_id: number; text: string; diff --git a/server/celerybeat-schedule-shm b/server/celerybeat-schedule-shm new file mode 100644 index 0000000000000000000000000000000000000000..8247f6cda01b4b87c44e24535c84436d541f99bf GIT binary patch literal 32768 zcmeI)J4!=Q5C-5s@ez%$#AtFAdqEJ~gsqT0qz?hN;Vx{lffRO@*@jCn2zGj|NU*ig z+k6AV42Q#+!?%E`UanKeLCIFqJ1=#kMQDZKYqmb@w&82SI_2LEI1ar9U2Ak*TSe!{^$fq6Y7)#Dq^srA%+AiFA_Xw`|!1- z?`fJi-&9tfobP%2oO4p_U!Q0FGj-1@u?s5{rAcwWE^VL8rC({E+4v4gvJ^*D)uTXr7_#cnE(-W!M8a4MfOEN#FYd?GB*3A8)=3elt*g!7eql0J)9N?x=U?3rdz{q&JCL}7bm6*EpHW>^qRJ5kJ~Fs zQnE_XQ}tD8C`p8u%Zr+K^rgiK)atEmcCh_{2a6-Te3aK5)iUp@X{kYBq}5bbXhvb! zkt5T)_)FQ#qBN7;n;z7<^C!rvbTD1a5$3}(&VkzigPc-o4bS2B?T*` z3PT&niu941o$Z$SBq8zRAI!1o41cU`b4U>$nMJ?wj{;88i1%kl!f-`m~QpFH~S|K$aW zX@SVwb85;A1_1;RKmY**5I_I{1Q3|>0^|kA3(Q_#VB-wt1t#vf@4XFwoY+oYV9wuU z7$5=&Ab+Xp9-J8$IA=2YYVt*3_y(l z0tg_000IagfB*srAaFAR5I_I{1Q0*~0R#|OXac^F zfUmgeiv0*~G%pZsefU#$3X z`H^^4t}-y^@&Y|GxF5mf@IMb9J^sc8@&dPe51>8*2q1s}0tg_000IagfB*usA>a!M z_=;D_3-nxXKZ3m2MeIISE#n(7-`E$^HT-rXFOTW@S`S9Tvl|FGF5Sr!KjXP7iNEfoASGG0cj5}?5 zz>Zp)IcUb+cH&+;YOx))(oREKwr*vHgkx8V<4#Yw{^Fu=sIIQz%_H)efkGy0Wen4? z>-Pyuv#h-A!)`c~GepMNo0G+-WwFmK%HtumU<_wcgIVDmb9Oj(aQChrClPSV?jsSf zo64j2#^E-c%I6GA8*m4o2qzRf(C+9f^_7;aG3pH5KPuZD(hjNxQ+~YdY&+g!$|JS= z3!)&09!`q`-K91i)2(4Q=Z4Lgixbm@mbZ#bdQIE3$L$p*DOn}%o~Zh&G!!L5;pOrL znRoQ1#R=5vt!;L&{ecIIBfNZ+qt0F3kKm@(7r2GIz>`Dk{{Gc9-{>PRP~CSZW`_U* z2q1s}0tg_000IagP%Q$!kbtlFxV!N{!AhyZ&<3(1eUgUFuQ$c*EzsfEfpz6%LM1&R(-QUK z;BY1{WP0MQ*NQ2LLcIA-=LN#AUb^@EOr zSb6WvhZ=c-oBf^Vd;}0c009ILKmY**5I_I{1n!1_FC^eAZoS34z|`t@$4-BK;=AMp z?#8vGPXrJ^009ILKmY**5U3u3v1Ee0z&yzdELW5lr@Xwt@`aukczxn;tM)%