10 KiB
10 KiB
提现双向校验实现
需求
前端和后端都必须使用相同的逻辑校验可提现金额,确保安全性和一致性。
校验逻辑(统一)
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额
前后端对比
前端校验(miniprogram)
文件:miniprogram/pages/referral/referral.js
作用:按钮启用/禁用判断
// 计算可提现金额
const totalCommissionNum = realData?.totalCommission || 0
const withdrawnNum = realData?.withdrawnEarnings || 0
const pendingWithdrawNum = realData?.pendingWithdrawAmount || 0
const availableEarningsNum = totalCommissionNum - withdrawnNum - pendingWithdrawNum
// 判断按钮状态
if (availableEarningsNum >= minWithdrawAmount) {
// 启用按钮
} else {
// 禁用按钮
}
后端校验(API)
文件:app/api/withdraw/route.ts
作用:提现申请最终验证
// 1. 查询累计佣金
const ordersResult = await query(`
SELECT COALESCE(SUM(amount), 0) as total_amount
FROM orders
WHERE referrer_id = ? AND status = 'paid'
`, [userId])
const totalAmount = parseFloat(ordersResult[0]?.total_amount || 0)
const totalCommission = totalAmount * distributorShare
// 2. 读取已提现金额
const withdrawnEarnings = parseFloat(user.withdrawn_earnings) || 0
// 3. 查询待审核金额
const pendingResult = await query(`
SELECT COALESCE(SUM(amount), 0) as pending_amount
FROM withdrawals
WHERE user_id = ? AND status = 'pending'
`, [userId])
const pendingWithdrawAmount = parseFloat(pendingResult[0]?.pending_amount || 0)
// 4. 计算可提现金额
const availableAmount = totalCommission - withdrawnEarnings - pendingWithdrawAmount
// 5. 验证
if (amount > availableAmount) {
return NextResponse.json({
success: false,
message: `可提现金额不足。当前可提现 ¥${availableAmount.toFixed(2)}(累计 ¥${totalCommission.toFixed(2)} - 已提现 ¥${withdrawnEarnings.toFixed(2)} - 待审核 ¥${pendingWithdrawAmount.toFixed(2)})`
})
}
修改内容
1. 后端添加已提现金额
修改前(错误):
// ❌ 只减去待审核,没减去已提现
const availableAmount = totalCommission - pendingWithdrawAmount
修改后(正确):
// ✅ 三元素完整校验
const withdrawnEarnings = parseFloat(user.withdrawn_earnings) || 0
const availableAmount = totalCommission - withdrawnEarnings - pendingWithdrawAmount
2. 详细日志输出
console.log('[Withdraw] 提现验证(完整版):')
console.log('- 累计佣金 (totalCommission):', totalCommission)
console.log('- 已提现金额 (withdrawnEarnings):', withdrawnEarnings)
console.log('- 待审核金额 (pendingWithdrawAmount):', pendingWithdrawAmount)
console.log('- 可提现金额 = 累计 - 已提现 - 待审核 =', totalCommission, '-', withdrawnEarnings, '-', pendingWithdrawAmount, '=', availableAmount)
console.log('- 申请提现金额 (amount):', amount)
console.log('- 判断:', amount, '>', availableAmount, '=', amount > availableAmount)
3. 优化错误提示
修改前:
message: `可提现金额不足,当前可提现 ¥${availableAmount.toFixed(2)},待审核 ¥${pendingWithdrawAmount.toFixed(2)}`
修改后:
message: `可提现金额不足。当前可提现 ¥${availableAmount.toFixed(2)}(累计 ¥${totalCommission.toFixed(2)} - 已提现 ¥${withdrawnEarnings.toFixed(2)} - 待审核 ¥${pendingWithdrawAmount.toFixed(2)})`
双向校验流程
用户点击提现按钮
↓
┌──────────────────────────┐
│ 前端校验(第一层) │
│ 按钮启用/禁用判断 │
├──────────────────────────┤
│ 可提现 >= 最低金额? │
│ YES → 允许点击 │
│ NO → 按钮禁用 │
└──────────────────────────┘
↓
用户确认提现
↓
┌──────────────────────────┐
│ 后端校验(第二层) │
│ API 最终验证 │
├──────────────────────────┤
│ 1. 查询累计佣金 │
│ 2. 读取已提现金额 │
│ 3. 查询待审核金额 │
│ 4. 计算可提现金额 │
│ 5. amount > available? │
│ YES → 拒绝提现 │
│ NO → 允许提现 │
└──────────────────────────┘
↓
创建提现记录
为什么需要双向校验?
前端校验的作用
优点:
- ✅ 快速响应,提升用户体验
- ✅ 减少无效请求,降低服务器压力
- ✅ 按钮禁用,防止误操作
局限:
- ❌ 数据可能过期(API 数据缓存)
- ❌ 可被绕过(客户端不可信)
- ❌ 无法阻止恶意请求
后端校验的作用
优点:
- ✅ 最终防线,确保资金安全
- ✅ 实时数据,准确无误
- ✅ 不可绕过,强制验证
必要性:
- ✅ 防止前端被篡改
- ✅ 防止并发提现
- ✅ 防止逻辑漏洞
攻击场景防御
场景1:前端被篡改
攻击:
- 黑客修改前端代码,移除按钮禁用逻辑
- 强制发送提现请求
防御:
- ✅ 后端独立校验,拒绝超额提现
场景2:并发提现
攻击:
- 用户快速点击两次提现按钮
- 或同时在多个设备上提现
防御:
- ✅ 后端查询最新的
pendingWithdrawAmount - ✅ 数据库事务保证原子性
场景3:API 重放攻击
攻击:
- 捕获提现请求,重复发送
防御:
- ✅ 后端实时校验可提现金额
- ✅ 创建提现记录后,
pendingWithdrawAmount增加 - ✅ 第二次请求时,可提现金额不足,拒绝
测试用例
测试1:正常提现
数据:
- 累计佣金: ¥100
- 已提现: ¥0
- 待审核: ¥0
- 申请提现: ¥50
前端:
可提现 = 100 - 0 - 0 = 100
50 < 100 → 按钮启用 ✅
后端:
可提现 = 100 - 0 - 0 = 100
50 ≤ 100 → 允许提现 ✅
结果:✅ 提现成功
测试2:超额提现
数据:
- 累计佣金: ¥100
- 已提现: ¥0
- 待审核: ¥0
- 申请提现: ¥150
前端:
可提现 = 100 - 0 - 0 = 100
150 > 100 → 按钮禁用 ✅
(正常情况下用户无法点击)
后端(假设黑客绕过前端):
可提现 = 100 - 0 - 0 = 100
150 > 100 → 拒绝提现 ✅
返回错误:可提现金额不足
结果:✅ 后端拦截成功
测试3:重复提现
数据:
- 累计佣金: ¥100
- 已提现: ¥0
- 待审核: ¥0
第一次提现 ¥50:
前端:100 - 0 - 0 = 100,允许 ✅
后端:100 - 0 - 0 = 100,允许 ✅
创建提现记录,pending += 50
第二次提现 ¥60(并发或快速点击):
前端:可能还是显示 100(数据未刷新)
后端:100 - 0 - 50 = 50
60 > 50 → 拒绝提现 ✅
结果:✅ 后端防御成功
测试4:审核通过后再次提现
初始:
- 累计佣金: ¥100
- 已提现: ¥50(之前提现已到账)
- 待审核: ¥0
申请提现 ¥60:
前端:100 - 50 - 0 = 50
60 > 50 → 按钮禁用 ✅
后端(假设绕过前端):
100 - 50 - 0 = 50
60 > 50 → 拒绝提现 ✅
结果:✅ 双重防护
日志示例
前端日志
=== [Referral] 收益计算(完整版)===
累计佣金 (totalCommission): 100
已提现金额 (withdrawnEarnings): 50
待审核金额 (pendingWithdrawAmount): 0
可提现金额 = 累计 - 已提现 - 待审核 = 100 - 50 - 0 = 50
最低提现金额 (minWithdrawAmount): 5
按钮判断: 50 >= 5 = true
✅ 按钮应该: 🟢 启用(绿色)
后端日志
[Withdraw] 佣金计算:
- 订单总金额: 111.11
- 分成比例: 90%
- 累计佣金: 100
[Withdraw] 提现验证(完整版):
- 累计佣金 (totalCommission): 100
- 已提现金额 (withdrawnEarnings): 50
- 待审核金额 (pendingWithdrawAmount): 0
- 可提现金额 = 累计 - 已提现 - 待审核 = 100 - 50 - 0 = 50
- 申请提现金额 (amount): 50
- 判断: 50 > 50 = false
✅ 提现申请通过
数据一致性保证
数据来源
| 字段 | 前端数据来源 | 后端数据来源 | 一致性 |
|---|---|---|---|
| 累计佣金 | /api/referral/data |
实时查询 orders |
✅ 相同算法 |
| 已提现 | /api/referral/data |
读取 users.withdrawn_earnings |
✅ 相同字段 |
| 待审核 | /api/referral/data |
实时查询 withdrawals |
✅ 相同查询 |
同步机制
- 前端数据:来自
/api/referral/data - 后端校验:独立查询,实时数据
- 一致性保证:
- 相同的数据库表
- 相同的计算公式
- 后端数据更准确(实时查询)
相关文件
miniprogram/pages/referral/referral.js- 前端校验 ✅app/api/withdraw/route.ts- 后端校验 ✅app/api/referral/data/route.ts- 数据查询
部署注意事项
1. 重启后端服务
python devlop.py restart mycontent
2. 清除前端缓存
微信开发者工具:工具 → 清除缓存 → 清除全部缓存数据
3. 监控日志
pm2 logs mycontent --lines 100
关注:
[Withdraw] 提现验证(完整版):- 后端校验日志[Referral] 收益计算(完整版)- 前端计算日志
4. 测试验证
- 正常提现(可提现 > 最低金额)
- 超额提现(申请金额 > 可提现)
- 并发提现(快速点击两次)
- 审核后再提现
安全检查清单
- 前端使用三元素计算
- 后端使用三元素校验
- 前后端公式完全一致
- 后端独立查询数据(不信任前端)
- 详细日志记录
- 清晰的错误提示
总结
双向校验的意义
前端:
- 用户体验优化
- 快速反馈
- 减少无效请求
后端:
- 安全防线
- 资金保护
- 最终决策
公式统一
// 前端和后端完全一致
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额
防御能力
- ✅ 防篡改
- ✅ 防并发
- ✅ 防重放
- ✅ 防超额
核心原则:前端便利,后端安全,双重保障。