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

12 KiB
Raw Blame History

绑定关系存储方案分析

📊 当前实现

表结构

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 推荐 B30天后过期

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
  • 简单场景方便

劣势

  • 数据一致性差(需要同步)
  • 过期后不准确
  • 切换时需要多表更新
  • 维护成本高

需要同步的场景

  1. 新绑定时
  2. 切换推荐人时
  3. 绑定过期时 ⚠️(当前未同步)
  4. 绑定取消时 ⚠️(当前未同步)

方案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 = ?
// 会查到已过期的用户

解决方案

  1. 停用 users.referred_by,只用 referral_bindings
  2. 或者在过期时清空 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不推荐

如果一定要保留,需要确保同步:

同步点

  1. 新绑定时(已实现)
  2. 切换推荐人时(已实现)
  3. 绑定过期时(需要添加)
  4. 绑定取消时(需要添加)

实现

// 在自动解绑时
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

理由

  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

// 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 表,性能差异微乎其微,但数据一致性大幅提升!