feat: 海报优化+小程序码生成API

1. 阅读页&推广中心海报去掉邀请码
2. 新增小程序码生成API(带推荐人ID参数)
3. 海报使用真实小程序码(扫码绑定推荐关系)
4. 修复章节数据库同步
This commit is contained in:
卡若
2026-01-25 21:04:31 +08:00
parent dbfbf65164
commit afa8c59376
3 changed files with 204 additions and 33 deletions

View File

@@ -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 }
)
}
}

View File

@@ -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 = []

View File

@@ -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 })