/** * User 状态机:纯函数 + 数据加载 * * 从 `user-machine.ts` 抽出的"Helpers"段: * - InitData 形状 * - `toView`:DTO → UserView 映射(纯函数) * - `readInitData`:从 UserStorage 读 user + avatarUrl(并行) * - `userStorage` 单例:被 helpers 和 actors 共用 * * 设计目标: * - helpers 无 XState 依赖(可独立 import / 测试) * - actors 依赖 helpers(import) * - machine 依赖 actors(import)—— 不直接依赖 helpers */ import type { UserView } from "@/models/user/user"; import { UserStorage } from "@/data/storage/user/user_storage"; import { Result } from "@/utils/result"; // ============================================================ // Storage singleton // ============================================================ /** * UserStorage 单例 —— 被 helpers(readInitData)和 actors(userFetchActor / userLogoutActor) * 共同使用。从 helpers 导出,避免在 actors 里重复 `UserStorage.getInstance()`。 */ export const userStorage = UserStorage.getInstance(); // ============================================================ // InitData 形状 // ============================================================ /** * `userInitActor` 的输出形状(user-machine.initializing.onDone 用)—— * 拆分 user 和 avatarUrl 是因为它们来自 UserStorage 两个独立接口。 */ export interface InitData { user: UserView | null; avatarUrl: string | null; } // ============================================================ // DTO → UserView 映射 // ============================================================ /** * UserView DTO → UserView 视图模型(纯函数,可单测) * - 与 chat 的 `localMessagesToUi` 同模式 * - 默认值收敛在此:undefined → "" / 0 / false,避免下游到处判空 */ export function toView(u: { id: string; username: string; email?: string; avatarUrl?: string; intimacy?: number; dolBalance?: number; relationshipStage?: string; currentMood?: string; isGuest?: boolean; isVip?: boolean; voiceMinutesRemaining?: number; stripeCustomerId?: string | null; }): UserView { return { id: u.id, username: u.username, email: u.email ?? "", avatarUrl: u.avatarUrl ?? "", intimacy: u.intimacy ?? 0, dolBalance: u.dolBalance ?? 0, relationshipStage: u.relationshipStage ?? "", currentMood: u.currentMood ?? "", isGuest: u.isGuest ?? false, isVip: u.isVip ?? false, voiceMinutesRemaining: u.voiceMinutesRemaining ?? 0, stripeCustomerId: u.stripeCustomerId ?? null, }; } // ============================================================ // Init 数据加载(userInitActor 用) // ============================================================ /** * 并行读 user + avatarUrl(UserStorage 两个独立接口)。 * - 任意一个失败 / 无数据 → 对应字段返回 null,不抛错(与 chat 的 * `readAndSyncHistory` "失败不卡 init" 一致的设计) * - Result.isOk(_) && data 双重判空:Result 包装层 + 业务 null */ export async function readInitData(): Promise { const [userResult, avatarResult] = await Promise.all([ userStorage.getUser(), userStorage.getAvatarUrl(), ]); let user: UserView | null = null; if (Result.isOk(userResult) && userResult.data) { user = toView(userResult.data); } let avatarUrl: string | null = null; if (Result.isOk(avatarResult) && avatarResult.data && avatarResult.data.length > 0) { avatarUrl = avatarResult.data; } return { user, avatarUrl }; }