本文约 4,763 字,预计阅读 12 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:活动现场多了三个人,座位只有一个
线下活动最尴尬的不是没人来,是超额。多人同时在最后几个名额上提交,程序各自读到”还剩 1″,各自写入成功,最后名单比座位多。这类超卖本质上和目标用户并发有关,和用什么插件关系不大;如果你用单选表自己 insert 记录,即便写了判断也会超。其次麻烦的是支付与流水的对应关系:报名表收完了,支付在哪收?付了之后怎么开票、怎么退、怎么改期?把这些串起来才叫活动系统。
原理分析:名额必须用「原子操作」控制,别自己数数
常见的错误做法是先 SELECT count(*) 再 INSERT。两个请求之间的时间窗口足以让数字失效。可靠的做法有三类:
一是数据库层面用唯一约束:orders 表里 (event_id,user_id) 唯一,或者用一个带 AUTO_INCREMENT 上限的票表,靠 INSERT ... SELECT 原子插入。二是借用 WooCommerce 的库存管理:把”票”当成一件库存有限的商品,扣减由 WooCommerce 在 WC_Order 层面完成,天然具备原子性与回滚。三是 Redis 原子自增(INCR)+ 上限判断,适合大流量秒杀。
对普通活动站,方案二性价比最高:支付、邮件、退款、发票全部现成。
| 方案 | 原子性 | 配套能力 | 适用规模 |
|---|---|---|---|
| 自建表 + 唯一索引 | 强 | 需各自实现 | 小型免费活动 |
| WooCommerce 商品做票 | 强 | 支付/退款/邮件齐全 | 中大型付费活动 |
| The Events Calendar + Event Tickets | 强 | 日历/参与者管理 | 需要日历与多场次 |
保姆级实操
步骤 1:确定票结构。 把每种票种(早鸟、全价、学生)建一个 WooCommerce 产品,勾选“管理库存”,把库存量设为名额,Sold individually 设为 true,并设为 Virtual 商品。还要特别注意“库存保留”设置:WooCommerce 有 woocommerce_hold_stock_minutes 选项,在付款未完成期间保留库存(默认是 60 分钟),报名类活动建议缩短到 10 分钟。
步骤 2:活动页与票关联。 用 The Events Calendar 承载活动时间地点,用其官方扩展 Event Tickets 或 Event Tickets Plus 关联 WooCommerce 票种;免费活动可直接用 Event Tickets 自带的 RSVP。
步骤 3:补一张报名信息 custom 表。 活动特有字段(单位、发票抬头、饮食禁忌)不该塞进 WooCommerce order meta 里几十年不清理:
register_activation_hook( __FILE__, function () {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( "CREATE TABLE {$wpdb->prefix}mf_attendees (
id bigint(20) NOT NULL AUTO_INCREMENT,
order_id bigint(20) NOT NULL,
product_id bigint(20) NOT NULL,
full_name varchar(100) NOT NULL,
phone varchar(32) DEFAULT '',
company varchar(255) DEFAULT '',
ticket_code varchar(32) NOT NULL,
checked_in tinyint(1) DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY ticket_code (ticket_code),
KEY order_product (order_id, product_id)
) {$wpdb->get_charset_collate()};" );
} );
步骤 4:付款完成写票,超卖在 INSERT 层被拦住:
add_action( 'woocommerce_order_status_processing', function ( $order_id ) {
$order = wc_get_order( $order_id );
global $wpdb;
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( ! $product || ! $product->is_virtual() ) {
continue;
}
for ( $i = 0; $i < $item->get_quantity(); $i++ ) {
$wpdb->query( $wpdb->prepare(
"INSERT IGNORE INTO {$wpdb->prefix}mf_attendees
(order_id, product_id, full_name, phone, company, ticket_code)
VALUES (%d,%d,%s,%s,%s,%s)",
$order_id,
$item->get_product_id(),
$order->get_formatted_billing_full_name(),
$order->get_billing_phone(),
$order->get_billing_company(),
strtoupper( wp_generate_password( 12, false, false ) )
) );
// INSERT IGNORE 若因 ticket_code 重复被忽略,说明需要重试生成
}
}
} );
这里的 INSERT IGNORE 只是为了幂等:重复的 order 提交流水不会生成第二张票。真正防止超卖的是 WooCommerce 的商品库存,别指望应用层去重。
步骤 5:生成二维码签到码。 前端可用 qrcodejs(David Shim 的开源库)渲染,认证接口用 REST:
const ticketCode = document.getElementById('ticket-code').dataset.code;
new QRCode(document.getElementById('qr-box'), {
text: `${window.location.origin}/checkin?code=${encodeURIComponent(ticketCode)}`,
width: 200,
height: 200,
correctLevel: QRCode.CorrectLevel.M
});
步骤 6:签到核销接口。 只允许特定角色调用,避免被人肉核销:
add_action( 'rest_api_init', function () {
register_rest_route( 'mimangfei/v1', '/checkin', [
'methods' => 'POST',
'permission_callback' => function () {
return current_user_can( 'edit_posts' ); // 现场工作人员账号用 shop_manager/editor
},
'callback' => function ( WP_REST_Request $req ) {
global $wpdb;
$code = sanitize_text_field( $req->get_param( 'code' ) );
$updated = $wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->prefix}mf_attendees SET checked_in = 1 WHERE ticket_code = %s AND checked_in = 0",
$code
) );
return new WP_REST_Response( [ 'ok' => (bool) $updated ], $updated ? 200 : 409 );
},
] );
} );
常见踩坑
- 名额用 select count 自己判断,高并发下必然超卖。
- 案例: 某沙龙活动主办方把库存保留改成 1440 分钟(一天),未付款订单占位一整天,实际到场率不到六成,多个渠道推广的引流全部作废。
- 用同一 Woocommerce 商品做了多个时间场次,早鸟票结束后忘记改库存导致长期超卖。
current_user_can()在 REST 请求早期执行取不到 cookie(未加 nonce),建议前端调 appearanceX-WP-Nonce,否则接口恒返回 401。- 报名 form 用了 mailto 提交,火狐/安全软件拦截后数据全部丢失。
wp_generate_password()默认含特殊字符,二维码内容需 encodeURIComponent,否则扫码后参数被截断。- 退票没有同时增加 WooCommerce 库存和标记 attendee 记录,二次统计出现虚位。WooCommerce 退款与订单管理
总结
判断一个报名系统是否靠谱的标准:名额会不会超、付了款有没有可核销凭证、退改是否成闭环。行动清单:用 WooCommerce 商品做票 → 库存=名额且缩短保留时长 → attendee 明细独立建表 → 票码 QR + REST 核销 → 退款时同时回滚。对于有大流量预期的活动,再考虑 Redis 预扣减方案。不追求成品插件、完全自建的话,报名校验与防重这块可以看 WordPress 表单提交与安全。
