PAYATHON 2026

Drupal moneyscripts 模块中测试 Dibs 定期付款

支付阿杰

结论

可以提前测试,但不要用一次性付款伪造定期扣款,也不建议修改模块的核心代码。

比较可靠的做法是,在 DIBS 测试环境中创建专用订阅,将试用期缩短到几分钟或几小时,再通过 Drupal 的 cron、队列或模块现有的定时任务触发首次续费。只有这样,才能完整测试”保存付款凭据、订阅到期、后台发起扣款、处理回调、更新订阅状态”这一流程。

如果一次性付款成功,首次定期付款却被拒绝,问题通常不在银行卡本身,而是出在定期付款使用的 token/ticket、交易参数、商户权限或任务执行过程中。

先确认定期扣款是如何触发的

先不要猜 API 方法名。应在测试站点中确认 moneyscripts 模块通过什么机制查找到期订阅,例如:

  • Drupal cron;
  • Queue API;
  • 独立的 Drush 命令;
  • 系统计划任务直接访问某个 URL;
  • DIBS 自己管理的订阅计划;
  • 模块保存付款 token/ticket 后,由 Drupal 主动发起后续扣款。

可以在 Drupal 管理后台查看 cron 最近一次运行时间,并检查日志中是否有 moneyscripts、DIBS、payment、subscription、recurring 等相关记录。

如果续费依赖 Drupal cron,就没有必要等待一个自然月。应该调整测试订阅的到期时间,而不是让一次性付款接口模拟续费。

推荐的测试步骤

1. 使用完全隔离的测试环境

确认以下配置全部指向 DIBS 测试环境:

  • API endpoint;
  • merchant/account ID;
  • API key 或其他认证信息;
  • 回调地址;
  • test mode 配置;
  • 测试卡对应的测试账户。

不要修改生产用户的账期来测试,也不要混用测试凭据和生产凭据。

2. 缩短测试订阅的试用期

先检查模块后台是否提供试用期配置。如果支持按天设置,可以临时改为 1 天。如果只能按月设置,可以在测试数据库中将某个测试订阅的下次扣款时间调整到当前时间之前。

直接修改数据库前,必须确认实际表名、字段含义和时间格式。下面仅用于说明操作方式,并不是 moneyscripts 的真实表结构:

SELECT id, uid, status, next_charge_at
FROM example_subscription
WHERE uid = :test_uid;

确认记录无误后,再在测试环境中修改:

UPDATE example_subscription
SET next_charge_at = :due_timestamp
WHERE id = :subscription_id
  AND status = 'active';

:due_timestamp 应使用该字段的实际格式,比如 Unix timestamp 或数据库日期时间。不要照搬示例中的表名和字段名。

修改后再次查询,确认只有目标测试订阅受到影响。

3. 手动运行 Drupal 的定时任务

如果使用 Drupal 7,常见的运行方式是:

drush cron

较新版本的 Drupal 通常也是:

drush cron

如果没有 Drush,可以从 Drupal 管理界面运行 cron。项目若使用系统 cron,还要检查服务器上的计划任务是否确实执行了正确的 Drupal 入口。

运行 cron 后,需要同时检查:

  • Drupal 日志;
  • PHP 日志;
  • Web 服务器日志;
  • DIBS 测试后台中的交易记录;
  • 订阅记录的状态和下次扣款时间。

cron 成功结束,并不代表 DIBS 请求已经成功。

4. 如果续费使用 Queue API

有些实现会先由 cron 创建队列任务,再由队列消费者执行扣款。因此,一次 cron 可能只完成了”发现到期订阅”,并没有真正调用 DIBS。

应检查队列中是否产生了待处理项目,并根据当前 Drupal 版本和项目实现运行对应的队列 worker。具体的 worker 名称必须从模块代码或服务定义中确认,不能凭空填写。

手动调用代码时应复用模块服务

如果确实需要增加”立即测试续费”功能,应调用 moneyscripts 现有的续费服务,不要在控制器或表单中重新拼装 DIBS HTTP 请求。自行拼装请求很容易漏掉签名、幂等控制、日志、状态更新或失败重试。

下面展示的是建议的代码结构,不代表 moneyscripts 已经提供了这些类名或方法:

<?php

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

final class RecurringPaymentTester implements ContainerInjectionInterface {

  public function __construct(
    private readonly object $recurringPaymentService,
  ) {}

  public static function create(ContainerInterface $container): self {
    return new self(
      $container->get('moneyscripts.recurring_payment'),
    );
  }

