TEST_PLAN.md 22 KB

腾讯-百度追踪回传系统 (tencent-baidu-tracking) 测试文档

一、项目概述

本系统是腾讯广告平台与百度ADX之间的追踪和转化回传中转服务器,主要功能:

  1. 接收腾讯曝光/点击监测请求
  2. 向百度ADX发起竞价请求并上报追踪事件
  3. 定时从百度拉取转化数据并回传给腾讯
  4. 失败回调自动重试

技术栈:Java 17 / Spring Boot 3.2.5 / MyBatis 3.0.3 / Redis (Lettuce) / MySQL(TiDB)


二、测试环境要求

组件 版本/配置
JDK 17+
Redis 6.0+ (需支持 Stream)
MySQL/TiDB 5.7+ / TiDB 6.0+
Maven 3.8+
Profile application-test.yml

环境准备

  • Redis 地址: 配置在 application-test.ymladx.redis-addr
  • MySQL/TiDB: 配置在 adx.tidb-url,需预先创建数据库 adx_tencent
  • 系统自动建表(TiDBColdStore.migrate()

三、API 接口测试

3.1 健康检查

项目 说明
端点 GET /health
预期响应 200 OK,Body: {"ok": true}

测试用例

编号 场景 请求 预期结果
HC-01 正常健康检查 GET /health 200, {"ok":true}
HC-02 服务启动后立即检查 GET /health 200, {"ok":true}

3.2 曝光监测接口

项目 说明
端点 GET /tencent/impression
必需参数 至少一个: click_id, trace_id, callback, request_id, oaid, imei, idfa, caid
成功响应 204 No Content

测试用例

编号 场景 请求参数 预期结果
IMP-01 正常曝光(click_id) ?click_id=test123&platform=android 204 No Content
IMP-02 正常曝光(oaid) ?oaid=abc123&platform=android 204 No Content
IMP-03 正常曝光(imei) ?imei=def456&platform=android 204 No Content
IMP-04 正常曝光(idfa+ios) ?idfa=guid123&platform=ios 204 No Content
IMP-05 缺少 trace id ?platform=android 400, {"error":"missing media trace id..."}
IMP-06 空 trace id ?click_id=&platform=android 400, {"error":"missing media trace id..."}
IMP-07 无 platform 且有默认配置 ?click_id=test123 204(使用默认 baidu_app_id/tag_id)
IMP-08 无 platform 且无默认配置 ?click_id=test123(未配置默认) 400, {"error":"missing baidu_tag_id or platform"}
IMP-09 指定 baidu_tag_id ?click_id=x&baidu_tag_id=tag001 204
IMP-10 无效的 baidu_tag_id ?click_id=x&baidu_tag_id=invalid 400, {"error":"unknown baidu tag id: invalid"}
IMP-11 百度 ADX 返回空响应 模拟百度返回空 502, {"error":"empty baidu ADX response"}
IMP-12 百度 ADX 网络超时 模拟网络超时 502, {"error":"..."}

验证点

  • Redis 中应存在 bid 缓存键:adx:tencent:bid:{qk}adx:tencent:media:tencent:{traceId}
  • Redis Stream adx:tencent:events 中应有 type=bid 的事件
  • 若有 showUrls,应能看到 impression tracking 的 Stream 事件

3.3 点击监测接口

项目 说明
端点 GET /tencent/click
必需参数 至少一个: click_id, trace_id, callback, request_id, oaid, imei, idfa, caid
成功响应 302 Redirect(重定向到落地页/应用商店)

测试用例

编号 场景 请求参数 预期结果
CLK-01 已有 bid 记录(重定向到 landingPage) ?click_id=test123&platform=android 302, Location=landingPage
CLK-02 已有 bid 记录(重定向到 appStoreLink) ?click_id=test123&platform=ios 302, Location=appStoreLink
CLK-03 无 bid 记录(临时竞价) ?click_id=newId&platform=android 302 或 JSON
CLK-04 缺少 trace id ?platform=android 400, {"error":"missing media trace id..."}
CLK-05 无重定向目标 ?click_id=noDest&platform=android 200 JSON: {"qk":"...","traceId":"...","tracking":[...]}
CLK-06 百度点击上报失败(不影响重定向) 模拟点击 URL 返回 500 仍然 302 重定向
CLK-07 百度 ADX 竞价失败(无已有 bid) 模拟 ADX 返回错误 502, {"error":"..."}

验证点

  • 点击上报后 Redis Stream 应有 type=tracking, kind=click 的事件
  • 302 重定向的 Location 头应为有效 URL

3.4 管理接口

3.4.1 查询百度转化

项目 说明
端点 POST /admin/conversions/query
Content-Type application/json
请求体 {"date":"20260702","pageSize":1,"acts":[1,2]}

测试用例

编号 场景 请求体 预期结果
AQ-01 正常查询 {"date":"20260702","pageSize":1} 200, ConversionResponse
AQ-02 缺少 date {"pageSize":1} 400, {"error":"date is required"}
AQ-03 pageSize 无效 {"date":"20260702","pageSize":0} 400, {"error":"pageSize must be > 0"}
AQ-04 百度 API 返回错误码 模拟百度返回 code!=200 502, {"error":"Baidu conversion API failed: ..."}

3.4.2 手动触发转化同步

项目 说明
端点 POST /admin/conversions/sync
Content-Type application/json

测试用例

编号 场景 请求体 预期结果
AS-01 正常同步 {"date":"20260702","pageSize":1,"acts":[1]} 200, SyncResult
AS-02 syncer 未配置 (TiDB 未配置时) 503, {"error":"conversion syncer is not configured"}
AS-03 百度无数据 {"date":"20260101","pageSize":1} 200, fetched=0

3.4.3 手动触发回调重试

项目 说明
端点 POST /admin/callbacks/retry
Content-Type application/json

测试用例

编号 场景 请求体 预期结果
AR-01 正常重试 {"limit":10} 200, RetryResult
AR-02 retryService 未配置 (TiDB 未配置时) 503, {"error":"callback retryer is not configured"}
AR-03 limit=0(使用默认值) {"limit":0} 200, RetryResult
AR-04 无待重试记录 {"limit":10} 200, {"fetched":0,"sent":0,"failed":0}

四、核心业务逻辑测试

4.1 竞价价格编码 (AuctionPriceEncoder)

编号 模式 输入 预期
PE-01 none price=1000 URL encode 的 "1000"
PE-02 aes-ecb price=1000,eKey=16字节 Base64 编码的 AES-ECB 加密值(URL encoded)
PE-03 aes-cbc price=1000,eKey+iKey Base64 编码的 AES-CBC 加密值(URL encoded)
PE-04 aes-cbc iKey 不足16字节 eKey=xxx, iKey=short 抛出 IllegalArgumentException
PE-05 未知模式 mode="unknown" 抛出 IllegalArgumentException
PE-06 自动推断(有 eKey+iKey) mode="",提供 eKey+iKey 使用 aes-cbc
PE-07 自动推断(仅 eKey) mode="",仅提供 eKey 使用 aes-ecb
PE-08 自动推断(无 key) mode="",无 key 使用 none

4.2 百度转化签名 (ConversionClient)

编号 场景 验证
CS-01 签名正确性 md5(timestamp + secret + randomStr) 结果为32位十六进制
CS-02 相同输入产生相同签名 固定 timestamp/secret/randomStr 验证一致性
CS-03 随机字符串唯一性 两次 newRandomString() 结果不同

4.3 MediaPlacement 解析

编号 场景 输入 params 预期结果
MP-01 指定 baidu_tag_id(匹配 platform 配置) {baidu_tag_id: "tag1"} Resolved(platform, appId, "tag1")
MP-02 指定 platform=android {platform: "android"} Resolved("android", androidAppId, androidTagId)
MP-03 指定 platform=ios {platform: "ios"} Resolved("ios", iosAppId, iosTagId)
MP-04 platform 别名("安卓") {platform: "安卓"} 归一化为 "android"
MP-05 platform 别名("1"=ios) {platform: "1"} 归一化为 "ios"
MP-06 无 tag/platform 但有默认值 {} 使用默认 baiduAppId/baiduTagId
MP-07 多 tagId 未指定具体 tag {platform: "android"}(配置多个 tagId) 抛出 IllegalArgumentException
MP-08 未知 tag_id {baidu_tag_id: "unknown"} 抛出 IllegalArgumentException

4.4 腾讯 Act 映射 (TencentClient)

编号 百度 Act 预期腾讯 ActionType
AM-01 1 ACTIVATE_APP
AM-02 2 PURCHASE
AM-03 3 REGISTER
AM-04 4 ONE_DAY_RETENTION
AM-05 5 COMPLETE_ORDER
AM-06 6 START_APP
AM-07 7 CUSTOM (custom_action=key_behavior)
AM-08 8 CUSTOM (custom_action=7day_retention)
AM-09 9 CUSTOM (custom_action=3day_retention)
AM-10 99(未映射) 抛出 IllegalArgumentException

4.5 设备标识 MD5 处理 (TencentClient.ensureMd5)

编号 输入 预期
MD-01 已是32位十六进制 "abcdef1234" 原值(小写化)
MD-02 原始 IMEI "123456789012345" 计算 MD5
MD-03 null null
MD-04 空字符串 null
MD-05 大写32位十六进制 转小写返回

五、数据存储测试

5.1 Redis 热存储 (RedisHotStore)

编号 场景 操作 验证
RH-01 记录 Bid recordBid(record) 1. adx:tencent:bid:{qk} 存在 2. adx:tencent:media:tencent:{traceId} 存在 3. TTL=24h 4. Stream 有 type=bid 事件
RH-02 按 qk 查找 Bid findBidByQk(qk) 返回正确的 BidRecord
RH-03 按 mediaTrace 查找 Bid findBidByMediaTrace("tencent", traceId) 返回正确的 BidRecord
RH-04 查找不存在的 Bid findBidByQk("nonexist") 返回 null
RH-05 Bid TTL 过期 等待 TTL 后查询 返回 null
RH-06 记录 Tracking recordTracking(record) Stream 有 type=tracking 事件
RH-07 确保消费者组 ensureGroup("test-group") 不抛异常,幂等
RH-08 读取 Stream 消息 read(group, consumer, 10) 返回待消费的事件列表
RH-09 ACK 消息 ack(group, ids) 消息从 pending 列表移除
RH-10 Trim Stream trim(maxLen) Stream 长度不超过 maxLen
RH-11 ClaimStale 模拟超时未 ACK 消息 claimStale 能获取到超时消息

5.2 TiDB 冷存储 (TiDBColdStore)

编号 场景 操作 验证
TC-01 自动建表 migrate() 4张表创建成功
TC-02 保存 Bid(新记录) saveBid(record) ad_bid_events 表有记录
TC-03 保存 Bid(重复 qk) 两次 saveBid(sameQk) 更新而非报错(UPSERT)
TC-04 qk 为空抛异常 saveBid(qk=null) 抛出 IllegalArgumentException
TC-05 保存 Tracking saveTracking(record) tracking_reports 表有记录
TC-06 保存 Conversion(新记录) saveConversion(record) baidu_conversions 表有记录
TC-07 保存 Conversion(重复 dedupeKey) 两次相同 dedupeKey 更新而非报错
TC-08 保存 MediaCallback saveMediaCallback(record) media_callbacks 表有记录
TC-09 查询成功回调存在 successfulCallbackExists(key) 有 ok=1 记录返回 true
TC-10 查询成功回调不存在 successfulCallbackExists(key) 无 ok=1 记录返回 false
TC-11 查询待重试回调 pendingMediaCallbacks("tencent", 10) 返回 ok=0 且无对应 ok=1 的记录
TC-12 空 dedupeKey 不查询 successfulCallbackExists("") 返回 false

六、后台任务测试

6.1 ColdWorker(冷路径数据持久化)

编号 场景 验证
CW-01 正常消费 bid 事件 Stream 中 bid 事件被消费,TiDB ad_bid_events 有新记录
CW-02 正常消费 tracking 事件 Stream 中 tracking 事件被消费,TiDB tracking_reports 有新记录
CW-03 未知事件类型 Stream 中有 type=unknown 事件 → 抛异常,该批次不 ACK
CW-04 TiDB 写入失败 模拟 TiDB 不可用 → 抛异常,消息不 ACK(可重试)
CW-05 空 Stream 无消息时返回 0
CW-06 超时消息重新领取 模拟消费者崩溃 → 超过 pendingIdle 后被其他 worker claim
CW-07 Stream Trim 消费后 Stream 长度被裁剪

6.2 ConversionSyncRunner(转化同步)

编号 场景 验证
SR-01 正常同步(有匹配的 tencent bid) fetched > 0, matched > 0, sent > 0
SR-02 无匹配 bid fetched > 0, skipped > 0, matched=0
SR-03 bid 不属于 tencent 记录到 baidu_conversions 但 skipped
SR-04 幂等 - 已成功发送过 alreadySent > 0,不重复调用腾讯
SR-05 腾讯回传失败 failed > 0,media_callbacks 记录 ok=0
SR-06 分页拉取多页 正确遍历多页数据
SR-07 最大页数限制(100页) 超过100页时停止
SR-08 dateOffsetDays 配置 offset=0 查当天,offset=-1 查昨天

6.3 RetryService(回调重试)

编号 场景 验证
RS-01 正常重试成功 fetched > 0, sent > 0, media_callbacks 新增 ok=1 记录
RS-02 重试仍失败 failed > 0, attempt 递增, 新记录 ok=0
RS-03 无待重试记录 fetched=0, sent=0, failed=0
RS-04 使用默认 limit limit=0 时使用配置的默认值
RS-05 网络异常 failed > 0, errorMessage 记录异常信息

6.4 Leader Election(分布式选举)

编号 场景 验证
LE-01 单实例获取锁 成功获取锁并执行任务
LE-02 多实例竞选 仅一个实例获得锁,其他等待
LE-03 锁续约成功 任务执行期间锁不过期
LE-04 续约失败(锁丢失) jobStop.isStopped() = true,任务停止
LE-05 任务完成释放锁 任务结束后其他实例可获得锁
LE-06 外部停止信号 stopped=true 时退出循环
LE-07 skip-leader-election=true 跳过选举直接运行任务

七、异常处理测试

7.1 全局异常处理器 (GlobalExceptionHandler)

编号 异常类型 预期 HTTP 状态码 预期响应格式
EH-01 ResponseStatusException(400, "bad") 400 {"error":"bad"}
EH-02 ResponseStatusException(503, "unavailable") 503 {"error":"unavailable"}
EH-03 IllegalArgumentException("invalid param") 400 {"error":"invalid param"}
EH-04 IOException("timeout") 502 {"error":"timeout"}
EH-05 InterruptedException 502 {"error":"..."}
EH-06 RuntimeException("unexpected") 502 {"error":"unexpected"}
EH-07 NullPointerException(message=null) 502 {"error":"internal error"}

八、数据库表结构验证

8.1 ad_bid_events

字段 类型 约束 验证
qk VARCHAR(128) UNIQUE NOT NULL 唯一键,重复插入应更新
media VARCHAR(64) - 值为 "tencent"
media_trace_id VARCHAR(255) 索引 与 media 联合索引
req_id VARCHAR(512) - 竞价请求 ID
price BIGINT UNSIGNED - 竞价价格(分)
show_urls JSON - JSON 数组
click_urls JSON - JSON 数组
created_at TIMESTAMP(3) 索引 毫秒精度

8.2 tracking_reports

字段 类型 约束 验证
qk VARCHAR(128) 联合索引 关联 bid
kind VARCHAR(32) NOT NULL "impression" 或 "click"
ok TINYINT(1) NOT NULL 0 或 1
tracking_url TEXT NOT NULL 上报的 URL

8.3 baidu_conversions

字段 类型 约束 验证
dedupe_key VARCHAR(255) UNIQUE 幂等键,格式: {media}:{qk}:{date}:{deviceId}:{act}
act INT 联合索引 百度行为类型码
payment DOUBLE - 支付金额

8.4 media_callbacks

字段 类型 约束 验证
dedupe_key VARCHAR(255) 索引 回调去重键
ok TINYINT(1) NOT NULL 回传是否成功
attempt INT DEFAULT 1 重试次数递增
event_type INT NOT NULL 对应百度 act
error_message TEXT - 失败时记录错误信息

九、端到端集成测试

9.1 完整曝光 → 点击 → 转化回传流程

步骤 1: 发送曝光请求
  GET /tencent/impression?click_id=e2e_test_001&platform=android&oaid=test_oaid_123

  验证:
  ✓ 返回 204
  ✓ Redis 存在 bid 缓存
  ✓ Redis Stream 有 bid 事件

步骤 2: 发送点击请求
  GET /tencent/click?click_id=e2e_test_001&platform=android&oaid=test_oaid_123

  验证:
  ✓ 返回 302 重定向
  ✓ Redis Stream 有 click tracking 事件

步骤 3: 等待 ColdWorker 消费
  验证:
  ✓ ad_bid_events 表有记录
  ✓ tracking_reports 表有 impression 和 click 记录

步骤 4: 触发转化同步
  POST /admin/conversions/sync
  Body: {"date":"20260702","pageSize":1,"acts":[1]}

  验证:
  ✓ 百度转化数据被拉取
  ✓ 匹配到腾讯 bid 后回传成功
  ✓ baidu_conversions 表有记录
  ✓ media_callbacks 表有 ok=1 的记录

步骤 5: 幂等验证 - 再次同步
  POST /admin/conversions/sync (相同参数)

  验证:
  ✓ alreadySent > 0,不重复回传

9.2 回调失败 → 自动重试流程

步骤 1: 模拟腾讯回传失败
  构造一条 media_callbacks 记录 (ok=0, attempt=1)

步骤 2: 触发回调重试
  POST /admin/callbacks/retry
  Body: {"limit":10}

  验证:
  ✓ 获取到待重试记录
  ✓ 重试后生成新的 callback 记录
  ✓ attempt = 2

十、性能与压力测试

10.1 API 性能基线

接口 目标 QPS 目标 P99 延迟 备注
GET /health >10000 <10ms 无外部依赖
GET /tencent/impression >500 <200ms 含百度竞价+Redis 写入
GET /tencent/click >500 <200ms 含 Redis 读取+百度上报

10.2 后台任务性能

任务 指标 目标
ColdWorker 消费速率 >1000 events/s
ConversionSync 处理速率 >100 records/batch(并发=10)
RetryService 重试速率 >50 records/batch

10.3 Redis 压力

  • bid 缓存 key 数量: 验证 TTL 自动清理
  • Stream 长度: 验证 XTRIM 保持在配置的 maxLen 以内
  • 内存使用: 监控 Redis 内存占用稳定性

十一、安全测试

编号 场景 验证
SEC-01 百度签名校验 MD5 签名格式正确、不可伪造
SEC-02 Access Token 保护 生产环境 token 从环境变量注入,不硬编码
SEC-03 Redis 锁安全性 Lua 脚本保证仅持有者能释放/续约
SEC-04 SQL 注入防护 MyBatis 参数化查询,无 SQL 拼接
SEC-05 设备标识脱敏 腾讯回传使用 MD5 哈希而非明文

十二、配置测试

编号 场景 验证
CFG-01 dev 环境启动 skip-leader-election=true,sync/retry 禁用
CFG-02 test 环境启动 sync/retry 启用,短间隔
CFG-03 prod 环境启动 环境变量注入,leader election 启用
CFG-04 TiDB 未配置 ColdStore=null,相关功能降级但不崩溃
CFG-05 Redis 连接失败 启动失败,打印明确错误信息
CFG-06 百度 ADX endpoint 为空 竞价时抛 IllegalStateException
CFG-07 bid TTL 配置 默认 24h,可自定义

十三、测试执行方式

单元测试(推荐 Mock)

# 运行全部测试
mvn test -Ptest

# 运行指定类
mvn test -Dtest=AuctionPriceEncoderTest

# 运行指定方法
mvn test -Dtest=MediaPlacementTest#testResolvePlatformAndroid

集成测试(需要外部依赖)

# 需要 Redis + MySQL 环境
mvn verify -Ptest -Dspring.profiles.active=test

手动接口测试(cURL)

# 健康检查
curl -s http://localhost:8787/health

# 曝光监测
curl -s -o /dev/null -w "%{http_code}" \
  "http://localhost:8787/tencent/impression?click_id=test001&platform=android&oaid=test_oaid"

# 点击监测
curl -s -o /dev/null -w "%{http_code}" -L \
  "http://localhost:8787/tencent/click?click_id=test001&platform=android"

# 查询转化
curl -s -X POST http://localhost:8787/admin/conversions/query \
  -H "Content-Type: application/json" \
  -d '{"date":"20260702","pageSize":1,"acts":[1,2]}'

# 触发同步
curl -s -X POST http://localhost:8787/admin/conversions/sync \
  -H "Content-Type: application/json" \
  -d '{"date":"20260702","pageSize":1,"acts":[1]}'

# 触发重试
curl -s -X POST http://localhost:8787/admin/callbacks/retry \
  -H "Content-Type: application/json" \
  -d '{"limit":10}'

十四、测试覆盖率目标

模块 目标覆盖率 重点
httpapi (Controller) ≥80% 参数校验、异常处理
baidu (AdxClient, ConversionClient) ≥75% 签名、加密、HTTP 调用
tencent (TencentClient) ≥80% act 映射、设备标识处理、回传逻辑
conversionsync ≥85% 核心业务逻辑
storage (Redis/TiDB) ≥70% CRUD、幂等性
leader ≥60% 锁获取/释放/续约
worker ≥75% 事件消费、错误处理

十五、已知限制与注意事项

  1. 重试限制:RetryService 当前实现中,重试使用原始 callbackUrl 发送空 body,正式环境需验证腾讯 API 是否接受此方式
  2. 并发安全:ConversionSyncService 使用 10 并发线程处理支付记录,需确保 Redis/TiDB 连接池够用
  3. 时钟依赖:转化同步的 dateOffsetDays 依赖系统时钟,需确保服务器时间同步
  4. 百度 ADX 模拟:单元测试需 Mock 百度 ADX HTTP 响应(推荐 WireMock 或 MockWebServer)
  5. 数据清理:集成测试后需清理测试数据,避免影响后续测试
  6. Adm 字段拼写:百度原始字段 landdingPage(双 d),需注意 JSON 序列化/反序列化一致性