# 服务端埋点审查流程（Go）

## 适用场景

当代码库是 Go 语言且埋点在服务端实现时使用此流程。

**识别特征**：
- 文件路径: `internal/http/{game}/sensor.go`
- 结构体定义: `struct { FieldName type \`json:"field_name"\` }`
- 埋点函数: `huiwan.SensorCustomizedProperties(eventName, uid, data)`

## 审查流程

### 第一步：定位埋点代码

```bash
# 1. 查找埋点定义文件
Glob pattern: "**/internal/http/**/sensor.go"

# 2. 查找特定埋点
Grep pattern: "func {EventName}|SensorCustomizedProperties.*{EventName}"

# 3. 查找调用点
Grep pattern: "{package}.{EventName}\\(|sensor\\.{EventName}\\("
```

**关键文件**：
- `internal/http/{game}/sensor.go` - 埋点定义
- `app/game/{game}/game/*.go` - 游戏逻辑和调用点
- `app/room/service/service.go` - 房间服务调用点

### 第二步：字段完整性检查

**创建对比表格**：

| 需求字段 | 类型 | 实现字段 | 实现类型 | 状态 | 位置 |
|---------|------|----------|----------|------|------|
| game_type | NUMBER | GameType | int32 | ✅/❌ | sensor.go:XX |
| ... | ... | ... | ... | ... | ... |

**检查要点**：
- [ ] 所有需求字段都已实现
- [ ] 字段类型匹配（NUMBER→int32/int64, STRING→string）
- [ ] JSON 标签正确（\`json:"field_name"\`）
- [ ] 没有多余的未定义字段

### 第三步：字段含义深度验证 ⭐

**这是服务端埋点的关键步骤！**

对于每个字段，验证三个层面：

#### 1. 字面值验证（基础）
```
✅ 检查: 字段是否存在
✅ 检查: 值是否非空
❌ 不够: 只检查字段存在是不够的
```

#### 2. 字段含义验证（重要）
```
✅ 检查: 字段的**业务含义**是否与需求一致
✅ 示例:
   - 需求: "survive_score = 作为躲藏者获得的总积分"
   - 代码: SurviveScore: totalScore - playerStat.GhostScore
   - 推导: 总积分 - 猎人积分 = 躲藏者积分 ✅ 含义正确
```

#### 3. 计算逻辑验证（核心）
```
✅ 检查: 计算公式是否正确
✅ 检查: 累加/减法/判断逻辑是否符合业务规则
✅ 检查: 边界情况是否处理

示例问题：
❌ 错误: survive_score = totalScore （遗漏减法）
❌ 错误: attack_score = roundInfo.Score （没有累加）
✅ 正确: attack_score = playerStat.GhostScore （正确累加）
```

**验证清单**：

对于**数值字段**（积分、回合数、时长等）：
- [ ] 计算公式是否符合需求定义
- [ ] 是否有累加逻辑（如 `+=`）
- [ ] 是否在正确的时机累加（如回合结束）
- [ ] 减法推导是否成立（如 `总数 - 部分 = 剩余`）

对于**枚举/分类字段**（身份、状态等）：
- [ ] 所有可能值都有实现分支
- [ ] 判断条件是否正确
- [ ] 字符串值是否与需求**精确匹配**（注意："躲藏者" ≠ "躲藏方"）

对于**时间字段**：
- [ ] 单位是否正确（秒 vs 毫秒 vs 微秒）
- [ ] 计算公式是否正确（结束时间 - 开始时间）
- [ ] 是否需要转换（如 `/ 1000`）

### 第四步：追踪调用链

**标准追踪模板（服务端）**：

```
游戏事件触发（如回合结束、游戏结束）
  ↓ [查找触发点]
