Skip to content

10 · 实战:一份生产级电商 API 完整文档

你已经学完了 9 章基础知识,这一章把所有知识揉成一份真实可用、能直接抄的 openapi.yaml——商品 + 购物车 + 订单 + 用户 + 鉴权 + 全套 components 复用,配套 docker-compose 一键起 UI + Mock + Editor。


1. 业务场景

              ┌─────────────────────────────┐
              │       电商前后端示例         │
              └─────────────────────────────┘

   GET /products            ← 商品列表(首页 / 分类)
   GET /products/{sku}      ← 商品详情
  POST /products            ← 上架商品(管理员)

   GET /cart                ← 我的购物车
  POST /cart/items          ← 加购
  PUT /cart/items/{sku}     ← 改数量
  DEL /cart/items/{sku}     ← 移除

  POST /orders              ← 下单(购物车 → 订单)
   GET /orders              ← 我的订单
   GET /orders/{id}         ← 订单详情
  POST /orders/{id}:cancel  ← 取消订单

  POST /auth/login          ← 登录拿 token
  POST /auth/register       ← 注册
   GET /me                  ← 当前用户信息

功能清单

  • ✅ 完整 CRUD + 子资源
  • ✅ JWT 鉴权(含全局 + 单接口豁免)
  • ✅ 完整 components.schemas 复用
  • ✅ 通用 responses (Unauthorized / NotFound / ValidationError)
  • ✅ 分页 / 排序 / 搜索通用参数
  • ✅ 多 example,调试体验好
  • ✅ 图片上传(multipart/form-data)
  • ✅ 自定义动作(POST /orders/{id}:cancel

2. 文件目录结构

deploy/
├── docker-compose.yml         ← 一键起 UI + Editor + Mock
├── openapi.yaml               ← 主文件(本章重点)
├── README.md
└── postman/                   ← 自动生成的测试集合

3. 完整 openapi.yaml

全文见 10_real_case/code/openapi.yaml(800+ 行)。 下面摘出关键设计点讲解。

3.1 全局头部 + 鉴权

yaml
openapi: 3.0.3
info:
  title: 电商示例 API
  version: 1.0.0
  description: |
    基于 OpenAPI 3.0 的电商 API 完整示例。

    ## 鉴权
    所有接口默认要 Bearer Token;标注 `🔓` 的接口免鉴权。

    ## 限流
    每用户每秒最多 10 次请求;超过返回 `429`。

    ## 联系方式
    📧 dev@example.com
  contact: { name: 团队, email: dev@example.com }
  license: { name: MIT }

servers:
  - url: http://localhost:3000/v1
    description: 本地
  - url: https://api.shop.example.com/v1
    description: 生产

tags:
  - { name: auth,    description: '🔐 鉴权 / 登录' }
  - { name: user,    description: '👤 用户' }
  - { name: product, description: '🛍 商品' }
  - { name: cart,    description: '🛒 购物车' }
  - { name: order,   description: '📦 订单' }

security:
  - bearerAuth: []

3.2 鉴权接口(免鉴权 = security: []

yaml
paths:
  /auth/login:
    post:
      tags: [auth]
      operationId: login
      summary: 登录
      security: []                       # 🔓 公开
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LoginRequest' }
      responses:
        '200':
          description: 登录成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LoginResponse' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/ValidationError'

3.3 商品列表(带分页 / 搜索 / 多 example)

yaml
paths:
  /products:
    get:
      tags: [product]
      operationId: listProducts
      summary: 商品列表
      security: []
      parameters:
        - $ref: '#/components/parameters/PageQuery'
        - $ref: '#/components/parameters/SizeQuery'
        - name: category
          in: query
          schema: { type: string }
          example: phone
        - name: q
          in: query
          schema: { type: string }
          description: 搜索关键词
          example: iPhone
      responses:
        '200':
          description: 商品列表
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductListResponse' }
              examples:
                empty:
                  summary: 空结果
                  value: { items: [], meta: { page: 1, size: 20, total: 0 } }
                normal:
                  summary: 有数据
                  value:
                    items:
                      - sku: P-001
                        name: iPhone 15
                        price: 5999
                    meta: { page: 1, size: 20, total: 100 }

3.4 上传商品图(multipart)

yaml
paths:
  /products/{sku}/image:
    post:
      tags: [product]
      operationId: uploadProductImage
      summary: 上传商品主图(管理员)
      parameters:
        - $ref: '#/components/parameters/SkuPath'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: 图片文件(PNG / JPG,<= 2MB)
              required: [file]
      responses:
        '200':
          description: 上传成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  url: { type: string, format: uri }

3.5 自定义动作 :cancel

yaml
paths:
  /orders/{id}:cancel:
    post:
      tags: [order]
      operationId: cancelOrder
      summary: 取消订单
      parameters:
        - $ref: '#/components/parameters/OrderIdPath'
      responses:
        '200':
          description: 取消成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '409':
          description: 状态冲突(已发货/已完成不能取消)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

3.6 完整的 components 复用

yaml
components:
  parameters:
    PageQuery:
      name: page
      in: query
      schema: { type: integer, default: 1, minimum: 1 }
    SizeQuery:
      name: size
      in: query
      schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
    SkuPath:
      name: sku
      in: path
      required: true
      schema: { type: string, pattern: '^P-\d{3,}$' }
      example: P-001
    OrderIdPath:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }

  schemas:
    Product:
      type: object
      required: [sku, name, price]
      properties:
        sku: { type: string, example: P-001 }
        name: { type: string, example: iPhone 15 }
        price: { type: number, format: double, example: 5999 }
        stock: { type: integer, example: 100 }
        category: { type: string, example: phone }
        imageUrl: { type: string, format: uri }
        createdAt: { type: string, format: date-time, readOnly: true }

    ProductListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Product' }
        meta: { $ref: '#/components/schemas/PageMeta' }

    PageMeta:
      type: object
      properties:
        page: { type: integer }
        size: { type: integer }
        total: { type: integer }

    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: integer, example: 400 }
        message: { type: string, example: '参数错误' }
        traceId: { type: string }

  responses:
    Unauthorized:
      description: 未登录或 token 过期
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { code: 401, message: 'Token 已过期' }
    NotFound:
      description: 资源不存在
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ValidationError:
      description: 参数校验失败
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

