- 我在我的小程序里增加充值购买虚拟商品(充值也是一样的),然后这个是虚拟商品,所以选择了虚拟支付,但是支付一直遇到{errMsg: "requestVirtualPayment:fail INVALID_BUY_QUANTITY", errno: -15001, errCode: -15001},这个错误,以下是请求参数:buyQuantity: 1
- currencyType: "CNY"
- env: 1
- goodsPrice: 30
- mode: "short_series_goods"
- offerId: "1450474855"
- orderId: "vp_1769924532122_3"
- outTradeNo: "vp_1769924532122_3"
- paySig: "f5255ef6304f213a861ab6a405d8046541233455ace3ce1565cd056102e7d3df"
- productId: "30602"
- signData: "{"out_trade_no":"vp_1769924532122_3","amount":3000,"product_id":"30602","quantity":1,"env":1,"openid":"oYD-F1w6FMS70RVMbclMoBBGyfNQ"}"
- signature: "fb78287a34e62222a719920647e7ef7cdfa36c6bb4d09a829548696aed843cad"
商品我在后台定义过了,然后最开始是走的充值,也是一直报这个错误,现在改成了购买虚拟商品也是这个错误。以下是代码:
const api = require('../../utils/api')
const defaultAvatar = '/images/logo.png'
/** Hide system-generated email (e.g. wechat_xxx@generated.com) - not meaningful to user */
function isGeneratedEmail(email) {
if (!email || typeof email !== 'string') return true
return /@generated\.com$/i.test(email.trim()) || /^wechat_[^@]*@/i.test(email.trim())
}
Page({
data: {
userNameDisplay: '微信用户(点击告诉我您的昵称)',
hasNickname: false,
userEmail: '',
userAvatar: defaultAvatar,
balance: 0,
balanceText: '0.00',
freeRemaining: 0,
commodities: [],
selectedId: null,
selectedCommodity: null,
agreementChecked: false
},
onLoad() {
this.loadBalance()
this.loadCommodities()
},
onShow() {
this.loadUser()
this.loadBalance()
},
loadBalance() {
// GET /user/balance 一次返回 balance + freeTranslationsRemaining
api.getBalance().then((res) => {
if (!res.success || res.data == null) return
const data = res.data
const balance = typeof data === 'object' && 'balance' in data
? Number(data.balance) || 0
: Number(data) || 0
const freeRemaining = typeof data === 'object' && 'freeTranslationsRemaining' in data
? (data.freeTranslationsRemaining != null ? Number(data.freeTranslationsRemaining) : 0)
: 0
this.setData({
balance,
balanceText: balance.toFixed(2),
freeRemaining
})
})
},
loadCommodities() {
// GET /api/commodities?platform=trans-web;sale===1 已上架,按价格从高到低排序,标签按价格档:尊享/进阶/入门/尝鲜
api.getCommodities('trans-web').then((res) => {
if (res.success && Array.isArray(res.data)) {
const raw = (res.data || [])
.filter((c) => c.isAvailable !== false && Number(c.sale) === 1)
const sorted = [...raw].sort((a, b) => (Number(b.price) || 0) - (Number(a.price) || 0))
const tierLabels = ['尊享', '豪华']
const commodities = sorted.map((c, index) => ({
id: c.id,
name: c.name,
description: c.description || c.shortDescription || '',
price: c.price,
priceText: ((c.price || 0) / 100).toFixed(0),
discount: c.discount || 0,
discountText: ((c.discount || 0) / 100).toFixed(0),
priceTierLabel: index < tierLabels.length ? tierLabels[index] : ''
}))
const selectedId = commodities.length > 0 ? commodities[0].id : null
const selectedCommodity = commodities.length > 0 ? commodities[0] : null
this.setData({ commodities, selectedId, selectedCommodity })
}
})
},
onSelectCommodity(e) {
const id = e.currentTarget.dataset.id
const commodities = this.data.commodities
const selectedCommodity = commodities.find((c) => c.id === id) || null
this.setData({ selectedId: id, selectedCommodity })
},
onToggleAgreement() {
this.setData({ agreementChecked: !this.data.agreementChecked })
},
onAgreementLinkTap() {
// Prevent agreement row tap when clicking link (navigator still navigates)
},
onRecharge() {
if (!this.data.selectedCommodity) {
wx.showToast({ title: '请先选择套餐', icon: 'none' })
return
}
if (!this.data.agreementChecked) {
wx.showModal({
title: '服务协议与隐私政策',
content: '请阅读并同意《服务协议》和《隐私政策》后再进行充值。是否同意?',
confirmText: '同意',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.setData({ agreementChecked: true })
this.doRecharge()
}
}
})
return
}
this.doRecharge()
},
doRecharge() {
const commodity = this.data.selectedCommodity
const amountFen = Number(commodity.price) || 0
if (amountFen <= 0) {
wx.showToast({ title: '请选择有效套餐', icon: 'none' })
return
}
const commodityId = commodity.id
wx.showLoading({ title: '准备支付…' })
api.prepareVirtualPay(commodityId, amountFen).then((res) => {
wx.hideLoading()
if (!res.success || !res.data) {
wx.showToast({
title: res.errorMessage || '获取支付参数失败',
icon: 'none'
})
return
}
const { orderId, signData, pay_sig, signature, offerId, env, productId } = res.data
if (!orderId || !signData || pay_sig == null || signature == null || offerId == null || env == null || !productId) {
wx.showToast({ title: '支付参数不完整', icon: 'none' })
return
}
// 商品购买:mode=short_series_goods,按文档传 buyQuantity=1、goodsPrice=价格(元)=commodity.price/100
const mode = 'short_series_goods'
const buyQuantity = 1
const goodsPrice = amountFen / 100
const requestParams = {
orderId,
signData,
paySig: pay_sig,
signature,
offerId,
env,
mode,
productId,
buyQuantity,
goodsPrice,
currencyType: 'CNY',
outTradeNo: orderId
}
console.log('[requestVirtualPayment] 商品 id:', commodityId, '金额(分):', amountFen, '元:', goodsPrice)
console.log('[requestVirtualPayment] 请求参数:', requestParams)
wx.requestVirtualPayment({
orderId,
signData,
paySig: pay_sig,
signature,
offerId,
env,
mode,
productId,
buyQuantity,
goodsPrice,
currencyType: 'CNY',
outTradeNo: orderId,
success: () => {
wx.showToast({ title: '购买成功', icon: 'success' })
this.loadBalance()
},
fail: (err) => {
console.error('[requestVirtualPayment] fail', err)
const msg = (err && err.errMsg) || (err && err.message) || '支付失败'
wx.showToast({ title: msg.indexOf('cancel') !== -1 ? '已取消' : msg, icon: 'none' })
}
})
}).catch(() => {
wx.hideLoading()
wx.showToast({ title: '网络异常', icon: 'none' })
})
},
goProfileComplete() {
wx.navigateTo({ url: '/pages/profileComplete/profileComplete' })
}
})
