Files
soul-yongping/开发文档/8、部署/提现双向校验实现.md

424 lines
10 KiB
Markdown
Raw Normal View History

# 提现双向校验实现
## 需求
前端和后端都必须使用**相同的逻辑**校验可提现金额,确保安全性和一致性。
## 校验逻辑(统一)
```javascript
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额
```
## 前后端对比
### 前端校验miniprogram
**文件**`miniprogram/pages/referral/referral.js`
**作用**:按钮启用/禁用判断
```javascript
// 计算可提现金额
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`
**作用**:提现申请最终验证
```typescript
// 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. 后端添加已提现金额
**修改前(错误)**
```typescript
// ❌ 只减去待审核,没减去已提现
const availableAmount = totalCommission - pendingWithdrawAmount
```
**修改后(正确)**
```typescript
// ✅ 三元素完整校验
const withdrawnEarnings = parseFloat(user.withdrawn_earnings) || 0
const availableAmount = totalCommission - withdrawnEarnings - pendingWithdrawAmount
```
### 2. 详细日志输出
```typescript
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. 优化错误提示
**修改前**
```typescript
message: `可提现金额不足,当前可提现 ¥${availableAmount.toFixed(2)},待审核 ¥${pendingWithdrawAmount.toFixed(2)}`
```
**修改后**
```typescript
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. 重启后端服务
```bash
python devlop.py restart mycontent
```
### 2. 清除前端缓存
```
微信开发者工具:工具 → 清除缓存 → 清除全部缓存数据
```
### 3. 监控日志
```bash
pm2 logs mycontent --lines 100
```
关注:
- `[Withdraw] 提现验证(完整版):` - 后端校验日志
- `[Referral] 收益计算(完整版)` - 前端计算日志
### 4. 测试验证
- [ ] 正常提现(可提现 > 最低金额)
- [ ] 超额提现(申请金额 > 可提现)
- [ ] 并发提现(快速点击两次)
- [ ] 审核后再提现
## 安全检查清单
- [x] 前端使用三元素计算
- [x] 后端使用三元素校验
- [x] 前后端公式完全一致
- [x] 后端独立查询数据(不信任前端)
- [x] 详细日志记录
- [x] 清晰的错误提示
## 总结
### 双向校验的意义
**前端**
- 用户体验优化
- 快速反馈
- 减少无效请求
**后端**
- 安全防线
- 资金保护
- 最终决策
### 公式统一
```javascript
// 前端和后端完全一致
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额
```
### 防御能力
- ✅ 防篡改
- ✅ 防并发
- ✅ 防重放
- ✅ 防超额
**核心原则**:前端便利,后端安全,双重保障。