Drupal 和 Ubercart 如何创建可在线支付的客户账单?
结论
Ubercart 支持管理员创建自定义金额的订单,但“创建订单”和“让客户在线支付已有订单”是两个独立环节。
管理员可以在后台为客户创建订单,添加一个通用收费商品,再修改订单行的价格。客户通常也能在订单历史中看到这张订单。不过,Ubercart 核心支付流程是围绕购物车结账设计的。创建一张待处理订单,不会自动将其放入客户的购物车,也未必会生成“立即支付”入口。
常见的解决办法有两种:
- 安装兼容当前 Drupal 和 Ubercart 版本的“支付未结订单”或“账单支付”扩展。
- 编写一个小型自定义模块,为已有订单提供安全的付款页面,并通过 Ubercart 的支付方式完成交易。
如果需要完全沿用原生购物车结账流程,也可以让客户领取一项专属收费商品后再结账,但必须额外处理价格校验、访问权限和重复付款问题。
为什么后台订单不会出现在客户购物车中
Ubercart 的购物车通常保存在当前用户的会话或购物车记录中,而管理员在后台创建的订单属于订单系统,不属于客户当前的购物车。因此可能出现以下情况:
- 管理员已经创建订单。
- 客户可以在订单历史中看到该订单。
- 客户的购物车仍然是空的。
- 支付网关只会在标准结账流程中被调用。
这并不是缺少某个“同步到购物车”选项。购物车和订单原本就是不同阶段的数据。
也不建议将已创建的订单重新放回购物车,因为这样很容易生成第二张订单,最终出现原账单仍未付款、新订单却已经付款的情况。
推荐的处理流程
1. 建立通用收费商品
可以在 Ubercart 中创建一个普通商品,例如:
- 商品名称:
Additional charge - SKU:
EXTRA-FEE - 默认价格:
0.00 - 不需要配送
- 不公开展示,或限制普通用户自行购买
管理员创建账单时,将该商品加入订单,再在订单行中填写实际金额和说明,例如:
- 墙面维修费:¥800
- 钥匙补办费:¥100
- 逾期管理费:¥50
能否直接修改订单行价格,取决于 Ubercart 版本、后台权限和已启用的模块。修改订单价格的权限应只授予受信任的管理员。
2. 以客户身份创建订单
在 Ubercart 后台新建订单时,应选择真实的客户账户,不要使用匿名用户。之后按以下步骤操作:
- 添加通用收费商品。
- 修改订单行的标题、SKU、数量和价格。
- 将订单保存为自定义待付款状态,例如
Invoice pending。 - 向客户发送包含订单号和付款入口的通知。
创建账单时,不要直接将订单状态设为 Completed。完成状态通常表示业务流程已经结束,还可能触发库存、通知或其他规则。
3. 提供已有订单的付款入口
付款页面至少需要检查以下内容:
- 当前登录用户是否为订单所有者。
- 订单是否处于允许付款的状态。
- 订单是否仍有未付余额。
- 订单是否已被取消或删除。
- 支付金额是否根据服务器端订单数据计算,不能直接采用 URL 参数或表单隐藏字段中的金额。
- 支付成功后是否记录交易并更新订单状态。
在 Drupal 7 + Ubercart 3 这类旧项目中,访问检查的核心逻辑可以写成下面这样。具体的菜单路由和支付表单 API 需要按项目使用的版本调整:
/**
* 检查当前用户是否可以支付指定订单。
*/
function rental_invoice_pay_access($order_id) {
global $user;
if (!$user->uid) {
return FALSE;
}
$order = uc_order_load($order_id);
if (!$order) {
return FALSE;
}
// 管理员可以代查,普通用户只能访问自己的订单。
if ($order->uid != $user->uid && !user_access('administer orders')) {
return FALSE;
}
// 只允许指定的未付款状态进入付款流程。
$payable_statuses = array(
'pending',
'invoice_pending',
);
if (!in_array($order->order_status, $payable_statuses, TRUE)) {
return FALSE;
}
// uc_payment_balance() 的可用性及参数形式应以当前版本为准。
$balance = uc_payment_balance($order);
return $balance > 0;
}
这段代码只处理访问控制,不负责收集银行卡信息,也不会直接调用第三方网关。支付表单应使用当前 Ubercart 版本提供的支付方式接口,或者调用相应支付网关模块的正式 API。
不要自行保存银行卡号、CVV 等敏感数据。
余额应如何计算
如果系统支持部分付款,未付余额应根据服务器端记录计算:
未付余额 = 订单应付总额 - 已成功入账的付款 - 已确认退款抵扣
示意代码如下:
$order_total = (float) uc_order_get_total($order);
$paid_total = rental_invoice_get_successful_payments($order->order_id);
$refund_credit = rental_invoice_get_confirmed_refund_credit($order->order_id);
$balance = max(
0,
$order_total - $paid_total - $refund_credit
);
其中,rental_invoice_get_successful_payments() 和 rental_invoice_get_confirmed_refund_credit() 是项目需要自行实现的业务函数,不属于 Ubercart 核心 API。实际计算时,必须明确哪些交易状态代表付款成功,不能将未确认或失败的付款计入已付金额。
如果业务不需要部分付款,流程会简单得多。客户只需一次性支付订单的全部未付金额。
使用原生结账流程的替代方案
如果不打算开发“支付已有订单”功能,可以将账单设计成客户专属的待购项目:
- 管理员创建一条内部账单记录。
- 系统生成一个仅限指定客户访问的付款链接。
- 客户打开链接后,服务器根据账单记录将收费商品加入购物车。
- 服务器使用账单金额覆盖商品价格。
- 客户通过正常的 Ubercart 流程结账。
- 新订单付款成功后,系统将内部账单标记为已支付。
这种方式可以复用现有支付网关,但必须防止客户修改价格或支付他人的账单。金额不能直接放在查询字符串中,例如:
/cart/add/EXTRA-FEE?price=800
即使页面隐藏了价格字段,客户仍然可以修改请求。安全的链接只能携带无法猜测的账单令牌:
/invoice/pay/4f2d8c7e...
服务器收到令牌后,再从数据库读取客户、金额、币种和账单状态。
扩展模块的选择
Ubercart 相关扩展的名称和维护状态会随 Drupal 和 Ubercart 版本变化。选择模块时,应确认它是否明确支持:
- 支付已有订单的余额;
- 当前使用的 Drupal 和 Ubercart 主版本;
- 项目正在使用的支付网关;
- 部分付款或多次付款;
- 支付回调的重复通知处理;
- 订单所有权和访问控制。
只提供 PDF 或 HTML 发票的模块,未必支持已有订单的在线支付。“生成发票”和“支付未结订单”不是同一项功能。
重要注意事项
防止重复付款
支付网关可能多次发送同一笔交易的成功回调。处理回调时,必须检查交易号或幂等键,确保同一笔交易只入账一次。
不要直接信任订单状态
判断订单是否已经付款时,应同时核对成功的支付记录和剩余余额。管理员误改订单状态、网关回调失败或部分付款,都可能造成订单状态与实际金额不一致。
保留收费明细
订单行不要只写“额外费用”,至少应记录收费原因、发生日期和对应物业,必要时附上维修记录或图片。客户可以据此了解账单内容,后续审计也有记录可查。
处理订单变更
客户开始付款后,不应再随意修改订单金额。如果确实需要修改,应让旧的付款会话失效,重新计算余额,并记录修改人、修改时间和修改原因。
先在测试环境验证
不同版本的 Ubercart、支付网关模块和订单状态配置可能存在较大差异。上线前至少需要验证:
- 管理员创建账单;
- 客户只能看到自己的账单;
- 全额付款和部分付款;
- 支付失败后重试;
- 网关重复回调;
- 管理员取消账单;
- 已付款订单无法再次支付;
- 退款后余额和状态是否正确。
Ubercart 可以处理按实际情况开具费用并在线收款的业务,但后台创建的订单不会自动恢复成客户的购物车。更稳妥的做法是保留该订单作为账单,再增加一个经过严格权限检查和余额校验的“支付此订单”流程。
备注:内容仅供参考。
