fix:后端错误

This commit is contained in:
吕新雨
2026-02-10 16:47:18 +08:00
parent 1fbc0aa3f8
commit dec3ac82e1
5 changed files with 29 additions and 14 deletions

View File

@@ -12,7 +12,7 @@ import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnai
import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco'; import { ensureDailyWidgetRecoUpToDate, syncWidgetConfig, syncWidgetUserProfileFromScoring } from '@/src/modules/dailyWidgetReco';
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale'; import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
import { fetchRecoFeed } from '@/src/services/recoApi'; import { fetchRecoFeed } from '@/src/services/recoApi';
import { getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi'; import { ensurePushTokenRegisteredIfPermitted, setPushPreferences } from '@/src/services/pushApi';
import { import {
recordRecoFeedServed, recordRecoFeedServed,
setOnboardingCompleted, setOnboardingCompleted,
@@ -128,7 +128,8 @@ export default function OnboardingScreen() {
await setPushPromptState('unknown'); await setPushPromptState('unknown');
try { try {
const { status } = await Notifications.requestPermissionsAsync(); const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') { // iOS 可能出现 provisional临时授权也应视为“已授权”
if (status !== 'granted' && status !== ('provisional' as any)) {
await setPushPromptState('skipped'); await setPushPromptState('skipped');
return; return;
} }
@@ -140,10 +141,8 @@ export default function OnboardingScreen() {
return; return;
} }
// 1) 获取 Expo Push Token失败才认为“推送开启失败”) // 1) 上报 token 到后端(幂等;失败才认为“推送开启失败”)
const expoPushToken = await getExpoPushTokenOrThrow(); await ensurePushTokenRegisteredIfPermitted();
// 2) 上报 token 到后端(幂等;失败才认为“推送开启失败”)
await registerPushToken({ pushToken: expoPushToken });
// 3) 上报推送偏好(幂等) // 3) 上报推送偏好(幂等)
// 注意:这一步失败时,后端仍可能已成功接收 token。 // 注意:这一步失败时,后端仍可能已成功接收 token。

View File

@@ -87,6 +87,17 @@ export default function RootLayout() {
}); });
}, []); }, []);
useEffect(() => {
// 兜底:当用户在系统弹窗/系统设置里变更权限后App 回到前台时再同步一次 token
const sub = AppState.addEventListener('change', (state) => {
if (state !== 'active') return;
ensurePushTokenRegisteredIfPermitted().catch(() => {
// ignore不阻塞
});
});
return () => sub.remove();
}, []);
useEffect(() => { useEffect(() => {
// 字体与 i18n 都准备好后,允许渲染 App原生 splash 的隐藏交给 onLayout避免“硬切/闪白”) // 字体与 i18n 都准备好后,允许渲染 App原生 splash 的隐藏交给 onLayout避免“硬切/闪白”)
if (loaded && i18nReady) setAppReady(true); if (loaded && i18nReady) setAppReady(true);

View File

@@ -41,7 +41,7 @@ import QuestionIcon from '@/assets/images/home/Profile/widget/question_icon.svg'
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import { changeLanguage } from '@/src/i18n'; import { changeLanguage } from '@/src/i18n';
import { fetchLegalLinks } from '@/src/services/legalApi'; import { fetchLegalLinks } from '@/src/services/legalApi';
import { ensurePushTokenRegisteredIfPermitted, getExpoPushTokenOrThrow, registerPushToken, setPushPreferences } from '@/src/services/pushApi'; import { ensurePushTokenRegisteredIfPermitted, setPushPreferences } from '@/src/services/pushApi';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@@ -430,14 +430,13 @@ function DailyReminderPage({ visible, onDone }: { visible: boolean; onDone: () =
// 调试:打印状态 // 调试:打印状态
console.log('Push Permission Status:', status); console.log('Push Permission Status:', status);
if (status === 'granted') { if (status === 'granted' || (status as any) === 'provisional') {
setPushEnabled(true); setPushEnabled(true);
setHasSystemPermission(true); setHasSystemPermission(true);
// 获取 token 并上报后端(幂等) // 获取 token 并上报后端(幂等)
try { try {
const expoPushToken = await getExpoPushTokenOrThrow(); await ensurePushTokenRegisteredIfPermitted();
await registerPushToken({ pushToken: expoPushToken });
// 偏好同步失败不应被用户感知为“开启失败” // 偏好同步失败不应被用户感知为“开启失败”
// (常见现象:后端已接收 token但偏好接口短暂失败/超时) // (常见现象:后端已接收 token但偏好接口短暂失败/超时)
try { try {

View File

@@ -160,10 +160,15 @@ export async function registerPushToken(args: { pushToken: string }): Promise<vo
}); });
} }
function isSystemNotificationPermissionGranted(status: unknown): boolean {
// iOS 可能出现 provisional临时授权在 Push 场景也应视为“可获取 token 并上报”
return status === 'granted' || status === 'provisional';
}
export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: boolean; reason: string }> { export async function ensurePushTokenRegisteredIfPermitted(): Promise<{ ok: boolean; reason: string }> {
// 只要系统权限已授权,就应尽早把 token 写入后端(不依赖用户在“每日提醒”里点确认) // 只要系统权限已授权,就应尽早把 token 写入后端(不依赖用户在“每日提醒”里点确认)
const settings = await Notifications.getPermissionsAsync(); const settings = await Notifications.getPermissionsAsync();
if (settings.status !== 'granted') return { ok: false, reason: 'permission_not_granted' }; if (!isSystemNotificationPermissionGranted(settings.status)) return { ok: false, reason: 'permission_not_granted' };
const token = await getExpoPushTokenOrThrow(); const token = await getExpoPushTokenOrThrow();

View File

@@ -194,9 +194,10 @@ async def put_preferences(req: PushPreferencesRequest, db: AsyncSession = Depend
await db.commit() await db.commit()
# 返回更新时间(从 ORM 读取到的可能不包含 server_onupdate这里用 now 兜底) # 返回更新时间
updated_at = getattr(pref, "updated_at", None) # - 某些运行环境/驱动组合下commit 后访问 ORM 字段可能触发隐式 IO导致 async 下报 MissingGreenlet。
updated_at_iso = updated_at.isoformat() if isinstance(updated_at, datetime) else None # - 这里直接用当前时间兜底(字段本身为可选,仅用于前端展示)。
updated_at_iso = datetime.now(timezone.utc).isoformat()
return PushPreferencesResponse( return PushPreferencesResponse(
client_user_id=req.client_user_id, client_user_id=req.client_user_id,