diff --git a/server/src/routes/admin.js b/server/src/routes/admin.js index 64bb7a1..1e8ebb3 100755 --- a/server/src/routes/admin.js +++ b/server/src/routes/admin.js @@ -1238,8 +1238,37 @@ router.post('/vpay/orders/:id/refund', requirePermission('orders:write'), async const user = await User.findById(order.userId) if (!user) return ApiResponse.error(res, '用户不存在', 400) - // 扣回星尘(允许扣到负数,管理员可判断) - user.vpayCoins = (user.vpayCoins || 0) - coins + // ===== 退款风控(三层,防恶意充值→消费→退款循环) ===== + // 第一层:余额必须够扣(星尘已消费 = 虚拟商品已使用,不支持退款) + const currentBalance = user.vpayCoins || 0 + if (currentBalance < coins) { + return ApiResponse.error(res, + `不可退款:用户星尘余额 ${currentBalance},需扣 ${coins}。已消费的星尘不支持退款。`, + 409, { blocked: 'insufficient_balance', currentBalance, coinsToDeduct: coins }) + } + + // 第二层:退款时间窗口(支付后 7 天内可退) + const paidAt = order.paidAt || order.createdAt + const daysSincePaid = (Date.now() - new Date(paidAt).getTime()) / 86400000 + if (daysSincePaid > 7) { + return ApiResponse.error(res, + `不可退款:订单支付已 ${Math.floor(daysSincePaid)} 天,超过 7 天退款窗口。`, + 409, { blocked: 'expired' }) + } + + // 第三层:同一用户 30 天内最多 1 次退款 + const recentRefund = await Order.findOne({ + userId: user._id, status: 'refunded', + refundedAt: { $gte: new Date(Date.now() - 30 * 86400000) }, + }) + if (recentRefund) { + return ApiResponse.error(res, + `不可退款:该用户 30 天内已有退款记录(${recentRefund.orderNo})。每人每月限退 1 次。`, + 409, { blocked: 'rate_limited', lastRefund: recentRefund.orderNo }) + } + + // 风控通过:先调微信退钱(钱是用户的)→ 成功后本地扣星尘 + user.vpayCoins = currentBalance - coins await user.save() // ===== 调用微信虚拟支付退款 API(真正退钱给用户) =====