Drupal 6 如何实现支持支付的活动预订系统?
结论
Date + Calendar + Signup + Ubercart + Signup Integration for Ubercart 能够搭建 Drupal 6 的基础活动售票流程,但算不上完整的活动预订系统。对于“每人报名一个名额,付款后确认”这类简单需求,这套组合基本可行。如果需要一次购买多个名额、代朋友报名、锁定待支付名额,或在取消和退款后释放库存,通常还要开发自定义模块。
比较稳妥的做法是:
- 使用
Date和Calendar管理及展示活动时间。 - 为每场活动关联一个 Ubercart 商品。
- 由 Ubercart 处理购物车、订单和支付。
- 用商品数量表示购买的名额数。
- 使用自定义数据表保存每位参加者的信息。
- 根据订单状态确认或释放名额。
- 通过数据库原子更新防止超卖。
如果技术方案仍有调整空间,不建议再新建 Drupal 6 系统。Drupal 6 已停止官方维护,而支付系统还涉及安全补丁、PHP 兼容性和支付网关可用性等问题。
现有模块组合能解决什么
这些模块各自负责不同的环节:
| 模块 | 主要职责 |
|---|---|
Date | 保存活动开始、结束时间 |
Calendar | 按日历形式展示活动 |
Signup | 记录用户对节点的报名 |
Ubercart | 商品、购物车、订单和支付 |
Signup Integration for Ubercart | 尝试关联报名与购买流程 |
可以用它们实现下面的基本流程:
- 管理员创建活动节点。
- 用户打开活动页面。
- 用户报名或购买对应商品。
- Ubercart 创建订单并完成支付。
- 系统根据订单状态确认报名。
主要问题是,Signup 和 Ubercart 对“数量”的处理方式不同。Signup 更接近“一个用户对应一条报名记录”,Ubercart 则允许一条订单商品设置任意购买数量。因此,用户“购买 3 个名额”后,系统不会自动生成 3 位参加者的信息。
名额限制也可能同时来自两个地方:
- Signup 的报名人数上限;
- Ubercart 的商品库存或自定义容量字段。
两套限制同时启用后,很容易出现订单已经付款但 Signup 名额已满,或者 Signup 已登记而商品库存没有扣减的情况。容量必须只有一个权威数据源。
推荐的数据模型
可以为每场活动创建一个活动节点,再关联一个 Ubercart 商品。活动节点保存以下内容:
- 活动时间;
- 活动地点;
- 总名额;
- 每单最多购买数量;
- 报名截止时间;
- 是否允许候补;
- 对应的 Ubercart 商品
nid。
另外建立参加者表,每个实际名额对应一条记录:
CREATE TABLE event_attendee (
attendee_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
event_nid INT UNSIGNED NOT NULL,
order_id INT UNSIGNED NOT NULL,
order_product_id INT UNSIGNED NOT NULL,
uid INT UNSIGNED NOT NULL DEFAULT 0,
attendee_name VARCHAR(255) NOT NULL,
attendee_email VARCHAR(255) NOT NULL DEFAULT '',
status VARCHAR(32) NOT NULL DEFAULT 'pending',
created INT UNSIGNED NOT NULL,
PRIMARY KEY (attendee_id),
KEY event_nid (event_nid),
KEY order_id (order_id)
);
用户购买 3 个名额时,订单商品数量仍是 3,同时创建 3 条参加者记录。购买者不必是实际参加者,因此也能支持代朋友报名。
名额预留流程
建议区分以下状态:
available:尚未占用;pending:已经下单,但尚未支付;confirmed:支付成功;cancelled:订单取消;expired:待支付超时;refunded:订单退款。
处理流程可以设计为:
- 用户选择购买数量。
- 加入购物车时进行提示性检查。
- 创建订单时再次检查容量,并临时锁定相应名额。
- 支付成功后,将名额状态改为
confirmed。 - 订单取消、支付失败或超时后释放名额。
- 退款后是否释放名额,由业务规则决定。
不能只在活动页面或购物车表单中检查剩余数量。两个用户可能同时看到“还剩 2 个名额”,并分别购买 2 个。最终的容量检查必须在数据库写入时完成。
下面是 Drupal 6 风格的原子占位示例。代码只用于说明并发控制思路,字段名和订单回调需要根据实际模块调整:
/**
* Atomically reserves event places.
*/
function custom_event_reserve_places($event_nid, $quantity) {
$event_nid = (int) $event_nid;
$quantity = (int) $quantity;
if ($event_nid <= 0 || $quantity <= 0) {
return FALSE;
}
db_query(
"UPDATE {custom_event_capacity}
SET reserved = reserved + %d
WHERE event_nid = %d
AND reserved + %d <= capacity",
$quantity,
$event_nid,
$quantity
);
return db_affected_rows() === 1;
}
释放名额时也要限制最小值,避免重复处理订单状态时将计数减成负数:
function custom_event_release_places($event_nid, $quantity) {
$event_nid = (int) $event_nid;
$quantity = (int) $quantity;
if ($event_nid <= 0 || $quantity <= 0) {
return FALSE;
}
db_query(
"UPDATE {custom_event_capacity}
SET reserved = reserved - %d
WHERE event_nid = %d
AND reserved >= %d",
$quantity,
$event_nid,
$quantity
);
return db_affected_rows() === 1;
}
订单状态处理还要保证幂等性。同一个“支付成功”通知即使到达两次,也只能确认一次,不能重复扣减名额。可以增加订单事件表,并为 order_id + event_nid + action 建立唯一索引。
收集多位参加者的信息
如果只需要记录购买数量,直接使用 Ubercart 的商品数量即可。如果每个名额都要填写姓名、邮箱或特殊需求,就需要在购物车或结账过程中按数量生成多组字段。
例如,购买数量为 3 时显示三组字段:
for ($i = 0; $i < $quantity; $i++) {
$form['attendees'][$i] = array(
'#type' => 'fieldset',
'#title' => t('Attendee @number', array('@number' => $i + 1)),
);
$form['attendees'][$i]['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#required' => TRUE,
);
$form['attendees'][$i]['email'] = array(
'#type' => 'textfield',
'#title' => t('Email'),
);
}
这些数据不适合只存在订单备注中。最好写入独立的参加者表,并保存对应的订单号和订单商品行,否则后续签到、导出名单、修改订单或处理部分退款都会很麻烦。
是否应该以 Signup 为核心
如果只是免费报名,可以把 Signup 作为核心模块,再用少量扩展处理容量和候补名单。
如果报名必须付款,更适合让 Ubercart 订单成为交易状态的权威来源。Signup 可以展示报名状态,但不应独立判断用户是否已成功购买。至少需要明确下面的状态映射:
| Ubercart 订单状态 | 报名状态 |
|---|---|
| 订单创建、等待付款 | pending |
| 支付确认 | confirmed |
| 订单取消或超时 | expired 或 cancelled |
| 退款 | refunded,是否释放名额按规则处理 |
具体的订单状态名称取决于站点的 Ubercart 配置和支付网关,不能假定所有网关都使用相同状态。尤其不能把“订单已创建”直接当作“已经付款”。
可考虑的替代方案
只使用 Ubercart 并进行定制
在 Drupal 6 中,这种方案相对容易控制。每场活动对应一个商品,以库存表示名额,以商品数量表示预订数量,再补充参加者信息和订单状态同步功能。
支付、订单和退款可以集中在一个系统中,但活动日历、候补、签到及参加者管理仍需自行开发。
CiviCRM 与 CiviEvent
如果系统除了售票,还要管理联系人、参加者、邮件通知和活动记录,可以评估 CiviCRM 的 CiviEvent。
使用前必须核对目标环境能够安装的历史版本,以及它与 Drupal 6、PHP 和数据库的兼容性,不能直接假定当前发布版本仍支持 Drupal 6。它提供的功能更完整,部署和维护成本也明显高于简单的 Ubercart 方案。
外部活动平台
如果不要求所有流程都在 Drupal 中完成,可以用 Drupal 展示活动介绍,再将报名和支付跳转到专门的活动平台。这样可以减少支付安全和并发库存方面的开发工作,但界面一致性和部分数据控制能力会受到影响。
升级后使用较新的活动或电商方案
Drupal Commerce 不能作为 Drupal 6 的替代模块直接安装。如果准备升级 Drupal,应重新评估目标版本可用的 Commerce、活动管理或票务方案,不能把适用于 Drupal 7 及以后版本的模块建议直接套用到 Drupal 6。
实施时容易遗漏的问题
- 待支付订单占用名额的时长;
- 支付网关是否可能重复发送异步通知;
- 用户关闭支付页面后,回调能否继续确认订单;
- 订单取消和全额退款后是否自动释放名额;
- 部分退款如何对应到具体参加者;
- 管理员手动修改订单数量后如何同步名单;
- 是否允许匿名用户报名;
- 同一邮箱能否重复报名;
- 每个订单和每个账号是否需要设置购买上限;
- 活动时间修改后是否通知已报名人员;
- 报名截止后,如何处理已创建但尚未支付的订单;
- 是否需要候补名单、签到、名单导出和隐私授权。
简单的付费活动可以使用现有模块组合,但只能把它当作基础框架,不能视为开箱即用的完整系统。通常,最小可维护方案是以 Ubercart 为订单和支付核心,再开发一个小型活动预订模块,统一管理容量、参加者资料、订单状态和名额释放。上线前还需要完整测试并发下单、重复支付回调、订单超时、取消和退款等情况。
备注:内容仅供参考。
