唯杰地图 logo唯杰地图
VJMAP
EN

vjmap.openapi.yaml

vjmap-service/openapi/vjmap.openapi.yaml 46.5 KB1273 行
原文
openapi: 3.1.0
info:
  title: vjmap HTTP API
  version: "1.0.0"
  summary: vjmap web服务 + 图形服务 HTTP 接口
  description: |
    本规范覆盖 `vjmap` 二次开发最常用的 ~40 个接口,按照 13 大分类组织。

    - **Base URL**:`http://127.0.0.1:27660`
    - **API 前缀**:`/api/v1`
    - **鉴权**:JWT Token,通过请求头 `token:`(优先)或查询参数 `?token=` 传递;REST 不认 `Authorization: Bearer`(仅 MCP 路由认)
    - **响应约定**:
      - web服务类接口:统一 `{"code":0,"msg":"ok","data":...}`
      - 图形服务直通接口(`/api/v1/map/cmd/...`): JSON
      - 字节流接口(瓦片 / PBF / PNG):直接返回二进制

    > 本规范随新后端文档项目一起分发,Swagger 页面可在线 "Try it out"。
  contact:
    name: vjmap dev
  license:
    name: Proprietary

servers:
  - url: http://127.0.0.1:27660
    description: 本地调试服务
  - url: http://127.0.0.1:27660/api/v1
    description: API 前缀路径(部分集成场景)

security:
  - TokenQuery: []
  - TokenHeader: []

tags:
  - name: Map Management
    description: 地图上传 / 打开 / 关闭 / 删除 / 元数据
  - name: Tiles & Styles
    description: 栅格 / 矢量瓦片、WMS、样式管理
  - name: Query
    description: 要素查询、坐标变换
  - name: Analysis
    description: 图形差分 / 对比 / 表格抽取 等高级分析
  - name: Compose & Export
    description: 组合拆分地图、导出 PDF / DXF / 离线包
  - name: WebCAD
    description: 在线 CAD 编辑(可选功能)
  - name: Workspace
    description: 工作区(命名空间)管理
  - name: Data Storage
    description: 键值数据存储
  - name: Full-text Search
    description: CAD 文字全文索引(Bleve)
  - name: Symbols
    description: 符号库(只读)
  - name: System
    description: 心跳 / 版本 / 运行状态 / 日志
  - name: Misc Services
    description: 二维码 / 字体 glyphs / 图片上传
  - name: AI
    description: 向量检索 / FAQ / 知识库文档

