PAYATHON 2026

如何为订阅构建 SMS payments platform?

支付小周

结论

这种流程通常称为运营商代扣(Direct Carrier Billing,DCB),早期也常通过 Premium SMS 实现。费用会计入用户的话费账单,或直接从预付费余额中扣除,并非由普通短信服务商收取。

Twilio 可以收发短信,但通常不能仅凭用户回复 “GO” 完成话费扣款或建立订阅。要在 Ruby on Rails 中实现这类系统,需要接入具备当地运营商结算能力的 DCB 聚合商,如 Boku、Bango、DIMOCO 等。各服务商支持的国家、运营商和订阅模式会不断调整,接入前需要逐项确认,不能只参考其宣传的”全球覆盖”数字。

如果费用不一定要计入手机账单,更实际的做法是使用 Stripe Billing 等支付平台管理订阅,再通过 Twilio 验证手机号并确认订阅。

推荐的系统结构

常见流程是:

  1. 用户输入手机号码。
  2. 系统识别号码所属的国家和运营商。
  3. DCB 服务商返回该号码是否支持话费支付。
  4. 页面向用户展示价格、扣费周期和取消方式。
  5. 服务商发送确认短信,或由应用通过 Twilio 发送提示。
  6. 用户回复 GO,或在运营商托管的页面上确认。
  7. DCB 服务商完成扣款,并通过 webhook 通知 Rails 应用。
  8. 应用收到成功通知后开通权限。
  9. 后续续费、取消、退款和扣款失败等状态也通过 webhook 同步。

这里最重要的一点是:**短信回复只能证明用户已授权,不能作为支付成功的依据。**开通服务前,必须等支付服务商确认扣款成功。

Rails 中的实现方式

建议优先使用服务商提供的托管支付页面或官方 SDK。运营商计费需要经过国家、运营商和业务类型等方面的审批。自行拼接 Twilio 短信和内部订单状态,并不能构成合法且可以结算的话费支付系统。

可以先建立一个不依赖具体服务商的订阅模型:

# db/migrate/xxxx_create_mobile_subscriptions.rb
class CreateMobileSubscriptions < ActiveRecord::Migration[7.1]
  def change
    create_table :mobile_subscriptions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :provider, null: false
      t.string :provider_subscription_id
      t.string :phone_number, null: false
      t.string :country_code, null: false
      t.string :status, null: false, default: "pending"
      t.string :currency
      t.integer :amount_minor
      t.datetime :current_period_ends_at

      t.timestamps
    end

    add_index :mobile_subscriptions,
              [:provider, :provider_subscription_id],
              unique: true
  end
end

创建订阅时,由 DCB 服务商生成支付会话。下面只演示接口结构,字段名和地址需要替换为所选服务商的真实 API:

class CarrierBillingClient
  def create_subscription(phone_number:, amount_minor:, currency:, callback_url:)
    # 使用服务商官方 SDK 或经过认证的 HTTP 客户端。
    # 不要假设下面这些字段是任何特定服务商的正式参数。
    provider_client.create_subscription(
      phone_number: phone_number,
      amount: amount_minor,
      currency: currency,
      callback_url: callback_url
    )
  end

  private

  def provider_client
    @provider_client ||= SelectedProvider::Client.new(
      api_key: Rails.application.credentials.dig(:carrier_billing, :api_key)
    )
  end
end

支付结果通过 webhook 更新:

