Files
makemd/docs/01_Architecture/15_Schema_Driven_Development.md
wurenzhi 427becbc8f refactor(types): 重构类型系统,统一共享类型定义
feat(types): 新增共享类型中心,包含用户、产品、订单等核心领域类型
fix(types): 修复类型定义错误,统一各模块类型引用
style(types): 优化类型文件格式和注释
docs(types): 更新类型文档和变更日志
test(types): 添加类型测试用例
build(types): 配置类型共享路径
chore(types): 清理重复类型定义文件
2026-03-20 17:53:46 +08:00

24 KiB
Raw Blame History

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 安装

npm install zod
npm install -D @types/zod

2.2 基础用法

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 类型

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

// 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

// 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

// 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

// 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

// 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

// 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>
// 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>
// 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

// 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>
// 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

// 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

// 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 中使用

// 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 中使用

// 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 中使用

// 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 调用验证

// 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 表单验证

// 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 扩展

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 条件验证

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 自定义错误消息

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 异步验证

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 避免重复解析

// ❌ 错误:每次都解析
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 避免异常

// ❌ 错误:使用 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 单元测试

// 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 常见错误

// ❌ 错误 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. 参考资源