From 016bc6fd2cfd997f076a9f75b5690a048c3c7e67 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 9 Jun 2026 18:30:49 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=9A=84=E5=8C=85=E5=AF=BC=E5=85=A5=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deviceid/[deviceId]/DeepLinkPersist.tsx | 40 ++++++++++--------- .../api => core/net}/config/api_config.ts | 0 src/data/repositories/chat_repository.ts | 2 +- src/data/repositories/metrics_repository.ts | 2 +- src/data/repositories/user_repository.ts | 2 +- src/data/services/api/http_client.ts | 2 +- src/data/services/api/index.ts | 2 +- .../api/interceptor/token_interceptor.ts | 20 +++++++++- src/data/services/index.ts | 2 +- src/integrations/chat-websocket.ts | 2 +- src/lib/auth/use-auth-gate.tsx | 16 ++++++-- src/utils/device_identifier.ts | 16 +++++--- src/utils/storage.ts | 7 ---- 13 files changed, 69 insertions(+), 44 deletions(-) rename src/{data/services/api => core/net}/config/api_config.ts (100%) diff --git a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx index 1c1db495..2550d829 100644 --- a/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx +++ b/src/app/chat/deviceid/[deviceId]/DeepLinkPersist.tsx @@ -8,18 +8,18 @@ * * 设计: * - **不**走 Server `redirect()`:避免把 fbid 暴露在 URL 里。 - * - deviceId 走 sync `authStorage.setDeviceId`(与 `token_interceptor.ts` 同源; + * - deviceId 走 `AuthStorage.getInstance().setDeviceId`(与 `token_interceptor.ts` 同源; * 写入键 `cozsweet.deviceId`)。 - * - fbid 走 `localStorage.setItem("facebook_id", ...)`,对齐 async - * `StorageKeys.facebookId`。Sync 单例**没有** `setFacebookId` 方法,本轮 - * 不重构 storage(见 plan §6.1),仅留 TODO。 + * - fbid 走 `window.localStorage.setItem(StorageKeys.facebookId, ...)`, + * 与 async `AuthStorage.setFacebookId` 同键名。Sync 层暂未统一抽象, + * 仅留 TODO。 * - 写入完成后 `router.replace('/chat')`,URL 不留深链痕迹。 */ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { authStorage } from "@/data/storage/auth_storage"; +import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { StorageKeys } from "@/data/storage/storage_keys"; import { ROUTES } from "@/lib/routes"; @@ -33,21 +33,23 @@ export default function DeepLinkPersist({ deviceId, fbid }: DeepLinkPersistProps const [done, setDone] = useState(false); useEffect(() => { - try { - authStorage.setDeviceId(deviceId); - if (fbid && fbid.length > 0) { - // TODO: 统一 sync/async AuthStorage 后改为 `authStorage.setFacebookId(fbid)`。 - // 当前 sync 单例未暴露该方法;使用 `StorageKeys.facebookId` 保持与 async 层 - // `src/data/storage/auth/auth_storage.ts` 键名一致。 - window.localStorage.setItem(StorageKeys.facebookId, fbid); + void (async () => { + try { + await AuthStorage.getInstance().setDeviceId(deviceId); + if (fbid && fbid.length > 0) { + // TODO: 统一 sync/async AuthStorage 后改为 `AuthStorage.getInstance().setFacebookId(fbid)`。 + // 当前 sync 层未暴露该方法;使用 `StorageKeys.facebookId` 保持与 async 层 + // `src/data/storage/auth/auth_storage.ts` 键名一致。 + window.localStorage.setItem(StorageKeys.facebookId, fbid); + } + } catch (err) { + // 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。 + console.warn("[DeepLinkPersist] failed to persist", err); + } finally { + setDone(true); + router.replace(ROUTES.chat); } - } catch (err) { - // 写不进 localStorage 时(例如隐私模式)也不阻塞跳转;记录到 console 即可。 - console.warn("[DeepLinkPersist] failed to persist", err); - } finally { - setDone(true); - router.replace(ROUTES.chat); - } + })(); }, [deviceId, fbid, router]); return ( diff --git a/src/data/services/api/config/api_config.ts b/src/core/net/config/api_config.ts similarity index 100% rename from src/data/services/api/config/api_config.ts rename to src/core/net/config/api_config.ts diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 58a4bc5f..64ee94c4 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -15,7 +15,7 @@ * * 原始 Dart: lib/data/repositories/chat_repository_impl.dart */ -import { ChatApi, chatApi } from "@/data/api"; +import { ChatApi, chatApi } from "@/data/services/api"; import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response"; import { ChatMessage } from "@/data/dto/chat/chat_message"; import { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; diff --git a/src/data/repositories/metrics_repository.ts b/src/data/repositories/metrics_repository.ts index e825bebb..42e96848 100644 --- a/src/data/repositories/metrics_repository.ts +++ b/src/data/repositories/metrics_repository.ts @@ -7,7 +7,7 @@ * * 原始 Dart: lib/data/repositories/metrics_repository_impl.dart */ -import { MetricsApi, metricsApi } from "@/data/api"; +import { MetricsApi, metricsApi } from "@/data/services/api"; import { AppEvent } from "@/data/dto/metrics/app_event"; import { PwaEvent } from "@/data/dto/metrics/pwa_event"; import { Result } from "@/utils/result"; diff --git a/src/data/repositories/user_repository.ts b/src/data/repositories/user_repository.ts index 1ad2401e..c68d5e98 100644 --- a/src/data/repositories/user_repository.ts +++ b/src/data/repositories/user_repository.ts @@ -8,7 +8,7 @@ * * 原始 Dart: lib/data/repositories/user_repository_impl.dart */ -import { UserApi, userApi } from "@/data/api"; +import { UserApi, userApi } from "@/data/services/api"; import { CreditsData } from "@/data/dto/user/credits_data"; import { CreditsHistoryData } from "@/data/dto/user/credits_history_data"; import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request"; diff --git a/src/data/services/api/http_client.ts b/src/data/services/api/http_client.ts index 922d0926..607f9ea2 100644 --- a/src/data/services/api/http_client.ts +++ b/src/data/services/api/http_client.ts @@ -6,7 +6,7 @@ */ import { ofetch, type $Fetch, type FetchHook } from "ofetch"; import { ApiError, ErrorCode } from "./api_result"; -import { getApiConfig } from "./config/api_config"; +import { getApiConfig } from "../../../core/net/config/api_config"; import { authRefreshInterceptor } from "./interceptor/auth_refresh_interceptor"; import { loggingInterceptor } from "./interceptor/logging_interceptor"; import { tokenInterceptor } from "./interceptor/token_interceptor"; diff --git a/src/data/services/api/index.ts b/src/data/services/api/index.ts index 9888954a..ab853157 100644 --- a/src/data/services/api/index.ts +++ b/src/data/services/api/index.ts @@ -10,7 +10,7 @@ export * from "./http_client"; export * from "./metrics_api"; export * from "./response_helper"; export * from "./user_api"; -export * from "./config/api_config"; +export * from "../../../core/net/config/api_config"; export * from "./interceptor/auth_refresh_interceptor"; export * from "./interceptor/logging_interceptor"; export * from "./interceptor/token_interceptor"; diff --git a/src/data/services/api/interceptor/token_interceptor.ts b/src/data/services/api/interceptor/token_interceptor.ts index 3f405999..b7907e47 100644 --- a/src/data/services/api/interceptor/token_interceptor.ts +++ b/src/data/services/api/interceptor/token_interceptor.ts @@ -7,7 +7,7 @@ */ import type { FetchHook } from "ofetch"; import { ApiPath } from "../api_path"; -import { authStorage } from "../../storage/auth_storage"; +import { AuthStorage } from "../../../storage/auth/auth_storage"; /** * 不需要 token 的路径 @@ -40,11 +40,27 @@ function needsToken(path: string): boolean { return !NO_TOKEN_PATHS.some((noTokenPath) => path.includes(noTokenPath)); } +/** + * 获取当前可用的 auth token(登录 token 优先,其次游客 token) + */ +async function getAuthToken(): Promise { + const storage = AuthStorage.getInstance(); + const loginR = await storage.getLoginToken(); + if (loginR.success && loginR.data && loginR.data.length > 0) { + return loginR.data; + } + const guestR = await storage.getGuestToken(); + if (guestR.success && guestR.data && guestR.data.length > 0) { + return guestR.data; + } + return null; +} + export const onRequestToken: FetchHook = async (ctx) => { const path = getRequestPath(ctx.request); if (!needsToken(path)) return; - const token = authStorage.getAuthToken(); + const token = await getAuthToken(); if (token && ctx.request instanceof Request) { ctx.request.headers.set("Authorization", `Bearer ${token}`); } diff --git a/src/data/services/index.ts b/src/data/services/index.ts index bae46852..103f0ebf 100644 --- a/src/data/services/index.ts +++ b/src/data/services/index.ts @@ -10,7 +10,7 @@ export * from "./api/http_client"; export * from "./api/metrics_api"; export * from "./api/response_helper"; export * from "./api/user_api"; -export * from "./api/config/api_config"; +export * from "../../core/net/config/api_config"; export * from "./api/interceptor/auth_refresh_interceptor"; export * from "./api/interceptor/logging_interceptor"; export * from "./api/interceptor/token_interceptor"; diff --git a/src/integrations/chat-websocket.ts b/src/integrations/chat-websocket.ts index 1a91732f..74b31482 100644 --- a/src/integrations/chat-websocket.ts +++ b/src/integrations/chat-websocket.ts @@ -14,7 +14,7 @@ * - onMoodUpdate(mood) * - onError(errorMessage) */ -import { getApiConfig } from "@/data/api/config/api_config"; +import { getApiConfig } from "@/core/net/config/api_config"; export interface SentencePayload { index: number; diff --git a/src/lib/auth/use-auth-gate.tsx b/src/lib/auth/use-auth-gate.tsx index 53239541..4c67a825 100644 --- a/src/lib/auth/use-auth-gate.tsx +++ b/src/lib/auth/use-auth-gate.tsx @@ -3,7 +3,7 @@ /** * 客户端鉴权门 hook * - * 单一真值 = `authStorage`(与 `src/data/api/interceptor/token_interceptor.ts` 同源)。 + * 单一真值 = `window.localStorage[StorageKeys.loginToken]`。 * 故意不引入 Context/Zustand:本轮只有 splash/auth 两处使用,直接 hook 调用更直白。 * * 实现要点(React 19 / Next.js 16 约定): @@ -22,11 +22,14 @@ * * 注:本轮不暴露 `ready` 标志——`useSyncExternalStore` 自身的 SSR / 客户端差异 * 处理已经能保证水合首帧拿到 `false`(与 SSR 一致),无需调用方再判 ready。 + * + * 注意:直接读 `window.localStorage` 是因为 `useSyncExternalStore.getSnapshot` + * 必须是同步函数。`AuthStorage.hasLoginToken()` 是 async,无法在此处使用。 */ import { useSyncExternalStore } from "react"; -import { authStorage } from "@/data/storage/auth_storage"; +import { StorageKeys } from "@/data/storage/storage_keys"; export interface AuthGateState { /** 已登录(持有非空 `cozsweet.loginToken`) */ @@ -36,7 +39,14 @@ export interface AuthGateState { // 静态 subscribe:localStorage 在本应用生命周期内不会主动变化。 const subscribe = (): (() => void) => () => {}; -const getSnapshot = (): boolean => authStorage.hasLoginToken(); +/** + * 同步读取 localStorage 中的 login_token(与 `AuthStorage.hasLoginToken` 语义一致) + */ +const getSnapshot = (): boolean => { + if (typeof window === "undefined") return false; + const token = window.localStorage.getItem(StorageKeys.loginToken); + return token !== null && token.length > 0; +}; const getServerSnapshot = (): boolean => false; diff --git a/src/utils/device_identifier.ts b/src/utils/device_identifier.ts index 8475c034..7caaf30c 100644 --- a/src/utils/device_identifier.ts +++ b/src/utils/device_identifier.ts @@ -6,7 +6,9 @@ * * 注:仅在浏览器环境可用;SSR/Server Components 中调用会抛错。 */ -import { authStorage } from "../data/storage/auth_storage"; +import { AuthStorage } from "../data/storage/auth/auth_storage"; +import { SpAsyncUtil } from "./storage"; +import { StorageKeys } from "../data/storage/storage_keys"; export class DeviceIdentifier { private cachedId: string | null = null; @@ -30,16 +32,18 @@ export class DeviceIdentifier { /** * 重置缓存与持久化(用于测试或多账号切换) */ - reset(): void { + async reset(): Promise { this.cachedId = null; this.initPromise = null; - authStorage.removeDeviceId(); + await SpAsyncUtil.remove(StorageKeys.deviceId); } private async initialize(): Promise { // 1. 优先从 localStorage 读取 - const stored = authStorage.getDeviceId(); - if (stored) return stored; + const storedR = await AuthStorage.getInstance().getDeviceId(); + if (storedR.success && storedR.data && storedR.data.length > 0) { + return storedR.data; + } // 2. SSR/Node 环境守卫:调用方必须保证在浏览器中调用 if (typeof window === "undefined") { @@ -55,7 +59,7 @@ export class DeviceIdentifier { const result = await fp.get(); // 4. 持久化 - authStorage.setDeviceId(result.visitorId); + await AuthStorage.getInstance().setDeviceId(result.visitorId); return result.visitorId; } } diff --git a/src/utils/storage.ts b/src/utils/storage.ts index fe578ea5..d98fadc8 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,13 +1,6 @@ /** * SpAsyncUtil — 键值对持久化工具类(基于 unstorage) * - * 原始 Dart: `lib/utils/src/sp_async_util.dart`(`SpAsyncUtil` 静态类) - * - * 设计目标: - * - **完全静态类化**:所有状态/方法/函数均封装在 SpAsyncUtil 类内 - * - 底层基于 unstorage 抽象(浏览器 localStorage / SSR memory 自动切换) - * - 保留 `Promise>` API 兼容(state machines、interceptor、repositories 零修改) - * * 用法: * ```ts * import { SpAsyncUtil } from "@/utils/storage";