3 Commits

Author SHA1 Message Date
173cee75d5 Merge pull request 'damer-0210' (#21) from damer-0210 into main
Reviewed-on: #21
2026-02-10 09:24:00 +00:00
吕新雨
076bd5636f fix:更新APP-PUSH 2026-02-10 17:23:38 +08:00
吕新雨
154f347ddb 注册token排查 2026-02-10 16:57:36 +08:00
5 changed files with 85 additions and 9 deletions

View File

@@ -7,10 +7,10 @@ import { httpJson } from '../utils/http';
import { APP_ENV } from '../constants/env'; import { APP_ENV } from '../constants/env';
import { import {
getDailyReminderSettings, getDailyReminderSettings,
getLastRegisteredPushToken, getLastRegisteredPushPayload,
getOrCreateClientUserId, getOrCreateClientUserId,
getUserProfileScoring, getUserProfileScoring,
setLastRegisteredPushToken, setLastRegisteredPushPayload,
} from '../storage/appStorage'; } from '../storage/appStorage';
import type { UserProfileScoring } from '../storage/appStorage'; import type { UserProfileScoring } from '../storage/appStorage';
import { toBackendLocaleFromLanguageTag } from '../i18n/locale'; import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
@@ -170,14 +170,27 @@ export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: bool
const settings = await Notifications.getPermissionsAsync(); const settings = await Notifications.getPermissionsAsync();
if (!isSystemNotificationPermissionGranted(settings.status)) return { ok: false, reason: 'permission_not_granted' }; if (!isSystemNotificationPermissionGranted(settings.status)) return { ok: false, reason: 'permission_not_granted' };
const clientUserId = await getOrCreateClientUserId();
const token = await getExpoPushTokenOrThrow(); const token = await getExpoPushTokenOrThrow();
const env = toPushEnv(APP_ENV);
const appId = pickAppId();
// 简单去重token 未变化则不重复上报(减少网络与日志噪音) // 去重规则(更严格):
const last = await getLastRegisteredPushToken().catch(() => null); // - 只有当 token + client_user_id + env + app_id 全都一致时才跳过
if (last && String(last) === String(token)) return { ok: true, reason: 'already_registered' }; // - 避免出现“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 registerPushToken({ pushToken: token });
await setLastRegisteredPushToken(token); await setLastRegisteredPushPayload({ pushToken: token, clientUserId, env, appId });
return { ok: true, reason: 'registered' }; return { ok: true, reason: 'registered' };
} }

View File

@@ -18,8 +18,9 @@ const KEY_RECO_FEED_HISTORY = 'reco.feedHistory';
const KEY_UI_THEME_MODE = 'ui.theme.mode'; const KEY_UI_THEME_MODE = 'ui.theme.mode';
const KEY_UI_THEME_SUIXIN_STATE = 'ui.theme.suixin.state'; const KEY_UI_THEME_SUIXIN_STATE = 'ui.theme.suixin.state';
const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings'; const KEY_DAILY_REMINDER_SETTINGS = 'dailyReminder.settings';
const KEY_PUSH_LAST_REGISTERED_TOKEN = 'push.lastRegisteredToken'; const KEY_PUSH_LAST_REGISTERED_TOKEN = 'push.lastRegisteredToken'; // 旧:仅 token保留兼容读取
const KEY_PUSH_LAST_REGISTERED_AT = 'push.lastRegisteredAt'; 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 PushPromptState = 'enabled' | 'skipped' | 'unknown';
export type Reaction = 'like' | 'dislike'; export type Reaction = 'like' | 'dislike';
@@ -81,6 +82,36 @@ export async function setLastRegisteredPushToken(token: string): Promise<void> {
await AsyncStorage.setItem(KEY_PUSH_LAST_REGISTERED_AT, new Date().toISOString()); 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<LastRegisteredPushPayload | null> {
const raw = await AsyncStorage.getItem(KEY_PUSH_LAST_REGISTERED_PAYLOAD);
if (!raw) return null;
try {
const obj = JSON.parse(raw) as Partial<LastRegisteredPushPayload>;
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<LastRegisteredPushPayload, 'savedAt'>): Promise<void> {
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 = { export type RecoFeedCacheItem = {
content_id: number; content_id: number;
text: string; text: string;

View File

@@ -7,7 +7,7 @@ import httpx
import redis import redis
from fastapi import APIRouter, Depends, Header, HTTPException, Query from fastapi import APIRouter, Depends, Header, HTTPException, Query
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.api.limits import rate_limit_push_by_ip from app.api.limits import rate_limit_push_by_ip
@@ -297,6 +297,7 @@ async def scheduler_health(db: AsyncSession = Depends(get_db)) -> dict[str, Any]
"worker": {"ok": False, "worker_count": 0}, "worker": {"ok": False, "worker_count": 0},
"beat": {"ok": False, "last_heartbeat_at": None, "age_seconds": None}, "beat": {"ok": False, "last_heartbeat_at": None, "age_seconds": None},
"db": {"ok": False, "push_send_log_latest_created_at": None}, "db": {"ok": False, "push_send_log_latest_created_at": None},
"db_push_tokens": {"ok": False, "count": None, "latest": None},
"now_utc": datetime.now(timezone.utc).isoformat(), "now_utc": datetime.now(timezone.utc).isoformat(),
} }
@@ -343,5 +344,36 @@ async def scheduler_health(db: AsyncSession = Depends(get_db)) -> dict[str, Any]
except Exception as e: except Exception as e:
out["db"]["error"] = f"{type(e).__name__}: {e}" out["db"]["error"] = f"{type(e).__name__}: {e}"
# 4) DB查询 push_tokens 计数与最近一条(用于确认 /v1/push/register 是否真正落库)
try:
qcount = select(func.count()).select_from(PushToken)
rcount = await db.execute(qcount)
cnt = int(rcount.scalar_one() or 0)
qlatest = select(PushToken).order_by(PushToken.last_seen_at.desc()).limit(1)
rlatest = await db.execute(qlatest)
t = rlatest.scalar_one_or_none()
latest_obj = None
if t is not None:
tok = str(t.push_token or "")
masked = tok[:10] + "***" + tok[-6:] if len(tok) > 20 else (tok[:6] + "***" if tok else "")
latest_obj = {
"id": int(getattr(t, "id", 0) or 0),
"client_user_id": str(t.client_user_id),
"env": str(t.env),
"app_id": str(t.app_id),
"platform": str(t.platform),
"is_active": bool(t.is_active),
"last_seen_at": t.last_seen_at.isoformat() if t.last_seen_at else None,
"push_token_masked": masked,
}
out["db_push_tokens"]["ok"] = True
out["db_push_tokens"]["count"] = cnt
out["db_push_tokens"]["latest"] = latest_obj
except Exception as e:
out["db_push_tokens"]["error"] = f"{type(e).__name__}: {e}"
return out return out

Binary file not shown.

Binary file not shown.