整份 yaml 看下来你会发现:90% 是 componentsresponsespaths 里几乎全是 $ref ——这就是工程级 yaml 的样子。


4. 一键起服务(docker-compose)

yaml
# docker-compose.yml
version: '3.8'
services:
  ui:
    image: swaggerapi/swagger-ui
    ports: ["8080:8080"]
    environment:
      SWAGGER_JSON: /spec/openapi.yaml
    volumes:
      - ./openapi.yaml:/spec/openapi.yaml:ro

  editor:
    image: swaggerapi/swagger-editor
    ports: ["8081:8080"]
    volumes:
      - ./openapi.yaml:/tmp/openapi.yaml:ro
    environment:
      SWAGGER_FILE: /tmp/openapi.yaml

  mock:
    image: stoplight/prism:4
    ports: ["4010:4010"]
    command: mock -h 0.0.0.0 /spec/openapi.yaml
    volumes:
      - ./openapi.yaml:/spec/openapi.yaml:ro
bash
docker compose up -d

打开:

  • http://localhost:8080 → Swagger UI 看文档
  • http://localhost:8081 → Swagger Editor 改 yaml
  • http://localhost:4010/products → Prism Mock 自动返回符合 schema 的假数据

5. 给前端生成 SDK

bash
docker run --rm -v $PWD:/local \
  openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g typescript-axios \
  -o /local/sdk-ts \
  --additional-properties=npmName=@shop/api-sdk,npmVersion=1.0.0

前端业务代码:

typescript
import { ProductApi, CartApi, OrderApi, Configuration } from '@shop/api-sdk';

