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