Files
soul-yongping/开发文档/8、部署/新分销逻辑设计方案.md

11 KiB
Raw Blame History

新分销逻辑设计方案

📌 业务需求

核心规则

  1. 动态绑定用户B点击谁的分享链接立即绑定谁无条件切换
  2. 佣金归属B购买时佣金给当前推荐人最新绑定的那个人
  3. 自动解绑绑定30天内如果B既没点击其他链接也没有任何购买 → 自动解绑

场景示例

时间线:
Day 0:  A推荐B → B注册 → B绑定A30天有效期
Day 5:  B点击C的链接 → B立即切换绑定C重新开始30天有效期
Day 10: B购买文章 → 佣金给C当前推荐人
Day 35: 绑定C的30天到期如果期间无购买 → 自动解绑

🗄️ 数据库设计

1. referral_bindings 表字段调整

字段 类型 说明 新增/修改
id VARCHAR(64) 主键 -
referee_id VARCHAR(64) 被推荐人B -
referrer_id VARCHAR(64) 推荐人(当前) -
referral_code VARCHAR(20) 推荐码 -
status ENUM active/converted/expired/cancelled 新增 cancelled
binding_date TIMESTAMP 最后一次绑定时间 -
expiry_date DATETIME 过期时间30天后 -
last_purchase_date DATETIME 最后一次购买时间 新增
purchase_count INT 购买次数 新增
total_commission DECIMAL 累计佣金 新增

2. 新增字段的 SQL

-- 添加新字段
ALTER TABLE referral_bindings 
ADD COLUMN last_purchase_date DATETIME NULL COMMENT '最后一次购买时间',
ADD COLUMN purchase_count INT DEFAULT 0 COMMENT '购买次数',
ADD COLUMN total_commission DECIMAL(10,2) DEFAULT 0.00 COMMENT '累计佣金',
ADD INDEX idx_expiry_status (expiry_date, status);

-- 修改 status 枚举(如果需要)
ALTER TABLE referral_bindings 
MODIFY COLUMN status ENUM('active', 'converted', 'expired', 'cancelled') DEFAULT 'active';

🔧 API 逻辑修改

1. /api/referral/bind - 立即切换绑定

修改前逻辑(现有):

if (existingBinding && expiryDate > now) {
  return { error: '绑定有效期内无法更换' } // ❌ 阻止切换
}

修改后逻辑(新):

// 查询B当前的绑定
const existingBinding = await query(`
  SELECT * FROM referral_bindings 
  WHERE referee_id = ? AND status = 'active'
`, [userId])

if (existingBinding.length > 0) {
  const current = existingBinding[0]
  
  // 情况1: 同一个推荐人 → 续期刷新30天
  if (current.referrer_id === newReferrerId) {
    await query(`
      UPDATE referral_bindings 
      SET expiry_date = DATE_ADD(NOW(), INTERVAL 30 DAY),
          binding_date = NOW()
      WHERE id = ?
    `, [current.id])
    return { success: true, action: 'renewed' }
  }
  
  // 情况2: 不同推荐人 → 立即切换
  else {
    // 旧绑定标记为 cancelled
    await query(`
      UPDATE referral_bindings 
      SET status = 'cancelled'
      WHERE id = ?
    `, [current.id])
    
    // 创建新绑定
    await query(`
      INSERT INTO referral_bindings 
      (id, referee_id, referrer_id, referral_code, status, binding_date, expiry_date)
      VALUES (?, ?, ?, ?, 'active', NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY))
    `, [newBindingId, userId, newReferrerId, referralCode])
    
    return { success: true, action: 'switched' }
  }
}