const cfg = new Configuration({
  basePath: 'http://localhost:3000/v1',
  accessToken: () => localStorage.getItem('token') || '',
});

const productApi = new ProductApi(cfg);
const orderApi = new OrderApi(cfg);

// 100% 类型安全
const { data } = await productApi.listProducts(1, 20, 'phone', 'iPhone');
data.items.forEach(p => console.log(p.name, p.price));

const order = await orderApi.createOrder({
  items: [{ sku: 'P-001', quantity: 2 }]
});

6. 给后端生成接口骨架

bash
docker run --rm -v $PWD:/local \
  openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g spring \
  -o /local/server \
  --additional-properties=interfaceOnly=true,useSpringBoot3=true

后端只要 implements 这些接口即可——所有 @RestController@RequestMapping、入参校验、文档注解全部生成好。


7. CI 全套自动化

yaml
# .github/workflows/api.yml
name: API
on:
  push:
    paths: ['openapi.yaml']
  pull_request:
    paths: ['openapi.yaml']

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint with Spectral
        run: npx -y @stoplight/spectral-cli lint openapi.yaml

      - name: Diff (breaking changes)
        run: |
          git fetch origin main
          npx -y openapi-diff origin/main:openapi.yaml openapi.yaml --fail-on-incompatible

  publish:
    needs: validate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Generate TS SDK
        run: |
          npx -y @openapitools/openapi-generator-cli generate \
            -i openapi.yaml -g typescript-axios -o sdk-ts \
            --additional-properties=npmName=@shop/api-sdk,npmVersion=$(date +%Y.%m.%d.%H%M)
      - name: Publish npm
        run: cd sdk-ts && npm publish --access public
        env: { NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} }

      - name: Build static docs
        run: npx -y @redocly/cli build-docs openapi.yaml -o public/api.html

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./public

效果:

  • 改了 yaml → push → 自动 lint + breaking 检测
  • 合并到 main → 自动发 npm SDK + 部署到 GitHub Pages 文档站

8. 工程经验总结

✅ openapi.yaml 跟业务代码同 repo,CI 严格 lint
✅ 所有数据模型走 components.schemas,paths 里全 $ref
✅ 通用响应(401/404/422/默认 Error)抽到 components.responses
✅ 通用参数(PageQuery、SizeQuery)抽到 components.parameters
✅ 给每个 operation 写 operationId(决定生成的方法名)
✅ Mock + UI + Editor 三件套一份 docker-compose 启
✅ CI 自动生成 SDK 发 npm,自动构建文档站
❌ 别在 yaml 里写真 token / API key(提交即泄漏)
❌ 别让一份 yaml 超过 5000 行 — 该拆就拆
❌ 别只写文档,要在 CI 里跑 schemathesis 模糊测试,确保后端真的符合

9. 代码资源

  • 10_real_case/code/openapi.yaml · 完整可用的 800+ 行 yaml
  • 10_real_case/code/docker-compose.yml · 一键起 UI + Editor + Mock
  • 10_real_case/code/README.md · 启动说明

→ 进 10_real_case/code/ 目录直接抄。

💻 示例代码

yaml
version: '3.8'

services:
  ui:
    image: swaggerapi/swagger-ui
    container_name: shop-swagger-ui
    ports: ["8080:8080"]
    environment:
      SWAGGER_JSON: /spec/openapi.yaml
      DOC_EXPANSION: list
      DEEP_LINKING: "true"
      DISPLAY_REQUEST_DURATION: "true"
    volumes:
      - ./openapi.yaml:/spec/openapi.yaml:ro

  editor:
    image: swaggerapi/swagger-editor
    container_name: shop-swagger-editor
    ports: ["8081:8080"]
    volumes:
      - ./openapi.yaml:/tmp/openapi.yaml:ro
    environment:
      SWAGGER_FILE: /tmp/openapi.yaml

  mock:
    image: stoplight/prism:4
    container_name: shop-mock
    ports: ["4010:4010"]
    command: mock -h 0.0.0.0 /spec/openapi.yaml
    volumes:
      - ./openapi.yaml:/spec/openapi.yaml:ro

  redoc:
    image: redocly/redoc
    container_name: shop-redoc
    ports: ["8082:80"]
    environment:
      SPEC_URL: openapi.yaml
    volumes:
      - ./openapi.yaml:/usr/share/nginx/html/openapi.yaml:ro
