bindbox-game/.claude/plan/channel-user-bindding-doc.md
2026-02-27 17:51:38 +08:00

613 lines
28 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.

# 渠道管理与用户注册绑定调用链文档
## 概述
本文档描述 Bindbox Game 项目中渠道管理模块与用户注册绑定的调用链关系。
**渠道绑定的三种方式:**
1. **用户登录时绑定** - 微信/抖音登录时传入 `channel_code`
2. **定时任务自动绑定** - 直播间奖品发放时,根据活动关联渠道自动绑定主播邀请人
3. **抖音登录绑定** - 抖音小程序登录时传入 `channel_code`
---
## 一、数据模型
### 1.1 渠道表 (channels)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int64 | 主键ID |
| name | string | 渠道名称 |
| code | string | 渠道唯一标识(用于登录时绑定) |
| type | string | 渠道类型 |
| remarks | string | 备注 |
| created_at | time | 创建时间 |
| updated_at | time | 更新时间 |
| deleted_at | time | 删除时间(软删) |
**文件位置**: `internal/repository/mysql/model/channels.gen.go:16-25`
### 1.2 用户表 (users) - 渠道相关字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int64 | 主键ID |
| channel_id | int64 | 渠道ID关联 channels.id |
| invite_code | string | 用户唯一邀请码 |
| inviter_id | int64 | 邀请人用户ID |
| openid | string | 微信openid |
| unionid | string | 微信unionid |
**文件位置**: `internal/repository/mysql/model/users.gen.go:16-33`
---
## 二、调用链架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 前端/客户端 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ API 路由层 │
│ internal/router/router.go │
│ │
│ 渠道管理路由: │
│ - POST /api/admin/channels → CreateChannel() │
│ - PUT /api/admin/channels/:id → ModifyChannel() │
│ - DELETE /api/admin/channels/:id → DeleteChannel() │
│ - GET /api/admin/channels → ListChannels() │
│ - GET /api/admin/channels/:id/stats → ChannelStats() │
│ │
│ 用户登录路由: │
│ - POST /api/app/users/weixin/login → WeixinLogin() │
│ (携带 channel_code 参数) │
│ - POST /api/app/users/douyin/login → DouyinLogin() │
│ (携带 channel_code 参数) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ 渠道管理 API 层 │ │ 用户登录 API 层 │ │ 定时任务调度器 │
│ internal/api/admin/ │ │ internal/api/user/│ │ internal/service/ │
│ channels.go │ │ login_app.go │ │ douyin/scheduler.go │
│ │ │ login_douyin_app │ │ │
│ - CreateChannel() │ │ - WeixinLogin() │ │ - GrantLivestreamPrizes()
│ - ModifyChannel() │ │ - DouyinLogin() │ │ - bindAnchorInviter │
│ - DeleteChannel() │ │ 接收channel_code│ │ IfNeeded() │
│ - ListChannels() │ │ │ │ │
│ - ChannelStats() │ │ │ │ 每5分钟自动执行 │
└─────────────────────────┘ └───────────────────┘ └───────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────┐ ┌───────────────────┐ ┌───────────────────────┐
│ 渠道 Service 层 │ │ 用户 Service 层 │ │ 用户 Service 层 │
│ internal/service/ │ │ internal/service/ │ │ internal/service/user │
│ channel/channel.go │ │ user/ │ │ │
│ │ │ login_weixin.go │ │ - BindInviter() │
│ - Create() │ │ login_douyin.go │ │ (定时任务调用) │
│ - Modify() │ │ │ │ │
│ - Delete() │ │ - LoginWeixin() │ │ │
│ - List() │ │ - LoginDouyin() │ │ │
│ - GetStats() │ │ 查渠道并绑定用户 │ │ │
│ - GetByID() │ │ │ │ │
└─────────────────────────┘ └───────────────────┘ └───────────────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ 数据访问层 (DAO) │
│ internal/repository/mysql/dao/ │
│ │
│ - channels.gen.go 渠道表操作 │
│ - users.gen.go 用户表操作 │
│ - user_invites.gen.go 邀请关系表操作 │
│ - livestream_activities.gen.go 直播间活动表(含渠道字段) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ MySQL 数据库 │
│ - channels 表 │
│ - users 表 (channel_id 字段) │
│ - user_invites 表 │
│ - livestream_activities 表 (channel_id, channel_code 字段) │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 三、详细调用链
### 3.1 渠道管理(管理端)
#### 创建渠道
```
HTTP POST /api/admin/channels
├── 参数: { name, code, type, remarks }
internal/api/admin/channels.go:26 CreateChannel()
├── 验证参数
internal/service/channel/channel.go:79 Create()
├── 创建 Channels 模型
dao.Channels.Create(m)
MySQL INSERT INTO channels
```
#### 查询渠道列表(含用户数统计)
```
HTTP GET /api/admin/channels
internal/api/admin/channels.go:135 ListChannels()
internal/service/channel/channel.go:111 List()
├── 1. 查询渠道列表
│ SELECT * FROM channels WHERE name LIKE ? ORDER BY id DESC
├── 2. 统计每个渠道的用户数
│ SELECT channel_id, count(*) as count
│ FROM users
│ WHERE channel_id IN (?)
│ GROUP BY channel_id
返回渠道列表(含 user_count 字段)
```
#### 渠道数据分析
```
HTTP GET /api/admin/channels/:channel_id/stats
internal/api/admin/channels.go:53 ChannelStats()
internal/service/channel/channel.go:169 GetStats()
├── 1. 统计渠道用户总数
│ SELECT count(*) FROM users WHERE channel_id = ?
├── 2. 统计渠道订单数和GMV
│ SELECT count(*) as count, sum(actual_amount) as gmv
│ FROM orders o
│ JOIN users u ON u.id = o.user_id
│ WHERE u.channel_id = ? AND o.status = 2
├── 3. 月度用户增长统计
│ SELECT DATE_FORMAT(created_at, '%Y-%m') as date, count(*) as count
│ FROM users
│ WHERE channel_id = ? AND created_at >= ?
│ GROUP BY date
├── 4. 月度订单统计
│ SELECT DATE_FORMAT(created_at, '%Y-%m') as date, count(*), sum(actual_amount)
│ FROM orders o
│ JOIN users u ON u.id = o.user_id
│ WHERE u.channel_id = ? AND o.status = 2 AND o.created_at >= ?
│ GROUP BY date
返回渠道统计数据
```
---
### 3.2 用户注册绑定渠道
#### 微信登录(绑定渠道)
```
HTTP POST /api/app/users/weixin/login
├── 参数: { code, invite_code, douyin_id, channel_code }
internal/api/user/login_app.go:47 WeixinLogin()
├── 1. 微信 code2session 获取 openid/unionid
internal/service/user/login_weixin.go:42 LoginWeixin()
├── 2. 查询渠道(如果传入 channel_code
│ ch, _ := s.readDB.Channels.Where(Channels.Code.Eq(in.ChannelCode)).First()
│ channelID = ch.ID
│ 【文件位置: login_weixin.go:86-92】
├── 3. 查找或创建用户
│ ├── 查找: WHERE openid = ? OR unionid = ?
│ │
│ └── 创建新用户:
│ u = &model.Users{
│ Nickname: nickname,
│ Openid: in.OpenID,
│ ChannelID: channelID, // 绑定渠道
│ ...
│ }
│ 【文件位置: login_weixin.go:113-124】
├── 4. 更新已有用户(如果传入 channel_code
│ if channelID > 0 {
│ UPDATE users SET channel_id = ? WHERE id = ?
│ }
│ 【文件位置: login_weixin.go:141-143】
├── 5. 处理邀请关系(如果传入 invite_code 且是新用户)
│ 【详见 3.3 节】
返回用户信息和 Token
```
**关键代码片段**`login_weixin.go:86-92`:
```go
// 查找渠道ID
var channelID int64
if in.ChannelCode != "" {
ch, _ := s.readDB.Channels.WithContext(ctx).Where(s.readDB.Channels.Code.Eq(in.ChannelCode)).First()
if ch != nil {
channelID = ch.ID
}
}
```
---
### 3.3 抖音登录(绑定渠道)
```
HTTP POST /api/app/users/douyin/login
├── 参数: { code, anonymous_code, invite_code, channel_code }
internal/api/user/login_douyin_app.go:44 DouyinLogin()
├── 参数校验
internal/service/user/login_douyin.go:39 LoginDouyin()
├── 1. 抖音 code2session 获取 openid
├── 2. 查询渠道(如果传入 channel_code
│ ch, _ := s.readDB.Channels.Where(Channels.Code.Eq(in.ChannelCode)).First()
│ channelID = ch.ID
│ 【文件位置: login_douyin.go:91-97】
├── 3. 查找或创建用户
│ ├── 查找: WHERE douyin_id = ? OR unionid = ?
│ │
│ └── 创建新用户:
│ u = &model.Users{
│ Nickname: nickname,
│ DouyinID: openID,
│ ChannelID: channelID, // 绑定渠道
│ ...
│ }
│ 【文件位置: login_douyin.go:119-127】
├── 4. 更新已有用户(如果传入 channel_code 且未绑定)
│ if channelID > 0 && u.ChannelID == 0 {
│ UPDATE users SET channel_id = ?
│ }
│ 【文件位置: login_douyin.go:143-144】
└── 5. 处理邀请关系(如果传入 invite_code 且是新用户)
```
---
### 3.4 邀请关系绑定
用户绑定邀请人有两种方式:
#### 方式一:登录时自动绑定(推荐)
```
用户登录时传入 invite_code 参数
LoginWeixin() / LoginDouyin() 内部处理
├── 检查是否新用户
├── 查找邀请人(通过 invite_code
├── 创建 user_invites 记录
├── 更新 users.inviter_id
触发任务中心奖励: task.OnInviteSuccess()
```
#### 方式二:用户主动绑定
```
HTTP POST /api/app/users/inviter/bind
├── 参数: { invite_code }
internal/api/user/bind_inviter_app.go:34 BindInviter()
internal/service/user/bind_inviter.go:33 BindInviter()
├── 1. 加锁获取当前用户
├── 2. 检查是否已绑定inviter_id != 0 则拒绝)
├── 3. 查找邀请人
├── 4. 创建 user_invites 记录
├── 5. 更新 users.inviter_id
触发任务中心奖励: task.OnInviteSuccess()
```
---
### 3.5 定时任务自动绑定渠道(直播间奖品发放)
**重要场景:直播间用户通过渠道绑定主播邀请人**
```
定时任务 (每5分钟)
internal/service/douyin/scheduler.go:24 StartDouyinOrderSync()
├── ticker5min.C 触发
internal/service/douyin/scheduler.go:155 GrantLivestreamPrizes()
├── 1. 查找未发放的直播抽奖记录
│ SELECT * FROM livestream_draw_logs WHERE is_granted = 0
├── 2. 解析活动关联的渠道/主播邀请码
│ resolveActivityAnchorCodes()
│ 【文件位置: scheduler.go:418-489】
│ ├── 查询直播间活动的渠道信息
│ │ SELECT id, channel_id, channel_code
│ │ FROM livestream_activities
│ │ WHERE id IN (?)
│ │ 【文件位置: scheduler.go:451-458】
│ │
│ └── 补充缺失的渠道 code
│ fetchChannelCodes()
│ SELECT id, code FROM channels WHERE id IN (?)
│ 【文件位置: scheduler.go:491-513】
├── 3. 自动绑定主播邀请人(如果用户未绑定)
│ bindAnchorInviterIfNeeded(ctx, userID, anchorCode)
│ 【文件位置: scheduler.go:515-546】
│ ├── 查询用户是否已有邀请人
│ │ SELECT inviter_id FROM users WHERE id = ?
│ │
│ └── 如果 inviter_id == 0调用绑定服务
│ s.userSvc.BindInviter(ctx, userID, BindInviterInput{InviteCode: anchorCode})
│ 【文件位置: scheduler.go:534】
└── 4. 发放奖品并更新状态
```
**关键代码:自动绑定主播邀请人**
```go
// scheduler.go:515-546
func (s *service) bindAnchorInviterIfNeeded(ctx context.Context, userID int64, anchorCode string) {
// 1. 检查用户是否已有邀请人
userRecord, err := s.readDB.Users.WithContext(ctx).
Select(s.readDB.Users.InviterID).
Where(s.readDB.Users.ID.Eq(userID)).
First()
if userRecord.InviterID != 0 {
return // 已绑定,跳过
}
// 2. 自动绑定主播邀请人
s.userSvc.BindInviter(ctx, userID, user.BindInviterInput{InviteCode: anchorCode})
}
```
**数据流:**
```
┌───────────────────────────────────────────────────────────────────────┐
│ 直播间活动配置 │
│ livestream_activities │
│ ├── channel_id (关联渠道ID) │
│ └── channel_code (主播邀请码) │
└───────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────┐
│ 直播抽奖记录 │
│ livestream_draw_logs │
│ ├── activity_id (关联活动) │
│ ├── local_user_id (本地用户ID) │
│ └── is_granted (发放状态) │
└───────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────┐
│ 定时任务处理 │
│ GrantLivestreamPrizes() │
│ │
│ 1. 查 activity → 获取 channel_code │
│ 2. 查 channels → 补充缺失的 code │
│ 3. 查 users.inviter_id → 检查是否已绑定 │
│ 4. 未绑定 → 调用 BindInviter() 绑定主播 │
└───────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────┐
│ 用户邀请关系 │
│ users.inviter_id = 主播用户ID │
│ user_invites 表新增记录 │
└───────────────────────────────────────────────────────────────────────┘
```
---
## 四、核心文件索引
| 文件路径 | 说明 | 关键函数 |
|----------|------|----------|
| `internal/api/admin/channels.go` | 渠道管理 API | CreateChannel, ListChannels, ChannelStats |
| `internal/service/channel/channel.go` | 渠道业务逻辑 | Create, List, GetStats |
| `internal/api/user/login_app.go` | 用户登录 API (微信) | WeixinLogin |
| `internal/service/user/login_weixin.go` | 微信登录逻辑 | LoginWeixin渠道绑定核心 |
| `internal/api/user/login_douyin_app.go` | 用户登录 API (抖音) | DouyinLogin |
| `internal/service/user/login_douyin.go` | 抖音登录逻辑 | LoginDouyin渠道绑定核心 |
| `internal/api/user/bind_inviter_app.go` | 绑定邀请人 API | BindInviter |
| `internal/service/user/bind_inviter.go` | 绑定邀请人逻辑 | BindInviter |
| **`internal/service/douyin/scheduler.go`** | **抖音定时任务** | **GrantLivestreamPrizes, bindAnchorInviterIfNeeded** |
| `internal/repository/mysql/model/channels.gen.go` | 渠道模型 | Channels struct |
| `internal/repository/mysql/model/users.gen.go` | 用户模型 | Users struct含 channel_id |
| `internal/repository/mysql/model/user_invites.gen.go` | 邀请关系模型 | UserInvites struct |
| `internal/repository/mysql/model/livestream_activities.gen.go` | 直播间活动模型 | LivestreamActivities含 channel_id, channel_code |
| `internal/router/router.go` | 路由配置 | 渠道路由: 215-219 行 |
---
## 五、数据流图
### 5.1 用户注册绑定渠道流程
```
┌──────────┐ ┌──────────────┐ ┌────────────────┐
│ 前端 │────▶│ 微信登录 API │────▶│ 用户 Service │
│ │ │ │ │ │
│ channel_ │ │ code │ │ 1. code2session│
│ code │ │ invite_code │ │ 2. 查渠道 │
└──────────┘ └──────────────┘ │ 3. 创建/更新用户│
│ 4. 绑定邀请人 │
└───────┬────────┘
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ channels 表 │ │ users 表 │ │user_invites表│
│ │ │ │ │ │
│ code → ID │ │ channel_id │ │ inviter_id │
│ │ │ inviter_id │ │ invitee_id │
└──────────────┘ └──────────────┘ └──────────────┘
```
### 5.2 渠道统计查询流程
```
┌──────────┐ ┌──────────────┐ ┌────────────────┐
│ 管理后台 │────▶│ 渠道统计 API │────▶│ 渠道 Service │
│ │ │ │ │ │
│ 选择渠道 │ │ channel_id │ │ 1. 统计用户数 │
│ 时间范围 │ │ days │ │ 2. 统计订单数 │
└──────────┘ │ start_date │ │ 3. 统计GMV │
│ end_date │ │ 4. 月度趋势 │
└──────────────┘ └───────┬────────┘
┌─────────────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ users 表 │ │ orders 表 │
│ │ │ │
│ WHERE │ │ JOIN users │
│ channel_id=? │ │ WHERE │
│ │ │ channel_id=? │
└──────────────┘ └──────────────┘
```
---
## 六、业务规则
### 6.1 渠道绑定规则
| 场景 | 触发条件 | 说明 |
|------|----------|------|
| 微信登录绑定 | 传入 `channel_code` | 查询 channels 表,绑定 channel_id 到用户 |
| 抖音登录绑定 | 传入 `channel_code` | 查询 channels 表,仅当用户未绑定时才更新 |
| 定时任务绑定 | 直播间活动配置了 `channel_code` | 自动绑定主播邀请人(邀请关系,非渠道) |
### 6.2 渠道与邀请人的区别
| 概念 | 字段 | 说明 |
|------|------|------|
| **渠道 (Channel)** | `users.channel_id` | 用户来源渠道,用于统计分析 |
| **邀请人 (Inviter)** | `users.inviter_id` | 邀请该用户注册的人,用于奖励计算 |
**定时任务场景:**
- 直播间活动的 `channel_code` 用作**主播邀请码**
- 定时任务调用 `BindInviter()` 绑定的是**邀请关系**,而非渠道
- 主播邀请码 = 某个用户的 `invite_code`(通常是主播账号)
### 6.3 邀请绑定规则
1. **仅限一次**: 用户绑定邀请人后不可更改
2. **不能自邀**: 用户不能邀请自己
3. **奖励触发**: 绑定成功后触发任务中心奖励逻辑
4. **定时任务补绑**: 直播间用户未绑定邀请人时,自动绑定主播
### 6.4 权限控制
渠道管理接口需要以下权限:
| 操作 | 权限标识 |
|------|----------|
| 创建渠道 | `channel:create` |
| 修改渠道 | `channel:modify` |
| 删除渠道 | `channel:delete` |
| 查看渠道 | `channel:view` |
---
## 七、相关迁移文件
| 文件 | 说明 |
|------|------|
| `migrations/20260223_add_channel_fields_to_livestream_activities.sql` | 直播间活动添加渠道字段 |
---
## 八、注意事项
1. **渠道 Code 唯一性**: 渠道的 `code` 字段必须唯一,用于用户登录时匹配
2. **统计性能**: 渠道统计涉及多表 JOIN大数据量时需注意性能优化
3. **事务处理**: 用户创建和渠道绑定在同一事务中,保证数据一致性
4. **软删除**: 渠道删除为软删除,不影响已绑定用户
---
*文档生成时间: 2026-02-27*
*项目: bindbox_game*