diff --git a/server/src/models/AdminLog.js b/server/src/models/AdminLog.js index 79705a3..3d9b1d3 100755 --- a/server/src/models/AdminLog.js +++ b/server/src/models/AdminLog.js @@ -1,54 +1,54 @@ -const mongoose = require('mongoose') -const { Schema } = mongoose - -/** - * 管理员操作日志模型 - */ -const AdminLogSchema = new Schema({ - // 管理员信息 - adminId: { - type: Schema.Types.ObjectId, - ref: 'Admin', - required: true, - index: true - }, - adminName: { - type: String, - required: true - }, - - // 操作信息 - action: { - type: String, - required: true, - enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export'] - }, - resource: { - type: String, - required: true - }, - resourceId: String, - - // 详细信息 - details: Schema.Types.Mixed, - - // IP 地址 - ip: String, - userAgent: String, - - // 时间戳 - timestamp: { - type: Date, - default: Date.now, - index: true - } -}, { - timestamps: false -}) - -// 索引 -AdminLogSchema.index({ adminId: 1, timestamp: -1 }) -AdminLogSchema.index({ resource: 1, timestamp: -1 }) -AdminLogSchema.index({ action: 1 }) - -module.exports = mongoose.model('AdminLog', AdminLogSchema) +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 管理员操作日志模型 + */ +const AdminLogSchema = new Schema({ + // 管理员信息 + adminId: { + type: Schema.Types.ObjectId, + ref: 'Admin', + required: true, + index: true + }, + adminName: { + type: String, + required: true + }, + + // 操作信息 + action: { + type: String, + required: true, + enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export', 'refund', 'adjust'] + }, + resource: { + type: String, + required: true + }, + resourceId: String, + + // 详细信息 + details: Schema.Types.Mixed, + + // IP 地址 + ip: String, + userAgent: String, + + // 时间戳 + timestamp: { + type: Date, + default: Date.now, + index: true + } +}, { + timestamps: false +}) + +// 索引 +AdminLogSchema.index({ adminId: 1, timestamp: -1 }) +AdminLogSchema.index({ resource: 1, timestamp: -1 }) +AdminLogSchema.index({ action: 1 }) + +module.exports = mongoose.model('AdminLog', AdminLogSchema) diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js index 5b5ee85..64bb7a1 100755 --- a/server/src/routes/admin.js +++ b/server/src/routes/admin.js @@ -1232,22 +1232,60 @@ router.post('/vpay/orders/:id/refund', requirePermission('orders:write'), async if (!order) return ApiResponse.notFound(res, '订单不存在') 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) if (!user) return ApiResponse.error(res, '用户不存在', 400) - // 扣回星尘(可能不够扣,允许负数让管理员判断) + // 扣回星尘(允许扣到负数,管理员可判断) user.vpayCoins = (user.vpayCoins || 0) - coins 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 CoinLedger.create({ openid: user.openid, userId: user._id, type: 'refund', amount: -coins, balanceAfter: user.vpayCoins, description: `退款 ${order.orderNo}`, 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 }, '退款成功') + return ApiResponse.success(res, { orderNo: order.orderNo, coinsDeducted: coins, balanceAfter: user.vpayCoins, wxRefund: wxRefundResult }, '退款成功') } catch (error) { logger.error('vpay refund 失败:', error) return ApiResponse.serverError(res, '退款失败')