555 lines
12 KiB
Markdown
555 lines
12 KiB
Markdown
# 绑定关系存储方案分析
|
||
|
||
## 📊 当前实现
|
||
|
||
### 表结构
|
||
|
||
#### 1. referral_bindings 表(主表)
|
||
```sql
|
||
CREATE TABLE referral_bindings (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
referrer_id VARCHAR(50), -- 推荐人ID
|
||
referee_id VARCHAR(50), -- 被推荐人ID
|
||
referral_code VARCHAR(50), -- 推荐码
|
||
status ENUM('active', 'expired', 'cancelled'), -- 状态
|
||
binding_date DATETIME, -- 绑定时间
|
||
expiry_date DATETIME, -- 过期时间
|
||
last_purchase_date DATETIME, -- 最后购买时间
|
||
purchase_count INT DEFAULT 0, -- 购买次数
|
||
total_commission DECIMAL(10,2) DEFAULT 0.00, -- 累计佣金
|
||
|
||
INDEX idx_referee_status (referee_id, status),
|
||
INDEX idx_referrer_status (referrer_id, status)
|
||
)
|
||
```
|
||
|
||
#### 2. users 表(冗余字段)
|
||
```sql
|
||
CREATE TABLE users (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
referred_by VARCHAR(50), -- 冗余:当前推荐人ID
|
||
referral_count INT DEFAULT 0, -- 冗余:推荐人的推广数量
|
||
referral_code VARCHAR(50), -- 自己的推荐码
|
||
pending_earnings DECIMAL(10,2), -- 待结算收益
|
||
earnings DECIMAL(10,2), -- 已结算收益
|
||
withdrawn_earnings DECIMAL(10,2) -- 已提现金额
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 当前使用情况分析
|
||
|
||
### 1. 绑定关系的创建/更新(/api/referral/bind)
|
||
|
||
**操作**:
|
||
```typescript
|
||
// 1. 查询当前绑定(使用 referral_bindings)
|
||
SELECT * FROM referral_bindings
|
||
WHERE referee_id = ? AND status = 'active'
|
||
|
||
// 2. 创建/更新绑定记录
|
||
INSERT INTO referral_bindings (...)
|
||
|
||
// 3. 同步更新 users.referred_by(冗余)
|
||
UPDATE users SET referred_by = ? WHERE id = ?
|
||
|
||
// 4. 更新 users.referral_count(冗余计数)
|
||
UPDATE users SET referral_count = referral_count + 1 WHERE id = ?
|
||
```
|
||
|
||
**问题**:
|
||
- ✅ `referral_bindings` 是真实来源
|
||
- ⚠️ `users.referred_by` 是冗余,可能不一致
|
||
|
||
---
|
||
|
||
### 2. 支付回调计算佣金(/api/miniprogram/pay/notify)
|
||
|
||
**操作**:
|
||
```typescript
|
||
// 查询绑定关系(使用 referral_bindings)
|
||
SELECT * FROM referral_bindings
|
||
WHERE referee_id = ? AND status = 'active'
|
||
ORDER BY binding_date DESC LIMIT 1
|
||
|
||
// 如果找到 → 给推荐人佣金
|
||
UPDATE users SET pending_earnings = pending_earnings + ? WHERE id = ?
|
||
```
|
||
|
||
**结论**:
|
||
- ✅ 只使用 `referral_bindings`
|
||
- ✅ 不依赖 `users.referred_by`
|
||
|
||
---
|
||
|
||
### 3. 分销中心数据(/api/referral/data)
|
||
|
||
**操作**:
|
||
```typescript
|
||
// 查询活跃绑定
|
||
SELECT * FROM referral_bindings
|
||
WHERE referrer_id = ? AND status = 'active' AND expiry_date > NOW()
|
||
|
||
// 查询已转化用户
|
||
SELECT * FROM referral_bindings
|
||
WHERE referrer_id = ? AND status = 'active' AND purchase_count > 0
|
||
|
||
// 查询过期绑定
|
||
SELECT * FROM referral_bindings
|
||
WHERE referrer_id = ? AND status IN ('expired', 'cancelled')
|
||
```
|
||
|
||
**结论**:
|
||
- ✅ 只使用 `referral_bindings`
|
||
- ✅ 不依赖 `users.referred_by`
|
||
|
||
---
|
||
|
||
### 4. 自动解绑(/api/cron/unbind-expired)
|
||
|
||
**操作**:
|
||
```typescript
|
||
// 查询需要解绑的记录
|
||
SELECT * FROM referral_bindings
|
||
WHERE status = 'active'
|
||
AND expiry_date < NOW()
|
||
AND purchase_count = 0
|
||
|
||
// 批量更新为 expired
|
||
UPDATE referral_bindings SET status = 'expired' WHERE id IN (...)
|
||
|
||
// 更新 referral_count
|
||
UPDATE users SET referral_count = GREATEST(referral_count - ?, 0) WHERE id = ?
|
||
```
|
||
|
||
**结论**:
|
||
- ✅ 只使用 `referral_bindings`
|
||
- ⚠️ 但没有更新 `users.referred_by`(可能导致不一致)
|
||
|
||
---
|
||
|
||
### 5. 旧代码兼容(/api/referral/bind - 旧接口)
|
||
|
||
**操作**:
|
||
```typescript
|
||
// 查询推荐的用户(使用 users.referred_by)
|
||
SELECT * FROM users WHERE referred_by = ?
|
||
```
|
||
|
||
**问题**:
|
||
- ⚠️ 使用了 `users.referred_by`
|
||
- ⚠️ 可能查到已过期的绑定
|
||
- ⚠️ 应该改用 `referral_bindings`
|
||
|
||
---
|
||
|
||
## 📊 数据一致性分析
|
||
|
||
### 场景1: 用户 A 推荐 B,30天后过期
|
||
|
||
#### referral_bindings 表
|
||
```sql
|
||
referrer_id: A
|
||
referee_id: B
|
||
status: expired ✅ 正确
|
||
expiry_date: 2026-01-01
|
||
```
|
||
|
||
#### users 表
|
||
```sql
|
||
B.referred_by: A ⚠️ 仍然是 A(未清空)
|
||
A.referral_count: 1 ⚠️ 未减少(自动解绑任务有更新)
|
||
```
|
||
|
||
**问题**:
|
||
- `users.referred_by` 没有在过期时清空
|
||
- 如果查询 `users.referred_by`,会得到错误结果
|
||
|
||
---
|
||
|
||
### 场景2: B 从 A 切换到 C
|
||
|
||
#### referral_bindings 表
|
||
```sql
|
||
-- 旧绑定
|
||
referrer_id: A
|
||
referee_id: B
|
||
status: cancelled ✅ 正确
|
||
|
||
-- 新绑定
|
||
referrer_id: C
|
||
referee_id: B
|
||
status: active ✅ 正确
|
||
```
|
||
|
||
#### users 表
|
||
```sql
|
||
B.referred_by: C ✅ 正确(已更新)
|
||
A.referral_count: 0 ✅ 正确(已减少)
|
||
C.referral_count: 1 ✅ 正确(已增加)
|
||
```
|
||
|
||
**结论**:切换时同步正确
|
||
|
||
---
|
||
|
||
## 🎯 性能分析
|
||
|
||
### 方案1: 只用 referral_bindings(推荐)
|
||
|
||
**优势**:
|
||
- ✅ 数据一致性强(单一数据源)
|
||
- ✅ 状态清晰(active / expired / cancelled)
|
||
- ✅ 信息完整(过期时间、购买次数等)
|
||
- ✅ 易于维护
|
||
|
||
**劣势**:
|
||
- ❌ 查询需要 JOIN 或多次查询
|
||
- ❌ 复杂查询性能稍低
|
||
|
||
**查询示例**:
|
||
```typescript
|
||
// 查询用户的当前推荐人
|
||
SELECT referrer_id FROM referral_bindings
|
||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||
ORDER BY binding_date DESC LIMIT 1
|
||
```
|
||
|
||
**性能**:
|
||
- 有索引 `idx_referee_status`
|
||
- 查询速度:~0.1ms
|
||
- 适合:几乎所有场景
|
||
|
||
---
|
||
|
||
### 方案2: 冗余到 users 表
|
||
|
||
**优势**:
|
||
- ✅ 查询快(直接读 users.referred_by)
|
||
- ✅ 简单场景方便
|
||
|
||
**劣势**:
|
||
- ❌ 数据一致性差(需要同步)
|
||
- ❌ 过期后不准确
|
||
- ❌ 切换时需要多表更新
|
||
- ❌ 维护成本高
|
||
|
||
**需要同步的场景**:
|
||
1. 新绑定时
|
||
2. 切换推荐人时
|
||
3. 绑定过期时 ⚠️(当前未同步)
|
||
4. 绑定取消时 ⚠️(当前未同步)
|
||
|
||
---
|
||
|
||
### 方案3: 视图或计算字段(推荐)
|
||
|
||
**实现**:
|
||
```sql
|
||
-- 创建视图
|
||
CREATE VIEW user_current_referrer AS
|
||
SELECT
|
||
rb.referee_id as user_id,
|
||
rb.referrer_id,
|
||
u.nickname as referrer_nickname,
|
||
rb.expiry_date,
|
||
rb.purchase_count
|
||
FROM referral_bindings rb
|
||
JOIN users u ON rb.referrer_id = u.id
|
||
WHERE rb.status = 'active'
|
||
AND rb.expiry_date > NOW()
|
||
```
|
||
|
||
**使用**:
|
||
```typescript
|
||
// 查询用户的当前推荐人
|
||
SELECT * FROM user_current_referrer WHERE user_id = ?
|
||
```
|
||
|
||
**优势**:
|
||
- ✅ 数据一致性强
|
||
- ✅ 查询方便
|
||
- ✅ 自动更新
|
||
- ✅ 无需维护冗余
|
||
|
||
---
|
||
|
||
## 🔧 当前问题
|
||
|
||
### 问题1: users.referred_by 不准确
|
||
|
||
**场景**:绑定过期后,`users.referred_by` 仍然有值
|
||
|
||
**影响**:
|
||
```typescript
|
||
// 错误的查询
|
||
SELECT * FROM users WHERE referred_by = ?
|
||
// 会查到已过期的用户
|
||
```
|
||
|
||
**解决方案**:
|
||
1. 停用 `users.referred_by`,只用 `referral_bindings`
|
||
2. 或者在过期时清空 `users.referred_by`
|
||
|
||
---
|
||
|
||
### 问题2: 旧代码依赖 users.referred_by
|
||
|
||
**位置**:`/api/referral/bind` 的 GET 接口
|
||
|
||
```typescript
|
||
// 旧代码
|
||
SELECT * FROM users WHERE referred_by = ?
|
||
```
|
||
|
||
**应该改为**:
|
||
```typescript
|
||
// 新代码
|
||
SELECT u.* FROM users u
|
||
JOIN referral_bindings rb ON u.id = rb.referee_id
|
||
WHERE rb.referrer_id = ?
|
||
AND rb.status = 'active'
|
||
AND rb.expiry_date > NOW()
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 推荐方案
|
||
|
||
### 方案A: 渐进式优化(推荐)
|
||
|
||
**步骤1: 停用 users.referred_by**
|
||
- 不再更新 `users.referred_by`
|
||
- 所有查询改用 `referral_bindings`
|
||
|
||
**步骤2: 优化索引**
|
||
- 确保 `referral_bindings` 有合适的索引
|
||
- `idx_referee_status` ✅ 已有
|
||
- `idx_referrer_status` ✅ 已有
|
||
|
||
**步骤3: 创建辅助函数**
|
||
```typescript
|
||
// 获取用户的当前推荐人
|
||
async function getCurrentReferrer(userId: string) {
|
||
const bindings = await query(`
|
||
SELECT referrer_id, expiry_date, purchase_count
|
||
FROM referral_bindings
|
||
WHERE referee_id = ?
|
||
AND status = 'active'
|
||
AND expiry_date > NOW()
|
||
ORDER BY binding_date DESC
|
||
LIMIT 1
|
||
`, [userId])
|
||
|
||
return bindings[0]?.referrer_id || null
|
||
}
|
||
```
|
||
|
||
**优势**:
|
||
- ✅ 数据一致性强
|
||
- ✅ 无需维护冗余
|
||
- ✅ 性能优秀(有索引)
|
||
- ✅ 维护成本低
|
||
|
||
---
|
||
|
||
### 方案B: 保留 users.referred_by(不推荐)
|
||
|
||
如果一定要保留,需要确保同步:
|
||
|
||
**同步点**:
|
||
1. ✅ 新绑定时(已实现)
|
||
2. ✅ 切换推荐人时(已实现)
|
||
3. ❌ 绑定过期时(需要添加)
|
||
4. ❌ 绑定取消时(需要添加)
|
||
|
||
**实现**:
|
||
```typescript
|
||
// 在自动解绑时
|
||
UPDATE users SET referred_by = NULL
|
||
WHERE id IN (
|
||
SELECT referee_id FROM referral_bindings
|
||
WHERE status = 'expired'
|
||
)
|
||
```
|
||
|
||
**劣势**:
|
||
- ❌ 维护成本高
|
||
- ❌ 容易出错
|
||
- ❌ 收益不大
|
||
|
||
---
|
||
|
||
## 📊 性能对比
|
||
|
||
### 查询1: 获取用户的推荐人
|
||
|
||
#### 使用 users.referred_by
|
||
```sql
|
||
SELECT referred_by FROM users WHERE id = ?
|
||
```
|
||
- 耗时:~0.01ms
|
||
- 准确性:❌ 可能过期
|
||
|
||
#### 使用 referral_bindings
|
||
```sql
|
||
SELECT referrer_id FROM referral_bindings
|
||
WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW()
|
||
LIMIT 1
|
||
```
|
||
- 耗时:~0.1ms(有索引)
|
||
- 准确性:✅ 完全准确
|
||
|
||
**差异**:0.09ms(几乎可以忽略)
|
||
|
||
---
|
||
|
||
### 查询2: 获取推荐人的下级列表
|
||
|
||
#### 使用 users.referred_by
|
||
```sql
|
||
SELECT * FROM users WHERE referred_by = ?
|
||
```
|
||
- 耗时:~1ms
|
||
- 准确性:❌ 包含过期用户
|
||
|
||
#### 使用 referral_bindings
|
||
```sql
|
||
SELECT u.* FROM users u
|
||
JOIN referral_bindings rb ON u.id = rb.referee_id
|
||
WHERE rb.referrer_id = ?
|
||
AND rb.status = 'active'
|
||
AND rb.expiry_date > NOW()
|
||
```
|
||
- 耗时:~1.5ms(有索引)
|
||
- 准确性:✅ 完全准确
|
||
|
||
**差异**:0.5ms(可接受)
|
||
|
||
---
|
||
|
||
## ✅ 结论与建议
|
||
|
||
### 推荐:方案A(只用 referral_bindings)
|
||
|
||
**理由**:
|
||
1. ✅ **数据一致性**:单一数据源,避免不一致
|
||
2. ✅ **逻辑清晰**:状态明确(active / expired / cancelled)
|
||
3. ✅ **维护简单**:无需同步冗余字段
|
||
4. ✅ **性能优秀**:有合适的索引,差异可忽略
|
||
5. ✅ **功能完整**:支持过期、切换、购买次数等
|
||
|
||
### 不推荐:保留 users.referred_by
|
||
|
||
**理由**:
|
||
1. ❌ 数据一致性差(容易出错)
|
||
2. ❌ 维护成本高(多处同步)
|
||
3. ❌ 性能提升微乎其微(0.09ms)
|
||
4. ❌ 功能受限(无法判断是否过期)
|
||
|
||
---
|
||
|
||
## 🔧 优化建议
|
||
|
||
### 短期优化(立即执行)
|
||
|
||
1. **停用 users.referred_by 的写入**
|
||
- 不再更新这个字段
|
||
- 保留字段(避免破坏性变更)
|
||
|
||
2. **修改旧查询**
|
||
- 找到所有使用 `users.referred_by` 的查询
|
||
- 改用 `referral_bindings`
|
||
|
||
3. **添加辅助函数**
|
||
- 封装常用查询
|
||
- 简化代码
|
||
|
||
### 中期优化(1-2周内)
|
||
|
||
1. **性能监控**
|
||
- 监控查询性能
|
||
- 确保没有性能问题
|
||
|
||
2. **数据清理**
|
||
- 可选:清空 `users.referred_by`
|
||
- 避免误用
|
||
|
||
### 长期优化(可选)
|
||
|
||
1. **删除冗余字段**
|
||
- 如果确认不再使用
|
||
- 彻底删除 `users.referred_by`
|
||
|
||
2. **创建视图或缓存**
|
||
- 如果有特殊性能需求
|
||
- 考虑 Redis 缓存
|
||
|
||
---
|
||
|
||
## 📝 具体修改建议
|
||
|
||
### 1. 停止更新 users.referred_by
|
||
|
||
```typescript
|
||
// app/api/referral/bind/route.ts
|
||
|
||
// 删除或注释掉这行
|
||
// await query('UPDATE users SET referred_by = ? WHERE id = ?', [referrer.id, user.id])
|
||
```
|
||
|
||
### 2. 修改旧查询
|
||
|
||
```typescript
|
||
// 旧代码
|
||
const users = await query('SELECT * FROM users WHERE referred_by = ?', [userId])
|
||
|
||
// 新代码
|
||
const users = await query(`
|
||
SELECT u.* FROM users u
|
||
JOIN referral_bindings rb ON u.id = rb.referee_id
|
||
WHERE rb.referrer_id = ?
|
||
AND rb.status = 'active'
|
||
AND rb.expiry_date > NOW()
|
||
`, [userId])
|
||
```
|
||
|
||
### 3. 添加辅助函数
|
||
|
||
```typescript
|
||
// lib/referral-helpers.ts
|
||
|
||
export async function getCurrentReferrer(userId: string) {
|
||
const bindings = await query(`
|
||
SELECT referrer_id, expiry_date, purchase_count, total_commission
|
||
FROM referral_bindings
|
||
WHERE referee_id = ?
|
||
AND status = 'active'
|
||
AND expiry_date > NOW()
|
||
ORDER BY binding_date DESC
|
||
LIMIT 1
|
||
`, [userId])
|
||
|
||
return bindings[0] || null
|
||
}
|
||
|
||
export async function getActiveReferrals(referrerId: string) {
|
||
return await query(`
|
||
SELECT
|
||
u.id, u.nickname, u.avatar,
|
||
rb.binding_date, rb.expiry_date, rb.purchase_count, rb.total_commission
|
||
FROM referral_bindings rb
|
||
JOIN users u ON rb.referee_id = u.id
|
||
WHERE rb.referrer_id = ?
|
||
AND rb.status = 'active'
|
||
AND rb.expiry_date > NOW()
|
||
ORDER BY rb.binding_date DESC
|
||
`, [referrerId])
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
**总结:建议停用 users.referred_by,只使用 referral_bindings 表,性能差异微乎其微,但数据一致性大幅提升!**
|