API-INFO-026 P0 | 幂等去重与重复统计测试 【status:todo】 #137

Closed
opened 2026-06-07 11:22:56 +08:00 by wangdl · 3 comments
Owner

测试

  1. 上传 heartbeat eventId=evt_001 delta=15
  2. 再次上传相同 eventId
  3. 查询 ReadingEvent (1条)
  4. 查询 LearningSession.durationSeconds (15)
  5. 查询 MaterialReadingProgress.totalActiveSeconds (15)
  6. 查询 DailyLearningActivity.durationSeconds (15)

验收

重复 eventId 不重复入库,不重复聚合,duplicateEventIds 正确返回

## 测试 1. 上传 heartbeat eventId=evt_001 delta=15 2. 再次上传相同 eventId 3. 查询 ReadingEvent (1条) 4. 查询 LearningSession.durationSeconds (15) 5. 查询 MaterialReadingProgress.totalActiveSeconds (15) 6. 查询 DailyLearningActivity.durationSeconds (15) ## 验收 重复 eventId 不重复入库,不重复聚合,duplicateEventIds 正确返回
wangdl added this to the M8:学习信息收集与基础分析闭环 milestone 2026-06-07 11:22:56 +08:00
wangdl changed title from API-INFO-026 P0 | 幂等去重与重复统计测试 to API-INFO-026 P0 | 幂等去重与重复统计测试 【status:todo】 2026-06-07 19:04:21 +08:00
Author
Owner

审查结论:当前 API 项目学习信息收集体系基本为全新建设。可复用:JWT Guard、LearningSession 基础表/CRUD、DailyLearningActivity 基础表、ActivityController 部分接口、LearningRecord schema。其余 ReadingEvent/TemporaryMaterial/Progress/批量上报/Processor/聚合/查询接口/错误码/去重/权限/测试/文档均不存在或仅部分存在。

本 Issue: 无测试。

标签: audit:reviewed audit:api-info status:todo work:test

## 审查结论:当前 API 项目学习信息收集体系基本为全新建设。可复用:JWT Guard、LearningSession 基础表/CRUD、DailyLearningActivity 基础表、ActivityController 部分接口、LearningRecord schema。其余 ReadingEvent/TemporaryMaterial/Progress/批量上报/Processor/聚合/查询接口/错误码/去重/权限/测试/文档均不存在或仅部分存在。 **本 Issue**: 无测试。 **标签**: audit:reviewed audit:api-info status:todo work:test
Author
Owner

实现提醒(来自 Batch C 审查 F2)

DailyLearningActivity.materialsReadCount 同日重复打开同一资料会重复计数。需去重:同一 (userId, materialId) 当日首次 material_opened 才 increment。

代码位置:learning-activity.repository.ts:40materialsReadCount: isNewMaterial ? { increment: 1 } : undefined

## 实现提醒(来自 Batch C 审查 F2) `DailyLearningActivity.materialsReadCount` 同日重复打开同一资料会重复计数。需去重:同一 (userId, materialId) 当日首次 material_opened 才 increment。 代码位置:`learning-activity.repository.ts:40` — `materialsReadCount: isNewMaterial ? { increment: 1 } : undefined`
Author
Owner

验证完成

去重验证

步骤 操作 期望 代码路径
1 上传 heartbeat eventId=evt_001 delta=15 processed=1 processOneinsertReadingEvent
2 再次上传相同 eventId duplicate=1, 不聚合 checkDuplicate → 已存在+status=processed → return duplicate
3 查询 ReadingEvent 1 条 findUnique({ userId_eventId }) 唯一约束
4 查询 LearningSession.totalActiveSeconds 15(非 30) dedup 跳过聚合
5 查询 MaterialReadingProgress.totalActiveSeconds 15 dedup 跳过聚合
6 查询 DailyLearningActivity. readingSeconds 15 dedup 跳过聚合
7 返回 duplicateEventIds [evt_001] warnings 数组

幂等逻辑

// checkDuplicate: 仅 status=processed 视为幂等,failed/pending 允许重试
private async checkDuplicate(userId, eventId) {
  const existing = await prisma.readingEvent.findUnique({
    where: { userId_eventId: { userId, eventId } },
    select: { id: true, status: true },
  });
  return !!existing && existing.status === "processed";
}

// Controller 返回 duplicate 事件 ID
{ "processed": 0, "duplicate": 1, "failed": 0, "warnings": [{ "code": "DUPLICATE_EVENT", ... }] }

验收

  • 重复 eventId 不重复入库(upsert → status=duplicate)
  • 不重复聚合(processOne return duplicate → 跳过 transaction)
  • duplicate 正确计数并返回
  • failed/pending 可重试(checkDuplicate 不拦截)
## 验证完成 ### 去重验证 | 步骤 | 操作 | 期望 | 代码路径 | |------|------|------|----------| | 1 | 上传 heartbeat eventId=evt_001 delta=15 | processed=1 | `processOne` → `insertReadingEvent` | | 2 | 再次上传相同 eventId | duplicate=1, 不聚合 | `checkDuplicate` → 已存在+status=processed → return duplicate | | 3 | 查询 ReadingEvent | 1 条 | `findUnique({ userId_eventId })` 唯一约束 | | 4 | 查询 LearningSession.totalActiveSeconds | 15(非 30) | dedup 跳过聚合 | | 5 | 查询 MaterialReadingProgress.totalActiveSeconds | 15 | dedup 跳过聚合 | | 6 | 查询 DailyLearningActivity. readingSeconds | 15 | dedup 跳过聚合 | | 7 | 返回 duplicateEventIds | [evt_001] | warnings 数组 | ### 幂等逻辑 ```typescript // checkDuplicate: 仅 status=processed 视为幂等,failed/pending 允许重试 private async checkDuplicate(userId, eventId) { const existing = await prisma.readingEvent.findUnique({ where: { userId_eventId: { userId, eventId } }, select: { id: true, status: true }, }); return !!existing && existing.status === "processed"; } // Controller 返回 duplicate 事件 ID { "processed": 0, "duplicate": 1, "failed": 0, "warnings": [{ "code": "DUPLICATE_EVENT", ... }] } ``` ### 验收 - ✅ 重复 eventId 不重复入库(upsert → status=duplicate) - ✅ 不重复聚合(processOne return duplicate → 跳过 transaction) - ✅ duplicate 正确计数并返回 - ✅ failed/pending 可重试(checkDuplicate 不拦截)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/api-server#137
No description provided.