关键变化

  • 删除"有效期内不能切换"的限制
  • 旧绑定标记为 cancelled(而不是 expired
  • 立即创建新绑定重新计算30天

2. /api/miniprogram/pay/notify - 支付回调更新

修改前逻辑(现有):

// 更新绑定为 converted
await query(`
  UPDATE referral_bindings 
  SET status = 'converted', 
      conversion_date = NOW(),
      commission_amount = ?
  WHERE id = ?
`, [commission, bindingId])

修改后逻辑(新):

// 查询B当前的绑定active状态
const binding = await query(`
  SELECT * FROM referral_bindings 
  WHERE referee_id = ? AND status = 'active'
  ORDER BY binding_date DESC LIMIT 1
`, [userId])

if (binding.length === 0) {
  console.log('[PayNotify] 无有效绑定,跳过分佣')
  return
}

const currentBinding = binding[0]
const referrerId = currentBinding.referrer_id

// 计算佣金
const commission = amount * distributorShare

// 更新绑定记录(累加购买次数和佣金)
await query(`
  UPDATE referral_bindings 
  SET last_purchase_date = NOW(),
      purchase_count = purchase_count + 1,
      total_commission = total_commission + ?
  WHERE id = ?
`, [commission, currentBinding.id])

// 更新推荐人收益
await query(`
  UPDATE users 
  SET pending_earnings = pending_earnings + ?
  WHERE id = ?
`, [commission, referrerId])

console.log('[PayNotify] 分佣成功:', {
  referee: userId,
  referrer: referrerId,
  commission,
  purchaseCount: currentBinding.purchase_count + 1
})

关键变化

  • 不再标记为 converted(保持 active
  • 记录 last_purchase_date(用于判断是否有购买)
  • 累加 purchase_counttotal_commission
  • 允许同一绑定多次购买分佣

3. 定时任务 - 自动解绑

新增文件: scripts/auto-unbind-expired.js

/**
 * 自动解绑定时任务
 * 每天凌晨2点运行建议配置 cron
 * 
 * 解绑条件:
 * 1. 绑定超过30天expiry_date < NOW
 * 2. 期间没有任何购买purchase_count = 0
 */

const { query } = require('../lib/db')

async function autoUnbind() {
  console.log('[AutoUnbind] 开始执行自动解绑任务...')
  
  try {
    // 查询需要解绑的记录
    const expiredBindings = await query(`
      SELECT id, referee_id, referrer_id, binding_date, expiry_date
      FROM referral_bindings
      WHERE status = 'active'
        AND expiry_date < NOW()
        AND purchase_count = 0
    `)
    
    if (expiredBindings.length === 0) {
      console.log('[AutoUnbind] 无需解绑的记录')
      return
    }
    
    console.log(`[AutoUnbind] 找到 ${expiredBindings.length} 条需要解绑的记录`)
    
    // 批量更新为 expired
    const ids = expiredBindings.map(b => b.id)
    await query(`
      UPDATE referral_bindings 
      SET status = 'expired'
      WHERE id IN (?)
    `, [ids])
    
    console.log(`[AutoUnbind] ✅ 已解绑 ${expiredBindings.length} 条记录`)
    
    // 输出明细
    expiredBindings.forEach(b => {
      console.log(`  - ${b.referee_id} 解除与 ${b.referrer_id} 的绑定(绑定于 ${b.binding_date}`)
    })
    
  } catch (error) {
    console.error('[AutoUnbind] ❌ 执行失败:', error)
  }
}

// 如果直接运行此脚本
if (require.main === module) {
  autoUnbind().then(() => {
    console.log('[AutoUnbind] 任务完成')
    process.exit(0)
  })
}

module.exports = { autoUnbind }

部署方式(宝塔面板)

  1. 进入"计划任务" → 添加 Shell 脚本
  2. 执行周期:每天 02:00
  3. 脚本内容:
    cd /www/wwwroot/soul && node scripts/auto-unbind-expired.js
    

📊 状态流转图

用户B的绑定状态流转

[无绑定]
  ↓ (点击A的链接)
[active - 绑定A] ← expiry_date = NOW + 30天
  ↓ (点击C的链接)
[active - 绑定C] ← 旧绑定变 cancelled新绑定 expiry_date = NOW + 30天
  ↓ (购买)
[active - 绑定C] ← purchase_count++, last_purchase_date = NOW
  ↓ (30天后无购买)
[expired] ← 自动解绑
  ↓ (再次点击D的链接)
[active - 绑定D] ← 重新绑定

status 枚举说明

  • active: 当前有效绑定
  • cancelled: 被切换(用户点了其他人链接)
  • expired: 30天到期且无购买
  • converted: 不再使用在新逻辑中购买不改变status

🧪 测试用例

用例1: 立即切换绑定

1. A推荐B → B注册
   预期: referral_bindings 新增一条 (referee=B, referrer=A, status=active)

2. B点击C的链接
   预期: 
   - 旧记录 (referrer=A) status → cancelled
   - 新记录 (referrer=C) status = active, expiry_date = NOW + 30天

3. B购买文章
   预期: 
   - 佣金给C不是A
   - binding.purchase_count = 1
   - binding.last_purchase_date = NOW

用例2: 30天无购买自动解绑

1. A推荐B → B注册
   预期: binding (referee=B, referrer=A, expiry_date = NOW + 30天)

2. 等待31天模拟
   手动执行: node scripts/auto-unbind-expired.js
   预期: binding.status → expired

3. B点击C的链接
   预期: 创建新绑定 (referrer=C)

用例3: 多次购买累加佣金

1. A推荐B → B绑定A
2. B购买文章11元
   预期: A获得佣金 0.9元binding.purchase_count = 1
3. B购买文章21元
   预期: A再获得佣金 0.9元binding.purchase_count = 2total_commission = 1.8

⚠️ 注意事项

1. 边界情况处理

Q1: B多次点击同一个人的链接

  • A: 刷新 expiry_date续期30天不创建新记录

Q2: B在切换推荐人后的旧订单佣金

  • A: 历史佣金不变,只影响新订单

Q3: 用户注册时没有推荐码?

  • A: 无绑定状态,等待首次点击分享链接

2. 数据一致性

  • 使用事务保证绑定切换的原子性
  • 定时任务运行时间建议在凌晨低峰期
  • 建议添加 idx_expiry_status 索引优化查询

3. 性能优化

-- 优化索引
CREATE INDEX idx_referee_status ON referral_bindings(referee_id, status);
CREATE INDEX idx_expiry_purchase ON referral_bindings(expiry_date, purchase_count);

🚀 部署步骤

Step 1: 数据库迁移

# 执行 SQL 添加新字段
mysql -u root -p mycontent_db < scripts/migration-add-binding-fields.sql

Step 2: 修改 API 代码

  • 修改 /api/referral/bind(立即切换逻辑)
  • 修改 /api/miniprogram/pay/notify(累加购买次数)

Step 3: 部署定时任务

  • 创建 scripts/auto-unbind-expired.js
  • 宝塔面板配置 cron每天02:00

Step 4: 测试验证

  • 测试切换绑定流程
  • 测试购买分佣
  • 手动运行定时任务验证解绑

📈 后续优化建议

  1. 管理后台增强

    • 查看绑定切换历史(谁被谁抢走了)
    • 统计推荐人的"流失率"(被切换走的比例)
  2. 用户端提示

    • 点击新链接时提示"即将切换推荐人"
    • 显示当前绑定的推荐人信息
  3. 防刷机制

    • 限制同一用户短时间内频繁切换绑定
    • 记录IP和设备指纹防止恶意刷绑定
  4. 数据分析

    • 统计平均绑定时长
    • 分析哪些推荐人容易被"抢走"
    • 优化推荐策略

🔗 相关文档