如何为慈善网站设置用户授权的自动扣款服务?
结论
不要自行保存银行卡号,也不要直接对接各家银行。更合适的做法是接入支持“定期付款”或“直接扣款”的持牌支付服务商,由服务商完成用户身份验证、付款授权、扣款凭证保存和每月自动扣款。
常见方案分为两类:
- 银行卡定期捐赠:用户首次填写卡信息并授权,支付服务商保存支付凭证,此后按月自动扣款。
- 银行账户自动扣款:通过 ACH Direct Debit、SEPA Direct Debit、Bacs Direct Debit 等机制建立 mandate(扣款授权),银行按照约定处理后续付款。
具体能使用哪种方案,取决于慈善组织的注册地、捐赠者所在国家、收款币种,以及支付服务商是否接入当地的自动扣款网络。
为什么不能让网站直接操作银行扣款
自动扣款并不是简单的定时转账,通常需要处理以下事项:
- 明确展示扣款金额、频率和生效时间;
- 获得并保存用户授权;
- 验证银行卡或银行账户;
- 按规定发送扣款前通知;
- 处理强客户认证、争议、退款和授权撤销;
- 安全保存支付凭证;
- 遵守 PCI DSS、隐私保护、反洗钱和当地慈善募款规定。
如果网站直接收集并保存完整卡号,PCI DSS 的合规范围会大幅扩大。使用支付服务商提供的托管结账页或安全支付组件,可以让敏感支付信息不经过网站自己的服务器。
推荐的实现流程
1. 选择支持定期捐赠的支付服务商
选择时应确认服务商是否:
- 允许慈善组织开户;
- 支持目标国家和币种;
- 支持 recurring payments、subscriptions 或 direct debit;
- 提供托管结账页或前端安全支付组件;
- 支持 webhook;
- 允许用户取消授权,并能处理退款和付款失败重试;
- 可以导出对账、捐赠记录和费用明细。
Stripe、PayPal、GoCardless 等服务商通常具备这些能力,但产品覆盖范围和慈善费率会发生变化,应以组织所在地对应的官方产品说明为准。
2. 明确定期捐赠条款
用户确认前,页面至少应说明:
- 每次扣款金额;
- 扣款币种;
- 扣款周期,例如每月一次;
- 首次扣款日期;
- 后续扣款日期或计算规则;
- 取消定期捐赠的方法;
- 退款和争议的处理方式;
- 收款组织的名称和联系方式;
- 隐私政策和支付数据的处理方式。
如果扣款金额会变化,就不能按固定金额订阅处理。可变金额扣款通常需要额外通知或不同类型的授权。
3. 在服务端创建定期付款会话
下面以类似 Stripe Checkout 的服务端流程说明整体架构。实际字段、支付方式和可用地区应以当前 SDK 和账户配置为准。
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.post("/api/donations/monthly", async (req, res) => {
const { priceId } = req.body;
// priceId 必须在服务端校验,不能允许浏览器任意指定金额。
const allowedPriceIds = new Set([
process.env.MONTHLY_DONATION_PRICE_ID
]);
if (!allowedPriceIds.has(priceId)) {
return res.status(400).json({ error: "Invalid donation option" });
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [
{
price: priceId,
quantity: 1
}
],
success_url:
"https://charity.example/donation/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url:
"https://charity.example/donation/cancelled"
});
res.json({ url: session.url });
});
浏览器只负责请求创建会话,然后跳转到支付服务商的页面:
async function startMonthlyDonation(priceId) {
const response = await fetch("/api/donations/monthly", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ priceId })
});
if (!response.ok) {
throw new Error("Unable to create donation session");
}
const { url } = await response.json();
window.location.assign(url);
}
这里使用预先在支付平台创建的 priceId,不直接采用前端提交的金额,以免用户篡改价格。如果允许用户自定义金额,必须在服务端校验币种、最小金额、最大金额和整数精度,再创建相应的付款计划。
4. 使用 webhook 确认付款状态
不能根据用户是否打开“支付成功”页面来判断捐赠是否成功。用户可能会关闭浏览器,银行账户扣款也可能仍在异步处理中。付款结果应以通过服务商签名验证的 webhook 事件为准。
app.post(
"/webhooks/payment-provider",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch {
return res.status(400).send("Invalid webhook signature");
}
switch (event.type) {
case "invoice.paid":
await recordSuccessfulDonation(event.data.object);
break;
case "invoice.payment_failed":
await recordFailedDonation(event.data.object);
break;
case "customer.subscription.deleted":
await markRecurringDonationCancelled(event.data.object);
break;
}
res.sendStatus(200);
}
);
不同支付方式和服务商使用的事件名称可能不同。例如,银行直接扣款可能先进入 processing 状态,几天后才会成功或失败。因此,数据库不能只有“已支付”和“未支付”两种状态,而应支持类似以下状态:
pending
processing
paid
failed
refunded
disputed
cancelled
Webhook 还需要做幂等处理。支付服务商可能重复发送同一个事件,因此应保存事件 ID,确保同一笔捐赠不会重复入账,也不会重复发送收据。
5. 提供取消和付款方式管理入口
用户应能方便地完成以下操作:
- 取消后续自动扣款;
- 更新银行卡或银行账户;
- 查看历史捐赠;
- 下载收据;
- 修改定期捐赠金额;
- 联系组织处理退款或错误扣款。
可以使用支付服务商提供的 customer portal,也可以自行制作管理页面,但敏感支付信息仍应交由支付服务商托管。
银行扣款与银行卡扣款的区别
| 方式 | 用户体验 | 到账速度 | 常见特点 |
|---|---|---|---|
| 银行卡定期付款 | 首次输入卡信息并验证 | 通常较快 | 覆盖广,但卡可能过期或被拒付 |
| ACH Direct Debit | 授权美国银行账户扣款 | 通常较慢 | 费用可能较低,付款结果具有延迟 |
| SEPA Direct Debit | 授权欧元区账户扣款 | 通常较慢 | 需要保存 mandate,并遵守通知和退款规则 |
| Bacs Direct Debit | 授权英国银行账户扣款 | 通常较慢 | 有特定的授权、通知和担保规则 |
“每个月月底扣款”需要明确定义,因为每个月的天数不同。可以选择:
- 每月最后一个自然日;
- 每月固定日期,例如 28 日;
- 用户首次捐赠日期所对应的每月日期。
如果支付服务商无法直接设置“每月最后一天”,可以由服务端定时创建付款,但这样会增加失败重试、幂等控制和时区处理的复杂度。对于慈善网站,固定在每月 28 日通常更容易实现。
安全与合规注意事项
- 不要在日志、数据库或错误报告中记录完整卡号、CVV 或银行登录凭据。
- API Secret Key 和 webhook secret 只能保存在服务端环境变量中。
- 全站使用 HTTPS,并为管理后台启用多因素认证。
- 金额、币种和捐赠计划不能只在前端校验。
- 验证 webhook 签名,并防止重复处理。
- 建立针对扣款失败、退款、拒付和授权撤销的处理流程。
- 明确区分一次性捐赠和定期捐赠,不能默认勾选定期扣款。
- 保存用户接受的条款版本、授权时间,以及支付服务商返回的 mandate 或订阅标识。
- 发送“可抵税收据”前,应先确认当地法律要求。普通付款确认不一定是合规的慈善捐赠凭证。
最终架构应由网站展示捐赠方案并管理捐赠记录,支付服务商采集付款信息、保存授权和执行定期扣款,网站通过 webhook 同步真实的付款状态。这样既能让用户在线授权,也能避免网站自行承担银行接入和敏感支付数据处理的全部风险。
备注:内容仅供参考。