class CarrierBillingWebhooksController < ActionController::API
  def create
    payload = request.raw_post
    signature = request.headers["X-Provider-Signature"]

    return head :unauthorized unless valid_signature?(payload, signature)

    event = JSON.parse(payload)
    process_event(event)

    head :ok
  end

  private

  def process_event(event)
    event_id = event.fetch("id")

    return if ProcessedWebhook.exists?(provider_event_id: event_id)

    ApplicationRecord.transaction do
      subscription = MobileSubscription.lock.find_by!(
        provider: "selected_provider",
        provider_subscription_id: event.fetch("subscription_id")
      )

      case event.fetch("type")
      when "subscription.activated"
        subscription.update!(status: "active")
      when "subscription.renewed"
        subscription.update!(
          status: "active",
          current_period_ends_at: Time.iso8601(event.fetch("period_ends_at"))
        )
      when "subscription.payment_failed"
        subscription.update!(status: "past_due")
      when "subscription.cancelled"
        subscription.update!(status: "cancelled")
      end

      ProcessedWebhook.create!(provider_event_id: event_id)
    end
  end

  def valid_signature?(payload, signature)
    # 必须按照服务商文档实现签名验证和时间戳检查。
    # 不应只比较一个普通请求参数。
    SelectedProvider::Webhook.verify(payload, signature)
  end
end

实际开发中还要处理 webhook 重放、乱序和重复通知。数据库应保存服务商事件 ID,并用唯一索引保证幂等性。

Twilio 在其中适合做什么

Twilio 可以用于:

  • 验证手机号码;
  • 发送订单状态和服务通知;
  • 接收 GO、STOP、HELP 等短信指令;
  • 将入站短信转发到 Rails webhook;
  • 在不支持 DCB 的地区提供备用确认流程。

Twilio 的入站短信 webhook 不应直接把订阅状态设为 active。收到用户回复后,最多只能将状态改为 authorization_received,然后调用 DCB 服务商并等待扣款结果。

如果通过 Twilio 接收回复,还需要验证 Twilio 的请求签名:

validator = Twilio::Security::RequestValidator.new(
  Rails.application.credentials.dig(:twilio, :auth_token)
)

valid = validator.validate(
  request.original_url,
  request.request_parameters,
  request.headers["X-Twilio-Signature"]
)

return head :unauthorized unless valid

如何选择服务商

评估 DCB 平台时,不能只比较覆盖的国家数量,还要逐项确认:

  • 目标国家是否已经正式上线,而非仍在规划中;
  • 每个国家支持哪些移动运营商;
  • 支持一次性支付、循环订阅,还是同时支持两者;
  • 是否需要使用运营商托管的确认页面或进行双重确认;
  • 单笔和每月扣款上限;
  • 分成比例、结算周期、拒付和退款规则;
  • 是否提供测试环境、带签名的 webhook 和对账接口;
  • 是否允许所经营的内容类别;
  • 用户取消订阅后,状态需要多长时间才能同步;
  • 是否要求本地公司、税务登记或当地短代码。

DCB 的覆盖通常按照”国家 × 运营商 × 业务类型”计算。一个平台即使覆盖多个国家,也可能只有部分运营商支持循环订阅。若希望覆盖更多地区,通常需要选择一个主要聚合商,再为重点市场接入备用服务商,很难依靠单一 API 覆盖全球。

合规和产品注意事项

订阅确认页面和短信应明确展示价格、扣费周期、服务名称和取消方法。月费说明不能隐藏在首次购买之后,也不能使用含糊的措辞,让用户误以为回复短信只是为了验证号码。

此外还要处理:

  • 保存用户的明确同意及相关证据;
  • 提供 STOP、退订页面和客服渠道;
  • 处理未成年人及受限制内容;
  • 遵守数据保护要求,并尽量减少手机号码的存储;
  • 遵守当地消费者保护、短信营销和自动续费规定;
  • 处理税费、运营商分成、退款和对账;
  • 防范 SIM 更换、号码回收以及账户被他人使用的风险。

如果目标只是让用户通过手机快速完成订阅,而非从话费中扣款,那么标准订阅支付平台配合短信验证,通常覆盖范围更广,结算更透明,也更容易通过审核。只有当目标用户缺少银行卡,或当地市场高度依赖预付费手机余额时,DCB 才通常值得承担额外的接入和合规成本。

备注:内容仅供参考。