Seatsurfing 1.136 API 使用详解

面向要把 Seatsurfing 1.136.3 接入自动化脚本、内部门户或运维流程的工程师,讲清 REST API 的鉴权模型、核心端点与真实请求/响应示例,并给出错误处理与最佳实践;本文全部端点均在 1.136.3 实测实例上逐条调用取证(命令与回显照实摘录),官方开发者文档入口 developer.seatsurfing.io 不可达期间以本文实测为准。

Seatsurfing自托管预约调度REST API2026/10/24

Seatsurfing 1.136 API 使用详解

目标:面向要把 Seatsurfing 1.136.3 接入自动化脚本、内部门户或运维流程的工程师,讲清 REST API 的鉴权模型、核心端点与真实请求/响应示例,并给出错误处理与最佳实践;本文全部端点均在 1.136.3 实测实例上逐条调用取证(命令与回显照实摘录),官方开发者文档入口 developer.seatsurfing.io 不可达期间以本文实测为准。

一、环境信息

本文实测环境(所有请求/响应均来自该实例,查询日期 2026-10-24):

  • Seatsurfing 版本:1.136.3(ghcr.io/seatsurfing/backend:1.136.3,2026-10-23 发布);
  • 部署形态:按《Seatsurfing 1.136 Docker Compose 部署指南》完成的双服务部署,API 与订座/管理界面同端口对外(本实例为 http://<服务器地址>:8136,应用服务监听 8080);
  • 测试账号:admin@seatsurfing.local / q1w2e3r4(组织管理员,部署时以 INIT_ORG_* 指定);
  • 调用工具:curl(命令行),响应为 JSON(个别回显经 python3 -m json.tool 美化,美化不改变内容);
  • 官方开发者文档入口:developer.seatsurfing.io(官网导航 "Rest API";本文成文时该站点在实验网络不可达,端点清单以实测归纳为准)。

REST API 的官方形态定位(官网导航与仓库信息,查询日期 2026-10-24):后端 Go 编写的 REST API 同时服务自有 Booking UI 与 Admin UI 两个前端,API 与前端同端口同源,不存在独立的 API 端口;另有 SCIM 2.0 自动开通接口与 Confluence/Teams 等集成应用走同一后端。

二、API 基础

本节要做什么:建立对接前的整体认知——Base URL、请求风格、公共响应头与鉴权模型。预期读完即可设计调用骨架。

Base URL 与路径风格:API 与 Web 界面同源,路径直接挂在站点根下(无 /api 前缀)。实测要点:部分资源端点必须带尾斜杠(如 /location/{id}/space/、/booking/),不带时命中前端 404 页——这是实测中最容易踩的调用坑之一。

请求风格:标准 REST。读 GET、写 POST(创建)/PUT(更新,部分设置类端点用 /setting/{name} 直改)、删 DELETE;请求/响应体 JSON(Content-Type: application/json);资源标识为 UUID(X-Object-Id 响应头返回新建资源 ID)。

公共响应头(实测全部响应均带):

Permissions-Policy: accelerometer=(), ambient-light-sensor=(), ... (安全策略头)
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Ratelimit-Limit: 250
X-Ratelimit-Remaining: 248
X-Ratelimit-Reset: 1792819483

限流三头对应部署篇环境变量 RATE_LIMIT(默认 250)与 RATE_LIMIT_PERIOD(默认 1-M,1 分钟窗口);Reset 为窗口重置的 Unix 时间戳。脚本对接应读取 X-Ratelimit-Remaining 并在临近 0 时按 X-Ratelimit-Reset 退避。

鉴权模型总述:登录(可带 TOTP)→ 发放 RS512 签名的 JWT 访问令牌(15 分钟有效)+ 不透明刷新令牌(UUID 形式)→ 请求带 Authorization: Bearer <accessToken> → 过期用刷新令牌换新。JWT 签名密钥可由部署篇的 JWT_PRIVATE_KEY/JWT_PUBLIC_KEY 固定。

三、前置条件

  1. 已有可访问的 Seatsurfing 1.136.3 实例与组织管理员账号(部署见《Seatsurfing 1.136 Docker Compose 部署指南》,账号语义见《Seatsurfing 1.136 配置参数详解》5.3);
  2. 本地具备 curl 与 python3(示例用 python3 做 JSON 解析/美化);
  3. 如对接生产实例,确认调用方在限流窗口内的请求量预算(默认 250 请求/分钟/IP);
  4. 本文所有示例的 $JWT 变量指登录获得的访问令牌,第四章演示其获取。

四、认证与令牌管理

4.1 获取组织 ID 并登录

本节要做什么:拿组织 UUID,然后带邮箱/密码/组织 ID 登录换取令牌。预期得到 200 + accessToken/refreshToken。

第一步:查询组织。邮箱登录页会按邮箱先查组织(组织可自定义域),公开端点 GET /auth/singleorg 返回默认组织:

curl -s http://localhost:8136/auth/singleorg | python3 -m json.tool

实测回显:

{
    "id": "3c595626-9cef-4945-b99d-30d6b84c443d",
    "name": "Quzuo Demo",
    "domain": "localhost",
    "language": "en"
}

第二步:登录。POST /auth/login,请求体需四个字段(organizationId 必填,缺失实测返回 400):

curl -s -i -X POST http://localhost:8136/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@seatsurfing.local","password":"q1w2e3r4","organizationId":"3c595626-9cef-4945-b99d-30d6b84c443d","code":""}'

实测回显(节选):

HTTP/1.1 200 OK
X-Ratelimit-Limit: 250
X-Ratelimit-Remaining: 248

{"accessToken":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6…","refreshToken":"743e8694-9c37-4ae1-bcbc-0378fe2f4c79"}

令牌保存(供后续示例使用):

JWT=$(curl -s -X POST http://localhost:8136/auth/login -H 'Content-Type: application/json' \
  -d '{"email":"admin@seatsurfing.local","password":"q1w2e3r4","organizationId":"3c595626-9cef-4945-b99d-30d6b84c443d","code":""}' \
  | python3 -c "import json,sys;print(json.load(sys.stdin)['accessToken'])")

💡 TOTP 两步验证:启用后 code 字段填 6 位验证码;登录请求实测时未开启 TOTP 传空串即可。组织设置 enforce_totp=1 可强制全员启用(见配置篇 4.2)。

4.2 JWT 载荷与有效期

本节要做什么:解读访问令牌内容与有效期,为调度刷新逻辑提供依据。预期得到载荷字段语义与 15 分钟有效期结论。

把 accessToken 的第二段做 Base64URL 解码,实测载荷:

{
 "email": "admin@seatsurfing.local",
 "userID": "21469d01-4841-4dcd-816a-71af3938e3c5",
 "sid": "b0dc1af6-96c3-41c2-be44-c9604a3bec12",
 "iss": "4526f3c2-bb60-47c6-a48d-490396168170",
 "aud": ["4526f3c2-bb60-47c6-a48d-490396168170"],
 "exp": 1792821261,
 "nbf": 1792820361,
 "iat": 1792820361,
 "jti": "a6d86824-2776-4186-a095-42e965c20c26"
}

字段语义:userID 用户 UUID(后续用户接口的路径参数);iss/aud 为安装实例 ID(实测与启动日志 instance ID 一致,也与组织设置 _sys_install_id 一致);exp - iat = 900 秒,即访问令牌有效期 15 分钟。头部实测为 {"alg":"RS512","typ":"JWT"}——RS512 非对称签名,验签方持公钥即可本地校验(对接网关时可校验签名+exp 而无需回查服务端)。

4.3 刷新与后续调用

本节要做什么:用刷新令牌换取新访问令牌,并掌握带令牌调用的统一形态。预期得到 200 + 新 accessToken。

POST /auth/refresh,请求体携带刷新令牌(不是 Bearer 头——实测 Bearer 头形式返回 400):

curl -s -w '\n[%{http_code}]\n' -X POST http://localhost:8136/auth/refresh \
  -H 'Content-Type: application/json' \
  -d "{\"refreshToken\":\"$REFRESH_TOKEN\"}"

实测回显(节选):

{"accessToken":"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.…"}
[200]

之后的全部调用统一带 Authorization: Bearer $JWT。典型调用形态(读当前用户档案):

curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/user/me | python3 -m json.tool

实测回显(节选):

{
    "id": "21469d01-4841-4dcd-816a-71af3938e3c5",
    "email": "admin@seatsurfing.local",
    "firstname": "",
    "lastname": "",
    "emailVerified": true,
    "orgId": "3c595626-9cef-4945-b99d-30d6b84c443d",
    "attr": {},
    "settings": [
        {
            "name": "EMAIL_NOTIFY",
            "value": "true"
        }
    ]
}

4.4 认证错误态实测

场景请求实测结果
密码错误POST /auth/login(密码 wrong-pass)400 Bad Request(注意不是 401)
缺 organizationIdPOST /auth/login(无该字段)400 Bad Request
未带令牌访问受保护端点GET /user/me(无 Authorization 头)401 Unauthorized
令牌过期/密钥更换同端点带旧令牌401 Unauthorized(实测换 JWT 密钥后旧令牌 401,见配置篇 5.4)
令牌有效但权限不足低权角色访问管理端点401(权限模型经 JWT 内角色判定)

⚠️ 实测经验:密码错误返回 400 而非 401——编写重试/告警逻辑时不要把 400 一律当「请求构造错误」,登录场景的 400 通常是凭据问题。连续失败会触发登录保护封禁(默认 10 次失败封 5 分钟,环境变量 LOGIN_PROTECTION_* 可调,见配置篇 5.7)。

五、核心资源端点与调用示例

以下端点全部实测通过。响应 JSON 较长的仅节选关键字段(内容照实摘录)。

5.1 组织与设置

组织设置是「组织级业务配置」的读写通道(49 项设置语义全表见《Seatsurfing 1.136 配置参数详解》4.2):

# 读全部组织设置(JSON 数组,49 项)
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/setting/ | python3 -m json.tool

# 读全部时区(423 项,供界面/校验用)
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/setting/timezones | head -c 200

# 改单项:PUT /setting/{name},成功 204
curl -s -o /dev/null -w '%{http_code}\n' -X PUT http://localhost:8136/setting/default_timezone \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' -d '{"value":"Asia/Shanghai"}'

实测回显(时区端点节选):

[{"id":0,"name":"Africa/Abidjan"},{"id":1,"name":"Africa/Accra"},{"id":2,"name":"Africa/Addis_Ababa"},

实测回显(改时区):

204

5.2 用户

# 用户数
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/user/count
# 用户列表
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/user/ | python3 -m json.tool

实测回显(count):

{"count":1}

实测回显(列表节选):返回用户对象数组,结构与 4.3 的 /user/me 相同(id/email/firstname/lastname/orgId/settings 等)。用户增删改与角色分配走同源接口(对应管理后台的用户与角色管理),权限域为 users(角色权限模型见官网 Key Concepts)。

以下示例中:$JWT 为访问令牌(4.1 获取),$LOC 为位置 UUID,$BID 为预订 UUID(均取自下方实测回显中的真实值可直接替换)。

5.3 位置/楼层

位置(location)= 一张楼层平面图 + 空间策略:

# 位置列表
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/location/ | python3 -m json.tool
# 位置详情
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/location/$LOC | python3 -m json.tool
# 位置地图(上传的平面图 base64 内嵌)
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/location/$LOC/map

实测回显(列表节选):

[
    {
        "id": "84ffbfe0-7348-48e0-983b-50600c1f72f5",
        "organizationId": "3c595626-9cef-4945-b99d-30d6b84c443d",
        "mapWidth": 2047,
        "mapHeight": 802,
        "mapMimeType": "jpeg",
        "name": "Sample Floor",
        "description": "Sample Map provided by Marco Garbelini under the Creative Commons Attribution 2.0 Generic (CC BY 2.0) License: https://www.flickr.com/photos/garbelini/300134781",
        "maxConcurrentBookings": 0,
        "timezone": "",
        "enabled": true,
        "mapScale": 1,
        "mapType": "",
        ...

地图端点返回 {"map":"<base64 原图>","mimeType":"image/jpeg"} 结构(实测 200)。

5.4 工位搜索与空间查询

可用工位搜索用 POST /location/search(注意是 POST,带时间窗条件;时间用带时区偏移的 ISO8601):

curl -s -X POST http://localhost:8136/location/search \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"enter":"2026-10-26T09:00:00+08:00","leave":"2026-10-26T17:00:00+08:00","attributes":[]}' | python3 -m json.tool

实测回显(节选):

[
    {
        "id": "84ffbfe0-7348-48e0-983b-50600c1f72f5",
        "name": "Sample Floor",
        "maxConcurrentBookings": 0,
        "timezone": "",
        "description": "Sample Map provided by Marco Garbelini ...",
        "enabled": true,
        "spacesCount": 17
    }
]

某位置的工位清单(注意路径尾斜杠必须带):

curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/location/$LOC/space/ | python3 -m json.tool

实测回显(节选前两个工位):

[
    {
        "id": "ac8ff93c-1158-4228-80d1-317ba9a60c93",
        "locationId": "84ffbfe0-7348-48e0-983b-50600c1f72f5",
        "name": "Conference 1",
        "x": 64,
        "y": 64,
        "width": 64,
        "height": 64,
        "rotation": 0,
        "enabled": true,
        "requireApproval": true,
        "available": false,
        "maxConcurrentBookings": 0,
        ...
    },
    {
        "id": "a9d8166b-b015-4fcf-86ca-9c79b9d4660a",
        "name": "Desk 1",
        "available": true,
        ...

要点:available 按请求时间上下文判定;requireApproval: true 的空间(会议室类)预订进入待审批状态。

5.5 预订:创建、查询、删除

创建预订 POST /booking/(201 Created,X-Object-Id 头返回新预订 UUID):

curl -s -i -X POST http://localhost:8136/booking/ \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"enter":"2026-10-26T09:00:00+08:00","leave":"2026-10-26T17:00:00+08:00","spaceId":"5b0a5453-02bf-4787-b3a0-c6c7ce197980"}'

实测回显(节选):

HTTP/1.1 201 Created
X-Object-Id: a80dd73c-0e7b-4ea6-8849-0c0874d7c041
X-Ratelimit-Remaining: 247

查询单条与删除:

# 单条详情
curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/booking/$BID | python3 -m json.tool
# 删除(204)
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE -H "Authorization: Bearer $JWT" http://localhost:8136/booking/$BID

实测回显(单条详情):

{
    "id": "a80dd73c-0e7b-4ea6-8849-0c0874d7c041",
    "userId": "21469d01-4841-4dcd-816a-71af3938e3c5",
    "orgId": "3c595626-9cef-4945-b99d-30d6b84c443d",
    "enter": "2026-10-26T01:00:00.000000000+00:00",
    "leave": "2026-10-26T09:00:00.000000000+00:00",
    "spaceId": "5b0a5453-02bf-4787-b3a0-c6c7ce197980",
    "creator": true,
    "approvals": []
}

💡 技术拓展:注意实测细节——提交 +08:00 时区的预订,服务端归一化为 UTC 存储(回读 enter 变为 +00:00 表示)。对接系统间传时间一律用带偏移的 ISO8601(或 UTC),避免按「服务器本地时区」猜测。删除返回 204;删除窗口受组织设置 max_hours_before_delete 约束(默认不限制)。

5.6 统计

curl -s -H "Authorization: Bearer $JWT" http://localhost:8136/stats/ | python3 -m json.tool

实测回显(全新实例):

{
    "numBookings": 1,
    "numSpaces": 17,
    "numUsers": 1,
    "numBookingsThisWeek": 1,
    "numBookingsPerWeekday": [0, 1, 0, 0, 0, 0, 0],
    "numBookingsPerHour": [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "numBookingsPerLocation": [1],
    "spaceUtilization": [5.882352941176471],
    "numSpaceCountPerArea": [17],
    "numUsersPerOrg": 1,
    "numLocations": 1
}

💡 实测彩蛋:创建一条预订后 spaceUtilization 出现 5.88%(1/17),说明利用率按「当天已订工位/总工位」口径即时计算,可直接供自建看板轮询。

六、错误处理

实测错误形态汇总(建议对接时按此对照):

状态码实测触发场景处理建议
400登录密码错误/缺 organizationId;刷新令牌形式错误登录场景 400 多为凭据问题(见 4.4);校验请求体字段
401未带令牌、令牌过期、JWT 密钥更换刷新令牌换新(4.3);刷新也失败则重新登录
404路径错误或资源不存在(响应体为前端 404 HTML 页)先核对尾斜杠(5.4 工位清单实测必须带),再核对 UUID
429超出 RATE_LIMIT 窗口按 X-Ratelimit-Reset 退避后重试
201/204创建成功/更新或删除成功201 读 X-Object-Id 取新资源 ID

统一特征:受保护端点缺令牌一律 401 且无响应体;管理端点对低权限令牌同样 401(权限模型按 JWT 内角色,角色管理见官网 Key Concepts 与管理后台)。

七、最佳实践

  1. 令牌生命周期管理:访问令牌仅 15 分钟(4.2 实测 exp-iat=900),脚本启动先登录,运行中监听 401 → 走 4.3 刷新 → 刷新失败重新登录;多线程任务共享一个登录会话而非各自登录(都计限流)。
  2. 限流退避:读 X-Ratelimit-Remaining,低于 10% 放慢;收到 429 按 X-Ratelimit-Reset(Unix 时间戳)sleep 后重试;批量任务把窗口内请求数控制在 RATE_LIMIT 的 70% 以下。
  3. 时间传递:所有时间字段用带偏移的 ISO8601(如 2026-10-26T09:00:00+08:00);服务端归一化 UTC 存储(5.5 实测),比较逻辑统一转 UTC。
  4. 尾斜杠敏感:/location/{id}/space/ 与 /booking/ 必须带尾斜杠(5.4/5.5 实测),封装 SDK 时在常量表固化,避免调用方拼错。
  5. 固定 JWT 密钥:生产实例按配置篇 5.4 固定 RSA 密钥(PKCS1 格式),否则每次服务重建全体集成脚本 401 重登;网关侧可缓存公钥做本地验签(RS512)。
  6. 幂等与审批:创建预订前先 POST /location/search 校验目标空间 available;requireApproval 空间的预订不会立即生效,集成方不要假设 201 即已确认,用 GET /booking/{id} 轮询 approvals 状态。
  7. 凭据安全:脚本不落明文密码——用受限角色的服务账号(角色按 users/bookings 等权限域最小授权),凭据走密钥管理注入(部署篇/C1 的环境注入机制),日志脱敏 Authorization 头。

八、参考链接