PAYATHON 2026

周期扣款时间判断与7天到期扣款测试问题

支付小周

结论

如果周期扣款接口只执行单次扣款,不托管扣款计划,商家就需要分别记录每个用户的下次扣款时间,并在到期后发起扣款。由于用户的签约时间或首次付款时间不同,next_charge_at 通常也不同。不过,无须为每个用户单独创建定时任务。一般可以设置一个统一的调度任务,定期查询已经到期的记录并批量处理。

如果支付平台支持自动续费、订阅计划或周期代扣托管,则由平台按照协议自动扣款,商家主要负责接收扣款结果通知。具体采用哪种方式,要看所接入产品的接口文档和签约模式。

至于最短周期为 7 天时能否缩短测试时间,要看官方测试环境是否支持时间加速、短测试周期、模拟扣款或修改下次扣款时间。如果平台没有提供这些能力,修改本机时间也无法绕过服务端的 7 天校验。此时需要把本地业务逻辑测试和支付平台的真实到期扣款测试分开。

如何管理不同用户的扣款时间

可以为每份扣款协议或订阅保存以下信息:

  • 商户侧协议编号
  • 支付平台协议编号
  • 用户编号
  • 扣款周期
  • 上次成功扣款时间
  • 下次计划扣款时间
  • 协议状态
  • 当前扣款状态
  • 重试次数
  • 幂等请求号

统一调度器每分钟或每几分钟运行一次,查询符合以下条件的记录:

SELECT *
FROM payment_subscriptions
WHERE status = 'ACTIVE'
  AND next_charge_at <= CURRENT_TIMESTAMP
  AND charge_status = 'WAITING'
ORDER BY next_charge_at
LIMIT 100;

查到记录后,先通过事务或分布式锁取得处理权,再调用扣款接口。这样即使有多个调度实例同时运行,也能避免重复扣款。

下次扣款时间需要按照产品规则计算,常见的算法有两种:

next_charge_at = last_success_at + period

或者:

next_charge_at = current_next_charge_at + period

扣款延迟或失败重试时,两种算法的结果不同。第一种可能让整个账期向后顺延,第二种则能保持固定账期。选择时应结合业务约定,并确认支付平台如何计算最小扣款间隔。

调度代码示例

下面是一个简化的 TypeScript 示例。paymentClient.charge()、请求字段和返回结构均为抽象表示,实际使用时必须替换为支付平台文档规定的接口和字段。

async function processDueCharges(): Promise<void> {
  const subscriptions = await subscriptionRepository.findDueSubscriptions(100);

  for (const subscription of subscriptions) {
    const locked = await subscriptionRepository.tryLock(subscription.id);

    if (!locked) {
      continue;
    }

    const requestId =
      `${subscription.id}:${subscription.nextChargeAt.toISOString()}`;

    try {
      const existing = await chargeRepository.findByRequestId(requestId);

      if (existing?.status === "SUCCESS") {
        continue;
      }

      await chargeRepository.createIfAbsent({
        requestId,
        subscriptionId: subscription.id,
        status: "PROCESSING"
      });

      const result = await paymentClient.charge({
        agreementId: subscription.platformAgreementId,
        merchantRequestId: requestId,
        amount: subscription.amount
      });

      if (result.status === "SUCCESS") {
        const nextChargeAt = addDays(subscription.nextChargeAt, 7);

        await subscriptionRepository.markChargeSucceeded({
          subscriptionId: subscription.id,
          requestId,
          nextChargeAt
        });
      } else if (result.status === "PROCESSING") {
        await chargeRepository.markPending(requestId);
      } else {
        await chargeRepository.markFailed(requestId, result.errorCode);
      }
    } catch (error) {
      await chargeRepository.recordException(requestId, error);
    } finally {
      await subscriptionRepository.unlock(subscription.id);
    }
  }
}

如果接口返回处理中,不要马上判定扣款失败,也不要直接再次扣款。应通过异步通知或主动查询接口确认最终状态。

7 天周期的测试方法

可以依次检查支付平台是否提供以下测试能力。

1. 使用官方沙箱的特殊测试周期

先查看沙箱文档是否允许:

  • 使用比生产环境更短的周期
  • 设置测试专用的下次扣款时间
  • 加速沙箱时间
  • 触发模拟到期事件
  • 调用扣款结果模拟接口

只有文档明确支持的能力才能使用。不要猜测隐藏参数,也不要把沙箱专用参数带入生产环境。

2. 在商户测试环境缩短内部调度时间

如果要验证的是商户系统自身的到期判断、任务调度和状态流转,可以把周期配置为几分钟,不要在代码中写死 7 天:

type BillingConfig = {
  periodMilliseconds: number;
};

const productionConfig: BillingConfig = {
  periodMilliseconds: 7 * 24 * 60 * 60 * 1000
};

const testConfig: BillingConfig = {
  periodMilliseconds: 5 * 60 * 1000
};

这种方法只能验证商户系统能否按时生成扣款请求,不能证明支付平台会接受间隔不足 7 天的真实扣款。

3. 注入可控时钟进行自动化测试

不要让业务代码直接依赖 new Date()。可以注入一个可替换的时钟,在测试中手动推进时间:

interface Clock {
  now(): Date;
}

class SystemClock implements Clock {
  now(): Date {
    return new Date();
  }
}

class FixedClock implements Clock {
  constructor(private currentTime: Date) {}

  now(): Date {
    return this.currentTime;
  }

  advanceDays(days: number): void {
    this.currentTime = new Date(
      this.currentTime.getTime() + days * 24 * 60 * 60 * 1000
    );
  }
}

function isChargeDue(nextChargeAt: Date, clock: Clock): boolean {
  return nextChargeAt.getTime() <= clock.now().getTime();
}

测试示例:

const clock = new FixedClock(new Date("2026-09-01T00:00:00Z"));
const nextChargeAt = new Date("2026-09-08T00:00:00Z");

console.assert(isChargeDue(nextChargeAt, clock) === false);

clock.advanceDays(7);

console.assert(isChargeDue(nextChargeAt, clock) === true);

这样可以快速验证 7 天到期判断,但不会改变支付平台服务端记录的时间。

4. 保留一次完整的真实周期测试

如果官方测试环境不支持时间加速,正式上线前仍建议至少完成一次真实的 7 天链路测试,覆盖:

  • 首次付款或签约
  • 协议状态查询
  • 到期任务触发
  • 扣款请求
  • 异步通知
  • 主动查询补偿
  • 下次扣款时间更新
  • 失败重试和解除协议后的停止扣款

本地模拟可以覆盖大部分业务逻辑,但无法代替支付平台对协议有效期、最小间隔、风控和异步通知的真实校验。

注意事项

扣款调度不能只靠单机定时器。服务重启或多实例部署时,这种方式容易导致漏扣或重复扣款。应结合数据库状态、幂等请求号和锁机制,让任务能够恢复执行。

不要只根据接口的同步响应判断最终扣款结果。支付平台可能已经受理请求,但商户侧因网络超时没有收到响应。此时直接重试可能产生重复请求。

还要确认 7 天从什么时间开始计算。起点可能是签约时间、首次扣款时间、上次成功扣款时间,也可能由平台账期决定。时区、自然日和连续 168 小时也未必采用相同口径。具体规则必须以对应支付产品的官方文档为准。

测试环境中的短周期配置只能用于商户内部测试。生产环境仍需遵守支付平台规定的最短周期,并确保用户授权内容、扣款金额和实际扣款时间符合协议约定。

备注:内容仅供参考。