openapi: 3.0.3
info:
  title: StockIndex Server API
  description: |
    个人全球证券名称索引服务 — A股 / 港股 / 美股 / ETF·LOF。

    - 公开接口无需鉴权，返回 JSON
    - 统一标的 ID：`{MARKET}:{VENUE}.{CODE}`（如 `CN:SH.600519`）
    - 分页：`page` 从 1 起，`pageSize` 默认 50、最大 100（别名 `limit`）
    - 完整说明见首页「API 文档」或 `/openapi/`
  version: 1.0.0
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT

servers:
  - url: /api/v1
    description: API v1（相对当前站点根路径）

tags:
  - name: stock
    description: 个股查询与列表
  - name: etf
    description: A股 ETF / LOF
  - name: board
    description: 板块目录与成分股
  - name: industry
    description: 申万行业目录与成分股（A股）
  - name: search
    description: 关键词搜索
  - name: meta
    description: 服务状态与统计
  - name: admin
    description: 管理接口（需鉴权）

paths:
  /health:
    get:
      tags: [meta]
      summary: 服务健康检查与接口索引
      operationId: getHealth
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'

  /stats:
    get:
      tags: [meta]
      summary: 数据统计
      operationId: getStats
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsResponse'

  /stock:
    get:
      tags: [stock]
      summary: 按代码查询单条标的详情
      operationId: getStock
      parameters:
        - $ref: '#/components/parameters/codeOptional'
        - $ref: '#/components/parameters/instrumentId'
        - $ref: '#/components/parameters/symbol'
        - $ref: '#/components/parameters/market'
      responses:
        '200':
          description: 找到标的
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StockDetailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: 代码跨市场歧义
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AmbiguousCodeResponse'

  /stocks:
    get:
      tags: [stock]
      summary: 分页个股列表（多条件筛选）
      operationId: listStocks
      parameters:
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/pageSize'
        - $ref: '#/components/parameters/market'
        - $ref: '#/components/parameters/board'
        - $ref: '#/components/parameters/industry'
        - $ref: '#/components/parameters/assetType'
        - $ref: '#/components/parameters/q'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StockListResponse'

  /etfs:
    get:
      tags: [etf]
      summary: A股 ETF/LOF 分页列表
      operationId: listEtfs
      parameters:
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/pageSize'
        - $ref: '#/components/parameters/q'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StockListResponse'

  /search:
    get:
      tags: [search]
      summary: 关键词搜索
      operationId: searchStocks
      parameters:
        - name: q
          in: query
          description: 搜索词（别名 query）
          schema:
            type: string
        - name: query
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - $ref: '#/components/parameters/market'
        - $ref: '#/components/parameters/board'
        - $ref: '#/components/parameters/industry'
        - $ref: '#/components/parameters/assetType'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'

  /boards:
    get:
      tags: [board]
      summary: 板块目录
      operationId: listBoards
      parameters:
        - $ref: '#/components/parameters/market'
        - name: withCount
          in: query
          description: 为 1 或 true 时返回 stockCount
          schema:
            type: string
            enum: ['0', '1', 'true', 'false']
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/BoardListResponse'
                  - $ref: '#/components/schemas/BoardDetailListResponse'

  /boards/detail:
    get:
      tags: [board]
      summary: 单个板块详情
      operationId: getBoardDetail
      parameters:
        - $ref: '#/components/parameters/boardRequired'
        - $ref: '#/components/parameters/market'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardDetailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /boards/stocks:
    get:
      tags: [board]
      summary: 板块成分股（分页）
      operationId: listBoardStocks
      parameters:
        - $ref: '#/components/parameters/boardRequired'
        - $ref: '#/components/parameters/market'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/pageSize'
        - $ref: '#/components/parameters/q'
        - $ref: '#/components/parameters/assetType'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardStockListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /industries:
    get:
      tags: [industry]
      summary: 行业目录（申万，A股）
      operationId: listIndustries
      parameters:
        - $ref: '#/components/parameters/market'
        - name: level
          in: query
          description: 1=一级行业，2=二级行业
          schema:
            type: integer
            enum: [1, 2]
        - name: q
          in: query
          description: 行业名称关键词
          schema:
            type: string
        - name: parent
          in: query
          description: 一级行业 code，筛二级（别名 parentCode）
          schema:
            type: string
        - name: withCount
          in: query
          schema:
            type: string
            enum: ['0', '1', 'true', 'false']
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/IndustryListResponse'
                  - $ref: '#/components/schemas/IndustryDetailListResponse'

  /industries/detail:
    get:
      tags: [industry]
      summary: 单个行业详情
      operationId: getIndustryDetail
      parameters:
        - $ref: '#/components/parameters/industryCodeRequired'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndustryDetailResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /industries/stocks:
    get:
      tags: [industry]
      summary: 行业成分股（分页）
      operationId: listIndustryStocks
      parameters:
        - $ref: '#/components/parameters/industryCodeRequired'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/pageSize'
        - $ref: '#/components/parameters/q'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IndustryStockListResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /admin/sync:
    get:
      tags: [admin]
      summary: 触发同步（GET）
      operationId: syncGet
      security:
        - BearerAuth: []
        - TokenQuery: []
      parameters:
        - $ref: '#/components/parameters/syncParams'
      responses:
        '200':
          description: 任务已启动或继续
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      tags: [admin]
      summary: 触发同步（POST）
      operationId: syncPost
      security:
        - BearerAuth: []
        - TokenQuery: []
      parameters:
        - $ref: '#/components/parameters/syncParams'
      responses:
        '200':
          description: 任务已启动或继续
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SyncResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: SYNC_ADMIN_TOKEN
    TokenQuery:
      type: apiKey
      in: query
      name: token

  parameters:
    page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    pageSize:
      name: pageSize
      in: query
      description: 别名 limit，最大 100
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    market:
      name: market
      in: query
      schema:
        type: string
        enum: [CN, HK, US]
    board:
      name: board
      in: query
      description: 板块 key（hsj/cyb/kcb/etf/MB/GEM/tec/cdr 等）
      schema:
        type: string
    boardRequired:
      name: board
      in: query
      required: true
      description: 板块 key（别名 key）
      schema:
        type: string
    industry:
      name: industry
      in: query
      description: 行业代码
      schema:
        type: string
    industryCodeRequired:
      name: code
      in: query
      required: true
      description: 行业代码（别名 industry）
      schema:
        type: string
    assetType:
      name: assetType
      in: query
      description: 别名 type
      schema:
        type: string
        enum: [equity, etf]
    q:
      name: q
      in: query
      description: 名称或代码关键词
      schema:
        type: string
    codeOptional:
      name: code
      in: query
      schema:
        type: string
    instrumentId:
      name: instrumentId
      in: query
      description: 统一标的 ID（别名 id）
      schema:
        type: string
        example: CN:SH.600519
    symbol:
      name: symbol
      in: query
      description: 行情 symbol，如 sh600519
      schema:
        type: string
    syncParams:
      name: mode
      in: query
      schema:
        type: string
        enum: [full, incremental, market]
        default: incremental

  responses:
    BadRequest:
      description: 缺少必填参数
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: 资源不存在
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unauthorized:
      description: 鉴权失败
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServerError:
      description: 服务端错误
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    Market:
      type: string
      enum: [CN, HK, US]
    AssetType:
      type: string
      enum: [equity, etf]

    SearchHit:
      type: object
      properties:
        instrumentId:
          type: string
          example: CN:SH.600519
        market:
          $ref: '#/components/schemas/Market'
        code:
          type: string
        symbol:
          type: string
        nameCn:
          type: string
        assetType:
          $ref: '#/components/schemas/AssetType'
        exchange:
          type: string
          nullable: true
        board:
          type: string
          nullable: true
        boards:
          type: array
          items:
            type: string
        industryCode:
          type: string
          nullable: true
        industryName:
          type: string
          nullable: true
        matchField:
          type: string
        score:
          type: number

    StockDetail:
      allOf:
        - $ref: '#/components/schemas/SearchHit'
        - type: object
          properties:
            status:
              type: string
              example: active
            source:
              type: string
              nullable: true
            updatedAt:
              type: string
              format: date-time

    Board:
      type: object
      properties:
        market:
          $ref: '#/components/schemas/Market'
        boardKey:
          type: string
        boardCode:
          type: string
        name:
          type: string
        priority:
          type: integer

    BoardDetail:
      allOf:
        - $ref: '#/components/schemas/Board'
        - type: object
          properties:
            stockCount:
              type: integer

    Industry:
      type: object
      properties:
        market:
          $ref: '#/components/schemas/Market'
        industryCode:
          type: string
        name:
          type: string
        level:
          type: integer
        parentCode:
          type: string
          nullable: true

    IndustryDetail:
      allOf:
        - $ref: '#/components/schemas/Industry'
        - type: object
          properties:
            stockCount:
              type: integer
            parentName:
              type: string
              nullable: true

    StockListResponse:
      type: object
      properties:
        page:
          type: integer
        pageSize:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/SearchHit'

    StockDetailResponse:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/StockDetail'

    SearchResponse:
      type: object
      properties:
        query:
          type: string
        total:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/SearchHit'

    BoardListResponse:
      type: object
      properties:
        total:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/Board'

    BoardDetailListResponse:
      type: object
      properties:
        total:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/BoardDetail'

    BoardDetailResponse:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/BoardDetail'

    BoardStockListResponse:
      allOf:
        - type: object
          properties:
            board:
              $ref: '#/components/schemas/BoardDetail'
        - $ref: '#/components/schemas/StockListResponse'

    IndustryListResponse:
      type: object
      properties:
        total:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/Industry'

    IndustryDetailListResponse:
      type: object
      properties:
        total:
          type: integer
        items:
          type: array
          items:
            $ref: '#/components/schemas/IndustryDetail'

    IndustryDetailResponse:
      type: object
      properties:
        item:
          $ref: '#/components/schemas/IndustryDetail'

    IndustryStockListResponse:
      allOf:
        - type: object
          properties:
            industry:
              $ref: '#/components/schemas/IndustryDetail'
        - $ref: '#/components/schemas/StockListResponse'

    StatsResponse:
      type: object
      properties:
        ok:
          type: boolean
        stocks:
          type: object
          additionalProperties:
            type: integer
        etfs:
          type: integer
        industries:
          type: integer
        boards:
          type: object
          additionalProperties:
            type: integer
        lastSync:
          type: string
          format: date-time
          nullable: true

    HealthResponse:
      type: object
      properties:
        ok:
          type: boolean
        service:
          type: string
        apiVersion:
          type: string
        basePath:
          type: string
        markets:
          type: array
          items:
            type: string
        endpoints:
          type: object
          additionalProperties:
            type: string

    ErrorResponse:
      type: object
      properties:
        error:
          type: string

    AmbiguousCodeResponse:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        candidates:
          type: array
          items:
            type: object
            properties:
              instrumentId:
                type: string
              market:
                type: string
              code:
                type: string
              nameCn:
                type: string

    SyncResponse:
      type: object
      properties:
        ok:
          type: boolean
        jobId:
          type: integer
        hasMore:
          type: boolean
        error:
          type: string
