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)