Files
cozsweet-frontend-nextjs/implementation_plan.md
T

7.7 KiB
Raw Blame History

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]

重构模式(统一范式)

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 模式验证与类型推导

安装命令

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 写入以下验证代码(完成后删除):

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. 批次 Auser/ (8 文件)
    • PersonalityTraits(最底层,被 User 引用)
    • User(引用 PersonalityTraits
    • RecentMemory(被 UserStatsResponse 引用)
    • UserStatsResponse(引用 PersonalityTraits + RecentMemory
    • CreditsDataCreditsHistoryDataAvatarDataUpdateProfileRequest
  3. 批次 Bauth/ 请求 (9 文件) — 独立
  4. 批次 Cauth/ 响应 (4 文件) — 依赖 User
  5. 批次 Dchat/ (10 文件) — 内部 ChatMessage 引用
  6. 批次 Emetrics/ (2 文件) — 独立
  7. 最终验证tsc --noEmit + lint + 端到端测试