优化小程序推荐码处理逻辑,支持通过扫码场景解析推荐码和初始章节ID。新增获取用户邀请码的功能以便于分享。更新分享配置,确保分享时自动带上推荐码。调整部分页面逻辑以提升用户体验。
This commit is contained in:
@@ -66,12 +66,18 @@ Page({
|
||||
isGeneratingPoster: false,
|
||||
|
||||
// 免费章节
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'],
|
||||
|
||||
// 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用)
|
||||
shareImagePath: ''
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
const { id, ref } = options
|
||||
|
||||
// 扫码进入时 id 可能在 app.globalData.initialSectionId(scene 里 id=1.1 由 app 解析)
|
||||
let id = options.id || app.globalData.initialSectionId
|
||||
if (app.globalData.initialSectionId) delete app.globalData.initialSectionId
|
||||
const ref = options.ref
|
||||
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight,
|
||||
@@ -268,10 +274,11 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 5000)
|
||||
if (res && res.content) {
|
||||
this.setData({ section: this.getSectionInfo(id) })
|
||||
this.setChapterContent(res)
|
||||
// 成功后缓存到本地
|
||||
wx.setStorageSync(cacheKey, res)
|
||||
console.log('[Read] 从API加载成功:', id)
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -282,10 +289,11 @@ Page({
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
this.setData({ section: this.getSectionInfo(id) })
|
||||
this.setChapterContent(cached)
|
||||
console.log('[Read] 从本地缓存加载成功:', id)
|
||||
// 后台静默刷新
|
||||
this.silentRefresh(id)
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -363,9 +371,11 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setData({ section: this.getSectionInfo(id) })
|
||||
this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
setTimeout(() => this.drawShareCard(), 600)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -454,33 +464,91 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
|
||||
|
||||
// 分享标题优化
|
||||
const shareTitle = section?.title
|
||||
// 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用
|
||||
drawShareCard() {
|
||||
const { section, sectionId, contentParagraphs } = this.data
|
||||
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
|
||||
const raw = (contentParagraphs && contentParagraphs.length)
|
||||
? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim()
|
||||
: ''
|
||||
const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事')
|
||||
const ctx = wx.createCanvasContext('shareCardCanvas', this)
|
||||
const w = 500
|
||||
const h = 400
|
||||
// 白底
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
// 顶部:平台名
|
||||
ctx.setFillStyle('#333333')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36)
|
||||
// 深色内容区(模拟参考图效果)
|
||||
const boxX = 24
|
||||
const boxY = 52
|
||||
const boxW = w - 48
|
||||
const boxH = 300
|
||||
ctx.setFillStyle('#2c2c2e')
|
||||
ctx.fillRect(boxX, boxY, boxW, boxH)
|
||||
// 文章标题(白字)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(15)
|
||||
const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15)
|
||||
let y = boxY + 28
|
||||
titleLines.slice(0, 2).forEach(line => {
|
||||
ctx.fillText(line, boxX + 16, y)
|
||||
y += 22
|
||||
})
|
||||
y += 8
|
||||
// 正文摘要(浅灰)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.88)')
|
||||
ctx.setFontSize(12)
|
||||
const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12)
|
||||
excerptLines.slice(0, 8).forEach(line => {
|
||||
ctx.fillText(line, boxX + 16, y)
|
||||
y += 20
|
||||
})
|
||||
// 底部:小程序标识
|
||||
ctx.setFillStyle('#999999')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('小程序', 24, h - 16)
|
||||
ctx.draw(false, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'shareCardCanvas',
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
this.setData({ shareImagePath: res.tempFilePath })
|
||||
}
|
||||
}, this)
|
||||
})
|
||||
},
|
||||
|
||||
// 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用)
|
||||
getShareConfig() {
|
||||
const { section, sectionId, shareImagePath } = this.data
|
||||
const ref = app.getMyReferralCode()
|
||||
const shareTitle = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 Soul创业派对 - 真实商业故事'
|
||||
|
||||
const path = ref
|
||||
? `/pages/read/read?id=${sectionId}&ref=${ref}`
|
||||
: `/pages/read/read?id=${sectionId}`
|
||||
return {
|
||||
title: shareTitle,
|
||||
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
|
||||
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
|
||||
path,
|
||||
imageUrl: shareImagePath || undefined
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
|
||||
onShareAppMessage() {
|
||||
return this.getShareConfig()
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
|
||||
query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
|
||||
query: ref ? `id=${sectionId}&ref=${ref}` : `id=${sectionId}`
|
||||
}
|
||||
},
|
||||
|
||||
@@ -734,7 +802,8 @@ Page({
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
paymentData = res.data.payParams
|
||||
console.log('[Pay] 获取支付参数成功:', paymentData)
|
||||
paymentData._orderSn = res.data.orderSn
|
||||
console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn)
|
||||
} else {
|
||||
throw new Error(res.error || res.message || '创建订单失败')
|
||||
}
|
||||
@@ -767,11 +836,12 @@ Page({
|
||||
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
|
||||
|
||||
try {
|
||||
const orderSn = paymentData._orderSn
|
||||
await this.callWechatPay(paymentData)
|
||||
|
||||
// 4. 【标准流程】支付成功后刷新权限并解锁内容
|
||||
// 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机)
|
||||
console.log('[Pay] 微信支付成功!')
|
||||
await this.onPaymentSuccess()
|
||||
await this.onPaymentSuccess(orderSn)
|
||||
|
||||
} catch (payErr) {
|
||||
console.error('[Pay] 微信支付调起失败:', payErr)
|
||||
@@ -807,13 +877,34 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 轮询订单状态,确认 paid 后刷新权限并解锁
|
||||
async pollOrderUntilPaid(orderSn) {
|
||||
const maxAttempts = 15
|
||||
const interval = 800
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true })
|
||||
if (r?.data?.status === 'paid') return true
|
||||
} catch (_) {}
|
||||
if (i < maxAttempts - 1) await this.sleep(interval)
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 【新增】支付成功后的标准处理流程
|
||||
async onPaymentSuccess() {
|
||||
async onPaymentSuccess(orderSn) {
|
||||
wx.showLoading({ title: '确认购买中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 等待服务端处理支付回调(1-2秒)
|
||||
await this.sleep(2000)
|
||||
// 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify)
|
||||
if (orderSn) {
|
||||
const paid = await this.pollOrderUntilPaid(orderSn)
|
||||
if (!paid) {
|
||||
console.warn('[Pay] 轮询超时,仍尝试刷新')
|
||||
}
|
||||
} else {
|
||||
await this.sleep(1500)
|
||||
}
|
||||
|
||||
// 2. 刷新用户购买状态
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
@@ -936,134 +1027,115 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报
|
||||
// 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
const { section, contentParagraphs, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
const safeParagraphs = contentParagraphs || []
|
||||
|
||||
// 通过 GET 接口下载二维码图片,得到 tempFilePath 便于开发工具与真机统一用 drawImage 绘制
|
||||
let qrcodeTempPath = null
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数)
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
|
||||
const qrRes = await app.request('/api/miniprogram/qrcode', {
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0, 10)}` : `id=${sectionId}`
|
||||
const baseUrl = app.globalData.baseUrl || ''
|
||||
const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280`
|
||||
qrcodeTempPath = await new Promise((resolve) => {
|
||||
wx.downloadFile({
|
||||
url,
|
||||
success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null),
|
||||
fail: () => resolve(null)
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
// 内容摘要
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
y += 30
|
||||
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
|
||||
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
// 底部区域背景
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
// 绘制小程序码或占位符
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeImage) {
|
||||
// 下载base64图片并绘制
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
}
|
||||
|
||||
const doDraw = () => {
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
y += 30
|
||||
const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150)
|
||||
const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事'
|
||||
const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeTempPath) {
|
||||
ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70)
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
drawQRCode().then(() => {
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(doDraw, 400)
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
@@ -1101,11 +1173,20 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
// 保存海报到相册(与海报绘制尺寸一致,必须传 destWidth/destHeight 否则部分机型导出失败)
|
||||
savePoster() {
|
||||
const width = 300
|
||||
const height = 450
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
destWidth: width,
|
||||
destHeight: height,
|
||||
fileType: 'png',
|
||||
success: (res) => {
|
||||
if (!res.tempFilePath) {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
@@ -1113,25 +1194,25 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
console.error('[savePoster] saveImageToPhotosAlbum fail:', err)
|
||||
if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
success: (sres) => {
|
||||
if (sres.confirm) wx.openSetting()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
fail: (err) => {
|
||||
console.error('[savePoster] canvasToTempFilePath fail:', err)
|
||||
wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user