# 关键教训

**从 HuntAndSeek、Mancala、SuperSorry 等项目实战中总结的血泪教训，必读！**

## 🔴 教训 #1: 字段名拼写必须精确匹配（最重要）

**案例**: `survival_num` vs `survive_num` - 只差 2 个字母，导致数据完全丢失

**症状**:
```go
// ❌ 错误
SurviveNum int32 `json:"survive_num"`   // 实际上报
// 需求要求: survival_num ← 少了 "al"
```

**为什么容易遗漏**:
- 肉眼判断 "survive" 和 "survival" 看起来很像
- 高相似度的拼写错误极易被忽略
- JSON 标签是手动输入，容易打错

**影响**:
- 🔴 **Critical**: 数数平台收不到数据
- 字段名不匹配 = 数据完全丢失
- 影响所有数据分析和产品决策

**预防方法**:
1. ✅ **禁止手动输入字段名** - 必须从需求文档复制粘贴
2. ✅ **使用 diff 工具验证** - 不能凭肉眼判断
3. ✅ **逐字段精确对比** - 对每个字段都要复制对比

---

## 🔴 教训 #2: 强退场景必须测试（最重要）

**案例**: `survival_num` 在强退场景下计算错误

**症状**:
```go
// ❌ 错误: 强退时使用游戏总回合数
SurviveNum: gameInfo.CurrentRound - playerStat.GhostRoundCount

// 场景: 游戏5回合，玩家第2回合强退
// 错误计算: 5 - 1 = 4（应该是 2 - 1 = 1）
```

**为什么容易遗漏**:
- 正常场景测试通过 → 以为所有场景都正确
- 没有主动设计强退测试用例
- 假设 `CurrentRound` 总是等于玩家参与回合数

**影响**:
- 强退场景的数据完全错误
- 验证公式失效（`survival_num + total_attack_num ≠ round`）
- 无法统计强退玩家的真实数据

**预防方法**:
1. ✅ **必须包含强退场景测试** - 第1回合/中间/最后回合强退
2. ✅ **主动验证强退分支** - 检查是否使用 `ForceQuitRound`
3. ✅ **设计验证公式** - 在多种场景下验证公式成立

---

## 🔴 教训 #3: 枚举字段必须逐值验证（重要）

**案例**: `scene` 字段只实现了 "cocos游戏"，遗漏 "语音房"

**症状**:
```go
// ❌ 错误: 只返回固定值
func trackScene() string {
    return "cocos游戏"  // 没有判断逻辑
}
```

**为什么容易遗漏**:
- 只检查一个枚举值是否存在
- 没有验证所有可能值都能返回
- 忽略了 "语音房" 等其他枚举值

**影响**:
- 无法区分不同场景的数据
- 数据分析时缺少重要维度
- 业务需求无法满足

**预防方法**:
1. ✅ **识别所有枚举值** - 从需求文档提取完整列表
2. ✅ **逐值验证** - 对每个值都检查是否有实现分支
3. ✅ **检查危险信号** - 固定返回值、只有 if 没有 else

---

## 🔴 教训 #4: 理解埋点的业务含义，区分成功路径和流失路径（最重要）⭐⭐⭐

**案例**: `GameStartWaitDuration` vs `GameLeaveWaitDuration` - 一对成功/流失埋点

### 核心理解（⭐ 重要！）

**这是两个互补的埋点，记录不同的用户路径**:

```
用户开始匹配
  ├─→ 成功路径: 等待 → 游戏开始 → 上报 GameStartWaitDuration ✅
  └─→ 流失路径: 等待 → 主动退出 → 上报 GameLeaveWaitDuration ✅
```

**GameStartWaitDuration（成功路径）**:
- **含义**: 开始匹配 → 等待 → **成功开始游戏** 的时长
- **上报时机**: 游戏开始时
- **场景**: 玩家成功等到游戏开始（正向结果）

**GameLeaveWaitDuration（流失路径）**:
- **含义**: 开始匹配 → 等待 → **中途放弃退出** 的时长
- **上报时机**: 游戏未开始时，用户**主动退出**
- **场景**: 玩家在等待中放弃，流失（负向结果）

