2026-01-25 21:04:31 +08:00
|
|
|
|
// app/api/miniprogram/qrcode/route.ts
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 生成带参数的小程序码 - 绑定推荐人ID和章节ID
|
2026-01-25 21:04:31 +08:00
|
|
|
|
|
|
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
|
|
|
|
|
|
|
|
const APPID = process.env.WECHAT_APPID || 'wxb8bbb2b10dec74aa'
|
2026-01-29 16:17:56 +08:00
|
|
|
|
const APPSECRET = process.env.WECHAT_APPSECRET || '3c1fb1f63e6e052222bbcead9d07fe0c'
|
2026-01-25 21:04:31 +08:00
|
|
|
|
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 简单的内存缓存
|
|
|
|
|
|
let cachedToken: { token: string; expireAt: number } | null = null
|
|
|
|
|
|
|
|
|
|
|
|
// 获取access_token(带缓存)
|
2026-01-25 21:04:31 +08:00
|
|
|
|
async function getAccessToken() {
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 检查缓存
|
|
|
|
|
|
if (cachedToken && cachedToken.expireAt > Date.now()) {
|
|
|
|
|
|
return cachedToken.token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-25 21:04:31 +08:00
|
|
|
|
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) {
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 缓存token,提前5分钟过期
|
|
|
|
|
|
cachedToken = {
|
|
|
|
|
|
token: data.access_token,
|
|
|
|
|
|
expireAt: Date.now() + (data.expires_in - 300) * 1000
|
|
|
|
|
|
}
|
2026-01-25 21:04:31 +08:00
|
|
|
|
return data.access_token
|
|
|
|
|
|
}
|
|
|
|
|
|
throw new Error(data.errmsg || '获取access_token失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function POST(req: NextRequest) {
|
|
|
|
|
|
try {
|
2026-01-29 17:15:00 +08:00
|
|
|
|
const body = await req.json()
|
|
|
|
|
|
const { scene, page, width = 280, chapterId, userId } = body
|
2026-01-25 21:04:31 +08:00
|
|
|
|
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 构建scene参数
|
|
|
|
|
|
// 格式:ref=用户ID&ch=章节ID(用于分享海报)
|
|
|
|
|
|
let finalScene = scene
|
|
|
|
|
|
if (!finalScene) {
|
|
|
|
|
|
const parts = []
|
|
|
|
|
|
if (userId) parts.push(`ref=${userId.slice(0, 15)}`)
|
|
|
|
|
|
if (chapterId) parts.push(`ch=${chapterId}`)
|
|
|
|
|
|
finalScene = parts.join('&') || 'soul'
|
2026-01-25 21:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-29 17:15:00 +08:00
|
|
|
|
console.log('[QRCode] 生成小程序码, scene:', finalScene)
|
|
|
|
|
|
|
2026-01-25 21:04:31 +08:00
|
|
|
|
// 获取access_token
|
|
|
|
|
|
const accessToken = await getAccessToken()
|
|
|
|
|
|
|
2026-01-29 17:15:00 +08:00
|
|
|
|
// 生成小程序码(使用无限制生成接口)
|
2026-01-25 21:04:31 +08:00
|
|
|
|
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({
|
2026-01-29 17:15:00 +08:00
|
|
|
|
scene: finalScene.slice(0, 32), // 最多32个字符
|
2026-01-25 21:04:31 +08:00
|
|
|
|
page: page || 'pages/index/index',
|
2026-01-29 17:15:00 +08:00
|
|
|
|
width: Math.min(width, 430), // 最大430
|
2026-01-25 21:04:31 +08:00
|
|
|
|
auto_color: false,
|
2026-01-29 17:15:00 +08:00
|
|
|
|
line_color: { r: 0, g: 206, b: 209 }, // 品牌色
|
|
|
|
|
|
is_hyaline: false,
|
|
|
|
|
|
env_version: 'trial' // 体验版,正式发布后改为 release
|
2026-01-25 21:04:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 检查响应类型
|
|
|
|
|
|
const contentType = qrcodeRes.headers.get('content-type')
|
|
|
|
|
|
|
|
|
|
|
|
if (contentType?.includes('application/json')) {
|
|
|
|
|
|
// 返回了错误信息
|
|
|
|
|
|
const errorData = await qrcodeRes.json()
|
|
|
|
|
|
console.error('[QRCode] 生成失败:', errorData)
|
|
|
|
|
|
return NextResponse.json({
|
2026-01-29 17:15:00 +08:00
|
|
|
|
success: false,
|
2026-01-25 21:04:31 +08:00
|
|
|
|
error: errorData.errmsg || '生成小程序码失败',
|
|
|
|
|
|
errcode: errorData.errcode
|
2026-01-29 17:15:00 +08:00
|
|
|
|
}, { status: 200 }) // 返回200但success为false
|
2026-01-25 21:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回图片
|
|
|
|
|
|
const imageBuffer = await qrcodeRes.arrayBuffer()
|
2026-01-29 17:15:00 +08:00
|
|
|
|
|
|
|
|
|
|
if (imageBuffer.byteLength < 1000) {
|
|
|
|
|
|
// 图片太小,可能是错误
|
|
|
|
|
|
console.error('[QRCode] 返回的图片太小:', imageBuffer.byteLength)
|
|
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '生成的小程序码无效'
|
|
|
|
|
|
}, { status: 200 })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-25 21:04:31 +08:00
|
|
|
|
const base64 = Buffer.from(imageBuffer).toString('base64')
|
|
|
|
|
|
|
2026-01-29 17:15:00 +08:00
|
|
|
|
console.log('[QRCode] 生成成功,图片大小:', base64.length, '字符')
|
|
|
|
|
|
|
2026-01-25 21:04:31 +08:00
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
success: true,
|
2026-01-29 17:15:00 +08:00
|
|
|
|
image: `data:image/png;base64,${base64}`,
|
|
|
|
|
|
scene: finalScene
|
2026-01-25 21:04:31 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[QRCode] Error:', error)
|
2026-01-29 17:15:00 +08:00
|
|
|
|
return NextResponse.json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: '生成小程序码失败: ' + String(error)
|
|
|
|
|
|
}, { status: 200 })
|
2026-01-25 21:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|