openapi: 3.0.3
info:
  title: Kids AI Posture Platform API
  version: 1.0.0
  description: |
    儿童AI体态管理与运动康复平台 V1.0 API 契约。
    所有报告用于健康管理建议，不构成医疗诊断。接口返回统一包含 requestId 以便追踪。
servers:
  - url: https://api.example.com
    description: production
  - url: https://staging-api.example.com
    description: staging
security:
  - bearerAuth: []

x-error-model: &error-model
  code: integer
  message: string
  details:
    type: object
    additionalProperties: true
  requestId: string

tags:
  - name: Auth
  - name: Children
  - name: Videos
  - name: Analysis
  - name: Reports
  - name: Training
  - name: Admin

paths:
  /api/auth/login:
    post:
      tags: [Auth]
      summary: 手机号或微信授权登录
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
            examples:
              phone:
                summary: 手机验证码
                value:
                  loginType: phone_code
                  credential: "186xxxx0000"
                  code: "682139"
              wechat:
                summary: 微信授权码
                value:
                  loginType: wechat
                  credential: "wx_auth_code"
      responses:
        '200':
          description: 登录成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          description: 登录失败
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/children:
    get:
      tags: [Children]
      summary: 获取当前用户可访问的儿童档案
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: 儿童档案分页列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChildrenPagedResponse'
    post:
      tags: [Children]
      summary: 创建儿童档案
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChildCreateRequest'
      responses:
        '201':
          description: 创建成功
          headers:
            Idempotency-Key:
              description: 冪等键回显，用于重试去重核对
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChildResponse'
        '400':
          description: 请求参数错误
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/children/{childId}:
    parameters:
      - $ref: '#/components/parameters/ChildId'
    get:
      tags: [Children]
      summary: 获取儿童档案详情
      responses:
        '200':
          description: 档案详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChildResponse'
        '404':
          description: 资源不存在
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags: [Children]
      summary: 更新儿童档案
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChildUpdateRequest'
      responses:
        '200':
          description: 更新成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChildResponse'
        '409':
          description: 更新冲突（数据并发）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags: [Children]
      summary: 注销/归档儿童档案
      responses:
        '204':
          description: 归档成功

  /api/videos/upload-token:
    post:
      tags: [Videos]
      summary: 获取视频直传凭证
      description: 建议请求头携带 Idempotency-Key，避免重试产生重复录像对象。
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadTokenRequest'
      responses:
        '200':
          description: 上传凭证
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadTokenResponse'
        '429':
          description: 请求过于频繁
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/videos:
    post:
      tags: [Videos]
      summary: 登记已上传视频
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VideoCreateRequest'
      responses:
        '201':
          description: 视频登记成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoResponse'
        '409':
          description: 去重/重复提交
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/analysis/tasks:
    post:
      tags: [Analysis]
      summary: 创建AI分析任务
      description: 同一 childId+videoId+taskType 可复用幂等键创建。
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalysisTaskCreateRequest'
      responses:
        '201':
          description: 任务创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalysisTaskResponse'
        '409':
          description: 重复任务，返回已有任务
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalysisTaskResponse'

  /api/analysis/tasks/{taskId}:
    get:
      tags: [Analysis]
      summary: 查询分析任务状态
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: 任务状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalysisTaskResponse'
        '404':
          description: 任务不存在
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/analysis/tasks/{taskId}/cancel:
    post:
      tags: [Analysis]
      summary: 取消分析任务
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: 取消成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StandardResponse'

  /api/analysis/webhook:
    post:
      tags: [Analysis]
      summary: 分析结果回调（异步）
      description: 系统内部回调给后端事件网关，需使用 HMAC 签名。
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnalysisTaskWebhook'
      responses:
        '204':
          description: 接收成功

  /api/reports/{reportId}:
    get:
      tags: [Reports]
      summary: 获取体态分析报告
      parameters:
        - $ref: '#/components/parameters/ReportId'
      responses:
        '200':
          description: 报告详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResponse'

  /api/training/plans:
    post:
      tags: [Training]
      summary: 创建训练计划
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrainingPlanCreateRequest'
      responses:
        '201':
          description: 训练计划创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrainingPlanResponse'

  /api/training/plans/{planId}:
    parameters:
      - name: planId
        in: path
        required: true
        schema:
          type: integer
    get:
      tags: [Training]
      summary: 获取训练计划
      responses:
        '200':
          description: 训练计划详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrainingPlanResponse'

  /api/training/plans/{planId}/records:
    post:
      tags: [Training]
      summary: 提交训练打卡记录
      parameters:
        - name: planId
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrainingRecordCreateRequest'
      responses:
        '201':
          description: 打卡成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrainingRecordResponse'

  /api/admin/dashboard:
    get:
      tags: [Admin]
      summary: 获取机构运营看板
      responses:
        '200':
          description: 看板指标
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AdminDashboardResponse'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    ChildId:
      name: childId
      in: path
      required: true
      schema: { type: integer }
    TaskId:
      name: taskId
      in: path
      required: true
      schema: { type: integer }
    ReportId:
      name: reportId
      in: path
      required: true
      schema: { type: integer }
  schemas:
    ApiMeta:
      type: object
      properties:
        requestId: { type: string }
        timestamp: { type: string, format: date-time }
        traceId: { type: string }
    StandardResponse:
      type: object
      properties:
        code: { type: integer }
        message: { type: string }
        data: { type: object, nullable: true }
        meta: { $ref: '#/components/schemas/ApiMeta' }
    ErrorResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            code: { type: integer, example: 10003 }
            message: { type: string, example: parameter_validation_failed }
            data:
              type: object
              properties:
                errorCode: { type: string }
                path: { type: string }
    PagedMeta:
      type: object
      properties:
        page: { type: integer }
        pageSize: { type: integer }
        total: { type: integer }
        hasMore: { type: boolean }
    LoginRequest:
      type: object
      required: [loginType, credential]
      properties:
        loginType: { type: string, enum: [phone_code, wechat] }
        credential: { type: string }
        code: { type: string }
    LoginResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                token: { type: string }
                user: { $ref: '#/components/schemas/User' }
    User:
      type: object
      properties:
        id: { type: integer }
        phoneMasked: { type: string }
        role: { type: string, enum: [parent, coach, org_admin, platform_admin] }
    ChildCreateRequest:
      type: object
      required: [name, birthday]
      properties:
        name: { type: string }
        birthday: { type: string, format: date }
        gender: { type: string, enum: [male, female, unknown] }
        height: { type: number }
        weight: { type: number }
        contraindications: { type: string }
    ChildUpdateRequest:
      allOf:
        - $ref: '#/components/schemas/ChildCreateRequest'
    Child:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        birthday: { type: string, format: date }
        age: { type: integer }
        height: { type: number }
        weight: { type: number }
        status: { type: string }
    ChildResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data: { $ref: '#/components/schemas/Child' }
    ChildrenPagedResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                list:
                  type: array
                  items: { $ref: '#/components/schemas/Child' }
                page: { $ref: '#/components/schemas/PagedMeta' }
    UploadTokenRequest:
      type: object
      required: [childId, fileName, contentType, size]
      properties:
        childId: { type: integer }
        fileName: { type: string }
        contentType: { type: string, example: video/mp4 }
        size: { type: integer }
    UploadTokenResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                uploadUrl: { type: string }
                objectKey: { type: string }
                expireAt: { type: string, format: date-time }
    VideoCreateRequest:
      type: object
      required: [childId, objectKey, scene]
      properties:
        childId: { type: integer }
        objectKey: { type: string }
        scene:
          type: string
          enum: [front_posture, side_posture, squat, balance, gait]
        captureHint: { type: string }
    VideoResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                status: { type: string, enum: [uploaded, rejected, archived] }
    AnalysisTaskCreateRequest:
      type: object
      required: [childId, videoId, taskType]
      properties:
        childId: { type: integer }
        videoId: { type: integer }
        taskType: { type: string, enum: [posture_screening, movement_scoring, reassessment] }
    AnalysisTaskResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                status:
                  type: string
                  enum: [CREATED, QUEUED, PROCESSING, SUCCEEDED, FAILED, CANCELLED]
                progress: { type: integer, minimum: 0, maximum: 100 }
                errorCode: { type: string }
                reportId: { type: integer }
                retryCount: { type: integer }
    AnalysisTaskWebhook:
      type: object
      required: [taskId, status]
      properties:
        taskId: { type: integer }
        status:
          type: string
          enum: [SUCCEEDED, FAILED, CANCELLED]
        progress: { type: integer }
        errorCode: { type: string }
        result: { type: object }
        signature: { type: string }
    ReportResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                childId: { type: integer }
                taskId: { type: integer }
                riskLevel: { type: string, enum: [low, medium, high, review_required] }
                summary: { type: string }
                metrics:
                  type: array
                  items:
                    type: object
                    properties:
                      name: { type: string }
                      value: { type: number }
                      level: { type: string }
                      confidence: { type: number }
                recommendations:
                  type: array
                  items: { type: string }
                disclaimer: { type: string }
                reviewedBy: { type: integer, nullable: true }
    TrainingPlanCreateRequest:
      type: object
      required: [childId, goal, cycleDays]
      properties:
        childId: { type: integer }
        reportId: { type: integer }
        goal: { type: string }
        cycleDays: { type: integer, minimum: 7 }
        exerciseIds:
          type: array
          items: { type: integer }
        constraints:
          type: object
          properties:
            maxDailyMinutes: { type: integer }
            coachNotes: { type: string }
    TrainingPlanResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                status: { type: string, enum: [draft, active, completed, paused] }
                detail: { type: object }
                startedAt: { type: string, format: date-time }
                endedAt: { type: string, format: date-time, nullable: true }
    TrainingRecordCreateRequest:
      type: object
      required: [exerciseId, completed]
      properties:
        exerciseId: { type: integer }
        completed: { type: boolean }
        score: { type: integer, minimum: 0, maximum: 100 }
        durationSeconds: { type: integer }
        note: { type: string }
        media:
          type: array
          items:
            type: string
    TrainingRecordResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                id: { type: integer }
                planId: { type: integer }
                createdAt: { type: string, format: date-time }
    AdminDashboardResponse:
      allOf:
        - $ref: '#/components/schemas/StandardResponse'
        - type: object
          properties:
            data:
              type: object
              properties:
                newChildren: { type: integer }
                uploadedVideos: { type: integer }
                completedReports: { type: integer }
                activePlans: { type: integer }
                conversionRate: { type: number }
                reassessmentCompletionRate: { type: number }
