Files
cozsweet-frontend-nextjs/implementation_plan.md
T

229 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Implementation Plan: Zod 重构数据模型
## [Overview]
`src/data/models/` 下 28 个数据类从「手写 `readonly` + 手动 `??` 兜底 + `fromJson` 守卫」模式重构为「Zod schema 驱动 + 类包装」模式,样板代码量减少约 70%,同时获得运行时验证能力。
## [Types]
### Schema 类型(每个数据类三个导出)
每个数据类使用同一份 Zod schema 同时驱动**验证、类型推导、默认值兜底**:
| 导出 | 来源 | 用途 |
|---|---|---|
| `XxxSchema` | `z.object({...})` | 验证规则与默认值定义 |
| `XxxInput` | `z.input<typeof XxxSchema>` | 构造输入类型(字段可缺失,触发 `.default()` |
| `XxxData` | `z.output<typeof XxxSchema>` | 解析后类型(所有字段已应用默认值) |
| `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<T> items` | `z.array(TItemSchema)` |
| `PersonalityTraits? personalityTraits` | `PersonalityTraitsSchema.default({})` |
| `Map<String, dynamic>` | `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<typeof XxxSchema>;
export type XxxData = z.output<typeof XxxSchema>;
// 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` + 端到端测试