# 红包系统完整流程文档

## 一、整体架构设计

### 核心原则
- **Redis 负责并发控制与计算**：使用 Lua 脚本实现原子操作
- **MongoDB 负责最终记录与查询**：异步写入，不参与并发竞争
- **立即返回 + 异步写入**：提升响应速度，降低延迟
- **失败补偿机制**：使用 Redis Stream 确保数据最终一致性

### 数据流向
```
客户端请求
  ↓
Go API（无状态）
  ↓
Redis Lua脚本（原子抢红包）← 并发控制核心
  ↓
立即返回结果 ✅
  ↓
异步写入 MongoDB（goroutine）
  ├─ 写入领取记录
  ├─ 更新钱包余额
  └─ 写入补偿Stream
```

---

## 二、发送红包流程（SendRedPacket）

### 1. 参数验证
- 验证 `totalAmount > 0`
- 验证 `totalCount > 0`
- 验证 `redPacketType` 为 1（普通）或 2（拼手气）

### 2. 群信息验证
- 获取群信息，验证群是否存在
- 获取群主ID（发送用户默认为群主）
- 获取群主用户信息（昵称、头像）

### 3. 生成红包ID和基本信息
- 生成红包ID：`MD5(sendUserID + groupID + timestamp)`
- 计算会话ID
- 设置过期时间：24小时

### 4. 初始化Redis数据结构（关键步骤）

#### Redis Key 设计
- `rp:{packetId}:list` - 红包金额队列（List）
- `rp:{packetId}:users` - 已领取用户集合（Set）
- 过期时间：24小时

#### 普通红包（type=1）
```go
avgAmount := totalAmount / totalCount
for i := 0; i < totalCount; i++ {
    RPush("rp:{packetId}:list", avgAmount)  // 每个元素都是平均金额
}
Expire("rp:{packetId}:list", 24h)
Expire("rp:{packetId}:users", 24h)
```

#### 拼手气红包（type=2）
```go
amounts := allocateRandomAmounts(totalAmount, totalCount)  // 预先分配随机金额
for _, amount := range amounts {
    RPush("rp:{packetId}:list", amount)  // 每个元素是不同的随机金额
}
Expire("rp:{packetId}:list", 24h)
Expire("rp:{packetId}:users", 24h)
```

**拼手气红包分配算法**：
- 确保每个红包至少 1 分
- 前 n-1 个红包：在最小金额和最大金额之间随机分配
- 最后一个红包：直接分配剩余金额
- 打乱顺序增加随机性

### 5. 创建MongoDB记录
```go
redPacketRecord := &model.RedPacket{
    RedPacketID:    redPacketID,
    SendUserID:     sendUserID,
    GroupID:        groupID,
    RedPacketType:  req.RedPacketType,
    TotalAmount:    req.TotalAmount,
    TotalCount:     req.TotalCount,
    RemainAmount:   req.TotalAmount,  // 初始等于总金额
    RemainCount:    req.TotalCount,   // 初始等于总个数
    Status:         Active,
    ExpireTime:     expireTime,
    CreateTime:     time.Now(),
}
```

### 6. 发送消息
- 构建红包消息内容（自定义消息格式）
- 通过 RPC 发送消息到群聊

### 7. 返回响应
- 返回 `redPacketID`、`serverMsgID`、`clientMsgID`、`sendTime`

---

## 三、领取红包流程（ReceiveRedPacket）

### 1. 参数验证
- 获取用户ID（从token中）
- 验证请求参数

### 2. 红包状态检查
- 查询MongoDB获取红包基本信息
- 检查红包是否过期（`Status == Expired`）
- **注意**：不检查是否已领完，因为Redis会处理

### 3. 【核心】Redis Lua脚本抢红包

