订阅制支付实现 - 周期性扣款与到期提醒
订阅制支付核心是 “自动续期 + 权益持续交付”,需兼顾 “支付稳定性、用户知情权、合规性”,以下从技术实现、周期扣款流程、到期提醒策略、合规风控四方面,提供适配微信 / 支付宝 / Apple Pay 的可落地方案,覆盖会员订阅、服务续费等场景。
一、核心前提:订阅制支付合规边界(避免踩线)
二、技术选型:多平台订阅支付接口适配
不同平台的订阅制支付接口差异较大,优先选择 “平台官方订阅接口”(稳定性高、合规性强),避免第三方自定义续期(易触发风控):
| 平台 | 官方订阅接口 | 核心优势 | 适配场景 |
|---|---|---|---|
| 微信小程序 | 微信支付 “商户代扣”/“订阅支付” | 支持自动续期、扣款成功率高 | 微信生态内会员、虚拟服务订阅 |
| 支付宝小程序 | 支付宝 “周期扣款” 接口 | 支持自定义扣款周期(日 / 周 / 月 / 年) | 生活服务、内容付费订阅 |
| Apple(iOS App / 小程序) | Apple Pay Subscriptions | 符合 App Store 审核规则,支持跨设备同步 | iOS 端会员订阅(需通过 App Store 审核) |
| 百度小程序 | 百度钱包 “自动续费” 接口 | 对接简单,支持余额 / 银行卡扣款 | 百度生态内工具类服务订阅 |
核心技术准备
三、订阅制支付全流程实现(以微信小程序为例)
1. 订阅开通流程(用户主动开通)
(1)前端展示与授权
// pages/subscribe/subscribe.js
const app = getApp();
Page({
data: {
plans: [
{ id: 'monthly', name: '月度会员', price: 30, cycle: '每月自动续费' },
{ id: 'yearly', name: '年度会员', price: 298, cycle: '每年自动续费' }
]
},
// 选择订阅方案并开通
async openSubscribe(planId) {
const plan = this.data.plans.find(p => p.id === planId);
try {
// 1. 引导用户同意自动续期协议(弹窗展示核心规则)
wx.showModal({
title: '订阅协议',
content: `您将订阅${plan.name}(${plan.price}元/${plan.cycle}),开通后将自动续期,可在“会员中心-订阅管理”取消。`,
confirmText: '同意并开通',
cancelText: '取消',
async confirm() {
// 2. 调用微信订阅支付接口,获取用户授权
const res = await wx.request({
url: app.globalData.baseUrl + '/subscribe/create',
method: 'POST',
data: {
userId: wx.getStorageSync('userId'),
planId: plan.id,
amount: plan.price,
cycle: plan.id === 'monthly' ? 'MONTH' : 'YEAR'
}
});
if (res.data.code === 0) {
// 3. 唤起微信订阅支付授权页
wx.requestPayment({
timeStamp: res.data.data.timeStamp,
nonceStr: res.data.data.nonceStr,
package: res.data.data.package,
signType: 'MD5',
paySign: res.data.data.paySign,
success: () => {
wx.showToast({ title: '订阅开通成功' });
// 跳转会员中心,展示订阅状态
wx.navigateTo({ url: '/pages/member/index' });
}
});
}
}
});
} catch (err) {
wx.showToast({ title: '订阅开通失败', icon: 'none' });
}
}
});(2)后端订阅创建与授权记录
// 后端Node.js示例(Express框架)
router.post('/subscribe/create', async (req, res) => {
const { userId, planId, amount, cycle } = req.body;
try {
// 1. 生成唯一订阅ID
const subscribeId = 'SUB' + Date.now() + Math.floor(Math.random() * 1000);
// 2. 计算下次扣款时间(首次开通即扣当期费用,下次扣款为周期后)
const nextDeductTime = cycle === 'MONTH'
? new Date(new Date().setMonth(new Date().getMonth() + 1))
: new Date(new Date().setFullYear(new Date().getFullYear() + 1));
// 3. 调用微信商户代扣接口,获取支付授权参数
const wechatRes = await wechatPay.subscribePay({
outTradeNo: subscribeId,
totalFee: amount * 100,
body: planId === 'monthly' ? '月度会员订阅' : '年度会员订阅',
openid: req.body.openid,
planId: cycle === 'MONTH' ? 'wx_month_plan' : 'wx_year_plan' // 微信后台配置的订阅计划ID
});
// 4. 存储订阅记录
await db.collection('subscribes').insertOne({
subscribeId,
userId,
planId,
amount,
cycle,
status: 'ACTIVE', // 订阅状态:ACTIVE(生效)/ CANCELLED(已取消)/ EXPIRED(过期)
createTime: new Date(),
nextDeductTime,
lastDeductTime: new Date() // 首次扣款时间
});
// 5. 返回支付授权参数给前端
res.json({ code: 0, data: wechatRes.payParams });
} catch (err) {
res.json({ code: -1, msg: '订阅创建失败', error: err.message });
}
});2. 周期性自动扣款流程(核心环节)
(1)扣款触发机制
(2)扣款逻辑实现
// 后端定时任务(Node.js + node-schedule)
const schedule = require('node-schedule');
// 每天凌晨2点执行扣款任务
schedule.scheduleJob('0 0 2 * * *', async () => {
const now = new Date();
// 查询待扣款的订阅(下次扣款时间在当前时间前后24小时内)
const pendingSubscribes = await db.collection('subscribes').find({
status: 'ACTIVE',
nextDeductTime: { $lte: new Date(now.getTime() + 24 * 60 * 60 * 1000) }
}).toArray();
for (const sub of pendingSubscribes) {
try {
// 调用微信商户代扣接口扣款
const deductRes = await wechatPay.autoDeduct({
outTradeNo: sub.subscribeId + '_' + Date.now(),
totalFee: sub.amount * 100,
body: sub.planId === 'monthly' ? '月度会员自动续费' : '年度会员自动续费',
openid: sub.openid,
contractId: sub.contractId // 微信订阅协议ID(首次授权时获取)
});
if (deductRes.tradeStatus === 'SUCCESS') {
// 扣款成功:更新订阅记录
const nextDeductTime = sub.cycle === 'MONTH'
? new Date(new Date().setMonth(new Date().getMonth() + 1))
: new Date(new Date().setFullYear(new Date().getFullYear() + 1));
await db.collection('subscribes').updateOne(
{ subscribeId: sub.subscribeId },
{ $set: {
nextDeductTime,
lastDeductTime: new Date(),
deductLogs: [...sub.deductLogs || [], { time: new Date(), amount: sub.amount, status: 'SUCCESS' }]
}}
);
// 发送扣款成功提醒
sendNotice(sub.userId, `您的${sub.planId === 'monthly' ? '月度' : '年度'}会员已自动续费${sub.amount}元,权益持续有效至${formatDate(nextDeductTime)}`);
} else {
// 扣款失败:记录日志,触发重试机制
await db.collection('subscribes').updateOne(
{ subscribeId: sub.subscribeId },
{ $set: {
deductLogs: [...sub.deductLogs || [], { time: new Date(), amount: sub.amount, status: 'FAIL', reason: deductRes.errMsg }]
}}
);
// 发送扣款失败提醒,引导用户手动续费
sendNotice(sub.userId, `您的${sub.planId === 'monthly' ? '月度' : '年度'}会员自动续费失败,请前往会员中心手动续费,避免权益中断`);
}
} catch (err) {
console.error(`订阅${sub.subscribeId}扣款异常:`, err);
// 记录异常日志,后续人工核查
await db.collection('subscribe_errors').insertOne({
subscribeId: sub.subscribeId,
userId: sub.userId,
errorMsg: err.message,
time: new Date()
});
}
}
});3. 到期提醒与续费引导流程
(1)提醒时机与渠道(多触点触达)
| 提醒节点 | 提醒渠道 | 提醒内容示例 |
|---|---|---|
| 下次扣款前 3 天 | 站内信 + 服务通知(微信 / 支付宝) | “您的月度会员将于 3 天后自动续费 30 元,可前往会员中心取消订阅” |
| 扣款成功后 1 小时 | 站内信 + 短信(可选) | “续费成功!您的月度会员有效期延长至 2026-01-28,权益持续生效” |
| 扣款失败后 1 小时 | 站内信 + 服务通知 + 短信 | “续费失败!您的会员将于 3 天后过期,请检查支付账户余额,前往手动续费” |
| 会员过期后 24 小时 | 站内信 + 服务通知 | “您的会员已过期,点击立即续费可恢复全部权益,立享 8 折优惠” |
(2)提醒功能实现(微信服务通知示例)
// 发送微信服务通知
async function sendWechatNotice(openid, title, content) {
const templateId = '微信服务通知模板ID'; // 需在微信公众平台申请
await wx.request({
url: `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${getAccessToken()}`,
method: 'POST',
data: {
touser: openid,
template_id: templateId,
page: '/pages/member/index', // 点击跳转会员中心
data: {
thing1: { value: title }, // 模板字段1:标题
thing2: { value: content }, // 模板字段2:内容
time3: { value: formatDate(new Date()) } // 模板字段3:时间
}
}
});
}4. 订阅取消流程(便捷操作,合规要求)
(1)前端取消入口实现
// pages/member/subscribeManage.js
async cancelSubscribe() {
wx.showModal({
title: '取消订阅',
content: '取消后将不再自动续费,会员权益将持续至当前周期结束,是否确认取消?',
confirmText: '确认取消',
cancelText: '暂不取消',
async confirm() {
const res = await wx.request({
url: app.globalData.baseUrl + '/subscribe/cancel',
method: 'POST',
data: {
subscribeId: wx.getStorageSync('subscribeId'),
userId: wx.getStorageSync('userId')
}
});
if (res.data.code === 0) {
wx.showToast({ title: '订阅取消成功' });
// 更新页面订阅状态
this.setData({ subscribeStatus: 'CANCELLED' });
} else {
wx.showToast({ title: '取消失败,请重试', icon: 'none' });
}
}
});
}(2)后端取消逻辑与平台同步
router.post('/subscribe/cancel', async (req, res) => {
const { subscribeId, userId } = req.body;
try {
// 1. 同步平台取消订阅(如微信解除代扣协议)
await wechatPay.cancelContract({
contractId: req.body.contractId,
openid: req.body.openid
});
// 2. 更新本地订阅状态
await db.collection('subscribes').updateOne(
{ subscribeId, userId },
{ $set: { status: 'CANCELLED', cancelTime: new Date() } }
);
// 3. 发送取消成功提醒
sendNotice(userId, '您的会员订阅已取消,当前周期权益将持续至' + formatDate(req.body.nextDeductTime));
res.json({ code: 0, msg: '取消成功' });
} catch (err) {
res.json({ code: -1, msg: '取消失败', error: err.message });
}
});四、合规与风控优化(保障长期运营)
1. 合规强化措施
2. 风控与异常处理
五、方案适配场景与落地建议
1. 适配场景
2. 落地建议
通过以上方案,可实现订阅制支付的 “自动续期、便捷管理、合规透明”,既提升用户留存与复购,又符合平台规则与监管要求,适配多平台小程序 / APP 的订阅场景。
