diff --git a/app/api/miniprogram/qrcode/route.ts b/app/api/miniprogram/qrcode/route.ts new file mode 100644 index 0000000..eed0e6a --- /dev/null +++ b/app/api/miniprogram/qrcode/route.ts @@ -0,0 +1,77 @@ +// app/api/miniprogram/qrcode/route.ts +// 生成带参数的小程序码 - 绑定推荐人ID + +import { NextRequest, NextResponse } from 'next/server' + +const APPID = process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa' +const APPSECRET = process.env.WECHAT_APPSECRET || '25b7e7fdb7998e5107e242ebb6ddabd0' + +// 获取access_token +async function getAccessToken() { + const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}` + const res = await fetch(url) + const data = await res.json() + + if (data.access_token) { + return data.access_token + } + throw new Error(data.errmsg || '获取access_token失败') +} + +export async function POST(req: NextRequest) { + try { + const { scene, page, width = 280 } = await req.json() + + if (!scene) { + return NextResponse.json({ error: '缺少scene参数' }, { status: 400 }) + } + + // 获取access_token + const accessToken = await getAccessToken() + + // 生成小程序码 + const qrcodeUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}` + + const qrcodeRes = await fetch(qrcodeUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + scene: scene.slice(0, 32), // 最多32个字符 + page: page || 'pages/index/index', + width, + auto_color: false, + line_color: { r: 0, g: 206, b: 209 }, + is_hyaline: false + }) + }) + + // 检查响应类型 + const contentType = qrcodeRes.headers.get('content-type') + + if (contentType?.includes('application/json')) { + // 返回了错误信息 + const errorData = await qrcodeRes.json() + console.error('[QRCode] 生成失败:', errorData) + return NextResponse.json({ + error: errorData.errmsg || '生成小程序码失败', + errcode: errorData.errcode + }, { status: 500 }) + } + + // 返回图片 + const imageBuffer = await qrcodeRes.arrayBuffer() + const base64 = Buffer.from(imageBuffer).toString('base64') + + return NextResponse.json({ + success: true, + image: `data:image/png;base64,${base64}` + }) + + } catch (error) { + console.error('[QRCode] Error:', error) + return NextResponse.json( + { error: '生成小程序码失败' }, + { status: 500 } + ) + } +} diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 7f4bb45..cc698aa 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -559,9 +559,24 @@ Page({ try { const ctx = wx.createCanvasContext('posterCanvas', this) - const { section, contentParagraphs } = this.data + const { section, contentParagraphs, sectionId } = this.data const userInfo = app.globalData.userInfo - const referralCode = userInfo?.referralCode || 'SOUL' + 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 } + }) + if (qrRes.success && qrRes.image) { + qrcodeImage = qrRes.image + } + } catch (e) { + console.log('[Poster] 获取小程序码失败,使用占位符') + } // 海报尺寸 300x450 const width = 300 @@ -614,27 +629,46 @@ Page({ // 底部区域背景 ctx.setFillStyle('rgba(0,206,209,0.1)') - ctx.fillRect(0, height - 120, width, 120) + ctx.fillRect(0, height - 100, width, 100) - // 小程序码占位(实际需要获取小程序码图片) + // 左侧提示文字 ctx.setFillStyle('#ffffff') - ctx.beginPath() - ctx.arc(width - 55, height - 60, 35, 0, Math.PI * 2) - ctx.fill() - ctx.setFillStyle('#00CED1') - ctx.setFontSize(10) - ctx.fillText('扫码阅读', width - 72, height - 58) - - // 邀请信息 - ctx.setFillStyle('#ffffff') - ctx.setFontSize(12) - ctx.fillText('长按识别 · 阅读全文', 20, height - 70) - ctx.setFillStyle('#FFD700') - ctx.setFontSize(11) - ctx.fillText(`邀请码: ${referralCode}`, 20, height - 50) + ctx.setFontSize(13) + ctx.fillText('长按识别小程序码', 20, height - 60) ctx.setFillStyle('rgba(255,255,255,0.6)') - ctx.setFontSize(10) - ctx.fillText('好友购买你获90%收益', 20, height - 32) + ctx.setFontSize(11) + ctx.fillText('阅读全文 · 好友购买你获90%收益', 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() @@ -648,6 +682,18 @@ Page({ } }, + // 绘制小程序码占位符 + drawQRPlaceholder(ctx, width, height) { + ctx.setFillStyle('#ffffff') + ctx.beginPath() + ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2) + ctx.fill() + ctx.setFillStyle('#00CED1') + ctx.setFontSize(9) + ctx.fillText('扫码', width - 57, height - 52) + ctx.fillText('阅读', width - 57, height - 40) + }, + // 文字换行处理 wrapText(ctx, text, maxWidth, fontSize) { const lines = [] diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js index 64f8e60..41728a2 100644 --- a/miniprogram/pages/referral/referral.js +++ b/miniprogram/pages/referral/referral.js @@ -144,7 +144,23 @@ Page({ try { const ctx = wx.createCanvasContext('promoPosterCanvas', this) - const { referralCode, userInfo, earnings, referralCount, distributorShare } = this.data + const { userInfo, earnings, referralCount, distributorShare } = this.data + const userId = userInfo?.id || '' + + // 获取小程序码(带推荐人参数) + let qrcodeImage = null + try { + const scene = userId ? `ref=${userId.slice(0,20)}` : 'ref=soul' + const qrRes = await app.request('/api/miniprogram/qrcode', { + method: 'POST', + data: { scene, page: 'pages/index/index', width: 280 } + }) + if (qrRes.success && qrRes.image) { + qrcodeImage = qrRes.image + } + } catch (e) { + console.log('[Poster] 获取小程序码失败,使用占位符') + } // 海报尺寸 300x450 const width = 300 @@ -206,23 +222,43 @@ Page({ ctx.setFillStyle('rgba(0,206,209,0.1)') ctx.fillRect(0, height - 80, width, 80) - // 小程序码占位 - ctx.setFillStyle('#ffffff') - ctx.beginPath() - ctx.arc(width - 55, height - 40, 30, 0, Math.PI * 2) - ctx.fill() - ctx.setFillStyle('#00CED1') - ctx.setFontSize(9) - ctx.fillText('扫码', width - 62, height - 42) - ctx.fillText('购买', width - 62, height - 30) - // 底部提示 ctx.setFillStyle('#ffffff') ctx.setFontSize(13) - ctx.fillText('长按识别 立即购买', 20, height - 45) + ctx.fillText('长按识别 立即购买', 20, height - 50) ctx.setFillStyle('rgba(255,255,255,0.6)') ctx.setFontSize(11) - ctx.fillText(`推广返利 ${distributorShare}%`, 20, height - 22) + ctx.fillText(`推广返利 ${distributorShare}%`, 20, height - 28) + + // 绘制小程序码 + const drawQRCode = () => { + return new Promise((resolve) => { + if (qrcodeImage) { + const fs = wx.getFileSystemManager() + const filePath = `${wx.env.USER_DATA_PATH}/qrcode_promo_${Date.now()}.png` + const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '') + + fs.writeFile({ + filePath, + data: base64Data, + encoding: 'base64', + success: () => { + ctx.drawImage(filePath, width - 75, height - 70, 60, 60) + resolve() + }, + fail: () => { + this.drawQRPlaceholder(ctx, width, height) + resolve() + } + }) + } else { + this.drawQRPlaceholder(ctx, width, height) + resolve() + } + }) + } + + await drawQRCode() ctx.draw(true, () => { wx.hideLoading() @@ -236,6 +272,18 @@ Page({ } }, + // 绘制小程序码占位符 + drawQRPlaceholder(ctx, width, height) { + ctx.setFillStyle('#ffffff') + ctx.beginPath() + ctx.arc(width - 45, height - 40, 30, 0, Math.PI * 2) + ctx.fill() + ctx.setFillStyle('#00CED1') + ctx.setFontSize(9) + ctx.fillText('扫码', width - 52, height - 42) + ctx.fillText('购买', width - 52, height - 30) + }, + // 关闭海报弹窗 closePosterModal() { this.setData({ showPosterModal: false })