### 常见误解（⚠️ 容易犯错）

**错误理解 1**: "匹配成功进入游戏房间时，玩家从准备房间'退出'了，所以应该上报 GameLeaveWaitDuration"

```go
// ❌ 错误思路
if reason == "UserExitRoomByEnterNew" {  // 匹配成功进入新房间
    GameLeaveWaitDuration(...)  // 误以为是"退出"
}
```

**正确理解**: 匹配成功进入游戏 = **成功路径**，不是"退出/流失"
- ✅ 应该上报 `GameStartWaitDuration`（成功等到游戏开始）
- ❌ 不应该上报 `GameLeaveWaitDuration`（这不是流失）

**错误理解 2**: "所有离开房间的行为都应该上报 GameLeaveWaitDuration"

**正确理解**: 只有**主动放弃、流失**的退出才应该上报
- ✅ `reason = "UserExitRoom"` → 用户主动点击退出按钮 → 真正的流失
- ❌ `reason = "UserExitRoomByEnterNew"` → 匹配成功进入游戏 → 不是流失

### reason 参数的正确使用

**SuperSorry 的实现**:
```go
if reason == "UserExitRoom" && roomInfo.GameType == int32(shared.WeGameType_WGTSuperSorry) {
    supersorryHttp.GameLeaveWaitDuration(...)
}
```

**为什么 reason 条件是正确的**:

| reason 值 | 场景 | 是否流失？ | 应上报 GameLeaveWaitDuration？ |
|-----------|------|----------|---------------------------|
| "UserExitRoom" | 用户主动点击退出 | ✅ 是 | ✅ **是** |
| "UserExitRoomByEnterNew" | 匹配成功进入游戏 | ❌ 否（成功） | ❌ **否** |
| "JoinRoomExitOldRoom" | 加入新房间 | ❌ 否（转移） | ❌ 否 |
| "kickHeartbeatExpiredUser" | 心跳超时被踢 | ⚠️ 可能是 | ⚠️ 看需求 |

**结论**: `reason == "UserExitRoom"` 条件是**正确的**，因为：
- 只记录用户主动退出的流失场景
- 匹配成功不应该算作"流失"
- 符合埋点的业务含义

### 仍需修复的问题（⚠️）

虽然 reason 条件正确，但**缺少游戏状态判断**:

```go
// ❌ 当前代码: 缺少状态判断
if reason == "UserExitRoom" && roomInfo.GameType == int32(shared.WeGameType_WGTSuperSorry) {
    supersorryHttp.GameLeaveWaitDuration(...)
}

// 需求标题: "开始匹配到游戏未开始-退出时长" ← "未开始"是关键限定！
```

**问题场景**: 游戏已经开始后，玩家点击"退出游戏"
- `reason = "UserExitRoom"` ✅ 条件满足
- `roomInfo.State = RoomStateGaming` ⚠️ 游戏已开始
- 结果: 错误上报 GameLeaveWaitDuration

**正确实现**:
```go
// ✅ 添加状态判断
if reason == "UserExitRoom" &&
   roomInfo.GameType == int32(shared.WeGameType_WGTSuperSorry) &&
   roomInfo.State != int32(pbRoom.RoomState_RoomStateGaming) {  // ← 只在游戏未开始时
    supersorryHttp.GameLeaveWaitDuration(...)
}
```

### 预防方法

1. ✅ **理解埋点的业务含义** - 区分成功路径和流失路径
2. ✅ **需求标题和正文都要读** - 标题中的限定词（如"未开始"）很重要
3. ✅ **不要简单套用其他游戏的实现** - 不同埋点的 reason 条件可能不同
4. ✅ **必须包含负向测试** - 验证"不应该上报"的场景

### 负向测试示例

