1030 lines
24 KiB
Markdown
1030 lines
24 KiB
Markdown
|
|
# Schema 驱动开发指南(Schema-Driven Development Guide)
|
|||
|
|
|
|||
|
|
> **模块**: 01_Architecture - Schema 驱动开发与数据验证
|
|||
|
|
> **更新日期**: 2026-03-20
|
|||
|
|
> **适用范围**: 全项目(dashboard、server、extension、node-agent)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 概述(Overview)
|
|||
|
|
|
|||
|
|
### 1.1 什么是 Schema 驱动开发
|
|||
|
|
|
|||
|
|
**Schema 驱动开发**是一种以数据结构定义为核心的软件开发方法,通过定义清晰的数据 Schema 来:
|
|||
|
|
|
|||
|
|
- **统一数据模型**:确保前后端数据结构一致
|
|||
|
|
- **自动类型推导**:从 Schema 自动生成 TypeScript 类型
|
|||
|
|
- **运行时验证**:在运行时验证数据完整性
|
|||
|
|
- **减少错误**:通过编译时和运行时双重检查减少类型错误
|
|||
|
|
|
|||
|
|
### 1.2 核心优势
|
|||
|
|
|
|||
|
|
| 优势 | 说明 | 效果 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **类型安全** | 编译时 + 运行时双重检查 | 减少 80% 类型错误 |
|
|||
|
|
| **自动推导** | 从 Schema 自动生成类型 | 减少重复定义 |
|
|||
|
|
| **数据验证** | 自动验证输入输出 | 防止脏数据 |
|
|||
|
|
| **文档化** | Schema 即文档 | 提高可维护性 |
|
|||
|
|
| **AI 友好** | 清晰的数据结构定义 | AI 更容易理解 |
|
|||
|
|
|
|||
|
|
### 1.3 工具选择
|
|||
|
|
|
|||
|
|
| 工具 | 类型 | 推荐场景 | 优先级 |
|
|||
|
|
|------|------|----------|--------|
|
|||
|
|
| **zod** | Schema 库 | 首选推荐 | P0 |
|
|||
|
|
| **class-validator** | 装饰器 | 类验证 | P1 |
|
|||
|
|
| **io-ts** | 函数式 | 函数式编程 | P2 |
|
|||
|
|
| **yup** | Schema 库 | 旧项目迁移 | P3 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. zod 快速入门(zod Quick Start)
|
|||
|
|
|
|||
|
|
### 2.1 安装
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
npm install zod
|
|||
|
|
npm install -D @types/zod
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 基础用法
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
// 定义 Schema
|
|||
|
|
const UserSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
name: z.string().min(1).max(100),
|
|||
|
|
email: z.string().email(),
|
|||
|
|
age: z.number().int().positive(),
|
|||
|
|
isActive: z.boolean().default(true),
|
|||
|
|
createdAt: z.date().default(() => new Date())
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 推导类型
|
|||
|
|
type User = z.infer<typeof UserSchema>
|
|||
|
|
|
|||
|
|
// 验证数据
|
|||
|
|
const userData = {
|
|||
|
|
id: '123e4567-e89b-12d3-a456-426614174000',
|
|||
|
|
name: 'John Doe',
|
|||
|
|
email: 'john@example.com',
|
|||
|
|
age: 30
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const user = UserSchema.parse(userData)
|
|||
|
|
console.log(user) // { id: '...', name: 'John Doe', email: 'john@example.com', age: 30, isActive: true, createdAt: Date }
|
|||
|
|
|
|||
|
|
// 安全解析(不抛出异常)
|
|||
|
|
const result = UserSchema.safeParse(userData)
|
|||
|
|
if (result.success) {
|
|||
|
|
console.log(result.data)
|
|||
|
|
} else {
|
|||
|
|
console.error(result.error)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 常用 Schema 类型
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
// 字符串
|
|||
|
|
const StringSchema = z.string()
|
|||
|
|
const EmailSchema = z.string().email()
|
|||
|
|
const UrlSchema = z.string().url()
|
|||
|
|
const UUIDSchema = z.string().uuid()
|
|||
|
|
|
|||
|
|
// 数字
|
|||
|
|
const NumberSchema = z.number()
|
|||
|
|
const PositiveNumberSchema = z.number().positive()
|
|||
|
|
const IntSchema = z.number().int()
|
|||
|
|
const MinMaxSchema = z.number().min(0).max(100)
|
|||
|
|
|
|||
|
|
// 布尔值
|
|||
|
|
const BooleanSchema = z.boolean()
|
|||
|
|
|
|||
|
|
// 日期
|
|||
|
|
const DateSchema = z.date()
|
|||
|
|
|
|||
|
|
// 数组
|
|||
|
|
const ArraySchema = z.array(z.string())
|
|||
|
|
const NonEmptyArraySchema = z.array(z.string()).nonempty()
|
|||
|
|
const MinLengthArraySchema = z.array(z.string()).min(1)
|
|||
|
|
|
|||
|
|
// 对象
|
|||
|
|
const ObjectSchema = z.object({
|
|||
|
|
name: z.string(),
|
|||
|
|
age: z.number()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 可选字段
|
|||
|
|
const OptionalSchema = z.object({
|
|||
|
|
name: z.string(),
|
|||
|
|
age: z.number().optional()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 可空字段
|
|||
|
|
const NullableSchema = z.object({
|
|||
|
|
name: z.string().nullable()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 联合类型
|
|||
|
|
const UnionSchema = z.union([z.string(), z.number()])
|
|||
|
|
const LiteralSchema = z.literal('hello')
|
|||
|
|
|
|||
|
|
// 枚举
|
|||
|
|
const EnumSchema = z.enum(['active', 'inactive', 'pending'])
|
|||
|
|
|
|||
|
|
// 任意类型
|
|||
|
|
const AnySchema = z.any()
|
|||
|
|
const UnknownSchema = z.unknown()
|
|||
|
|
|
|||
|
|
// 自定义验证
|
|||
|
|
const CustomSchema = z.string().refine(
|
|||
|
|
(val) => val.length >= 3,
|
|||
|
|
{ message: 'String must be at least 3 characters' }
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// 转换
|
|||
|
|
const TransformSchema = z.string().transform((val) => val.toUpperCase())
|
|||
|
|
const CoerceSchema = z.coerce.number() // 自动转换字符串为数字
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 项目 Schema 组织(Project Schema Organization)
|
|||
|
|
|
|||
|
|
### 3.1 目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
/src
|
|||
|
|
/schemas
|
|||
|
|
/api # API 请求/响应 Schema
|
|||
|
|
/user
|
|||
|
|
CreateUserRequest.schema.ts
|
|||
|
|
CreateUserResponse.schema.ts
|
|||
|
|
GetUserResponse.schema.ts
|
|||
|
|
/product
|
|||
|
|
CreateProductRequest.schema.ts
|
|||
|
|
ProductResponse.schema.ts
|
|||
|
|
/dto # 数据传输对象 Schema
|
|||
|
|
UserDTO.schema.ts
|
|||
|
|
ProductDTO.schema.ts
|
|||
|
|
/domain # 领域模型 Schema
|
|||
|
|
User.schema.ts
|
|||
|
|
Product.schema.ts
|
|||
|
|
Order.schema.ts
|
|||
|
|
/shared # 共享 Schema
|
|||
|
|
Pagination.schema.ts
|
|||
|
|
Error.schema.ts
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 命名规范
|
|||
|
|
|
|||
|
|
| 类型 | 命名规则 | 示例 |
|
|||
|
|
|------|----------|------|
|
|||
|
|
| **Schema 文件** | `{Name}.schema.ts` | `User.schema.ts` |
|
|||
|
|
| **Schema 变量** | `{Name}Schema` | `UserSchema` |
|
|||
|
|
| **类型推导** | `{Name}` | `User` |
|
|||
|
|
| **DTO Schema** | `{Name}DTOSchema` | `UserDTOSchema` |
|
|||
|
|
| **API 请求** | `{Action}{Resource}RequestSchema` | `CreateUserRequestSchema` |
|
|||
|
|
| **API 响应** | `{Action}{Resource}ResponseSchema` | `CreateUserResponseSchema` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 领域模型 Schema(Domain Model Schemas)
|
|||
|
|
|
|||
|
|
### 4.1 User Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/domain/User.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const UserSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
merchantId: z.string().uuid(),
|
|||
|
|
username: z.string().min(3).max(50),
|
|||
|
|
email: z.string().email(),
|
|||
|
|
role: z.enum(['ADMIN', 'MANAGER', 'OPERATOR', 'VIEWER']),
|
|||
|
|
status: z.enum(['ACTIVE', 'INACTIVE', 'SUSPENDED']).default('ACTIVE'),
|
|||
|
|
createdAt: z.date(),
|
|||
|
|
updatedAt: z.date()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type User = z.infer<typeof UserSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 Product Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/domain/Product.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const ProductSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
merchantId: z.string().uuid(),
|
|||
|
|
storeId: z.string().uuid(),
|
|||
|
|
name: z.string().min(1).max(200),
|
|||
|
|
sku: z.string().min(1).max(50),
|
|||
|
|
price: z.number().positive(),
|
|||
|
|
cost: z.number().nonnegative(),
|
|||
|
|
stock: z.number().int().nonnegative(),
|
|||
|
|
status: z.enum(['ACTIVE', 'INACTIVE', 'OUT_OF_STOCK']).default('ACTIVE'),
|
|||
|
|
images: z.array(z.string().url()),
|
|||
|
|
attributes: z.record(z.string(), z.unknown()).optional(),
|
|||
|
|
createdAt: z.date(),
|
|||
|
|
updatedAt: z.date()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type Product = z.infer<typeof ProductSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 Order Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/domain/Order.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const OrderItemSchema = z.object({
|
|||
|
|
productId: z.string().uuid(),
|
|||
|
|
productName: z.string(),
|
|||
|
|
quantity: z.number().int().positive(),
|
|||
|
|
unitPrice: z.number().positive(),
|
|||
|
|
totalPrice: z.number().positive()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export const OrderSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
merchantId: z.string().uuid(),
|
|||
|
|
userId: z.string().uuid(),
|
|||
|
|
orderNumber: z.string().min(1),
|
|||
|
|
status: z.enum([
|
|||
|
|
'PENDING',
|
|||
|
|
'PAID',
|
|||
|
|
'PROCESSING',
|
|||
|
|
'SHIPPED',
|
|||
|
|
'COMPLETED',
|
|||
|
|
'CANCELLED',
|
|||
|
|
'REFUNDED'
|
|||
|
|
]).default('PENDING'),
|
|||
|
|
items: z.array(OrderItemSchema).nonempty(),
|
|||
|
|
subtotal: z.number().positive(),
|
|||
|
|
shippingFee: z.number().nonnegative(),
|
|||
|
|
tax: z.number().nonnegative(),
|
|||
|
|
totalAmount: z.number().positive(),
|
|||
|
|
shippingAddress: z.object({
|
|||
|
|
recipientName: z.string(),
|
|||
|
|
street: z.string(),
|
|||
|
|
city: z.string(),
|
|||
|
|
state: z.string(),
|
|||
|
|
zipCode: z.string(),
|
|||
|
|
country: z.string()
|
|||
|
|
}),
|
|||
|
|
paymentMethod: z.enum(['CREDIT_CARD', 'PAYPAL', 'BANK_TRANSFER']),
|
|||
|
|
createdAt: z.date(),
|
|||
|
|
updatedAt: z.date()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type Order = z.infer<typeof OrderSchema>
|
|||
|
|
export type OrderItem = z.infer<typeof OrderItemSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. DTO Schema(Data Transfer Object Schemas)
|
|||
|
|
|
|||
|
|
### 5.1 User DTO
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/dto/UserDTO.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const UserDTOSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
username: z.string(),
|
|||
|
|
email: z.string().email(),
|
|||
|
|
role: z.enum(['ADMIN', 'MANAGER', 'OPERATOR', 'VIEWER']),
|
|||
|
|
status: z.enum(['ACTIVE', 'INACTIVE', 'SUSPENDED'])
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type UserDTO = z.infer<typeof UserDTOSchema>
|
|||
|
|
|
|||
|
|
// 转换函数:从领域模型到 DTO
|
|||
|
|
export function toUserDTO(user: unknown): UserDTO {
|
|||
|
|
return UserDTOSchema.parse(user)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 Product DTO
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/dto/ProductDTO.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const ProductDTOSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
name: z.string(),
|
|||
|
|
sku: z.string(),
|
|||
|
|
price: z.number(),
|
|||
|
|
stock: z.number(),
|
|||
|
|
status: z.enum(['ACTIVE', 'INACTIVE', 'OUT_OF_STOCK']),
|
|||
|
|
imageUrl: z.string().url().optional()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type ProductDTO = z.infer<typeof ProductDTOSchema>
|
|||
|
|
|
|||
|
|
export function toProductDTO(product: unknown): ProductDTO {
|
|||
|
|
const validated = ProductDTOSchema.parse(product)
|
|||
|
|
return {
|
|||
|
|
...validated,
|
|||
|
|
imageUrl: validated.images?.[0]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. API Schema(API Request/Response Schemas)
|
|||
|
|
|
|||
|
|
### 6.1 用户 API
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/api/user/CreateUserRequest.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const CreateUserRequestSchema = z.object({
|
|||
|
|
username: z.string().min(3).max(50),
|
|||
|
|
email: z.string().email(),
|
|||
|
|
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
|
|||
|
|
role: z.enum(['ADMIN', 'MANAGER', 'OPERATOR', 'VIEWER']).default('OPERATOR')
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/api/user/CreateUserResponse.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const CreateUserResponseSchema = z.object({
|
|||
|
|
success: z.boolean(),
|
|||
|
|
data: z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
username: z.string(),
|
|||
|
|
email: z.string(),
|
|||
|
|
role: z.string(),
|
|||
|
|
createdAt: z.date()
|
|||
|
|
}),
|
|||
|
|
message: z.string().optional()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type CreateUserResponse = z.infer<typeof CreateUserResponseSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/api/user/GetUserResponse.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const GetUserResponseSchema = z.object({
|
|||
|
|
success: z.boolean(),
|
|||
|
|
data: z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
username: z.string(),
|
|||
|
|
email: z.string(),
|
|||
|
|
role: z.string(),
|
|||
|
|
status: z.string(),
|
|||
|
|
createdAt: z.date(),
|
|||
|
|
updatedAt: z.date()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type GetUserResponse = z.infer<typeof GetUserResponseSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 商品 API
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/api/product/CreateProductRequest.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const CreateProductRequestSchema = z.object({
|
|||
|
|
storeId: z.string().uuid(),
|
|||
|
|
name: z.string().min(1).max(200),
|
|||
|
|
sku: z.string().min(1).max(50),
|
|||
|
|
price: z.number().positive(),
|
|||
|
|
cost: z.number().nonnegative(),
|
|||
|
|
stock: z.number().int().nonnegative(),
|
|||
|
|
images: z.array(z.string().url()),
|
|||
|
|
attributes: z.record(z.string(), z.unknown()).optional()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type CreateProductRequest = z.infer<typeof CreateProductRequestSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/api/product/ProductResponse.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const ProductResponseSchema = z.object({
|
|||
|
|
success: z.boolean(),
|
|||
|
|
data: z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
name: z.string(),
|
|||
|
|
sku: z.string(),
|
|||
|
|
price: z.number(),
|
|||
|
|
stock: z.number(),
|
|||
|
|
status: z.string(),
|
|||
|
|
imageUrl: z.string().url().optional(),
|
|||
|
|
createdAt: z.date(),
|
|||
|
|
updatedAt: z.date()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type ProductResponse = z.infer<typeof ProductResponseSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 共享 Schema(Shared Schemas)
|
|||
|
|
|
|||
|
|
### 7.1 分页 Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/shared/Pagination.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const PaginationParamsSchema = z.object({
|
|||
|
|
page: z.number().int().positive().default(1),
|
|||
|
|
pageSize: z.number().int().positive().max(100).default(20)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type PaginationParams = z.infer<typeof PaginationParamsSchema>
|
|||
|
|
|
|||
|
|
export const PaginatedResponseSchema = <T extends z.ZodType>(itemSchema: T) =>
|
|||
|
|
z.object({
|
|||
|
|
success: z.boolean(),
|
|||
|
|
data: z.object({
|
|||
|
|
items: z.array(itemSchema),
|
|||
|
|
total: z.number().int().nonnegative(),
|
|||
|
|
page: z.number().int().positive(),
|
|||
|
|
pageSize: z.number().int().positive(),
|
|||
|
|
totalPages: z.number().int().nonnegative()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type PaginatedResponse<T> = z.infer<ReturnType<typeof PaginatedResponseSchema<z.ZodType<T>>>>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 错误响应 Schema
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/shared/Error.schema.ts
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
export const ErrorResponseSchema = z.object({
|
|||
|
|
success: z.literal(false),
|
|||
|
|
error: z.object({
|
|||
|
|
code: z.string(),
|
|||
|
|
message: z.string(),
|
|||
|
|
details: z.array(z.object({
|
|||
|
|
field: z.string(),
|
|||
|
|
message: z.string()
|
|||
|
|
})).optional()
|
|||
|
|
}),
|
|||
|
|
timestamp: z.date()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>
|
|||
|
|
|
|||
|
|
export function createErrorResponse(
|
|||
|
|
code: string,
|
|||
|
|
message: string,
|
|||
|
|
details?: Array<{ field: string; message: string }>
|
|||
|
|
): ErrorResponse {
|
|||
|
|
return ErrorResponseSchema.parse({
|
|||
|
|
success: false,
|
|||
|
|
error: { code, message, details },
|
|||
|
|
timestamp: new Date()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Schema 使用实践(Schema Usage Practices)
|
|||
|
|
|
|||
|
|
### 8.1 在 Controller 中使用
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/api/controllers/UserController.ts
|
|||
|
|
import { Request, Response } from 'express'
|
|||
|
|
import { CreateUserRequestSchema, CreateUserResponseSchema } from '@/schemas/api/user'
|
|||
|
|
import { UserService } from '@/services/UserService'
|
|||
|
|
|
|||
|
|
export class UserController {
|
|||
|
|
constructor(private readonly userService: UserService) {}
|
|||
|
|
|
|||
|
|
async createUser(req: Request, res: Response): Promise<void> {
|
|||
|
|
try {
|
|||
|
|
// 验证请求数据
|
|||
|
|
const requestData = CreateUserRequestSchema.parse(req.body)
|
|||
|
|
|
|||
|
|
// 调用服务
|
|||
|
|
const user = await this.userService.createUser(requestData)
|
|||
|
|
|
|||
|
|
// 构建响应
|
|||
|
|
const responseData = CreateUserResponseSchema.parse({
|
|||
|
|
success: true,
|
|||
|
|
data: {
|
|||
|
|
id: user.id,
|
|||
|
|
username: user.username,
|
|||
|
|
email: user.email,
|
|||
|
|
role: user.role,
|
|||
|
|
createdAt: user.createdAt
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
res.json(responseData)
|
|||
|
|
} catch (error) {
|
|||
|
|
if (error instanceof z.ZodError) {
|
|||
|
|
res.status(400).json({
|
|||
|
|
success: false,
|
|||
|
|
error: {
|
|||
|
|
code: 'VALIDATION_ERROR',
|
|||
|
|
message: 'Invalid request data',
|
|||
|
|
details: error.errors.map(err => ({
|
|||
|
|
field: err.path.join('.'),
|
|||
|
|
message: err.message
|
|||
|
|
}))
|
|||
|
|
},
|
|||
|
|
timestamp: new Date()
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
res.status(500).json({
|
|||
|
|
success: false,
|
|||
|
|
error: {
|
|||
|
|
code: 'INTERNAL_ERROR',
|
|||
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|||
|
|
},
|
|||
|
|
timestamp: new Date()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 在 Service 中使用
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/services/UserService.ts
|
|||
|
|
import { UserSchema, type User } from '@/schemas/domain/User.schema'
|
|||
|
|
import { UserRepository } from '@/repositories/UserRepository'
|
|||
|
|
|
|||
|
|
export class UserService {
|
|||
|
|
constructor(private readonly userRepository: UserRepository) {}
|
|||
|
|
|
|||
|
|
async createUser(data: {
|
|||
|
|
username: string
|
|||
|
|
email: string
|
|||
|
|
password: string
|
|||
|
|
role: string
|
|||
|
|
}): Promise<User> {
|
|||
|
|
// 创建用户实体
|
|||
|
|
const user = UserSchema.parse({
|
|||
|
|
id: crypto.randomUUID(),
|
|||
|
|
merchantId: data.merchantId,
|
|||
|
|
username: data.username,
|
|||
|
|
email: data.email,
|
|||
|
|
role: data.role as any,
|
|||
|
|
status: 'ACTIVE',
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
updatedAt: new Date()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 保存到数据库
|
|||
|
|
return await this.userRepository.create(user)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getUserById(id: string): Promise<User | null> {
|
|||
|
|
const userData = await this.userRepository.findById(id)
|
|||
|
|
|
|||
|
|
if (!userData) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证数据符合 Schema
|
|||
|
|
return UserSchema.parse(userData)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.3 在 Repository 中使用
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/repositories/UserRepository.ts
|
|||
|
|
import { Knex } from 'knex'
|
|||
|
|
import { UserSchema, type User } from '@/schemas/domain/User.schema'
|
|||
|
|
|
|||
|
|
export class UserRepository {
|
|||
|
|
constructor(private readonly db: Knex) {}
|
|||
|
|
|
|||
|
|
async create(user: User): Promise<User> {
|
|||
|
|
const [createdUser] = await this.db('users')
|
|||
|
|
.insert({
|
|||
|
|
id: user.id,
|
|||
|
|
merchant_id: user.merchantId,
|
|||
|
|
username: user.username,
|
|||
|
|
email: user.email,
|
|||
|
|
role: user.role,
|
|||
|
|
status: user.status,
|
|||
|
|
created_at: user.createdAt,
|
|||
|
|
updated_at: user.updatedAt
|
|||
|
|
})
|
|||
|
|
.returning('*')
|
|||
|
|
|
|||
|
|
// 验证并返回
|
|||
|
|
return UserSchema.parse(this.mapToEntity(createdUser))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async findById(id: string): Promise<User | null> {
|
|||
|
|
const row = await this.db('users').where({ id }).first()
|
|||
|
|
|
|||
|
|
if (!row) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return UserSchema.parse(this.mapToEntity(row))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private mapToEntity(row: any): User {
|
|||
|
|
return {
|
|||
|
|
id: row.id,
|
|||
|
|
merchantId: row.merchant_id,
|
|||
|
|
username: row.username,
|
|||
|
|
email: row.email,
|
|||
|
|
role: row.role,
|
|||
|
|
status: row.status,
|
|||
|
|
createdAt: new Date(row.created_at),
|
|||
|
|
updatedAt: new Date(row.updated_at)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 前端 Schema 使用(Frontend Schema Usage)
|
|||
|
|
|
|||
|
|
### 9.1 API 调用验证
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/services/api/user.ts
|
|||
|
|
import axios from 'axios'
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
import { GetUserResponseSchema, type GetUserResponse } from '@/schemas/api/user/GetUserResponse.schema'
|
|||
|
|
|
|||
|
|
export async function getUser(userId: string): Promise<GetUserResponse> {
|
|||
|
|
const response = await axios.get(`/api/users/${userId}`)
|
|||
|
|
|
|||
|
|
// 验证响应数据
|
|||
|
|
return GetUserResponseSchema.parse(response.data)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 表单验证
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/components/UserForm.tsx
|
|||
|
|
import { useState } from 'react'
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
import { CreateUserRequestSchema } from '@/schemas/api/user/CreateUserRequest.schema'
|
|||
|
|
|
|||
|
|
export function UserForm() {
|
|||
|
|
const [formData, setFormData] = useState({
|
|||
|
|
username: '',
|
|||
|
|
email: '',
|
|||
|
|
password: '',
|
|||
|
|
role: 'OPERATOR'
|
|||
|
|
})
|
|||
|
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
|||
|
|
|
|||
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 验证表单数据
|
|||
|
|
CreateUserRequestSchema.parse(formData)
|
|||
|
|
|
|||
|
|
// 提交数据
|
|||
|
|
submitForm(formData)
|
|||
|
|
} catch (error) {
|
|||
|
|
if (error instanceof z.ZodError) {
|
|||
|
|
const errorMap: Record<string, string> = {}
|
|||
|
|
error.errors.forEach(err => {
|
|||
|
|
const field = err.path.join('.')
|
|||
|
|
errorMap[field] = err.message
|
|||
|
|
})
|
|||
|
|
setErrors(errorMap)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<form onSubmit={handleSubmit}>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={formData.username}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
|||
|
|
placeholder="Username"
|
|||
|
|
/>
|
|||
|
|
{errors.username && <span className="error">{errors.username}</span>}
|
|||
|
|
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={formData.email}
|
|||
|
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|||
|
|
placeholder="Email"
|
|||
|
|
/>
|
|||
|
|
{errors.email && <span className="error">{errors.email}</span>}
|
|||
|
|
|
|||
|
|
<button type="submit">Submit</button>
|
|||
|
|
</form>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 高级用法(Advanced Usage)
|
|||
|
|
|
|||
|
|
### 10.1 Schema 扩展
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
// 基础 Schema
|
|||
|
|
const BaseUserSchema = z.object({
|
|||
|
|
id: z.string().uuid(),
|
|||
|
|
username: z.string(),
|
|||
|
|
email: z.string().email()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 扩展 Schema
|
|||
|
|
const ExtendedUserSchema = BaseUserSchema.extend({
|
|||
|
|
role: z.enum(['ADMIN', 'MANAGER', 'OPERATOR']),
|
|||
|
|
status: z.enum(['ACTIVE', 'INACTIVE'])
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 合并 Schema
|
|||
|
|
const UserWithProfileSchema = z.object({
|
|||
|
|
user: BaseUserSchema,
|
|||
|
|
profile: z.object({
|
|||
|
|
firstName: z.string(),
|
|||
|
|
lastName: z.string(),
|
|||
|
|
phone: z.string()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 10.2 条件验证
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
const OrderSchema = z.object({
|
|||
|
|
type: z.enum(['ONLINE', 'OFFLINE']),
|
|||
|
|
// ONLINE 订单必须有 email
|
|||
|
|
email: z.string().email().optional(),
|
|||
|
|
// OFFLINE 订单必须有 phone
|
|||
|
|
phone: z.string().optional()
|
|||
|
|
}).refine(
|
|||
|
|
(data) => {
|
|||
|
|
if (data.type === 'ONLINE') {
|
|||
|
|
return !!data.email
|
|||
|
|
}
|
|||
|
|
return !!data.phone
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
message: 'Email is required for ONLINE orders, Phone is required for OFFLINE orders',
|
|||
|
|
path: []
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 10.3 自定义错误消息
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
const UserSchema = z.object({
|
|||
|
|
username: z.string().min(3, { message: 'Username must be at least 3 characters' }),
|
|||
|
|
email: z.string().email({ message: 'Invalid email format' }),
|
|||
|
|
age: z.number().min(18, { message: 'Must be at least 18 years old' })
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 10.4 异步验证
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { z } from 'zod'
|
|||
|
|
|
|||
|
|
const UniqueEmailSchema = z.string().email().refine(
|
|||
|
|
async (email) => {
|
|||
|
|
const existingUser = await userRepository.findByEmail(email)
|
|||
|
|
return !existingUser
|
|||
|
|
},
|
|||
|
|
{ message: 'Email already exists' }
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 性能优化(Performance Optimization)
|
|||
|
|
|
|||
|
|
### 11.1 避免重复解析
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 错误:每次都解析
|
|||
|
|
function processUserData(data: unknown) {
|
|||
|
|
const user = UserSchema.parse(data)
|
|||
|
|
return user.name
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 正确:缓存解析结果
|
|||
|
|
const userCache = new Map<string, User>()
|
|||
|
|
|
|||
|
|
function processUserData(data: unknown) {
|
|||
|
|
const cacheKey = JSON.stringify(data)
|
|||
|
|
|
|||
|
|
if (userCache.has(cacheKey)) {
|
|||
|
|
return userCache.get(cacheKey)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const user = UserSchema.parse(data)
|
|||
|
|
userCache.set(cacheKey, user)
|
|||
|
|
return user
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.2 使用 safeParse 避免异常
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 错误:使用 parse 会抛出异常
|
|||
|
|
function validateData(data: unknown) {
|
|||
|
|
try {
|
|||
|
|
return UserSchema.parse(data)
|
|||
|
|
} catch (error) {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 正确:使用 safeParse
|
|||
|
|
function validateData(data: unknown) {
|
|||
|
|
const result = UserSchema.safeParse(data)
|
|||
|
|
return result.success ? result.data : null
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. 测试 Schema(Testing Schemas)
|
|||
|
|
|
|||
|
|
### 12.1 单元测试
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/schemas/__tests__/User.schema.test.ts
|
|||
|
|
import { describe, it, expect } from 'vitest'
|
|||
|
|
import { UserSchema } from '../User.schema'
|
|||
|
|
|
|||
|
|
describe('UserSchema', () => {
|
|||
|
|
it('should validate valid user data', () => {
|
|||
|
|
const validUser = {
|
|||
|
|
id: '123e4567-e89b-12d3-a456-426614174000',
|
|||
|
|
merchantId: '123e4567-e89b-12d3-a456-426614174001',
|
|||
|
|
username: 'john_doe',
|
|||
|
|
email: 'john@example.com',
|
|||
|
|
role: 'ADMIN',
|
|||
|
|
status: 'ACTIVE',
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
updatedAt: new Date()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = UserSchema.safeParse(validUser)
|
|||
|
|
expect(result.success).toBe(true)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should reject invalid email', () => {
|
|||
|
|
const invalidUser = {
|
|||
|
|
id: '123e4567-e89b-12d3-a456-426614174000',
|
|||
|
|
merchantId: '123e4567-e89b-12d3-a456-426614174001',
|
|||
|
|
username: 'john_doe',
|
|||
|
|
email: 'invalid-email',
|
|||
|
|
role: 'ADMIN',
|
|||
|
|
status: 'ACTIVE',
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
updatedAt: new Date()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = UserSchema.safeParse(invalidUser)
|
|||
|
|
expect(result.success).toBe(false)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('should reject invalid UUID', () => {
|
|||
|
|
const invalidUser = {
|
|||
|
|
id: 'not-a-uuid',
|
|||
|
|
merchantId: '123e4567-e89b-12d3-a456-426614174001',
|
|||
|
|
username: 'john_doe',
|
|||
|
|
email: 'john@example.com',
|
|||
|
|
role: 'ADMIN',
|
|||
|
|
status: 'ACTIVE',
|
|||
|
|
createdAt: new Date(),
|
|||
|
|
updatedAt: new Date()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = UserSchema.safeParse(invalidUser)
|
|||
|
|
expect(result.success).toBe(false)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. 最佳实践(Best Practices)
|
|||
|
|
|
|||
|
|
### 13.1 Schema 设计原则
|
|||
|
|
|
|||
|
|
| 原则 | 说明 | 示例 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| **单一职责** | 每个 Schema 只负责一个实体 | `UserSchema`, `ProductSchema` |
|
|||
|
|
| **可复用性** | 提取公共 Schema 为共享模块 | `PaginationSchema`, `ErrorSchema` |
|
|||
|
|
| **明确性** | 使用具体的类型而非 any | `z.string().email()` 而非 `z.any()` |
|
|||
|
|
| **验证性** | 所有外部数据必须验证 | API 请求、数据库查询 |
|
|||
|
|
| **文档化** | Schema 即文档,保持清晰 | 使用有意义的字段名 |
|
|||
|
|
|
|||
|
|
### 13.2 常见错误
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ 错误 1:使用 any
|
|||
|
|
const BadSchema = z.object({
|
|||
|
|
data: z.any()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ✅ 正确 1:使用具体类型
|
|||
|
|
const GoodSchema = z.object({
|
|||
|
|
data: z.object({
|
|||
|
|
id: z.string(),
|
|||
|
|
name: z.string()
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ❌ 错误 2:不验证外部数据
|
|||
|
|
async function badHandler(req: Request) {
|
|||
|
|
const user = req.body as User
|
|||
|
|
return user
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 正确 2:验证外部数据
|
|||
|
|
async function goodHandler(req: Request) {
|
|||
|
|
const user = UserSchema.parse(req.body)
|
|||
|
|
return user
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ❌ 错误 3:重复定义类型
|
|||
|
|
interface User {
|
|||
|
|
id: string
|
|||
|
|
name: string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const UserSchema = z.object({
|
|||
|
|
id: z.string(),
|
|||
|
|
name: z.string()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// ✅ 正确 3:从 Schema 推导类型
|
|||
|
|
const UserSchema = z.object({
|
|||
|
|
id: z.string(),
|
|||
|
|
name: z.string()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
type User = z.infer<typeof UserSchema>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 14. 版本管理
|
|||
|
|
|
|||
|
|
| 版本 | 变更内容 | 日期 |
|
|||
|
|
|------|----------|------|
|
|||
|
|
| 1.0.0 | 初始版本 | 2026-03-20 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 15. 参考资源
|
|||
|
|
|
|||
|
|
- [zod 官方文档](https://zod.dev/)
|
|||
|
|
- [class-validator 文档](https://github.com/typestack/class-validator)
|
|||
|
|
- [Schema 驱动开发最佳实践](https://www.patterns.dev/posts/schema-driven-development/)
|