预订会话
预订会话是单个活动的服务器端购物车。其持有的座位和通用入场名额归该会话所有,直到确认、取消或过期,因此买家刷新页面不会丢失选择,同一座位也不会卖给两个人。公共端点无需 API 密钥,预订页面可在浏览器驱动流程;私密 API 密钥保留在服务器,用于最终确认。
如何选择流程
- 预订会话:浏览器驱动交互结算,页面创建会话,随点击保留或释放座位,再结算,支付后服务器确认。
- 标准销售流程:服务器端使用私密 API 密钥,在支付期间 lock 座位,确认后 sale,自己维护购物车。
- 直接销售:无用户持有座位的后台销售、赠票和导入。
生命周期
| 状态 | 持有库存 | 进入方式 |
|---|---|---|
ACTIVE |
是 | create;每次 lock 和 unlock 保持此状态 |
PENDING_PAYMENT |
是 | checkout;冻结购物车,开始支付宽限期 |
CONFIRMED |
否,已售 | 私有 confirm;在一个事务中将全部持有条目转为 SOLD |
CANCELLED |
否 | 公共或私有 cancel |
EXPIRED |
否 | ACTIVE 保留时间或 PENDING_PAYMENT 宽限期耗尽 |
默认保留十五分钟、支付宽限十分钟,两者均由运营配置。公共会话载荷提供 expiresAt、expiresInSeconds 和 serverTime,页面无需信任浏览器时钟即可倒计时;create 返回 expiresAt 和 serverTime。私有会话载荷含 expiresAt 和 expiresInSeconds,列表摘要仅含 expiresInSeconds。
为组织启用会话
组织启用公共会话前,所有公共调用(包括 get 和 cancel)均返回 403 NOT_ENABLED:
PUT /api/private/management/v2.0/session-config/
X-API-Key: {organizationToken}
Content-Type: application/json
{ "publicSessionsEnabled": true }
下文私有端点不受此设置影响。
公共端点
基础路径 /api/public/v2.0/session/。无需 API 密钥,create 返回的会话 ID 就是后续调用凭据。将其视为秘密,存储在刷新后仍能读取的位置,例如 sessionStorage。这些端点与其他公共端点一样,不包含在已发布 OpenAPI 文档中;以本页和 Renderer 的 BookingSessionClient 为参考。
| 方法 | 路径 | 用途 |
|---|---|---|
POST |
/ |
创建会话。正文 { "eventId": "...", "publicKey": "..." },必须有 Idempotency-Key 请求头;重复相同键返回同一会话。 |
GET |
/{id} |
获取状态、含锁定时价格的购物车、总额和到期倒计时。 |
POST |
/{id}/lock |
保留座位及 GA 容量。座位以追加方式处理,可重复传入本会话已有 ID;GA capacity 表示该区域期望总量,重复调用安全。全成或全败。 |
POST |
/{id}/unlock |
释放部分持有内容,按所有者限定且幂等;本会话未持有的条目不作处理。 |
POST |
/{id}/checkout |
先锁定可选正文中尚未持有的内容,再冻结购物车并启动支付宽限期;对冻结会话重复调用返回相同载荷。 |
POST |
/{id}/cancel |
释放会话。ACTIVE 时释放全部;已冻结等待支付时必须传 { "startOver": true }。 |
create 返回会话 ID、到期时间、服务器时间、maxSeats(座位数加 GA 容量的会话上限)及 ttlSeconds(倒计时重置后的保留时长)。
lock、unlock、checkout 接受选择对象,每个列表最多 1000 项:
{
"seats": [1001, 1002],
"groupOfSeats": [{ "id": 77, "capacity": 2 }]
}
其他公共调用返回会话:
{
"sessionId": "2c1f0e3a-...",
"eventId": "8f4b...",
"state": "ACTIVE",
"cart": {
"seats": [{ "id": 1001, "priceId": 12, "priceName": "Stalls" }],
"groupOfSeats": [{ "id": 77, "capacity": 2, "priceId": 14, "priceName": "Standing" }]
},
"total": 145.0,
"expiresAt": "2026-09-11T10:15:00",
"expiresInSeconds": 842,
"serverTime": "2026-09-11T10:00:58"
}
条目锁定时捕获价格,此后保留在购物车中。
私有端点
基础路径 /api/private/v2.0/session/,使用 X-API-Key: {organizationToken} 身份验证。将预先配置的组织私密 API 密钥原样发送。
| 方法 | 路径 | 用途 |
|---|---|---|
GET |
/ |
按最新在前列出活动会话。eventId 必填,state 可选,limit 默认 50、最大 200,offset 默认 0。 |
GET |
/{id} |
完整会话:id、eventId、state、cart、reference、total、expiresAt、expiresInSeconds、createdAt。私有载荷用 id 标识会话,不含 serverTime。 |
POST |
/{id}/confirm |
原子地将全部持有条目转为 SOLD 并保存订单引用。可选正文 { "reference": "..." },最多 255 字符。从 PENDING_PAYMENT 调用;CONFIRMED 时重复调用返回已存结果。 |
POST |
/{id}/cancel |
释放全部持有内容并标记 CANCELLED;CANCELLED 或 EXPIRED 时重复调用返回当前状态。 |
列表返回摘要:id、state、cartSize(座位数加 GA 容量)、total、createdAt、expiresInSeconds。
错误
使用 v2 API 通用 Problem Details 正文,包含 errorCode,必要时还包含额外成员。
| 状态 | errorCode | 情况 | 额外成员 |
|---|---|---|---|
| 403 | NOT_ENABLED |
组织未启用公共会话时的任何公共调用,包括 get 和 cancel | |
| 404 | ENTITY_NOT_FOUND |
会话 ID 未知,或 create 的 eventId/publicKey 组合无法解析 | |
| 409 | SEAT_CONFLICT |
lock 或 checkout 无法取得条目,或 confirm 时持有条目无法出售;整个调用回滚 | conflicts: { seats: [...], groupOfSeats: [...] } |
| 409 | SESSION_FROZEN |
对等待支付的冻结会话 lock 或 unlock | state |
| 409 | SESSION_DEAD |
对 CONFIRMED、CANCELLED、EXPIRED 或保留已到期会话 lock、unlock、checkout;对 CONFIRMED cancel;对 CANCELLED 或 EXPIRED confirm | state |
| 409 | SESSION_NOT_CHECKED_OUT |
未 checkout 就 confirm | state |
| 409 | START_OVER_REQUIRED |
公共 cancel 冻结会话时缺少 startOver true | state |
| 422 | EMPTY_CART |
checkout 空购物车 | |
| 429 | CAP_SEATS |
lock 或 checkout 将超过 maxSeats | maxSeats |
get 不会因会话已终止而失败,而是返回 200、已存状态及 expiresInSeconds 0。因此刷新后读取会话必须检查后再复用 ID。已到期保留在清理任务执行前仍可能显示 ACTIVE,冻结会话又会拒绝 lock,所以继续保留要求 state 为 ACTIVE 且 expiresInSeconds 大于零。重复确认 CONFIRMED,以及重复取消 CANCELLED 或 EXPIRED,也返回 200 和已存状态。
会话与其他预订端点
有效会话持有的座位会被 lock、sale、directsale 拒绝,返回 409 和 SESSION_HELD,conflicts.seats 列出座位。它们会明确报告而非跳过,因此直接销售不会静默丢弃订单的一部分。不使用会话的集成不受影响。
浏览器端
@seatmap.pro/renderer 导出公共端点客户端。会话 ID 跨刷新保存,私密 API 密钥永不进入页面。
import {
BookingSessionClient,
BookingSessionError,
selectionFromCart,
} from '@seatmap.pro/renderer';
const sessions = new BookingSessionClient({ baseUrl: 'https://booking.seatmap.pro', publicKey });
const stored = sessionStorage.getItem('seatmap-session');
const existing = stored ? await sessions.get(stored).catch(() => null) : null;
const reusable = existing !== null && existing.state === 'ACTIVE' && existing.expiresInSeconds > 0;
const sessionId = reusable ? existing.sessionId : (await sessions.create(eventId)).sessionId;
sessionStorage.setItem('seatmap-session', sessionId);
try {
await sessions.lock(sessionId, selectionFromCart(cart));
} catch (e) {
if (e instanceof BookingSessionError && e.code === 'SEAT_CONFLICT') {
showTaken(e.conflicts?.seats ?? []);
}
}
const frozen = await sessions.checkout(sessionId);
startCountdown(frozen.expiresInSeconds);
selectionFromCart 将渲染器购物车转换为 lock 的选择对象;selectionFromSession 构造释放会话全部持有内容的 unlock 选择对象。刷新后读取会话,在渲染器中标记 session.cart 的座位即可恢复。所有方法失败时抛出 BookingSessionError,包含 HTTP 状态、errorCode,以及保留被拒绝时的冲突座位和区域。
支付后从服务器确认:
POST /api/private/v2.0/session/{sessionId}/confirm
X-API-Key: {organizationToken}
Content-Type: application/json
{ "reference": "ORDER-2026-000123" }
运营配置
保留时间、支付宽限、每会话座位上限及到期清理由 Booking 服务配置,见预订会话配置。Helm 的 global.bookingSessions.enabled 控制是否提供整个端点集合,见 Helm chart 文档。