# 红包与游戏横幅 — 架构文档

> 对应 Gitea issues #19–#24
> 参考实现：`im-client-ios-swift-demo` RedEnvelopeSendView + BannerViewModel + PlatformBannerViewModel
> Bug 参考：bug 截图文件夹（currencyType=PEA 错误 + [红包] 文本未渲染）

---

## 1. 功能范围

| Issue | 功能 | 状态 |
|-------|------|------|
| #19 | currencyType 动态化（修复 PEA 硬编码） | ✅ 已实现 |
| #20 | 红包消息气泡（typ=8，三态 UI） | ✅ 已实现 |
| #21 | 领取红包（/app/api/wallet/rp/receive） | ✅ 已实现 |
| #22 | 发送红包 UI（STANDARD_RP + LUCKY_RP） | ✅ 已实现 |
| #23 | BannerViewModel — 游戏横幅 + WS NewRound | ✅ 已实现 |
| #24 | 游戏悬浮按钮（MiniAppFloatButton） | ✅ 已实现 |

---

## 2. 目录结构

```
data/remote/
├── red_envelope_request.dart      # 发送/领取 API Request
└── banner_request.dart            # 游戏横幅 API Request

features/chat/
├── di/
│   └── red_envelope_provider.dart # 红包 DI 装配
├── presentation/
│   └── banner_view_model.dart     # 游戏横幅 ViewModel
├── usecases/
│   ├── send_red_envelope_usecase.dart    # 发送红包
│   ├── receive_red_envelope_usecase.dart # 领取红包
│   └── fetch_banner_usecase.dart         # 拉取游戏横幅
└── view/widgets/
    ├── red_envelope_bubble.dart    # 红包气泡（typ=8）
    ├── send_red_envelope_sheet.dart # 发送 BottomSheet
    ├── banner_view.dart            # 游戏横幅条
    └── miniapp_float_button.dart   # 游戏悬浮按钮
```

---

## 3. 关键数据格式

### 3.1 红包消息 content（typ=8）

```json
{
  "id":           "rp_xxx",
  "rp_type":      "STANDARD_RP",
  "remark":       "恭喜发财",
  "total_amount": "10.00",
  "total_num":    5,
  "rp_status":    0
}
```

**rp_status 含义：**
| 值 | 含义 | UI |
|----|------|----|
| 0/1 | 未领取 | 橙色 + 领取按钮 |
| 2 | 已领取 | 灰色 "已领取" |
| 3 | 已过期 | 灰色 "红包已过期" |
| 4 | 已抢完 | 灰色 "手慢了" |
| 6 | 等待开奖（NN_RP） | 橙色 + 等待 |

### 3.2 发送红包 API

**POST /payment/rp/send**

```json
{
  "amount":      "10.00",
  "currencyType": "PEA",
  "chatID":      12345,
  "chatType":    2,
  "rpType":      "STANDARD_RP",
  "recipientIDs": [],
  "rpNum":       5,
  "remark":      "恭喜发财",
  "msgSendTime": 1234567890000000
}
```

**⚠️ currencyType 规则：**
- `workspaceId > 0` → 从 `/workspace/workspace/get` 取 `workspace.currency`（如 `"USDT"`）
- `workspaceId == 0` → `"PEA"`（默认）

### 3.3 领取红包 API

**POST /app/api/wallet/rp/receive**

```json
{
  "rpID":            "rp_xxx",
  "chatID":          12345,
  "rpType":          "STANDARD_RP",
  "sendRpMsgID":     99999,
  "supportMask":     true,
  "supportHideTail": true
}
```

**⚠️ 必须 JSON typed body（非 form 字符串），否则 code=30007**

---

## 4. 数据流

### 4.1 发送红包

```
ChatPage → SendRedEnvelopeSheet
  └─ SendRedEnvelopeViewModel.send(params)
       ├─ currencyType = workspaceId > 0 ? workspace.currency : "PEA"
       ├─ SendRedEnvelopeUseCase.execute() → POST /payment/rp/send → rpID
       └─ 构建 rawContent JSON → 通过 ChatViewModel.sendMessage(typ=8, content)
```

### 4.2 领取红包

```
RedEnvelopeBubble.onTap
  └─ ReceiveRedEnvelopeUseCase.execute(rpID, chatID, rpType, messageId)
       └─ POST /app/api/wallet/rp/receive
            → grabFlag=true → SnackBar 显示金额
            → grabFlag=false → SnackBar 显示原因
```

### 4.3 游戏横幅

```
ChatPage (群聊)
  └─ BannerViewModel(chatId).build()
       ├─ 解析 Group.topic JSON → gameId
       ├─ FetchBannerUseCase(gameId) → POST /lucky/banner/get
       ├─ Timer 每秒 tick → countdown
       └─ SocketManager 消息流 → 过滤 miniapp.NewRound
            → applyGameInfo(bean) → 刷新横幅
```

---

## 5. Provider 设计

```
sendRedEnvelopeUseCaseProvider      → networkSdkApiProvider
receiveRedEnvelopeUseCaseProvider   → networkSdkApiProvider
fetchBannerUseCaseProvider          → networkSdkApiProvider
bannerViewModelProvider.family(chatId)
```

---

## 6. Bug 修复说明

| Bug | 原因 | 修复 |
|-----|------|------|
| code=150001 | iOS 硬编码 `currencyType=PEA`，workspace 用 USDT | Flutter 动态取 workspace.currency |
| `[红包]` 文本 | 旧消息 typ=1 content="[红包]"，未路由到红包气泡 | `typ=8` → RedEnvelopeBubble；typ=1 content=="[红包]" fallback 检测 |

---

## 7. 待完成

- **地雷红包 / 牛牛红包**：MINE_RP + NN_RP 发送 UI（需 rp-config/get）
- **Mini-app WebView**：MiniAppFloatButton 点击后打开小程序（#25）
- **领取详情页**：抢红包排行榜（iOS RedEnvelopeDetailSheet）
- **Game settle polling**：drawTime 后每 3s 轮询直到新一期
