openapi: 3.1.0
info:
  title: Mobile Publish Batches and Jobs API
  version: 0.1.0
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  description: |
    面向内容生产系统的扫码发布任务 API。本 API 只保存任务元数据和媒体 URL，
    不下载、转存或转码媒体。“已尝试打开”不代表内容已最终发布。
    项目级 API Key 只能访问所属项目；创建内容任务可能消耗客户空间额度。
servers:
  - url: https://{host}
    description: 部署后的实际 HTTPS 域名
    variables:
      host:
        default: scan.your-domain.cn
        description: 当前部署使用的公开域名
security:
  - ApiKeyAuth: []
paths:
  /healthz:
    get:
      operationId: readinessCheck
      summary: 检查生产配置与数据库就绪状态
      security: []
      responses:
        '200':
          description: 服务可接收流量
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    const: ok
        '503':
          description: 生产配置或数据库暂未就绪
  /api/health:
    get:
      operationId: healthCheck
      summary: 检查应用与数据库健康状态
      security: []
      responses:
        '200':
          description: 服务可用
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    const: ok
        '503':
          description: 服务或数据库暂不可用
  /api/v1/batches:
    get:
      operationId: listPublishBatches
      summary: 查询发布批次列表
      parameters:
        - name: q
          in: query
          schema:
            type: string
            maxLength: 100
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/BatchStatus'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: 发布批次列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createPublishBatch
      summary: 创建发布批次，可在同一事务中批量创建内容
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateBatchRequest'
      responses:
        '201':
          description: 发布批次已创建
          headers:
            Location:
              description: 批次查询地址
              schema:
                type: string
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreateResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/batches/{id}:
    parameters:
      - $ref: '#/components/parameters/BatchId'
    get:
      operationId: getPublishBatch
      summary: 查询发布批次
      responses:
        '200':
          description: 发布批次详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
    patch:
      operationId: updatePublishBatch
      summary: 编辑、归档或撤销发布批次
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateBatchRequest'
      responses:
        '200':
          description: 发布批次已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      operationId: deletePublishBatch
      summary: 删除发布批次
      description: 删除批次不会删除任务或上游媒体，只解除任务归属并撤销批次公开入口。
      responses:
        '204':
          description: 批次已删除；重复删除也返回 204
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/batches/{id}/summary:
    parameters:
      - $ref: '#/components/parameters/BatchId'
    get:
      operationId: getPublicBatchSummary
      summary: 查询移动端所需的批次计数
      description: 只返回批次名称和聚合计数，不返回未领取内容。
      security: []
      responses:
        '200':
          description: 批次公开摘要
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/jobs:
    post:
      operationId: createPublishJob
      summary: 创建扫码发布任务
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
            examples:
              gallery:
                summary: 图文任务
                value:
                  external_ref: lk_20260905_0001
                  content_type: gallery
                  title: 秋日城市漫步
                  body: 一组适合周末出发的城市漫步灵感。
                  tags: [城市漫步, 周末去哪]
                  media:
                    images:
                      - https://cdn.example.com/content/cover.jpg
                  expires_in: 86400
              video:
                summary: 视频任务
                value:
                  content_type: video
                  title: 今日片段
                  body: 一段简短的日常记录。
                  tags: [日常记录]
                  media:
                    video:
                      url: https://cdn.example.com/content/video.mp4
                      cover_url: https://cdn.example.com/content/cover.jpg
      responses:
        '201':
          description: 任务已创建
          headers:
            Location:
              description: 任务查询地址
              schema:
                type: string
            Idempotency-Replayed:
              $ref: '#/components/headers/IdempotencyReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/JobId'
    get:
      operationId: getPublishJob
      summary: 查询任务
      responses:
        '200':
          description: 任务详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
    patch:
      operationId: updatePublishJob
      summary: 更新任务批次归属或人工完成状态
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateJobRequest'
      responses:
        '200':
          description: 任务已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      operationId: deletePublishJob
      summary: 删除任务
      responses:
        '204':
          description: 任务已删除；重复删除也返回 204
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/v1/claims:
    post:
      operationId: claimBatchJob
      summary: 点击领取时原子分配一篇
      description: |
        本站手机领取页使用的浏览器会话接口，必须从本站同源页面调用。如果当前匿名
        会话已有活动认领，则幂等恢复该认领；否则原子认领一条可用任务。外部系统
        不应在服务端模拟此流程。
      security: []
      parameters:
        - $ref: '#/components/parameters/SameOrigin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateClaimRequest'
      responses:
        '201':
          description: 已认领一条内容，或恢复当前会话已有的活动认领；新会话同时设置匿名 Cookie
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '403':
          $ref: '#/components/responses/ForbiddenOrigin'
        '410':
          $ref: '#/components/responses/Gone'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /api/v1/claims/{id}:
    parameters:
      - $ref: '#/components/parameters/ClaimId'
    get:
      operationId: getCurrentClaim
      summary: 恢复当前匿名会话的认领
      security:
        - ClaimSessionAuth: []
      responses:
        '200':
          description: 当前认领及内容预览
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
    patch:
      operationId: manageClaim
      summary: 项目接口释放异常认领
      description: 使用所属项目的 API Key 释放仍处于 claimed 状态的异常占用。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ManageClaimRequest'
      responses:
        '200':
          description: 认领已更新
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
    delete:
      operationId: releaseOwnClaim
      summary: 当前匿名会话主动释放认领
      security:
        - ClaimSessionAuth: []
      parameters:
        - $ref: '#/components/parameters/SameOrigin'
      responses:
        '204':
          description: 认领已释放
        '404':
          $ref: '#/components/responses/NotFound'
        '403':
          $ref: '#/components/responses/ForbiddenOrigin'
  /api/v1/claims/{id}/switch:
    parameters:
      - $ref: '#/components/parameters/ClaimId'
    post:
      operationId: switchBatchClaim
      summary: 原子释放当前认领并换一篇
      security:
        - ClaimSessionAuth: []
      parameters:
        - $ref: '#/components/parameters/SameOrigin'
      responses:
        '200':
          description: 已认领新的内容
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '403':
          $ref: '#/components/responses/ForbiddenOrigin'
  /api/v1/claims/{id}/complete:
    parameters:
      - $ref: '#/components/parameters/ClaimId'
    post:
      operationId: completeOwnClaim
      summary: 当前匿名会话人工确认流程完成
      security:
        - ClaimSessionAuth: []
      parameters:
        - $ref: '#/components/parameters/SameOrigin'
      responses:
        '200':
          description: 认领已完成
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '403':
          $ref: '#/components/responses/ForbiddenOrigin'
  /api/v1/claims/{id}/launch:
    parameters:
      - $ref: '#/components/parameters/ClaimId'
    post:
      operationId: recordClaimLaunchAttempt
      summary: 记录当前会话已发起移动端接力
      description: 仅记录用户点击和续租，不将客户端唤起推断为发布完成。
      security:
        - ClaimSessionAuth: []
      parameters:
        - $ref: '#/components/parameters/SameOrigin'
      responses:
        '200':
          description: 已记录接力尝试
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClaimResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '403':
          $ref: '#/components/responses/ForbiddenOrigin'
  /p/{id}:
    get:
      operationId: openPublishPage
      summary: 打开手机发布页
      security: []
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: 手机发布页
          content:
            text/html:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
  /b/{id}:
    get:
      operationId: openBatchPage
      summary: 打开批次手机认领页
      security: []
      parameters:
        - $ref: '#/components/parameters/BatchId'
      responses:
        '200':
          description: 推荐一条可认领任务并进入认领、换稿与预览流程的手机页面
          content:
            text/html:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
  /q/{id}:
    get:
      operationId: getPublishQrCode
      summary: 获取任务或批次二维码
      security: []
      parameters:
        - $ref: '#/components/parameters/ResourceId'
      responses:
        '200':
          description: 根据资源标识指向 /p/{id} 或 /b/{id} 的 SVG 二维码
          headers:
            Cache-Control:
              schema:
                type: string
                example: private, max-age=300
          content:
            image/svg+xml:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  headers:
    IdempotencyReplayed:
      description: true 表示本响应复用了同一幂等请求此前创建的资源。
      schema:
        type: boolean
    RequestId:
      description: 与错误响应体 request_id 相同的请求编号，用于安全地定位服务端日志。
      schema:
        type: string
        format: uuid
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: 客户工作台创建的项目级密钥；完整值只显示一次。
    ClaimSessionAuth:
      type: apiKey
      in: cookie
      name: claim_session
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 8–128 个可见 ASCII 字符；相同键与相同请求返回同一任务。
      schema:
        type: string
        minLength: 8
        maxLength: 128
    JobId:
      name: id
      in: path
      required: true
      description: 不可猜测的任务标识，至少包含 128 bit 随机性。
      schema:
        type: string
        pattern: '^job_[A-Za-z0-9_-]{22,64}$'
    BatchId:
      name: id
      in: path
      required: true
      description: 不可猜测的批次标识，至少包含 128 bit 随机性。
      schema:
        type: string
        pattern: '^batch_[A-Za-z0-9_-]{22,64}$'
    ResourceId:
      name: id
      in: path
      required: true
      description: 不可猜测的任务或批次标识。
      schema:
        type: string
        pattern: '^(?:job|batch)_[A-Za-z0-9_-]{22,64}$'
    ClaimId:
      name: id
      in: path
      required: true
      description: 不可猜测的认领标识。
      schema:
        type: string
        pattern: '^claim_[A-Za-z0-9_-]{22,64}$'
    SameOrigin:
      name: Origin
      in: header
      required: true
      description: 浏览器自动发送，且必须与部署后的站点来源完全一致。
      schema:
        type: string
        format: uri
  schemas:
    CreateBatchRequest:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        external_ref:
          type: string
          maxLength: 128
          description: 上游系统的批次标识。
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          type: string
          maxLength: 500
        expires_in:
          type: integer
          minimum: 900
          maximum: 604800
          default: 86400
        items:
          type: array
          maxItems: 50
          default: []
          description: |
            可选的批量内容列表。省略时只创建空批次；提供时，批次、内容与额度扣减
            在同一事务中完成，任一内容失败则全部回滚。内嵌内容不得提供 batch_id，
            内容有效期不会超过批次有效期。
          items:
            $ref: '#/components/schemas/CreateBatchItemRequest'
    UpdateBatchRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          type: string
          maxLength: 500
        status:
          type: string
          enum: [active, archived]
    BatchStatus:
      type: string
      enum: [active, archived, expired]
    Batch:
      type: object
      required:
        - id
        - name
        - status
        - item_count
        - available_count
        - claimed_count
        - completed_count
        - progress
        - publish_url
        - qr_url
        - created_at
        - expires_at
      properties:
        id:
          type: string
          example: batch_5W98BdYgaXUSvM7tweP0ig
        external_ref:
          type: string
        name:
          type: string
        description:
          type: string
        status:
          $ref: '#/components/schemas/BatchStatus'
        item_count:
          type: integer
          minimum: 0
        available_count:
          type: integer
          minimum: 0
        claimed_count:
          type: integer
          minimum: 0
        completed_count:
          type: integer
          minimum: 0
        progress:
          type: number
          minimum: 0
          maximum: 1
          description: completed_count / item_count；空批次为 0。
        publish_url:
          type: string
          format: uri
          example: https://example.com/b/batch_5W98BdYgaXUSvM7tweP0ig
        qr_url:
          type: string
          format: uri
          example: https://example.com/q/batch_5W98BdYgaXUSvM7tweP0ig
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        last_completed_at:
          type: [string, 'null']
          format: date-time
    BatchResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Batch'
    BatchCreateResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          allOf:
            - $ref: '#/components/schemas/Batch'
            - type: object
              required: [jobs]
              properties:
                jobs:
                  type: array
                  description: 本次事务中创建的内容；创建空批次时为空数组。
                  items:
                    $ref: '#/components/schemas/Job'
    BatchListResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: object
          additionalProperties: false
          required: [items, next_cursor]
          properties:
            items:
              type: array
              items:
                $ref: '#/components/schemas/Batch'
            next_cursor:
              type: [string, 'null']
    CreateJobRequest:
      type: object
      additionalProperties: false
      required: [content_type, media]
      properties:
        batch_id:
          oneOf:
            - type: 'null'
            - type: string
              pattern: '^batch_[A-Za-z0-9_-]{22,64}$'
          description: 创建时直接归入发布批次；省略或 null 表示独立任务。
        external_ref:
          type: string
          maxLength: 128
          description: 上游系统的业务标识，便于排查和对账。
        content_type:
          type: string
          enum: [gallery, video]
        title:
          type: string
          maxLength: 200
        body:
          type: string
          maxLength: 5000
        tags:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 50
            pattern: '^(?=.*[^#\s]).+$'
            description: '去除首部 # 与两端空白后必须非空；规范化后不得重复或包含控制字符。'
        media:
          oneOf:
            - $ref: '#/components/schemas/GalleryMedia'
            - $ref: '#/components/schemas/VideoMedia'
        sort_order:
          type: integer
          minimum: 0
          maximum: 1000000
          default: 0
          description: 批次内展示顺序。
        expires_in:
          type: integer
          minimum: 900
          maximum: 604800
          default: 86400
          description: 任务有效秒数，15 分钟至 7 天。
      oneOf:
        - title: 图文任务
          required: [content_type, media]
          properties:
            content_type:
              const: gallery
            media:
              $ref: '#/components/schemas/GalleryMedia'
        - title: 视频任务
          required: [content_type, media]
          properties:
            content_type:
              const: video
            media:
              $ref: '#/components/schemas/VideoMedia'
    CreateBatchItemRequest:
      type: object
      additionalProperties: false
      required: [content_type, media]
      description: 批次创建时内嵌的内容，不接受 batch_id。
      properties:
        external_ref:
          type: string
          maxLength: 128
          description: 上游系统的业务标识，便于排查和对账。
        content_type:
          type: string
          enum: [gallery, video]
        title:
          type: string
          maxLength: 200
        body:
          type: string
          maxLength: 5000
        tags:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 50
            pattern: '^(?=.*[^#\s]).+$'
        media:
          oneOf:
            - $ref: '#/components/schemas/GalleryMedia'
            - $ref: '#/components/schemas/VideoMedia'
        sort_order:
          type: integer
          minimum: 0
          maximum: 1000000
          default: 0
          description: 为 0 或省略时，服务端按 items 数组顺序补齐稳定排序。
        expires_in:
          type: integer
          minimum: 900
          maximum: 604800
          default: 86400
          description: 内容有效期会被限制为不超过批次有效期。
      oneOf:
        - title: 图文内容
          properties:
            content_type:
              const: gallery
            media:
              $ref: '#/components/schemas/GalleryMedia'
        - title: 视频内容
          properties:
            content_type:
              const: video
            media:
              $ref: '#/components/schemas/VideoMedia'
    GalleryMedia:
      type: object
      additionalProperties: false
      required: [images]
      properties:
        images:
          type: array
          minItems: 1
          maxItems: 18
          items:
            $ref: '#/components/schemas/PublicMediaUrl'
    VideoMedia:
      type: object
      additionalProperties: false
      required: [video]
      properties:
        video:
          type: object
          additionalProperties: false
          required: [url]
          properties:
            url:
              $ref: '#/components/schemas/PublicMediaUrl'
            cover_url:
              $ref: '#/components/schemas/PublicMediaUrl'
    PublicMediaUrl:
      type: string
      format: uri
      maxLength: 2048
      pattern: '^https://'
      description: |
        必须是无用户信息的稳定 HTTPS 公网直链；禁止 localhost、内网 IP、
        链路本地地址、URL fragment、临时重定向页和需要 Cookie 的地址。
        完整规则见 PUBLIC_MEDIA_URL_POLICY.md。
    JobStatus:
      type: string
      enum: [ready, opened, launched, failed, expired]
      description: launched 仅表示已尝试打开移动端编辑器。
    CompletionStatus:
      type: string
      enum: [pending, completed]
      description: completed 仅表示用户或可信上游人工确认流程完成。
    ClaimStatus:
      type: string
      enum: [claimed, completed, released, expired]
    CreateClaimRequest:
      type: object
      additionalProperties: false
      required: [batch_id]
      properties:
        batch_id:
          type: string
          pattern: '^batch_[A-Za-z0-9_-]{22,64}$'
        claimant_label:
          type: string
          maxLength: 80
          description: 可选员工称呼或工号；普通页面应按隐私规则展示。
    ManageClaimRequest:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          const: released
        reason:
          type: string
          maxLength: 200
    ClaimPreview:
      type: object
      required: [id, content_type, title, body, tags, media]
      properties:
        id:
          type: string
          pattern: '^job_[A-Za-z0-9_-]{22,64}$'
        content_type:
          type: string
          enum: [gallery, video]
        title:
          type: string
        body:
          type: string
        tags:
          type: array
          items:
            type: string
        media:
          oneOf:
            - $ref: '#/components/schemas/GalleryMedia'
            - $ref: '#/components/schemas/VideoMedia'
    Claim:
      type: object
      required:
        - id
        - batch_id
        - status
        - job
        - claimed_at
        - lease_expires_at
      properties:
        id:
          type: string
          example: claim_Lj2mZ7EckHq0T0j4VxGFNQ
        batch_id:
          type: string
        status:
          $ref: '#/components/schemas/ClaimStatus'
        claimant_label:
          type: [string, 'null']
        job:
          $ref: '#/components/schemas/ClaimPreview'
        claimed_at:
          type: string
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time
        lease_expires_at:
          type: string
          format: date-time
    ClaimResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Claim'
    UpdateJobRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        batch_id:
          oneOf:
            - type: 'null'
            - type: string
              pattern: '^batch_[A-Za-z0-9_-]{22,64}$'
          description: 设置批次归属；null 表示从当前批次移除。
        completion_status:
          $ref: '#/components/schemas/CompletionStatus'
    Job:
      type: object
      required:
        - id
        - status
        - content_type
        - completion_status
        - title
        - body
        - tags
        - media
        - publish_url
        - qr_url
        - created_at
        - expires_at
      properties:
        id:
          type: string
          example: job_R7oM4VNNX8w7u2gGqA6qYw
        external_ref:
          type: string
        status:
          $ref: '#/components/schemas/JobStatus'
        content_type:
          type: string
          enum: [gallery, video]
        batch_id:
          type: [string, 'null']
        completion_status:
          $ref: '#/components/schemas/CompletionStatus'
        title:
          type: string
        body:
          type: string
        tags:
          type: array
          items:
            type: string
        media:
          oneOf:
            - $ref: '#/components/schemas/GalleryMedia'
            - $ref: '#/components/schemas/VideoMedia'
        completed_at:
          type: [string, 'null']
          format: date-time
        publish_url:
          type: string
          format: uri
          example: https://example.com/p/job_R7oM4VNNX8w7u2gGqA6qYw
        qr_url:
          type: string
          format: uri
          example: https://example.com/q/job_R7oM4VNNX8w7u2gGqA6qYw
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        last_opened_at:
          type: [string, 'null']
          format: date-time
        last_launch_attempt_at:
          type: [string, 'null']
          format: date-time
        failure:
          oneOf:
            - type: 'null'
            - type: object
              properties:
                code:
                  type: string
                message:
                  type: string
    JobResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/Job'
    Problem:
      type: object
      additionalProperties: true
      required: [type, title, status, code, request_id]
      properties:
        type:
          type: string
          format: uri-reference
        title:
          type: string
        status:
          type: integer
        code:
          type: string
          example: invalid_media_url
        detail:
          type: string
        request_id:
          type: string
        errors:
          type: array
          items:
            type: object
            required: [field, code]
            properties:
              field:
                type: string
              code:
                type: string
              message:
                type: string
  responses:
    BadRequest:
      description: 请求格式错误
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: API 凭证缺失或无效
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    ForbiddenOrigin:
      description: 浏览器请求来源不是当前站点
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PaymentRequired:
      description: 客户空间额度不足，资源和扣费均未写入
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: about:blank
            title: 当前额度不足，请充值后重试
            status: 402
            code: insufficient_balance
            request_id: 28da4b73-ea57-4902-a3e3-cf44db71e42f
    Conflict:
      description: 幂等键已用于不同请求，或当前资源状态无法执行该操作
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PayloadTooLarge:
      description: JSON 请求体超过 1 MiB，资源和扣费均未写入
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    UnprocessableEntity:
      description: 请求字段或媒体地址不符合规则
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    TooManyRequests:
      description: 请求过于频繁
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          description: 建议等待秒数
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: 任务不存在或已被删除
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Gone:
      description: 任务已过期
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
