/** * 会员/订阅中心 * * 支持两种入口模式: * mode=vip(默认): 开通全频道会员 * mode=channel : 订阅指定频道,URL 需带 channelId/channelName/monthlyPrice/quarterlyPrice/annualPrice */ const app = getApp() const api = require('../../utils/api') Page({ data: { isVip: false, // 模式:'vip' | 'channel' mode: 'vip', // 频道订阅模式下的频道信息 channelId: '', channelName: '', monthlyPrice: 0, quarterlyPrice: 0, annualPrice: 0, // 预计算(WXML 不支持方法调用) _quarterlySaving: 0, _quarterlyMonthly: '0', _annualSaving: 0, _annualMonthly: '0', // 当前选中的套餐,vip 模式固定 'vip-all',channel 模式为 'monthly'|'quarterly'|'annual' selectedPlan: 'vip-all', currentPrice: '19.9' }, onLoad(options) { const isChannelMode = !!options.channelId if (isChannelMode) { // ─── 频道订阅模式 ─── const monthly = (parseFloat(options.monthlyPrice) || 0) / 100 const quarterly = (parseFloat(options.quarterlyPrice) || 0) / 100 const annual = (parseFloat(options.annualPrice) || 0) / 100 // 默认选中包年,否则最合算的 let defaultPlan = 'monthly' let defaultPrice = monthly if (annual > 0) { defaultPlan = 'annual'; defaultPrice = annual } else if (quarterly > 0) { defaultPlan = 'quarterly'; defaultPrice = quarterly } this.setData({ mode: 'channel', channelId: options.channelId, channelName: decodeURIComponent(options.channelName || ''), monthlyPrice: monthly, quarterlyPrice: quarterly, annualPrice: annual, selectedPlan: defaultPlan, currentPrice: defaultPrice.toFixed(2), _quarterlySaving: (monthly > 0 && quarterly > 0) ? Math.round(monthly * 3 - quarterly) : 0, _quarterlyMonthly: quarterly > 0 ? (quarterly / 3).toFixed(1) : '0', _annualSaving: (monthly > 0 && annual > 0) ? Math.round(monthly * 12 - annual) : 0, _annualMonthly: annual > 0 ? (annual / 12).toFixed(1) : '0' }) } else { // ─── VIP 会员模式 ─── this.setData({ mode: 'vip', isVip: app.globalData.isVip, selectedPlan: 'vip-all', currentPrice: '19.9' }) } }, onShow() { if (this.data.mode === 'vip') { this.setData({ isVip: app.globalData.isVip }) } }, /** 选择套餐 */ selectPlan(e) { const plan = e.currentTarget.dataset.plan let price = '19.9' if (this.data.mode === 'channel') { const map = { monthly: this.data.monthlyPrice, quarterly: this.data.quarterlyPrice, annual: this.data.annualPrice } price = (map[plan] || 0).toFixed(2) } this.setData({ selectedPlan: plan, currentPrice: price }) }, /** 发起支付 */ onPay() { const self = this const { mode, selectedPlan, currentPrice, channelId } = this.data if (mode === 'vip') { // ── VIP 全频道(模拟,后续接入时替换) ── wx.showModal({ title: '确认支付', content: `即将支付 ¥${currentPrice} 开通全频道会员`, success(res) { if (res.confirm) { app.upgradeVip() wx.showToast({ title: '开通成功!', icon: 'success' }) setTimeout(function () { self.setData({ isVip: true }) }, 500) } } }) return } // ── 频道订阅:唤起微信支付 ── if (!selectedPlan) { wx.showToast({ title: '请选择订阅方案', icon: 'none' }) return } // 套餐 → type 映射(后端约定:1=包月 2=包季 3=包年) const typeMap = { monthly: '1', quarterly: '2', annual: '3' } const payType = typeMap[selectedPlan] if (!payType) { wx.showToast({ title: '未知套餐类型', icon: 'none' }) return } wx.showLoading({ title: '获取支付信息...' }) api.unlockChannel(channelId, payType) .then(function (res) { if (res.code !== 200 || !res.data || !res.data.payments) { wx.hideLoading() wx.showToast({ title: res.msg || '获取支付信息失败', icon: 'none' }) return } const payments = res.data.payments const outTradeNo = res.data.outTradeNo wx.hideLoading() // 唤起微信支付 wx.requestPayment({ timeStamp: payments.timeStamp, nonceStr: payments.nonceStr, package: payments.package, signType: payments.signType || 'RSA', paySign: payments.paySign, success() { // 支付 UI 完成后,主动轮询查询支付结果 // 策略:立即查一次,失败则间隔 2s 重试,最多 3 次 // 原因:微信回调是异步的,可能比 success 回调晚几秒到 self._pollPayStatus(outTradeNo, 3, 2000) }, fail(err) { if (err.errMsg && err.errMsg.indexOf('cancel') > -1) { return // 用户主动取消,静默处理 } wx.showToast({ title: '支付失败,请重试', icon: 'none' }) console.error('[支付] wx.requestPayment 失败:', err) } }) }) .catch(function (err) { wx.hideLoading() console.error('[支付] unlockChannel 请求失败:', err) wx.showToast({ title: '网络异常,请重试', icon: 'none' }) }) }, /** * 轮询支付状态 * @param {string} outTradeNo 商户订单号 * @param {number} retries 剩余重试次数 * @param {number} interval 每次重试间隔(ms) */ _pollPayStatus(outTradeNo, retries, interval) { const self = this wx.showLoading({ title: '验证中...' }) api.queryPayStatus(outTradeNo) .then(function (paid) { if (paid) { // ✅ 支付确认成功 wx.hideLoading() wx.showToast({ title: '订阅成功!', icon: 'success' }) app.emit('subscriptionChange') setTimeout(function () { wx.navigateBack() }, 1500) } else if (retries > 1) { // 🔄 尚未到账,等待后重试(回调可能还在路上) setTimeout(function () { self._pollPayStatus(outTradeNo, retries - 1, interval) }, interval) } else { // ⏳ 重试耗尽:回调可能仍在处理,给用户友好提示后返回 wx.hideLoading() wx.showModal({ title: '支付处理中', content: '支付已完成,订阅正在确认中,稍后请刷新查看', showCancel: false, success: function () { wx.navigateBack() } }) } }) .catch(function (err) { wx.hideLoading() console.error('[支付] 查询状态失败:', err) // 查询本身网络失败,乐观处理——让后端 webhook 兜底 wx.showModal({ title: '支付处理中', content: '支付已提交,订阅确认中,稍后请刷新查看', showCancel: false, success: function () { wx.navigateBack() } }) }) }, goBack() { wx.navigateBack() } })