#### Lua脚本逻辑
```lua
-- KEYS[1] = rp:{packetId}:list  (红包金额队列)
-- KEYS[2] = rp:{packetId}:users (已领取用户集合)
-- ARGV[1] = userId

-- 步骤1：检查是否已领取
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
    return -1  -- 用户已领取
end

-- 步骤2：从队列中抢红包（原子操作）
local money = redis.call('LPOP', KEYS[1])
if not money then
    return -2  -- 红包已领完
end

-- 步骤3：记录用户已领取（原子操作）
redis.call('SADD', KEYS[2], ARGV[1])

-- 步骤4：返回金额
return money
```

#### 返回值处理
- `-1` → 用户已领取 → 返回 `ErrRedPacketAlreadyReceived`
- `-2` → 红包已领完 → 返回 `ErrRedPacketFinished`
- `>0` → 领取成功 → 返回金额（分）

### 4. 立即返回结果
```go
resp.RedPacketID = req.RedPacketID
resp.Amount = amount
resp.IsLucky = false
apiresp.GinSuccess(c, resp)  // 立即返回，不等待MongoDB写入
```

### 5. 【异步写入】后台goroutine处理

#### 5.1 写入补偿Stream
```go
streamKey := "rp:" + redPacketID + ":stream"
XAdd(streamKey, {
    "userID": userID,
    "amount": amount,
    "time": timestamp
})
```
**作用**：用于失败补偿，确保数据最终一致性

#### 5.2 写入MongoDB领取记录
```go
receiveRecord := &model.RedPacketReceive{
    ReceiveID:     receiveID,
    RedPacketID:   redPacketID,
    ReceiveUserID: userID,
    Amount:        amount,
    ReceiveTime:   time.Now(),
    IsLucky:       false,
}
Create(receiveRecord)
```
**幂等性保护**：
- 唯一索引：`(receive_user_id, red_packet_id)`
- 如果唯一索引冲突（E11000错误），说明已写入，忽略错误

#### 5.3 更新钱包余额
```go
// 查询或创建钱包
wallet := Take(userID)
if not exists {
    Create(wallet)  // 初始余额为0
}

// 使用版本号乐观锁更新余额
UpdateBalanceWithVersion(userID, "add", amount, oldVersion)
// 如果版本号冲突，自动重试（最多3次）

// 创建余额记录
CreateBalanceRecord({
    Operation: "add",
    Amount: amount,
    Remark: "领取红包: " + redPacketID
})
```

---

## 四、并发控制机制

### 1. Redis层面（核心）
- **Lua脚本原子性**：整个抢红包过程是原子操作
- **LPOP原子性**：从队列中取出金额是原子操作
- **SADD原子性**：记录用户是原子操作
- **SISMEMBER检查**：防止重复领取

### 2. MongoDB层面（兜底）
- **唯一索引**：`(receive_user_id, red_packet_id)` 唯一索引
- **幂等写入**：如果已存在，忽略重复写入错误

### 3. 钱包余额更新
- **版本号乐观锁**：使用 `version` 字段防止并发覆盖
- **自动重试**：并发冲突时自动重试（最多3次）

---

## 五、失败补偿机制

### 1. Redis Stream记录
- 每次领取成功后，写入Redis Stream
- Stream包含：`userID`、`amount`、`time`

### 2. 补偿Worker（待实现）
```go
// 后台worker消费Stream
for {
    messages := XRead("rp:{packetId}:stream", lastID)
    for _, msg := range messages {
        // 检查MongoDB是否已写入
        if !ExistsInMongoDB(msg.userID, msg.redPacketID) {
            // 重新写入MongoDB
            WriteToMongoDB(msg)
        }
        // ACK消息
        XAck(streamKey, msg.ID)
    }
}
```

### 3. 补偿场景
- Redis成功，但MongoDB写入失败
- 服务宕机导致异步写入中断
- 网络闪断导致写入失败

---

## 六、Redis Key设计总结

| Key | 类型 | 说明 | 过期时间 |
|-----|------|------|----------|
| `rp:{packetId}:list` | List | 红包金额队列 | 24小时 |
| `rp:{packetId}:users` | Set | 已领取用户集合 | 24小时 |
| `rp:{packetId}:stream` | Stream | 领取日志流（补偿用） | 24小时 |

