refactor(data): merge DTO models into schemas

This commit is contained in:
2026-07-17 12:47:18 +08:00
parent 2796010971
commit eac3d8f0a7
274 changed files with 1466 additions and 1956 deletions
+33 -57
View File
@@ -12,7 +12,7 @@ description: Sync this Next.js frontend with backend API documentation
当后端 API 新增、删除、字段结构调整、响应语义变化时,使用本 skill 更新: 当后端 API 新增、删除、字段结构调整、响应语义变化时,使用本 skill 更新:
- 网络层 - 网络层
- schema / DTO - schema / 不可变数据类
- repository - repository
- storage - storage
- XState store - XState store
@@ -26,15 +26,14 @@ description: Sync this Next.js frontend with backend API documentation
| --- | --- | --- | | --- | --- | --- |
| API Path | `src/data/services/api/api_path.ts` | 统一维护接口路径 | | API Path | `src/data/services/api/api_path.ts` | 统一维护接口路径 |
| API Service | `src/data/services/api/*_api.ts` | 调用 `httpClient` 并解析响应 | | API Service | `src/data/services/api/*_api.ts` | 调用 `httpClient` 并解析响应 |
| Schema | `src/data/schemas/<module>/*.ts` | Zod schema,负责防御性解析 | | Schema Model | `src/data/schemas/<module>/*.ts` | Zod schema 与不可变数据类,负责解析和序列化 |
| DTO | `src/data/dto/<module>/*.ts` | DTO class,封装 `fromJson/toJson` |
| Repository Interface | `src/data/repositories/interfaces/i*_repository.ts` | 仓库接口 | | Repository Interface | `src/data/repositories/interfaces/i*_repository.ts` | 仓库接口 |
| Repository | `src/data/repositories/*_repository.ts` | 业务仓库实现 | | Repository | `src/data/repositories/*_repository.ts` | 业务仓库实现 |
| Storage | `src/data/storage/*` | 本地持久化 | | Storage | `src/data/storage/*` | 本地持久化 |
| Store | `src/stores/<module>/*` | XState 状态机、actors、helpers、sync | | Store | `src/stores/<module>/*` | XState 状态机、actors、helpers、sync |
| UI | `src/app/**` | Next.js app router UI | | UI | `src/app/**` | Next.js app router UI |
| Mock | `src/data/mock/<module>/**` | 模拟请求 / 响应数据 | | Mock | `src/data/mock/<module>/**` | 模拟请求 / 响应数据 |
| Tests | `**/__tests__/*.test.ts` | DTO、helper、machine transition 测试 | | Tests | `**/__tests__/*.test.ts` | schema model、helper、machine transition 测试 |
## 后端文档来源 ## 后端文档来源
@@ -108,7 +107,7 @@ src/data/services/api/api_path.ts
static readonly userEntitlements = `${ApiPath._user}/entitlements`; static readonly userEntitlements = `${ApiPath._user}/entitlements`;
``` ```
### 4. 更新 Schema ### 4. 更新 Schema Model
在对应模块下新增或修改: 在对应模块下新增或修改:
@@ -123,6 +122,13 @@ src/data/schemas/<module>/*.ts
- 输入类型用 `z.input<typeof Schema>` - 输入类型用 `z.input<typeof Schema>`
- 对后端可能返回 `null` 的字段做防御性处理。 - 对后端可能返回 `null` 的字段做防御性处理。
- 不要在 schema 中写 UI 逻辑。 - 不要在 schema 中写 UI 逻辑。
- Schema 与对应不可变数据类必须定义在同一个文件中。
- 数据类的 `declare readonly` 只声明业务代码直接访问的字段。
- 后端返回但前端既不读取、也不透传的字段,不要加入 Schema。
- 仅用于请求序列化、响应兼容或内部透传的字段保留在 Schema 中,不声明 class 属性。
- 数据类统一使用 `from()``fromJson()``toJson()``Object.freeze(this)`
- 没有嵌套转换或自定义方法时,使用 `src/data/schemas/schema_model.ts``createSchemaModel()`
- 不要再创建独立的数据传输对象层。
示例: 示例:
@@ -134,36 +140,8 @@ export const XxxResponseSchema = z.object({
export type XxxResponseInput = z.input<typeof XxxResponseSchema>; export type XxxResponseInput = z.input<typeof XxxResponseSchema>;
export type XxxResponseData = z.output<typeof XxxResponseSchema>; export type XxxResponseData = z.output<typeof XxxResponseSchema>;
```
### 5. 更新 DTO
在对应模块下新增或修改:
```text
src/data/dto/<module>/*.ts
```
要求:
- DTO class 的 `declare readonly` 只声明前端业务代码会直接访问的字段。
- 不要因为后端响应包含某字段,就机械地为它增加 class 属性、嵌套类型或类型别名。
- 后端返回但前端既不读取、也不透传的字段,可以不加入 schema 和 DTO。
- 仅用于请求序列化、响应兼容或内部透传的字段保留在 schema 中,不添加 class 属性声明。
- `Object.assign(this, data)` 会保留 schema 解析后的字段;即使 class 未声明对应属性,`toJson()` 仍可按 schema 正常序列化。
- 新增 DTO 字段前先搜索实际调用点;没有直接读取方时默认不声明。
DTO 统一模式:
```ts
export const XxxResponseSchema = z.object({
id: z.string(),
// 仅用于内部透传,不需要在 DTO class 中声明。
traceId: z.string(),
});
export class XxxResponse { export class XxxResponse {
// 业务代码直接读取的字段才公开声明。
declare readonly id: string; declare readonly id: string;
private constructor(input: XxxResponseInput) { private constructor(input: XxxResponseInput) {
@@ -186,7 +164,7 @@ export class XxxResponse {
} }
``` ```
### 6. 更新 API Service ### 5. 更新 API Service
修改: 修改:
@@ -210,7 +188,7 @@ return XxxResponse.fromJson(unwrap(env) as Record<string, unknown>);
- 不要在 API 层处理 UI 行为。 - 不要在 API 层处理 UI 行为。
- multipart 仍使用 `FormData` - multipart 仍使用 `FormData`
### 7. 更新 Repository ### 6. 更新 Repository
修改: 修改:
@@ -229,14 +207,14 @@ async getXxx(): Promise<Result<XxxResponse>> {
Repository 可以做轻量编排,例如: Repository 可以做轻量编排,例如:
- request DTO 构造 - request 数据类构造
- 多接口组合 - 多接口组合
- 本地 storage 同步 - 本地 storage 同步
- local model 与 DTO 转换 - 本地模型与接口模型转换
不要把 React / UI 逻辑放进 repository。 不要把 React / UI 逻辑放进 repository。
### 8. 更新 Storage ### 7. 更新 Storage
如果接口影响本地持久化,修改: 如果接口影响本地持久化,修改:
@@ -262,7 +240,7 @@ getEntitlementSnapshot(): Promise<Result<UserEntitlementSnapshotData | null>> {
} }
``` ```
### 9. 更新 Store / 状态机 ### 8. 更新 Store / 状态机
如果接口影响状态流,修改: 如果接口影响状态流,修改:
@@ -284,7 +262,7 @@ src/stores/<module>/*-sync.tsx
- 页面组件不承担全局监听职责。 - 页面组件不承担全局监听职责。
- 状态机事件应表达业务事实,而不是 UI 操作细节。 - 状态机事件应表达业务事实,而不是 UI 操作细节。
### 10. 更新 UI ### 9. 更新 UI
只有在接口变更影响展示或用户明确要求时,才修改: 只有在接口变更影响展示或用户明确要求时,才修改:
@@ -298,19 +276,18 @@ src/app/**
- 不在页面组件里直接写复杂业务编排。 - 不在页面组件里直接写复杂业务编排。
- 全局监听逻辑优先放到 `stores/*/*-sync.tsx` - 全局监听逻辑优先放到 `stores/*/*-sync.tsx`
### 11. 更新桶文件 ### 10. 更新桶文件
如果新增 schema / DTO / component,更新对应 `index.ts` 如果新增 schema model 或 component,更新对应 `index.ts`
```text ```text
src/data/schemas/<module>/index.ts src/data/schemas/<module>/index.ts
src/data/dto/<module>/index.ts
src/app/**/components/index.ts src/app/**/components/index.ts
``` ```
如果项目使用 barrelsby 生成,不要手动破坏现有导出格式。 如果项目使用 barrelsby 生成,不要手动破坏现有导出格式。
### 12. 更新 Mock 数据 ### 11. 更新 Mock 数据
如果新增或调整数据结构,按一个结构一个文件的原则放到: 如果新增或调整数据结构,按一个结构一个文件的原则放到:
@@ -322,11 +299,11 @@ src/data/mock/<module>/websocket-events
不要把多个结构塞进一个大 JSON。 不要把多个结构塞进一个大 JSON。
### 13. 更新测试 ### 12. 更新测试
优先补充: 优先补充:
- DTO / schema parse 测试 - schema model 解析与序列化测试
- helper 纯函数测试 - helper 纯函数测试
- XState transition 测试 - XState transition 测试
- repository 编排测试(如已有测试基础) - repository 编排测试(如已有测试基础)
@@ -334,11 +311,11 @@ src/data/mock/<module>/websocket-events
常见位置: 常见位置:
```text ```text
src/data/dto/<module>/__tests__/*.test.ts src/data/schemas/<module>/__tests__/*.test.ts
src/stores/<module>/__tests__/*.test.ts src/stores/<module>/__tests__/*.test.ts
``` ```
### 14. 验证 ### 13. 验证
至少运行: 至少运行:
@@ -356,18 +333,17 @@ pnpm exec vitest run <related tests>
1. 文档确认 path / method / payload。 1. 文档确认 path / method / payload。
2. `api_path.ts` 新增路径。 2. `api_path.ts` 新增路径。
3. 新增 schema。 3. 在同一文件新增 schema 与不可变数据类
4. 新增 DTO 4. API service 新增方法
5. API service 新增方法。 5. repository interface + implementation 新增方法。
6. repository interface + implementation 新增方法 6. 如需要,接入 store actor / event
7. 如需要,接入 store actor / event 7. 补 mock 和测试
8. 补 mock 和测试 8. 运行验证
9. 运行验证。
### 修改响应字段 ### 修改响应字段
1. 更新 schema。 1. 更新 schema。
2. 更新 DTO declare 字段和 `toJson` 2. 更新同文件数据类的公开字段和 `toJson`
3. 更新 mapper/helper。 3. 更新 mapper/helper。
4. 更新 UI 或状态机依赖字段。 4. 更新 UI 或状态机依赖字段。
5. 更新 mock 和测试。 5. 更新 mock 和测试。
@@ -377,7 +353,7 @@ pnpm exec vitest run <related tests>
1. 搜索所有引用。 1. 搜索所有引用。
2. 删除 API path / API method / repository method。 2. 删除 API path / API method / repository method。
3. 删除 DTO/schema/mock/test 3. 删除 schema model、mock 和测试
4. 更新状态机和 UI 调用。 4. 更新状态机和 UI 调用。
5. 运行 `rg` 确认无残留引用。 5. 运行 `rg` 确认无残留引用。
6. 运行验证。 6. 运行验证。
-7
View File
@@ -12,13 +12,6 @@
"./src/data/repositories", "./src/data/repositories",
"./src/data/repositories/interfaces", "./src/data/repositories/interfaces",
"./src/data/services/api", "./src/data/services/api",
"./src/data/dto/auth",
"./src/data/dto/chat",
"./src/data/dto/character",
"./src/data/dto/feedback",
"./src/data/dto/feedback/response",
"./src/data/dto/metrics",
"./src/data/dto/user",
"./src/data/schemas/auth", "./src/data/schemas/auth",
"./src/data/schemas/auth/request", "./src/data/schemas/auth/request",
"./src/data/schemas/auth/response", "./src/data/schemas/auth/response",
+1 -1
View File
@@ -21,7 +21,7 @@ BACKEND_OPENAPI_SOURCE=https://backend.example.com/openapi.json pnpm contract:ch
检查会验证前端依赖的每个 method/path 是否仍由后端公开。路径参数名称不同 检查会验证前端依赖的每个 method/path 是否仍由后端公开。路径参数名称不同
(例如 `{albumId}``{album_id}`)不会产生误报。字段级兼容仍由前端 Zod (例如 `{albumId}``{album_id}`)不会产生误报。字段级兼容仍由前端 Zod
Schema 和 DTO 测试负责,避免仅凭 OpenAPI 生成代码覆盖现有防御性解析规则。 Schema Model 测试负责,避免仅凭 OpenAPI 生成代码覆盖现有防御性解析规则。
## CI 配置 ## CI 配置
@@ -2,7 +2,7 @@ import { act } from "react";
import { createRoot, type Root } from "react-dom/client"; import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PaymentPlan } from "@/data/dto/payment"; import { PaymentPlan } from "@/data/schemas/payment";
import { behaviorAnalytics } from "@/lib/analytics"; import { behaviorAnalytics } from "@/lib/analytics";
import { usePaymentPlanAnalytics } from "../use-payment-plan-analytics"; import { usePaymentPlanAnalytics } from "../use-payment-plan-analytics";
+1 -1
View File
@@ -2,7 +2,7 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import type { PaymentPlan } from "@/data/dto/payment"; import type { PaymentPlan } from "@/data/schemas/payment";
import { import {
behaviorAnalytics, behaviorAnalytics,
type PaymentAnalyticsContext, type PaymentAnalyticsContext,
+1 -1
View File
@@ -2,7 +2,7 @@
import { type Dispatch, useEffect, useRef } from "react"; import { type Dispatch, useEffect, useRef } from "react";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/schemas/payment";
import type { PendingPaymentSubscriptionType } from "@/lib/payment/pending_payment_order"; import type { PendingPaymentSubscriptionType } from "@/lib/payment/pending_payment_order";
import { import {
usePaymentDispatch, usePaymentDispatch,
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/stores/chat/ui-message";
import { import {
buildChatRenderItems, buildChatRenderItems,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/stores/chat/ui-message";
export type ChatMessageKeyResolver = (message: UiMessage) => string; export type ChatMessageKeyResolver = (message: UiMessage) => string;
+1 -1
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import type { ChatUpgradeReason } from "@/stores/chat/chat-state"; import type { ChatUpgradeReason } from "@/stores/chat/chat-state";
import { AppEnvUtil } from "@/utils/app-env"; import { AppEnvUtil } from "@/utils/app-env";
import { BrowserDetector } from "@/utils/browser-detect"; import { BrowserDetector } from "@/utils/browser-detect";
@@ -2,7 +2,7 @@ import { act } from "react";
import { createRoot, type Root } from "react-dom/client"; import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/stores/chat/ui-message";
import { ChatArea } from "../chat-area"; import { ChatArea } from "../chat-area";
+1 -1
View File
@@ -23,7 +23,7 @@ import {
} from "react"; } from "react";
import { LoaderCircle } from "lucide-react"; import { LoaderCircle } from "lucide-react";
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/stores/chat/ui-message";
import { DEFAULT_CHARACTER_ID } from "@/data/constants/character"; import { DEFAULT_CHARACTER_ID } from "@/data/constants/character";
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh"; import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
@@ -3,7 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { shallowEqual } from "@xstate/react"; import { shallowEqual } from "@xstate/react";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import { useAppNavigator } from "@/router/use-app-navigator"; import { useAppNavigator } from "@/router/use-app-navigator";
import { import {
usePaymentDispatch, usePaymentDispatch,
@@ -2,8 +2,8 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import type { UiMessage } from "@/data/dto/chat"; import type { UiMessage } from "@/stores/chat/ui-message";
import { resolveSplashLatestMessageCacheIdentity } from "@/lib/chat/splash_latest_message"; import { resolveSplashLatestMessageCacheIdentity } from "@/lib/chat/splash_latest_message";
import { getLatestSplashMessagePreview } from "@/lib/chat/splash_latest_message_preview"; import { getLatestSplashMessagePreview } from "@/lib/chat/splash_latest_message_preview";
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider"; import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
+1 -1
View File
@@ -1,6 +1,6 @@
import packageInfo from "../../../package.json"; import packageInfo from "../../../package.json";
import type { FeedbackContext } from "@/data/dto/feedback"; import type { FeedbackContext } from "@/data/schemas/feedback";
import { BrowserDetector } from "@/utils/browser-detect"; import { BrowserDetector } from "@/utils/browser-detect";
import { PlatformDetector } from "@/utils/platform-detect"; import { PlatformDetector } from "@/utils/platform-detect";
+1 -1
View File
@@ -16,7 +16,7 @@ import {
import { BackButton } from "@/app/_components"; import { BackButton } from "@/app/_components";
import { MobileShell } from "@/app/_components/core"; import { MobileShell } from "@/app/_components/core";
import type { FeedbackCategory } from "@/data/dto/feedback"; import type { FeedbackCategory } from "@/data/schemas/feedback";
import { ROUTES } from "@/router/routes"; import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator"; import { useAppNavigator } from "@/router/use-app-navigator";
+1 -1
View File
@@ -2,7 +2,7 @@
import { type FormEvent, useEffect, useRef, useState } from "react"; import { type FormEvent, useEffect, useRef, useState } from "react";
import type { FeedbackCategory } from "@/data/dto/feedback"; import type { FeedbackCategory } from "@/data/schemas/feedback";
import { submitFeedback } from "@/lib/feedback"; import { submitFeedback } from "@/lib/feedback";
import { createFeedbackContext } from "./feedback-context"; import { createFeedbackContext } from "./feedback-context";
@@ -2,7 +2,7 @@ import { act, type Dispatch } from "react";
import { createRoot, type Root } from "react-dom/client"; import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import { ROUTES } from "@/router/routes"; import { ROUTES } from "@/router/routes";
import type { PrivateRoomEvent } from "@/stores/private-room"; import type { PrivateRoomEvent } from "@/stores/private-room";
import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state"; import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state";
@@ -1,7 +1,7 @@
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { PrivateAlbum } from "@/data/dto/private-room"; import { PrivateAlbum } from "@/data/schemas/private-room";
import { PrivateAlbumCard } from "../private-album-card"; import { PrivateAlbumCard } from "../private-album-card";
@@ -3,7 +3,7 @@ import Image from "next/image";
import { ImageIcon, LockKeyhole } from "lucide-react"; import { ImageIcon, LockKeyhole } from "lucide-react";
import { CharacterAvatar } from "@/app/_components"; import { CharacterAvatar } from "@/app/_components";
import type { PrivateAlbum } from "@/data/dto/private-room"; import type { PrivateAlbum } from "@/data/schemas/private-room";
import { isPrivateAlbumLocked } from "@/lib/private-room/private_album"; import { isPrivateAlbumLocked } from "@/lib/private-room/private_album";
import styles from "../private-room-screen.module.css"; import styles from "../private-room-screen.module.css";
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
import Image from "next/image"; import Image from "next/image";
import { ChevronLeft, ChevronRight, X } from "lucide-react"; import { ChevronLeft, ChevronRight, X } from "lucide-react";
import type { PrivateAlbum } from "@/data/dto/private-room"; import type { PrivateAlbum } from "@/data/schemas/private-room";
import styles from "../private-room-screen.module.css"; import styles from "../private-room-screen.module.css";
@@ -1,4 +1,4 @@
import type { PrivateAlbum } from "@/data/dto/private-room"; import type { PrivateAlbum } from "@/data/schemas/private-room";
import styles from "../private-room-screen.module.css"; import styles from "../private-room-screen.module.css";
@@ -2,7 +2,7 @@
import { type Dispatch, useEffect, useRef } from "react"; import { type Dispatch, useEffect, useRef } from "react";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import { useGuestLoginBootstrap } from "@/hooks/use-guest-login-bootstrap"; import { useGuestLoginBootstrap } from "@/hooks/use-guest-login-bootstrap";
import { behaviorAnalytics } from "@/lib/analytics"; import { behaviorAnalytics } from "@/lib/analytics";
import { ROUTES } from "@/router/routes"; import { ROUTES } from "@/router/routes";
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { UserView } from "@/data/dto/user"; import type { UserView } from "@/stores/user/user-view";
import { import {
getSidebarViewModel, getSidebarViewModel,
+2 -2
View File
@@ -1,5 +1,5 @@
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import type { UserView } from "@/data/dto/user"; import type { UserView } from "@/stores/user/user-view";
export type SidebarUserState = "guest" | "member" | "vip"; export type SidebarUserState = "guest" | "member" | "vip";
@@ -2,7 +2,7 @@
import { type Dispatch, useEffect } from "react"; import { type Dispatch, useEffect } from "react";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
interface SidebarAuthBootstrapState { interface SidebarAuthBootstrapState {
hasInitialized: boolean; hasInitialized: boolean;
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import { useHasHydrated } from "@/hooks/use-has-hydrated"; import { useHasHydrated } from "@/hooks/use-has-hydrated";
import { loadSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message"; import { loadSplashLatestMessagePreview } from "@/lib/chat/splash_latest_message";
import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider"; import { useSplashLatestMessageCache } from "@/providers/splash-latest-message-provider";
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { PaymentPlan } from "@/data/dto/payment"; import { PaymentPlan } from "@/data/schemas/payment";
import { import {
canChooseSubscriptionPayChannel, canChooseSubscriptionPayChannel,
@@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/schemas/payment";
const PAYMENT_METHODS: { const PAYMENT_METHODS: {
channel: PayChannel; channel: PayChannel;
@@ -1,4 +1,4 @@
import type { PayChannel, PaymentPlan } from "@/data/dto/payment"; import type { PayChannel, PaymentPlan } from "@/data/schemas/payment";
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel"; import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
import { AppEnvUtil } from "@/utils/app-env"; import { AppEnvUtil } from "@/utils/app-env";
export { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel"; export { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
+1 -1
View File
@@ -5,7 +5,7 @@ import { BackButton } from "@/app/_components";
import { Checkbox, MobileShell } from "@/app/_components/core"; import { Checkbox, MobileShell } from "@/app/_components/core";
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics"; import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
import { AppConstants } from "@/core/app_constants"; import { AppConstants } from "@/core/app_constants";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/schemas/payment";
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit"; import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
import { import {
behaviorAnalytics, behaviorAnalytics,
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow"; import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow";
import { useAppNavigator } from "@/router/use-app-navigator"; import { useAppNavigator } from "@/router/use-app-navigator";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/schemas/payment";
import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit"; import type { SubscriptionReturnTo } from "@/lib/navigation/subscription_exit";
import type { SubscriptionType } from "./subscription-screen.helpers"; import type { SubscriptionType } from "./subscription-screen.helpers";
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { PaymentPlan } from "@/data/dto/payment"; import { PaymentPlan } from "@/data/schemas/payment";
import type { PaymentPlanInput } from "@/data/schemas/payment/payment_plan"; import type { PaymentPlanInput } from "@/data/schemas/payment/payment_plan";
import { import {
+2 -2
View File
@@ -1,5 +1,5 @@
import type { LoginStatus } from "@/data/dto/auth"; import type { LoginStatus } from "@/data/schemas/auth";
import type { PaymentPlan } from "@/data/dto/payment"; import type { PaymentPlan } from "@/data/schemas/payment";
import { import {
getTipCoffeeOption, getTipCoffeeOption,
type TipCoffeeType, type TipCoffeeType,
+1 -1
View File
@@ -8,7 +8,7 @@ import { CharacterAvatar } from "@/app/_components";
import { MobileShell } from "@/app/_components/core"; import { MobileShell } from "@/app/_components/core";
import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics"; import { usePaymentPlanAnalytics } from "@/app/_hooks/use-payment-plan-analytics";
import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow"; import { usePaymentRouteFlow } from "@/app/_hooks/use-payment-route-flow";
import type { PayChannel } from "@/data/dto/payment"; import type { PayChannel } from "@/data/schemas/payment";
import { import {
behaviorAnalytics, behaviorAnalytics,
type PaymentAnalyticsContext, type PaymentAnalyticsContext,
-39
View File
@@ -1,39 +0,0 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { createSchemaDto, type SchemaDto } from "../schema_dto";
const ExampleSchema = z.object({
id: z.string(),
traceId: z.string().default("generated-trace"),
});
type ExampleDto = SchemaDto<typeof ExampleSchema, "id">;
const ExampleDto = createSchemaDto<typeof ExampleSchema, "id">(
ExampleSchema,
);
describe("createSchemaDto", () => {
it("parses, freezes and serializes through the source schema", () => {
const dto: ExampleDto = ExampleDto.from({ id: "item-1" });
expect(dto).toBeInstanceOf(ExampleDto);
expect(Object.isFrozen(dto)).toBe(true);
expect(dto.id).toBe("item-1");
expect(dto.toJson()).toEqual({
id: "item-1",
traceId: "generated-trace",
});
});
it("keeps non-public schema fields out of the declared DTO interface", () => {
const dto = ExampleDto.fromJson({ id: "item-1", traceId: "trace-1" });
// @ts-expect-error traceId is serialized but not part of the public DTO API.
expect(dto.traceId).toBe("trace-1");
});
it("preserves schema validation errors", () => {
expect(() => ExampleDto.fromJson({ id: 1 })).toThrow(z.ZodError);
});
});
-35
View File
@@ -1,35 +0,0 @@
/**
* Facebook 用户数据 DTO
*
*
*/
import {
FacebookUserDataSchema,
type FacebookUserDataInput,
type FacebookUserDataData,
} from "@/data/schemas/auth/facebook_user_data";
export class FacebookUserData {
declare readonly id: string | null;
declare readonly name: string | null;
declare readonly email: string | null;
declare readonly pictureUrl: string | null;
private constructor(input: FacebookUserDataInput) {
const data = FacebookUserDataSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: FacebookUserDataInput): FacebookUserData {
return new FacebookUserData(input);
}
static fromJson(json: unknown): FacebookUserData {
return FacebookUserData.from(json as FacebookUserDataInput);
}
toJson(): FacebookUserDataData {
return FacebookUserDataSchema.parse(this);
}
}
-9
View File
@@ -1,9 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./auth_panel_mode";
export * from "./facebook_user_data";
export * from "./login_status";
export * from "./request";
export * from "./response";
@@ -1,17 +0,0 @@
/**
* Facebook identity 绑定请求 DTO
*/
import {
FacebookIdentityRequestSchema,
} from "@/data/schemas/auth/request/facebook_identity_request";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type FacebookIdentityRequest = SchemaDto<
typeof FacebookIdentityRequestSchema
>;
export const FacebookIdentityRequest = createSchemaDto(
FacebookIdentityRequestSchema,
);
@@ -1,33 +0,0 @@
/**
* Facebook 登录请求 DTO
*/
import {
FacebookLoginRequestSchema,
type FacebookLoginRequestInput,
type FacebookLoginRequestData,
} from "@/data/schemas/auth/request/facebook_login_request";
export class FacebookLoginRequest {
declare readonly accessToken: string;
declare readonly platform: string;
declare readonly guestId: string;
declare readonly isTestAccount: boolean;
private constructor(input: FacebookLoginRequestInput) {
const data = FacebookLoginRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: FacebookLoginRequestInput): FacebookLoginRequest {
return new FacebookLoginRequest(input);
}
static fromJson(json: unknown): FacebookLoginRequest {
return FacebookLoginRequest.from(json as FacebookLoginRequestInput);
}
toJson(): FacebookLoginRequestData {
return FacebookLoginRequestSchema.parse(this);
}
}
@@ -1,17 +0,0 @@
/**
* Facebook PSID 登录请求 DTO
*/
import {
FacebookPsidLoginRequestSchema,
} from "@/data/schemas/auth/request/facebook_psid_login_request";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type FacebookPsidLoginRequest = SchemaDto<
typeof FacebookPsidLoginRequestSchema
>;
export const FacebookPsidLoginRequest = createSchemaDto(
FacebookPsidLoginRequestSchema,
);
@@ -1,32 +0,0 @@
/**
* Facebook ID 登录请求 DTO
*/
import {
FbIdLoginRequestSchema,
type FbIdLoginRequestInput,
type FbIdLoginRequestData,
} from "@/data/schemas/auth/request/fb_id_login_request";
export class FbIdLoginRequest {
declare readonly fbId: string;
declare readonly avatarUrl: string;
declare readonly isTestAccount: boolean;
private constructor(input: FbIdLoginRequestInput) {
const data = FbIdLoginRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: FbIdLoginRequestInput): FbIdLoginRequest {
return new FbIdLoginRequest(input);
}
static fromJson(json: unknown): FbIdLoginRequest {
return FbIdLoginRequest.from(json as FbIdLoginRequestInput);
}
toJson(): FbIdLoginRequestData {
return FbIdLoginRequestSchema.parse(this);
}
}
@@ -1,33 +0,0 @@
/**
* Google 登录请求 DTO
*/
import {
GoogleLoginRequestSchema,
type GoogleLoginRequestInput,
type GoogleLoginRequestData,
} from "@/data/schemas/auth/request/google_login_request";
export class GoogleLoginRequest {
declare readonly idToken: string;
declare readonly platform: string;
declare readonly guestId: string;
declare readonly isTestAccount: boolean;
private constructor(input: GoogleLoginRequestInput) {
const data = GoogleLoginRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: GoogleLoginRequestInput): GoogleLoginRequest {
return new GoogleLoginRequest(input);
}
static fromJson(json: unknown): GoogleLoginRequest {
return GoogleLoginRequest.from(json as GoogleLoginRequestInput);
}
toJson(): GoogleLoginRequestData {
return GoogleLoginRequestSchema.parse(this);
}
}
@@ -1,31 +0,0 @@
/**
* 游客登录请求 DTO
*/
import {
GuestLoginRequestSchema,
type GuestLoginRequestInput,
type GuestLoginRequestData,
} from "@/data/schemas/auth/request/guest_login_request";
export class GuestLoginRequest {
declare readonly deviceId: string;
declare readonly isTestAccount: boolean;
private constructor(input: GuestLoginRequestInput) {
const data = GuestLoginRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: GuestLoginRequestInput): GuestLoginRequest {
return new GuestLoginRequest(input);
}
static fromJson(json: unknown): GuestLoginRequest {
return GuestLoginRequest.from(json as GuestLoginRequestInput);
}
toJson(): GuestLoginRequestData {
return GuestLoginRequestSchema.parse(this);
}
}
-9
View File
@@ -1,9 +0,0 @@
export * from "./facebook_identity_request";
export * from "./facebook_login_request";
export * from "./facebook_psid_login_request";
export * from "./fb_id_login_request";
export * from "./google_login_request";
export * from "./guest_login_request";
export * from "./login_request";
export * from "./refresh_token_request";
export * from "./register_request";
@@ -1,35 +0,0 @@
/**
* 登录请求 DTO
*/
import {
LoginRequestSchema,
type LoginRequestInput,
type LoginRequestData,
} from "@/data/schemas/auth/request/login_request";
export class LoginRequest {
declare readonly email: string;
declare readonly username: string;
declare readonly password: string;
declare readonly platform: string;
declare readonly guestId: string;
declare readonly isTestAccount: boolean;
private constructor(input: LoginRequestInput) {
const data = LoginRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: LoginRequestInput): LoginRequest {
return new LoginRequest(input);
}
static fromJson(json: unknown): LoginRequest {
return LoginRequest.from(json as LoginRequestInput);
}
toJson(): LoginRequestData {
return LoginRequestSchema.parse(this);
}
}
@@ -1,19 +0,0 @@
/**
* 刷新 Token 请求 DTO
*/
import {
RefreshTokenRequestSchema,
} from "@/data/schemas/auth/request/refresh_token_request";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type RefreshTokenRequest = SchemaDto<
typeof RefreshTokenRequestSchema,
"refreshToken"
>;
export const RefreshTokenRequest = createSchemaDto<
typeof RefreshTokenRequestSchema,
"refreshToken"
>(RefreshTokenRequestSchema);
@@ -1,35 +0,0 @@
/**
* 注册请求 DTO
*/
import {
RegisterRequestSchema,
type RegisterRequestInput,
type RegisterRequestData,
} from "@/data/schemas/auth/request/register_request";
export class RegisterRequest {
declare readonly username: string;
declare readonly email: string;
declare readonly password: string;
declare readonly platform: string;
declare readonly guestId: string;
declare readonly isTestAccount: boolean;
private constructor(input: RegisterRequestInput) {
const data = RegisterRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: RegisterRequestInput): RegisterRequest {
return new RegisterRequest(input);
}
static fromJson(json: unknown): RegisterRequest {
return RegisterRequest.from(json as RegisterRequestInput);
}
toJson(): RegisterRequestData {
return RegisterRequestSchema.parse(this);
}
}
@@ -1,17 +0,0 @@
/**
* Facebook identity 绑定响应 DTO
*/
import {
FacebookIdentityResponseSchema,
} from "@/data/schemas/auth/response/facebook_identity_response";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type FacebookIdentityResponse = SchemaDto<
typeof FacebookIdentityResponseSchema
>;
export const FacebookIdentityResponse = createSchemaDto(
FacebookIdentityResponseSchema,
);
@@ -1,32 +0,0 @@
/**
* Facebook PSID 登录响应 DTO
*/
import {
FacebookPsidLoginResponseSchema,
type FacebookPsidLoginResponseInput,
type FacebookPsidLoginResponseData,
} from "@/data/schemas/auth/response/facebook_psid_login_response";
export class FacebookPsidLoginResponse {
private constructor(input: FacebookPsidLoginResponseInput) {
const data = FacebookPsidLoginResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: FacebookPsidLoginResponseInput,
): FacebookPsidLoginResponse {
return new FacebookPsidLoginResponse(input);
}
static fromJson(json: unknown): FacebookPsidLoginResponse {
return FacebookPsidLoginResponse.from(
json as FacebookPsidLoginResponseInput,
);
}
toJson(): FacebookPsidLoginResponseData {
return FacebookPsidLoginResponseSchema.parse(this);
}
}
@@ -1,37 +0,0 @@
/**
* 游客登录响应 DTO
*/
import {
GuestLoginResponseSchema,
type GuestLoginResponseInput,
type GuestLoginResponseData,
} from "@/data/schemas/auth/response/guest_login_response";
import { User } from "@/data/dto/user/user";
export class GuestLoginResponse {
declare readonly token: string;
declare readonly userId: string;
declare readonly user: User;
private constructor(input: GuestLoginResponseInput) {
const parsed = GuestLoginResponseSchema.parse(input);
Object.assign(this, {
...parsed,
user: parsed.user ? User.fromJson(parsed.user) : User.empty(),
});
Object.freeze(this);
}
static from(input: GuestLoginResponseInput): GuestLoginResponse {
return new GuestLoginResponse(input);
}
static fromJson(json: unknown): GuestLoginResponse {
return GuestLoginResponse.from(json as GuestLoginResponseInput);
}
toJson(): GuestLoginResponseData {
const data = GuestLoginResponseSchema.parse(this);
return data;
}
}
-6
View File
@@ -1,6 +0,0 @@
export * from "./facebook_identity_response";
export * from "./facebook_psid_login_response";
export * from "./guest_login_response";
export * from "./login_response";
export * from "./logout_response";
export * from "./refresh_token_response";
@@ -1,36 +0,0 @@
/**
* 登录响应 DTO
*/
import {
LoginResponseSchema,
type LoginResponseInput,
type LoginResponseData,
} from "@/data/schemas/auth/response/login_response";
import { User } from "@/data/dto/user/user";
export class LoginResponse {
declare readonly user: User;
declare readonly token: string;
declare readonly refreshToken: string;
private constructor(input: LoginResponseInput) {
const data = LoginResponseSchema.parse(input);
Object.assign(this, {
...data,
user: User.fromJson(data.user),
});
Object.freeze(this);
}
static from(input: LoginResponseInput): LoginResponse {
return new LoginResponse(input);
}
static fromJson(json: unknown): LoginResponse {
return LoginResponse.from(json as LoginResponseInput);
}
toJson(): LoginResponseData {
return LoginResponseSchema.parse(this);
}
}
@@ -1,19 +0,0 @@
/**
* 退出登录响应 DTO
*/
import {
LogoutResponseSchema,
} from "@/data/schemas/auth/response/logout_response";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type LogoutResponse = SchemaDto<
typeof LogoutResponseSchema,
"success"
>;
export const LogoutResponse = createSchemaDto<
typeof LogoutResponseSchema,
"success"
>(LogoutResponseSchema);
@@ -1,31 +0,0 @@
/**
* 刷新 Token 响应 DTO
*/
import {
RefreshTokenResponseSchema,
type RefreshTokenResponseInput,
type RefreshTokenResponseData,
} from "@/data/schemas/auth/response/refresh_token_response";
export class RefreshTokenResponse {
declare readonly token: string;
declare readonly refreshToken: string;
private constructor(input: RefreshTokenResponseInput) {
const data = RefreshTokenResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: RefreshTokenResponseInput): RefreshTokenResponse {
return new RefreshTokenResponse(input);
}
static fromJson(json: unknown): RefreshTokenResponse {
return RefreshTokenResponse.from(json as RefreshTokenResponseInput);
}
toJson(): RefreshTokenResponseData {
return RefreshTokenResponseSchema.parse(this);
}
}
-29
View File
@@ -1,29 +0,0 @@
import {
CharacterSchema,
type CharacterData,
type CharacterInput,
} from "@/data/schemas/character";
export class Character {
declare readonly id: string;
declare readonly slug: string;
declare readonly displayName: string;
declare readonly avatarUrl: string;
private constructor(input: CharacterInput) {
Object.assign(this, CharacterSchema.parse(input));
Object.freeze(this);
}
static from(input: CharacterInput): Character {
return new Character(input);
}
static fromJson(json: unknown): Character {
return Character.from(json as CharacterInput);
}
toJson(): CharacterData {
return CharacterSchema.parse(this);
}
}
@@ -1,33 +0,0 @@
import {
CharactersResponseSchema,
type CharactersResponseData,
type CharactersResponseInput,
} from "@/data/schemas/character";
import { Character } from "./character";
export class CharactersResponse {
declare readonly items: Character[];
private constructor(input: CharactersResponseInput) {
const data = CharactersResponseSchema.parse(input);
Object.assign(this, {
items: data.items.map((item) => Character.from(item)),
});
Object.freeze(this);
}
static from(input: CharactersResponseInput): CharactersResponse {
return new CharactersResponse(input);
}
static fromJson(json: unknown): CharactersResponse {
return CharactersResponse.from(json as CharactersResponseInput);
}
toJson(): CharactersResponseData {
return CharactersResponseSchema.parse({
items: this.items.map((item) => item.toJson()),
});
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./character";
export * from "./characters_response";
-28
View File
@@ -1,28 +0,0 @@
/**
* 聊天列表项 discriminated union
*
*
*
* 4 种类型:
* - ai-disclosure:顶部 AI 披露横幅
* - date-separator:日期分隔条
* - loading-animationAI 正在输入的动画
* - message:实际消息
*/
import type { UiMessage } from "./ui_message";
export type ChatListItem =
| { type: "ai-disclosure" }
| { type: "date-separator"; date: string }
| { type: "loading-animation" }
| { type: "message"; message: UiMessage; key: string };
export const ChatListItemType = {
AiDisclosure: "ai-disclosure",
DateSeparator: "date-separator",
LoadingAnimation: "loading-animation",
Message: "message",
} as const;
export type ChatListItemType =
(typeof ChatListItemType)[keyof typeof ChatListItemType];
-39
View File
@@ -1,39 +0,0 @@
/**
* 聊天历史中的单条消息 DTO
*/
import {
ChatMessageSchema,
type ChatMessageInput,
type ChatMessageData,
type ChatImageData,
type ChatLockDetailData,
} from "@/data/schemas/chat";
export class ChatMessage {
declare readonly role: string;
declare readonly type: string;
declare readonly content: string;
declare readonly id: string;
declare readonly createdAt: string;
declare readonly audioUrl: string | null;
declare readonly image: ChatImageData;
declare readonly lockDetail: ChatLockDetailData;
private constructor(input: ChatMessageInput) {
const data = ChatMessageSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: ChatMessageInput): ChatMessage {
return new ChatMessage(input);
}
static fromJson(json: unknown): ChatMessage {
return ChatMessage.from(json as ChatMessageInput);
}
toJson(): ChatMessageData {
return ChatMessageSchema.parse(this);
}
}
-10
View File
@@ -1,10 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./chat_list_item";
export * from "./chat_media";
export * from "./chat_message";
export * from "./request";
export * from "./response";
export * from "./ui_message";
-3
View File
@@ -1,3 +0,0 @@
export * from "./send_message_request";
export * from "./unlock_private_request";
export * from "./unlock_history_request";
@@ -1,38 +0,0 @@
/**
* 发送消息请求 DTO
*/
import {
SendMessageRequestSchema,
type SendMessageRequestInput,
type SendMessageRequestData,
} from "@/data/schemas/chat/request/send_message_request";
export class SendMessageRequest {
declare readonly characterId: string;
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;
private constructor(input: SendMessageRequestInput) {
const data = SendMessageRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: SendMessageRequestInput): SendMessageRequest {
return new SendMessageRequest(input);
}
static fromJson(json: unknown): SendMessageRequest {
return SendMessageRequest.from(json as SendMessageRequestInput);
}
toJson(): SendMessageRequestData {
return SendMessageRequestSchema.parse(this);
}
}
@@ -1,22 +0,0 @@
import {
UnlockHistoryRequestSchema,
type UnlockHistoryRequestData,
type UnlockHistoryRequestInput,
} from "@/data/schemas/chat/request/unlock_history_request";
export class UnlockHistoryRequest {
declare readonly characterId: string;
private constructor(input: UnlockHistoryRequestInput) {
Object.assign(this, UnlockHistoryRequestSchema.parse(input));
Object.freeze(this);
}
static from(input: UnlockHistoryRequestInput): UnlockHistoryRequest {
return new UnlockHistoryRequest(input);
}
toJson(): UnlockHistoryRequestData {
return UnlockHistoryRequestSchema.parse(this);
}
}
@@ -1,33 +0,0 @@
/**
* 私密消息解锁请求 DTO
*/
import {
UnlockPrivateRequestSchema,
type UnlockPrivateRequestData,
type UnlockPrivateRequestInput,
} from "@/data/schemas/chat/request/unlock_private_request";
export class UnlockPrivateRequest {
declare readonly characterId: string;
declare readonly messageId?: string;
declare readonly lockType?: UnlockPrivateRequestData["lockType"];
declare readonly clientLockId?: string;
private constructor(input: UnlockPrivateRequestInput) {
const data = UnlockPrivateRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockPrivateRequestInput): UnlockPrivateRequest {
return new UnlockPrivateRequest(input);
}
static fromJson(json: unknown): UnlockPrivateRequest {
return UnlockPrivateRequest.from(json as UnlockPrivateRequestInput);
}
toJson(): UnlockPrivateRequestData {
return UnlockPrivateRequestSchema.parse(this);
}
}
@@ -1,36 +0,0 @@
/**
* 聊天历史响应 DTO
*/
import {
ChatHistoryResponseSchema,
type ChatHistoryResponseInput,
type ChatHistoryResponseData,
} from "@/data/schemas/chat/response/chat_history_response";
import { ChatMessage } from "../chat_message";
export class ChatHistoryResponse {
declare readonly messages: ChatMessage[];
declare readonly total: number;
declare readonly limit: number;
private constructor(input: ChatHistoryResponseInput) {
const data = ChatHistoryResponseSchema.parse(input);
Object.assign(this, {
...data,
messages: data.messages.map((m) => ChatMessage.from(m)),
});
Object.freeze(this);
}
static from(input: ChatHistoryResponseInput): ChatHistoryResponse {
return new ChatHistoryResponse(input);
}
static fromJson(json: unknown): ChatHistoryResponse {
return ChatHistoryResponse.from(json as ChatHistoryResponseInput);
}
toJson(): ChatHistoryResponseData {
return ChatHistoryResponseSchema.parse(this);
}
}
@@ -1,42 +0,0 @@
/**
* 发送消息响应 DTO
*/
import {
ChatSendResponseSchema,
type ChatSendResponseInput,
type ChatSendResponseData,
type ChatImageData,
type ChatLockDetailData,
} from "@/data/schemas/chat";
export class ChatSendResponse {
declare readonly reply: string;
declare readonly audioUrl: string;
declare readonly messageId: string;
declare readonly timestamp: number;
declare readonly image: ChatImageData;
declare readonly lockDetail: ChatLockDetailData;
declare readonly canSendMessage: boolean;
declare readonly creditBalance: number;
declare readonly creditsCharged: number;
declare readonly requiredCredits: number;
declare readonly shortfallCredits: number;
private constructor(input: ChatSendResponseInput) {
const data = ChatSendResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: ChatSendResponseInput): ChatSendResponse {
return new ChatSendResponse(input);
}
static fromJson(json: unknown): ChatSendResponse {
return ChatSendResponse.from(json as ChatSendResponseInput);
}
toJson(): ChatSendResponseData {
return ChatSendResponseSchema.parse(this);
}
}
-4
View File
@@ -1,4 +0,0 @@
export * from "./chat_history_response";
export * from "./chat_send_response";
export * from "./unlock_history_response";
export * from "./unlock_private_response";
@@ -1,33 +0,0 @@
/**
* 一键解锁历史锁定消息响应 DTO
*/
import {
UnlockHistoryResponseSchema,
type UnlockHistoryReason,
type UnlockHistoryResponseData,
type UnlockHistoryResponseInput,
} from "@/data/schemas/chat/response/unlock_history_response";
export class UnlockHistoryResponse {
declare readonly unlocked: boolean;
declare readonly reason: UnlockHistoryReason;
declare readonly shortfallCredits: number;
private constructor(input: UnlockHistoryResponseInput) {
const data = UnlockHistoryResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockHistoryResponseInput): UnlockHistoryResponse {
return new UnlockHistoryResponse(input);
}
static fromJson(json: unknown): UnlockHistoryResponse {
return UnlockHistoryResponse.from(json as UnlockHistoryResponseInput);
}
toJson(): UnlockHistoryResponseData {
return UnlockHistoryResponseSchema.parse(this);
}
}
@@ -1,41 +0,0 @@
import {
UnlockPrivateResponseSchema,
type UnlockPrivateReason,
type UnlockPrivateResponseData,
type UnlockPrivateResponseInput,
} from "@/data/schemas/chat/response/unlock_private_response";
import type { ChatImageData } from "@/data/schemas/chat";
/**
* 单条历史付费 / 私密消息解锁响应 DTO。
*/
export class UnlockPrivateResponse {
declare readonly unlocked: boolean;
declare readonly messageId: string;
declare readonly content: string;
declare readonly audioUrl: string;
declare readonly image: ChatImageData;
declare readonly reason: UnlockPrivateReason;
declare readonly creditBalance: number;
declare readonly creditsCharged: number;
declare readonly requiredCredits: number;
declare readonly shortfallCredits: number;
private constructor(input: UnlockPrivateResponseInput) {
const data = UnlockPrivateResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockPrivateResponseInput): UnlockPrivateResponse {
return new UnlockPrivateResponse(input);
}
static fromJson(json: unknown): UnlockPrivateResponse {
return UnlockPrivateResponse.from(json as UnlockPrivateResponseInput);
}
toJson(): UnlockPrivateResponseData {
return UnlockPrivateResponseSchema.parse(this);
}
}
-2
View File
@@ -1,2 +0,0 @@
export * from "./feedback_submission";
export * from "./response";
@@ -1,16 +0,0 @@
import {
FeedbackSubmitResponseSchema,
} from "@/data/schemas/feedback";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
export type FeedbackSubmitResponse = SchemaDto<
typeof FeedbackSubmitResponseSchema,
"feedbackId"
>;
export const FeedbackSubmitResponse = createSchemaDto<
typeof FeedbackSubmitResponseSchema,
"feedbackId"
>(FeedbackSubmitResponseSchema);
-1
View File
@@ -1 +0,0 @@
export * from "./feedback_submit_response";
-5
View File
@@ -1,5 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./request";
-17
View File
@@ -1,17 +0,0 @@
/**
* 应用事件上报请求 DTO
*/
import {
AppEventSchema,
} from "@/data/schemas/metrics/request/app_event";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
type AppEventPublicKey = "userId" | "browser" | "userAgent";
export type AppEvent = SchemaDto<typeof AppEventSchema, AppEventPublicKey>;
export const AppEvent = createSchemaDto<
typeof AppEventSchema,
AppEventPublicKey
>(AppEventSchema);
-2
View File
@@ -1,2 +0,0 @@
export * from "./app_event";
export * from "./pwa_event";
-22
View File
@@ -1,22 +0,0 @@
/**
* PWA 事件上报请求 DTO
*/
import {
PwaEventSchema,
} from "@/data/schemas/metrics/request/pwa_event";
import {
createSchemaDto,
type SchemaDto,
} from "@/data/dto/schema_dto";
type PwaEventPublicKey =
| "deviceId"
| "deviceType"
| "timestamp"
| "pwaInstalled"
| "pwaSupported";
export type PwaEvent = SchemaDto<typeof PwaEventSchema, PwaEventPublicKey>;
export const PwaEvent = createSchemaDto<
typeof PwaEventSchema,
PwaEventPublicKey
>(PwaEventSchema);
-8
View File
@@ -1,8 +0,0 @@
/**
* @file Payment DTO barrel.
*/
export * from "./payment_plan";
export * from "./tip_payment_plan";
export * from "./request";
export * from "./response";
-42
View File
@@ -1,42 +0,0 @@
/**
* 支付套餐 DTO
*/
import {
PaymentPlanSchema,
type PaymentPlanData,
type PaymentPlanInput,
} from "@/data/schemas/payment/payment_plan";
export class PaymentPlan {
declare readonly planId: string;
declare readonly planName: string;
declare readonly orderType: string;
declare readonly vipDays: number | null;
declare readonly dolAmount: number | null;
declare readonly creditBalance: number;
declare readonly amountCents: number;
declare readonly originalAmountCents: number | null;
declare readonly currency: string;
declare readonly isFirstRechargeOffer: boolean;
declare readonly mostPopular: boolean;
declare readonly firstRechargeDiscountPercent: number | null;
declare readonly promotionType: string | null;
private constructor(input: PaymentPlanInput) {
const data = PaymentPlanSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PaymentPlanInput): PaymentPlan {
return new PaymentPlan(input);
}
static fromJson(json: unknown): PaymentPlan {
return PaymentPlan.from(json as PaymentPlanInput);
}
toJson(): PaymentPlanData {
return PaymentPlanSchema.parse(this);
}
}
@@ -1,39 +0,0 @@
/**
* 创建支付订单请求 DTO
*/
import {
CreatePaymentOrderRequestSchema,
type CreatePaymentOrderRequestData,
type CreatePaymentOrderRequestInput,
type PayChannel,
} from "@/data/schemas/payment/request/create_payment_order_request";
export type { PayChannel };
export class CreatePaymentOrderRequest {
declare readonly planId: string;
declare readonly payChannel: PayChannel;
declare readonly autoRenew: boolean;
private constructor(input: CreatePaymentOrderRequestInput) {
const data = CreatePaymentOrderRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: CreatePaymentOrderRequestInput,
): CreatePaymentOrderRequest {
return new CreatePaymentOrderRequest(input);
}
static fromJson(json: unknown): CreatePaymentOrderRequest {
return CreatePaymentOrderRequest.from(
json as CreatePaymentOrderRequestInput,
);
}
toJson(): CreatePaymentOrderRequestData {
return CreatePaymentOrderRequestSchema.parse(this);
}
}
-1
View File
@@ -1 +0,0 @@
export * from "./create_payment_order_request";
@@ -1,35 +0,0 @@
/**
* 创建支付订单响应 DTO
*/
import {
CreatePaymentOrderResponseSchema,
type CreatePaymentOrderResponseData,
type CreatePaymentOrderResponseInput,
} from "@/data/schemas/payment/response/create_payment_order_response";
export class CreatePaymentOrderResponse {
declare readonly orderId: string;
declare readonly payParams: Record<string, unknown>;
private constructor(input: CreatePaymentOrderResponseInput) {
const data = CreatePaymentOrderResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: CreatePaymentOrderResponseInput,
): CreatePaymentOrderResponse {
return new CreatePaymentOrderResponse(input);
}
static fromJson(json: unknown): CreatePaymentOrderResponse {
return CreatePaymentOrderResponse.from(
json as CreatePaymentOrderResponseInput,
);
}
toJson(): CreatePaymentOrderResponseData {
return CreatePaymentOrderResponseSchema.parse(this);
}
}
-4
View File
@@ -1,4 +0,0 @@
export * from "./create_payment_order_response";
export * from "./payment_order_status_response";
export * from "./payment_plans_response";
export * from "./tip_payment_plans_response";
@@ -1,37 +0,0 @@
/**
* 支付订单状态响应 DTO
*/
import {
PaymentOrderStatusResponseSchema,
type PaymentOrderStatus,
type PaymentOrderStatusResponseData,
type PaymentOrderStatusResponseInput,
} from "@/data/schemas/payment/response/payment_order_status_response";
export type { PaymentOrderStatus };
export class PaymentOrderStatusResponse {
declare readonly status: PaymentOrderStatus;
private constructor(input: PaymentOrderStatusResponseInput) {
const data = PaymentOrderStatusResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: PaymentOrderStatusResponseInput,
): PaymentOrderStatusResponse {
return new PaymentOrderStatusResponse(input);
}
static fromJson(json: unknown): PaymentOrderStatusResponse {
return PaymentOrderStatusResponse.from(
json as PaymentOrderStatusResponseInput,
);
}
toJson(): PaymentOrderStatusResponseData {
return PaymentOrderStatusResponseSchema.parse(this);
}
}
@@ -1,45 +0,0 @@
/**
* 支付套餐列表响应 DTO
*/
import {
PaymentPlansResponseSchema,
type FirstRechargeOfferData,
type PaymentPlansResponseData,
type PaymentPlansResponseInput,
} from "@/data/schemas/payment/response/payment_plans_response";
import { PaymentPlan } from "../payment_plan";
export class PaymentPlansResponse {
declare readonly isFirstRecharge: boolean;
declare readonly firstRechargeOffer: FirstRechargeOfferData | null;
declare readonly plans: PaymentPlan[];
private constructor(input: PaymentPlansResponseInput) {
const data = PaymentPlansResponseSchema.parse(input);
Object.assign(this, {
isFirstRecharge: data.isFirstRecharge,
firstRechargeOffer: data.firstRechargeOffer,
plans: data.plans.map((plan) => PaymentPlan.from(plan)),
});
Object.freeze(this);
}
static from(input: PaymentPlansResponseInput): PaymentPlansResponse {
return new PaymentPlansResponse(input);
}
static fromJson(json: unknown): PaymentPlansResponse {
return PaymentPlansResponse.from(json as PaymentPlansResponseInput);
}
toJson(): PaymentPlansResponseData {
return {
isFirstRecharge: this.isFirstRecharge,
firstRechargeOffer: this.firstRechargeOffer,
plans: this.plans.map((plan) => plan.toJson()),
};
}
}
export type FirstRechargeOffer = FirstRechargeOfferData;
@@ -1,35 +0,0 @@
import {
TipPaymentPlansResponseSchema,
type TipPaymentPlansResponseData,
type TipPaymentPlansResponseInput,
} from "@/data/schemas/payment/response/tip_payment_plans_response";
import { TipPaymentPlan } from "../tip_payment_plan";
export class TipPaymentPlansResponse {
declare readonly plans: TipPaymentPlan[];
private constructor(input: TipPaymentPlansResponseInput) {
const data = TipPaymentPlansResponseSchema.parse(input);
Object.assign(this, {
plans: data.plans.map((plan) => TipPaymentPlan.from(plan)),
});
Object.freeze(this);
}
static from(input: TipPaymentPlansResponseInput): TipPaymentPlansResponse {
return new TipPaymentPlansResponse(input);
}
static fromJson(json: unknown): TipPaymentPlansResponse {
return TipPaymentPlansResponse.from(
json as TipPaymentPlansResponseInput,
);
}
toJson(): TipPaymentPlansResponseData {
return {
plans: this.plans.map((plan) => plan.toJson()),
};
}
}
-30
View File
@@ -1,30 +0,0 @@
import {
TipPaymentPlanSchema,
type TipPaymentPlanData,
type TipPaymentPlanInput,
} from "@/data/schemas/payment/tip_payment_plan";
export class TipPaymentPlan {
declare readonly planId: string;
declare readonly planName: string;
declare readonly amountCents: number;
declare readonly currency: string;
private constructor(input: TipPaymentPlanInput) {
const data = TipPaymentPlanSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: TipPaymentPlanInput): TipPaymentPlan {
return new TipPaymentPlan(input);
}
static fromJson(json: unknown): TipPaymentPlan {
return TipPaymentPlan.from(json as TipPaymentPlanInput);
}
toJson(): TipPaymentPlanData {
return TipPaymentPlanSchema.parse(this);
}
}
-3
View File
@@ -1,3 +0,0 @@
export * from "./private_album";
export * from "./request";
export * from "./response";
@@ -1,37 +0,0 @@
import {
PrivateAlbumSchema,
type PrivateAlbumData,
type PrivateAlbumInput,
} from "@/data/schemas/private-room";
export class PrivateAlbum {
declare readonly albumId: string;
declare readonly title: string;
declare readonly content: string | null;
declare readonly previewText: string;
declare readonly imageCount: number;
declare readonly images: PrivateAlbumData["images"];
declare readonly locked: boolean;
declare readonly unlocked: boolean;
declare readonly unlockCost: number;
declare readonly publishedAt: string | null;
declare readonly lockDetail: PrivateAlbumData["lockDetail"];
private constructor(input: PrivateAlbumInput) {
const data = PrivateAlbumSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PrivateAlbumInput): PrivateAlbum {
return new PrivateAlbum(input);
}
static fromJson(json: unknown): PrivateAlbum {
return PrivateAlbum.from(json as PrivateAlbumInput);
}
toJson(): PrivateAlbumData {
return PrivateAlbumSchema.parse(this);
}
}
@@ -1 +0,0 @@
export * from "./unlock_private_album_request";
@@ -1,21 +0,0 @@
import {
UnlockPrivateAlbumRequestSchema,
type UnlockPrivateAlbumRequestData,
type UnlockPrivateAlbumRequestInput,
} from "@/data/schemas/private-room";
export class UnlockPrivateAlbumRequest {
private constructor(input: UnlockPrivateAlbumRequestInput) {
const data = UnlockPrivateAlbumRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockPrivateAlbumRequestInput): UnlockPrivateAlbumRequest {
return new UnlockPrivateAlbumRequest(input);
}
toJson(): UnlockPrivateAlbumRequestData {
return UnlockPrivateAlbumRequestSchema.parse(this);
}
}
@@ -1,2 +0,0 @@
export * from "./private_albums_response";
export * from "./private_album_unlock_response";
@@ -1,38 +0,0 @@
import {
PrivateAlbumUnlockResponseSchema,
type PrivateAlbumUnlockReason,
type PrivateAlbumUnlockResponseData,
type PrivateAlbumUnlockResponseInput,
} from "@/data/schemas/private-room";
export class PrivateAlbumUnlockResponse {
declare readonly albumId: string;
declare readonly locked: boolean;
declare readonly unlocked: boolean;
declare readonly reason: PrivateAlbumUnlockReason | string;
declare readonly unlockCost: number;
declare readonly requiredCredits: number;
declare readonly creditBalance: number;
declare readonly shortfallCredits: number;
declare readonly images: PrivateAlbumUnlockResponseData["images"];
private constructor(input: PrivateAlbumUnlockResponseInput) {
const data = PrivateAlbumUnlockResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PrivateAlbumUnlockResponseInput): PrivateAlbumUnlockResponse {
return new PrivateAlbumUnlockResponse(input);
}
static fromJson(json: unknown): PrivateAlbumUnlockResponse {
return PrivateAlbumUnlockResponse.from(
json as PrivateAlbumUnlockResponseInput,
);
}
toJson(): PrivateAlbumUnlockResponseData {
return PrivateAlbumUnlockResponseSchema.parse(this);
}
}
@@ -1,36 +0,0 @@
import {
PrivateAlbumsResponseSchema,
type PrivateAlbumsResponseData,
type PrivateAlbumsResponseInput,
} from "@/data/schemas/private-room";
import { PrivateAlbum } from "../private_album";
export class PrivateAlbumsResponse {
declare readonly items: PrivateAlbum[];
declare readonly creditBalance: number;
private constructor(input: PrivateAlbumsResponseInput) {
const data = PrivateAlbumsResponseSchema.parse(input);
Object.assign(this, {
items: data.items.map((item) => PrivateAlbum.from(item)),
creditBalance: data.creditBalance,
});
Object.freeze(this);
}
static from(input: PrivateAlbumsResponseInput): PrivateAlbumsResponse {
return new PrivateAlbumsResponse(input);
}
static fromJson(json: unknown): PrivateAlbumsResponse {
return PrivateAlbumsResponse.from(json as PrivateAlbumsResponseInput);
}
toJson(): PrivateAlbumsResponseData {
return {
items: this.items.map((item) => item.toJson()),
creditBalance: this.creditBalance,
};
}
}
-59
View File
@@ -1,59 +0,0 @@
import type { z } from "zod";
export type SchemaDto<
TSchema extends z.ZodType,
TPublicKey extends keyof z.output<TSchema> = never,
> = Readonly<Pick<z.output<TSchema>, TPublicKey>> & {
toJson(): z.output<TSchema>;
};
export interface SchemaDtoFactory<
TSchema extends z.ZodType,
TPublicKey extends keyof z.output<TSchema> = never,
> {
readonly prototype: SchemaDto<TSchema, TPublicKey>;
[Symbol.hasInstance](value: unknown): boolean;
from(input: z.input<TSchema>): SchemaDto<TSchema, TPublicKey>;
fromJson(json: unknown): SchemaDto<TSchema, TPublicKey>;
}
/**
* 为没有自定义映射逻辑的对象 Schema 创建不可变 DTO。
*
* `TPublicKey` 只暴露业务代码实际读取的字段;完整解析结果仍会保留在实例中,
* 并由 `toJson` 按原 Schema 输出。包含嵌套 DTO 转换或领域方法的模型继续使用
* 显式 class,避免把业务逻辑隐藏进通用工厂。
*/
export function createSchemaDto<
TSchema extends z.ZodType,
TPublicKey extends keyof z.output<TSchema> = never,
>(schema: TSchema): SchemaDtoFactory<TSchema, TPublicKey> {
class GeneratedSchemaDto {
private constructor(input: z.input<TSchema>) {
Object.assign(this, schema.parse(input) as object);
Object.freeze(this);
}
static from(
input: z.input<TSchema>,
): SchemaDto<TSchema, TPublicKey> {
return new GeneratedSchemaDto(input) as SchemaDto<
TSchema,
TPublicKey
>;
}
static fromJson(json: unknown): SchemaDto<TSchema, TPublicKey> {
return GeneratedSchemaDto.from(json as z.input<TSchema>);
}
toJson(): z.output<TSchema> {
return schema.parse(this);
}
}
return GeneratedSchemaDto as unknown as SchemaDtoFactory<
TSchema,
TPublicKey
>;
}
-31
View File
@@ -1,31 +0,0 @@
/**
* 头像数据 DTO
*/
import {
AvatarDataSchema,
type AvatarDataInput,
type AvatarDataData,
} from "@/data/schemas/user/avatar_data";
export class AvatarData {
declare readonly avatarUrl: string;
declare readonly userId: string;
private constructor(input: AvatarDataInput) {
const data = AvatarDataSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: AvatarDataInput): AvatarData {
return new AvatarData(input);
}
static fromJson(json: unknown): AvatarData {
return AvatarData.from(json as AvatarDataInput);
}
toJson(): AvatarDataData {
return AvatarDataSchema.parse(this);
}
}
-10
View File
@@ -1,10 +0,0 @@
/**
* @file Automatically generated by barrelsby.
*/
export * from "./avatar_data";
export * from "./personality_traits";
export * from "./recent_memory";
export * from "./user";
export * from "./user_entitlements";
export * from "./user_view";
-34
View File
@@ -1,34 +0,0 @@
/**
* 个性特征 DTO
*/
import {
PersonalityTraitsSchema,
type PersonalityTraitsInput,
type PersonalityTraitsData,
} from "@/data/schemas/user/personality_traits";
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(input: PersonalityTraitsInput) {
const data = PersonalityTraitsSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PersonalityTraitsInput): PersonalityTraits {
return new PersonalityTraits(input);
}
static fromJson(json: unknown): PersonalityTraits {
return PersonalityTraits.from(json as PersonalityTraitsInput);
}
toJson(): PersonalityTraitsData {
return PersonalityTraitsSchema.parse(this);
}
}
-33
View File
@@ -1,33 +0,0 @@
/**
* 最近记忆 DTO
*/
import {
RecentMemorySchema,
type RecentMemoryInput,
type RecentMemoryData,
} from "@/data/schemas/user/recent_memory";
export class RecentMemory {
declare readonly type: string;
declare readonly content: string;
declare readonly importance: number;
declare readonly createdAt: string;
private constructor(input: RecentMemoryInput) {
const data = RecentMemorySchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: RecentMemoryInput): RecentMemory {
return new RecentMemory(input);
}
static fromJson(json: unknown): RecentMemory {
return RecentMemory.from(json as RecentMemoryInput);
}
toJson(): RecentMemoryData {
return RecentMemorySchema.parse(this);
}
}
-64
View File
@@ -1,64 +0,0 @@
/**
* 用户 DTO
*/
import {
UserSchema,
type UserInput,
type UserData,
} from "@/data/schemas/user/user";
import { PersonalityTraits } from "./personality_traits";
export class User {
declare readonly id: string;
declare readonly username: string;
declare readonly email: string;
declare readonly platform: string;
declare readonly country: string;
declare readonly countryCode: string;
declare readonly fbAsid: string;
declare readonly fbPsid: string;
declare readonly intimacy: number;
declare readonly dolBalance: number;
declare readonly creditBalance: number;
declare readonly dailyFreeChatLimit: number;
declare readonly dailyFreeChatUsed: number;
declare readonly dailyFreeChatRemaining: number;
declare readonly dailyFreePrivateLimit: number;
declare readonly dailyFreePrivateUsed: number;
declare readonly dailyFreePrivateRemaining: number;
declare readonly personalityTraits: PersonalityTraits;
declare readonly preferredLanguage: string;
declare readonly createdAt: string;
declare readonly lastMessageAt: string;
private constructor(input: UserInput) {
const data = UserSchema.parse(input);
Object.assign(this, {
...data,
personalityTraits: PersonalityTraits.from(data.personalityTraits),
});
Object.freeze(this);
}
static from(input: UserInput): User {
return new User(input);
}
static fromJson(json: unknown): User {
return User.from(json as UserInput);
}
/**
* 创建一个具有空 id/username 的 User 实例(用于可选 user 字段的默认占位)
*/
static empty(): User {
return new User({ id: "", username: "" });
}
toJson(): UserData {
return UserSchema.parse({
...this,
personalityTraits: this.personalityTraits.toJson(),
});
}
}
-39
View File
@@ -1,39 +0,0 @@
/**
* 用户权益快照 DTO
*/
import {
UserEntitlementsSchema,
type UserEntitlementsData,
type UserEntitlementsInput,
} from "@/data/schemas/user/user_entitlements";
export class UserEntitlements {
declare readonly userId: string;
declare readonly isGuest: boolean;
declare readonly isVip: boolean;
declare readonly vipExpiresAt: string | null;
declare readonly creditBalance: number;
declare readonly dolBalance: number;
declare readonly policy: UserEntitlementsData["policy"];
declare readonly costs: UserEntitlementsData["costs"];
declare readonly quotas: UserEntitlementsData["quotas"];
declare readonly historyUnlock: UserEntitlementsData["historyUnlock"];
private constructor(input: UserEntitlementsInput) {
const data = UserEntitlementsSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UserEntitlementsInput): UserEntitlements {
return new UserEntitlements(input);
}
static fromJson(json: unknown): UserEntitlements {
return UserEntitlements.from(json as UserEntitlementsInput);
}
toJson(): UserEntitlementsData {
return UserEntitlementsSchema.parse(this);
}
}

Some files were not shown because too many files have changed in this diff Show More