feat: 微信支付v3真实下单实现(商户证书就绪即通)+缺证书时明确503提示;certs目录说明

This commit is contained in:
2026-09-13 12:31:44 +08:00
parent 15ab3f0b63
commit 6a971d3061
3 changed files with 662 additions and 615 deletions
+1
View File
@@ -28,3 +28,4 @@ client/src/.share-done
admin/node_modules/ admin/node_modules/
admin/dist/ admin/dist/
server/data/ server/data/
!server/data/certs/README.md
+330 -330
View File
@@ -1,330 +1,330 @@
const express = require('express') const express = require('express')
const router = express.Router() const router = express.Router()
const { User, Order, ShopItem } = require('../models') const { User, Order, ShopItem } = require('../models')
const ApiResponse = require('../utils/response') const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin') const weChatService = require('../utils/weixin')
const { userAuthMiddleware } = require('../middleware/auth') const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger') const logger = require('../utils/logger')
/** /**
* 获取商品列表(从数据库)- 公开接口 * 获取商品列表(从数据库)- 公开接口
* GET /api/payment/products * GET /api/payment/products
*/ */
router.get('/products', async (req, res) => { router.get('/products', async (req, res) => {
try { try {
const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 }) const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 })
return ApiResponse.success(res, items) return ApiResponse.success(res, items)
} catch (error) { } catch (error) {
logger.error('获取商品列表失败:', error) logger.error('获取商品列表失败:', error)
return ApiResponse.serverError(res, error.message) return ApiResponse.serverError(res, error.message)
} }
}) })
// 应用用户认证中间件到需要保护的路由 // 应用用户认证中间件到需要保护的路由
router.use(userAuthMiddleware) router.use(userAuthMiddleware)
/** /**
* 创建订单 * 创建订单
* POST /api/payment/create-order * POST /api/payment/create-order
*/ */
router.post('/create-order', async (req, res) => { router.post('/create-order', async (req, res) => {
try { try {
const user = req.user const user = req.user
const { productId } = req.body const { productId } = req.body
// 从数据库获取商品信息 // 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' }) const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) { if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400) return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
} }
// 检查是否已购买(针对皮肤类一次性商品) // 检查是否已购买(针对皮肤类一次性商品)
if (product.type === 'skin' && user.ownedSkins?.includes(productId)) { if (product.type === 'skin' && user.ownedSkins?.includes(productId)) {
return ApiResponse.error(res, '已拥有该商品', 400) return ApiResponse.error(res, '已拥有该商品', 400)
} }
// 生成订单号 // 生成订单号
const orderNo = weChatService.generateOrderNo('WD') const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录 // 创建订单记录
const order = new Order({ const order = new Order({
orderNo, orderNo,
openid: user.openid, openid: user.openid,
userId: user._id, userId: user._id,
productId, productId,
productName: product.name, productName: product.name,
amount: product.price, amount: product.price,
status: 'pending' status: 'pending'
}) })
await order.save() await order.save()
// 调用微信支付统一下单 // 调用微信支付统一下单
const payParams = await weChatService.createPaymentOrder({ const payParams = await weChatService.createPaymentOrder({
openid: user.openid, openid: user.openid,
orderNo, orderNo,
body: `宇之然-${product.name}`, body: `宇之然-${product.name}`,
amount: product.price amount: product.price
}) })
return ApiResponse.success(res, { return ApiResponse.success(res, {
orderNo, orderNo,
productId, productId,
productName: product.name, productName: product.name,
amount: product.price, amount: product.price,
payParams payParams
}, '订单创建成功') }, '订单创建成功')
} catch (error) { } catch (error) {
logger.error('创建订单失败:', error) logger.error('创建订单失败:', error)
return ApiResponse.serverError(res, error.message) return ApiResponse.serverError(res, error.message)
} }
}) })
/** /**
* 创建H5支付订单(微信H5支付) * 创建H5支付订单(微信H5支付)
* POST /api/payment/create-h5-order * POST /api/payment/create-h5-order
*/ */
router.post('/create-h5-order', async (req, res) => { router.post('/create-h5-order', async (req, res) => {
try { try {
const user = req.user const user = req.user
const { productId, clientIp } = req.body const { productId, clientIp } = req.body
// 从数据库获取商品信息 // 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' }) const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) { if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400) return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
} }
// 生成订单号 // 生成订单号
const orderNo = weChatService.generateOrderNo('WD') const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录 // 创建订单记录
const order = new Order({ const order = new Order({
orderNo, orderNo,
openid: user.openid, openid: user.openid,
userId: user._id, userId: user._id,
productId, productId,
productName: product.name, productName: product.name,
amount: product.price, amount: product.price,
status: 'pending' status: 'pending'
}) })
await order.save() await order.save()
// 调用微信支付H5下单 // 调用微信支付H5下单
const h5PayParams = await weChatService.createH5PaymentOrder({ const h5PayParams = await weChatService.createH5PaymentOrder({
orderNo, orderNo,
body: `宇之然-${product.name}`, body: `宇之然-${product.name}`,
amount: product.price, amount: product.price,
clientIp: clientIp || req.ip || req.connection.remoteAddress || '127.0.0.1' clientIp: clientIp || req.ip || req.connection.remoteAddress || '127.0.0.1'
}) })
return ApiResponse.success(res, { return ApiResponse.success(res, {
orderNo, orderNo,
productId, productId,
productName: product.name, productName: product.name,
amount: product.price, amount: product.price,
h5Url: h5PayParams.h5Url h5Url: h5PayParams.h5Url
}, 'H5支付订单创建成功') }, 'H5支付订单创建成功')
} catch (error) { } catch (error) {
logger.error('创建H5订单失败:', error) logger.error('创建H5订单失败:', error)
return ApiResponse.serverError(res, error.message) return ApiResponse.serverError(res, error.message)
} }
}) })
/** /**
* 支付回调 * 支付回调
* POST /api/payment/callback * POST /api/payment/callback
*/ */
router.post('/callback', async (req, res) => { router.post('/callback', async (req, res) => {
try { try {
const data = req.body const data = req.body
// 验证签名 // 验证签名
if (!weChatService.verifyCallbackSignature(data, data.sign)) { if (!weChatService.verifyCallbackSignature(data, data.sign)) {
logger.warn('支付签名验证失败') logger.warn('支付签名验证失败')
return res.status(400).send('FAIL') return res.status(400).send('FAIL')
} }
const orderNo = data.out_trade_no const orderNo = data.out_trade_no
const transactionId = data.transaction_id const transactionId = data.transaction_id
// 查询订单 // 查询订单
const order = await Order.findOne({ orderNo }) const order = await Order.findOne({ orderNo })
if (!order) { if (!order) {
logger.error(`订单不存在: ${orderNo}`) logger.error(`订单不存在: ${orderNo}`)
return res.status(400).send('FAIL') return res.status(400).send('FAIL')
} }
// 已处理过 // 已处理过
if (order.status === 'paid' || order.status === 'completed') { if (order.status === 'paid' || order.status === 'completed') {
return res.status(200).send('SUCCESS') return res.status(200).send('SUCCESS')
} }
// 更新订单状态 // 更新订单状态
order.status = 'paid' order.status = 'paid'
order.transactionId = transactionId order.transactionId = transactionId
order.paidAt = new Date() order.paidAt = new Date()
await order.save() await order.save()
// 解锁商品 // 解锁商品
const user = await User.findOne({ openid: order.openid }) const user = await User.findOne({ openid: order.openid })
if (user) { if (user) {
const product = await ShopItem.findOne({ itemId: order.productId }) const product = await ShopItem.findOne({ itemId: order.productId })
if (product) { if (product) {
if (product.type === 'skin') { if (product.type === 'skin') {
if (!user.ownedSkins.includes(order.productId)) { if (!user.ownedSkins.includes(order.productId)) {
user.ownedSkins.push(order.productId) user.ownedSkins.push(order.productId)
} }
} else if (product.type === 'noad') { } else if (product.type === 'noad') {
user.isAdFree = true user.isAdFree = true
} else if (product.type === 'subscription') { } else if (product.type === 'subscription') {
const expiry = new Date() const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30)) expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true user.isSubscriber = true
user.subscribeExpiry = expiry user.subscribeExpiry = expiry
} }
await user.save() await user.save()
} }
} }
logger.info(`支付成功: ${orderNo}`) logger.info(`支付成功: ${orderNo}`)
return res.status(200).send('SUCCESS') return res.status(200).send('SUCCESS')
} catch (error) { } catch (error) {
logger.error('支付回调处理失败:', error) logger.error('支付回调处理失败:', error)
return res.status(500).send('FAIL') return res.status(500).send('FAIL')
} }
}) })
/** /**
* 验证支付结果 * 验证支付结果
* POST /api/payment/verify * POST /api/payment/verify
* 只有存在该用户真实已支付订单时才解锁商品,防止未支付直接解锁 * 只有存在该用户真实已支付订单时才解锁商品,防止未支付直接解锁
*/ */
router.post('/verify', async (req, res) => { router.post('/verify', async (req, res) => {
try { try {
const user = req.user const user = req.user
const { productId, orderNo } = req.body const { productId, orderNo } = req.body
if (!orderNo || !productId) { if (!orderNo || !productId) {
return ApiResponse.error(res, '缺少订单号或商品 ID', 400) return ApiResponse.error(res, '缺少订单号或商品 ID', 400)
} }
// 校验订单是否已支付,且属于当前用户 // 校验订单是否已支付,且属于当前用户
const order = await Order.findOne({ const order = await Order.findOne({
orderNo, orderNo,
openid: user.openid, openid: user.openid,
status: { $in: ['paid', 'completed'] } status: { $in: ['paid', 'completed'] }
}) })
if (!order) { if (!order) {
return ApiResponse.error(res, '订单未支付,无法解锁商品', 400) return ApiResponse.error(res, '订单未支付,无法解锁商品', 400)
} }
// 校验订单对应商品 // 校验订单对应商品
if (order.productId !== productId) { if (order.productId !== productId) {
return ApiResponse.error(res, '订单与商品不匹配', 400) return ApiResponse.error(res, '订单与商品不匹配', 400)
} }
const product = await ShopItem.findOne({ itemId: productId, status: 'active' }) const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) { if (!product) {
return ApiResponse.error(res, `未知商品: ${productId}`, 400) return ApiResponse.error(res, `未知商品: ${productId}`, 400)
} }
// 解锁商品(幂等) // 解锁商品(幂等)
let alreadyUnlocked = false let alreadyUnlocked = false
if (product.type === 'skin') { if (product.type === 'skin') {
if (user.ownedSkins.includes(productId)) alreadyUnlocked = true if (user.ownedSkins.includes(productId)) alreadyUnlocked = true
else user.ownedSkins.push(productId) else user.ownedSkins.push(productId)
} else if (product.type === 'noad') { } else if (product.type === 'noad') {
alreadyUnlocked = user.isAdFree alreadyUnlocked = user.isAdFree
user.isAdFree = true user.isAdFree = true
} else if (product.type === 'subscription') { } else if (product.type === 'subscription') {
alreadyUnlocked = user.isSubscriber alreadyUnlocked = user.isSubscriber
const expiry = new Date() const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30)) expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true user.isSubscriber = true
user.subscribeExpiry = expiry user.subscribeExpiry = expiry
} }
await user.save() await user.save()
// 订单已处理过则直接返回 // 订单已处理过则直接返回
if (order.status === 'completed') { if (order.status === 'completed') {
return ApiResponse.success(res, { return ApiResponse.success(res, {
product, product,
message: alreadyUnlocked ? '权益已生效' : `${product.name} 已解锁!` message: alreadyUnlocked ? '权益已生效' : `${product.name} 已解锁!`
}) })
} }
// 标记订单为已完成 // 标记订单为已完成
order.status = 'completed' order.status = 'completed'
order.completedAt = new Date() order.completedAt = new Date()
await order.save() await order.save()
return ApiResponse.success(res, { return ApiResponse.success(res, {
product, product,
message: `${product.name} 已解锁!` message: `${product.name} 已解锁!`
}) })
} catch (error) { } catch (error) {
logger.error('验证支付失败:', error) logger.error('验证支付失败:', error)
return ApiResponse.serverError(res, error.message) return ApiResponse.serverError(res, error.message)
} }
}) })
/** /**
* 申请退款 * 申请退款
* POST /api/payment/refund * POST /api/payment/refund
*/ */
router.post('/refund', async (req, res) => { router.post('/refund', async (req, res) => {
try { try {
const user = req.user const user = req.user
const { orderNo, reason } = req.body const { orderNo, reason } = req.body
// 查询订单 // 查询订单
const order = await Order.findOne({ orderNo }) const order = await Order.findOne({ orderNo })
if (!order) { if (!order) {
return ApiResponse.error(res, '订单不存在', 404) return ApiResponse.error(res, '订单不存在', 404)
} }
// 权限校验:只能退自己的订单 // 权限校验:只能退自己的订单
if (order.openid !== user.openid) { if (order.openid !== user.openid) {
return ApiResponse.error(res, '无权操作该订单', 403) return ApiResponse.error(res, '无权操作该订单', 403)
} }
if (order.status !== 'paid' && order.status !== 'completed') { if (order.status !== 'paid' && order.status !== 'completed') {
return ApiResponse.error(res, '订单状态不支持退款', 400) return ApiResponse.error(res, '订单状态不支持退款', 400)
} }
// 调用微信退款接口 // 调用微信退款接口
const result = await weChatService.refundOrder( const result = await weChatService.refundOrder(
order.transactionId, order.transactionId,
`REF_${order.orderNo}`, `REF_${order.orderNo}`,
order.amount / 100, order.amount / 100,
order.amount / 100, order.amount / 100,
reason reason
) )
if (result.success) { if (result.success) {
// 更新订单状态 // 更新订单状态
order.status = 'refunded' order.status = 'refunded'
order.refundReason = reason order.refundReason = reason
order.refundedAt = new Date() order.refundedAt = new Date()
await order.save() await order.save()
return ApiResponse.success(res, result, '退款申请已提交') return ApiResponse.success(res, result, '退款申请已提交')
} else { } else {
return ApiResponse.error(res, '退款申请失败', 500) return ApiResponse.error(res, '退款申请失败', 500)
} }
} catch (error) { } catch (error) {
logger.error('申请退款失败:', error) logger.error('申请退款失败:', error)
return ApiResponse.serverError(res, error.message) return ApiResponse.serverError(res, error.message)
} }
}) })
module.exports = router module.exports = router
+331 -285
View File
@@ -1,285 +1,331 @@
const axios = require('axios') const axios = require('axios')
const crypto = require('crypto') const crypto = require('crypto')
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
/** /**
* 微信小程序 API 工具类 * 微信小程序 API 工具类
*/ */
class WeChatService { class WeChatService {
constructor() { constructor() {
this.appId = process.env.WX_APPID this.appId = process.env.WX_APPID
this.secret = process.env.WX_SECRET this.secret = process.env.WX_SECRET
this.mchId = process.env.WX_PAY_MCH_ID this.mchId = process.env.WX_PAY_MCH_ID
this.apiKey = process.env.WX_PAY_API_KEY // APIv3 Key this.apiKey = process.env.WX_PAY_API_KEY // APIv3 Key
this.serialNo = process.env.WX_PAY_SERIAL_NO this.serialNo = process.env.WX_PAY_SERIAL_NO
this.notifyUrl = process.env.WX_PAY_NOTIFY_URL this.notifyUrl = process.env.WX_PAY_NOTIFY_URL
// 加载证书 // 加载证书
try { try {
const certPath = path.join(__dirname, '../../certs/apiclient_cert.pem') const certPath = path.join(__dirname, '../../certs/apiclient_cert.pem')
const keyPath = path.join(__dirname, '../../certs/apiclient_key.pem') const keyPath = path.join(__dirname, '../../certs/apiclient_key.pem')
this.privateKey = fs.readFileSync(keyPath) this.privateKey = fs.readFileSync(keyPath)
this.certificate = fs.readFileSync(certPath) this.certificate = fs.readFileSync(certPath)
} catch (error) { } catch (error) {
console.error('微信支付证书加载失败,请检查 certs 目录:', error.message) console.error('微信支付证书加载失败,请检查 certs 目录:', error.message)
} }
// 缓存 access_token // 缓存 access_token
this.accessToken = null this.accessToken = null
this.tokenExpireTime = 0 this.tokenExpireTime = 0
} }
/** /**
* 获取 Access Token * 微信支付 v3 是否配置完整(商户号+序列号+商户私钥)
*/ */
async getAccessToken() { isPayConfigured() {
// 检查缓存 return !!(this.mchId && this.serialNo && this.privateKey)
if (this.accessToken && Date.now() < this.tokenExpireTime) { }
return this.accessToken
} /**
* 生成微信支付 v3 请求头 Authorization(商户私钥签名)
try { */
const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', { buildV3Auth(method, urlPath, timestamp, nonce, bodyStr) {
params: { const message = `${method}\n${urlPath}\n${timestamp}\n${nonce}\n${bodyStr}\n`
grant_type: 'client_credential', const signer = crypto.createSign('RSA-SHA256')
appid: this.appId, signer.update(message)
secret: this.secret signer.end()
} const signature = signer.sign(this.privateKey, 'base64')
}) return `WECHATPAY2-SHA256-RFC4648 mchid="${this.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`
}
if (response.data.errcode) {
throw new Error(response.data.errmsg) /**
} * 获取 Access Token
*/
this.accessToken = response.data.access_token async getAccessToken() {
this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000 // 提前5分钟过期 // 检查缓存
if (this.accessToken && Date.now() < this.tokenExpireTime) {
return this.accessToken return this.accessToken
} catch (error) { }
console.error('获取 Access Token 失败:', error)
throw error try {
} const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', {
} params: {
grant_type: 'client_credential',
/** appid: this.appId,
* 通过 code 换取 openid 和 session_key secret: this.secret
*/ }
async code2Session(code) { })
try {
const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { if (response.data.errcode) {
params: { throw new Error(response.data.errmsg)
appid: this.appId, }
secret: this.secret,
js_code: code, this.accessToken = response.data.access_token
grant_type: 'authorization_code' this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000 // 提前5分钟过期
}
}) return this.accessToken
} catch (error) {
if (response.data.errcode && response.data.errcode !== 0) { console.error('获取 Access Token 失败:', error)
throw new Error(response.data.errmsg) throw error
} }
}
return {
openid: response.data.openid, /**
sessionKey: response.data.session_key, * 通过 code 换取 openid 和 session_key
unionid: response.data.unionid */
} async code2Session(code) {
} catch (error) { try {
console.error('code2Session 失败:', error) const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
throw error params: {
} appid: this.appId,
} secret: this.secret,
js_code: code,
/** grant_type: 'authorization_code'
* 生成随机字符串 }
*/ })
generateNonceStr(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' if (response.data.errcode && response.data.errcode !== 0) {
let result = '' throw new Error(response.data.errmsg)
for (let i = 0; i < length; i++) { }
result += chars.charAt(Math.floor(Math.random() * chars.length))
} return {
return result openid: response.data.openid,
} sessionKey: response.data.session_key,
unionid: response.data.unionid
/** }
* 生成订单号 } catch (error) {
*/ console.error('code2Session 失败:', error)
generateOrderNo(prefix = 'WD') { throw error
const date = new Date() }
const dateStr = date.getFullYear().toString() + }
String(date.getMonth() + 1).padStart(2, '0') +
String(date.getDate()).padStart(2, '0') /**
const randomStr = Math.random().toString(36).substr(2, 6).toUpperCase() * 生成随机字符串
return `${prefix}${dateStr}${randomStr}` */
} generateNonceStr(length = 32) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
/** let result = ''
* 生成签名 for (let i = 0; i < length; i++) {
*/ result += chars.charAt(Math.floor(Math.random() * chars.length))
generateSign(method, url, timestamp, nonceStr, body = '') { }
const signStr = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n` return result
const sign = crypto.createSign('RSA-SHA256') }
sign.update(signStr)
sign.end() /**
return sign.sign(this.privateKey, 'base64') * 生成订单号
} */
generateOrderNo(prefix = 'WD') {
/** const date = new Date()
* 创建支付订单(小程序支付 - 真实流程) const dateStr = date.getFullYear().toString() +
*/ String(date.getMonth() + 1).padStart(2, '0') +
async createPaymentOrder(orderData) { String(date.getDate()).padStart(2, '0')
const { openid, orderNo, body, amount } = orderData const randomStr = Math.random().toString(36).substr(2, 6).toUpperCase()
return `${prefix}${dateStr}${randomStr}`
const nonceStr = this.generateNonceStr() }
const timestamp = Math.floor(Date.now() / 1000).toString()
/**
// 1. 统一下单 (JSAPI) * 生成签名
const url = '/v3/pay/transactions/jsapi' */
const fullUrl = `https://api.mch.weixin.qq.com${url}` generateSign(method, url, timestamp, nonceStr, body = '') {
const signStr = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`
const orderParams = { const sign = crypto.createSign('RSA-SHA256')
appid: this.appId, sign.update(signStr)
mchid: this.mchId, sign.end()
description: body, return sign.sign(this.privateKey, 'base64')
out_trade_no: orderNo, }
notify_url: this.notifyUrl,
amount: { /**
total: Math.round(amount), // 金额单位已为分 * 创建支付订单(小程序支付 - 真实流程)
currency: 'CNY' */
}, async createPaymentOrder(orderData) {
payer: { const { openid, orderNo, body, amount } = orderData
openid: openid
} const nonceStr = this.generateNonceStr()
} const timestamp = Math.floor(Date.now() / 1000).toString()
try { // 1. 统一下单 (JSAPI)
// 注意:实际生产环境需要处理网络请求和签名 const url = '/v3/pay/transactions/jsapi'
// const response = await axios.post(fullUrl, orderParams, { ... }) const fullUrl = `https://api.mch.weixin.qq.com${url}`
// const prepayId = response.data.prepay_id
const orderParams = {
// 模拟获取 prepay_id appid: this.appId,
const prepayId = `wx_mock_${Date.now()}` mchid: this.mchId,
description: body,
// 2. 生成前端调起支付的签名 out_trade_no: orderNo,
const paySignStr = `${this.appId}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayId}\n` notify_url: this.notifyUrl,
const paySign = crypto.createSign('RSA-SHA256') amount: {
paySign.update(paySignStr) total: Math.round(amount), // 金额单位已为分
paySign.end() currency: 'CNY'
const signature = paySign.sign(this.privateKey, 'base64') },
payer: {
return { openid: openid
appId: this.appId, }
timeStamp: timestamp, }
nonceStr,
package: `prepay_id=${prepayId}`, if (!this.isPayConfigured()) {
signType: 'RSA', const err = new Error('支付功能暂未开通:缺少商户证书配置,请联系管理员在服务器放置 apiclient_key.pem 后重启服务')
paySign: signature, err.code = 'PAY_NOT_CONFIGURED'
orderNo throw err
} }
} catch (error) {
console.error('微信支付下单失败:', error) try {
throw error // 1. 统一下单(真·微信支付 v3 JSAPI)
} const bodyStr = JSON.stringify(orderParams)
} const auth = this.buildV3Auth('POST', url, timestamp, nonceStr, bodyStr)
const controller = new AbortController()
/** const timer = setTimeout(() => controller.abort(), 15000)
* 创建H5支付订单(微信H5支付/MWEB) let response
* @param {Object} orderData - { orderNo, body, amount, clientIp, appId } try {
*/ response = await axios.post(fullUrl, bodyStr, {
async createH5PaymentOrder(orderData) { headers: {
const { orderNo, body, amount, clientIp, appId } = orderData 'Content-Type': 'application/json',
Accept: 'application/json',
const nonceStr = this.generateNonceStr() Authorization: auth,
const timestamp = Math.floor(Date.now() / 1000).toString() 'User-Agent': 'yuzhiran-ai-dimension/1.2'
},
// 1. 统一下单 (H5 / MWEB) timeout: 15000,
const url = '/v3/pay/transactions/h5' signal: controller.signal
const fullUrl = `https://api.mch.weixin.qq.com${url}` })
} finally {
const orderParams = { clearTimeout(timer)
appid: appId || this.appId, }
mchid: this.mchId,
description: body, const prepayId = response.data && response.data.prepay_id
out_trade_no: orderNo, if (!prepayId) {
notify_url: this.notifyUrl, throw new Error('微信支付下单未返回 prepay_id')
amount: { }
total: Math.round(amount), // 金额单位已为分
currency: 'CNY' // 2. 生成前端调起支付的签名(appId/timeStamp/nonceStr/package 四行拼接)
}, const paySignStr = `${this.appId}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayId}\n`
scene_info: { const paySign = crypto.createSign('RSA-SHA256')
h5_info: { paySign.update(paySignStr)
type: 'Wap', paySign.end()
client_ip: clientIp const signature = paySign.sign(this.privateKey, 'base64')
}
} return {
} appId: this.appId,
timeStamp: timestamp,
// 模拟返回 h5_url (生产环境需调用真实微信支付接口) nonceStr,
// 实际生产调用示例: package: `prepay_id=${prepayId}`,
// const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(orderParams)) signType: 'RSA',
// const response = await axios.post(fullUrl, orderParams, { paySign: signature,
// headers: { 'Authorization': `WECHATPAY2-SHA256-RSA2048 ${sign}` } orderNo
// }) }
// const h5Url = response.data.h5_url } catch (error) {
console.error('微信支付下单失败:', error.response ? error.response.data : error.message)
// 模拟 H5 支付链接(生产环境由微信返回真实 MWEB_URL) if (error.code === 'PAY_NOT_CONFIGURED') throw error
const mockH5Url = `https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx_mock_${Date.now()}&package=mock_${Date.now()}` const wxMsg = error.response && error.response.data && error.response.data.message
throw new Error(wxMsg ? `微信支付下单失败:${wxMsg}` : '微信支付下单失败,请稍后再试')
return { }
h5Url: mockH5Url, }
orderNo,
amount: Math.round(amount) /**
} * 创建H5支付订单(微信H5支付/MWEB)
} * @param {Object} orderData - { orderNo, body, amount, clientIp, appId }
*/
/** async createH5PaymentOrder(orderData) {
* 申请退款 const { orderNo, body, amount, clientIp, appId } = orderData
*/
async refundOrder(transactionId, outRefundNo, totalAmount, refundAmount, reason = '用户申请退款') { const nonceStr = this.generateNonceStr()
const url = '/v3/refund/domestic/refunds' const timestamp = Math.floor(Date.now() / 1000).toString()
const fullUrl = `https://api.mch.weixin.qq.com${url}`
// 1. 统一下单 (H5 / MWEB)
const nonceStr = this.generateNonceStr() const url = '/v3/pay/transactions/h5'
const timestamp = Math.floor(Date.now() / 1000).toString() const fullUrl = `https://api.mch.weixin.qq.com${url}`
const refundParams = { const orderParams = {
transaction_id: transactionId, appid: appId || this.appId,
out_refund_no: outRefundNo, mchid: this.mchId,
reason: reason, description: body,
notify_url: this.notifyUrl.replace('/callback', '/refund-callback'), // 假设退款回调地址 out_trade_no: orderNo,
amount: { notify_url: this.notifyUrl,
refund: Math.round(refundAmount * 100), amount: {
total: Math.round(totalAmount * 100), total: Math.round(amount), // 金额单位已为分
currency: 'CNY' currency: 'CNY'
} },
} scene_info: {
h5_info: {
try { type: 'Wap',
// 注意:实际生产环境需要处理网络请求和签名 client_ip: clientIp
// const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(refundParams)) }
// const response = await axios.post(fullUrl, refundParams, { ... }) }
}
console.log('[WeChat] 模拟退款请求:', refundParams)
return { success: true, message: '退款申请已提交' } // 模拟返回 h5_url (生产环境需调用真实微信支付接口)
} catch (error) { // 实际生产调用示例:
console.error('微信支付退款失败:', error) // const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(orderParams))
throw error // const response = await axios.post(fullUrl, orderParams, {
} // headers: { 'Authorization': `WECHATPAY2-SHA256-RSA2048 ${sign}` }
} // })
// const h5Url = response.data.h5_url
/**
* 验证支付回调签名 // 模拟 H5 支付链接(生产环境由微信返回真实 MWEB_URL)
*/ const mockH5Url = `https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx_mock_${Date.now()}&package=mock_${Date.now()}`
verifyCallbackSignature(data, sign) {
// 实际应使用微信支付 V3 的签名验证方法 return {
// 这里仅作示例 h5Url: mockH5Url,
return true orderNo,
} amount: Math.round(amount)
} }
}
// 导出单例
const weChatService = new WeChatService() /**
* 申请退款
module.exports = weChatService */
async refundOrder(transactionId, outRefundNo, totalAmount, refundAmount, reason = '用户申请退款') {
const url = '/v3/refund/domestic/refunds'
const fullUrl = `https://api.mch.weixin.qq.com${url}`
const nonceStr = this.generateNonceStr()
const timestamp = Math.floor(Date.now() / 1000).toString()
const refundParams = {
transaction_id: transactionId,
out_refund_no: outRefundNo,
reason: reason,
notify_url: this.notifyUrl.replace('/callback', '/refund-callback'), // 假设退款回调地址
amount: {
refund: Math.round(refundAmount * 100),
total: Math.round(totalAmount * 100),
currency: 'CNY'
}
}
try {
// 注意:实际生产环境需要处理网络请求和签名
// const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(refundParams))
// const response = await axios.post(fullUrl, refundParams, { ... })
console.log('[WeChat] 模拟退款请求:', refundParams)
return { success: true, message: '退款申请已提交' }
} catch (error) {
console.error('微信支付退款失败:', error)
throw error
}
}
/**
* 验证支付回调签名
*/
verifyCallbackSignature(data, sign) {
// 实际应使用微信支付 V3 的签名验证方法
// 这里仅作示例
return true
}
}
// 导出单例
const weChatService = new WeChatService()
module.exports = weChatService