fix: sessionsCount 修复 + 手动会话结束更新 DailyLearningActivity

- upsertFromReadingEvent 加 isNewSession 参数,material_opened 时 increment sessionsCount
- LearningSessionService.end() 调用 incrementDailySessions 更新日活会话计数

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-07-04 14:54:04 +08:00
parent c97b58fc5f
commit d23b9953ec
3 changed files with 24 additions and 2 deletions

View File

@ -27,9 +27,10 @@ export class LearningActivityRepository {
activeSecondsDelta: number; activeSecondsDelta: number;
isNewMaterial?: boolean; isNewMaterial?: boolean;
isMarkedRead?: boolean; isMarkedRead?: boolean;
isNewSession?: boolean;
}) { }) {
const activityDate = this.computeActivityDate(data.clientTimestampMs, data.clientTimezoneOffsetMinutes); 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({ return tx.dailyLearningActivity.upsert({
where: { userId_activityDate: { userId, activityDate } }, where: { userId_activityDate: { userId, activityDate } },
@ -40,16 +41,28 @@ export class LearningActivityRepository {
readingSeconds: activeSecondsDelta, readingSeconds: activeSecondsDelta,
materialsReadCount: isNewMaterial ? 1 : 0, materialsReadCount: isNewMaterial ? 1 : 0,
markedReadCount: isMarkedRead ? 1 : 0, markedReadCount: isMarkedRead ? 1 : 0,
sessionsCount: isNewSession ? 1 : 0,
}, },
update: { update: {
durationSeconds: { increment: activeSecondsDelta }, durationSeconds: { increment: activeSecondsDelta },
readingSeconds: { increment: activeSecondsDelta }, readingSeconds: { increment: activeSecondsDelta },
materialsReadCount: isNewMaterial ? { increment: 1 } : undefined, materialsReadCount: isNewMaterial ? { increment: 1 } : undefined,
markedReadCount: isMarkedRead ? { 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. */ /** Compute local date from timestamp and timezone offset. */
private computeActivityDate(timestampMs: bigint, offsetMinutes: number | null): Date { private computeActivityDate(timestampMs: bigint, offsetMinutes: number | null): Date {
const offsetMs = (offsetMinutes ?? 0) * 60 * 1000; const offsetMs = (offsetMinutes ?? 0) * 60 * 1000;

View File

@ -1,9 +1,13 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { LearningSessionRepository } from './learning-session.repository'; import { LearningSessionRepository } from './learning-session.repository';
import { LearningActivityRepository } from '../learning-activity/learning-activity.repository';
@Injectable() @Injectable()
export class LearningSessionService { export class LearningSessionService {
constructor(private readonly repository: LearningSessionRepository) {} constructor(
private readonly repository: LearningSessionRepository,
private readonly activityRepo: LearningActivityRepository,
) {}
async start(userId: string, dto: any) { async start(userId: string, dto: any) {
return this.repository.create(userId, dto); return this.repository.create(userId, dto);
@ -12,6 +16,10 @@ export class LearningSessionService {
async end(id: string) { async end(id: string) {
const session = await this.repository.end(id); const session = await this.repository.end(id);
if (!session) throw new NotFoundException('会话不存在'); if (!session) throw new NotFoundException('会话不存在');
// 更新 DailyLearningActivity 的会话计数
if (session.userId) {
this.activityRepo.incrementDailySessions(session.userId, session.startedAt ?? new Date());
}
return session; return session;
} }

View File

@ -148,6 +148,7 @@ export class ReadingEventProcessorService {
activeSecondsDelta: validated.activeSecondsDelta, activeSecondsDelta: validated.activeSecondsDelta,
isNewMaterial: validated.eventType === 'material_opened', isNewMaterial: validated.eventType === 'material_opened',
isMarkedRead: validated.eventType === 'marked_as_read', isMarkedRead: validated.eventType === 'marked_as_read',
isNewSession: validated.eventType === 'material_opened',
}); });
// 5d. Write LearningRecord (first open / closed / marked read) // 5d. Write LearningRecord (first open / closed / marked read)