Files
soul-yongping/开发文档/8、部署/绑定关系存储方案分析.md

555 lines
12 KiB
Markdown
Raw Normal View History

# 绑定关系存储方案分析
## 📊 当前实现
### 表结构
#### 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 推荐 B30天后过期
#### 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 表,性能差异微乎其微,但数据一致性大幅提升!**