yaml
openapi: 3.0.3

info:
  title: 电商示例 API
  version: 1.0.0
  description: |
    基于 OpenAPI 3.0 的电商 API 完整示例。

    ## 鉴权
    所有接口默认要 Bearer Token;标注 `🔓` 的接口免鉴权(公开接口 / 登录注册)。

    ## 限流
    每用户每秒最多 10 次请求;超过返回 `429 Too Many Requests`。

    ## 错误格式
    ```json
    { "code": 400, "message": "...", "traceId": "abc-123" }
    ```
  contact:
    name: 团队联系人
    email: dev@example.com
  license:
    name: MIT

servers:
  - url: http://localhost:3000/v1
    description: 本地
  - url: https://staging.shop.example.com/v1
    description: 测试
  - url: https://api.shop.example.com/v1
    description: 生产

tags:
  - { name: auth,    description: '🔐 鉴权 / 登录注册' }
  - { name: user,    description: '👤 用户' }
  - { name: product, description: '🛍 商品' }
  - { name: cart,    description: '🛒 购物车' }
  - { name: order,   description: '📦 订单' }

security:
  - bearerAuth: []

paths:
  # ========================== AUTH ==========================
  /auth/register:
    post:
      tags: [auth]
      operationId: register
      summary: 注册
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RegisterRequest' }
      responses:
        '201':
          description: 注册成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '400':
          $ref: '#/components/responses/ValidationError'
        '409':
          description: 邮箱已被注册
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /auth/login:
    post:
      tags: [auth]
      operationId: login
      summary: 登录
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LoginRequest' }
            examples:
              normal:
                summary: 普通用户
                value: { email: alice@x.com, password: 's3cret' }
              admin:
                summary: 管理员
                value: { email: admin@x.com, password: 'admin123' }
      responses:
        '200':
          description: 登录成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LoginResponse' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ========================== ME ==========================
  /me:
    get:
      tags: [user]
      operationId: getMe
      summary: 当前用户信息
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ========================== PRODUCTS ==========================
  /products:
    get:
      tags: [product]
      operationId: listProducts
      summary: 商品列表
      security: []
      parameters:
        - $ref: '#/components/parameters/PageQuery'
        - $ref: '#/components/parameters/SizeQuery'
        - name: category
          in: query
          schema: { type: string }
          example: phone
        - name: q
          in: query
          description: 搜索关键词
          schema: { type: string }
          example: iPhone
        - name: sort
          in: query
          schema:
            type: string
            enum: [price_asc, price_desc, newest]
            default: newest
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ProductListResponse' }
              examples:
                empty:
                  summary: 空结果
                  value: { items: [], meta: { page: 1, size: 20, total: 0 } }
                normal:
                  summary: 有数据
                  value:
                    items:
                      - { sku: P-001, name: iPhone 15, price: 5999, stock: 100, category: phone }
                      - { sku: P-002, name: AirPods Pro, price: 1899, stock: 50, category: audio }
                    meta: { page: 1, size: 20, total: 100 }

    post:
      tags: [product]
      operationId: createProduct
      summary: 上架商品(管理员)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProductCreate' }
      responses:
        '201':
          description: 创建成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Product' }
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'

  /products/{sku}:
    parameters:
      - $ref: '#/components/parameters/SkuPath'
    get:
      tags: [product]
      operationId: getProduct
      summary: 商品详情
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Product' }
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [product]
      operationId: updateProduct
      summary: 修改商品(管理员)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProductUpdate' }
      responses:
        '200':
          description: 更新成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Product' }
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [product]
      operationId: deleteProduct
      summary: 下架商品(管理员)
      responses:
        '204':
          description: 删除成功
        '404':
          $ref: '#/components/responses/NotFound'

  /products/{sku}/image:
    post:
      tags: [product]
      operationId: uploadProductImage
      summary: 上传商品主图
      parameters:
        - $ref: '#/components/parameters/SkuPath'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file:
                  type: string
                  format: binary
                  description: 图片文件(PNG / JPG,<= 2MB)
              required: [file]
      responses:
        '200':
          description: 上传成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    example: https://cdn.example.com/products/P-001.png

  # ========================== CART ==========================
  /cart:
    get:
      tags: [cart]
      operationId: getCart
      summary: 获取购物车
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Cart' }

    delete:
      tags: [cart]
      operationId: clearCart
      summary: 清空购物车
      responses:
        '204':
          description: 已清空

  /cart/items:
    post:
      tags: [cart]
      operationId: addCartItem
      summary: 加入购物车
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CartItemCreate' }
      responses:
        '200':
          description: 已加入
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Cart' }
        '404':
          $ref: '#/components/responses/NotFound'

  /cart/items/{sku}:
    parameters:
      - $ref: '#/components/parameters/SkuPath'
    put:
      tags: [cart]
      operationId: updateCartItem
      summary: 改数量
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [quantity]
              properties:
                quantity: { type: integer, minimum: 1, maximum: 999 }
      responses:
        '200':
          description: 已更新
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Cart' }

    delete:
      tags: [cart]
      operationId: removeCartItem
      summary: 移除商品
      responses:
        '204':
          description: 已移除

  # ========================== ORDERS ==========================
  /orders:
    get:
      tags: [order]
      operationId: listOrders
      summary: 我的订单
      parameters:
        - $ref: '#/components/parameters/PageQuery'
        - $ref: '#/components/parameters/SizeQuery'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, paid, shipped, completed, cancelled]
      responses:
        '200':
          description: 订单列表
          content:
            application/json:
              schema: { $ref: '#/components/schemas/OrderListResponse' }

    post:
      tags: [order]
      operationId: createOrder
      summary: 创建订单(购物车 → 订单)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OrderCreate' }
      responses:
        '201':
          description: 订单已创建
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '400':
          $ref: '#/components/responses/ValidationError'
        '402':
          description: 余额不足 / 支付失败
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

  /orders/{id}:
    parameters:
      - $ref: '#/components/parameters/OrderIdPath'
    get:
      tags: [order]
      operationId: getOrder
      summary: 订单详情
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '404':
          $ref: '#/components/responses/NotFound'

  /orders/{id}:cancel:
    post:
      tags: [order]
      operationId: cancelOrder
      summary: 取消订单
      parameters:
        - $ref: '#/components/parameters/OrderIdPath'
      responses:
        '200':
          description: 取消成功
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Order' }
        '409':
          description: 状态冲突(已发货/已完成不能取消)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }

