409 lines
11 KiB
Markdown
409 lines
11 KiB
Markdown
# 新分销逻辑设计方案
|
||
|
||
## 📌 业务需求
|
||
|
||
### 核心规则
|
||
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)
|