---

## 七、数据一致性保证

### 1. 强一致性（Redis）
- Redis Lua脚本保证抢红包的原子性
- 确保不会超发、不会重复领取

### 2. 最终一致性（MongoDB）
- 异步写入MongoDB，可能短暂延迟
- 唯一索引确保幂等性
- 补偿机制确保最终一致

### 3. 钱包余额
- 使用版本号乐观锁
- 自动重试机制
- 确保余额更新成功

---

## 八、性能特点

### 1. 高并发支持
- Redis Lua脚本原子操作，天然支持高并发
- 支持万人同时抢红包

### 2. 低延迟
- 立即返回结果，不等待MongoDB写入
- 响应时间主要取决于Redis性能

### 3. 高可用
- MongoDB不参与并发竞争，避免被打爆
- 异步写入不影响主流程
- 补偿机制确保数据不丢失

---

## 九、关键代码位置

### 发送红包
- `internal/api/redpacket.go:SendRedPacket()` - 主流程
- `internal/api/redpacket.go:allocateRandomAmounts()` - 拼手气红包分配算法

### 领取红包
- `internal/api/redpacket.go:ReceiveRedPacket()` - 主流程
- `internal/api/redpacket.go:grabRedPacketFromRedis()` - Redis Lua脚本执行
- `internal/api/redpacket.go:grabRedPacketLuaScript` - Lua脚本定义
- `internal/api/redpacket.go:writeRedPacketRecordToMongoDB()` - 异步写入MongoDB
- `internal/api/redpacket.go:updateWalletBalanceAsync()` - 异步更新钱包余额
- `internal/api/redpacket.go:writeToCompensationStream()` - 写入补偿Stream

### Redis Key生成
- `internal/api/redpacket.go:getRedPacketQueueKey()` - 队列Key
- `internal/api/redpacket.go:getRedPacketUsersKey()` - 用户Set Key
- `internal/api/redpacket.go:getRedPacketStreamKey()` - Stream Key

---

## 十、注意事项

1. **Redis必须可用**：发送和领取红包都依赖Redis
2. **MongoDB异步写入**：可能短暂延迟，但不影响用户体验
3. **补偿机制**：需要实现后台worker消费Stream
4. **唯一索引**：MongoDB领取记录的唯一索引是幂等性的关键
5. **钱包余额**：异步更新，可能有短暂延迟

---

## 十一、流程图

### 发送红包流程图
```
验证参数
  ↓
验证群信息
  ↓
生成红包ID
  ↓
初始化Redis（关键）
  ├─ 普通红包：推送N个平均金额
  └─ 拼手气红包：推送N个随机金额
  ↓
创建MongoDB记录
  ↓
发送消息
  ↓
返回结果
```

### 领取红包流程图
```
验证参数
  ↓
查询红包基本信息
  ↓
检查是否过期
  ↓
【核心】Redis Lua脚本抢红包
  ├─ 检查是否已领取（SISMEMBER）
  ├─ 抢红包（LPOP）
  └─ 记录用户（SADD）
  ↓
立即返回结果 ✅
  ↓
异步写入（goroutine）
  ├─ 写入补偿Stream
  ├─ 写入MongoDB领取记录
  └─ 更新钱包余额
```

---

## 十二、错误处理

### 发送红包错误
- Redis不可用 → 返回错误，不允许发送
- MongoDB写入失败 → 返回错误，不允许发送

### 领取红包错误
- 红包不存在 → 返回 `red packet not found`
- 红包已过期 → 返回 `red packet expired`
- 用户已领取 → 返回 `red packet already received`（Redis检查）
- 红包已领完 → 返回 `red packet finished`（Redis检查）
- Redis不可用 → 返回 `redis client is not available`

### 异步写入错误
- MongoDB写入失败 → 记录日志，补偿机制处理
- 钱包余额更新失败 → 记录日志，可手动补偿
- Stream写入失败 → 记录日志，不影响主流程
