Files
soul-yongping/开发文档/8、部署/分销与绑定流程图.md
2026-02-09 15:09:29 +08:00

384 lines
20 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.

# 分销与绑定流程图
> 用流程图把「绑定」和「推荐人/邀请码」在系统中的用法讲清楚。
> 建议配合《邀请码分销规则说明》一起看。
---
## 一、概念速查
| 名词 | 是什么 | 存哪儿 | 谁用 |
|------|--------|--------|------|
| **邀请码** | 一串码,如 `SOULABC123` | 每个用户一条:`users.referral_code` | 链接里 `ref=邀请码`,用来**认出**是谁推荐的 |
| **推荐人** | 拿佣金的那个人(用户) | 用**用户ID**存:`referrer_id` | 绑定表、订单表、分佣都只认这个 ID |
| **被推荐人** | 通过链接进来的访客/买家 | 用**用户ID**存:`referee_id` | 绑定表里「谁被谁推荐」 |
关系:**邀请码** → 查 `users` 表 → 得到**推荐人用户ID**referrer_id。系统里所有「归属、分佣」只认 referrer_id不直接认邀请码字符串。
---
## 二、整体流程总览(一图看懂)
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 分销全流程:从分享到分佣 │
└─────────────────────────────────────────────────────────────────────────────────┘
推广者 A推荐人 访客/买家 B被推荐人 系统
│ │ │
│ 1. 分享带 ref 的链接 │ │
│ ?ref=A的邀请码 │ │
├─────────────────────────────────────>│ 2. 点击链接进入小程序/阅读页 │
│ ├─────────────────────────────────>│
│ │ app.js: 存 referral_code │
│ │ 可选: 记录访问 referral_visit │
│ │ │
│ │ 3. 登录(微信/手机号/getOpenId 拿到 user
│ ├─────────────────────────────────>│
│ │ 登录成功即调 /api/referral/bind │
│ │ 入参: userId, referralCode │
│ │ │
│ │ 4. 绑定逻辑 │
│ │ referral_code │
│ │ → 查 users 得 │
│ │ referrer_id=A │
│ │ 写 referral_ │
│ │ bindings │
│ │ (referee=B, │
│ │ referrer=A) │
│ │<─────────────────────────────────┤
│ │ 绑定成功new/renew/takeover
│ │ │
│ │ 5. 下单(章节/找伙伴) │
│ │ POST /api/miniprogram/pay │
│ │ body: referralCode(可选) │
│ ├─────────────────────────────────>│
│ │ 6. 定推荐人 │
│ │ 先查 bindings │
│ │ (referee=B)→A │
│ │ 无则用 referral│
│ │ Code 解析→A │
│ │ 写 orders. │
│ │ referrer_id=A,│
│ │ referral_code │
│ │<─────────────────────────────────┤
│ │ 返回支付参数 │
│ │ │
│ │ 7. 调起微信支付 │
│ ├───────────────────────────────> 微信
│ │ 8. 用户付款成功 │
│ │<─────────────────────────────── 微信
│ │ │
│ │ 9. 支付回调 │
│ │ POST .../notify│
│ │ 查 bindings │
│ │ (referee=B)→A │
│ │ 佣金=金额×90% │
│ │ A.pending_ │
│ │ earnings += 佣金│
│ │ binding→ │
│ │ converted │
│ │ │
│ 10. 推广者 A 看到待结算收益 +90% │ │
│<─────────────────────────────────────│ │
```
---
## 三、绑定流程(邀请码 → 推荐关系)
绑定解决的是:**「谁B是通过谁A的链接来的」**,并写入 `referral_bindings`
**绑定规则(后端统一保证):**
- **不重复绑定**:被推荐人 B 已有**当前推荐人 A** 的有效绑定时,再次用 A 的邀请码调用 bind → **不新建记录**,只做**续期**(把过期时间再延长 30 天)。
- **有时效**:每条绑定的有效期为 **30 天**`expiry_date`);分佣、下单定推荐人时只认「未过期」的绑定。
- **超时可重新绑定**:超过 30 天未续期的绑定视为过期;此时 B 再通过**其他人 C** 的链接进来并登录 → 允许绑定到 C旧绑定标记过期新绑定 C即「抢夺」若仍通过 A 的链接 → 续期 A 的绑定。
**绑定从登录就开始**:只要前端拿到 userId登录成功就立刻用当前的 `pendingReferralCode``/api/referral/bind`,不等到下单。后端根据上述规则决定是**新绑定 / 续期 / 抢夺 / 拒绝**。
```mermaid
flowchart TB
subgraph 入口
A1["推广者 A 分享链接<br/>带 ref=A的邀请码"]
A2["访客 B 点击链接进入"]
end
subgraph 前端
B1["app.js: 检测到 ref"]
B2["写入 storage: referral_code + pendingReferralCode"]
B3["若已登录 → 立即调 bind"]
B4["若未登录 → 等任意登录成功后再调 bind"]
B5["登录含: login / loginWithPhone / getOpenId 拿到 user 时"]
end
subgraph 后端绑定API["POST /api/referral/bind"]
C1["入参: userId(B), referralCode"]
C2["用 referralCode 查 users 表 → 推荐人 A"]
C3["不能自己推荐自己"]
C4["查 B 是否已有有效绑定(active)"]
C5{"已有绑定?"}
C6["同一推荐人 A → 只续期,不重复绑定<br/>expiry = 当前+30天"]
C7["不同人且已过期(>30天) → 可重新绑定<br/>旧绑定过期,新绑定 A"]
C8["不同人且未过期 → 拒绝"]
C9["无绑定 / 续期 / 抢夺后 → 写 binding<br/>referrer_id=A, referee_id=B, expiry=+30天"]
end
A1 --> A2 --> B1 --> B2
B2 --> B3
B2 --> B4
B4 --> B5
B3 --> C1
B5 --> C1
C1 --> C2 --> C3 --> C4 --> C5
C5 -->|无| C9
C5 -->|有,同一人| C6 --> C9
C5 -->|有,另一人已过期| C7 --> C9
C5 -->|有,另一人未过期| C8
```
要点:
- **绑定表**是「谁推荐了谁」的**唯一权威**;分佣只看这张表。
- **已有绑定不重复**:同一推荐人再次绑只续期;**30 天**内不能换绑其他推荐人,超过 30 天可重新绑定(被新推荐人「抢夺」或原推荐人续期)。
- **邀请码**只在「解析出推荐人是谁」时用,解析完得到的是 **referrer_id**用户ID
---
## 四、下单时「推荐人」怎么定(写订单)
创建订单时要把「这笔单算谁的推广」记在 `orders.referrer_id``orders.referral_code`。逻辑是:**先认绑定,再认邀请码**。
```mermaid
flowchart LR
subgraph 请求
R1["POST /api/miniprogram/pay"]
R2["body: userId(B), referralCode(可选)"]
end
subgraph 定推荐人
S1["查 referral_bindings"]
S2["WHERE referee_id = B<br/>AND status='active'<br/>AND expiry_date > NOW()"]
S3{"查到有效绑定?"}
S4["referrer_id = 绑定里的 referrer_id"]
S5["referrer_id = 用 referralCode<br/>查 users 得到的 id"]
S6["都无 → referrer_id = null"]
end
subgraph 写订单
T1["INSERT orders"]
T2["referrer_id = 上面得到的"]
T3["referral_code = 请求里的 referralCode<br/>或推荐人当前 users.referral_code"]
end
R1 --> R2 --> S1 --> S2 --> S3
S3 -->|是| S4
S3 -->|否,但有 referralCode| S5
S3 -->|否且无| S6
S4 --> T1
S5 --> T1
S6 --> T1
T1 --> T2 --> T3
```
结论:
- **有绑定** → 订单的推荐人 = 绑定里的推荐人(与下单时传不传 referralCode 无关)。
- **无绑定但传了 referralCode** → 用邀请码解析出推荐人,写入订单。
- 订单上的 **referrer_id** 用于后台展示、对账;**分佣不看订单**,只看绑定表。
---
## 五、分佣流程(支付成功后)
分佣**只看绑定表**,不看订单上的 referrer_id。
```mermaid
flowchart TB
subgraph 触发
P1["微信支付成功"]
P2["POST /api/miniprogram/pay/notify"]
P3["body: 订单号、金额、买家等"]
end
subgraph 回调逻辑
Q1["更新订单 status=paid"]
Q2["解锁用户权限(章节/全书)"]
Q3["查 referral_bindings"]
Q4["WHERE referee_id = 买家"]
Q5["AND status='active'"]
Q6["AND expiry_date > NOW()"]
Q7{"查到有效绑定?"}
Q8["取 referrer_id = 推广者 A"]
Q9["佣金 = 订单金额 × 90%"]
Q10["A.pending_earnings += 佣金"]
Q11["该绑定 status → converted"]
Q12["记录 commission_amount, order_id"]
Q13["不分佣"]
end
P1 --> P2 --> P3 --> Q1 --> Q2 --> Q3 --> Q4 --> Q5 --> Q6 --> Q7
Q7 -->|是| Q8 --> Q9 --> Q10 --> Q11 --> Q12
Q7 -->|否| Q13
```
要点:
- 分佣**只认** `referral_bindings` 里「买家 → 有效绑定 → 推荐人」。
- 订单里的 referrer_id / referral_code **不参与**分佣计算,只用于统计和展示。
---
### 什么情况下能拿到佣金(推广者视角)
满足下面**全部**条件时,你(推广者)才能拿到这笔订单的佣金:
1. **对方是通过你的链接进来的**
对方点击的链接里带有你的邀请码(如 `?ref=你的邀请码`),进入小程序后系统会记下推荐码,并在登录时用于绑定。
2. **对方已经绑定到你**
对方完成登录后,系统成功调用了绑定接口(新绑定或续期),且当前存在一条「被推荐人 = 对方、推荐人 = 你」的绑定记录,且该绑定 **status = active**、**expiry_date > 当前时间**(在 30 天有效期内或已续期)。
3. **对方在绑定有效期内下单并支付成功**
对方在上述有效期内发起了购买(章节或全书),并完成微信支付;支付成功后,微信会回调我们的接口。
4. **支付回调时仍能查到你的有效绑定**
支付成功回调执行时,系统按「买家 = 对方」查 `referral_bindings`,能查到一条有效绑定且推荐人是你,才会把约 90% 的佣金计入你的待结算收益pending_earnings并把该绑定标记为已转化converted
**简单记**:你的链接 → 对方进来并登录绑定到你 → 有效期内对方付款 → 你拿佣金。
---
### 章节分享这块的分销收益方式
章节页分享(读某一章时分享给好友/朋友圈)与首页、推广中心的分享**用同一套绑定与分佣规则**,只是落地页是「某一章」的阅读页。收益方式如下:
1. **入口与绑定**
你从阅读页分享出去的链接带 `ref=你的邀请码`(例如 `/pages/read/read?id=1.2&ref=你的邀请码`)。对方点进后进入**该章节**阅读页,系统记下推荐码;对方**登录**后即完成绑定新绑定或续期。绑定规则30 天、不重复绑、超时可重绑)与其它分享入口一致。
2. **收益比例**
订单实付金额的**约 90%** 给推广者(与全书、其它章节一致,由 `referral_config.distributorShare` 配置)。对方买的是**这一章、别的章还是全书**,都按该笔订单金额 × 90% 计算佣金。
3. **计佣次数(每个被推荐人只计一次)**
系统在支付成功回调里会查「该买家」的**有效绑定**,有则给推荐人加佣金,并把这条绑定标记为**已转化converted**。
因此:**同一个被推荐人在绑定有效期内,只有其「第一笔」支付会给你分佣**;该用户之后再买其它章节或全书,**不再**重复给你分佣(绑定已用掉)。
4. **小结**
**章节分享的收益**:你分享章节链接(带 ref→ 对方进来并登录绑定到你 → 对方在有效期内**第一次**支付(可以是这一章、别的章或全书)→ 你获得**该笔订单金额的约 90%**;该用户后续订单不再给你分佣。
---
## 六、推荐人 vs 邀请码(怎么用、不混用)
```mermaid
flowchart LR
subgraph 入口
L1["链接 ref=SOULABC123"]
end
subgraph 解析
L2["邀请码 = SOULABC123"]
L3["users WHERE referral_code = ?"]
L4["推荐人 = 该用户的 id"]
end
subgraph 存储
M1["referral_bindings.referrer_id"]
M2["orders.referrer_id"]
M3["分佣发给谁"]
end
L1 --> L2 --> L3 --> L4
L4 --> M1
L4 --> M2
L4 --> M3
style L2 fill:#f9f,stroke:#333
style L4 fill:#9f9,stroke:#333
```
- **邀请码**:只在「从链接/请求里认出是谁」这一步用,用完就解析成 **referrer_id**
- **推荐人**:所有「归属、分佣、统计」都只用 **referrer_id**,不会把邀请码字符串当推荐人存。
---
## 七、表与字段关系简图
```mermaid
erDiagram
users ||--o{ referral_bindings : "referrer_id"
users ||--o{ referral_bindings : "referee_id"
users {
string id PK
string referral_code "自己的邀请码"
}
referral_bindings {
string referrer_id "推荐人(谁拿佣金)"
string referee_id "被推荐人(买家)"
string status "active|converted|expired"
timestamp expiry_date
}
orders {
string user_id "买家"
string referrer_id "推荐人ID(展示/对账)"
string referral_code "下单时邀请码(展示)"
}
referral_bindings ||--o{ orders : "分佣时关联"
```
- **绑定**`referrer_id` = 推荐人,`referee_id` = 被推荐人;分佣只看这张表。
- **订单**`referrer_id``referral_code` 只做展示和对账,不参与分佣计算。
---
## 八、逻辑漏洞与注意点
以下为与流程图、实现对照后容易出现的漏洞和设计注意点,便于排查与加固。
### 8.1 严重:支付回调中买家身份不能信任客户端
**问题**:支付回调(`/api/miniprogram/pay/notify`)里若**优先**使用请求体/attach 里的 `userId` 作为买家,则该 `userId` 来自**创建订单时客户端传入**的 `body.userId`。若被篡改(如传成他人 userId会导致
- 订单归属、解锁权限记到错误用户;
- 分佣按「错误买家」查绑定表,可能把佣金算到错误推荐人或不分佣。
**正确做法****买家身份必须以微信回调中的 `openId` 为准**(微信侧不可伪造),用 `openId``users` 得到 `buyerUserId`attach 中的 `userId` 仅作辅助或校验,不一致时以 openId 解析结果为准。
**实现建议**:在 notify 中先 `buyerUserId = 由 openId 查 users 得到`;若查不到再回退到 attach.userId并打日志告警。
---
### 8.2 设计缺口:先下单、后绑定会导致无分佣
**问题**:流程图要求「先绑定、再下单」分佣才生效。若用户通过 A 的链接进入但**未调用** `/api/referral/bind`(未登录就下单、或 bind 失败/漏调),下单时传了 `referralCode`,订单上会有 `referrer_id=A`,但**分佣只看绑定表**,此时无绑定 → 不会给 A 分佣。
**结论**:这是当前设计下的预期行为,不是 bug但需要在产品/运营上保证「进入后尽快登录并完成绑定」,或在文档中明确写清:**只有存在有效绑定时支付成功才会分佣**。
---
### 8.3 重复回调与重复分佣
**现状**:微信可能对同一笔支付多次回调。当前实现:
- 订单状态已为 `paid` 时跳过订单更新;
- 分佣时只取 `status='active'` 的绑定,且分佣后将该绑定置为 `converted`,同一买家不会再有第二条 active 绑定参与分佣。
因此**不会重复加佣**。无需改流程图,实现已防护。
---
### 8.4 绑定表与 users.referred_by 双写
**现状**:绑定 API 在「新绑定」或「抢夺」时会写 `users.referred_by`,与 `referral_bindings` 双写;「续期」只更新绑定表,不改 `referred_by`
分佣、下单定推荐人**只读绑定表**GET 查询「我的推荐人」等可能读 `users.referred_by`。只要绑定接口保证 new/takeover 时双写一致,则无逻辑漏洞。若以后有接口只改 `referred_by` 而不改绑定表,就会不一致,需避免。
---
### 8.5 小结
| 类型 | 说明 |
|------------|------|
| 必须修 | 支付回调中买家身份以 openId 解析为准,不信任 attach.userId。 |
| 文档/产品 | 明确「先绑定再下单才能分佣」;未绑定仅下单只记订单归属、不分佣。 |
| 已防护 | 重复回调不会导致重复分佣。 |
| 需长期一致 | 绑定表与 users.referred_by 在 new/takeover 时双写,避免单改其一。 |
---
若要把某一段改成「按步骤」的纯文字版或拆成多张图,可以说明要哪一段(绑定 / 下单 / 分佣 / 概念)。