# Implementation Plan: Zod 重构数据模型 ## [Overview] 将 `src/data/models/` 下 28 个数据类从「手写 `readonly` + 手动 `??` 兜底 + `fromJson` 守卫」模式重构为「Zod schema 驱动 + 类包装」模式,样板代码量减少约 70%,同时获得运行时验证能力。 ## [Types] ### Schema 类型(每个数据类三个导出) 每个数据类使用同一份 Zod schema 同时驱动**验证、类型推导、默认值兜底**: | 导出 | 来源 | 用途 | |---|---|---| | `XxxSchema` | `z.object({...})` | 验证规则与默认值定义 | | `XxxInput` | `z.input` | 构造输入类型(字段可缺失,触发 `.default()`) | | `XxxData` | `z.output` | 解析后类型(所有字段已应用默认值) | | `class Xxx` | 手写 + `Object.assign` | 不可变类,保留自定义方法 | ### Zod 字段类型映射 | Dart 字段 | Zod schema | |---|---| | `required String id` | `z.string()` | | `@Default('web') String platform` | `z.string().default('web')` | | `String? email` | `z.string().default('')` | | `@Default(0) int intimacy` | `z.number().default(0)` | | `bool? isGuest` | `z.boolean().default(false)` | | `required List items` | `z.array(TItemSchema)` | | `PersonalityTraits? personalityTraits` | `PersonalityTraitsSchema.default({})` | | `Map` | `z.record(z.string(), z.unknown())` | ### 28 个类清单 **user/ (8)**: PersonalityTraits, User, CreditsData, CreditsHistoryData, AvatarData, UpdateProfileRequest, RecentMemory, UserStatsResponse **auth/ 请求 (9)**: SendCodeRequest, RegisterRequest, GoogleLoginRequest, FacebookLoginRequest, GuestLoginRequest, LoginRequest, AppleLoginRequest, RefreshTokenRequest, FbIdLoginRequest **auth/ 响应 (4)**: LoginResponse, GuestLoginResponse, LogoutResponse, RefreshTokenResponse **chat/ (10)**: ChatMessage, ChatSendResponse, ChatHistoryResponse, ChatSyncRequest, ChatSyncData, SyncMessage, GuestChatQuota, ImageUploadResponse, SendMessageRequest, SttData **metrics/ (2)**: AppEvent, PwaEvent ## [Files] ### 新增 | 路径 | 用途 | |---|---| | `implementation_plan.md` | 本文档 | ### 修改 | 路径 | 修改 | |---|---| | `package.json` | 添加 `zod: ^3.23.0` 依赖 | | `src/data/models/**/*.ts` | 28 个模型文件全部重构为 Zod 驱动 + 类包装模式 | | `src/data/models/index.ts` | 不变(仍然 `export * from "./..."`) | | `src/data/models/auth/index.ts` | 不变 | | `src/data/models/chat/index.ts` | 不变 | | `src/data/models/metrics/index.ts` | 不变 | | `src/data/models/user/index.ts` | 不变 | | `pnpm-lock.yaml` | `pnpm install` 自动更新 | ### 删除 无(保留原文件结构,重写内容) ## [Functions] ### 移除的样板函数(每个类) | 函数 | 位置 | 移除原因 | |---|---|---| | `requireString` 局部辅助 | 每个 `fromJson` | 由 Zod schema 验证替代 | | `requireNumber` 局部辅助 | 每个 `fromJson` | 由 Zod schema 验证替代 | | 构造函数中 `params.x ?? defaultX` ×N | 每个类 | 由 `.default()` schema 替代 | ### 保留/调整的方法 | 方法 | 签名 | 变化 | |---|---|---| | `static fromJson(json)` | `(json: unknown) => Xxx` | 接收 `unknown`(更安全),内部 `Schema.parse(json)` | | `toJson()` | `() => XxxData` | 返回 `Schema.parse(this)`,保证输出一致性 | | 静态常量(`GuestChatQuota`) | `static readonly X` | 不变 | | 静态方法(`GuestChatQuota.threshold` 等) | `static get X()` | 不变 | | 实例方法(`GuestChatQuota.needsReset`) | `(todayString: string) => boolean` | 不变 | ## [Classes] ### 重构模式(统一范式) ```typescript import { z } from "zod"; // 1. Schema(单一数据源) export const XxxSchema = z.object({ field1: z.string(), field2: z.number().default(0), // ... }); export type XxxInput = z.input; export type XxxData = z.output; // 2. 类(包装 + 不可变 + 自定义方法) export class Xxx { // 仅类型声明,无运行时初始化 declare readonly field1: string; declare readonly field2: number; // ... private constructor(data: XxxData) { Object.assign(this, data); // 运行时填值 Object.freeze(this); // 不可变 } static from(input: XxxInput): Xxx { return new Xxx(XxxSchema.parse(input)); } static fromJson(json: unknown): Xxx { return Xxx.from(json as XxxInput); } toJson(): XxxData { return XxxSchema.parse(this); } } ``` ### 28 个类的具体调整 | 类 | 特殊处理 | |---|---| | `PersonalityTraits` | 嵌套引用基类,5 个 double 字段各有 `.default(0.5)` | | `User` | 引用 `PersonalityTraitsSchema.default({})` | | `UserStatsResponse` | 引用 `PersonalityTraitsSchema` + `RecentMemory` 数组 | | `LoginResponse` | 引用 `User` | | `GuestLoginResponse` | 引用 `User`(默认值 `new User({id:"",username:""})` 仍需保留逻辑) | | `ChatHistoryResponse` | 数组字段 `z.array(ChatMessageSchema)` | | `ChatSyncRequest` | 数组字段 `z.array(SyncMessageSchema)` | | `CreditsHistoryData` | `records: z.array(z.record(z.string(), z.unknown()))` | | `GuestChatQuota` | 保留所有静态常量/方法/实例方法 | | 其他 19 个 | 简单结构,直接映射 | ## [Dependencies] ### 新增 | 包 | 版本 | 用途 | |---|---|---| | `zod` | `^3.23.0` | 模式验证与类型推导 | ### 安装命令 ```bash pnpm add zod ``` ### 版本选择 选择 zod v3(最新稳定版),原因: - 文档完善,社区生态成熟 - 与 TypeScript 5 完全兼容 - 体积适中(按需 tree-shake 后约 10-15KB) - v4 仍在 beta,暂不采用 ## [Testing] ### 验证策略 每个批次完成后: - `npx tsc --noEmit` — 0 错误 - `pnpm lint` — 通过 全部完成后进行端到端测试: - 构造最小输入(仅 required 字段)→ `Model.fromJson({...})` → 验证默认值已应用 - 构造完整输入 → `Model.fromJson({...})` → `.toJson()` → 验证往返一致性 - 构造无效输入(缺失 required 字段)→ `Model.fromJson({...})` → 验证抛出 `ZodError` ### 端到端测试脚本(临时) 实施完成后在 `src/_test_zod.ts` 写入以下验证代码(完成后删除): ```typescript import { User, ChatMessage, AppEvent } from "@/data/models"; import { z } from "zod"; // 1. 最小输入 + 默认值 const u1 = User.fromJson({ id: "u1", username: "alice" }); console.assert(u1.email === ""); console.assert(u1.platform === "web"); console.assert(u1.intimacy === 0); console.assert(u1.personalityTraits.cheerful === 0.5); // 2. 完整输入 + 往返 const original = { id: "u1", username: "alice", email: "a@b.com", intimacy: 10 }; const u2 = User.fromJson(original); const json = u2.toJson(); console.assert(JSON.stringify(json) === JSON.stringify({...original, platform: "web", dolBalance: 0, ...})); // 3. 无效输入抛错 try { User.fromJson({ id: "u1" }); // 缺 username console.assert(false, "应该抛错"); } catch (e) { console.assert(e instanceof z.ZodError); } ``` ## [Implementation Order] 按依赖关系倒序执行,**5 个批次**: 1. **安装依赖**:`pnpm add zod`(修改 `package.json` + `pnpm-lock.yaml`) 2. **批次 A:`user/` (8 文件)** - `PersonalityTraits`(最底层,被 `User` 引用) - `User`(引用 `PersonalityTraits`) - `RecentMemory`(被 `UserStatsResponse` 引用) - `UserStatsResponse`(引用 `PersonalityTraits` + `RecentMemory`) - `CreditsData`、`CreditsHistoryData`、`AvatarData`、`UpdateProfileRequest` 3. **批次 B:`auth/` 请求 (9 文件)** — 独立 4. **批次 C:`auth/` 响应 (4 文件)** — 依赖 `User` 5. **批次 D:`chat/` (10 文件)** — 内部 `ChatMessage` 引用 6. **批次 E:`metrics/` (2 文件)** — 独立 7. **最终验证**:`tsc --noEmit` + `lint` + 端到端测试