预订会话

预订会话是单个活动的服务器端购物车。其持有的座位和通用入场名额归该会话所有,直到确认、取消或过期,因此买家刷新页面不会丢失选择,同一座位也不会卖给两个人。公共端点无需 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 文档。

输入功能、设置、端点或 SDK 方法。