# 抢票与限量发售的排队等候室

> 在开售页前排一条公平、实时的队伍：访客看着自己的位置前进，按你设定的速度进场，你的网站验证签名 pass。任何主机都能用，包括 Laravel 和 Filament。

Source: https://hibira.io/zh-cn/use-cases/waiting-room

抢票、限量发售、预购开跑，一整天的流量会挤在开卖的第一分钟。自动扩展来不及反应，结账对每个人都变慢，最后拼的是谁的网速快。等候室在最忙的页面前排一条平静、公平的队伍：访客拿到号码、实时看着队伍前进，按你的网站撑得住的速度进场。自己注册、自己设置，不用找销售，也不用换主机或 CDN。

## 访客会看到什么

- 排队号码与「前面还有 4,812 人」，等待期间实时更新。
- 刷新或多开标签页，位置不会掉，也不会往前插队。
- 托管等候页支持英文、繁体中文、简体中文、日文、韩文、泰文与越南文。
- 开卖前有清楚的倒数，卖完时清楚显示「已售完」。

![托管等候页：访客的排队位置、前面人数、预估等待时间与商家公告，以访客的语言显示。](https://hibira.io/portal/articles/waitroom-page-zh-CN-light.webp)

## 你会得到什么

- 按你设置的速度前进的队伍，例如每分钟 300 人。
- 关键时刻用得到的按钮：暂停队伍、立刻放 500 人进来、库存卖完就关闭。
- 每位放行的访客都会拿到签名 pass，你的网站验证后才提供受保护的页面，猜网址也插不了队。
- 需要的话，用 pass 里的 ticket ID 做到一个号码只能结账一次。
- 排队中的访客不占用 app 的连接数，抢购期间网站其他实时功能照样有余量。
- 对所有排队的人发公告，例如「A 区已售完」，等候页立即显示。

![控制台里的等候室：实时人数、放行速度、预估清空时间，以及暂停、放行、售罄与公告按钮。](https://hibira.io/portal/articles/waitroom-console-zh-CN-light.webp)

等候室是任何 Hibira 方案都能加购的附加功能。一个房间、每月 5,000 位排队访客免费；付费方案每月 $29 起，单场活动票 $49 可用 72 小时。超过方案用量时，多出的访客从点数扣抵，不会有人被挡在门外。详见[价格](https://hibira.io/zh-cn/pricing#waiting-room)。

## 用到的功能

- Hibira app 里的**等候室**，在控制台或用签名的 HTTP API 管理。
- **托管等候页** `ws.hibira.io/wait/APP_KEY/ROOM`，或用 [@hibira-io/waiting-room](https://www.npmjs.com/package/@hibira-io/waiting-room) 自建等候页。
- 每个房间一个 **cache 频道**（`cache-waitroom-ROOM`）：一则消息告诉所有等候中的访客队伍走到哪里，每个浏览器自己算出位置。
- 用你的 app secret 签名（HMAC-SHA256）的 **pass**，你的网站自己验证，不必调用 Hibira。

```text
flow: 没有 pass 的访客先排队，轮到时带着签名 pass 回来
访客|打开 /drop --没有 pass：导向--> *Hibira|托管等候页 --轮到了：`?hibira_pass=…`--> 你的网站|验证 pass、设置 cookie
== 让队伍前进
你的团队|控制台、Filament 或 API --打开、暂停、放行、关闭--> *Hibira|cache-waitroom-presale --一则更新--> 所有等候中的访客|各自算出位置
```

## 选择接入方式

按网站跑在哪里来选。越上面的方式越难被绕过。

| 网站跑在哪里 | 怎么接入 | 在哪里检查 |
| --- | --- | --- |
| Cloudflare Workers、Vercel 上的 Next.js、Deno、Bun | [@hibira-io/waiting-room](https://www.npmjs.com/package/@hibira-io/waiting-room) 的 `protect()` | 页面送出之前 |
| 自己的服务器：Express、Laravel 等 | 一个验证 pass 的 middleware | 路由运行之前 |
| 完全自定义的等候页 | 浏览器端的 `joinWaitingRoom()`，再搭配上面任一种检查 | 路由运行之前 |
| iOS、Android、Flutter 或 Unity App | 等候室的公开端点，再把 pass 放在 API header | 你的 API 里 |
| 没有自己的后端（托管商店） | 托管等候页，加上结账时检查 pass | 结账时 |

## 步骤 1：创建房间

在控制台创建，或用签名的 HTTP API 创建，签名方式和发布事件相同：

```http
POST /apps/APP_ID/waiting_rooms/presale
{
  "admit_per_minute": 300,
  "state": "paused",
  "starts_at": "2026-12-04T12:00:00+09:00",
  "title": "Winter tour presale",
  "return_urls": ["https://shop.example.com/"]
}
```

`open` 按速度放行、`paused` 先把队伍停住、`closed` 在卖完时停止发号码。在 `starts_at` 之前到的访客会拿到号码并看到倒数，时间一到队伍才开始前进。

## 步骤 2：在 edge 保护页面

在 Cloudflare Workers、Next.js middleware、Deno 或 Bun 上，一个调用就搞定：

```js
// middleware.js (Next.js)
import { NextResponse } from 'next/server';
import { protect } from '@hibira-io/waiting-room/server';

export async function middleware(request) {
  const blocked = await protect(request, {
    appKey: process.env.HIBIRA_KEY,
    secret: process.env.HIBIRA_SECRET,
    room: 'presale',
  });
  return blocked ?? NextResponse.next();
}

export const config = { matcher: ['/drop/:path*'] };
```

没有 pass 的访客会被导到等候页。轮到时带着 pass 回来，`protect()` 验证后存进 HttpOnly cookie。Express 请用同一个套件里的 `expressWaitingRoom()`。

## 步骤 3：Laravel

使用 [hibira-io/laravel](https://hibira.io/zh-cn/guides/laravel) 0.4 以上版本（你可能已经用它做广播），用命令创建房间，再用一个middleware 保护路由：

```shell
composer require hibira-io/laravel:^0.4
php artisan hibira:waiting-room presale create --rate=300 --title="Winter presale"
```

```php
Route::middleware('hibira.waiting:presale')->group(function () {
    Route::get('/drop', [DropController::class, 'show']);
    Route::post('/drop/checkout', [DropController::class, 'checkout']);
});
```

没有 pass 的访客会被导到等候页，语言跟你的 app 一致。访客带回来的 pass用你的 app secret 验证，不需要调用 API，并存在 HttpOnly cookie 里直到过期。没有 pass 的 `POST` 会得到 `403`。在 controller 里可以从 request 取得 pass ：

```php
$pass = $request->attributes->get('hibira_pass');
$pass->number;   // queue number
$pass->ticketId; // record it at checkout: one order per number
```

在代码、队列任务或命令行都能操作房间：

```php
use Hibira\Laravel\Facades\Hibira;

Hibira::waitingRoom('presale')->pause();
Hibira::waitingRoom('presale')->admit(500);
Hibira::waitingRoom('presale')->notice('Zone A is sold out');
Hibira::waitingRoom('presale')->close();
```

本地开发时设置 `HIBIRA_WAITING_ROOMS=false`，就不用排队。

## 步骤 4：在 Filament 里操作队伍

开卖当天，团队不需要再多开一个后台。使用 [hibira-io/filament](https://hibira.io/zh-cn/use-cases/filament) 0.5 以上版本，一行就能在后台加上「等候室」页面：

```php
HibiraPlugin::make()->waitingRooms(
    authorize: fn ($user) => $user->is_admin,
)
```

这个页面实时显示每个房间的排队人数、放行进度和速度。团队可以在这里暂停、开放、立即放行、发布公告、售完时关闭房间，也能创建或修改房间。不写 `authorize` 的话，所有后台用户都能进入。

## 步骤 5：自建等候页

托管等候页适合大多数网站。想完全符合自家品牌，可以自己做：

```js
import { joinWaitingRoom } from '@hibira-io/waiting-room';

joinWaitingRoom({
  key: 'APP_KEY',
  room: 'presale',
  onUpdate: ({ ahead, etaMinutes }) => render(ahead, etaMinutes),
  onTurn: (turn) => turn.redirect('https://shop.example.com/drop'),
  onClosed: () => showSoldOut(),
});
```

客户端会把访客的 ticket 存在浏览器里，刷新也不会掉位置。如果连不上，它会改成每隔几秒查一次状态，不会有人被挡在外面。

## 步骤 6：在 App 里排队

手机或游戏客户端用的是和托管等候页相同的公开端点：加入队伍、订阅房间的 cache 频道、轮到时换 pass，之后在 API 调用中带上 pass。你的 API 用 app secret 验证 pass，不需要调用 Hibira；在 Laravel 上用 `WaitingRoom::verifyPass($pass, 'presale')` 即可。

```http
POST /waitroom/APP_KEY/presale/join         -> {"ticket": "…", "number": 5121}
GET  /waitroom/APP_KEY/presale              -> {"admitted": 4980, "state": "open", …}
POST /waitroom/APP_KEY/presale/pass         {"ticket": "…"}  -> {"pass": "…"}
```

## 与 Queue-it、Cloudflare Waiting Room 比较

|  | Hibira | Queue-it | Cloudflare Waiting Room |
| --- | --- | --- | --- |
| 怎么开始 | 自己注册、自己创建房间 | 联系销售；有一个公开的单场方案 | 需要 Business 或 Enterprise 方案 |
| 网站跑在哪里 | 任何主机、CDN 或框架 | 任何主机，搭配他们的 connector | 流量要经过 Cloudflare 代理 |
| 调度开卖与倒数 | 有 | 有 | Enterprise |
| 多个房间 | 有 | 有 | Enterprise，加购 Advanced |
| 自定义等候页 | 有，用小巧的浏览器客户端 | 有 | Enterprise 自定义范本 |
| 在自己的后台操作队伍 | 可以，例如 Filament 页面 | 他们的后台 | Cloudflare 后台或 API |

根据 2026 年 11 月各家公开的价格与方案页面。方案会变动，做决定前请再向各家确认。

完整对比（含价格与各自适合的情况）：[Queue-it](https://hibira.io/zh-cn/compare/queue-it)、[Cloudflare Waiting Room](https://hibira.io/zh-cn/compare/cloudflare-waiting-room)、[CrowdHandler](https://hibira.io/zh-cn/compare/crowdhandler)、[Queue-Fair](https://hibira.io/zh-cn/compare/queue-fair)。

## 小提醒

- 保护真正会卖完的页面：商品页、购物车与结账。首页和图片放在等候室外面。
- 在关键的请求上验证 pass，不要只在浏览器检查。访客可以改自己页面上的任何脚本，但伪造不了签名。
- 如果一个号码只能买一次，结账时记下 pass 的 ticket ID，拒绝用同一个 ID 下第二笔订单。
- 开卖前 15 到 30 分钟就打开房间并设置 `starts_at`。早到的访客会在倒数页等，而不是在你的网站不停刷新。
- 一开始先用结账撑得住的速度，一边看错误率一边往上调。暂停永远只差一个按钮。
