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
