feat: 退款三层风控——余额必须够扣(已消费不可退)+7天窗口+每人每月限1次

This commit is contained in:
2026-09-14 13:03:01 +08:00
parent 7b92a7308d
commit f6d99aff3c
+31 -2
View File
@@ -1238,8 +1238,37 @@ router.post('/vpay/orders/:id/refund', requirePermission('orders:write'), async
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 // 第一层:余额必须够扣(星尘已消费 = 虚拟商品已使用,不支持退款)
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() await user.save()
// ===== 调用微信虚拟支付退款 API(真正退钱给用户) ===== // ===== 调用微信虚拟支付退款 API(真正退钱给用户) =====