Files
soul/miniprogram/pages/read/read.js
卡若 14e3993303 feat: 小程序使用真实支付和真实章节内容
1. 更新AppID为wx0976665c3a3d5a7c
2. 添加微信支付商户号配置(1318592501)
3. 阅读页改为从API读取真实章节内容
4. 移除测试支付模式,直接调用真实支付
5. API失败时显示加载提示而非假内容
2026-01-23 22:08:30 +08:00

516 lines
15 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.

/**
* Soul创业实验 - 阅读页
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 章节信息
sectionId: '',
section: null,
partTitle: '',
chapterTitle: '',
// 内容
content: '',
previewContent: '',
contentParagraphs: [],
previewParagraphs: [],
loading: true,
// 用户状态
isLoggedIn: false,
hasFullBook: false,
canAccess: false,
purchasedCount: 0,
// 阅读进度
readingProgress: 0,
showPaywall: false,
// 上一篇/下一篇
prevSection: null,
nextSection: null,
// 价格
sectionPrice: 1,
fullBookPrice: 9.9,
totalSections: 62,
// 弹窗
showShareModal: false,
showLoginModal: false,
isPaying: false,
// 免费章节
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
},
onLoad(options) {
const { id, ref } = options
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight,
sectionId: id
})
// 保存推荐码
if (ref) {
wx.setStorageSync('referral_code', ref)
}
this.initSection(id)
},
onPageScroll(e) {
// 计算阅读进度
const query = wx.createSelectorQuery()
query.select('.page').boundingClientRect()
query.exec((res) => {
if (res[0]) {
const scrollTop = e.scrollTop
const pageHeight = res[0].height - this.data.statusBarHeight - 200
const progress = pageHeight > 0 ? Math.min((scrollTop / pageHeight) * 100, 100) : 0
this.setData({ readingProgress: progress })
}
})
},
// 初始化章节
async initSection(id) {
this.setData({ loading: true })
try {
// 模拟获取章节数据
const section = this.getSectionInfo(id)
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
const isFree = this.data.freeIds.includes(id)
const isPurchased = hasFullBook || (purchasedSections && purchasedSections.includes(id))
const canAccess = isFree || isPurchased
const purchasedCount = purchasedSections?.length || 0
this.setData({
section,
isLoggedIn,
hasFullBook,
canAccess,
purchasedCount,
showPaywall: !canAccess
})
// 加载内容
await this.loadContent(id)
// 获取上一篇/下一篇
this.loadNavigation(id)
} catch (e) {
console.error('初始化章节失败:', e)
wx.showToast({ title: '加载失败', icon: 'none' })
} finally {
this.setData({ loading: false })
}
},
// 获取章节信息
getSectionInfo(id) {
// 特殊章节
if (id === 'preface') {
return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 }
}
if (id === 'epilogue') {
return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 }
}
if (id.startsWith('appendix')) {
const appendixTitles = {
'appendix-1': 'Soul派对房精选对话',
'appendix-2': '创业者自检清单',
'appendix-3': '本书提到的工具和资源'
}
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
}
// 普通章节
return {
id: id,
title: this.getSectionTitle(id),
isFree: id === '1.1',
price: 1
}
},
// 获取章节标题
getSectionTitle(id) {
const titles = {
'1.1': '荷包:电动车出租的被动收入模式',
'1.2': '老墨:资源整合高手的社交方法',
'1.3': '笑声背后的MBTI',
'1.4': '人性的三角结构:利益、情感、价值观',
'1.5': '沟通差的问题:为什么你说的别人听不懂',
'2.1': '相亲故事:你以为找的是人,实际是在找模式',
'2.2': '找工作迷茫者:为什么简历解决不了人生',
'2.3': '撸运费险:小钱困住大脑的真实心理',
'2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力',
'2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒',
'3.1': '3000万流水如何跑出来(退税模式解析)',
'8.1': '流量杠杆:抖音、Soul、飞书',
'9.14': '大健康私域一个月150万的70后'
}
return titles[id] || `章节 ${id}`
},
// 加载内容 - 从真实API读取章节内容
async loadContent(id) {
try {
// 从API获取真实章节内容
const res = await app.request(`/api/book/chapter/${id}`)
if (res && res.content) {
const lines = res.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
this.setData({
content: res.content,
previewContent: lines.slice(0, previewCount).join('\n'),
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount),
partTitle: res.partTitle || '',
chapterTitle: res.chapterTitle || ''
})
console.log('[Read] 成功加载章节内容:', id)
return
}
} catch (e) {
console.error('[Read] API加载章节失败:', e.message)
}
// API失败时显示提示不使用假内容
this.setData({
content: '章节内容加载中,请稍候...',
previewContent: '章节内容加载中,请稍候...',
contentParagraphs: ['章节内容加载中,请稍候...', '如果长时间无法加载,请检查网络连接后刷新页面。'],
previewParagraphs: ['章节内容加载中,请稍候...']
})
// 延迟重试一次
setTimeout(async () => {
try {
const res = await app.request(`/api/book/chapter/${id}`)
if (res && res.content) {
const lines = res.content.split('\n').filter(line => line.trim())
const previewCount = Math.ceil(lines.length * 0.2)
this.setData({
content: res.content,
previewContent: lines.slice(0, previewCount).join('\n'),
contentParagraphs: lines,
previewParagraphs: lines.slice(0, previewCount),
partTitle: res.partTitle || '',
chapterTitle: res.chapterTitle || ''
})
}
} catch (e) {
console.error('[Read] 重试加载失败:', e.message)
}
}, 2000)
},
// 加载导航
loadNavigation(id) {
const sectionOrder = [
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
'2.1', '2.2', '2.3', '2.4', '2.5',
'3.1', '3.2', '3.3', '3.4',
'4.1', '4.2', '4.3', '4.4', '4.5',
'5.1', '5.2', '5.3', '5.4', '5.5',
'6.1', '6.2', '6.3', '6.4',
'7.1', '7.2', '7.3', '7.4', '7.5',
'8.1', '8.2', '8.3', '8.4', '8.5', '8.6',
'9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14',
'10.1', '10.2', '10.3', '10.4',
'11.1', '11.2', '11.3', '11.4', '11.5',
'epilogue'
]
const currentIndex = sectionOrder.indexOf(id)
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
this.setData({
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
})
},
// 返回
goBack() {
wx.navigateBack({
fail: () => wx.switchTab({ url: '/pages/chapters/chapters' })
})
},
// 分享弹窗
showShare() {
this.setData({ showShareModal: true })
},
closeShareModal() {
this.setData({ showShareModal: false })
},
// 复制链接
copyLink() {
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || ''
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
wx.setClipboardData({
data: shareUrl,
success: () => {
wx.showToast({ title: '链接已复制', icon: 'success' })
this.setData({ showShareModal: false })
}
})
},
// 分享到微信
onShareAppMessage() {
const { section, sectionId } = this.data
const userInfo = app.globalData.userInfo
const referralCode = userInfo?.referralCode || ''
return {
title: `📚 ${section?.title || '推荐阅读'}`,
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
}
},
// 显示登录弹窗
showLoginModal() {
this.setData({ showLoginModal: true })
},
closeLoginModal() {
this.setData({ showLoginModal: false })
},
// 微信登录
async handleWechatLogin() {
try {
const result = await app.login()
if (result) {
this.setData({ showLoginModal: false })
this.initSection(this.data.sectionId)
wx.showToast({ title: '登录成功', icon: 'success' })
}
} catch (e) {
wx.showToast({ title: '登录失败', icon: 'none' })
}
},
// 手机号登录
async handlePhoneLogin(e) {
if (!e.detail.code) {
return this.handleWechatLogin()
}
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
this.setData({ showLoginModal: false })
this.initSection(this.data.sectionId)
wx.showToast({ title: '登录成功', icon: 'success' })
}
} catch (e) {
wx.showToast({ title: '登录失败', icon: 'none' })
}
},
// 购买章节 - 直接调起支付
async handlePurchaseSection() {
if (!this.data.isLoggedIn) {
this.setData({ showLoginModal: true })
return
}
await this.processPayment('section', this.data.sectionId, this.data.section.price)
},
// 购买全书 - 直接调起支付
async handlePurchaseFullBook() {
if (!this.data.isLoggedIn) {
this.setData({ showLoginModal: true })
return
}
await this.processPayment('fullbook', null, this.data.fullBookPrice)
},
// 处理支付 - 调用真实微信支付接口
async processPayment(type, sectionId, amount) {
// 检查是否已购买(避免重复购买)
if (type === 'section' && sectionId) {
const purchasedSections = app.globalData.purchasedSections || []
if (purchasedSections.includes(sectionId)) {
wx.showToast({ title: '已购买过此章节', icon: 'none' })
return
}
}
if (type === 'fullbook' && app.globalData.hasFullBook) {
wx.showToast({ title: '已购买全书', icon: 'none' })
return
}
this.setData({ isPaying: true })
try {
// 1. 先获取openId (支付必需)
let openId = app.globalData.openId || wx.getStorageSync('openId')
if (!openId) {
console.log('[Pay] 需要先获取openId尝试静默获取')
openId = await app.getOpenId()
if (!openId) {
// openId获取失败但已登录用户可以使用用户ID替代
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
console.log('[Pay] 使用用户ID作为替代')
openId = app.globalData.userInfo.id
} else {
wx.showModal({
title: '提示',
content: '需要登录后才能支付,请先登录',
showCancel: false
})
this.setData({ showLoginModal: true, isPaying: false })
return
}
}
}
console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' })
// 2. 调用后端创建预支付订单
let paymentData = null
try {
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {
openId,
productType: type,
productId: sectionId,
amount,
description: type === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节-${sectionId}`,
userId: app.globalData.userInfo?.id || ''
}
})
console.log('[Pay] 创建订单响应:', res)
if (res.success && res.data?.payParams) {
paymentData = res.data.payParams
console.log('[Pay] 获取支付参数成功')
} else {
throw new Error(res.error || '创建订单失败')
}
} catch (apiError) {
console.error('[Pay] API创建订单失败:', apiError.message)
wx.showToast({ title: '支付服务暂时不可用,请稍后重试', icon: 'none', duration: 3000 })
this.setData({ isPaying: false })
return
}
// 3. 调用微信支付
console.log('[Pay] 调起微信支付')
await this.callWechatPay(paymentData)
// 4. 支付成功,更新本地数据
this.mockPaymentSuccess(type, sectionId)
wx.showToast({ title: '购买成功', icon: 'success' })
// 5. 刷新页面
this.initSection(this.data.sectionId)
} catch (e) {
console.error('[Pay] 支付失败:', e)
if (e.errMsg && e.errMsg.includes('cancel')) {
wx.showToast({ title: '已取消支付', icon: 'none' })
} else {
wx.showToast({ title: e.errMsg || '支付失败', icon: 'none' })
}
} finally {
this.setData({ isPaying: false })
}
},
// 模拟支付成功
mockPaymentSuccess(type, sectionId) {
if (type === 'fullbook') {
app.globalData.hasFullBook = true
const userInfo = app.globalData.userInfo || {}
userInfo.hasFullBook = true
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
} else if (sectionId) {
const purchasedSections = app.globalData.purchasedSections || []
if (!purchasedSections.includes(sectionId)) {
purchasedSections.push(sectionId)
app.globalData.purchasedSections = purchasedSections
const userInfo = app.globalData.userInfo || {}
userInfo.purchasedSections = purchasedSections
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
}
}
},
// 调用微信支付
callWechatPay(paymentData) {
return new Promise((resolve, reject) => {
wx.requestPayment({
timeStamp: paymentData.timeStamp,
nonceStr: paymentData.nonceStr,
package: paymentData.package,
signType: paymentData.signType || 'MD5',
paySign: paymentData.paySign,
success: resolve,
fail: reject
})
})
},
// 跳转到上一篇
goToPrev() {
if (this.data.prevSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
}
},
// 跳转到下一篇
goToNext() {
if (this.data.nextSection) {
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
}
},
// 跳转到推广中心
goToReferral() {
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 阻止冒泡
stopPropagation() {}
})