feat: 小程序真实微信支付功能
新增: 1. /api/miniprogram/pay - 创建预支付订单API 2. /api/miniprogram/pay/notify - 支付回调处理 3. /api/miniprogram/login - 小程序登录获取openId 配置: - 小程序AppID: wxb8bbb2b10dec74aa - 商户号: 1318592501 - 回调地址: https://soul.cunbao.net/api/miniprogram/pay/notify 更新: - app.js: 添加getOpenId方法,支付前获取openId - read.js: processPayment调用真实支付接口 - 支持API不可用时回退到测试模式
This commit is contained in:
103
app/api/miniprogram/login/route.ts
Normal file
103
app/api/miniprogram/login/route.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* 小程序登录API
|
||||||
|
* 使用code换取openId和session_key
|
||||||
|
*
|
||||||
|
* 小程序配置:
|
||||||
|
* - AppID: wxb8bbb2b10dec74aa
|
||||||
|
* - AppSecret: 85d3fa31584d06acdb1de4a597d25b7b
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
const MINIPROGRAM_CONFIG = {
|
||||||
|
appId: 'wxb8bbb2b10dec74aa',
|
||||||
|
appSecret: '85d3fa31584d06acdb1de4a597d25b7b',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST - 小程序登录,获取openId
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { code } = body
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少登录code'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[MiniLogin] 收到登录请求, code:', code.slice(0, 10) + '...')
|
||||||
|
|
||||||
|
// 调用微信接口获取openId
|
||||||
|
const wxUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${MINIPROGRAM_CONFIG.appId}&secret=${MINIPROGRAM_CONFIG.appSecret}&js_code=${code}&grant_type=authorization_code`
|
||||||
|
|
||||||
|
const response = await fetch(wxUrl)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
console.log('[MiniLogin] 微信接口返回:', {
|
||||||
|
errcode: data.errcode,
|
||||||
|
errmsg: data.errmsg,
|
||||||
|
hasOpenId: !!data.openid,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.errcode) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `微信登录失败: ${data.errmsg || data.errcode}`
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const openId = data.openid
|
||||||
|
const sessionKey = data.session_key
|
||||||
|
const unionId = data.unionid
|
||||||
|
|
||||||
|
if (!openId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '获取openId失败'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建或更新用户
|
||||||
|
// TODO: 这里应该连接数据库操作
|
||||||
|
const user = {
|
||||||
|
id: `user_${openId.slice(-8)}`,
|
||||||
|
openId,
|
||||||
|
nickname: '微信用户',
|
||||||
|
avatar: '',
|
||||||
|
referralCode: 'SOUL' + Date.now().toString(36).toUpperCase().slice(-6),
|
||||||
|
purchasedSections: [],
|
||||||
|
hasFullBook: false,
|
||||||
|
earnings: 0,
|
||||||
|
pendingEarnings: 0,
|
||||||
|
referralCount: 0,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成token
|
||||||
|
const token = `tk_${openId.slice(-8)}_${Date.now()}`
|
||||||
|
|
||||||
|
console.log('[MiniLogin] 登录成功, userId:', user.id)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
openId,
|
||||||
|
sessionKey, // 注意:生产环境不应返回sessionKey给前端
|
||||||
|
unionId,
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MiniLogin] 登录失败:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '登录失败'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
141
app/api/miniprogram/pay/notify/route.ts
Normal file
141
app/api/miniprogram/pay/notify/route.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* 小程序支付回调通知处理
|
||||||
|
* 微信支付成功后会调用此接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
const WECHAT_PAY_CONFIG = {
|
||||||
|
appId: 'wxb8bbb2b10dec74aa',
|
||||||
|
mchId: '1318592501',
|
||||||
|
mchKey: 'wx3e31b068be59ddc131b068be59ddc2',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
function generateSign(params: Record<string, string>, key: string): string {
|
||||||
|
const sortedKeys = Object.keys(params).sort()
|
||||||
|
const signString = sortedKeys
|
||||||
|
.filter((k) => params[k] && k !== 'sign')
|
||||||
|
.map((k) => `${k}=${params[k]}`)
|
||||||
|
.join('&')
|
||||||
|
|
||||||
|
const signWithKey = `${signString}&key=${key}`
|
||||||
|
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
function verifySign(params: Record<string, string>, key: string): boolean {
|
||||||
|
const receivedSign = params.sign
|
||||||
|
if (!receivedSign) return false
|
||||||
|
|
||||||
|
const data = { ...params }
|
||||||
|
delete data.sign
|
||||||
|
|
||||||
|
const calculatedSign = generateSign(data, key)
|
||||||
|
return receivedSign === calculatedSign
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML转对象
|
||||||
|
function xmlToDict(xml: string): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
||||||
|
let match
|
||||||
|
while ((match = regex.exec(xml)) !== null) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g
|
||||||
|
while ((match = simpleRegex.exec(xml)) !== null) {
|
||||||
|
if (!result[match[1]]) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应
|
||||||
|
const SUCCESS_RESPONSE = `<xml>
|
||||||
|
<return_code><![CDATA[SUCCESS]]></return_code>
|
||||||
|
<return_msg><![CDATA[OK]]></return_msg>
|
||||||
|
</xml>`
|
||||||
|
|
||||||
|
// 失败响应
|
||||||
|
const FAIL_RESPONSE = `<xml>
|
||||||
|
<return_code><![CDATA[FAIL]]></return_code>
|
||||||
|
<return_msg><![CDATA[ERROR]]></return_msg>
|
||||||
|
</xml>`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST - 接收微信支付回调
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const xmlData = await request.text()
|
||||||
|
console.log('[PayNotify] 收到支付回调')
|
||||||
|
|
||||||
|
const data = xmlToDict(xmlData)
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if (!verifySign(data, WECHAT_PAY_CONFIG.mchKey)) {
|
||||||
|
console.error('[PayNotify] 签名验证失败')
|
||||||
|
return new Response(FAIL_RESPONSE, {
|
||||||
|
headers: { 'Content-Type': 'application/xml' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查支付结果
|
||||||
|
if (data.return_code !== 'SUCCESS' || data.result_code !== 'SUCCESS') {
|
||||||
|
console.log('[PayNotify] 支付未成功:', data.err_code, data.err_code_des)
|
||||||
|
return new Response(SUCCESS_RESPONSE, {
|
||||||
|
headers: { 'Content-Type': 'application/xml' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderSn = data.out_trade_no
|
||||||
|
const transactionId = data.transaction_id
|
||||||
|
const totalFee = parseInt(data.total_fee || '0', 10)
|
||||||
|
const openId = data.openid
|
||||||
|
|
||||||
|
console.log('[PayNotify] 支付成功:', {
|
||||||
|
orderSn,
|
||||||
|
transactionId,
|
||||||
|
totalFee,
|
||||||
|
openId: openId?.slice(0, 10) + '...',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 解析附加数据
|
||||||
|
let attach: Record<string, string> = {}
|
||||||
|
if (data.attach) {
|
||||||
|
try {
|
||||||
|
attach = JSON.parse(data.attach)
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { productType, productId, userId } = attach
|
||||||
|
|
||||||
|
// TODO: 这里应该更新数据库中的订单状态
|
||||||
|
// 1. 更新订单状态为已支付
|
||||||
|
// 2. 如果是章节购买,将章节添加到用户已购列表
|
||||||
|
// 3. 如果是全书购买,更新用户为全书用户
|
||||||
|
|
||||||
|
console.log('[PayNotify] 订单处理完成:', {
|
||||||
|
orderSn,
|
||||||
|
productType,
|
||||||
|
productId,
|
||||||
|
userId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回成功响应给微信
|
||||||
|
return new Response(SUCCESS_RESPONSE, {
|
||||||
|
headers: { 'Content-Type': 'application/xml' }
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PayNotify] 处理回调失败:', error)
|
||||||
|
return new Response(FAIL_RESPONSE, {
|
||||||
|
headers: { 'Content-Type': 'application/xml' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
279
app/api/miniprogram/pay/route.ts
Normal file
279
app/api/miniprogram/pay/route.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* 小程序支付API
|
||||||
|
* 对接真实微信支付接口
|
||||||
|
*
|
||||||
|
* 配置来源: 用户规则
|
||||||
|
* - 小程序AppID: wxb8bbb2b10dec74aa
|
||||||
|
* - 商户号: 1318592501
|
||||||
|
* - API密钥: wx3e31b068be59ddc131b068be59ddc2
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
// 微信支付配置
|
||||||
|
const WECHAT_PAY_CONFIG = {
|
||||||
|
appId: 'wxb8bbb2b10dec74aa', // 小程序AppID
|
||||||
|
appSecret: '85d3fa31584d06acdb1de4a597d25b7b', // 小程序AppSecret
|
||||||
|
mchId: '1318592501', // 商户号
|
||||||
|
mchKey: 'wx3e31b068be59ddc131b068be59ddc2', // API密钥
|
||||||
|
notifyUrl: 'https://soul.cunbao.net/api/miniprogram/pay/notify', // 支付回调地址
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
function generateNonceStr(): string {
|
||||||
|
return crypto.randomBytes(16).toString('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
function generateSign(params: Record<string, string>, key: string): string {
|
||||||
|
const sortedKeys = Object.keys(params).sort()
|
||||||
|
const signString = sortedKeys
|
||||||
|
.filter((k) => params[k] && k !== 'sign')
|
||||||
|
.map((k) => `${k}=${params[k]}`)
|
||||||
|
.join('&')
|
||||||
|
|
||||||
|
const signWithKey = `${signString}&key=${key}`
|
||||||
|
return crypto.createHash('md5').update(signWithKey, 'utf8').digest('hex').toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对象转XML
|
||||||
|
function dictToXml(data: Record<string, string>): string {
|
||||||
|
const xml = ['<xml>']
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
xml.push(`<${key}><![CDATA[${value}]]></${key}>`)
|
||||||
|
}
|
||||||
|
xml.push('</xml>')
|
||||||
|
return xml.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML转对象
|
||||||
|
function xmlToDict(xml: string): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {}
|
||||||
|
const regex = /<(\w+)><!\[CDATA\[(.*?)\]\]><\/\1>/g
|
||||||
|
let match
|
||||||
|
while ((match = regex.exec(xml)) !== null) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
const simpleRegex = /<(\w+)>([^<]*)<\/\1>/g
|
||||||
|
while ((match = simpleRegex.exec(xml)) !== null) {
|
||||||
|
if (!result[match[1]]) {
|
||||||
|
result[match[1]] = match[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成订单号
|
||||||
|
function generateOrderSn(): string {
|
||||||
|
const now = new Date()
|
||||||
|
const timestamp = now.getFullYear().toString() +
|
||||||
|
(now.getMonth() + 1).toString().padStart(2, '0') +
|
||||||
|
now.getDate().toString().padStart(2, '0') +
|
||||||
|
now.getHours().toString().padStart(2, '0') +
|
||||||
|
now.getMinutes().toString().padStart(2, '0') +
|
||||||
|
now.getSeconds().toString().padStart(2, '0')
|
||||||
|
const random = Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
|
||||||
|
return `MP${timestamp}${random}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST - 创建小程序支付订单
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { openId, productType, productId, amount, description } = body
|
||||||
|
|
||||||
|
if (!openId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少openId参数,请先登录'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '支付金额无效'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderSn = generateOrderSn()
|
||||||
|
const totalFee = Math.round(amount * 100) // 转换为分
|
||||||
|
const goodsBody = description || (productType === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节购买-${productId}`)
|
||||||
|
|
||||||
|
// 获取客户端IP
|
||||||
|
const forwarded = request.headers.get('x-forwarded-for')
|
||||||
|
const clientIp = forwarded ? forwarded.split(',')[0] : '127.0.0.1'
|
||||||
|
|
||||||
|
// 构建统一下单参数
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
appid: WECHAT_PAY_CONFIG.appId,
|
||||||
|
mch_id: WECHAT_PAY_CONFIG.mchId,
|
||||||
|
nonce_str: generateNonceStr(),
|
||||||
|
body: goodsBody.slice(0, 128),
|
||||||
|
out_trade_no: orderSn,
|
||||||
|
total_fee: totalFee.toString(),
|
||||||
|
spbill_create_ip: clientIp,
|
||||||
|
notify_url: WECHAT_PAY_CONFIG.notifyUrl,
|
||||||
|
trade_type: 'JSAPI',
|
||||||
|
openid: openId,
|
||||||
|
attach: JSON.stringify({ productType, productId, userId: body.userId || '' }),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成签名
|
||||||
|
params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey)
|
||||||
|
|
||||||
|
console.log('[MiniPay] 创建订单:', {
|
||||||
|
orderSn,
|
||||||
|
totalFee,
|
||||||
|
openId: openId.slice(0, 10) + '...',
|
||||||
|
productType,
|
||||||
|
productId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调用微信统一下单接口
|
||||||
|
const xmlData = dictToXml(params)
|
||||||
|
const response = await fetch('https://api.mch.weixin.qq.com/pay/unifiedorder', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/xml' },
|
||||||
|
body: xmlData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
const result = xmlToDict(responseText)
|
||||||
|
|
||||||
|
console.log('[MiniPay] 微信响应:', {
|
||||||
|
return_code: result.return_code,
|
||||||
|
result_code: result.result_code,
|
||||||
|
err_code: result.err_code,
|
||||||
|
err_code_des: result.err_code_des,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 检查返回结果
|
||||||
|
if (result.return_code !== 'SUCCESS') {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `微信支付请求失败: ${result.return_msg || '未知错误'}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.result_code !== 'SUCCESS') {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `微信支付失败: ${result.err_code_des || result.err_code || '未知错误'}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建小程序支付参数
|
||||||
|
const prepayId = result.prepay_id
|
||||||
|
if (!prepayId) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '微信支付返回数据异常'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString()
|
||||||
|
const nonceStr = generateNonceStr()
|
||||||
|
|
||||||
|
const payParams: Record<string, string> = {
|
||||||
|
appId: WECHAT_PAY_CONFIG.appId,
|
||||||
|
timeStamp: timestamp,
|
||||||
|
nonceStr,
|
||||||
|
package: `prepay_id=${prepayId}`,
|
||||||
|
signType: 'MD5',
|
||||||
|
}
|
||||||
|
payParams.paySign = generateSign(payParams, WECHAT_PAY_CONFIG.mchKey)
|
||||||
|
|
||||||
|
console.log('[MiniPay] 支付参数生成成功:', { orderSn, prepayId: prepayId.slice(0, 20) + '...' })
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
orderSn,
|
||||||
|
prepayId,
|
||||||
|
payParams: {
|
||||||
|
timeStamp: timestamp,
|
||||||
|
nonceStr,
|
||||||
|
package: `prepay_id=${prepayId}`,
|
||||||
|
signType: 'MD5',
|
||||||
|
paySign: payParams.paySign,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MiniPay] 创建订单失败:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '创建支付订单失败'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET - 查询订单状态
|
||||||
|
*/
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const orderSn = searchParams.get('orderSn')
|
||||||
|
|
||||||
|
if (!orderSn) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少订单号'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
appid: WECHAT_PAY_CONFIG.appId,
|
||||||
|
mch_id: WECHAT_PAY_CONFIG.mchId,
|
||||||
|
out_trade_no: orderSn,
|
||||||
|
nonce_str: generateNonceStr(),
|
||||||
|
}
|
||||||
|
params.sign = generateSign(params, WECHAT_PAY_CONFIG.mchKey)
|
||||||
|
|
||||||
|
const xmlData = dictToXml(params)
|
||||||
|
const response = await fetch('https://api.mch.weixin.qq.com/pay/orderquery', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/xml' },
|
||||||
|
body: xmlData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const responseText = await response.text()
|
||||||
|
const result = xmlToDict(responseText)
|
||||||
|
|
||||||
|
if (result.return_code !== 'SUCCESS' || result.result_code !== 'SUCCESS') {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: { status: 'unknown', orderSn }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const tradeState = result.trade_state
|
||||||
|
let status = 'paying'
|
||||||
|
if (tradeState === 'SUCCESS') status = 'paid'
|
||||||
|
else if (['CLOSED', 'REVOKED', 'PAYERROR'].includes(tradeState)) status = 'failed'
|
||||||
|
else if (tradeState === 'REFUND') status = 'refunded'
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
status,
|
||||||
|
orderSn,
|
||||||
|
transactionId: result.transaction_id,
|
||||||
|
totalFee: parseInt(result.total_fee || '0', 10),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MiniPay] 查询订单失败:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '查询订单失败'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,14 @@
|
|||||||
App({
|
App({
|
||||||
globalData: {
|
globalData: {
|
||||||
// API基础地址
|
// API基础地址
|
||||||
baseUrl: 'https://soul.ckb.fit',
|
baseUrl: 'https://soul.cunbao.net',
|
||||||
|
|
||||||
|
// 小程序配置
|
||||||
|
appId: 'wxb8bbb2b10dec74aa',
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
|
openId: null, // 微信openId,支付必需
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
|
|
||||||
// 书籍数据
|
// 书籍数据
|
||||||
@@ -170,7 +174,7 @@ App({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
// 登录方法 - 支持模拟登录回退
|
// 登录方法 - 获取openId用于支付
|
||||||
async login() {
|
async login() {
|
||||||
try {
|
try {
|
||||||
// 获取微信登录code
|
// 获取微信登录code
|
||||||
@@ -181,38 +185,81 @@ App({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('[App] 获取登录code成功')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 尝试发送code到服务器
|
// 发送code到服务器获取openId
|
||||||
const res = await this.request('/api/wechat/login', {
|
const res = await this.request('/api/miniprogram/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { code: loginRes.code }
|
data: { code: loginRes.code }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
|
// 保存openId
|
||||||
|
if (res.data.openId) {
|
||||||
|
this.globalData.openId = res.data.openId
|
||||||
|
wx.setStorageSync('openId', res.data.openId)
|
||||||
|
console.log('[App] 获取openId成功')
|
||||||
|
}
|
||||||
|
|
||||||
// 保存用户信息
|
// 保存用户信息
|
||||||
|
if (res.data.user) {
|
||||||
this.globalData.userInfo = res.data.user
|
this.globalData.userInfo = res.data.user
|
||||||
this.globalData.isLoggedIn = true
|
this.globalData.isLoggedIn = true
|
||||||
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
||||||
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
||||||
|
|
||||||
wx.setStorageSync('userInfo', res.data.user)
|
wx.setStorageSync('userInfo', res.data.user)
|
||||||
wx.setStorageSync('token', res.data.token)
|
wx.setStorageSync('token', res.data.token || '')
|
||||||
|
}
|
||||||
|
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.log('API登录失败,使用模拟登录:', apiError)
|
console.log('[App] API登录失败,使用模拟登录:', apiError.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// API不可用时使用模拟登录
|
// API不可用时使用模拟登录
|
||||||
return this.mockLogin()
|
return this.mockLogin()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('登录失败:', e)
|
console.error('[App] 登录失败:', e)
|
||||||
// 最后尝试模拟登录
|
// 最后尝试模拟登录
|
||||||
return this.mockLogin()
|
return this.mockLogin()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取openId (支付必需)
|
||||||
|
async getOpenId() {
|
||||||
|
// 先检查缓存
|
||||||
|
const cachedOpenId = wx.getStorageSync('openId')
|
||||||
|
if (cachedOpenId) {
|
||||||
|
this.globalData.openId = cachedOpenId
|
||||||
|
return cachedOpenId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有缓存则登录获取
|
||||||
|
try {
|
||||||
|
const loginRes = await new Promise((resolve, reject) => {
|
||||||
|
wx.login({ success: resolve, fail: reject })
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await this.request('/api/miniprogram/login', {
|
||||||
|
method: 'POST',
|
||||||
|
data: { code: loginRes.code }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.success && res.data?.openId) {
|
||||||
|
this.globalData.openId = res.data.openId
|
||||||
|
wx.setStorageSync('openId', res.data.openId)
|
||||||
|
return res.data.openId
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[App] 获取openId失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
|
||||||
// 模拟登录(后端不可用时使用)
|
// 模拟登录(后端不可用时使用)
|
||||||
mockLogin() {
|
mockLogin() {
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
|
|||||||
@@ -361,65 +361,96 @@ ${id === 'preface' || id === 'epilogue' || id.startsWith('appendix') || id === '
|
|||||||
await this.processPayment('fullbook', null, this.data.fullBookPrice)
|
await this.processPayment('fullbook', null, this.data.fullBookPrice)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 处理支付
|
// 处理支付 - 调用真实微信支付接口
|
||||||
async processPayment(type, sectionId, amount) {
|
async processPayment(type, sectionId, amount) {
|
||||||
this.setData({ isPaying: true })
|
this.setData({ isPaying: true })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建订单
|
// 1. 先获取openId (支付必需)
|
||||||
|
let openId = app.globalData.openId || wx.getStorageSync('openId')
|
||||||
|
|
||||||
|
if (!openId) {
|
||||||
|
console.log('[Pay] 需要先获取openId')
|
||||||
|
openId = await app.getOpenId()
|
||||||
|
|
||||||
|
if (!openId) {
|
||||||
|
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
|
let paymentData = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await app.request('/api/payment/create-order', {
|
const res = await app.request('/api/miniprogram/pay', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { type, sectionId, amount }
|
data: {
|
||||||
|
openId,
|
||||||
|
productType: type,
|
||||||
|
productId: sectionId,
|
||||||
|
amount,
|
||||||
|
description: type === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节-${sectionId}`,
|
||||||
|
userId: app.globalData.userInfo?.id || ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.success && res.data) {
|
console.log('[Pay] 创建订单响应:', res)
|
||||||
paymentData = res.data
|
|
||||||
|
if (res.success && res.data?.payParams) {
|
||||||
|
paymentData = res.data.payParams
|
||||||
|
console.log('[Pay] 获取支付参数成功')
|
||||||
|
} else {
|
||||||
|
throw new Error(res.error || '创建订单失败')
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
console.log('API创建订单失败,使用模拟支付')
|
console.log('[Pay] API创建订单失败:', apiError.message)
|
||||||
}
|
|
||||||
|
|
||||||
// 如果API不可用,使用模拟支付
|
// 开发环境:API不可用时使用模拟支付
|
||||||
if (!paymentData) {
|
const useMock = await new Promise((resolve) => {
|
||||||
paymentData = {
|
|
||||||
isMock: true,
|
|
||||||
orderId: 'mock_' + Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用微信支付或模拟支付
|
|
||||||
if (paymentData.isMock) {
|
|
||||||
// 模拟支付确认
|
|
||||||
const confirmRes = await new Promise((resolve) => {
|
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '确认支付',
|
title: '支付服务暂不可用',
|
||||||
content: `支付 ¥${amount} 购买${type === 'section' ? '本章' : '全书'}?\n(测试环境模拟支付)`,
|
content: '是否使用测试模式完成购买?',
|
||||||
|
confirmText: '测试购买',
|
||||||
|
cancelText: '取消',
|
||||||
success: (res) => resolve(res.confirm)
|
success: (res) => resolve(res.confirm)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (confirmRes) {
|
if (useMock) {
|
||||||
// 模拟支付成功,更新本地数据
|
|
||||||
this.mockPaymentSuccess(type, sectionId)
|
this.mockPaymentSuccess(type, sectionId)
|
||||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||||
|
this.initSection(this.data.sectionId)
|
||||||
}
|
}
|
||||||
} else {
|
this.setData({ isPaying: false })
|
||||||
// 真实微信支付
|
return
|
||||||
await this.callWechatPay(paymentData)
|
|
||||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新页面
|
// 3. 调用微信支付
|
||||||
|
console.log('[Pay] 调起微信支付')
|
||||||
|
await this.callWechatPay(paymentData)
|
||||||
|
|
||||||
|
// 4. 支付成功,更新本地数据
|
||||||
|
this.mockPaymentSuccess(type, sectionId)
|
||||||
|
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||||
|
|
||||||
|
// 5. 刷新页面
|
||||||
this.initSection(this.data.sectionId)
|
this.initSection(this.data.sectionId)
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('[Pay] 支付失败:', e)
|
||||||
|
|
||||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||||
wx.showToast({ title: '已取消支付', icon: 'none' })
|
wx.showToast({ title: '已取消支付', icon: 'none' })
|
||||||
} else {
|
} else {
|
||||||
wx.showToast({ title: '支付失败', icon: 'none' })
|
wx.showToast({ title: e.errMsg || '支付失败', icon: 'none' })
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.setData({ isPaying: false })
|
this.setData({ isPaying: false })
|
||||||
|
|||||||
Reference in New Issue
Block a user