PAYATHON 2026

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尝试关联报名与购买流程

可以用它们实现下面的基本流程:

  1. 管理员创建活动节点。
  2. 用户打开活动页面。
  3. 用户报名或购买对应商品。
  4. Ubercart 创建订单并完成支付。
  5. 系统根据订单状态确认报名。

主要问题是,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:订单退款。

处理流程可以设计为:

  1. 用户选择购买数量。
  2. 加入购物车时进行提示性检查。
  3. 创建订单时再次检查容量,并临时锁定相应名额。
  4. 支付成功后,将名额状态改为 confirmed。
  5. 订单取消、支付失败或超时后释放名额。
  6. 退款后是否释放名额,由业务规则决定。

不能只在活动页面或购物车表单中检查剩余数量。两个用户可能同时看到“还剩 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 为订单和支付核心,再开发一个小型活动预订模块,统一管理容量、参加者资料、订单状态和名额释放。上线前还需要完整测试并发下单、重复支付回调、订单超时、取消和退款等情况。

备注:内容仅供参考。