components:
  securitySchemes:
    TokenQuery:
      type: apiKey
      in: query
      name: token
    TokenHeader:
      type: apiKey
      in: header
      name: token
  parameters:
    MapIdPath:
      name: mapid
      in: path
      required: true
      schema: { type: string }
      description: 地图 ID
    VersionPath:
      name: version
      in: path
      required: true
      schema: { type: string, default: v1 }
      description: 版本号,默认 v1
  schemas:
    GatewayError:
      description: web服务标准错误包
      type: object
      properties:
        code: { type: integer, example: 7 }
        msg: { type: string, example: "Token is invalid" }
        data: { type: object, example: { reload: true } }
    ServiceOk:
      description: /service/* 系列的标准 OK 包
      type: object
      properties:
        status: { type: string, example: "ok" }
        data: { }
    CmdBoolResult:
      description: "/cmd/* 命令返回的 { status: true|false, ... } 结构"
      type: object
      properties:
        status: { type: boolean }
      additionalProperties: true
    # ---------- Map ----------
    UploadResponse:
      type: object
      description: |
        上传结果。同内容文件已被某图使用时 mapid 为该图并带 version/geom;
        图片带 maptype;失败时为 {uploadname, error}。
      properties:
        fileid:      { type: string, example: "caf27de8a225", description: "c/t + 内容 md5 前 11 位" }
        mapid:       { type: string, example: "caf27de8a225" }
        uploadname:  { type: string, example: "my.dwg", description: 回显上传原文件名 }
        version:     { type: string, example: "v1" }
        geom:        { type: boolean }
        maptype:     { type: string, example: "image" }
        error:       { type: string }
    OpenMapRequest:
      type: object
      required: [mapid]
      description: |
        打开地图请求体。
        - `mapid` 必填,需与 URL path 中的 `{mapid}` 保持一致。
        - `fileid` 选填:首次打开新上传的图时必填(取自 `POST /api/v1/map/uploads`
          的返回值);已存在的图可省略。
        - 打开方式只看布尔 `geom`(后端缺省 false = Memory);后端不读 `mapopenway`
          (客户端枚举,官方 SDK 换算为 `geom = mapopenway !== 'Memory'`)。
        - GeomRender 首次打开异步:返回 `{mapid,version,status:"working",error}`,轮询
          `cmd/listmaps/{mapid}/{version}` 到 status=finish 后再打开一次。
      properties:
        mapid:       { type: string, example: "demo_123", description: "地图 ID(与 URL path 中的 {mapid} 保持一致)" }
        version:     { type: string, example: "v1", description: "POST 时只读 body(query 里的 version 不读)" }
        fileid:      { type: string, example: "caf27de8a225", description: "上传接口返回的 fileid" }
        uploadname:  { type: string, example: "caf27de8a225.dwg", description: "上传接口返回的 uploadname,选填" }
        geom:        { type: boolean, default: false, description: "true=GeomRender(几何渲染),false=Memory(内存打开;从未以 true 打开过的图 cmd 走内存进程,不支持 condition 查询)" }
        style:       { type: object }
        secretKey:   { type: string, description: "v1 首次打开且 ≥10 位时设为该图密钥(原样存);之后用于校验。同名请求头会覆盖它" }
        accessKey:   { type: string, description: "该图 accessKey(仅浏览)或实例 map_super_key" }
        mapbounds:   { type: string, example: "[0,0,100,100]" }
        layers:      { type: array, items: { type: string } }
      example:
        mapid: "demo_123"
        fileid: "caf27de8a225"
        uploadname: "caf27de8a225.dwg"
        geom: true
    OpenMapResponse:
      type: object
      additionalProperties: true
      properties:
        mapid:      { type: string }
        version:    { type: string }
        fileid:     { type: string }
        status:     { type: string, description: "GeomRender 首次打开为 working;完成后为 finish" }
        geom:       { type: string, enum: ["true", "false"], description: 实际打开方式(字符串) }
        layer:      { type: string, example: "s6d04d896e", description: "当前样式图层名 = 瓦片 URL 的 {layer} 段" }
        bounds:     { type: string, description: "'[x1,y1,x2,y2]'" }
        layers:     { type: string, description: "图层数组的 JSON 字符串" }
        styles:     { type: string, description: "样式数组的 JSON 字符串" }
        defaultlayer: { type: string }
        error:      { type: string }
    MapVersionInfo:
      type: object
      properties:
        version:    { type: string, example: "v1" }
        status:     { type: string, example: "finish", description: "working / finish / error / runerror" }
        geom:       { type: boolean }
        db:         { type: string }
        filename:   { type: string }
        uploadname: { type: string }
        mtime:      { type: string, example: "2026-04-10 12:00:00" }
        errmsg:     { type: string }
    ListMapsResponse:
      description: |
        不带分页参数:{mapid: [MapVersionInfo]}(顶层键是动态 mapid)。
        带 curPage/pageCount(均 >0):分页信封 {count, curPage, pageCount, mapIds:[本页 mapid],
        mapInfos:{mapid:[MapVersionInfo]}, allMapIds:[实例全部 mapid,不做前缀隐藏]}。
      oneOf:
        - type: object
          additionalProperties:
            type: array
            items: { $ref: "#/components/schemas/MapVersionInfo" }
        - type: object
          properties:
            count:     { type: integer }
            curPage:   { type: integer }
            pageCount: { type: integer }
            mapIds:    { type: array, items: { type: string } }
            mapInfos:
              type: object
              additionalProperties:
                type: array
                items: { $ref: "#/components/schemas/MapVersionInfo" }
            allMapIds: { type: array, items: { type: string } }
    MapMetadata:
      description: metadata 返回的 JSON(图形服务 原样返回,可能含字符串 JSON 字段)
      type: object
      additionalProperties: true
    # ---------- Query ----------
    QueryFeaturesRequest:
      type: object
      required: [querytype]
      properties:
        querytype: { type: string, enum: [point, rect, condition, expresion] }
        x: { type: number }
        y: { type: number }
        pixelsize: { type: number }
        x1: { type: number }
        y1: { type: number }
        x2: { type: number }
        y2: { type: number }
        expr: { type: string }
        condition: { type: string }
        bounds:
          type: string
          description: >-
            condition 查询的空间范围,必须是字符串:"x1,y1,x2,y2" / "[x1,y1,x2,y2]",
            或 "x,y;x,y;..."(折线,首尾相同为多边形)。传 JSON 数组会被转成空串、过滤静默失效。
        zoom: { type: integer, default: 1 }
        limit: { type: integer }
        fields: { type: string }
        geom:
          type: boolean
          description: >-
            命令路由开关(不是返回几何的开关):缺省按图打开方式;false 强制内存进程,
            condition 会返回 "not support query type!"。
        layername: { type: string }
    QueryFeaturesResponse:
      type: object
      properties:
        result:
          oneOf:
            - type: array
              items: { type: object }
            - type: object
              properties:
                features:
                  type: array
                  items: { type: object }
                total: { type: integer }
    CoordTransformRequest:
      type: object
      required: [srs, crs, points]
      properties:
        srs:    { type: string, example: "EPSG:4326", description: 源坐标系 }
        crs:    { type: string, example: "EPSG:3857", description: 目标坐标系 }
        points: { type: string, example: "116.39,39.9;121.47,31.23", description: '多点串 "x,y;x,y"' }
        fourParameter: { type: string, example: "0,0,1,0", description: "xOff,yOff,scale,rotRad" }
        isInverseFourParamter: { type: boolean, description: 反向应用四参数(后端拼写 Paramter) }
    # ---------- Data storage ----------
    KvSaveRequest:
      type: object
      properties:
        value: { type: string }
        prop:  { type: string }
    KvMultipleEntry:
      type: object
      required: [key]
      properties:
        key:   { type: string }
        value: { type: string }
        prop:  { type: string }
        ttl:   { type: string, description: 秒数(字符串) }
    # ---------- Full-text search ----------
    FullSearchDocument:
      type: object
      description: 全文搜索文档
      required: [id]
      properties:
        id:          { type: string, description: 全库唯一,必填 }
        map_ver:     { type: string, example: "sys_zp_v1" }
        content:     { type: string, description: 被索引的文本 }
        type:        { type: integer, description: "1 单行文字 / 2 多行 / 3 属性注记 / 4 块属性注记 / 5 图层 / 6 块名 / 7 线型 / 8 填充 / 9 自定义" }
        min_x:       { type: number }
        min_y:       { type: number }
        max_x:       { type: number }
        max_y:       { type: number }
        props:       { type: object, description: 任意附加属性(原样 JSON) }
        create_time: { type: string, format: date-time }
        workspace:   { type: string, description: '默认工作区要显式写 "_"' }
        data:        { type: string }
    FullSearchQuery:
      type: object
      description: 全文搜索请求(POST 走 JSON,GET 走同名 query)
      properties:
        query:        { type: string }
        map_ver:      { type: string, description: 多个逗号分隔;纯 mapid 取最新版本 }
        type:         { type: string, example: "1-3,6" }
        workspace:    { type: string, description: '多个逗号分隔;缺省 "_","*" 查全部' }
        limit:        { type: integer, default: 10 }
        offset:       { type: integer, default: 0 }
        fields:       { type: string }
        facet:        { type: boolean }
        bounding_box: { type: string, example: "[0,0,1000,1000]" }
        time_range:   { type: string, example: "2023-01-01~2023-12-31" }
    # ---------- AI ----------
    VectorSearchRequest:
      type: object
      required: [query]
      properties:
        query: { type: string }
        numDocuments: { type: integer, default: 20 }
        collection: { type: string, default: "vj" }
        where: { type: string }
        whereDocument: { type: string }
        searchDescription: { type: string }
        disableSplitSentence: { type: boolean, default: false }
        disableSearchKnowledge: { type: boolean, default: false }
        documents:
          type: array
          items:
            type: object
            properties:
              content: { type: string }
              metadata: { type: object }

paths:
  # ==========================================================
  # 1. Map Management
  # ==========================================================
  /api/v1/map/uploads:
    post:
      tags: [Map Management]
      summary: 上传 DWG(multipart)
      description: |
        服务端只取第一个文件部分,表单字段(md5、secretKey 等)一律不读;
        文件按内容 md5 命名。加密只能在首次 openmap 时设。
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                md5:  { type: string, description: 后端不读(官方 SDK 会带上,无害) }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UploadResponse" }
              examples:
                sample:
                  value:
                    fileid: "caf27de8a225"
                    mapid: "caf27de8a225"
                    uploadname: "my.dwg"

  /api/v1/map/openmap/{mapid}:
    post:
      tags: [Map Management]
      summary: 打开地图
      description: |
        打开一张地图。请求体中只有 `mapid` 必填(且需与 URL path 中的 `{mapid}` 一致)。
        `fileid` / `uploadname` 选填:
        - 首次打开刚上传的新图:建议带上 `fileid`(取自 `POST /api/v1/map/uploads` 的返回)。
        - 已经存在的图:可只传 `mapid`,服务端会按 mapid 自行定位文件。
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - name: version
          in: query
          schema: { type: string, default: v1 }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OpenMapRequest" }
            examples:
              new_upload:
                summary: 首次打开新上传的图(带 fileid,GeomRender)
                value:
                  mapid: "demo_123"
                  fileid: "caf27de8a225"
                  uploadname: "caf27de8a225.dwg"
                  geom: true
              existing:
                summary: 打开已存在的图(仅 mapid,GeomRender)
                value:
                  mapid: "demo_123"
                  geom: true
              memory:
                summary: 内存模式(Memory,geom 为 false 或缺省)
                value:
                  mapid: "demo_123"
                  geom: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OpenMapResponse" }

  /api/v1/map/cmd/closemap/{mapid}/{version}:
    get:
      tags: [Map Management]
      summary: 关闭地图(SDK 走 GET,图形服务 POST 空体会 500)
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CmdBoolResult" }

  /api/v1/map/cmd/deletemap/{mapid}/{version}:
    get:
      tags: [Map Management]
      summary: 删除地图
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        - name: deleteDwgFile
          in: query
          description: true=强制删除源文件;缺省时是否删除取决于服务端 noAutoDeleteDwgFile 配置。注意 openapi/旧文档的 keepSource 参数后端没有消费点。
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CmdBoolResult" }

  /api/v1/map/cmd/listmaps/_/v1:
    get:
      tags: [Map Management]
      summary: 列出地图
      description: |
        非 root 默认隐藏 `ns_` 前缀(config map_list_exclude_prefix),root 隐藏 `ns_temp_`;`temp_` 不隐藏。
        `mapIds` 过滤须 POST JSON body `{"mapIds":[...]}`。
      parameters:
        - name: curPage
          in: query
          description: 页码(1 起,与 pageCount 同时 >0 才分页并改回分页信封)。旧文档的 keyword/page/pageSize 无消费点
          schema: { type: integer }
        - name: pageCount
          in: query
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ListMapsResponse" }

  /api/v1/map/cmd/metadata/{mapid}/{version}:
    get:
      tags: [Map Management]
      summary: 获取地图元数据
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        # 本接口在源码中不读业务参数;官方文档记载的 enableEditMeta/fileid 均无消费点
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MapMetadata" }

  # ==========================================================
  # 2. Tiles & Styles
  # ==========================================================
  /api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}:
    get:
      tags: [Tiles & Styles]
      summary: 栅格瓦片
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        - { name: layer, in: path, required: true, schema: { type: string } }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
        - name: tag
          in: query
          schema: { type: string, description: fileid,用来让 CDN 缓存失效 }
      responses:
        "200":
          description: PNG 瓦片字节流
          content:
            image/png:
              schema: { type: string, format: binary }

  /api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}.mvt:
    get:
      tags: [Tiles & Styles]
      summary: 矢量瓦片
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        - { name: layer, in: path, required: true, schema: { type: string, default: main } }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: MVT 二进制
          content:
            application/vnd.vjmap-vector-tile:
              schema: { type: string, format: binary }

  /api/v1/map/cmd/wms/{mapid}/{version}:
    get:
      tags: [Tiles & Styles]
      summary: WMS 单张出图
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        - name: bbox
          in: query
          required: true
          schema: { type: string, example: "{bbox-epsg-3857}" }
        - name: srs
          in: query
          schema: { type: string, default: "EPSG:3857" }
        - name: width
          in: query
          schema: { type: integer, default: 256 }
        - name: height
          in: query
          schema: { type: integer, default: 256 }
        - name: layers
          in: query
          description: 样式名(注意是复数 layers,非 layer)
          schema: { type: string }
        - name: crs
          in: query
          description: 坐标系别名(与 srs 等价,后端两者都读)
          schema: { type: string }
        - name: transparent
          in: query
          description: 默认 true;为 false 时可用 backgroundColor 指定底色
          schema: { type: string, default: "true" }
        - name: backgroundColor
          in: query
          description: '"r,g,b" 或 "r,g,b,a" 字符串,仅 transparent=false 生效'
          schema: { type: string }
        - name: fourParameter
          in: query
          schema: { type: string }
        - name: useImageRotate
          in: query
          schema: { type: boolean }
      responses:
        "200":
          description: PNG
          content:
            image/png:
              schema: { type: string, format: binary }

  /api/v1/service/wms:
    get:
      tags: [Tiles & Styles]
      summary: WMS 多图叠加
      description: >-
        mapid/version/layers/crs/fourParameter 均为 "_," 分隔的等长数组,
        长度不一致返回 paramter error。不支持 .mvt。
      parameters:
        - { name: mapid, in: query, required: true, schema: { type: string, example: "a_,b_,c" } }
        - { name: version, in: query, required: true, schema: { type: string, example: "v1_,v1_,v1" } }
        - { name: layers, in: query, required: true, schema: { type: string, example: "_,_," } }
        - { name: crs, in: query, required: true, schema: { type: string, example: "_,_," } }
        - { name: fourParameter, in: query, required: true, schema: { type: string, example: "_,_," } }
        - { name: bbox, in: query, required: true, schema: { type: string, example: "{bbox-epsg-3857}" } }
        - { name: srs, in: query, schema: { type: string, default: "EPSG:3857" } }
        - { name: width, in: query, schema: { type: integer, default: 256 } }
        - { name: height, in: query, schema: { type: integer, default: 256 } }
        - name: transparent
          in: query
          description: Go 端会转发到底层单图 wms;backgroundColor 则不转发(仅单图 cmd/wms 有效)
          schema: { type: string, default: "true" }
      responses:
        "200":
          description: PNG
          content:
            image/png:
              schema: { type: string, format: binary }

  /api/v1/map/cmd/thumbnail/{mapid}/{version}:
    get:
      tags: [Tiles & Styles]
      summary: 缩略图
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
        - { name: width, in: query, schema: { type: integer, default: 200 } }
        - { name: height, in: query, schema: { type: integer, default: 200 } }
        - { name: darkTheme, in: query, schema: { type: boolean }, description: 暗色主题(仅这三个参数有消费点) }
      responses:
        "200":
          description: PNG
          content:
            image/png:
              schema: { type: string, format: binary }

  /api/v1/map/cmd/createMapStyle/{mapid}/{version}:
    post:
      tags: [Tiles & Styles]
      summary: 创建自定义样式
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                stylename:  { type: string, description: 样式名(后端读 stylename,不读 name);缺省为系统样式 }
                geom:       { type: boolean, description: "true 几何样式(s 前缀),false 栅格样式(m 前缀)" }
                layeron:    { type: string }
                layeroff:   { type: string }
                backcolor:  { type: string }
                clipbounds: { type: string }
                expression: { type: string }
                lineweight: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stylename: { type: string, example: "sfbd35705c" }

  # ==========================================================
  # 3. Query
  # ==========================================================
  /api/v1/map/cmd/queryFeatures/{mapid}/{version}:
    post:
      tags: [Query]
      summary: 要素查询(点 / 矩形 / 条件 / 表达式)
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QueryFeaturesRequest" }
            examples:
              rect:
                summary: 按矩形查 10 条
                value:
                  querytype: rect
                  x1: 587227463.27
                  y1: 3103509765.63
                  x2: 588006538.39
                  y2: 3104288840.75
                  limit: 10
                  fields: "name,objectid,layername"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/QueryFeaturesResponse" }

  /api/v1/map/cmd/coordtransform/_/_:
    post:
      tags: [Query]
      summary: 坐标变换
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CoordTransformRequest" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  points:
                    type: array
                    description: 与入参 points 顺序一一对应的 [x, y]
                    items: { type: array, items: { type: number } }
              examples:
                sample:
                  value:
                    points: [[12957588.73, 4851421.18]]

  # ==========================================================
  # 4. Analysis (pass-through 风格)
  # ==========================================================
  /api/v1/map/cmd/mapDiff/_null/v1:
    post:
      tags: [Analysis]
      summary: 两张图差异分析
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mapid1:   { type: string }
                version1: { type: string }
                mapid2:   { type: string }
                version2: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CmdBoolResult" }

  /api/v1/map/cmd/extractTable/_null/v1:
    post:
      tags: [Analysis]
      summary: 抽取 CAD 中的表格
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                mapid:   { type: string }
                version: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CmdBoolResult" }

  # ==========================================================
  # 5. Compose & Export
  # ==========================================================
  /api/v1/map/cmd/exportPdf/{mapid}/{version}:
    post:
      tags: [Compose & Export]
      summary: 导出 PDF
      description: |
        ODA PDF 导出选项,不支持 bounds/width/height/scale/outName。
        cmd 路由对空 JSON body 的 POST 回 500 param error——无参数时用 GET。
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                pageWidth:        { type: number, description: 纸张宽(mm),与 pageHeight 同时给才生效 }
                pageHeight:       { type: number, description: 纸张高(mm) }
                pageLeftMargin:   { type: number, default: 0 }
                pageRightMargin:  { type: number, default: 0 }
                pageTopMargin:    { type: number, default: 0 }
                pageBottomMargin: { type: number, default: 0 }
                bZoomToExtents:   { type: boolean, default: true }
                bActiveLayout:    { type: boolean, default: true }
                geomDPI:          { type: integer, default: 600 }
                colorImagesDPI:   { type: integer, default: 400 }
                bwImagesDPI:      { type: integer, default: 400 }
                isBlackWhiteMode: { type: boolean, default: false }
                isGrayMode:       { type: boolean, default: false }
                bEmbededTTF:      { type: boolean, default: false }
                title:            { type: string, default: "PDF File" }
                author:           { type: string, default: "vjmap.com" }
                pdfPath:          { type: string, description: 缺省写入 data/download、按参数 hash 命名 }
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  path:   { type: string, example: "download/7733d6c688.pdf" }
                  status: { type: boolean }

  /api/v1/map/cmd/exportDxf/{mapid}/{version}:
    post:
      tags: [Compose & Export]
      summary: 导出 DXF
      description: |
        整库导出,不支持裁剪/选层。
        cmd 路由对空 JSON body 的 POST 回 500 param error——无参数时用 GET。
      parameters:
        - $ref: "#/components/parameters/MapIdPath"
        - $ref: "#/components/parameters/VersionPath"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                filename:   { type: string, description: 输出名(自动补 .dxf);缺省 <源文件名>_.dxf }
                cadVersion: { type: string, example: "2018", description: "2000/2004/2007/2010/2013/2018;* = 沿用源图版本" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: boolean }
                  fileid: { type: string, example: "c177098c15d1_.dxf", description: 写入 mapfiles 目录的文件名 }
                  error:  { type: string }

  # ==========================================================
  # 7. Workspace
  # ==========================================================
  /api/v1/service/getWorkspace:
    get:
      tags: [Workspace]
      summary: 列出所有工作区
      responses:
        "200": { description: OK }

  /api/v1/service/workspaceCreate:
    post:
      tags: [Workspace]
      summary: 新建工作区
      description: '默认 casbin 策略仅 root 可调(admin/guest 被 /api/v1/service/workspace* 拒,403 权限不足)。'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:              { type: string }
                alias:             { type: string }
                isPublic:          { type: boolean }
                workDir:           { type: string }
                disableFullSearch: { type: boolean }
                # WsData 请求结构无 idleCloseTime/description/token/createUser
      responses:
        "200": { description: OK }

  /api/v1/service/workspaceModify:
    post:
      tags: [Workspace]
      summary: 修改工作区
      description: |
        服务端按 name 定位,只更新 alias/isPublic/disableFullSearch,
        未给的这三项被重置为零值;workDir/idleCloseTime/createTime 传了也不改。仅 root 可调。
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                # ModifyWorkSpace 绑定完整 MapWorkspace,但只更新 alias/isPublic/disableFullSearch
                name:              { type: string }
                alias:             { type: string }
                isPublic:          { type: boolean }
                disableFullSearch: { type: boolean }
                workDir:           { type: string, description: 不会被修改 }
                idleCloseTime:     { type: integer, description: 不会被修改 }
      responses:
        "200": { description: OK }

  /api/v1/service/workspaceDelete/{name}:
    delete:
      tags: [Workspace]
      summary: 删除工作区
      description: '不可逆——停进程、删全文索引并 os.RemoveAll 整个工作目录(全部图/瓦片)。仅 root 可调。'
      parameters:
        - name: name
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: OK }

  /api/v1/service/workspaceStatus:
    get:
      tags: [Workspace]
      summary: 全部工作区状态
      description: '返回所有命名工作区的 WsDataStatus 数组(未起进程的 ProcIsNull: true);一个都没有时为 null。仅 root 可调。'
      responses:
        "200": { description: OK }

  # ==========================================================
  # 8. Data Storage (KV)
  # ==========================================================
  /api/v1/service/data/{key}:
    get:
      tags: [Data Storage]
      summary: 读单/多键
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string, description: 多键用 `;;` 连接 }
        - name: retDataType
          in: query
          schema: { type: string, enum: [value, prop, valueprop] }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ServiceOk" }
    post:
      tags: [Data Storage]
      summary: 写单键
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string }
        - name: ttl
          in: query
          schema: { type: integer }
      description: |
        只用 JSON body(ShouldBindJSON)。raw body 分支不可用:body 已被 JSON 解码器读走,
        小 body 存成 ""、大 body 被截断;value/prop 非字符串同样静默存成 ""。二进制请 base64 后放 value。
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KvSaveRequest" }
      responses:
        "200": { description: OK }
    delete:
      tags: [Data Storage]
      summary: 删除键
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string }
        - name: isPrefix
          in: query
          schema: { type: boolean }
      responses:
        "200": { description: OK }

  /api/v1/service/datas:
    post:
      tags: [Data Storage]
      summary: 批量写键
      description: '四个字段都必须是字符串(含 ttl);绑定失败返回 HTTP 403 {code:7, msg:"json: cannot unmarshal ..."},不是权限问题。'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items: { $ref: "#/components/schemas/KvMultipleEntry" }
      responses:
        "200": { description: OK }

  /api/v1/service/datakeys:
    get:
      tags: [Data Storage]
      summary: 按前缀列出键
      description: |
        SQL `key like 'data_<prefix>%'`:`_`/`%` 为通配符、ASCII 不分大小写、`'` 会破坏 SQL。
      parameters:
        - name: prefix
          in: query
          schema: { type: string }
        - name: retDataType
          in: query
          schema: { type: string, enum: [value, prop, valueprop] }
        - name: isSys
          in: query
          description: 'true 时 prefix 不补 data_,返回整行对象 {keys:[{Key,Value,Prop,Ttl}]}'
          schema: { type: string, enum: ["true"] }
      responses:
        "200": { description: OK }

  # ==========================================================
  # 9. Full-text Search
  # ==========================================================
  /api/v1/service/fullsearch/search:
    get:
      tags: [Full-text Search]
      summary: 全文检索(GET,参数走 query,字段同 FullSearchQuery)
      parameters:
        - name: query
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 10 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: map_ver
          in: query
          schema: { type: string }
        - name: type
          in: query
          schema: { type: string }
        - name: workspace
          in: query
          schema: { type: string }
        - name: fields
          in: query
          schema: { type: string }
        - name: bounding_box
          in: query
          schema: { type: string }
        - name: time_range
          in: query
          schema: { type: string }
      responses:
        "200": { description: OK }
    post:
      tags: [Full-text Search]
      summary: 全文检索(POST,参数走 JSON)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FullSearchQuery" }
      responses:
        "200": { description: OK }

  /api/v1/service/fullsearch/add:
    post:
      tags: [Full-text Search]
      summary: 索引单条文档
      description: 'body 必须包一层 `{document: {...}}`;document.id 不能为空。'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [document]
              properties:
                document: { $ref: "#/components/schemas/FullSearchDocument" }
      responses:
        "200": { description: OK }

  /api/v1/service/fullsearch/delete:
    delete:
      tags: [Full-text Search]
      summary: 删除文档
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: { type: string }
      responses:
        "200": { description: OK }

  # ==========================================================
  # 10. Symbols
  # ==========================================================
  /api/v1/service/symbol/categories:
    get:
      tags: [Symbols]
      summary: 符号分类
      responses:
        "200":
          description: OK
          content:
            application/json:
              examples:
                sample:
                  value:
                    status: ok
                    data:
                      - id: default
                        name: 默认分类
                        createTime: "2026-04-10 11:29:15"

  /api/v1/service/symbol/list/{categoryId}:
    get:
      tags: [Symbols]
      summary: 符号列表(可分类+分页)
      parameters:
        - name: categoryId
          in: path
          required: true
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20 }
      responses:
        "200": { description: OK }

  /api/v1/service/symbol/{id}:
    get:
      tags: [Symbols]
      summary: 获取单个符号
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200": { description: OK }
        "404": { description: 符号不存在 }

  # ==========================================================
  # 11. System
  # ==========================================================
  /heart:
    get:
      tags: [System]
      summary: 心跳(顶层路径,不走 /api/v1 前缀)
      security: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              examples:
                sample: { value: { status: ok } }

  /version:
    get:
      tags: [System]
      summary: 版本信息(顶层路径)
      security: []
      responses:
        "200": { description: OK }

  /api/v1/map/cmd/runstatus/_/v1:
    get:
      tags: [System]
      summary: 运行状态(图形服务命令)
      parameters:
        - name: detail
          in: query
          schema: { type: boolean, default: true }
      responses:
        "200": { description: OK }

  /api/v1/map/cmd/viewlogs/_/v1:
    get:
      tags: [System]
      summary: 查看日志(图形服务命令)
      parameters:
        - name: time
          in: query
          description: 最近 N 分钟的日志
          schema: { type: integer, default: 60 }
        - name: record
          in: query
          schema: { type: integer, default: 100 }
      responses:
        "200":
          description: OK
          content:
            text/plain:
              schema: { type: string }

  # ==========================================================
  # 12. Misc Services
  # ==========================================================
  /api/v1/service/qrcode:
    get:
      tags: [Misc Services]
      summary: 生成二维码 PNG
      parameters:
        - name: content
          in: query
          required: true
          schema: { type: string }
        - name: size
          in: query
          schema: { type: integer, default: 256 }
      responses:
        "200":
          description: PNG
          content:
            image/png:
              schema: { type: string, format: binary }

  /api/v1/service/fonts/{fontstack}/{range}.pbf:
    get:
      tags: [Misc Services]
      summary: 字体 glyphs
      parameters:
        - name: fontstack
          in: path
          required: true
          schema: { type: string, example: "Open Sans Regular" }
        - name: range
          in: path
          required: true
          schema: { type: string, example: "0-255" }
      responses:
        "200":
          description: PBF
          content:
            application/x-protobuf:
              schema: { type: string, format: binary }

  /api/v1/service/uploadImg:
    post:
      tags: [Misc Services]
      summary: 上传图片(JWT token + 固定 Basic Authorization)
      description: |
        路由在全局 JWT/casbin 之后注册,普通 token 照常需要;处理函数另外要求
        `Authorization: Basic X3ZqbWFwQF8=`,并通过 `Uri` 头告知服务端存储目录。
        响应 `{code:0, path: Uri+文件名, url: ImgHost 头+文件名}`。
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [upload]
              properties:
                # 注意:字段名是 upload(后端 c.FormFile("upload")),不是 file
                upload: { type: string, format: binary }
                watermark: { type: boolean }
      responses:
        "200": { description: OK }

  # ==========================================================
  # 13. AI
  # ==========================================================
  /api/v1/service/vectorSearch:
    post:
      tags: [AI]
      summary: 相似性检索
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VectorSearchRequest" }
      responses:
        "200": { description: OK }
        "500":
          description: embedding 模型未配置
          content:
            application/json:
              examples:
                missingModel:
                  value:
                    code: 8
                    data: {}
                    msg: "bge-large-zh-v1.5-f16.gguf is not exist"

  /api/v1/service/chatFaq:
    get:
      tags: [AI]
      summary: FAQ 知识库
      responses:
        "200": { description: OK }

  /api/v1/service/vectorDocs:
    get:
      tags: [AI]
      summary: 向量库文档元数据
      responses:
        "200": { description: OK }