```markdown
# GameLeaveWaitDuration 测试用例

## 正向场景（应该上报）
- [ ] 等待中主动退出 (State=Waiting, reason=UserExitRoom) ✅
- [ ] 匹配中主动退出 (State=Matching, reason=UserExitRoom) ✅

## 负向场景（不应该上报）⭐ 容易遗漏！
- [ ] 匹配成功进入游戏 (reason=UserExitRoomByEnterNew) ❌
- [ ] 游戏中主动退出 (State=Gaming, reason=UserExitRoom) ❌
- [ ] 游戏结束后退出 (State=GameOver, reason=UserExitRoom) ❌

## 对应的正确上报
- [ ] 匹配成功进入游戏 → 应上报 GameStartWaitDuration ✅
- [ ] 游戏中退出 → 应上报 EndGame (user_state=2) ✅
```

### 为什么容易理解错误

1. **表面现象混淆** - "退出房间"这个动作在两种路径中都有
   - 匹配成功 → 退出准备房间 → 进入游戏房间（成功路径）
   - 主动放弃 → 退出房间（流失路径）

2. **只看代码不看需求** - 看到"退出房间"就以为应该上报 GameLeaveWaitDuration

3. **没有区分 reason** - 不同的 reason 代表不同的退出原因

### 教训总结

> **核心教训**: 理解埋点的业务含义，区分成功路径和流失路径。不是所有"退出"都是"流失"。

**关键点**:
- ✅ GameStartWaitDuration = 成功路径（等待 → 游戏开始）
- ✅ GameLeaveWaitDuration = 流失路径（等待 → 主动退出）
- ✅ reason="UserExitRoom" 条件是正确的（只记录主动退出的流失）
- ⚠️ 但需要添加状态判断（只在游戏未开始时上报）

---

## 🔴 教训 #5: 多游戏模式全覆盖测试（最重要）⭐ 新增！

**案例**: SuperSorry `match_mode` 字段 - 金币场正确，免费场错误

**症状**:
```go
// 金币场 (supersorry_matchmaker_coin.go:82) ✅
CreateFullMatchRoom(..., matchTeamList)  // 正确

// 免费场 (supersorry_matchmaker_free.go:76) ❌
CreateFullMatchRoom(..., []int32{})      // Bug：传空数组而非 matchTeamList
```

**为什么容易遗漏**:
- 只测试了金币场 → 发现 match_mode 正确（值为 1）
- **错误假设**："金币场正确，免费场应该也对"
- 没有意识到不同模式有**独立的实现文件**
- 复制粘贴代码时忘记修改参数

**影响**:
- 🔴 **Critical**: 免费场匹配后 match_mode 错误（应该是1，实际是2）
- 无法区分免费场的匹配和自建房数据
- 测试覆盖率只有 50%（测了2个模式中的1个）

**核心教训**:
> **游戏有 N 个模式，就必须测试 N 个模式。没有例外，没有假设，必须全覆盖。**

**预防方法**:
1. ✅ **识别所有游戏模式** - 免费场、金币场、排位场等
2. ✅ **建立测试矩阵** - 游戏模式 × 进入方式（匹配/自建房）
3. ✅ **逐模式验证** - 每个模式都要独立测试
4. ✅ **代码审查警示标志** - 声明变量但传硬编码值

**测试矩阵示例**:
```markdown
| 游戏模式 | 进入方式 | match_mode 预期 | 实际值 | 状态 |
|---------|---------|----------------|--------|------|
| 免费场   | 匹配    | 1              | ?      | [ ]  |
| 免费场   | 自建房  | 2              | ?      | [ ]  |
| 金币场   | 匹配    | 1              | ?      | [ ]  |
| 金币场   | 自建房  | 2              | ?      | [ ]  |
```

---

## 🔴 教训 #6: 一致性测试必须做，防止"偶然正确"（重要）⭐ 新增！

**案例**: SuperSorry `system_coin` 字段 - 平局时不一致（有的0，有的5）

**症状**:
```go
// 修复前代码
systemCoin := int32(supersorry.CoinMatchTicket)  // 默认 5
if in.WinnerUid == 0 {
    giveCoin = player.PreDeduct
    // ❌ 忘记设置 systemCoin = 0
    reasonStr = "金币场结算_平局返还"
}
```

