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

555 lines
12 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.

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