Files
makemd/docs/ARCHIVE/01_Architecture/15_Schema_Driven_Development.md

1030 lines
24 KiB
Markdown
Raw Normal View History

# Schema 驱动开发指南Schema-Driven Development Guide
> **模块**: 01_Architecture - Schema 驱动开发与数据验证
> **更新日期**: 2026-03-20
> **适用范围**: 全项目dashboard、server、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. 领域模型 SchemaDomain 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 SchemaData 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 SchemaAPI 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. 共享 SchemaShared 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. 测试 SchemaTesting 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/)