12 KiB
12 KiB
绑定关系存储方案分析
📊 当前实现
表结构
1. referral_bindings 表(主表)
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 表(冗余字段)
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)
操作:
// 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)
操作:
// 查询绑定关系(使用 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)
操作:
// 查询活跃绑定
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)
操作:
// 查询需要解绑的记录
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 - 旧接口)
操作:
// 查询推荐的用户(使用 users.referred_by)
SELECT * FROM users WHERE referred_by = ?
问题:
- ⚠️ 使用了
users.referred_by - ⚠️ 可能查到已过期的绑定
- ⚠️ 应该改用
referral_bindings
📊 数据一致性分析
场景1: 用户 A 推荐 B,30天后过期
referral_bindings 表
referrer_id: A
referee_id: B
status: expired ✅ 正确
expiry_date: 2026-01-01
users 表
B.referred_by: A ⚠️ 仍然是 A(未清空)
A.referral_count: 1 ⚠️ 未减少(自动解绑任务有更新)
问题:
users.referred_by没有在过期时清空- 如果查询
users.referred_by,会得到错误结果
场景2: B 从 A 切换到 C
referral_bindings 表
-- 旧绑定
referrer_id: A
referee_id: B
status: cancelled ✅ 正确
-- 新绑定
referrer_id: C
referee_id: B
status: active ✅ 正确
users 表
B.referred_by: C ✅ 正确(已更新)
A.referral_count: 0 ✅ 正确(已减少)
C.referral_count: 1 ✅ 正确(已增加)
结论:切换时同步正确
🎯 性能分析
方案1: 只用 referral_bindings(推荐)
优势:
- ✅ 数据一致性强(单一数据源)
- ✅ 状态清晰(active / expired / cancelled)
- ✅ 信息完整(过期时间、购买次数等)
- ✅ 易于维护
劣势:
- ❌ 查询需要 JOIN 或多次查询
- ❌ 复杂查询性能稍低
查询示例:
// 查询用户的当前推荐人
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)
- ✅ 简单场景方便
劣势:
- ❌ 数据一致性差(需要同步)
- ❌ 过期后不准确
- ❌ 切换时需要多表更新
- ❌ 维护成本高
需要同步的场景:
- 新绑定时
- 切换推荐人时
- 绑定过期时 ⚠️(当前未同步)
- 绑定取消时 ⚠️(当前未同步)
方案3: 视图或计算字段(推荐)
实现:
-- 创建视图
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()
使用:
// 查询用户的当前推荐人
SELECT * FROM user_current_referrer WHERE user_id = ?
优势:
- ✅ 数据一致性强
- ✅ 查询方便
- ✅ 自动更新
- ✅ 无需维护冗余
🔧 当前问题
问题1: users.referred_by 不准确
场景:绑定过期后,users.referred_by 仍然有值
影响:
// 错误的查询
SELECT * FROM users WHERE referred_by = ?
// 会查到已过期的用户
解决方案:
- 停用
users.referred_by,只用referral_bindings - 或者在过期时清空
users.referred_by
问题2: 旧代码依赖 users.referred_by
位置:/api/referral/bind 的 GET 接口
// 旧代码
SELECT * FROM users WHERE referred_by = ?
应该改为:
// 新代码
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: 创建辅助函数
// 获取用户的当前推荐人
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(不推荐)
如果一定要保留,需要确保同步:
同步点:
- ✅ 新绑定时(已实现)
- ✅ 切换推荐人时(已实现)
- ❌ 绑定过期时(需要添加)
- ❌ 绑定取消时(需要添加)
实现:
// 在自动解绑时
UPDATE users SET referred_by = NULL
WHERE id IN (
SELECT referee_id FROM referral_bindings
WHERE status = 'expired'
)
劣势:
- ❌ 维护成本高
- ❌ 容易出错
- ❌ 收益不大
📊 性能对比
查询1: 获取用户的推荐人
使用 users.referred_by
SELECT referred_by FROM users WHERE id = ?
- 耗时:~0.01ms
- 准确性:❌ 可能过期
使用 referral_bindings
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
SELECT * FROM users WHERE referred_by = ?
- 耗时:~1ms
- 准确性:❌ 包含过期用户
使用 referral_bindings
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)
理由:
- ✅ 数据一致性:单一数据源,避免不一致
- ✅ 逻辑清晰:状态明确(active / expired / cancelled)
- ✅ 维护简单:无需同步冗余字段
- ✅ 性能优秀:有合适的索引,差异可忽略
- ✅ 功能完整:支持过期、切换、购买次数等
不推荐:保留 users.referred_by
理由:
- ❌ 数据一致性差(容易出错)
- ❌ 维护成本高(多处同步)
- ❌ 性能提升微乎其微(0.09ms)
- ❌ 功能受限(无法判断是否过期)
🔧 优化建议
短期优化(立即执行)
-
停用 users.referred_by 的写入
- 不再更新这个字段
- 保留字段(避免破坏性变更)
-
修改旧查询
- 找到所有使用
users.referred_by的查询 - 改用
referral_bindings
- 找到所有使用
-
添加辅助函数
- 封装常用查询
- 简化代码
中期优化(1-2周内)
-
性能监控
- 监控查询性能
- 确保没有性能问题
-
数据清理
- 可选:清空
users.referred_by - 避免误用
- 可选:清空
长期优化(可选)
-
删除冗余字段
- 如果确认不再使用
- 彻底删除
users.referred_by
-
创建视图或缓存
- 如果有特殊性能需求
- 考虑 Redis 缓存
📝 具体修改建议
1. 停止更新 users.referred_by
// app/api/referral/bind/route.ts
// 删除或注释掉这行
// await query('UPDATE users SET referred_by = ? WHERE id = ?', [referrer.id, user.id])
2. 修改旧查询
// 旧代码
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. 添加辅助函数
// 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 表,性能差异微乎其微,但数据一致性大幅提升!