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
+330 -330
View File
@@ -1,330 +1,330 @@
const express = require('express')
const router = express.Router()
const { User, Order, ShopItem } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取商品列表(从数据库)- 公开接口
* GET /api/payment/products
*/
router.get('/products', async (req, res) => {
try {
const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 })
return ApiResponse.success(res, items)
} catch (error) {
logger.error('获取商品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
// 应用用户认证中间件到需要保护的路由
router.use(userAuthMiddleware)
/**
* 创建订单
* POST /api/payment/create-order
*/
router.post('/create-order', async (req, res) => {
try {
const user = req.user
const { productId } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 检查是否已购买(针对皮肤类一次性商品)
if (product.type === 'skin' && user.ownedSkins?.includes(productId)) {
return ApiResponse.error(res, '已拥有该商品', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付统一下单
const payParams = await weChatService.createPaymentOrder({
openid: user.openid,
orderNo,
body: `宇之然-${product.name}`,
amount: product.price
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
payParams
}, '订单创建成功')
} catch (error) {
logger.error('创建订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 创建H5支付订单(微信H5支付)
* POST /api/payment/create-h5-order
*/
router.post('/create-h5-order', async (req, res) => {
try {
const user = req.user
const { productId, clientIp } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付H5下单
const h5PayParams = await weChatService.createH5PaymentOrder({
orderNo,
body: `宇之然-${product.name}`,
amount: product.price,
clientIp: clientIp || req.ip || req.connection.remoteAddress || '127.0.0.1'
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
h5Url: h5PayParams.h5Url
}, 'H5支付订单创建成功')
} catch (error) {
logger.error('创建H5订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 支付回调
* POST /api/payment/callback
*/
router.post('/callback', async (req, res) => {
try {
const data = req.body
// 验证签名
if (!weChatService.verifyCallbackSignature(data, data.sign)) {
logger.warn('支付签名验证失败')
return res.status(400).send('FAIL')
}
const orderNo = data.out_trade_no
const transactionId = data.transaction_id
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
logger.error(`订单不存在: ${orderNo}`)
return res.status(400).send('FAIL')
}
// 已处理过
if (order.status === 'paid' || order.status === 'completed') {
return res.status(200).send('SUCCESS')
}
// 更新订单状态
order.status = 'paid'
order.transactionId = transactionId
order.paidAt = new Date()
await order.save()
// 解锁商品
const user = await User.findOne({ openid: order.openid })
if (user) {
const product = await ShopItem.findOne({ itemId: order.productId })
if (product) {
if (product.type === 'skin') {
if (!user.ownedSkins.includes(order.productId)) {
user.ownedSkins.push(order.productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
}
}
logger.info(`支付成功: ${orderNo}`)
return res.status(200).send('SUCCESS')
} catch (error) {
logger.error('支付回调处理失败:', error)
return res.status(500).send('FAIL')
}
})
/**
* 验证支付结果
* POST /api/payment/verify
* 只有存在该用户真实已支付订单时才解锁商品,防止未支付直接解锁
*/
router.post('/verify', async (req, res) => {
try {
const user = req.user
const { productId, orderNo } = req.body
if (!orderNo || !productId) {
return ApiResponse.error(res, '缺少订单号或商品 ID', 400)
}
// 校验订单是否已支付,且属于当前用户
const order = await Order.findOne({
orderNo,
openid: user.openid,
status: { $in: ['paid', 'completed'] }
})
if (!order) {
return ApiResponse.error(res, '订单未支付,无法解锁商品', 400)
}
// 校验订单对应商品
if (order.productId !== productId) {
return ApiResponse.error(res, '订单与商品不匹配', 400)
}
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, `未知商品: ${productId}`, 400)
}
// 解锁商品(幂等)
let alreadyUnlocked = false
if (product.type === 'skin') {
if (user.ownedSkins.includes(productId)) alreadyUnlocked = true
else user.ownedSkins.push(productId)
} else if (product.type === 'noad') {
alreadyUnlocked = user.isAdFree
user.isAdFree = true
} else if (product.type === 'subscription') {
alreadyUnlocked = user.isSubscriber
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
// 订单已处理过则直接返回
if (order.status === 'completed') {
return ApiResponse.success(res, {
product,
message: alreadyUnlocked ? '权益已生效' : `${product.name} 已解锁!`
})
}
// 标记订单为已完成
order.status = 'completed'
order.completedAt = new Date()
await order.save()
return ApiResponse.success(res, {
product,
message: `${product.name} 已解锁!`
})
} catch (error) {
logger.error('验证支付失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 申请退款
* POST /api/payment/refund
*/
router.post('/refund', async (req, res) => {
try {
const user = req.user
const { orderNo, reason } = req.body
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
return ApiResponse.error(res, '订单不存在', 404)
}
// 权限校验:只能退自己的订单
if (order.openid !== user.openid) {
return ApiResponse.error(res, '无权操作该订单', 403)
}
if (order.status !== 'paid' && order.status !== 'completed') {
return ApiResponse.error(res, '订单状态不支持退款', 400)
}
// 调用微信退款接口
const result = await weChatService.refundOrder(
order.transactionId,
`REF_${order.orderNo}`,
order.amount / 100,
order.amount / 100,
reason
)
if (result.success) {
// 更新订单状态
order.status = 'refunded'
order.refundReason = reason
order.refundedAt = new Date()
await order.save()
return ApiResponse.success(res, result, '退款申请已提交')
} else {
return ApiResponse.error(res, '退款申请失败', 500)
}
} catch (error) {
logger.error('申请退款失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router
const express = require('express')
const router = express.Router()
const { User, Order, ShopItem } = require('../models')
const ApiResponse = require('../utils/response')
const weChatService = require('../utils/weixin')
const { userAuthMiddleware } = require('../middleware/auth')
const logger = require('../utils/logger')
/**
* 获取商品列表(从数据库)- 公开接口
* GET /api/payment/products
*/
router.get('/products', async (req, res) => {
try {
const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 })
return ApiResponse.success(res, items)
} catch (error) {
logger.error('获取商品列表失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
// 应用用户认证中间件到需要保护的路由
router.use(userAuthMiddleware)
/**
* 创建订单
* POST /api/payment/create-order
*/
router.post('/create-order', async (req, res) => {
try {
const user = req.user
const { productId } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 检查是否已购买(针对皮肤类一次性商品)
if (product.type === 'skin' && user.ownedSkins?.includes(productId)) {
return ApiResponse.error(res, '已拥有该商品', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付统一下单
const payParams = await weChatService.createPaymentOrder({
openid: user.openid,
orderNo,
body: `宇之然-${product.name}`,
amount: product.price
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
payParams
}, '订单创建成功')
} catch (error) {
logger.error('创建订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 创建H5支付订单(微信H5支付)
* POST /api/payment/create-h5-order
*/
router.post('/create-h5-order', async (req, res) => {
try {
const user = req.user
const { productId, clientIp } = req.body
// 从数据库获取商品信息
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400)
}
// 生成订单号
const orderNo = weChatService.generateOrderNo('WD')
// 创建订单记录
const order = new Order({
orderNo,
openid: user.openid,
userId: user._id,
productId,
productName: product.name,
amount: product.price,
status: 'pending'
})
await order.save()
// 调用微信支付H5下单
const h5PayParams = await weChatService.createH5PaymentOrder({
orderNo,
body: `宇之然-${product.name}`,
amount: product.price,
clientIp: clientIp || req.ip || req.connection.remoteAddress || '127.0.0.1'
})
return ApiResponse.success(res, {
orderNo,
productId,
productName: product.name,
amount: product.price,
h5Url: h5PayParams.h5Url
}, 'H5支付订单创建成功')
} catch (error) {
logger.error('创建H5订单失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 支付回调
* POST /api/payment/callback
*/
router.post('/callback', async (req, res) => {
try {
const data = req.body
// 验证签名
if (!weChatService.verifyCallbackSignature(data, data.sign)) {
logger.warn('支付签名验证失败')
return res.status(400).send('FAIL')
}
const orderNo = data.out_trade_no
const transactionId = data.transaction_id
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
logger.error(`订单不存在: ${orderNo}`)
return res.status(400).send('FAIL')
}
// 已处理过
if (order.status === 'paid' || order.status === 'completed') {
return res.status(200).send('SUCCESS')
}
// 更新订单状态
order.status = 'paid'
order.transactionId = transactionId
order.paidAt = new Date()
await order.save()
// 解锁商品
const user = await User.findOne({ openid: order.openid })
if (user) {
const product = await ShopItem.findOne({ itemId: order.productId })
if (product) {
if (product.type === 'skin') {
if (!user.ownedSkins.includes(order.productId)) {
user.ownedSkins.push(order.productId)
}
} else if (product.type === 'noad') {
user.isAdFree = true
} else if (product.type === 'subscription') {
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
}
}
logger.info(`支付成功: ${orderNo}`)
return res.status(200).send('SUCCESS')
} catch (error) {
logger.error('支付回调处理失败:', error)
return res.status(500).send('FAIL')
}
})
/**
* 验证支付结果
* POST /api/payment/verify
* 只有存在该用户真实已支付订单时才解锁商品,防止未支付直接解锁
*/
router.post('/verify', async (req, res) => {
try {
const user = req.user
const { productId, orderNo } = req.body
if (!orderNo || !productId) {
return ApiResponse.error(res, '缺少订单号或商品 ID', 400)
}
// 校验订单是否已支付,且属于当前用户
const order = await Order.findOne({
orderNo,
openid: user.openid,
status: { $in: ['paid', 'completed'] }
})
if (!order) {
return ApiResponse.error(res, '订单未支付,无法解锁商品', 400)
}
// 校验订单对应商品
if (order.productId !== productId) {
return ApiResponse.error(res, '订单与商品不匹配', 400)
}
const product = await ShopItem.findOne({ itemId: productId, status: 'active' })
if (!product) {
return ApiResponse.error(res, `未知商品: ${productId}`, 400)
}
// 解锁商品(幂等)
let alreadyUnlocked = false
if (product.type === 'skin') {
if (user.ownedSkins.includes(productId)) alreadyUnlocked = true
else user.ownedSkins.push(productId)
} else if (product.type === 'noad') {
alreadyUnlocked = user.isAdFree
user.isAdFree = true
} else if (product.type === 'subscription') {
alreadyUnlocked = user.isSubscriber
const expiry = new Date()
expiry.setDate(expiry.getDate() + (product.duration || 30))
user.isSubscriber = true
user.subscribeExpiry = expiry
}
await user.save()
// 订单已处理过则直接返回
if (order.status === 'completed') {
return ApiResponse.success(res, {
product,
message: alreadyUnlocked ? '权益已生效' : `${product.name} 已解锁!`
})
}
// 标记订单为已完成
order.status = 'completed'
order.completedAt = new Date()
await order.save()
return ApiResponse.success(res, {
product,
message: `${product.name} 已解锁!`
})
} catch (error) {
logger.error('验证支付失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
/**
* 申请退款
* POST /api/payment/refund
*/
router.post('/refund', async (req, res) => {
try {
const user = req.user
const { orderNo, reason } = req.body
// 查询订单
const order = await Order.findOne({ orderNo })
if (!order) {
return ApiResponse.error(res, '订单不存在', 404)
}
// 权限校验:只能退自己的订单
if (order.openid !== user.openid) {
return ApiResponse.error(res, '无权操作该订单', 403)
}
if (order.status !== 'paid' && order.status !== 'completed') {
return ApiResponse.error(res, '订单状态不支持退款', 400)
}
// 调用微信退款接口
const result = await weChatService.refundOrder(
order.transactionId,
`REF_${order.orderNo}`,
order.amount / 100,
order.amount / 100,
reason
)
if (result.success) {
// 更新订单状态
order.status = 'refunded'
order.refundReason = reason
order.refundedAt = new Date()
await order.save()
return ApiResponse.success(res, result, '退款申请已提交')
} else {
return ApiResponse.error(res, '退款申请失败', 500)
}
} catch (error) {
logger.error('申请退款失败:', error)
return ApiResponse.serverError(res, error.message)
}
})
module.exports = router