From d7943f5f06085b2818a3d7727b3b60512f9ea8b8 Mon Sep 17 00:00:00 2001 From: chenhang Date: Mon, 8 Jun 2026 14:50:20 +0800 Subject: [PATCH] refactor(models): migrate 28 data models to Zod schema-driven pattern --- implementation_plan.md | 228 ++++++++++++++++++ package.json | 5 +- pnpm-lock.yaml | 3 + src/data/models/auth/apple_login_request.ts | 49 ++-- .../models/auth/facebook_login_request.ts | 61 ++--- src/data/models/auth/fb_id_login_request.ts | 49 ++-- src/data/models/auth/google_login_request.ts | 61 ++--- src/data/models/auth/guest_login_request.ts | 36 +-- src/data/models/auth/guest_login_response.ts | 95 +++----- src/data/models/auth/login_request.ts | 76 +++--- src/data/models/auth/login_response.ts | 54 ++--- src/data/models/auth/logout_response.ts | 32 ++- src/data/models/auth/refresh_token_request.ts | 36 +-- .../models/auth/refresh_token_response.ts | 59 ++--- src/data/models/auth/register_request.ts | 75 +++--- src/data/models/auth/send_code_request.ts | 36 +-- src/data/models/chat/chat_history_response.ts | 73 +++--- src/data/models/chat/chat_message.ts | 69 +++--- src/data/models/chat/chat_send_response.ts | 128 ++++------ src/data/models/chat/chat_sync_data.ts | 49 ++-- src/data/models/chat/chat_sync_request.ts | 35 +-- src/data/models/chat/guest_chat_quota.ts | 63 +++-- src/data/models/chat/image_upload_response.ts | 103 +++----- src/data/models/chat/send_message_request.ts | 108 +++------ src/data/models/chat/stt_data.ts | 34 ++- src/data/models/chat/sync_message.ts | 55 ++--- src/data/models/metrics/app_event.ts | 55 ++--- src/data/models/metrics/pwa_event.ts | 86 +++---- src/data/models/user/avatar_data.ts | 46 ++-- src/data/models/user/credits_data.ts | 46 ++-- src/data/models/user/credits_history_data.ts | 72 +++--- src/data/models/user/personality_traits.ts | 73 +++--- src/data/models/user/recent_memory.ts | 74 +++--- .../models/user/update_profile_request.ts | 62 +++-- src/data/models/user/user.ts | 177 +++++--------- src/data/models/user/user_stats_response.ts | 199 +++++---------- 36 files changed, 1204 insertions(+), 1358 deletions(-) create mode 100644 implementation_plan.md diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 00000000..37f4b4a8 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,228 @@ +# 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` + 端到端测试 diff --git a/package.json b/package.json index 372f9f3e..ac16a9b8 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,14 @@ "build": "next build", "start": "next start", "lint": "eslint", - "lint:fix": "eslint --fix", + "lint:fix": "eslint --fix", "generate-barrels": "barrelsby --delete" }, "dependencies": { "next": "16.2.7", "react": "19.2.4", - "react-dom": "19.2.4" + "react-dom": "19.2.4", + "zod": "^4.4.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e5ad650..e1b69443 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@tailwindcss/postcss': specifier: ^4 diff --git a/src/data/models/auth/apple_login_request.ts b/src/data/models/auth/apple_login_request.ts index 0f3dfd59..8b084f06 100644 --- a/src/data/models/auth/apple_login_request.ts +++ b/src/data/models/auth/apple_login_request.ts @@ -2,36 +2,35 @@ * Apple 登录请求 * 原始 Dart: AppleLoginRequest (lib/data/models/auth/auth_request.dart) */ -export class AppleLoginRequest { - readonly identityToken: string; - readonly platform: string; +import { z } from "zod"; - constructor(params: { identityToken: string; platform: string }) { - this.identityToken = params.identityToken; - this.platform = params.platform; +export const AppleLoginRequestSchema = z.object({ + identityToken: z.string(), + platform: z.string(), +}); + +export type AppleLoginRequestInput = z.input; +export type AppleLoginRequestData = z.output; + +export class AppleLoginRequest { + declare readonly identityToken: string; + declare readonly platform: string; + + private constructor(input: AppleLoginRequestInput) { + const data = AppleLoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - identityToken: this.identityToken, - platform: this.platform, - }; + static from(input: AppleLoginRequestInput): AppleLoginRequest { + return new AppleLoginRequest(input); } - static fromJson(json: Record): AppleLoginRequest { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `AppleLoginRequest.${key} is required and must be a string` - ); - } - return v; - }; - return new AppleLoginRequest({ - identityToken: requireString("identityToken"), - platform: requireString("platform"), - }); + static fromJson(json: unknown): AppleLoginRequest { + return AppleLoginRequest.from(json as AppleLoginRequestInput); + } + + toJson(): AppleLoginRequestData { + return AppleLoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/facebook_login_request.ts b/src/data/models/auth/facebook_login_request.ts index 6847b6fa..78617f57 100644 --- a/src/data/models/auth/facebook_login_request.ts +++ b/src/data/models/auth/facebook_login_request.ts @@ -1,47 +1,38 @@ /** * Facebook 登录请求 * 原始 Dart: FacebookLoginRequest (lib/data/models/auth/auth_request.dart) - * - * 注:原始 Dart 中 `guestId` 为 `String?`,按"全部非空"约定统一兜底为空串。 */ -export class FacebookLoginRequest { - readonly accessToken: string; - readonly platform: string; - readonly guestId: string; +import { z } from "zod"; - constructor(params: { - accessToken: string; - platform: string; - guestId?: string; - }) { - this.accessToken = params.accessToken; - this.platform = params.platform; - this.guestId = params.guestId ?? ""; +export const FacebookLoginRequestSchema = z.object({ + accessToken: z.string(), + platform: z.string(), + guestId: z.string().default(""), +}); + +export type FacebookLoginRequestInput = z.input; +export type FacebookLoginRequestData = z.output; + +export class FacebookLoginRequest { + declare readonly accessToken: string; + declare readonly platform: string; + declare readonly guestId: string; + + private constructor(input: FacebookLoginRequestInput) { + const data = FacebookLoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - accessToken: this.accessToken, - platform: this.platform, - guestId: this.guestId, - }; + static from(input: FacebookLoginRequestInput): FacebookLoginRequest { + return new FacebookLoginRequest(input); } - static fromJson(json: Record): FacebookLoginRequest { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `FacebookLoginRequest.${key} is required and must be a string` - ); - } - return v; - }; - return new FacebookLoginRequest({ - accessToken: requireString("accessToken"), - platform: requireString("platform"), - guestId: typeof json.guestId === "string" ? json.guestId : undefined, - }); + static fromJson(json: unknown): FacebookLoginRequest { + return FacebookLoginRequest.from(json as FacebookLoginRequestInput); + } + + toJson(): FacebookLoginRequestData { + return FacebookLoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/fb_id_login_request.ts b/src/data/models/auth/fb_id_login_request.ts index bc45f1a0..0925173a 100644 --- a/src/data/models/auth/fb_id_login_request.ts +++ b/src/data/models/auth/fb_id_login_request.ts @@ -1,37 +1,36 @@ /** * Facebook ID 登录请求 * 原始 Dart: FbIdLoginRequest (lib/data/models/auth/auth_request.dart) - * - * 注:原始 Dart 中 `avatarUrl` 为 `String?`,按"全部非空"约定统一兜底为空串。 */ -export class FbIdLoginRequest { - readonly fbId: string; - readonly avatarUrl: string; +import { z } from "zod"; - constructor(params: { fbId: string; avatarUrl?: string }) { - this.fbId = params.fbId; - this.avatarUrl = params.avatarUrl ?? ""; +export const FbIdLoginRequestSchema = z.object({ + fbId: z.string(), + avatarUrl: z.string().default(""), +}); + +export type FbIdLoginRequestInput = z.input; +export type FbIdLoginRequestData = z.output; + +export class FbIdLoginRequest { + declare readonly fbId: string; + declare readonly avatarUrl: string; + + private constructor(input: FbIdLoginRequestInput) { + const data = FbIdLoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - fbId: this.fbId, - avatarUrl: this.avatarUrl, - }; + static from(input: FbIdLoginRequestInput): FbIdLoginRequest { + return new FbIdLoginRequest(input); } - static fromJson(json: Record): FbIdLoginRequest { - const fbId = json.fbId; - if (typeof fbId !== "string") { - throw new Error( - "FbIdLoginRequest.fbId is required and must be a string" - ); - } - return new FbIdLoginRequest({ - fbId, - avatarUrl: - typeof json.avatarUrl === "string" ? json.avatarUrl : undefined, - }); + static fromJson(json: unknown): FbIdLoginRequest { + return FbIdLoginRequest.from(json as FbIdLoginRequestInput); + } + + toJson(): FbIdLoginRequestData { + return FbIdLoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/google_login_request.ts b/src/data/models/auth/google_login_request.ts index 9331c140..1c3eb419 100644 --- a/src/data/models/auth/google_login_request.ts +++ b/src/data/models/auth/google_login_request.ts @@ -1,47 +1,38 @@ /** * Google 登录请求 * 原始 Dart: GoogleLoginRequest (lib/data/models/auth/auth_request.dart) - * - * 注:原始 Dart 中 `guestId` 为 `String?`,按"全部非空"约定统一兜底为空串。 */ -export class GoogleLoginRequest { - readonly idToken: string; - readonly platform: string; - readonly guestId: string; +import { z } from "zod"; - constructor(params: { - idToken: string; - platform: string; - guestId?: string; - }) { - this.idToken = params.idToken; - this.platform = params.platform; - this.guestId = params.guestId ?? ""; +export const GoogleLoginRequestSchema = z.object({ + idToken: z.string(), + platform: z.string(), + guestId: z.string().default(""), +}); + +export type GoogleLoginRequestInput = z.input; +export type GoogleLoginRequestData = z.output; + +export class GoogleLoginRequest { + declare readonly idToken: string; + declare readonly platform: string; + declare readonly guestId: string; + + private constructor(input: GoogleLoginRequestInput) { + const data = GoogleLoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - idToken: this.idToken, - platform: this.platform, - guestId: this.guestId, - }; + static from(input: GoogleLoginRequestInput): GoogleLoginRequest { + return new GoogleLoginRequest(input); } - static fromJson(json: Record): GoogleLoginRequest { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `GoogleLoginRequest.${key} is required and must be a string` - ); - } - return v; - }; - return new GoogleLoginRequest({ - idToken: requireString("idToken"), - platform: requireString("platform"), - guestId: typeof json.guestId === "string" ? json.guestId : undefined, - }); + static fromJson(json: unknown): GoogleLoginRequest { + return GoogleLoginRequest.from(json as GoogleLoginRequestInput); + } + + toJson(): GoogleLoginRequestData { + return GoogleLoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/guest_login_request.ts b/src/data/models/auth/guest_login_request.ts index cfd6d695..4898ab33 100644 --- a/src/data/models/auth/guest_login_request.ts +++ b/src/data/models/auth/guest_login_request.ts @@ -2,25 +2,33 @@ * 游客登录请求 * 原始 Dart: GuestLoginRequest (lib/data/models/auth/auth_request.dart) */ -export class GuestLoginRequest { - readonly deviceId: string; +import { z } from "zod"; - constructor(params: { deviceId: string }) { - this.deviceId = params.deviceId; +export const GuestLoginRequestSchema = z.object({ + deviceId: z.string(), +}); + +export type GuestLoginRequestInput = z.input; +export type GuestLoginRequestData = z.output; + +export class GuestLoginRequest { + declare readonly deviceId: string; + + private constructor(input: GuestLoginRequestInput) { + const data = GuestLoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { deviceId: this.deviceId }; + static from(input: GuestLoginRequestInput): GuestLoginRequest { + return new GuestLoginRequest(input); } - static fromJson(json: Record): GuestLoginRequest { - const deviceId = json.deviceId; - if (typeof deviceId !== "string") { - throw new Error( - "GuestLoginRequest.deviceId is required and must be a string" - ); - } - return new GuestLoginRequest({ deviceId }); + static fromJson(json: unknown): GuestLoginRequest { + return GuestLoginRequest.from(json as GuestLoginRequestInput); + } + + toJson(): GuestLoginRequestData { + return GuestLoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/guest_login_response.ts b/src/data/models/auth/guest_login_response.ts index 00004957..5081e37a 100644 --- a/src/data/models/auth/guest_login_response.ts +++ b/src/data/models/auth/guest_login_response.ts @@ -1,73 +1,52 @@ /** * 游客登录响应 * 原始 Dart: GuestLoginResponse (lib/data/models/auth/auth_response.dart) - * - * 注:原始 Dart 中 `user` 为 `User?`,按"全部非空"约定兜底为默认 User 实例 - * (id/username 为空串,使用方可通过 user.id === "" 判定无用户数据)。 */ -import { User } from "../user/user"; +import { z } from "zod"; +import { User, UserSchema } from "../user/user"; + +export const GuestLoginResponseSchema = z.object({ + token: z.string(), + deviceId: z.string(), + userId: z.string(), + isDeviceUser: z.boolean().default(true), + expiresIn: z.number().default(2592000), + user: UserSchema.optional(), +}); + +export type GuestLoginResponseInput = z.input; +export type GuestLoginResponseData = z.output; export class GuestLoginResponse { - readonly token: string; - readonly deviceId: string; - readonly userId: string; - readonly isDeviceUser: boolean; - readonly expiresIn: number; - readonly user: User; + declare readonly token: string; + declare readonly deviceId: string; + declare readonly userId: string; + declare readonly isDeviceUser: boolean; + declare readonly expiresIn: number; + declare readonly user: User; - constructor(params: { - token: string; - deviceId: string; - userId: string; - isDeviceUser?: boolean; - expiresIn?: number; - user?: User; - }) { - this.token = params.token; - this.deviceId = params.deviceId; - this.userId = params.userId; - this.isDeviceUser = params.isDeviceUser ?? true; - this.expiresIn = params.expiresIn ?? 2592000; - this.user = params.user ?? new User({ id: "", username: "" }); + private constructor(data: GuestLoginResponseData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - token: this.token, - deviceId: this.deviceId, - userId: this.userId, - isDeviceUser: this.isDeviceUser, - expiresIn: this.expiresIn, - user: this.user.toJson(), - }; + static from(input: GuestLoginResponseInput): GuestLoginResponse { + const parsed = GuestLoginResponseSchema.parse(input); + return new GuestLoginResponse({ + ...parsed, + user: parsed.user ?? User.empty(), + }); } - static fromJson(json: Record): GuestLoginResponse { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `GuestLoginResponse.${key} is required and must be a string` - ); - } - return v; - }; - const userJson = json.user; - const user: User = - typeof userJson === "object" && userJson !== null - ? User.fromJson(userJson as Record) - : new User({ id: "", username: "" }); - return new GuestLoginResponse({ - token: requireString("token"), - deviceId: requireString("deviceId"), - userId: requireString("userId"), - isDeviceUser: - typeof json.isDeviceUser === "boolean" - ? json.isDeviceUser - : undefined, - expiresIn: typeof json.expiresIn === "number" ? json.expiresIn : undefined, - user, + static fromJson(json: unknown): GuestLoginResponse { + return GuestLoginResponse.from(json as GuestLoginResponseInput); + } + + toJson(): GuestLoginResponseData { + const { user, ...rest } = this; + return GuestLoginResponseSchema.parse({ + ...rest, + ...(user.id !== "" ? { user } : {}), }); } } diff --git a/src/data/models/auth/login_request.ts b/src/data/models/auth/login_request.ts index 5e49f28e..8b37fff3 100644 --- a/src/data/models/auth/login_request.ts +++ b/src/data/models/auth/login_request.ts @@ -1,58 +1,42 @@ /** * 登录请求 - 支持用户名或邮箱登录 * 原始 Dart: LoginRequest (lib/data/models/auth/auth_request.dart) - * - * 注:原始 Dart 中 `email`/`username`/`guestId` 为 `String?`,按"全部非空"约定统一兜底为空串。 - * 后端校验 email 与 username 至少传入一个。 */ -export class LoginRequest { - readonly email: string; - readonly username: string; - readonly password: string; - readonly platform: string; - readonly guestId: string; +import { z } from "zod"; - constructor(params: { - email?: string; - username?: string; - password: string; - platform: string; - guestId?: string; - }) { - this.email = params.email ?? ""; - this.username = params.username ?? ""; - this.password = params.password; - this.platform = params.platform; - this.guestId = params.guestId ?? ""; +export const LoginRequestSchema = z.object({ + email: z.string().default(""), + username: z.string().default(""), + password: z.string(), + platform: z.string(), + guestId: z.string().default(""), +}); + +export type LoginRequestInput = z.input; +export type LoginRequestData = z.output; + +export class LoginRequest { + declare readonly email: string; + declare readonly username: string; + declare readonly password: string; + declare readonly platform: string; + declare readonly guestId: string; + + private constructor(input: LoginRequestInput) { + const data = LoginRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - email: this.email, - username: this.username, - password: this.password, - platform: this.platform, - guestId: this.guestId, - }; + static from(input: LoginRequestInput): LoginRequest { + return new LoginRequest(input); } - static fromJson(json: Record): LoginRequest { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `LoginRequest.${key} is required and must be a string` - ); - } - return v; - }; - return new LoginRequest({ - email: typeof json.email === "string" ? json.email : undefined, - username: typeof json.username === "string" ? json.username : undefined, - password: requireString("password"), - platform: requireString("platform"), - guestId: typeof json.guestId === "string" ? json.guestId : undefined, - }); + static fromJson(json: unknown): LoginRequest { + return LoginRequest.from(json as LoginRequestInput); + } + + toJson(): LoginRequestData { + return LoginRequestSchema.parse(this); } } diff --git a/src/data/models/auth/login_response.ts b/src/data/models/auth/login_response.ts index 12b858a8..9df10d32 100644 --- a/src/data/models/auth/login_response.ts +++ b/src/data/models/auth/login_response.ts @@ -2,42 +2,38 @@ * 登录响应 * 原始 Dart: LoginResponse (lib/data/models/auth/auth_response.dart) */ -import { User } from "../user/user"; +import { z } from "zod"; +import { User, UserSchema } from "../user/user"; + +export const LoginResponseSchema = z.object({ + user: UserSchema, + token: z.string(), + refreshToken: z.string().default(""), +}); + +export type LoginResponseInput = z.input; +export type LoginResponseData = z.output; export class LoginResponse { - readonly user: User; - readonly token: string; - readonly refreshToken: string; + declare readonly user: User; + declare readonly token: string; + declare readonly refreshToken: string; - constructor(params: { user: User; token: string; refreshToken?: string }) { - this.user = params.user; - this.token = params.token; - this.refreshToken = params.refreshToken ?? ""; + private constructor(input: LoginResponseInput) { + const data = LoginResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - user: this.user.toJson(), - token: this.token, - refreshToken: this.refreshToken, - }; + static from(input: LoginResponseInput): LoginResponse { + return new LoginResponse(input); } - static fromJson(json: Record): LoginResponse { - const token = json.token; - if (typeof token !== "string") { - throw new Error("LoginResponse.token is required and must be a string"); - } - const userJson = json.user; - if (typeof userJson !== "object" || userJson === null) { - throw new Error("LoginResponse.user is required and must be an object"); - } - return new LoginResponse({ - user: User.fromJson(userJson as Record), - token, - refreshToken: - typeof json.refreshToken === "string" ? json.refreshToken : undefined, - }); + static fromJson(json: unknown): LoginResponse { + return LoginResponse.from(json as LoginResponseInput); + } + + toJson(): LoginResponseData { + return LoginResponseSchema.parse(this); } } diff --git a/src/data/models/auth/logout_response.ts b/src/data/models/auth/logout_response.ts index 53b54d5e..57a943d5 100644 --- a/src/data/models/auth/logout_response.ts +++ b/src/data/models/auth/logout_response.ts @@ -2,21 +2,33 @@ * 退出登录响应 * 原始 Dart: LogoutResponse (lib/data/models/auth/auth_response.dart) */ -export class LogoutResponse { - readonly success: boolean; +import { z } from "zod"; - constructor(params: { success?: boolean }) { - this.success = params.success ?? true; +export const LogoutResponseSchema = z.object({ + success: z.boolean().default(true), +}); + +export type LogoutResponseInput = z.input; +export type LogoutResponseData = z.output; + +export class LogoutResponse { + declare readonly success: boolean; + + private constructor(input: LogoutResponseInput) { + const data = LogoutResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { success: this.success }; + static from(input: LogoutResponseInput): LogoutResponse { + return new LogoutResponse(input); } - static fromJson(json: Record): LogoutResponse { - return new LogoutResponse({ - success: typeof json.success === "boolean" ? json.success : undefined, - }); + static fromJson(json: unknown): LogoutResponse { + return LogoutResponse.from(json as LogoutResponseInput); + } + + toJson(): LogoutResponseData { + return LogoutResponseSchema.parse(this); } } diff --git a/src/data/models/auth/refresh_token_request.ts b/src/data/models/auth/refresh_token_request.ts index 44feabb4..2fa504f7 100644 --- a/src/data/models/auth/refresh_token_request.ts +++ b/src/data/models/auth/refresh_token_request.ts @@ -2,25 +2,33 @@ * 刷新 Token 请求 * 原始 Dart: RefreshTokenRequest (lib/data/models/auth/auth_request.dart) */ -export class RefreshTokenRequest { - readonly refreshToken: string; +import { z } from "zod"; - constructor(params: { refreshToken: string }) { - this.refreshToken = params.refreshToken; +export const RefreshTokenRequestSchema = z.object({ + refreshToken: z.string(), +}); + +export type RefreshTokenRequestInput = z.input; +export type RefreshTokenRequestData = z.output; + +export class RefreshTokenRequest { + declare readonly refreshToken: string; + + private constructor(input: RefreshTokenRequestInput) { + const data = RefreshTokenRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { refreshToken: this.refreshToken }; + static from(input: RefreshTokenRequestInput): RefreshTokenRequest { + return new RefreshTokenRequest(input); } - static fromJson(json: Record): RefreshTokenRequest { - const refreshToken = json.refreshToken; - if (typeof refreshToken !== "string") { - throw new Error( - "RefreshTokenRequest.refreshToken is required and must be a string" - ); - } - return new RefreshTokenRequest({ refreshToken }); + static fromJson(json: unknown): RefreshTokenRequest { + return RefreshTokenRequest.from(json as RefreshTokenRequestInput); + } + + toJson(): RefreshTokenRequestData { + return RefreshTokenRequestSchema.parse(this); } } diff --git a/src/data/models/auth/refresh_token_response.ts b/src/data/models/auth/refresh_token_response.ts index 774bdc70..2e8635af 100644 --- a/src/data/models/auth/refresh_token_response.ts +++ b/src/data/models/auth/refresh_token_response.ts @@ -2,44 +2,37 @@ * 刷新 Token 响应 * 原始 Dart: RefreshTokenResponse (lib/data/models/auth/auth_response.dart) */ -export class RefreshTokenResponse { - readonly token: string; - readonly refreshToken: string; - readonly userId: string; +import { z } from "zod"; - constructor(params: { - token: string; - refreshToken: string; - userId: string; - }) { - this.token = params.token; - this.refreshToken = params.refreshToken; - this.userId = params.userId; +export const RefreshTokenResponseSchema = z.object({ + token: z.string(), + refreshToken: z.string(), + userId: z.string(), +}); + +export type RefreshTokenResponseInput = z.input; +export type RefreshTokenResponseData = z.output; + +export class RefreshTokenResponse { + declare readonly token: string; + declare readonly refreshToken: string; + declare readonly userId: string; + + private constructor(input: RefreshTokenResponseInput) { + const data = RefreshTokenResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - token: this.token, - refreshToken: this.refreshToken, - userId: this.userId, - }; + static from(input: RefreshTokenResponseInput): RefreshTokenResponse { + return new RefreshTokenResponse(input); } - static fromJson(json: Record): RefreshTokenResponse { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `RefreshTokenResponse.${key} is required and must be a string` - ); - } - return v; - }; - return new RefreshTokenResponse({ - token: requireString("token"), - refreshToken: requireString("refreshToken"), - userId: requireString("userId"), - }); + static fromJson(json: unknown): RefreshTokenResponse { + return RefreshTokenResponse.from(json as RefreshTokenResponseInput); + } + + toJson(): RefreshTokenResponseData { + return RefreshTokenResponseSchema.parse(this); } } diff --git a/src/data/models/auth/register_request.ts b/src/data/models/auth/register_request.ts index 70ba5868..63608ee5 100644 --- a/src/data/models/auth/register_request.ts +++ b/src/data/models/auth/register_request.ts @@ -1,57 +1,42 @@ /** * 注册请求 * 原始 Dart: RegisterRequest (lib/data/models/auth/auth_request.dart) - * - * 注:原始 Dart 中 `guestId` 为 `String?`,按"全部非空"约定统一兜底为空串。 */ -export class RegisterRequest { - readonly username: string; - readonly email: string; - readonly password: string; - readonly platform: string; - readonly guestId: string; +import { z } from "zod"; - constructor(params: { - username: string; - email: string; - password: string; - platform: string; - guestId?: string; - }) { - this.username = params.username; - this.email = params.email; - this.password = params.password; - this.platform = params.platform; - this.guestId = params.guestId ?? ""; +export const RegisterRequestSchema = z.object({ + username: z.string(), + email: z.string(), + password: z.string(), + platform: z.string(), + guestId: z.string().default(""), +}); + +export type RegisterRequestInput = z.input; +export type RegisterRequestData = z.output; + +export class RegisterRequest { + declare readonly username: string; + declare readonly email: string; + declare readonly password: string; + declare readonly platform: string; + declare readonly guestId: string; + + private constructor(input: RegisterRequestInput) { + const data = RegisterRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - username: this.username, - email: this.email, - password: this.password, - platform: this.platform, - guestId: this.guestId, - }; + static from(input: RegisterRequestInput): RegisterRequest { + return new RegisterRequest(input); } - static fromJson(json: Record): RegisterRequest { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `RegisterRequest.${key} is required and must be a string` - ); - } - return v; - }; - return new RegisterRequest({ - username: requireString("username"), - email: requireString("email"), - password: requireString("password"), - platform: requireString("platform"), - guestId: typeof json.guestId === "string" ? json.guestId : undefined, - }); + static fromJson(json: unknown): RegisterRequest { + return RegisterRequest.from(json as RegisterRequestInput); + } + + toJson(): RegisterRequestData { + return RegisterRequestSchema.parse(this); } } diff --git a/src/data/models/auth/send_code_request.ts b/src/data/models/auth/send_code_request.ts index 14282865..454392b5 100644 --- a/src/data/models/auth/send_code_request.ts +++ b/src/data/models/auth/send_code_request.ts @@ -2,25 +2,33 @@ * 发送验证码请求 * 原始 Dart: SendCodeRequest (lib/data/models/auth/auth_request.dart) */ -export class SendCodeRequest { - readonly email: string; +import { z } from "zod"; - constructor(params: { email: string }) { - this.email = params.email; +export const SendCodeRequestSchema = z.object({ + email: z.string(), +}); + +export type SendCodeRequestInput = z.input; +export type SendCodeRequestData = z.output; + +export class SendCodeRequest { + declare readonly email: string; + + private constructor(input: SendCodeRequestInput) { + const data = SendCodeRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { email: this.email }; + static from(input: SendCodeRequestInput): SendCodeRequest { + return new SendCodeRequest(input); } - static fromJson(json: Record): SendCodeRequest { - const email = json.email; - if (typeof email !== "string") { - throw new Error( - "SendCodeRequest.email is required and must be a string" - ); - } - return new SendCodeRequest({ email }); + static fromJson(json: unknown): SendCodeRequest { + return SendCodeRequest.from(json as SendCodeRequestInput); + } + + toJson(): SendCodeRequestData { + return SendCodeRequestSchema.parse(this); } } diff --git a/src/data/models/chat/chat_history_response.ts b/src/data/models/chat/chat_history_response.ts index f81cd14d..2781ab14 100644 --- a/src/data/models/chat/chat_history_response.ts +++ b/src/data/models/chat/chat_history_response.ts @@ -1,60 +1,41 @@ /** * 聊天历史响应 * 原始 Dart: ChatHistoryResponse (lib/data/models/chat/chat_response.dart) - * - * 注:原始 Dart 中 `total` 为 `int?`,按"全部非空"约定兜底为 0。 */ -import { ChatMessage } from "./chat_message"; +import { z } from "zod"; +import { ChatMessage, ChatMessageSchema } from "./chat_message"; + +export const ChatHistoryResponseSchema = z.object({ + messages: z.array(ChatMessageSchema), + total: z.number().default(0), + limit: z.number(), + offset: z.number(), +}); + +export type ChatHistoryResponseInput = z.input; +export type ChatHistoryResponseData = z.output; export class ChatHistoryResponse { - readonly messages: ChatMessage[]; - readonly total: number; - readonly limit: number; - readonly offset: number; + declare readonly messages: ChatMessage[]; + declare readonly total: number; + declare readonly limit: number; + declare readonly offset: number; - constructor(params: { - messages: ChatMessage[]; - total?: number; - limit: number; - offset: number; - }) { - this.messages = params.messages; - this.total = params.total ?? 0; - this.limit = params.limit; - this.offset = params.offset; + private constructor(input: ChatHistoryResponseInput) { + const data = ChatHistoryResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - messages: this.messages.map((m) => m.toJson()), - total: this.total, - limit: this.limit, - offset: this.offset, - }; + static from(input: ChatHistoryResponseInput): ChatHistoryResponse { + return new ChatHistoryResponse(input); } - static fromJson(json: Record): ChatHistoryResponse { - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error( - `ChatHistoryResponse.${key} is required and must be a number` - ); - } - return v; - }; - const rawMessages = json.messages; - const messages: ChatMessage[] = Array.isArray(rawMessages) - ? (rawMessages as Record[]).map((m) => - ChatMessage.fromJson(m) - ) - : []; - return new ChatHistoryResponse({ - messages, - total: typeof json.total === "number" ? json.total : undefined, - limit: requireNumber("limit"), - offset: requireNumber("offset"), - }); + static fromJson(json: unknown): ChatHistoryResponse { + return ChatHistoryResponse.from(json as ChatHistoryResponseInput); + } + + toJson(): ChatHistoryResponseData { + return ChatHistoryResponseSchema.parse(this); } } diff --git a/src/data/models/chat/chat_message.ts b/src/data/models/chat/chat_message.ts index 1d1be8c7..ae1d440e 100644 --- a/src/data/models/chat/chat_message.ts +++ b/src/data/models/chat/chat_message.ts @@ -1,53 +1,40 @@ /** * 聊天历史中的单条消息 * 原始 Dart: ChatMessage (lib/data/models/chat/chat_response.dart) - * - * 注:原始 Dart 中 `id`/`createdAt` 为 `String?`,按"全部非空"约定兜底为空串。 */ -export class ChatMessage { - readonly role: string; - readonly content: string; - readonly id: string; - readonly createdAt: string; +import { z } from "zod"; - constructor(params: { - role: string; - content: string; - id?: string; - createdAt?: string; - }) { - this.role = params.role; - this.content = params.content; - this.id = params.id ?? ""; - this.createdAt = params.createdAt ?? ""; +export const ChatMessageSchema = z.object({ + role: z.string(), + content: z.string(), + id: z.string().default(""), + createdAt: z.string().default(""), +}); + +export type ChatMessageInput = z.input; +export type ChatMessageData = z.output; + +export class ChatMessage { + declare readonly role: string; + declare readonly content: string; + declare readonly id: string; + declare readonly createdAt: string; + + private constructor(input: ChatMessageInput) { + const data = ChatMessageSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - role: this.role, - content: this.content, - id: this.id, - createdAt: this.createdAt, - }; + static from(input: ChatMessageInput): ChatMessage { + return new ChatMessage(input); } - static fromJson(json: Record): ChatMessage { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `ChatMessage.${key} is required and must be a string` - ); - } - return v; - }; - return new ChatMessage({ - role: requireString("role"), - content: requireString("content"), - id: typeof json.id === "string" ? json.id : undefined, - createdAt: - typeof json.createdAt === "string" ? json.createdAt : undefined, - }); + static fromJson(json: unknown): ChatMessage { + return ChatMessage.from(json as ChatMessageInput); + } + + toJson(): ChatMessageData { + return ChatMessageSchema.parse(this); } } diff --git a/src/data/models/chat/chat_send_response.ts b/src/data/models/chat/chat_send_response.ts index cdc063c3..248dc684 100644 --- a/src/data/models/chat/chat_send_response.ts +++ b/src/data/models/chat/chat_send_response.ts @@ -1,98 +1,54 @@ /** * 发送消息响应 * 原始 Dart: ChatSendResponse (lib/data/models/chat/chat_response.dart) - * - * 注:原始 Dart 中 `mode`/`currentMood`/`isGuest` 为可空字段,按"全部非空"约定兜底: - * - 字符串 → "" - * - bool → false - * - int → 0 */ -export class ChatSendResponse { - readonly mode: string; - readonly reply: string; - readonly voiceUrl: string; - readonly audioUrl: string; - readonly intimacyChange: number; - readonly newIntimacy: number; - readonly relationshipStage: string; - readonly currentMood: string; - readonly messageId: string; - readonly isGuest: boolean; - readonly timestamp: number; +import { z } from "zod"; - constructor(params: { - mode?: string; - reply: string; - voiceUrl?: string; - audioUrl?: string; - intimacyChange?: number; - newIntimacy?: number; - relationshipStage: string; - currentMood?: string; - messageId: string; - isGuest?: boolean; - timestamp?: number; - }) { - this.mode = params.mode ?? ""; - this.reply = params.reply; - this.voiceUrl = params.voiceUrl ?? ""; - this.audioUrl = params.audioUrl ?? ""; - this.intimacyChange = params.intimacyChange ?? 0; - this.newIntimacy = params.newIntimacy ?? 0; - this.relationshipStage = params.relationshipStage; - this.currentMood = params.currentMood ?? ""; - this.messageId = params.messageId; - this.isGuest = params.isGuest ?? false; - this.timestamp = params.timestamp ?? 0; +export const ChatSendResponseSchema = z.object({ + mode: z.string().default(""), + reply: z.string(), + voiceUrl: z.string().default(""), + audioUrl: z.string().default(""), + intimacyChange: z.number().default(0), + newIntimacy: z.number().default(0), + relationshipStage: z.string(), + currentMood: z.string().default(""), + messageId: z.string(), + isGuest: z.boolean().default(false), + timestamp: z.number().default(0), +}); + +export type ChatSendResponseInput = z.input; +export type ChatSendResponseData = z.output; + +export class ChatSendResponse { + declare readonly mode: string; + declare readonly reply: string; + declare readonly voiceUrl: string; + declare readonly audioUrl: string; + declare readonly intimacyChange: number; + declare readonly newIntimacy: number; + declare readonly relationshipStage: string; + declare readonly currentMood: string; + declare readonly messageId: string; + declare readonly isGuest: boolean; + declare readonly timestamp: number; + + private constructor(input: ChatSendResponseInput) { + const data = ChatSendResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - mode: this.mode, - reply: this.reply, - voiceUrl: this.voiceUrl, - audioUrl: this.audioUrl, - intimacyChange: this.intimacyChange, - newIntimacy: this.newIntimacy, - relationshipStage: this.relationshipStage, - currentMood: this.currentMood, - messageId: this.messageId, - isGuest: this.isGuest, - timestamp: this.timestamp, - }; + static from(input: ChatSendResponseInput): ChatSendResponse { + return new ChatSendResponse(input); } - static fromJson(json: Record): ChatSendResponse { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `ChatSendResponse.${key} is required and must be a string` - ); - } - return v; - }; - return new ChatSendResponse({ - mode: typeof json.mode === "string" ? json.mode : undefined, - reply: requireString("reply"), - voiceUrl: - typeof json.voiceUrl === "string" ? json.voiceUrl : undefined, - audioUrl: - typeof json.audioUrl === "string" ? json.audioUrl : undefined, - intimacyChange: - typeof json.intimacyChange === "number" - ? json.intimacyChange - : undefined, - newIntimacy: - typeof json.newIntimacy === "number" ? json.newIntimacy : undefined, - relationshipStage: requireString("relationshipStage"), - currentMood: - typeof json.currentMood === "string" ? json.currentMood : undefined, - messageId: requireString("messageId"), - isGuest: typeof json.isGuest === "boolean" ? json.isGuest : undefined, - timestamp: - typeof json.timestamp === "number" ? json.timestamp : undefined, - }); + static fromJson(json: unknown): ChatSendResponse { + return ChatSendResponse.from(json as ChatSendResponseInput); + } + + toJson(): ChatSendResponseData { + return ChatSendResponseSchema.parse(this); } } diff --git a/src/data/models/chat/chat_sync_data.ts b/src/data/models/chat/chat_sync_data.ts index a6c2287d..3f40e100 100644 --- a/src/data/models/chat/chat_sync_data.ts +++ b/src/data/models/chat/chat_sync_data.ts @@ -2,36 +2,35 @@ * 消息同步响应 * 原始 Dart: ChatSyncData (lib/data/models/chat/chat_sync.dart) */ -export class ChatSyncData { - readonly syncedCount: number; - readonly totalHistory: number; +import { z } from "zod"; - constructor(params: { syncedCount: number; totalHistory: number }) { - this.syncedCount = params.syncedCount; - this.totalHistory = params.totalHistory; +export const ChatSyncDataSchema = z.object({ + syncedCount: z.number(), + totalHistory: z.number(), +}); + +export type ChatSyncDataInput = z.input; +export type ChatSyncDataData = z.output; + +export class ChatSyncData { + declare readonly syncedCount: number; + declare readonly totalHistory: number; + + private constructor(input: ChatSyncDataInput) { + const data = ChatSyncDataSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - syncedCount: this.syncedCount, - totalHistory: this.totalHistory, - }; + static from(input: ChatSyncDataInput): ChatSyncData { + return new ChatSyncData(input); } - static fromJson(json: Record): ChatSyncData { - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error( - `ChatSyncData.${key} is required and must be a number` - ); - } - return v; - }; - return new ChatSyncData({ - syncedCount: requireNumber("syncedCount"), - totalHistory: requireNumber("totalHistory"), - }); + static fromJson(json: unknown): ChatSyncData { + return ChatSyncData.from(json as ChatSyncDataInput); + } + + toJson(): ChatSyncDataData { + return ChatSyncDataSchema.parse(this); } } diff --git a/src/data/models/chat/chat_sync_request.ts b/src/data/models/chat/chat_sync_request.ts index 2a2acec8..d4d7fc2e 100644 --- a/src/data/models/chat/chat_sync_request.ts +++ b/src/data/models/chat/chat_sync_request.ts @@ -2,27 +2,34 @@ * 消息同步请求 * 原始 Dart: ChatSyncRequest (lib/data/models/chat/chat_sync.dart) */ -import { SyncMessage } from "./sync_message"; +import { z } from "zod"; +import { SyncMessage, SyncMessageSchema } from "./sync_message"; + +export const ChatSyncRequestSchema = z.object({ + messages: z.array(SyncMessageSchema), +}); + +export type ChatSyncRequestInput = z.input; +export type ChatSyncRequestData = z.output; export class ChatSyncRequest { - readonly messages: SyncMessage[]; + declare readonly messages: SyncMessage[]; - constructor(params: { messages: SyncMessage[] }) { - this.messages = params.messages; + private constructor(input: ChatSyncRequestInput) { + const data = ChatSyncRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { messages: this.messages.map((m) => m.toJson()) }; + static from(input: ChatSyncRequestInput): ChatSyncRequest { + return new ChatSyncRequest(input); } - static fromJson(json: Record): ChatSyncRequest { - const rawMessages = json.messages; - const messages: SyncMessage[] = Array.isArray(rawMessages) - ? (rawMessages as Record[]).map((m) => - SyncMessage.fromJson(m) - ) - : []; - return new ChatSyncRequest({ messages }); + static fromJson(json: unknown): ChatSyncRequest { + return ChatSyncRequest.from(json as ChatSyncRequestInput); + } + + toJson(): ChatSyncRequestData { + return ChatSyncRequestSchema.parse(this); } } diff --git a/src/data/models/chat/guest_chat_quota.ts b/src/data/models/chat/guest_chat_quota.ts index 3ec456be..8a16c152 100644 --- a/src/data/models/chat/guest_chat_quota.ts +++ b/src/data/models/chat/guest_chat_quota.ts @@ -2,17 +2,23 @@ * 游客每日聊天配额数据 * 原始 Dart: GuestChatQuota (lib/data/models/chat/guest_chat_quota.dart) * - * 注:原始 Dart 中的静态常量与运行时环境判断方法(isDevelopment)已迁移为 - * 静态成员。环境检测逻辑(threshold / totalQuotaDefault 等)保留为方法, - * 内部使用 `process.env.NODE_ENV` 替代 Dart 中的 `AppEnvUtil.isDevelopment`。 - * `initial()` 工厂方法已迁移为静态方法;`needReset` getter 保留为实例方法。 - * - * Dart 中 `initial()` 依赖 `app_date.DateUtils.todayString()`, - * 在 TS 中由调用方通过 `date` 字段传入。 + * 注:保留原始 Dart 中的静态常量与运行时环境判断方法。 + * `process.env.NODE_ENV` 替代 Dart 的 `AppEnvUtil.isDevelopment`。 + * `initial()` 工厂方法与 `needReset` 实例方法保留。 */ +import { z } from "zod"; + +export const GuestChatQuotaSchema = z.object({ + remaining: z.number(), + date: z.string(), +}); + +export type GuestChatQuotaInput = z.input; +export type GuestChatQuotaData = z.output; + export class GuestChatQuota { - readonly remaining: number; - readonly date: string; + declare readonly remaining: number; + declare readonly date: string; // 静态常量 static readonly maxQuotaPerDay = 40; @@ -23,12 +29,24 @@ export class GuestChatQuota { static readonly defaultTotalQuotaDev = 5; static readonly quotaExhausted = 0; - constructor(params: { remaining: number; date: string }) { - this.remaining = params.remaining; - this.date = params.date; + private constructor(input: GuestChatQuotaInput) { + const data = GuestChatQuotaSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } + static from(input: GuestChatQuotaInput): GuestChatQuota { + return new GuestChatQuota(input); + } + + static fromJson(json: unknown): GuestChatQuota { + return GuestChatQuota.from(json as GuestChatQuotaInput); + } + + toJson(): GuestChatQuotaData { + return GuestChatQuotaSchema.parse(this); + } + /** * 配额耗尽状态值(便捷引用) */ @@ -89,25 +107,4 @@ export class GuestChatQuota { needsReset(todayString: string): boolean { return this.date !== todayString; } - - toJson(): Record { - return { - remaining: this.remaining, - date: this.date, - }; - } - - static fromJson(json: Record): GuestChatQuota { - const remaining = json.remaining; - const date = json.date; - if (typeof remaining !== "number") { - throw new Error( - "GuestChatQuota.remaining is required and must be a number" - ); - } - if (typeof date !== "string") { - throw new Error("GuestChatQuota.date is required and must be a string"); - } - return new GuestChatQuota({ remaining, date }); - } } diff --git a/src/data/models/chat/image_upload_response.ts b/src/data/models/chat/image_upload_response.ts index 04fbb080..221919f8 100644 --- a/src/data/models/chat/image_upload_response.ts +++ b/src/data/models/chat/image_upload_response.ts @@ -2,78 +2,47 @@ * 图片上传响应 * 原始 Dart: ImageUploadResponse (lib/data/models/chat/image_upload_response.dart) */ -export class ImageUploadResponse { - readonly success: boolean; - readonly imageId: string; - readonly thumbUrl: string; - readonly mediumUrl: string; - readonly originalUrl: string; - readonly width: number; - readonly height: number; - readonly bytes: number; +import { z } from "zod"; - constructor(params: { - success?: boolean; - imageId: string; - thumbUrl: string; - mediumUrl: string; - originalUrl: string; - width: number; - height: number; - bytes: number; - }) { - this.success = params.success ?? true; - this.imageId = params.imageId; - this.thumbUrl = params.thumbUrl; - this.mediumUrl = params.mediumUrl; - this.originalUrl = params.originalUrl; - this.width = params.width; - this.height = params.height; - this.bytes = params.bytes; +export const ImageUploadResponseSchema = z.object({ + success: z.boolean().default(true), + imageId: z.string(), + thumbUrl: z.string(), + mediumUrl: z.string(), + originalUrl: z.string(), + width: z.number(), + height: z.number(), + bytes: z.number(), +}); + +export type ImageUploadResponseInput = z.input; +export type ImageUploadResponseData = z.output; + +export class ImageUploadResponse { + declare readonly success: boolean; + declare readonly imageId: string; + declare readonly thumbUrl: string; + declare readonly mediumUrl: string; + declare readonly originalUrl: string; + declare readonly width: number; + declare readonly height: number; + declare readonly bytes: number; + + private constructor(input: ImageUploadResponseInput) { + const data = ImageUploadResponseSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - success: this.success, - imageId: this.imageId, - thumbUrl: this.thumbUrl, - mediumUrl: this.mediumUrl, - originalUrl: this.originalUrl, - width: this.width, - height: this.height, - bytes: this.bytes, - }; + static from(input: ImageUploadResponseInput): ImageUploadResponse { + return new ImageUploadResponse(input); } - static fromJson(json: Record): ImageUploadResponse { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `ImageUploadResponse.${key} is required and must be a string` - ); - } - return v; - }; - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error( - `ImageUploadResponse.${key} is required and must be a number` - ); - } - return v; - }; - return new ImageUploadResponse({ - success: typeof json.success === "boolean" ? json.success : undefined, - imageId: requireString("imageId"), - thumbUrl: requireString("thumbUrl"), - mediumUrl: requireString("mediumUrl"), - originalUrl: requireString("originalUrl"), - width: requireNumber("width"), - height: requireNumber("height"), - bytes: requireNumber("bytes"), - }); + static fromJson(json: unknown): ImageUploadResponse { + return ImageUploadResponse.from(json as ImageUploadResponseInput); + } + + toJson(): ImageUploadResponseData { + return ImageUploadResponseSchema.parse(this); } } diff --git a/src/data/models/chat/send_message_request.ts b/src/data/models/chat/send_message_request.ts index ab636a7b..436f0e3b 100644 --- a/src/data/models/chat/send_message_request.ts +++ b/src/data/models/chat/send_message_request.ts @@ -1,82 +1,50 @@ /** * 发送消息请求 - 用于 /api/chat/send 接口 * 原始 Dart: SendMessageRequest (lib/data/models/chat/send_message_request.dart) - * - * 注:原始 Dart 中所有图片相关字段为可空,按"全部非空"约定兜底: - * - 字符串 → "" - * - 数字 → 0 */ -export class SendMessageRequest { - readonly message: string; - readonly image: string; - readonly imageId: string; - readonly imageThumbUrl: string; - readonly imageMediumUrl: string; - readonly imageOriginalUrl: string; - readonly imageWidth: number; - readonly imageHeight: number; - readonly useWebSocket: boolean; +import { z } from "zod"; - constructor(params: { - message?: string; - image?: string; - imageId?: string; - imageThumbUrl?: string; - imageMediumUrl?: string; - imageOriginalUrl?: string; - imageWidth?: number; - imageHeight?: number; - useWebSocket?: boolean; - }) { - this.message = params.message ?? ""; - this.image = params.image ?? ""; - this.imageId = params.imageId ?? ""; - this.imageThumbUrl = params.imageThumbUrl ?? ""; - this.imageMediumUrl = params.imageMediumUrl ?? ""; - this.imageOriginalUrl = params.imageOriginalUrl ?? ""; - this.imageWidth = params.imageWidth ?? 0; - this.imageHeight = params.imageHeight ?? 0; - this.useWebSocket = params.useWebSocket ?? false; +export const SendMessageRequestSchema = z.object({ + message: z.string().default(""), + image: z.string().default(""), + imageId: z.string().default(""), + imageThumbUrl: z.string().default(""), + imageMediumUrl: z.string().default(""), + imageOriginalUrl: z.string().default(""), + imageWidth: z.number().default(0), + imageHeight: z.number().default(0), + useWebSocket: z.boolean().default(false), +}); + +export type SendMessageRequestInput = z.input; +export type SendMessageRequestData = z.output; + +export class SendMessageRequest { + declare readonly message: string; + declare readonly image: string; + declare readonly imageId: string; + declare readonly imageThumbUrl: string; + declare readonly imageMediumUrl: string; + declare readonly imageOriginalUrl: string; + declare readonly imageWidth: number; + declare readonly imageHeight: number; + declare readonly useWebSocket: boolean; + + private constructor(input: SendMessageRequestInput) { + const data = SendMessageRequestSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - message: this.message, - image: this.image, - imageId: this.imageId, - imageThumbUrl: this.imageThumbUrl, - imageMediumUrl: this.imageMediumUrl, - imageOriginalUrl: this.imageOriginalUrl, - imageWidth: this.imageWidth, - imageHeight: this.imageHeight, - useWebSocket: this.useWebSocket, - }; + static from(input: SendMessageRequestInput): SendMessageRequest { + return new SendMessageRequest(input); } - static fromJson(json: Record): SendMessageRequest { - return new SendMessageRequest({ - message: typeof json.message === "string" ? json.message : undefined, - image: typeof json.image === "string" ? json.image : undefined, - imageId: typeof json.imageId === "string" ? json.imageId : undefined, - imageThumbUrl: - typeof json.imageThumbUrl === "string" - ? json.imageThumbUrl - : undefined, - imageMediumUrl: - typeof json.imageMediumUrl === "string" - ? json.imageMediumUrl - : undefined, - imageOriginalUrl: - typeof json.imageOriginalUrl === "string" - ? json.imageOriginalUrl - : undefined, - imageWidth: - typeof json.imageWidth === "number" ? json.imageWidth : undefined, - imageHeight: - typeof json.imageHeight === "number" ? json.imageHeight : undefined, - useWebSocket: - typeof json.useWebSocket === "boolean" ? json.useWebSocket : undefined, - }); + static fromJson(json: unknown): SendMessageRequest { + return SendMessageRequest.from(json as SendMessageRequestInput); + } + + toJson(): SendMessageRequestData { + return SendMessageRequestSchema.parse(this); } } diff --git a/src/data/models/chat/stt_data.ts b/src/data/models/chat/stt_data.ts index 23349291..7f1f23f8 100644 --- a/src/data/models/chat/stt_data.ts +++ b/src/data/models/chat/stt_data.ts @@ -2,23 +2,33 @@ * STT(语音转文字)响应 * 原始 Dart: SttData (lib/data/models/chat/stt_response.dart) */ -export class SttData { - readonly text: string; +import { z } from "zod"; - constructor(params: { text: string }) { - this.text = params.text; +export const SttDataSchema = z.object({ + text: z.string(), +}); + +export type SttDataInput = z.input; +export type SttDataData = z.output; + +export class SttData { + declare readonly text: string; + + private constructor(input: SttDataInput) { + const data = SttDataSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { text: this.text }; + static from(input: SttDataInput): SttData { + return new SttData(input); } - static fromJson(json: Record): SttData { - const text = json.text; - if (typeof text !== "string") { - throw new Error("SttData.text is required and must be a string"); - } - return new SttData({ text }); + static fromJson(json: unknown): SttData { + return SttData.from(json as SttDataInput); + } + + toJson(): SttDataData { + return SttDataSchema.parse(this); } } diff --git a/src/data/models/chat/sync_message.ts b/src/data/models/chat/sync_message.ts index cb9d98b2..8cd5dccf 100644 --- a/src/data/models/chat/sync_message.ts +++ b/src/data/models/chat/sync_message.ts @@ -2,40 +2,37 @@ * 待同步的单条消息 * 原始 Dart: SyncMessage (lib/data/models/chat/chat_sync.dart) */ -export class SyncMessage { - readonly role: string; - readonly content: string; - readonly timestamp: string; +import { z } from "zod"; - constructor(params: { role: string; content: string; timestamp: string }) { - this.role = params.role; - this.content = params.content; - this.timestamp = params.timestamp; +export const SyncMessageSchema = z.object({ + role: z.string(), + content: z.string(), + timestamp: z.string(), +}); + +export type SyncMessageInput = z.input; +export type SyncMessageData = z.output; + +export class SyncMessage { + declare readonly role: string; + declare readonly content: string; + declare readonly timestamp: string; + + private constructor(input: SyncMessageInput) { + const data = SyncMessageSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - role: this.role, - content: this.content, - timestamp: this.timestamp, - }; + static from(input: SyncMessageInput): SyncMessage { + return new SyncMessage(input); } - static fromJson(json: Record): SyncMessage { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `SyncMessage.${key} is required and must be a string` - ); - } - return v; - }; - return new SyncMessage({ - role: requireString("role"), - content: requireString("content"), - timestamp: requireString("timestamp"), - }); + static fromJson(json: unknown): SyncMessage { + return SyncMessage.from(json as SyncMessageInput); + } + + toJson(): SyncMessageData { + return SyncMessageSchema.parse(this); } } diff --git a/src/data/models/metrics/app_event.ts b/src/data/models/metrics/app_event.ts index f7de520e..61c4dd17 100644 --- a/src/data/models/metrics/app_event.ts +++ b/src/data/models/metrics/app_event.ts @@ -2,40 +2,37 @@ * 应用事件上报请求 - 用于 /api/data/report-user-info 接口 * 原始 Dart: AppEvent (lib/data/models/metrics/app_event.dart) */ -export class AppEvent { - readonly userId: string; - readonly browser: string; - readonly userAgent: string; +import { z } from "zod"; - constructor(params: { userId: string; browser: string; userAgent: string }) { - this.userId = params.userId; - this.browser = params.browser; - this.userAgent = params.userAgent; +export const AppEventSchema = z.object({ + userId: z.string(), + browser: z.string(), + userAgent: z.string(), +}); + +export type AppEventInput = z.input; +export type AppEventData = z.output; + +export class AppEvent { + declare readonly userId: string; + declare readonly browser: string; + declare readonly userAgent: string; + + private constructor(input: AppEventInput) { + const data = AppEventSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - userId: this.userId, - browser: this.browser, - userAgent: this.userAgent, - }; + static from(input: AppEventInput): AppEvent { + return new AppEvent(input); } - static fromJson(json: Record): AppEvent { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `AppEvent.${key} is required and must be a string` - ); - } - return v; - }; - return new AppEvent({ - userId: requireString("userId"), - browser: requireString("browser"), - userAgent: requireString("userAgent"), - }); + static fromJson(json: unknown): AppEvent { + return AppEvent.from(json as AppEventInput); + } + + toJson(): AppEventData { + return AppEventSchema.parse(this); } } diff --git a/src/data/models/metrics/pwa_event.ts b/src/data/models/metrics/pwa_event.ts index af753aeb..13656969 100644 --- a/src/data/models/metrics/pwa_event.ts +++ b/src/data/models/metrics/pwa_event.ts @@ -2,67 +2,41 @@ * PWA 事件上报请求 - 用于 /api/metrics/pwa/event 接口 * 原始 Dart: PwaEvent (lib/data/models/metrics/pwa_event.dart) */ -export class PwaEvent { - readonly deviceId: string; - readonly deviceType: string; - readonly timestamp: number; - readonly pwaInstalled: boolean; - readonly pwaSupported: boolean; +import { z } from "zod"; - constructor(params: { - deviceId: string; - deviceType: string; - timestamp: number; - pwaInstalled?: boolean; - pwaSupported?: boolean; - }) { - this.deviceId = params.deviceId; - this.deviceType = params.deviceType; - this.timestamp = params.timestamp; - this.pwaInstalled = params.pwaInstalled ?? false; - this.pwaSupported = params.pwaSupported ?? false; +export const PwaEventSchema = z.object({ + deviceId: z.string(), + deviceType: z.string(), + timestamp: z.number(), + pwaInstalled: z.boolean().default(false), + pwaSupported: z.boolean().default(false), +}); + +export type PwaEventInput = z.input; +export type PwaEventData = z.output; + +export class PwaEvent { + declare readonly deviceId: string; + declare readonly deviceType: string; + declare readonly timestamp: number; + declare readonly pwaInstalled: boolean; + declare readonly pwaSupported: boolean; + + private constructor(input: PwaEventInput) { + const data = PwaEventSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - deviceId: this.deviceId, - deviceType: this.deviceType, - timestamp: this.timestamp, - pwaInstalled: this.pwaInstalled, - pwaSupported: this.pwaSupported, - }; + static from(input: PwaEventInput): PwaEvent { + return new PwaEvent(input); } - static fromJson(json: Record): PwaEvent { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `PwaEvent.${key} is required and must be a string` - ); - } - return v; - }; - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error(`PwaEvent.${key} is required and must be a number`); - } - return v; - }; - return new PwaEvent({ - deviceId: requireString("deviceId"), - deviceType: requireString("deviceType"), - timestamp: requireNumber("timestamp"), - pwaInstalled: - typeof json.pwaInstalled === "boolean" - ? json.pwaInstalled - : undefined, - pwaSupported: - typeof json.pwaSupported === "boolean" - ? json.pwaSupported - : undefined, - }); + static fromJson(json: unknown): PwaEvent { + return PwaEvent.from(json as PwaEventInput); + } + + toJson(): PwaEventData { + return PwaEventSchema.parse(this); } } diff --git a/src/data/models/user/avatar_data.ts b/src/data/models/user/avatar_data.ts index ebcea4df..78addf14 100644 --- a/src/data/models/user/avatar_data.ts +++ b/src/data/models/user/avatar_data.ts @@ -1,35 +1,35 @@ /** * 头像数据模型 - 对应后端 AvatarData schema * 原始 Dart: AvatarData (lib/data/models/user/user.dart) - * - * 注:原始 Dart 中 `avatarUrl` 为 `String?`,按"全部非空"约定兜底为空串。 */ -export class AvatarData { - readonly avatarUrl: string; - readonly userId: string; +import { z } from "zod"; - constructor(params: { avatarUrl?: string; userId: string }) { - this.avatarUrl = params.avatarUrl ?? ""; - this.userId = params.userId; +export const AvatarDataSchema = z.object({ + avatarUrl: z.string().default(""), + userId: z.string(), +}); + +export type AvatarDataInput = z.input; +export type AvatarDataData = z.output; + +export class AvatarData { + declare readonly avatarUrl: string; + declare readonly userId: string; + + private constructor(data: AvatarDataData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - avatarUrl: this.avatarUrl, - userId: this.userId, - }; + static from(input: AvatarDataInput): AvatarData { + return new AvatarData(AvatarDataSchema.parse(input)); } - static fromJson(json: Record): AvatarData { - const userId = json.userId; - if (typeof userId !== "string") { - throw new Error("AvatarData.userId is required and must be a string"); - } - return new AvatarData({ - userId, - avatarUrl: - typeof json.avatarUrl === "string" ? json.avatarUrl : undefined, - }); + static fromJson(json: unknown): AvatarData { + return AvatarData.from(json as AvatarDataInput); + } + + toJson(): AvatarDataData { + return AvatarDataSchema.parse(this); } } diff --git a/src/data/models/user/credits_data.ts b/src/data/models/user/credits_data.ts index 0f8b7f92..f8e3eeee 100644 --- a/src/data/models/user/credits_data.ts +++ b/src/data/models/user/credits_data.ts @@ -2,34 +2,34 @@ * 积分数据模型 - 对应后端 CreditsData schema * 原始 Dart: CreditsData (lib/data/models/user/user.dart) */ -export class CreditsData { - readonly userId: string; - readonly dolBalance: number; +import { z } from "zod"; - constructor(params: { userId: string; dolBalance: number }) { - this.userId = params.userId; - this.dolBalance = params.dolBalance; +export const CreditsDataSchema = z.object({ + userId: z.string(), + dolBalance: z.number(), +}); + +export type CreditsDataInput = z.input; +export type CreditsDataData = z.output; + +export class CreditsData { + declare readonly userId: string; + declare readonly dolBalance: number; + + private constructor(data: CreditsDataData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - userId: this.userId, - dolBalance: this.dolBalance, - }; + static from(input: CreditsDataInput): CreditsData { + return new CreditsData(CreditsDataSchema.parse(input)); } - static fromJson(json: Record): CreditsData { - const userId = json.userId; - const dolBalance = json.dolBalance; - if (typeof userId !== "string") { - throw new Error("CreditsData.userId is required and must be a string"); - } - if (typeof dolBalance !== "number") { - throw new Error( - "CreditsData.dolBalance is required and must be a number" - ); - } - return new CreditsData({ userId, dolBalance }); + static fromJson(json: unknown): CreditsData { + return CreditsData.from(json as CreditsDataInput); + } + + toJson(): CreditsDataData { + return CreditsDataSchema.parse(this); } } diff --git a/src/data/models/user/credits_history_data.ts b/src/data/models/user/credits_history_data.ts index f74e6986..62dba106 100644 --- a/src/data/models/user/credits_history_data.ts +++ b/src/data/models/user/credits_history_data.ts @@ -1,57 +1,39 @@ /** * 积分历史记录模型 - 对应后端 CreditsHistoryData schema * 原始 Dart: CreditsHistoryData (lib/data/models/user/user.dart) - * - * 注:原始 Dart 中 `records` 为 `List>`, - * 迁移为 `Record[]`。 */ -export class CreditsHistoryData { - readonly records: Record[]; - readonly total: number; - readonly limit: number; - readonly offset: number; +import { z } from "zod"; - constructor(params: { - records: Record[]; - total: number; - limit: number; - offset: number; - }) { - this.records = params.records; - this.total = params.total; - this.limit = params.limit; - this.offset = params.offset; +export const CreditsHistoryDataSchema = z.object({ + records: z.array(z.record(z.string(), z.unknown())), + total: z.number(), + limit: z.number(), + offset: z.number(), +}); + +export type CreditsHistoryDataInput = z.input; +export type CreditsHistoryDataData = z.output; + +export class CreditsHistoryData { + declare readonly records: Record[]; + declare readonly total: number; + declare readonly limit: number; + declare readonly offset: number; + + private constructor(data: CreditsHistoryDataData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - records: this.records, - total: this.total, - limit: this.limit, - offset: this.offset, - }; + static from(input: CreditsHistoryDataInput): CreditsHistoryData { + return new CreditsHistoryData(CreditsHistoryDataSchema.parse(input)); } - static fromJson(json: Record): CreditsHistoryData { - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error( - `CreditsHistoryData.${key} is required and must be a number` - ); - } - return v; - }; - const rawRecords = json.records; - const records: Record[] = Array.isArray(rawRecords) - ? (rawRecords as Record[]) - : []; - return new CreditsHistoryData({ - records, - total: requireNumber("total"), - limit: requireNumber("limit"), - offset: requireNumber("offset"), - }); + static fromJson(json: unknown): CreditsHistoryData { + return CreditsHistoryData.from(json as CreditsHistoryDataInput); + } + + toJson(): CreditsHistoryDataData { + return CreditsHistoryDataSchema.parse(this); } } diff --git a/src/data/models/user/personality_traits.ts b/src/data/models/user/personality_traits.ts index 3b3d46e2..f9bc397c 100644 --- a/src/data/models/user/personality_traits.ts +++ b/src/data/models/user/personality_traits.ts @@ -1,48 +1,49 @@ /** * 个性特征模型 * 原始 Dart: PersonalityTraits (lib/data/models/user/user.dart) - * - * 注:所有 double 字段在 Dart 中使用 @Default(0.5)。 */ -export class PersonalityTraits { - readonly cheerful: number; - readonly caring: number; - readonly playful: number; - readonly serious: number; - readonly romantic: number; +import { z } from "zod"; - constructor(params: { - cheerful?: number; - caring?: number; - playful?: number; - serious?: number; - romantic?: number; - }) { - this.cheerful = params.cheerful ?? 0.5; - this.caring = params.caring ?? 0.5; - this.playful = params.playful ?? 0.5; - this.serious = params.serious ?? 0.5; - this.romantic = params.romantic ?? 0.5; +export const PERSONALITY_TRAITS_DEFAULTS: PersonalityTraitsData = { + cheerful: 0.5, + caring: 0.5, + playful: 0.5, + serious: 0.5, + romantic: 0.5, +}; + +export const PersonalityTraitsSchema = z.object({ + cheerful: z.number().default(0.5), + caring: z.number().default(0.5), + playful: z.number().default(0.5), + serious: z.number().default(0.5), + romantic: z.number().default(0.5), +}); + +export type PersonalityTraitsInput = z.input; +export type PersonalityTraitsData = z.output; + +export class PersonalityTraits { + declare readonly cheerful: number; + declare readonly caring: number; + declare readonly playful: number; + declare readonly serious: number; + declare readonly romantic: number; + + private constructor(data: PersonalityTraitsData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - cheerful: this.cheerful, - caring: this.caring, - playful: this.playful, - serious: this.serious, - romantic: this.romantic, - }; + static from(input: PersonalityTraitsInput): PersonalityTraits { + return new PersonalityTraits(PersonalityTraitsSchema.parse(input)); } - static fromJson(json: Record): PersonalityTraits { - return new PersonalityTraits({ - cheerful: typeof json.cheerful === "number" ? json.cheerful : undefined, - caring: typeof json.caring === "number" ? json.caring : undefined, - playful: typeof json.playful === "number" ? json.playful : undefined, - serious: typeof json.serious === "number" ? json.serious : undefined, - romantic: typeof json.romantic === "number" ? json.romantic : undefined, - }); + static fromJson(json: unknown): PersonalityTraits { + return PersonalityTraits.from(json as PersonalityTraitsInput); + } + + toJson(): PersonalityTraitsData { + return PersonalityTraitsSchema.parse(this); } } diff --git a/src/data/models/user/recent_memory.ts b/src/data/models/user/recent_memory.ts index f66ebb7a..46f1084b 100644 --- a/src/data/models/user/recent_memory.ts +++ b/src/data/models/user/recent_memory.ts @@ -1,59 +1,39 @@ /** * 最近记忆模型 * 原始 Dart: RecentMemory (lib/data/models/user/recent_memory.dart) - * - * 注:原始 Dart 中 `createdAt` 为 `String?`,按"全部非空"约定兜底为空串。 */ -export class RecentMemory { - readonly type: string; - readonly content: string; - readonly importance: number; - readonly createdAt: string; +import { z } from "zod"; - constructor(params: { - type: string; - content: string; - importance: number; - createdAt?: string; - }) { - this.type = params.type; - this.content = params.content; - this.importance = params.importance; - this.createdAt = params.createdAt ?? ""; +export const RecentMemorySchema = z.object({ + type: z.string(), + content: z.string(), + importance: z.number(), + createdAt: z.string().default(""), +}); + +export type RecentMemoryInput = z.input; +export type RecentMemoryData = z.output; + +export class RecentMemory { + declare readonly type: string; + declare readonly content: string; + declare readonly importance: number; + declare readonly createdAt: string; + + private constructor(data: RecentMemoryData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - type: this.type, - content: this.content, - importance: this.importance, - createdAt: this.createdAt, - }; + static from(input: RecentMemoryInput): RecentMemory { + return new RecentMemory(RecentMemorySchema.parse(input)); } - static fromJson(json: Record): RecentMemory { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `RecentMemory.${key} is required and must be a string` - ); - } - return v; - }; - const importance = json.importance; - if (typeof importance !== "number") { - throw new Error( - "RecentMemory.importance is required and must be a number" - ); - } - return new RecentMemory({ - type: requireString("type"), - content: requireString("content"), - importance, - createdAt: - typeof json.createdAt === "string" ? json.createdAt : undefined, - }); + static fromJson(json: unknown): RecentMemory { + return RecentMemory.from(json as RecentMemoryInput); + } + + toJson(): RecentMemoryData { + return RecentMemorySchema.parse(this); } } diff --git a/src/data/models/user/update_profile_request.ts b/src/data/models/user/update_profile_request.ts index 507f5708..1b6136e7 100644 --- a/src/data/models/user/update_profile_request.ts +++ b/src/data/models/user/update_profile_request.ts @@ -1,47 +1,39 @@ /** * 更新个人资料请求 * 原始 Dart: UpdateProfileRequest (lib/data/models/user/user.dart) - * - * 注:原始 Dart 中所有字段为 `String?`,按"全部非空"约定兜底为空串。 */ -export class UpdateProfileRequest { - readonly username: string; - readonly nickname: string; - readonly preferredLanguage: string; - readonly currentMood: string; +import { z } from "zod"; - constructor(params: { - username?: string; - nickname?: string; - preferredLanguage?: string; - currentMood?: string; - }) { - this.username = params.username ?? ""; - this.nickname = params.nickname ?? ""; - this.preferredLanguage = params.preferredLanguage ?? ""; - this.currentMood = params.currentMood ?? ""; +export const UpdateProfileRequestSchema = z.object({ + username: z.string().default(""), + nickname: z.string().default(""), + preferredLanguage: z.string().default(""), + currentMood: z.string().default(""), +}); + +export type UpdateProfileRequestInput = z.input; +export type UpdateProfileRequestData = z.output; + +export class UpdateProfileRequest { + declare readonly username: string; + declare readonly nickname: string; + declare readonly preferredLanguage: string; + declare readonly currentMood: string; + + private constructor(data: UpdateProfileRequestData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - username: this.username, - nickname: this.nickname, - preferredLanguage: this.preferredLanguage, - currentMood: this.currentMood, - }; + static from(input: UpdateProfileRequestInput): UpdateProfileRequest { + return new UpdateProfileRequest(UpdateProfileRequestSchema.parse(input)); } - static fromJson(json: Record): UpdateProfileRequest { - return new UpdateProfileRequest({ - username: typeof json.username === "string" ? json.username : undefined, - nickname: typeof json.nickname === "string" ? json.nickname : undefined, - preferredLanguage: - typeof json.preferredLanguage === "string" - ? json.preferredLanguage - : undefined, - currentMood: - typeof json.currentMood === "string" ? json.currentMood : undefined, - }); + static fromJson(json: unknown): UpdateProfileRequest { + return UpdateProfileRequest.from(json as UpdateProfileRequestInput); + } + + toJson(): UpdateProfileRequestData { + return UpdateProfileRequestSchema.parse(this); } } diff --git a/src/data/models/user/user.ts b/src/data/models/user/user.ts index 3ba9d3b1..95066674 100644 --- a/src/data/models/user/user.ts +++ b/src/data/models/user/user.ts @@ -1,128 +1,77 @@ /** * 用户模型 - 对应后端 UserData schema * 原始 Dart: User (lib/data/models/user/user.dart) - * - * 注:原始 Dart 中 `email`/`personalityTraits`/`preferredLanguage`/`createdAt`/ - * `lastMessageAt`/`avatarUrl` 为可空字段,按"全部非空"约定统一兜底: - * - 字符串字段 → 空串 "" - * - PersonalityTraits → 默认实例(全 0.5) */ -import { PersonalityTraits } from "./personality_traits"; +import { z } from "zod"; +import { + PersonalityTraits, + PersonalityTraitsSchema, + PERSONALITY_TRAITS_DEFAULTS, +} from "./personality_traits"; + +export const UserSchema = z.object({ + id: z.string(), + username: z.string(), + email: z.string().default(""), + platform: z.string().default("web"), + intimacy: z.number().default(0), + dolBalance: z.number().default(0), + relationshipStage: z.string().default("密友"), + currentMood: z.string().default("happy"), + personalityTraits: PersonalityTraitsSchema.default( + PERSONALITY_TRAITS_DEFAULTS + ), + preferredLanguage: z.string().default(""), + createdAt: z.string().default(""), + lastMessageAt: z.string().default(""), + avatarUrl: z.string().default(""), + loginProvider: z.string().default("email"), + isGuest: z.boolean().default(false), +}); + +export type UserInput = z.input; +export type UserData = z.output; export class User { - readonly id: string; - readonly username: string; - readonly email: string; - readonly platform: string; - readonly intimacy: number; - readonly dolBalance: number; - readonly relationshipStage: string; - readonly currentMood: string; - readonly personalityTraits: PersonalityTraits; - readonly preferredLanguage: string; - readonly createdAt: string; - readonly lastMessageAt: string; - readonly avatarUrl: string; - readonly loginProvider: string; - readonly isGuest: boolean; + declare readonly id: string; + declare readonly username: string; + declare readonly email: string; + declare readonly platform: string; + declare readonly intimacy: number; + declare readonly dolBalance: number; + declare readonly relationshipStage: string; + declare readonly currentMood: string; + declare readonly personalityTraits: PersonalityTraits; + declare readonly preferredLanguage: string; + declare readonly createdAt: string; + declare readonly lastMessageAt: string; + declare readonly avatarUrl: string; + declare readonly loginProvider: string; + declare readonly isGuest: boolean; - constructor(params: { - id: string; - username: string; - email?: string; - platform?: string; - intimacy?: number; - dolBalance?: number; - relationshipStage?: string; - currentMood?: string; - personalityTraits?: PersonalityTraits; - preferredLanguage?: string; - createdAt?: string; - lastMessageAt?: string; - avatarUrl?: string; - loginProvider?: string; - isGuest?: boolean; - }) { - this.id = params.id; - this.username = params.username; - this.email = params.email ?? ""; - this.platform = params.platform ?? "web"; - this.intimacy = params.intimacy ?? 0; - this.dolBalance = params.dolBalance ?? 0; - this.relationshipStage = params.relationshipStage ?? "密友"; - this.currentMood = params.currentMood ?? "happy"; - this.personalityTraits = params.personalityTraits ?? new PersonalityTraits({}); - this.preferredLanguage = params.preferredLanguage ?? ""; - this.createdAt = params.createdAt ?? ""; - this.lastMessageAt = params.lastMessageAt ?? ""; - this.avatarUrl = params.avatarUrl ?? ""; - this.loginProvider = params.loginProvider ?? "email"; - this.isGuest = params.isGuest ?? false; + private constructor(input: UserInput) { + const data = UserSchema.parse(input); + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - id: this.id, - username: this.username, - email: this.email, - platform: this.platform, - intimacy: this.intimacy, - dolBalance: this.dolBalance, - relationshipStage: this.relationshipStage, - currentMood: this.currentMood, - personalityTraits: this.personalityTraits.toJson(), - preferredLanguage: this.preferredLanguage, - createdAt: this.createdAt, - lastMessageAt: this.lastMessageAt, - avatarUrl: this.avatarUrl, - loginProvider: this.loginProvider, - isGuest: this.isGuest, - }; + static from(input: UserInput): User { + return new User(input); } - static fromJson(json: Record): User { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error(`User.${key} is required and must be a string`); - } - return v; - }; - return new User({ - id: requireString("id"), - username: requireString("username"), - email: typeof json.email === "string" ? json.email : undefined, - platform: typeof json.platform === "string" ? json.platform : undefined, - intimacy: typeof json.intimacy === "number" ? json.intimacy : undefined, - dolBalance: - typeof json.dolBalance === "number" ? json.dolBalance : undefined, - relationshipStage: - typeof json.relationshipStage === "string" - ? json.relationshipStage - : undefined, - currentMood: - typeof json.currentMood === "string" ? json.currentMood : undefined, - personalityTraits: json.personalityTraits - ? PersonalityTraits.fromJson( - json.personalityTraits as Record - ) - : undefined, - preferredLanguage: - typeof json.preferredLanguage === "string" - ? json.preferredLanguage - : undefined, - createdAt: - typeof json.createdAt === "string" ? json.createdAt : undefined, - lastMessageAt: - typeof json.lastMessageAt === "string" ? json.lastMessageAt : undefined, - avatarUrl: - typeof json.avatarUrl === "string" ? json.avatarUrl : undefined, - loginProvider: - typeof json.loginProvider === "string" - ? json.loginProvider - : undefined, - isGuest: typeof json.isGuest === "boolean" ? json.isGuest : undefined, - }); + static fromJson(json: unknown): User { + return User.from(json as UserInput); + } + + /** + * 创建一个具有空 id/username 的 User 实例,用于可选 user 字段的默认占位。 + * 调用方可通过 `user.id === ""` 判定"无用户数据"。 + */ + static empty(): User { + return new User({ id: "", username: "" }); + } + + toJson(): UserData { + return UserSchema.parse(this); } } diff --git a/src/data/models/user/user_stats_response.ts b/src/data/models/user/user_stats_response.ts index cf50f0c5..93b1bb08 100644 --- a/src/data/models/user/user_stats_response.ts +++ b/src/data/models/user/user_stats_response.ts @@ -1,156 +1,71 @@ /** * 用户统计响应 * 原始 Dart: UserStatsResponse (lib/data/models/user/user_stats_response.dart) - * - * 注:原始 Dart 中 `lastMessageAt`/`accountCreatedAt`/`currentMood` 为 `String?`, - * `personalityTraits` 为 `PersonalityTraits?`,按"全部非空"约定兜底: - * - 字符串 → "" - * - PersonalityTraits → 默认实例 - * - recentMemories 缺失或 null 时使用 [] */ -import { PersonalityTraits } from "./personality_traits"; -import { RecentMemory } from "./recent_memory"; +import { z } from "zod"; +import { + PersonalityTraits, + PERSONALITY_TRAITS_DEFAULTS, + PersonalityTraitsSchema, +} from "./personality_traits"; +import { RecentMemory, RecentMemorySchema } from "./recent_memory"; + +export const UserStatsResponseSchema = z.object({ + userId: z.string(), + username: z.string(), + platform: z.string(), + intimacy: z.number(), + relationshipStage: z.string(), + dolBalance: z.number(), + totalMessages: z.number(), + lastMessageAt: z.string().default(""), + accountCreatedAt: z.string().default(""), + currentMood: z.string().default(""), + personalityTraits: PersonalityTraitsSchema.default( + PERSONALITY_TRAITS_DEFAULTS + ), + recentMemories: z.array(RecentMemorySchema).default([]), + promptTokensTotal: z.number().default(0), + completionTokensTotal: z.number().default(0), + cacheHitTokensTotal: z.number().default(0), + tokensTotal: z.number().default(0), +}); + +export type UserStatsResponseInput = z.input; +export type UserStatsResponseData = z.output; export class UserStatsResponse { - readonly userId: string; - readonly username: string; - readonly platform: string; - readonly intimacy: number; - readonly relationshipStage: string; - readonly dolBalance: number; - readonly totalMessages: number; - readonly lastMessageAt: string; - readonly accountCreatedAt: string; - readonly currentMood: string; - readonly personalityTraits: PersonalityTraits; - readonly recentMemories: RecentMemory[]; - readonly promptTokensTotal: number; - readonly completionTokensTotal: number; - readonly cacheHitTokensTotal: number; - readonly tokensTotal: number; + declare readonly userId: string; + declare readonly username: string; + declare readonly platform: string; + declare readonly intimacy: number; + declare readonly relationshipStage: string; + declare readonly dolBalance: number; + declare readonly totalMessages: number; + declare readonly lastMessageAt: string; + declare readonly accountCreatedAt: string; + declare readonly currentMood: string; + declare readonly personalityTraits: PersonalityTraits; + declare readonly recentMemories: RecentMemory[]; + declare readonly promptTokensTotal: number; + declare readonly completionTokensTotal: number; + declare readonly cacheHitTokensTotal: number; + declare readonly tokensTotal: number; - constructor(params: { - userId: string; - username: string; - platform: string; - intimacy: number; - relationshipStage: string; - dolBalance: number; - totalMessages: number; - lastMessageAt?: string; - accountCreatedAt?: string; - currentMood?: string; - personalityTraits?: PersonalityTraits; - recentMemories?: RecentMemory[]; - promptTokensTotal?: number; - completionTokensTotal?: number; - cacheHitTokensTotal?: number; - tokensTotal?: number; - }) { - this.userId = params.userId; - this.username = params.username; - this.platform = params.platform; - this.intimacy = params.intimacy; - this.relationshipStage = params.relationshipStage; - this.dolBalance = params.dolBalance; - this.totalMessages = params.totalMessages; - this.lastMessageAt = params.lastMessageAt ?? ""; - this.accountCreatedAt = params.accountCreatedAt ?? ""; - this.currentMood = params.currentMood ?? ""; - this.personalityTraits = - params.personalityTraits ?? new PersonalityTraits({}); - this.recentMemories = params.recentMemories ?? []; - this.promptTokensTotal = params.promptTokensTotal ?? 0; - this.completionTokensTotal = params.completionTokensTotal ?? 0; - this.cacheHitTokensTotal = params.cacheHitTokensTotal ?? 0; - this.tokensTotal = params.tokensTotal ?? 0; + private constructor(data: UserStatsResponseData) { + Object.assign(this, data); Object.freeze(this); } - toJson(): Record { - return { - userId: this.userId, - username: this.username, - platform: this.platform, - intimacy: this.intimacy, - relationshipStage: this.relationshipStage, - dolBalance: this.dolBalance, - totalMessages: this.totalMessages, - lastMessageAt: this.lastMessageAt, - accountCreatedAt: this.accountCreatedAt, - currentMood: this.currentMood, - personalityTraits: this.personalityTraits.toJson(), - recentMemories: this.recentMemories.map((m) => m.toJson()), - promptTokensTotal: this.promptTokensTotal, - completionTokensTotal: this.completionTokensTotal, - cacheHitTokensTotal: this.cacheHitTokensTotal, - tokensTotal: this.tokensTotal, - }; + static from(input: UserStatsResponseInput): UserStatsResponse { + return new UserStatsResponse(UserStatsResponseSchema.parse(input)); } - static fromJson(json: Record): UserStatsResponse { - const requireString = (key: string): string => { - const v = json[key]; - if (typeof v !== "string") { - throw new Error( - `UserStatsResponse.${key} is required and must be a string` - ); - } - return v; - }; - const requireNumber = (key: string): number => { - const v = json[key]; - if (typeof v !== "number") { - throw new Error( - `UserStatsResponse.${key} is required and must be a number` - ); - } - return v; - }; - const rawMemories = json.recentMemories; - const recentMemories: RecentMemory[] = Array.isArray(rawMemories) - ? (rawMemories as Record[]).map((m) => - RecentMemory.fromJson(m) - ) - : []; - return new UserStatsResponse({ - userId: requireString("userId"), - username: requireString("username"), - platform: requireString("platform"), - intimacy: requireNumber("intimacy"), - relationshipStage: requireString("relationshipStage"), - dolBalance: requireNumber("dolBalance"), - totalMessages: requireNumber("totalMessages"), - lastMessageAt: - typeof json.lastMessageAt === "string" - ? json.lastMessageAt - : undefined, - accountCreatedAt: - typeof json.accountCreatedAt === "string" - ? json.accountCreatedAt - : undefined, - currentMood: - typeof json.currentMood === "string" ? json.currentMood : undefined, - personalityTraits: json.personalityTraits - ? PersonalityTraits.fromJson( - json.personalityTraits as Record - ) - : undefined, - recentMemories, - promptTokensTotal: - typeof json.promptTokensTotal === "number" - ? json.promptTokensTotal - : undefined, - completionTokensTotal: - typeof json.completionTokensTotal === "number" - ? json.completionTokensTotal - : undefined, - cacheHitTokensTotal: - typeof json.cacheHitTokensTotal === "number" - ? json.cacheHitTokensTotal - : undefined, - tokensTotal: - typeof json.tokensTotal === "number" ? json.tokensTotal : undefined, - }); + static fromJson(json: unknown): UserStatsResponse { + return UserStatsResponse.from(json as UserStatsResponseInput); + } + + toJson(): UserStatsResponseData { + return UserStatsResponseSchema.parse(this); } }