fix: 退款三修复——coins按金额×100兜底旧订单、调微信xpay/refund_order真实退钱、AdminLog枚举补refund/adjust
This commit is contained in:
@@ -1,54 +1,54 @@
|
|||||||
const mongoose = require('mongoose')
|
const mongoose = require('mongoose')
|
||||||
const { Schema } = mongoose
|
const { Schema } = mongoose
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员操作日志模型
|
* 管理员操作日志模型
|
||||||
*/
|
*/
|
||||||
const AdminLogSchema = new Schema({
|
const AdminLogSchema = new Schema({
|
||||||
// 管理员信息
|
// 管理员信息
|
||||||
adminId: {
|
adminId: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Admin',
|
ref: 'Admin',
|
||||||
required: true,
|
required: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
adminName: {
|
adminName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
|
||||||
// 操作信息
|
// 操作信息
|
||||||
action: {
|
action: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export']
|
enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export', 'refund', 'adjust']
|
||||||
},
|
},
|
||||||
resource: {
|
resource: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
resourceId: String,
|
resourceId: String,
|
||||||
|
|
||||||
// 详细信息
|
// 详细信息
|
||||||
details: Schema.Types.Mixed,
|
details: Schema.Types.Mixed,
|
||||||
|
|
||||||
// IP 地址
|
// IP 地址
|
||||||
ip: String,
|
ip: String,
|
||||||
userAgent: String,
|
userAgent: String,
|
||||||
|
|
||||||
// 时间戳
|
// 时间戳
|
||||||
timestamp: {
|
timestamp: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now,
|
default: Date.now,
|
||||||
index: true
|
index: true
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: false
|
timestamps: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// 索引
|
// 索引
|
||||||
AdminLogSchema.index({ adminId: 1, timestamp: -1 })
|
AdminLogSchema.index({ adminId: 1, timestamp: -1 })
|
||||||
AdminLogSchema.index({ resource: 1, timestamp: -1 })
|
AdminLogSchema.index({ resource: 1, timestamp: -1 })
|
||||||
AdminLogSchema.index({ action: 1 })
|
AdminLogSchema.index({ action: 1 })
|
||||||
|
|
||||||
module.exports = mongoose.model('AdminLog', AdminLogSchema)
|
module.exports = mongoose.model('AdminLog', AdminLogSchema)
|
||||||
|
|||||||
@@ -1232,22 +1232,60 @@ router.post('/vpay/orders/:id/refund', requirePermission('orders:write'), async
|
|||||||
if (!order) return ApiResponse.notFound(res, '订单不存在')
|
if (!order) return ApiResponse.notFound(res, '订单不存在')
|
||||||
if (order.status !== 'paid') return ApiResponse.error(res, '仅已支付订单可退款', 400)
|
if (order.status !== 'paid') return ApiResponse.error(res, '仅已支付订单可退款', 400)
|
||||||
|
|
||||||
const coins = (order.metadata || {}).coins || 0
|
// 旧订单可能没有 metadata.coins,按金额 ×100(1:100 兑换比例)兜底
|
||||||
|
const coins = (order.metadata || {}).coins || Math.round(order.amount * 100)
|
||||||
|
if (!coins || coins <= 0) return ApiResponse.error(res, '无法确定退款星尘数量', 400)
|
||||||
const user = await User.findById(order.userId)
|
const user = await User.findById(order.userId)
|
||||||
if (!user) return ApiResponse.error(res, '用户不存在', 400)
|
if (!user) return ApiResponse.error(res, '用户不存在', 400)
|
||||||
|
|
||||||
// 扣回星尘(可能不够扣,允许负数让管理员判断)
|
// 扣回星尘(允许扣到负数,管理员可判断)
|
||||||
user.vpayCoins = (user.vpayCoins || 0) - coins
|
user.vpayCoins = (user.vpayCoins || 0) - coins
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|
||||||
|
// ===== 调用微信虚拟支付退款 API(真正退钱给用户) =====
|
||||||
|
let wxRefundResult = 'not_called'
|
||||||
|
try {
|
||||||
|
const ENV = parseInt(process.env.VPAY_ENV || '1')
|
||||||
|
const APPKEY = ENV === 0 ? (process.env.VPAY_APPKEY_PROD || '') : (process.env.VPAY_APPKEY_SANDBOX || '')
|
||||||
|
const OFFER_ID = process.env.VPAY_OFFER_ID || ''
|
||||||
|
// 获取 access_token(小程序全局凭证)
|
||||||
|
const tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${process.env.WX_APPID}&secret=${process.env.WX_SECRET}`
|
||||||
|
const tokenRes = await fetch(tokenUrl).then(r => r.json())
|
||||||
|
const accessToken = tokenRes.access_token
|
||||||
|
if (!accessToken) throw new Error('获取access_token失败: ' + JSON.stringify(tokenRes))
|
||||||
|
|
||||||
|
const refundBody = JSON.stringify({ offerId: OFFER_ID, outTradeNo: order.orderNo })
|
||||||
|
const paySig = require('crypto').createHmac('sha256', APPKEY).update('/xpay/refund_order&' + refundBody).digest('hex')
|
||||||
|
|
||||||
|
const wxRes = await fetch(`https://api.weixin.qq.com/xpay/refund_order?access_token=${accessToken}&pay_sig=${paySig}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: refundBody,
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
}).then(r => r.json())
|
||||||
|
|
||||||
|
if (wxRes.errcode === 0) {
|
||||||
|
wxRefundResult = 'success'
|
||||||
|
logger.info(`[vpay-refund] 微信退款成功: ${order.orderNo}`)
|
||||||
|
} else {
|
||||||
|
wxRefundResult = 'wx_error: ' + (wxRes.errmsg || wxRes.errcode)
|
||||||
|
logger.warn(`[vpay-refund] 微信退款返回异常: ${JSON.stringify(wxRes)}`)
|
||||||
|
// 微信退款失败时不继续本地退款
|
||||||
|
return ApiResponse.error(res, `微信退款失败: ${wxRefundResult},请检查虚拟支付配置`, 500)
|
||||||
|
}
|
||||||
|
} catch (wxErr) {
|
||||||
|
wxRefundResult = 'error: ' + wxErr.message
|
||||||
|
logger.error('[vpay-refund] 微信退款调用失败:', wxErr.message)
|
||||||
|
return ApiResponse.error(res, `微信退款调用失败: ${wxErr.message}`, 500)
|
||||||
|
}
|
||||||
|
|
||||||
await Order.updateOne({ _id: order._id }, { status: 'refunded', refundedAt: new Date() })
|
await Order.updateOne({ _id: order._id }, { status: 'refunded', refundedAt: new Date() })
|
||||||
await CoinLedger.create({
|
await CoinLedger.create({
|
||||||
openid: user.openid, userId: user._id, type: 'refund', amount: -coins,
|
openid: user.openid, userId: user._id, type: 'refund', amount: -coins,
|
||||||
balanceAfter: user.vpayCoins, description: `退款 ${order.orderNo}`,
|
balanceAfter: user.vpayCoins, description: `退款 ${order.orderNo}`,
|
||||||
orderNo: order.orderNo, adminName: req.admin.username,
|
orderNo: order.orderNo, adminName: req.admin.username,
|
||||||
})
|
})
|
||||||
await logAction(req, 'refund', 'vpay_order', order._id, { orderNo: order.orderNo, coins })
|
return ApiResponse.success(res, { orderNo: order.orderNo, coinsDeducted: coins, balanceAfter: user.vpayCoins, wxRefund: wxRefundResult }, '退款成功')
|
||||||
return ApiResponse.success(res, { orderNo: order.orderNo, coinsDeducted: coins, balanceAfter: user.vpayCoins }, '退款成功')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('vpay refund 失败:', error)
|
logger.error('vpay refund 失败:', error)
|
||||||
return ApiResponse.serverError(res, '退款失败')
|
return ApiResponse.serverError(res, '退款失败')
|
||||||
|
|||||||
Reference in New Issue
Block a user