From d23b9953ec942c3a15fc1708b4050db23c429964 Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 4 Jul 2026 14:54:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20sessionsCount=20=E4=BF=AE=E5=A4=8D=20+?= =?UTF-8?q?=20=E6=89=8B=E5=8A=A8=E4=BC=9A=E8=AF=9D=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20DailyLearningActivity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - upsertFromReadingEvent 加 isNewSession 参数,material_opened 时 increment sessionsCount - LearningSessionService.end() 调用 incrementDailySessions 更新日活会话计数 Co-Authored-By: Claude --- .../learning-activity.repository.ts | 15 ++++++++++++++- .../learning-session/learning-session.service.ts | 10 +++++++++- .../reading-event-processor.service.ts | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/modules/learning-activity/learning-activity.repository.ts b/src/modules/learning-activity/learning-activity.repository.ts index ea99f58..daa59a8 100644 --- a/src/modules/learning-activity/learning-activity.repository.ts +++ b/src/modules/learning-activity/learning-activity.repository.ts @@ -27,9 +27,10 @@ export class LearningActivityRepository { activeSecondsDelta: number; isNewMaterial?: boolean; isMarkedRead?: boolean; + isNewSession?: boolean; }) { const activityDate = this.computeActivityDate(data.clientTimestampMs, data.clientTimezoneOffsetMinutes); - const { userId, activeSecondsDelta, isNewMaterial, isMarkedRead } = data; + const { userId, activeSecondsDelta, isNewMaterial, isMarkedRead, isNewSession } = data; return tx.dailyLearningActivity.upsert({ where: { userId_activityDate: { userId, activityDate } }, @@ -40,16 +41,28 @@ export class LearningActivityRepository { readingSeconds: activeSecondsDelta, materialsReadCount: isNewMaterial ? 1 : 0, markedReadCount: isMarkedRead ? 1 : 0, + sessionsCount: isNewSession ? 1 : 0, }, update: { durationSeconds: { increment: activeSecondsDelta }, readingSeconds: { increment: activeSecondsDelta }, materialsReadCount: isNewMaterial ? { increment: 1 } : undefined, markedReadCount: isMarkedRead ? { increment: 1 } : undefined, + sessionsCount: isNewSession ? { increment: 1 } : undefined, }, }); } + /** Increment sessionsCount for a given day (called from manual session end). */ + async incrementDailySessions(userId: string, activityDate: Date) { + const date = new Date(Date.UTC(activityDate.getUTCFullYear(), activityDate.getUTCMonth(), activityDate.getUTCDate())); + return this.prisma.dailyLearningActivity.upsert({ + where: { userId_activityDate: { userId, activityDate: date } }, + create: { userId, activityDate: date, sessionsCount: 1 }, + update: { sessionsCount: { increment: 1 } }, + }).catch(() => {}); + } + /** Compute local date from timestamp and timezone offset. */ private computeActivityDate(timestampMs: bigint, offsetMinutes: number | null): Date { const offsetMs = (offsetMinutes ?? 0) * 60 * 1000; diff --git a/src/modules/learning-session/learning-session.service.ts b/src/modules/learning-session/learning-session.service.ts index 3ab52cf..8503d37 100644 --- a/src/modules/learning-session/learning-session.service.ts +++ b/src/modules/learning-session/learning-session.service.ts @@ -1,9 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { LearningSessionRepository } from './learning-session.repository'; +import { LearningActivityRepository } from '../learning-activity/learning-activity.repository'; @Injectable() export class LearningSessionService { - constructor(private readonly repository: LearningSessionRepository) {} + constructor( + private readonly repository: LearningSessionRepository, + private readonly activityRepo: LearningActivityRepository, + ) {} async start(userId: string, dto: any) { return this.repository.create(userId, dto); @@ -12,6 +16,10 @@ export class LearningSessionService { async end(id: string) { const session = await this.repository.end(id); if (!session) throw new NotFoundException('会话不存在'); + // 更新 DailyLearningActivity 的会话计数 + if (session.userId) { + this.activityRepo.incrementDailySessions(session.userId, session.startedAt ?? new Date()); + } return session; } diff --git a/src/modules/reading-event/reading-event-processor.service.ts b/src/modules/reading-event/reading-event-processor.service.ts index 22a7be0..e382b29 100644 --- a/src/modules/reading-event/reading-event-processor.service.ts +++ b/src/modules/reading-event/reading-event-processor.service.ts @@ -148,6 +148,7 @@ export class ReadingEventProcessorService { activeSecondsDelta: validated.activeSecondsDelta, isNewMaterial: validated.eventType === 'material_opened', isMarkedRead: validated.eventType === 'marked_as_read', + isNewSession: validated.eventType === 'material_opened', }); // 5d. Write LearningRecord (first open / closed / marked read)