# ========================== COMPONENTS ==========================
components:

  parameters:
    PageQuery:
      name: page
      in: query
      description: 页码
      schema: { type: integer, default: 1, minimum: 1 }
    SizeQuery:
      name: size
      in: query
      description: 每页条数
      schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
    SkuPath:
      name: sku
      in: path
      required: true
      description: 商品 SKU
      schema:
        type: string
        pattern: '^P-\d{3,}$'
      example: P-001
    OrderIdPath:
      name: id
      in: path
      required: true
      description: 订单 ID
      schema: { type: string, format: uuid }
      example: 550e8400-e29b-41d4-a716-446655440000

  schemas:

    User:
      type: object
      required: [id, email, name]
      properties:
        id: { type: integer, readOnly: true, example: 1 }
        email: { type: string, format: email, example: alice@x.com }
        name: { type: string, example: Alice }
        role:
          type: string
          enum: [user, admin]
          default: user
        avatarUrl: { type: string, format: uri, nullable: true }
        createdAt: { type: string, format: date-time, readOnly: true }

    RegisterRequest:
      type: object
      required: [email, password, name]
      properties:
        email: { type: string, format: email }
        password: { type: string, format: password, minLength: 6, maxLength: 50 }
        name: { type: string, maxLength: 50 }

    LoginRequest:
      type: object
      required: [email, password]
      properties:
        email: { type: string, format: email }
        password: { type: string, format: password }

    LoginResponse:
      type: object
      required: [token, user]
      properties:
        token: { type: string, description: JWT, example: 'eyJhbGciOi...' }
        expiresIn: { type: integer, description: , example: 7200 }
        user: { $ref: '#/components/schemas/User' }

    Product:
      type: object
      required: [sku, name, price, stock]
      properties:
        sku: { type: string, example: P-001 }
        name: { type: string, example: iPhone 15 }
        description: { type: string, example: 全新 A17 芯片 }
        price: { type: number, format: double, minimum: 0, example: 5999 }
        stock: { type: integer, minimum: 0, example: 100 }
        category: { type: string, example: phone }
        imageUrl: { type: string, format: uri, nullable: true }
        createdAt: { type: string, format: date-time, readOnly: true }

    ProductCreate:
      type: object
      required: [sku, name, price, stock]
      properties:
        sku: { type: string, pattern: '^P-\d{3,}$' }
        name: { type: string, maxLength: 100 }
        description: { type: string, maxLength: 1000 }
        price: { type: number, format: double, minimum: 0 }
        stock: { type: integer, minimum: 0 }
        category: { type: string }

    ProductUpdate:
      type: object
      properties:
        name: { type: string, maxLength: 100 }
        description: { type: string, maxLength: 1000 }
        price: { type: number, format: double, minimum: 0 }
        stock: { type: integer, minimum: 0 }
        category: { type: string }

    ProductListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Product' }
        meta: { $ref: '#/components/schemas/PageMeta' }

    PageMeta:
      type: object
      properties:
        page: { type: integer }
        size: { type: integer }
        total: { type: integer }

    Cart:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CartItem' }
        totalQuantity: { type: integer, readOnly: true }
        totalPrice: { type: number, format: double, readOnly: true }

    CartItem:
      type: object
      required: [sku, quantity]
      properties:
        sku: { type: string }
        name: { type: string, readOnly: true }
        price: { type: number, format: double, readOnly: true }
        quantity: { type: integer, minimum: 1, maximum: 999 }
        imageUrl: { type: string, readOnly: true }

    CartItemCreate:
      type: object
      required: [sku, quantity]
      properties:
        sku: { type: string }
        quantity: { type: integer, minimum: 1, maximum: 999, default: 1 }

    Order:
      type: object
      required: [id, userId, items, totalPrice, status, createdAt]
      properties:
        id: { type: string, format: uuid, readOnly: true }
        userId: { type: integer, readOnly: true }
        items:
          type: array
          items: { $ref: '#/components/schemas/OrderItem' }
        totalPrice: { type: number, format: double, readOnly: true }
        status:
          type: string
          enum: [pending, paid, shipped, completed, cancelled]
          readOnly: true
        shippingAddress: { $ref: '#/components/schemas/Address' }
        createdAt: { type: string, format: date-time, readOnly: true }

    OrderItem:
      type: object
      properties:
        sku: { type: string }
        name: { type: string }
        price: { type: number, format: double }
        quantity: { type: integer }

    OrderCreate:
      type: object
      required: [items, shippingAddress]
      properties:
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku: { type: string }
              quantity: { type: integer, minimum: 1 }
        shippingAddress: { $ref: '#/components/schemas/Address' }
        couponCode: { type: string, nullable: true }

    Address:
      type: object
      required: [recipient, phone, country, province, city, detail]
      properties:
        recipient: { type: string, example: 张三 }
        phone: { type: string, example: 13800138000 }
        country: { type: string, example: 中国 }
        province: { type: string, example: 广东 }
        city: { type: string, example: 深圳 }
        detail: { type: string, example: 南山区科技园 }
        zipCode: { type: string, nullable: true }

    OrderListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Order' }
        meta: { $ref: '#/components/schemas/PageMeta' }

    Error:
      type: object
      required: [code, message]
      properties:
        code: { type: integer, example: 400 }
        message: { type: string, example: 参数错误 }
        traceId: { type: string, example: trace-abc-123 }
        details:
          type: object
          additionalProperties: true

  responses:
    Unauthorized:
      description: 未登录或 token 已过期
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { code: 401, message: 'Token 已过期', traceId: 'trace-xxx' }
    Forbidden:
      description: 已登录但无权限
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { code: 403, message: '需要管理员权限' }
    NotFound:
      description: 资源不存在
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example: { code: 404, message: '资源不存在' }
    ValidationError:
      description: 参数校验失败
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            code: 400
            message: 请求参数错误
            details: { email: '必须是合法邮箱' }

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
markdown
# 10 章 · 综合实战代码

