From d41b3c44738aff2a7424c98a31918b11a9426877 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 12:05:09 +0800 Subject: [PATCH 1/3] refactor(schemas): extract nullable-default helpers to shared file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three defensive Zod helpers (stringOrEmpty, numberOrZero, booleanOrFalse) used by user.ts are general-purpose Zod utilities that any schema may need — they're not user-specific. The original inline declaration in user.ts had two downsides: 1. Any new schema (chat, auth, metrics, etc.) that needs the same 'null | undefined → default scalar' pattern would have to duplicate the helper. 2. The long defensive-parse comment block (11 lines) lived inside user.ts and didn't explain the helpers' general purpose. Move them to src/data/schemas/nullable-defaults.ts (top-level sibling of auth/, chat/, metrics/, user/ subdirs) so any schema can import them. Keep the original explanation verbatim at the top of the new file. Bundled in this commit: - user.ts imports the helpers from the new file (drops 11 lines of comment + 3 const declarations) - 3 auto-generated barrel index files regenerated by barrelsby to pick up the new exports (chat/, subscription/, stores/user/) --- src/app/chat/components/index.ts | 1 - src/app/subscription/components/index.ts | 2 ++ src/data/schemas/nullable-defaults.ts | 33 ++++++++++++++++++++++++ src/data/schemas/user/user.ts | 18 ++++--------- src/stores/user/index.ts | 2 ++ 5 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 src/data/schemas/nullable-defaults.ts diff --git a/src/app/chat/components/index.ts b/src/app/chat/components/index.ts index 5a830fd3..e11b2589 100644 --- a/src/app/chat/components/index.ts +++ b/src/app/chat/components/index.ts @@ -10,7 +10,6 @@ export * from "./chat-input-bar"; export * from "./chat-input-text-field"; export * from "./chat-quota-exhausted-banner"; export * from "./chat-screen"; - export * from "./chat-send-button"; export * from "./date-header"; export * from "./fullscreen-image-viewer"; diff --git a/src/app/subscription/components/index.ts b/src/app/subscription/components/index.ts index ac8be8c5..cc0d758f 100644 --- a/src/app/subscription/components/index.ts +++ b/src/app/subscription/components/index.ts @@ -5,7 +5,9 @@ export * from "./subscription-back-link"; export * from "./subscription-banner"; export * from "./subscription-benefits-card"; +export * from "./subscription-checkout-button"; export * from "./subscription-cta-button"; +export * from "./subscription-manage-button"; export * from "./subscription-plan-card"; export * from "./subscription-screen"; export * from "./subscription-user-row"; diff --git a/src/data/schemas/nullable-defaults.ts b/src/data/schemas/nullable-defaults.ts new file mode 100644 index 00000000..4d9df94b --- /dev/null +++ b/src/data/schemas/nullable-defaults.ts @@ -0,0 +1,33 @@ +/** + * Zod 默认值辅助器(处理 `null | undefined` → 标量默认) + * + * 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`), + * Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。 + * 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式: + * - input 接受 `string | null | undefined`(保持 optional) + * - output 永远是 `string`(User 类的 `email: string` 声明不会破) + * + * 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。 + */ +import { z } from "zod"; + +/** `string | null | undefined` → `string`(默认空串) */ +export const stringOrEmpty = z + .string() + .nullable() + .transform((v) => v ?? "") + .default(""); + +/** `number | null | undefined` → `number`(默认 0) */ +export const numberOrZero = z + .number() + .nullable() + .transform((v) => v ?? 0) + .default(0); + +/** `boolean | null | undefined` → `boolean`(默认 false) */ +export const booleanOrFalse = z + .boolean() + .nullable() + .transform((v) => v ?? false) + .default(false); \ No newline at end of file diff --git a/src/data/schemas/user/user.ts b/src/data/schemas/user/user.ts index b2c9f9d8..d1bae781 100644 --- a/src/data/schemas/user/user.ts +++ b/src/data/schemas/user/user.ts @@ -4,19 +4,11 @@ */ import { z } from "zod"; import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits"; - -/** - * 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`), - * Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。 - * 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式: - * - input 接受 `string | null | undefined`(保持 optional) - * - output 永远是 `string`(User 类的 `email: string` 声明不会破) - * - * 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。 - */ -const stringOrEmpty = z.string().nullable().transform((v) => v ?? "").default(""); -const numberOrZero = z.number().nullable().transform((v) => v ?? 0).default(0); -const booleanOrFalse = z.boolean().nullable().transform((v) => v ?? false).default(false); +import { + booleanOrFalse, + numberOrZero, + stringOrEmpty, +} from "../nullable-defaults"; export const UserSchema = z.object({ id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御 diff --git a/src/stores/user/index.ts b/src/stores/user/index.ts index be8f44b5..b6c636b5 100644 --- a/src/stores/user/index.ts +++ b/src/stores/user/index.ts @@ -4,5 +4,7 @@ export * from "./user-context"; export * from "./user-events"; +export * from "./user-machine.actors"; +export * from "./user-machine.helpers"; export * from "./user-machine"; export * from "./user-state"; From 1152491088ac5f0be91e0bbdedf87f6469c151fd Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 12:09:40 +0800 Subject: [PATCH 2/3] refactor(repositories): use barrel imports --- src/data/repositories/auth_repository.ts | 36 +++++++++---------- src/data/repositories/chat_repository.ts | 15 ++++---- .../interfaces/iauth_repository.ts | 10 +++--- .../interfaces/ichat_repository.ts | 8 +++-- .../interfaces/iuser_repository.ts | 12 ++++--- src/data/repositories/metrics_repository.ts | 5 ++- src/data/repositories/user_repository.ts | 14 ++++---- 7 files changed, 54 insertions(+), 46 deletions(-) diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index e5cbcdf7..22656eb9 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -13,25 +13,25 @@ * 原始 Dart: lib/data/repositories/auth_repository_impl.dart */ import { AuthApi, authApi } from "@/data/services/api"; -import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request"; -import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request"; -import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request"; -import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request"; -import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request"; -import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response"; -import { LoginRequest } from "@/data/dto/auth/login_request"; -import { LoginResponse } from "@/data/dto/auth/login_response"; -import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request"; -import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response"; -import { RegisterRequest } from "@/data/dto/auth/register_request"; -import { SendCodeRequest } from "@/data/dto/auth/send_code_request"; -import { User } from "@/data/dto/user/user"; +import { + AppleLoginRequest, + FacebookLoginRequest, + FbIdLoginRequest, + GoogleLoginRequest, + GuestLoginRequest, + GuestLoginResponse, + LoginRequest, + LoginResponse, + RefreshTokenRequest, + RefreshTokenResponse, + RegisterRequest, + SendCodeRequest, +} from "@/data/dto/auth"; +import { User } from "@/data/dto/user"; import { Result } from "@/utils/result"; -import type { IAuthRepository } from "@/data/repositories/interfaces/iauth_repository"; -import { AuthStorage } from "@/data/storage/auth/auth_storage"; -import type { IAuthStorage } from "@/data/storage/auth/iauth_storage"; -import { UserStorage } from "@/data/storage/user/user_storage"; -import type { IUserStorage } from "@/data/storage/user/iuser_storage"; +import type { IAuthRepository } from "@/data/repositories/interfaces"; +import { AuthStorage, type IAuthStorage } from "@/data/storage/auth"; +import { UserStorage, type IUserStorage } from "@/data/storage/user"; /** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`(Web 平台)。 */ const WEB_PLATFORM = "web"; diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 1c2fb6e7..a3540cfa 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -16,14 +16,15 @@ * 原始 Dart: lib/data/repositories/chat_repository_impl.dart */ 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"; -import { SendMessageRequest } from "@/data/dto/chat/send_message_request"; +import { + ChatHistoryResponse, + ChatMessage, + ChatSendResponse, + SendMessageRequest, +} from "@/data/dto/chat"; import { Result } from "@/utils/result"; -import type { IChatRepository } from "@/data/repositories/interfaces/ichat_repository"; -import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage"; -import { LocalMessage } from "@/data/storage/chat/local_message"; +import type { IChatRepository } from "@/data/repositories/interfaces"; +import { LocalChatStorage, LocalMessage } from "@/data/storage/chat"; export class ChatRepository implements IChatRepository { constructor( diff --git a/src/data/repositories/interfaces/iauth_repository.ts b/src/data/repositories/interfaces/iauth_repository.ts index e406d990..a3038cfb 100644 --- a/src/data/repositories/interfaces/iauth_repository.ts +++ b/src/data/repositories/interfaces/iauth_repository.ts @@ -14,10 +14,12 @@ */ import type { Result } from "@/utils/result"; -import type { GuestLoginResponse } from "@/data/dto/auth/guest_login_response"; -import type { LoginResponse } from "@/data/dto/auth/login_response"; -import type { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response"; -import type { User } from "@/data/dto/user/user"; +import type { + GuestLoginResponse, + LoginResponse, + RefreshTokenResponse, +} from "@/data/dto/auth"; +import type { User } from "@/data/dto/user"; export interface IAuthRepository { /** 用户注册。注册成功不会自动登录(与 Dart 行为一致)。 */ diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts index a0f52360..5ba54115 100644 --- a/src/data/repositories/interfaces/ichat_repository.ts +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -12,9 +12,11 @@ */ import type { Result } from "@/utils/result"; -import type { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response"; -import type { ChatMessage } from "@/data/dto/chat/chat_message"; -import type { ChatSendResponse } from "@/data/dto/chat/chat_send_response"; +import type { + ChatHistoryResponse, + ChatMessage, + ChatSendResponse, +} from "@/data/dto/chat"; export interface IChatRepository { /** 发送一条消息。 */ diff --git a/src/data/repositories/interfaces/iuser_repository.ts b/src/data/repositories/interfaces/iuser_repository.ts index ce1aefe7..185bd659 100644 --- a/src/data/repositories/interfaces/iuser_repository.ts +++ b/src/data/repositories/interfaces/iuser_repository.ts @@ -10,11 +10,13 @@ */ import type { Result } from "@/utils/result"; -import type { CreditsData } from "@/data/dto/user/credits_data"; -import type { CreditsHistoryData } from "@/data/dto/user/credits_history_data"; -import type { UpdateProfileRequest } from "@/data/dto/user/update_profile_request"; -import type { User } from "@/data/dto/user/user"; -import type { UserStatsResponse } from "@/data/dto/user/user_stats_response"; +import type { + CreditsData, + CreditsHistoryData, + UpdateProfileRequest, + User, + UserStatsResponse, +} from "@/data/dto/user"; export interface IUserRepository { /** 获取用户统计信息。 */ diff --git a/src/data/repositories/metrics_repository.ts b/src/data/repositories/metrics_repository.ts index 508a4c34..ab9c0703 100644 --- a/src/data/repositories/metrics_repository.ts +++ b/src/data/repositories/metrics_repository.ts @@ -8,10 +8,9 @@ * 原始 Dart: lib/data/repositories/metrics_repository_impl.dart */ import { MetricsApi, metricsApi } from "@/data/services/api"; -import { AppEvent } from "@/data/dto/metrics/app_event"; -import { PwaEvent } from "@/data/dto/metrics/pwa_event"; +import { AppEvent, PwaEvent } from "@/data/dto/metrics"; import { Result } from "@/utils/result"; -import type { IMetricsRepository } from "@/data/repositories/interfaces/imetrics_repository"; +import type { IMetricsRepository } from "@/data/repositories/interfaces"; export class MetricsRepository implements IMetricsRepository { constructor(private readonly api: MetricsApi) {} diff --git a/src/data/repositories/user_repository.ts b/src/data/repositories/user_repository.ts index b0bab76c..3e17bda1 100644 --- a/src/data/repositories/user_repository.ts +++ b/src/data/repositories/user_repository.ts @@ -9,13 +9,15 @@ * 原始 Dart: lib/data/repositories/user_repository_impl.dart */ 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"; -import { User } from "@/data/dto/user/user"; -import { UserStatsResponse } from "@/data/dto/user/user_stats_response"; +import { + CreditsData, + CreditsHistoryData, + UpdateProfileRequest, + User, + UserStatsResponse, +} from "@/data/dto/user"; import { Result } from "@/utils/result"; -import type { IUserRepository } from "@/data/repositories/interfaces/iuser_repository"; +import type { IUserRepository } from "@/data/repositories/interfaces"; export class UserRepository implements IUserRepository { constructor(private readonly api: UserApi) {} From c3f2f8d9540cbb4728c42ea4833ad111c26bc710 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 12:59:48 +0800 Subject: [PATCH 3/3] fix(sidebar): trigger logout from click --- src/app/sidebar/components/sidebar-screen.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/app/sidebar/components/sidebar-screen.tsx b/src/app/sidebar/components/sidebar-screen.tsx index 48c9eef9..d47a97be 100644 --- a/src/app/sidebar/components/sidebar-screen.tsx +++ b/src/app/sidebar/components/sidebar-screen.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { MobileShell } from "@/app/_components/core/mobile-shell"; @@ -9,8 +9,6 @@ import { ROUTES } from "@/router/routes"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; import { useUserDispatch, useUserState } from "@/stores/user/user-context"; -import type { UserView } from "@/models/user/user"; - import { BackBar } from "./back-bar"; import { UserHeader, type SidebarUserState } from "./user-header"; import { VipBenefitsCard } from "./vip-benefits-card"; @@ -41,16 +39,11 @@ export function SidebarScreen() { userDispatch({ type: "UserInit" }); }, [userDispatch]); - const prevUserRef = useRef(user.currentUser); - useEffect(() => { - const wasLoggedIn = prevUserRef.current != null; - const isNowLoggedOut = user.currentUser == null; - prevUserRef.current = user.currentUser; - if (wasLoggedIn && isNowLoggedOut) { - authDispatch({ type: "AuthReset" }); - router.replace(ROUTES.chat); - } - }, [user.currentUser, authDispatch, router]); + const handleLogoutClick = () => { + authDispatch({ type: "AuthReset" }); + userDispatch({ type: "UserLogout" }); + router.replace(ROUTES.chat); + }; // 状态派生:未登录 / 已登录未 VIP / 已登录 VIP const sidebarState: SidebarUserState = @@ -111,7 +104,7 @@ export function SidebarScreen() { id: "logout", title: "Log out", destructive: true, - onClick: () => userDispatch({ type: "UserLogout" }), + onClick: handleLogoutClick, }, ]} />