API-INFO-012 P0 | 聚合 ReadingEvent 到 LearningSession 【status:todo】 #111

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

目标

按 userId+clientSessionId 聚合阅读事件到 LearningSession。

规则

  • MaterialOpened → 创建 session, mode=reading, status=active
  • Heartbeat → durationSeconds += countedActiveSecondsDelta
  • MaterialClosed → durationSeconds += delta, endedAt, status=completed
  • PositionChanged → 不增加时长
  • 同一 clientSessionId 不重复创建 session
  • 不重复增加 duration

详见设计文档 API-INFO-000。

## 目标 按 userId+clientSessionId 聚合阅读事件到 LearningSession。 ### 规则 - MaterialOpened → 创建 session, mode=reading, status=active - Heartbeat → durationSeconds += countedActiveSecondsDelta - MaterialClosed → durationSeconds += delta, endedAt, status=completed - PositionChanged → 不增加时长 - 同一 clientSessionId 不重复创建 session - 不重复增加 duration 详见设计文档 API-INFO-000。
wangdl added this to the M8:学习信息收集与基础分析闭环 milestone 2026-06-07 11:03:39 +08:00
wangdl changed title from API-INFO-006 P0 | 聚合 ReadingEvent 到 LearningSession to API-INFO-012 P0 | 聚合 ReadingEvent 到 LearningSession 2026-06-07 11:22:16 +08:00
wangdl changed title from API-INFO-012 P0 | 聚合 ReadingEvent 到 LearningSession to API-INFO-012 P0 | 聚合 ReadingEvent 到 LearningSession 【status:todo】 2026-06-07 19:04:13 +08:00
Author
Owner

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

本 Issue: 聚合逻辑不存在。依赖 processor。blocked-by:processor

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

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

实现提醒(来自 Batch B 审查 F6)

ReadingEventProcessorService.processOne()insertReadingEventaggregateEvent 不在同一事务中。

如果聚合失败,事件已写入 status=processing 且不会回滚。实现聚合时需将 Step 4+5+6 包装在数据库事务中:

await this.prisma.$transaction(async (tx) => {
  // 4. insert ReadingEvent
  // 5. aggregate → LearningSession / MaterialReadingProgress / DailyLearningActivity
  // 6. mark processed
});

代码位置:reading-event-processor.service.ts:52-76 (processOne)

## 实现提醒(来自 Batch B 审查 F6) `ReadingEventProcessorService.processOne()` 中 `insertReadingEvent` 和 `aggregateEvent` 不在同一事务中。 如果聚合失败,事件已写入 status=processing 且不会回滚。实现聚合时需将 Step 4+5+6 包装在数据库事务中: ```typescript await this.prisma.$transaction(async (tx) => { // 4. insert ReadingEvent // 5. aggregate → LearningSession / MaterialReadingProgress / DailyLearningActivity // 6. mark processed }); ``` 代码位置:`reading-event-processor.service.ts:52-76 (processOne)`
Author
Owner

完成报告

交付

1. LearningSessionRepository.upsertFromReadingEvent(tx, data) — 聚合核心方法:

// MaterialOpened → 新建 session (mode=reading, status=active)
// 后续事件 → 更新已有 session (按 clientSessionId 查)
// Heartbeat → totalActiveSeconds += delta
// PositionChanged → 更新 lastPosition(不增加时长)
// MaterialClosed → status=completed, endedAt=now
// 同一 clientSessionId 不重复创建

2. ReadingEventProcessorService.processOne() — Step 4+5+6 包装在事务中(F6 修复):

await this.prisma.$transaction(async (tx) => {
  // 4. insert ReadingEvent
  await this.insertReadingEvent(tx, userId, validated, knowledgeBaseId, warnings);
  // 5. aggregate → LearningSession
  await this.sessionRepo.upsertFromReadingEvent(tx, { ... });
  // 6. mark processed
  await tx.readingEvent.update({ ... status: "processed" ... });
});

3. Module wiring

  • LearningSessionModule 导出 LearningSessionRepository
  • ReadingEventModule 导入 LearningSessionModule

聚合规则

eventType 行为
material_opened 新建 session(不重复创建)
heartbeat totalActiveSeconds += delta
position_changed 更新 lastPosition
material_closed totalActiveSeconds += delta, status=completed
marked_as_read 更新 lastPosition
## 完成报告 ### 交付 **1. `LearningSessionRepository.upsertFromReadingEvent(tx, data)`** — 聚合核心方法: ```typescript // MaterialOpened → 新建 session (mode=reading, status=active) // 后续事件 → 更新已有 session (按 clientSessionId 查) // Heartbeat → totalActiveSeconds += delta // PositionChanged → 更新 lastPosition(不增加时长) // MaterialClosed → status=completed, endedAt=now // 同一 clientSessionId 不重复创建 ``` **2. `ReadingEventProcessorService.processOne()`** — Step 4+5+6 包装在事务中(F6 修复): ```typescript await this.prisma.$transaction(async (tx) => { // 4. insert ReadingEvent await this.insertReadingEvent(tx, userId, validated, knowledgeBaseId, warnings); // 5. aggregate → LearningSession await this.sessionRepo.upsertFromReadingEvent(tx, { ... }); // 6. mark processed await tx.readingEvent.update({ ... status: "processed" ... }); }); ``` **3. Module wiring**: - `LearningSessionModule` 导出 `LearningSessionRepository` - `ReadingEventModule` 导入 `LearningSessionModule` ### 聚合规则 | eventType | 行为 | |-----------|------| | material_opened | 新建 session(不重复创建) | | heartbeat | totalActiveSeconds += delta | | position_changed | 更新 lastPosition | | material_closed | totalActiveSeconds += delta, status=completed | | marked_as_read | 更新 lastPosition |
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#111
No description provided.