# 新分销逻辑设计方案 ## 📌 业务需求 ### 核心规则 1. **动态绑定**:用户B点击谁的分享链接,立即绑定谁(无条件切换) 2. **佣金归属**:B购买时,佣金给当前推荐人(最新绑定的那个人) 3. **自动解绑**:绑定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 ```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` - 立即切换绑定 **修改前逻辑(现有):** ```javascript if (existingBinding && expiryDate > now) { return { error: '绑定有效期内无法更换' } // ❌ 阻止切换 } ``` **修改后逻辑(新):** ```javascript // 查询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` - 支付回调更新 **修改前逻辑(现有):** ```javascript // 更新绑定为 converted await query(` UPDATE referral_bindings SET status = 'converted', conversion_date = NOW(), commission_amount = ? WHERE id = ? `, [commission, bindingId]) ``` **修改后逻辑(新):** ```javascript // 查询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` ```javascript /** * 自动解绑定时任务 * 每天凌晨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. 脚本内容: ```bash 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. 性能优化 ```sql -- 优化索引 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: 数据库迁移 ```bash # 执行 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. **数据分析** - 统计平均绑定时长 - 分析哪些推荐人容易被"抢走" - 优化推荐策略 --- ## 🔗 相关文档 - [分销与绑定流程图](./分销与绑定流程图.md) - [推广设置功能完整修复清单](./推广设置功能-完整修复清单.md) - [API接入说明](./API接入说明.md)