feat: 微信支付v3真实下单实现(商户证书就绪即通)+缺证书时明确503提示;certs目录说明
This commit is contained in:
+330
-330
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user