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

409 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 新分销逻辑设计方案
## 📌 业务需求
### 核心规则
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
```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购买文章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. 性能优化
```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)