**为什么容易遗漏**:
- **正常流程有 bug** - systemCoin = 5（错误）
- **异常流程偶然正确** - RPC 失败返回 nil → systemCoin = 0（对但不可靠）
- **单次测试可能通过** - 运气好遇到异常流程 → 值正确
- **数据不一致才暴露** - 需要大量数据才能发现"有的 0 有的 5"

**影响**:
- 🔴 **Critical**: 平局时 system_coin 不一致
- 偶然正确掩盖了 bug，更难发现
- 修复一个问题可能暴露另一个问题

**"偶然正确"反模式**:
```
正常流程（90%） → systemCoin = 5 ❌ 错误
异常流程（10%） → systemCoin = 0 ✅ 偶然正确

单次测试 → 10% 概率遇到异常 → 测试通过 → bug 未发现
```

**预防方法**:
1. ✅ **重复测试验证一致性** - 执行 10 次，检查结果是否一致
2. ✅ **测试异常路径** - RPC 失败、网络超时等场景
3. ✅ **避免硬编码默认值** - 每个分支显式设置所有字段
4. ✅ **数据监控** - 检测同一场景下字段值不一致

**一致性测试示例**:
```go
func TestSystemCoinConsistency(t *testing.T) {
    // 重复测试 10 次
    var results []int32
    for i := 0; i < 10; i++ {
        result := checkout(0, 1001, false)  // 平局
        results = append(results, result.SystemCoin)
    }

    // 验证所有结果都一致
    for _, r := range results {
        assert.Equal(t, results[0], r, "所有结果应该一致")
    }
}
```

---

## 🔴 教训 #7: 测试用例必须逐条覆盖，每条都要预期vs实际（最重要）⭐⭐⭐ 新增！

**案例**: SuperSorry 测试报告 - 最初说"TC12-01 user_state ✅"但没有验证逻辑

**症状**:
```markdown
❌ 错误的测试报告格式：
TC12-01: 正常游戏结束（赢）
- user_state: ✅  // 只说"通过"，没有验证逻辑
- game_result: ✅
```

**为什么不够**:
- 只给出"✅"但没有说明如何验证
- 没有追踪代码逻辑（ForceQuit=false → userState=1）
- 无法复现验证过程
- 其他人看不懂为什么是"✅"

**影响**:
- 🔴 **Critical**: 测试报告缺乏可信度
- 无法验证测试的准确性
- 后续修复时无法确认是否真的通过
- 测试覆盖率无法量化

**核心要求**:
> **如果提供了测试用例文档，必须逐条覆盖每一个测试用例，每条都要提供：**
> 1. **测试场景描述**
> 2. **预期结果**（从测试用例文档复制）
> 3. **实际结果**（基于代码审查的详细验证）
> 4. **验证逻辑**（代码位置 + 逻辑推导）
> 5. **状态标记**（✅/❌/⚠️）

**正确的测试报告格式**:

```markdown
✅ 正确的测试报告格式：

### TC12-01: 正常游戏结束（赢）

**测试场景**: 玩家正常完成游戏并获胜

**预期结果**:
- user_state = 1（正常结束）
- game_result = 1（获胜）
- event_duration ≈ game_duration

**实际结果（代码审查）**:

1. **user_state 验证** (sensor.go:151-154):
   ```go
   var userState int32 = 1  // 默认值为1（正常）
   if param.IsForceQuit {   // TC12-01场景: ForceQuit = false
       userState = 2        // 不执行此分支
   }
   // 结果: userState保持默认值1 ✅
   ```

2. **game_result 验证** (fsm_handler.go:211-215):
   ```go
   gameResult := int32(0)  // 默认平局
   if player.Uid == g.Winner {  // TC12-01场景: 玩家是赢家
       gameResult = 1            // 设置为1（获胜）✅
   } else if g.Winner != 0 {
       gameResult = -1           // 其他玩家获胜
   }
   ```

3. **event_duration 验证** (fsm_handler.go:228):
   ```go
   eventDuration = (now - g.CreateTime) / 1000  // 秒
   gameDuration = (now - g.CreateTime) / 1000   // 秒
   // 正常结束场景: eventDuration == gameDuration ✅
   ```

**状态**: ✅ **通过** - 所有字段逻辑正确，代码路径验证完整

**代码位置**:
- user_state: `sensor.go:151-154`
- game_result: `fsm_handler.go:211-215`
- event_duration: `fsm_handler.go:228`
```