一份生产级的电商 API 完整 OpenAPI 文档 + 一键启动的开发环境。

## 文件清单

code/ ├── openapi.yaml ← 800 行完整文档(认真读完一次能学很多) ├── docker-compose.yml ← 一键起 4 个服务 └── README.md ← 本文


## 快速开始

### 1. 启动全套服务

```bash
docker compose up -d

四个服务一键就绪:

服务地址用途
Swagger UIhttp://localhost:8080在线浏览 API 文档
Swagger Editorhttp://localhost:8081在浏览器编辑 yaml
Prism Mockhttp://localhost:4010Mock API 服务
Redochttp://localhost:8082漂亮的发布版文档

2. 调用 Mock API 试试

bash
# 商品列表(公开接口)
curl http://localhost:4010/v1/products

# 登录拿 token
curl -X POST http://localhost:4010/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"alice@x.com","password":"s3cret"}'

# 拿 token 调需要鉴权的接口
curl http://localhost:4010/v1/me \
  -H 'authorization: Bearer fake-token-for-mock'

# Prism 会自动按 schema 校验请求体
curl -X POST http://localhost:4010/v1/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"not-an-email"}'
# → 返回 422,提示 email 格式错误

3. 给前端生成 SDK

bash
docker run --rm -v $PWD:/local \
  openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g typescript-axios \
  -o /local/sdk-ts

4. 给后端生成接口骨架

bash
# Spring Boot
docker run --rm -v $PWD:/local \
  openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g spring \
  -o /local/server-spring \
  --additional-properties=interfaceOnly=true,useSpringBoot3=true

# Node.js Express
docker run --rm -v $PWD:/local \
  openapitools/openapi-generator-cli generate \
  -i /local/openapi.yaml \
  -g nodejs-express-server \
  -o /local/server-node

5. 静态文档发布

bash
npx -y @redocly/cli build-docs openapi.yaml -o public/api.html
# public/api.html 是单文件,扔到任何静态服务器都能用

学习建议

  1. 先用 Swagger UI 浏览全部接口http://localhost:8080),有个全局印象
  2. 打开 openapi.yaml,对照看每个接口对应哪段 yaml
  3. 试着改 yaml 加字段 —— 比如给 Product 加一个 tags: array
  4. 用 codegen 生成自己感兴趣语言的 SDK,看下生成的代码长啥样
  5. 接入到自己的项目,把 yaml 当真理之源

工程检查清单

  • [ ] 用 npx @stoplight/spectral-cli lint openapi.yaml 检查 yaml 质量
  • [ ] CI 里跑 openapi-diff,PR 自动检测 breaking change
  • [ ] 给每个 operation 起好 operationId,让生成的方法名漂亮
  • [ ] 用 components 把模型 / 参数 / 响应都抽出来复用
  • [ ] 自动化部署 Redoc 到内部 wiki / GitHub Pages
  • [ ] 配合 schemathesis 跑模糊测试,确保后端真的符合 yaml

docker-compose.yml ↗ · openapi.yaml ↗ · README.md ↗