317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
/**
|
||
* 会员/订阅中心
|
||
*
|
||
* 支持两种入口模式:
|
||
* 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 originalPriceYuan = (cfg.price / 100).toFixed(2)
|
||
var hasDiscount = cfg.discountedPrice > 0 && cfg.discountedPrice < cfg.price
|
||
var payPrice = hasDiscount ? (cfg.discountedPrice / 100).toFixed(2) : originalPriceYuan
|
||
self.setData({
|
||
currentPrice: payPrice,
|
||
vipPrice: payPrice,
|
||
vipOriginalPrice: hasDiscount ? originalPriceYuan : '',
|
||
vipRemark: cfg.remark || ''
|
||
})
|
||
}
|
||
}).catch(function (err) {
|
||
console.error('[VIP] 获取配置失败:', err)
|
||
self.setData({ currentPrice: '--', vipPrice: '--', vipOriginalPrice: '' })
|
||
})
|
||
},
|
||
|
||
onShow() {
|
||
if (this.data.mode === 'vip') {
|
||
this.setData({ isVip: app.globalData.isVip })
|
||
}
|
||
},
|
||
|
||
/** 选择套餐 */
|
||
selectPlan(e) {
|
||
const plan = e.currentTarget.dataset.plan
|
||
let price = this.data.vipPrice || '--'
|
||
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()
|
||
}
|
||
})
|