**测试用例覆盖表格**:

```markdown
| 用例ID | 测试场景 | 预期结果 | 实际结果（代码审查） | 状态 | 问题 |
|--------|---------|---------|---------------------|------|------|
| TC12-01 | 正常游戏结束（赢） | user_state=1, game_result=1 | user_state=1(ForceQuit=false→默认1)✅, game_result=1(Winner判断)✅ | ✅ | - |
| TC12-02 | 正常游戏结束（输） | user_state=1, game_result=-1 | user_state=1✅, game_result=-1✅ | ✅ | - |
| TC12-04 | 游戏中强退 | user_state=2 | user_state=2(ForceQuit=true→2)✅ | ✅ | - |
```

**预防方法**:
1. ✅ **建立测试用例追踪表** - 列出所有测试用例，逐条标记状态
2. ✅ **每条用例都要详细验证** - 不能只说"✅"，必须追踪代码逻辑
3. ✅ **提供代码位置和逻辑链路** - 让其他人能复现验证过程
4. ✅ **区分"字段存在"和"逻辑正确"** - user_state 字段存在≠user_state 值正确
5. ✅ **统计覆盖率** - X/Y 个测试用例通过，明确剩余问题

**常见错误对比**:

| 错误做法 | 正确做法 |
|---------|---------|
| ❌ user_state: ✅ | ✅ user_state=1 (sensor.go:151-154: ForceQuit=false→默认值1) |
| ❌ game_result: ✅ | ✅ game_result=1 (fsm_handler.go:211: player.Uid==Winner→1) |
| ❌ "看起来正确" | ✅ 代码路径追踪 + 逻辑推导 + 边界验证 |
| ❌ "应该没问题" | ✅ 实际验证：正常场景✅、强退场景✅、边界场景✅ |

**测试覆盖统计示例**:

```markdown
## CreateRoom 埋点测试结果

**测试用例总数**: 9个
**✅ 预期通过**: 8个 (89%)
**❌ 预期失败**: 0个
**⚠️ 需要实测**: 1个 (TC06-05 失败处理)

**详细结果**:
- TC06-01~TC06-04: ✅ 通过（已详细验证代码逻辑）
- TC06-05: ⚠️ 需要实测（失败分支代码未找到）
- TC06-06~TC06-09: ✅ 通过（已验证类型兼容性）
```

**教训总结**:
> **测试用例不是形式主义，每一条都要认真验证！**
>
> - 提供了测试用例文档 = 必须逐条覆盖
> - 每条用例都要"预期 vs 实际"对比
> - 不能只说"✅"，必须说明验证逻辑
> - 代码位置 + 逻辑推导 = 可复现的验证

---

## 核心原则

**记住这8条原则，避免 95% 的埋点错误：**

1. 🚫 **禁止凭肉眼/记忆** - 字段名必须复制粘贴对比
2. 🚫 **禁止只测正常场景** - 强退、异常、边界场景必测
3. 🚫 **禁止只验证一个枚举值** - 所有可能值都要逐个检查
4. 🚫 **禁止只做正向测试** - "不应该上报"的场景同样要验证
5. 🚫 **禁止只测一个模式** - N个模式必须测N次，不能假设 ⭐ 新增！
6. 🚫 **禁止单次测试就下结论** - 重复测试验证一致性，测试异常路径 ⭐ 新增！
7. 🚫 **禁止只看代码不看业务含义** - 理解埋点的业务含义，区分成功路径和流失路径 ⭐ 新增！
8. 🚫 **禁止只说"✅"不验证逻辑** - 每个测试用例都要详细追踪代码逻辑，提供验证证据 ⭐⭐⭐ 新增！
