Files
sundynix-radio-mp/pages/vip/index.js
T
2026-03-05 17:04:40 +08:00

318 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 会员/订阅中心
*
* 支持两种入口模式:
* 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 会员模式 ───
const gd = app.globalData
this.setData({
mode: 'vip',
isVip: gd.isVip,
selectedPlan: 'vip-all',
currentPrice: '--',
vipExpireAt: gd.vipExpireAt ? gd.vipExpireAt.substring(0, 10) : ''
})
// 从后端拉 VIP 配置
this._loadVipConfig()
}
},
_loadVipConfig() {
const self = this
api.getVipConfig().then(function (res) {
if (res.code === 200 && res.data) {
var cfg = res.data
// 后端单位:分 → 元
var price = cfg.discountedPrice > 0 ? (cfg.discountedPrice / 100).toFixed(2) : (cfg.price / 100).toFixed(2)
var originalPrice = cfg.price > 0 ? (cfg.price / 100).toFixed(2) : ''
var hasDiscount = cfg.discountedPrice > 0 && cfg.discountedPrice < cfg.price
self.setData({
currentPrice: price,
vipPrice: price,
vipOriginalPrice: hasDiscount ? originalPrice : '',
vipRemark: cfg.remark || ''
})
}
}).catch(function (err) {
console.error('[VIP] 获取配置失败:', err)
// 容错:使用默认价格
self.setData({ currentPrice: '19.90', vipPrice: '19.90', vipOriginalPrice: '29.90' })
})
},
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.showLoading({ title: '获取支付信息...' })
api.initiateVipPayment()
.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() {
self._pollVipStatus(outTradeNo, 3, 2000)
},
fail(err) {
if (err.errMsg && err.errMsg.indexOf('cancel') > -1) return
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
console.error('[VIP支付] wx.requestPayment 失败:', err)
}
})
})
.catch(function (err) {
wx.hideLoading()
console.error('[VIP支付] 接口请求失败:', err)
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
})
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() }
})
})
},
/**
* 轮询 VIP 支付状态
* 成功后更新全局 isVip + 触发 vipChange 事件
*/
_pollVipStatus(outTradeNo, retries, interval) {
const self = this
wx.showLoading({ title: '验证中...' })
api.queryPayStatus(outTradeNo)
.then(function (paid) {
if (paid) {
// ✅ VIP 开通确认成功
wx.hideLoading()
// 更新全局状态
app.globalData.isVip = true
app.emit('vipChange', { isVip: true })
self.setData({ isVip: true })
wx.showToast({ title: '🎉 VIP 开通成功!', icon: 'none' })
setTimeout(function () { wx.navigateBack() }, 1500)
} else if (retries > 1) {
// 🔄 尚未到账,等待后重试
setTimeout(function () {
self._pollVipStatus(outTradeNo, retries - 1, interval)
}, interval)
} else {
// ⏳ 重试耗尽,乐观提示
wx.hideLoading()
wx.showModal({
title: '支付处理中',
content: 'VIP 开通已提交,正在确认中,稍后请重新进入查看',
showCancel: false,
success: function () { wx.navigateBack() }
})
}
})
.catch(function (err) {
wx.hideLoading()
console.error('[VIP支付] 查询状态失败:', err)
wx.showModal({
title: '支付处理中',
content: 'VIP 开通已提交,稍后请刷新查看是否生效',
showCancel: false,
success: function () { wx.navigateBack() }
})
})
},
goBack() {
wx.navigateBack()
}
})