游戏逻辑层（game/*.go）
  ↓ [检查字段计算]
埋点函数调用（sensor.{EventName}）
  ↓ [检查参数传递]
埋点定义（sensor.go）
  ↓ [检查结构体字段]
SensorCustomizedProperties
  ↓
埋点上报
```

**追踪方法**：

1. **向前追踪**（从调用到定义）：
```bash
# 找调用点
Grep: "sensor\\.EndGame\\(|huntandseek\\.EndGame\\("

# 看参数构造
Read file + 查看上下文

# 看字段计算
追踪参数来源（如 playerStat.GhostScore）
```

2. **向后追踪**（从定义到来源）：
```bash
# 找字段定义
Grep: "GhostScore.*int32|type.*Stat.*struct"

# 找字段赋值
Grep: "GhostScore.*=|GhostScore.*\\+="

# 找赋值时机
查看周围代码逻辑（回合结束？游戏结束？）
```

### 第五步：数据类型和单位检查 ⭐

**类型兼容性规则**（基于 SuperSorry 项目经验）：

**兼容的类型转换** ✅:
- `int32 → STRING`: **兼容** - 数数平台会自动转换（12345 → "12345"）
- `int64 → STRING`: **兼容** - 同上
- 示例：需求要求 `rid: STRING`，实现为 `rid: int32` → ✅ **兼容，标注为 Info 级别**

**不兼容的类型转换** ❌:
- `bool → STRING`: **很可能不兼容** - 语义差异大（true ≠ "true"）
- 示例：需求要求 `is_again: STRING "true"/"false"`，实现为 `is_again: bool` → ❌ **不兼容，标注为 Critical**

**严重程度判定**:
```markdown
STRING vs int32/int64:
  - 严重程度: ℹ️ Info（兼容但建议统一）
  - 说明: 数数平台自动转换，不影响数据收集
  - 建议: 建议统一类型，但不阻塞上线

STRING vs bool:
  - 严重程度: 🔴 Critical（很可能不兼容）
  - 说明: bool→STRING 不常见，数数平台可能拒绝或解析错误
  - 影响: 可能导致字段数据丢失
  - 要求: 必须立即修复
```

**常见问题清单**：

| 问题类型 | 检查方法 | 示例 | 严重程度 |
|---------|---------|------|---------|
| int32→STRING | 需求STRING vs 实现int32 | rid: int32 vs STRING | ℹ️ Info（兼容） |
| bool→STRING | 需求STRING vs 实现bool | is_again: bool vs STRING | 🔴 Critical（不兼容） |
| 单位错误 | 需求秒 vs 实现毫秒 | duration: 传入毫秒但需求要秒 | 🔴 Critical |
| 字符串不匹配 | 需求"躲藏方" vs 实现"躲藏者" | identity 字符串值不一致 | ⚠️ Warning |
| 枚举遗漏 | 只实现部分枚举值 | scene 只返回"cocos游戏"，缺少"语音房" | 🔴 Critical |
| 精度丢失 | int32 vs int64 | 大数值应使用 int64 | ⚠️ Warning |
| 字段名拼写 | JSON标签拼写错误 | survive_num vs survival_num | 🔴 Critical |

**检查清单**：
- [ ] **类型兼容性验证** - 区分兼容（int32→STRING）和不兼容（bool→STRING）⭐⭐⭐
- [ ] 时间字段单位是否一致（秒/毫秒）
- [ ] 枚举字符串是否**精确匹配**需求（区分大小写、标点）
- [ ] 数值范围是否会溢出（int32 vs int64）
- [ ] JSON 字段名是否与需求文档**精确匹配**（逐字符对比，不能凭记忆）⭐⭐⭐

### 第六步：特殊逻辑验证

**累计统计验证**：

如果字段是"总XX"、"累计XX"，必须验证：
```go
// ❌ 错误模式: 只取当前值
TotalScore: roundInfo.Score  // 只有当前回合分数

// ✅ 正确模式: 累加所有值
totalScore := int32(0)
for _, roundInfo := range player.RoundInfos {
    totalScore += roundInfo.Score  // 累加所有回合
}
```

**强退/异常场景验证**：

检查是否有特殊逻辑处理：
```go
if player.ForceQuit {
    // 使用强退时间
    param.EventDuration = (player.ForceQuitTimeMs - gameInfo.CreateTimestampMs) / 1000
} else {
    // 使用正常结束时间
    param.EventDuration = (now - gameInfo.CreateTimestampMs) / 1000
}
```

**条件分支完整性**：

对于有多个可能值的字段：
```go
// ❌ 危险: 只有 if，没有 else
if condition {
    return "值1"
}
// 缺少其他情况

// ✅ 正确: 完整覆盖
if condition1 {
    return "值1"
} else if condition2 {
    return "值2"
} else {
    return "值3" // 或报错
}
```

### 第七步：生成测试报告

**服务端埋点报告模板**：

```markdown
# {游戏名} - {埋点名} 审查报告

## 基本信息
- **游戏**: {游戏名}（game_type={游戏ID}）
- **埋点**: {埋点名}
- **文件**: {sensor.go路径}
- **调用位置**: {调用文件}:{行号}

## 字段检查结果

| 需求字段 | 类型 | 实现 | 状态 | 问题 |
|---------|------|------|------|------|
| game_type | NUMBER | int32(1052) | ✅ | - |
| identity | STRING | "躲藏者" | ⚠️ | 应为"躲藏方" |
| ... | ... | ... | ... | ... |

## 重点字段深度分析

### survive_score（躲藏者总积分）

**需求定义**: 作为躲藏者获得的总积分

**代码实现**:
\`\`\`go
SurviveScore: totalScore - playerStat.GhostScore
\`\`\`

**逻辑推导**:
- totalScore = 所有回合积分总和 ✅
- GhostScore = 作为猎人时的积分累加 ✅
- 推导: 总积分 - 猎人积分 = 躲藏者积分 ✅

**结论**: ✅ 含义正确，计算逻辑正确

## 发现的问题

### 🔴 问题 #1: identity 字符串不匹配
- **严重程度**: Warning
- **位置**: sensor.go:109
- **问题**: 实现为"躲藏者"，需求为"躲藏方"
- **影响**: 数据分析无法匹配
- **修复**: 修改字符串为"躲藏方"

## 调用链路图

\`\`\`
游戏结束
  ↓
fsm.go:sendSensorEndGame()
  ↓
计算 totalScore（循环累加）
计算 SurviveScore = totalScore - GhostScore
  ↓
huntandseek.EndGame(uid, param)
  ↓
sensor.go:EndGame() → SensorCustomizedProperties
  ↓
埋点上报
\`\`\`

## 测试建议

1. **正常结束场景**: 验证所有字段值
2. **强退场景**: 验证 ForceQuitRole 和 stage
3. **身份切换场景**: 验证多回合积分累加
4. **边界场景**: 验证第1回合和最后回合

## 总体评估

- **字段完整性**: ✅ 18/18 (100%)
- **类型正确性**: ⚠️ 17/18 (94%)
- **逻辑正确性**: ✅ 18/18 (100%)
- **严重问题**: 0 个
- **一般问题**: 1 个（字符串不匹配）

**结论**: 实现质量良好，仅需修复字符串匹配问题。
```

## 常见错误模式

### 错误模式 1: 字段含义理解错误

**症状**: 字段存在但计算错误

**案例**:
```go
// ❌ 错误: survive_score 直接取总分
SurviveScore: totalScore  // 没有减去猎人得分

// ✅ 正确: 减去猎人得分
SurviveScore: totalScore - playerStat.GhostScore
```

**检查方法**: 对照需求定义，推导计算公式是否成立

### 错误模式 2: 缺少累加逻辑

**症状**: "总XX"字段只取当前值

**案例**:
```go
// ❌ 错误: 只取当前回合
TotalScore: currentRoundScore

// ✅ 正确: 累加所有回合
totalScore := int32(0)
for _, roundInfo := range player.RoundInfos {
    totalScore += roundInfo.Score
}
```

**检查方法**: 搜索字段赋值，看是否有循环累加

### 错误模式 3: 字符串值硬编码错误

**症状**: 字符串与需求不完全匹配

**案例**:
```go
// ❌ 错误: "躲藏者"
identity = "躲藏者"

// ✅ 正确: "躲藏方"（需求文档的准确用词）
identity = "躲藏方"
```

**预防**: 复制粘贴需求文档中的字符串，不要手动输入

### 错误模式 4: 单位遗漏转换

**症状**: 时间单位不匹配

**案例**:
```go
// ❌ 错误: 传入毫秒但需求要秒
EventDuration: now - startTime  // 毫秒

// ✅ 正确: 转换为秒
EventDuration: (now - startTime) / 1000  // 秒
```

**检查方法**: 确认需求单位，看代码是否有 `/1000` 或 `*1000`

### 错误模式 5: 字段名拼写错误 ⭐⭐⭐

**症状**: JSON 字段名与需求文档不完全匹配

**案例 - survival_num 字段**（HuntAndSeek 项目）:
```go
// ❌ 错误: 字段名少了 "al"
SurviveNum     int32  `json:"survive_num"`   // 实际上报: survive_num

// ✅ 正确: 与需求文档精确匹配
SurviveNum     int32  `json:"survival_num"`  // 需求要求: survival_num
```

**为什么容易遗漏**:
```
需求: survival_num  ← 8个字母
实际: survive_num   ← 7个字母
差异: 只差 "al" 两个字母

❌ 错误的审查方法:
1. 看需求: survival_num ✓
2. 看代码: survive_num ✓
3. 肉眼判断: 看起来一样 → 标记为正确 ❌

✅ 正确的审查方法:
1. 复制需求字段名: survival_num
2. 复制代码字段名: survive_num
3. 精确对比:
   - 字符1-6: s-u-r-v-i-v ✅
   - 字符7-8: al vs e ❌ 发现不同！
4. 或使用 diff 工具逐字符比较
```

**影响**:
- 🔴 **Critical**: 数数平台收不到数据
- 数据分析平台期望 `survival_num`
- 服务端实际上报 `survive_num`
- 字段无法匹配，数据丢失

**预防方法**:
1. **禁止手动输入字段名** - 必须从需求文档复制粘贴
2. **使用精确字符串匹配** - 不能凭肉眼或记忆判断
3. **逐字段对比验证** - 使用表格或脚本对比每个字段名
4. **代码审查时重点检查** - JSON 标签是高风险区域

**检查方法**:
```bash
# 1. 提取需求文档中的字段名列表
cat requirements.md | grep "字段名" > required_fields.txt

# 2. 提取代码中的 JSON 标签
grep -o 'json:"[^"]*"' sensor.go | sort > actual_fields.txt

# 3. 对比差异
diff required_fields.txt actual_fields.txt
```

## 服务端埋点审查清单

**开始审查前**：
- [ ] 确认游戏 ID（game_type）
- [ ] 定位 sensor.go 文件
- [ ] 获取需求文档（字段列表）

**字段级别检查**：
- [ ] 所有需求字段都存在
- [ ] 字段类型匹配
- [ ] JSON 标签正确
- [ ] **JSON 字段名精确匹配需求文档**（不能凭记忆，必须复制对比）⭐⭐⭐
- [ ] 字段含义符合需求定义 ⭐

**逻辑级别检查**：
- [ ] 计算公式正确
- [ ] 累加逻辑完整
- [ ] 分支覆盖所有情况
- [ ] 时间单位正确
- [ ] 字符串精确匹配

**调用链检查**：
- [ ] 找到所有调用点
- [ ] 调用时机正确
- [ ] 参数传递完整
- [ ] 无遗漏场景

**特殊场景检查**：
- [ ] 强退/异常逻辑
- [ ] 第一回合/最后回合
- [ ] 边界值处理
- [ ] 多回合累计

**测试用例覆盖检查**（⭐⭐⭐ 如果提供了测试用例文档）：
- [ ] **建立测试用例追踪表** - 列出所有测试用例ID和状态
- [ ] **逐条验证** - 每个测试用例都要提供预期vs实际对比
- [ ] **详细验证逻辑** - 不能只说"✅"，必须追踪代码路径
- [ ] **提供代码位置** - 每个验证点都标注文件:行号
- [ ] **统计覆盖率** - X/Y个测试用例通过，明确剩余问题

## HuntAndSeek 案例研究

### 关键学习点

1. **字段名精确匹配的重要性**（⭐⭐⭐ 最重要）
   - survival_num vs survive_num - 只差2个字母导致数据完全丢失
   - 禁止凭肉眼判断，必须复制粘贴精确对比
   - JSON 字段名错误 = Critical Bug

2. **强退场景测试的必要性**（⭐⭐⭐ 最重要）
   - survival_num 在强退场景下计算错误（使用 CurrentRound 而非 ForceQuitRound）
   - 正常场景测试通过不代表所有场景都正确
   - 必须主动设计强退、异常、边界测试用例

3. **字段含义验证的重要性**
   - survive_score 不是"存活加分"，而是"作为躲藏者的总积分"
   - 需要通过 `总分 - 猎人分` 计算得出

4. **累计统计的正确实现**
   - GhostScore 在每回合结束时判断身份并累加
   - GhostRoundCount 同步累加回合数

5. **字符串匹配的精确性**
   - "躲藏者" vs "躲藏方" - 一字之差导致匹配失败
   - 需要从需求文档复制粘贴，不能凭记忆输入

6. **强退逻辑的完整性**
   - 强退时使用 ForceQuitTimeMs 而非当前时间
   - ForceQuitRole 记录强退时的身份
   - 需要区分强退和正常场景的不同计算逻辑

### 测试流程示例

```
1. 定位文件 → sensor.go 和 fsm.go
2. 字段检查 → 18个字段全部存在
3. 深度验证 → 重点检查7个计算字段
4. 发现问题 → identity 字符串不匹配
5. 追踪逻辑 → 验证 GhostScore 累加正确
6. 生成报告 → 含字段表、逻辑分析、问题清单
```
