# 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 // 验证数据 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 ``` ### 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 ``` ### 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 export type OrderItem = z.infer ``` --- ## 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 // 转换函数:从领域模型到 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 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 ``` ```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 ``` ```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 ``` ### 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 ``` ```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 ``` --- ## 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 export const PaginatedResponseSchema = (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 = z.infer>>> ``` ### 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 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 { 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 { // 创建用户实体 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 { 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 { 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 { 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 { 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>({}) const handleSubmit = (e: React.FormEvent) => { e.preventDefault() try { // 验证表单数据 CreateUserRequestSchema.parse(formData) // 提交数据 submitForm(formData) } catch (error) { if (error instanceof z.ZodError) { const errorMap: Record = {} error.errors.forEach(err => { const field = err.path.join('.') errorMap[field] = err.message }) setErrors(errorMap) } } } return (
setFormData({ ...formData, username: e.target.value })} placeholder="Username" /> {errors.username && {errors.username}} setFormData({ ...formData, email: e.target.value })} placeholder="Email" /> {errors.email && {errors.email}}
) } ``` --- ## 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() 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 ``` --- ## 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/)