refactor(data): replace schema classes with readonly models

This commit is contained in:
2026-07-17 13:21:40 +08:00
parent 3437312167
commit ae97366a4a
103 changed files with 1220 additions and 2117 deletions
+41 -52
View File
@@ -12,7 +12,7 @@ description: Sync this Next.js frontend with backend API documentation
当后端 API 新增、删除、字段结构调整、响应语义变化时,使用本 skill 更新:
- 网络层
- schema / 不可变数据
- schema / 不可变数据
- repository
- storage
- XState store
@@ -22,18 +22,18 @@ description: Sync this Next.js frontend with backend API documentation
## 当前项目结构
| 层级 | 路径 | 说明 |
| --- | --- | --- |
| API Path | `src/data/services/api/api_path.ts` | 统一维护接口路径 |
| API Service | `src/data/services/api/*_api.ts` | 调用 `httpClient` 并解析响应 |
| Schema Model | `src/data/schemas/<module>/*.ts` | Zod schema 与不可变数据类,负责解析和序列化 |
| Repository Interface | `src/data/repositories/interfaces/i*_repository.ts` | 仓库接口 |
| Repository | `src/data/repositories/*_repository.ts` | 业务仓库实现 |
| Storage | `src/data/storage/*` | 本地持久化 |
| Store | `src/stores/<module>/*` | XState 状态机、actors、helpers、sync |
| UI | `src/app/**` | Next.js app router UI |
| Mock | `src/data/mock/<module>/**` | 模拟请求 / 响应数据 |
| Tests | `**/__tests__/*.test.ts` | schema model、helper、machine transition 测试 |
| 层级 | 路径 | 说明 |
| -------------------- | --------------------------------------------------- | ----------------------------------------------- |
| API Path | `src/data/services/api/api_path.ts` | 统一维护接口路径 |
| API Service | `src/data/services/api/*_api.ts` | 调用 `httpClient` 并解析响应 |
| Schema Model | `src/data/schemas/<module>/*.ts` | Zod schema 与不可变数据类,负责解析和归一化 |
| Repository Interface | `src/data/repositories/interfaces/i*_repository.ts` | 仓库接口 |
| Repository | `src/data/repositories/*_repository.ts` | 业务仓库实现 |
| Storage | `src/data/storage/*` | 本地持久化 |
| Store | `src/stores/<module>/*` | XState 状态机、actors、helpers、sync |
| UI | `src/app/**` | Next.js app router UI |
| Mock | `src/data/mock/<module>/**` | 模拟请求 / 响应数据 |
| Tests | `**/__tests__/*.test.ts` | schema model、helper、machine transition 测试 |
## 后端文档来源
@@ -84,13 +84,16 @@ git status --short
否则 schema 应尽量防御性解析,例如:
```ts
z.string().nullable().transform((v) => v ?? "").default("")
z.string()
.nullable()
.transform((v) => v ?? "")
.default("");
```
项目已有通用 helper
```ts
src/data/schemas/nullable-defaults.ts
src / data / schemas / nullable - defaults.ts;
```
### 3. 更新 API Path
@@ -98,7 +101,7 @@ src/data/schemas/nullable-defaults.ts
新增或修改:
```ts
src/data/services/api/api_path.ts
src / data / services / api / api_path.ts;
```
示例:
@@ -122,46 +125,32 @@ src/data/schemas/<module>/*.ts
- 输入类型用 `z.input<typeof Schema>`
- 对后端可能返回 `null` 的字段做防御性处理。
- 不要在 schema 中写 UI 逻辑。
- Schema 与对应不可变数据类必须定义在同一个文件中
- 数据类的 `declare readonly` 只声明业务代码直接访问的字段
- Schema 输出必须是纯数据,不要定义 class 或工厂对象
- 对象 Schema 使用 `.readonly()`;已知嵌套对象、数组和 Record 也必须逐层 readonly
- transform 应先完成归一化,再在最终输出应用 `.readonly()`
- 容器默认值必须经过 Schema 解析;使用 `prefault` 或在最外层应用 readonly,不能让 `.default()` 返回未冻结对象。
- 后端返回但前端既不读取、也不透传的字段,不要加入 Schema。
- 仅用于请求序列化、响应兼容或内部透传的字段保留在 Schema 中,不声明 class 属性
- 数据类统一使用 `from()``fromJson()``toJson()` `Object.freeze(this)`
- 没有嵌套转换或自定义方法时,使用 `src/data/schemas/schema_model.ts``createSchemaModel()`
- 仅用于请求序列化、响应兼容或内部透传的字段直接保留在 Schema 中。
- 统一使用 `XxxSchema.parse()` 构造请求与解析响应,不增加 `from()``fromJson()` `toJson()` 包装
- 不要再创建独立的数据传输对象层。
示例:
```ts
export const XxxResponseSchema = z.object({
id: z.string(),
count: z.number().default(0),
});
export const XxxResponseSchema = z
.object({
id: z.string(),
tags: z
.array(z.string())
.default(() => [])
.readonly(),
count: z.number().default(0),
})
.readonly();
export type XxxResponseInput = z.input<typeof XxxResponseSchema>;
export type XxxResponseData = z.output<typeof XxxResponseSchema>;
export class XxxResponse {
declare readonly id: string;
private constructor(input: XxxResponseInput) {
const data = XxxResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: XxxResponseInput): XxxResponse {
return new XxxResponse(input);
}
static fromJson(json: unknown): XxxResponse {
return XxxResponse.from(json as XxxResponseInput);
}
toJson(): XxxResponseData {
return XxxResponseSchema.parse(this);
}
}
export type XxxResponse = XxxResponseData;
```
### 5. 更新 API Service
@@ -177,9 +166,9 @@ src/data/services/api/<module>_api.ts
```ts
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.xxx, {
method: "POST",
body: request.toJson(),
body: request,
});
return XxxResponse.fromJson(unwrap(env) as Record<string, unknown>);
return XxxResponseSchema.parse(unwrap(env));
```
注意:
@@ -207,7 +196,7 @@ async getXxx(): Promise<Result<XxxResponse>> {
Repository 可以做轻量编排,例如:
- request 数据类构造
- request schema 解析
- 多接口组合
- 本地 storage 同步
- 本地模型与接口模型转换
@@ -333,7 +322,7 @@ pnpm exec vitest run <related tests>
1. 文档确认 path / method / payload。
2. `api_path.ts` 新增路径。
3. 在同一文件新增 schema 与不可变数据类
3. 在同一文件新增 readonly schema 与输入/输出类型
4. API service 新增方法。
5. repository interface + implementation 新增方法。
6. 如需要,接入 store actor / event。
@@ -343,7 +332,7 @@ pnpm exec vitest run <related tests>
### 修改响应字段
1. 更新 schema。
2. 更新同文件数据类的公开字段和 `toJson`
2. 更新同文件输入/输出类型及相关 parse 调用
3. 更新 mapper/helper。
4. 更新 UI 或状态机依赖字段。
5. 更新 mock 和测试。