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

424 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 提现双向校验实现
## 需求
前端和后端都必须使用**相同的逻辑**校验可提现金额,确保安全性和一致性。
## 校验逻辑(统一)
```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
// 前端和后端完全一致
可提现金额 = 累计佣金 - 已提现金额 - 待审核金额
```
### 防御能力
- ✅ 防篡改
- ✅ 防并发
- ✅ 防重放
- ✅ 防超额
**核心原则**:前端便利,后端安全,双重保障。