  public function charge(int $subscriptionId): void {
    $this->recurringPaymentService->chargeDueSubscription(
      $subscriptionId,
      [
        'test_mode' => TRUE,
      ],
    );
  }

}

实施前需要在项目中确认:

  • 实际服务 ID;
  • 实际续费方法;
  • 参数类型;
  • 是否允许指定单个订阅;
  • 是否有防止重复扣款的幂等机制;
  • 调用后由谁更新订阅和订单状态。

如果模块没有公开服务,更安全的做法是新增一个只在测试环境启用的 Drush 命令,并让它复用 cron 调用的内部函数。不要通过缺少严格权限控制的公开 URL 触发扣款。

Drupal 7 项目通常没有上述服务容器结构。这种情况下,应找到模块的 hook_cron() 实现,再确认它调用了哪个内部续费函数。仍然应该复用该函数,不要复制其中的 DIBS 请求代码。

为什么一次性付款成功,定期付款仍会失败

一次性付款和后续无卡扣款通常不是同一种交易,需要重点检查以下问题。

token 或 ticket 没有正确保存

首次授权时,可能需要明确请求保存银行卡凭据,并取得后续扣款所需的 token、ticket 或 transaction reference。如果数据库没有保存该值,或者保存的是一次性交易编号,续费请求就可能被拒绝。

排查时,不要在日志中输出完整卡号、CVV、API 密钥或可直接用于扣款的完整 token。

商户账户未启用定期付款能力

测试账户能处理一次性交易,不代表它已经启用了 recurring、subscription 或 merchant-initiated transaction。需要让 DIBS 根据实际商户账户确认:

  • 测试环境是否支持该类交易;
  • 商户是否开通相应权限;
  • 当前 API 产品是否支持后台续费;
  • 测试卡是否支持后续无卡交易;
  • 首次授权是否符合保存凭据的要求。

不同 DIBS 产品和版本使用的接口、字段及术语可能不同,应以当前账户所用 API 的文档为准。

续费金额或货币格式错误

常见问题包括:

  • 把 99.00 传成 99,但接口要求使用最小货币单位;
  • 应传 9900,却传入了带小数点的字符串;
  • 货币代码与首次授权不同;
  • 免费试用阶段保存的订单金额为零,续费时仍读取零金额;
  • 税费、折扣或四舍五入使金额不符合接口要求。

金额必须按照所用 DIBS API 对单位的定义处理,不能根据一次性付款页面上的显示格式推断。

请求引用了错误的原交易

后续扣款可能要求引用最初用于建立定期付款关系的交易,而不是任意一笔成功的一次性交易。还需要检查 merchant ID、order ID、token 与用户订阅之间是否发生错配。

cron 没有在预期时间运行

需要区分”付款被拒绝”和”付款请求根本没有发出”。先确认 DIBS 后台是否收到了续费请求:

  • 没有交易记录:先检查 cron、队列、订阅查询条件和 PHP 错误;
  • 有交易记录并返回拒绝:检查 DIBS 返回的错误代码和请求参数;
  • DIBS 显示成功,但 Drupal 显示失败:检查响应解析、回调验证和状态更新。

重复提交

手动运行的 cron、系统 cron 和队列消费者可能同时处理同一订阅。如果模块没有锁或幂等键,第二次请求可能被拒绝,甚至造成重复扣款。

测试”立即续费”前,应确认系统会先锁定订阅,并使用唯一订单号或接口支持的幂等标识。

最有效的定位材料

下一次测试时,建议记录同一笔续费的这些信息:

  • Drupal 内部订阅 ID;
  • 脱敏后的订单号;
  • 请求时间和时区;
  • 调用的是一次性付款还是 recurring API;
  • DIBS 返回的 HTTP 状态码;
  • DIBS 的错误代码和错误消息;
  • token/ticket 是否存在,但不要记录完整值;
  • cron 和队列的执行日志;
  • 扣款前后的订阅状态及 next_charge 值。

其中最有用的是 DIBS 返回的错误代码。只有一个”denied”,无法判断问题究竟是测试卡限制、商户权限、token 无效、金额格式错误,还是重复交易。

建议先提前测试订阅的到期时间,再手动运行现有的 cron 或队列。如果仍然被拒绝,可以把脱敏后的请求类型、错误代码和相关日志交给 DIBS 核对。这样测试的才是实际的首次定期付款流程,而不是又一次成功的一次性付款。

备注:内容仅供参考。