11 KiB
11 KiB
新分销逻辑设计方案
📌 业务需求
核心规则
- 动态绑定:用户B点击谁的分享链接,立即绑定谁(无条件切换)
- 佣金归属:B购买时,佣金给当前推荐人(最新绑定的那个人)
- 自动解绑:绑定30天内,如果B既没点击其他链接,也没有任何购买 → 自动解绑
场景示例
时间线:
Day 0: A推荐B → B注册 → B绑定A(30天有效期)
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_count和total_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 }
部署方式(宝塔面板):
- 进入"计划任务" → 添加 Shell 脚本
- 执行周期:每天 02:00
- 脚本内容:
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购买文章1(1元)
预期: A获得佣金 0.9元,binding.purchase_count = 1
3. B购买文章2(1元)
预期: A再获得佣金 0.9元,binding.purchase_count = 2,total_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: 测试验证
- ✅ 测试切换绑定流程
- ✅ 测试购买分佣
- ✅ 手动运行定时任务验证解绑
📈 后续优化建议
-
管理后台增强
- 查看绑定切换历史(谁被谁抢走了)
- 统计推荐人的"流失率"(被切换走的比例)
-
用户端提示
- 点击新链接时提示"即将切换推荐人"
- 显示当前绑定的推荐人信息
-
防刷机制
- 限制同一用户短时间内频繁切换绑定
- 记录IP和设备指纹防止恶意刷绑定
-
数据分析
- 统计平均绑定时长
- 分析哪些推荐人容易被"抢走"
- 优化推荐策略