Files
soul-yongping/开发文档/8、部署/提现双向校验实现.md
2026-02-09 15:09:29 +08:00

10 KiB
Raw Blame History

提现双向校验实现

需求

前端和后端都必须使用相同的逻辑校验可提现金额,确保安全性和一致性。

校验逻辑(统一)

可提现金额 = 累计佣金 - 已提现金额 - 待审核金额

前后端对比

前端校验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
  • 数据库事务保证原子性

场景3API 重放攻击

攻击

  • 捕获提现请求,重复发送

防御

  • 后端实时校验可提现金额
  • 创建提现记录后,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 相同查询

同步机制

  1. 前端数据:来自 /api/referral/data
  2. 后端校验:独立查询,实时数据
  3. 一致性保证
    • 相同的数据库表
    • 相同的计算公式
    • 后端数据更准确(实时查询)

相关文件

  • 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. 测试验证

  • 正常提现(可提现 > 最低金额)
  • 超额提现(申请金额 > 可提现)
  • 并发提现(快速点击两次)
  • 审核后再提现

安全检查清单

  • 前端使用三元素计算
  • 后端使用三元素校验
  • 前后端公式完全一致
  • 后端独立查询数据(不信任前端)
  • 详细日志记录
  • 清晰的错误提示

总结

双向校验的意义

前端

  • 用户体验优化
  • 快速反馈
  • 减少无效请求

后端

  • 安全防线
  • 资金保护
  • 最终决策

公式统一

// 前端和后端完全一致
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额

防御能力

  • 防篡改
  • 防并发
  • 防重放
  • 防超额

核心原则:前端便利,后端安全,双重保障。