Seatsurfing 1.136 API 使用详解
面向要把 Seatsurfing 1.136.3 接入自动化脚本、内部门户或运维流程的工程师,讲清 REST API 的鉴权模型、核心端点与真实请求/响应示例,并给出错误处理与最佳实践;本文全部端点均在 1.136.3 实测实例上逐条调用取证(命令与回显照实摘录),官方开发者文档入口 developer.seatsurfing.io 不可达期间以本文实测为准。
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 固定。
三、前置条件
- 已有可访问的 Seatsurfing 1.136.3 实例与组织管理员账号(部署见《Seatsurfing 1.136 Docker Compose 部署指南》,账号语义见《Seatsurfing 1.136 配置参数详解》5.3);
- 本地具备 curl 与 python3(示例用 python3 做 JSON 解析/美化);
- 如对接生产实例,确认调用方在限流窗口内的请求量预算(默认 250 请求/分钟/IP);
- 本文所有示例的
$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) |
| 缺 organizationId | POST /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 与管理后台)。
七、最佳实践
- 令牌生命周期管理:访问令牌仅 15 分钟(4.2 实测 exp-iat=900),脚本启动先登录,运行中监听 401 → 走 4.3 刷新 → 刷新失败重新登录;多线程任务共享一个登录会话而非各自登录(都计限流)。
- 限流退避:读
X-Ratelimit-Remaining,低于 10% 放慢;收到 429 按X-Ratelimit-Reset(Unix 时间戳)sleep 后重试;批量任务把窗口内请求数控制在 RATE_LIMIT 的 70% 以下。 - 时间传递:所有时间字段用带偏移的 ISO8601(如
2026-10-26T09:00:00+08:00);服务端归一化 UTC 存储(5.5 实测),比较逻辑统一转 UTC。 - 尾斜杠敏感:
/location/{id}/space/与/booking/必须带尾斜杠(5.4/5.5 实测),封装 SDK 时在常量表固化,避免调用方拼错。 - 固定 JWT 密钥:生产实例按配置篇 5.4 固定 RSA 密钥(PKCS1 格式),否则每次服务重建全体集成脚本 401 重登;网关侧可缓存公钥做本地验签(RS512)。
- 幂等与审批:创建预订前先 POST /location/search 校验目标空间
available;requireApproval空间的预订不会立即生效,集成方不要假设 201 即已确认,用 GET /booking/{id} 轮询approvals状态。 - 凭据安全:脚本不落明文密码——用受限角色的服务账号(角色按 users/bookings 等权限域最小授权),凭据走密钥管理注入(部署篇/C1 的环境注入机制),日志脱敏 Authorization 头。
八、参考链接
- 官方开发者文档入口:https://developer.seatsurfing.io(本文成文时实验网络不可达,端点以 1.136.3 实测为准)
- 源码仓库(API 实现在 server 目录):https://github.com/seatsurfing/seatsurfing
- 本专栏系列:《Seatsurfing 1.136 介绍与选型指南》、《Seatsurfing 1.136 Docker Compose 部署指南》、《Seatsurfing 1.136 配置参数详解》、《Seatsurfing 1.136 API 使用详解》