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