New_Micro_Learning/backend/app/Modules/Analytics/Application/AnalyticsProjectionService.php

79 خطوط
3.7 KiB
PHP

<?php
namespace App\Modules\Analytics\Application;
use App\Modules\Learner\Domain\LearningEvent;
use Illuminate\Support\Facades\DB;
final class AnalyticsProjectionService
{
private const PROCESSOR = 'daily-metrics';
private const VERSION = 1;
public function process(LearningEvent $event): bool
{
return DB::transaction(function () use ($event): bool {
$claimed = DB::table('analytics_event_projections')->insertOrIgnore([
'id' => (string) str()->ulid(),
'organization_id' => $event->organization_id,
'learning_event_id' => $event->getKey(),
'processor' => self::PROCESSOR,
'processor_version' => self::VERSION,
'processed_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
if ($claimed === 0) {
return false;
}
$this->refreshDate($event->organization_id, $event->occurred_at->toDateString());
return true;
});
}
public function rebuild(string $organizationId): int
{
DB::table('analytics_event_projections')->where('organization_id', $organizationId)->delete();
DB::table('analytics_daily_metrics')->where('organization_id', $organizationId)->delete();
$count = 0;
LearningEvent::query()->where('organization_id', $organizationId)->orderBy('occurred_at')->orderBy('id')->each(function (LearningEvent $event) use (&$count): void {
if ($this->process($event)) {
$count++;
}
});
return $count;
}
private function refreshDate(string $organizationId, string $date): void
{
$events = LearningEvent::query()->where('organization_id', $organizationId)->whereDate('occurred_at', $date)->get();
$durations = $events->sum(fn (LearningEvent $event) => max(0, min(21600, (int) ($event->payload['durationSeconds'] ?? 0))));
$assessmentScores = $events->where('event_type', 'assessment.completed')->map(fn (LearningEvent $event) => $event->payload['score'] ?? null)->filter(fn ($score) => is_numeric($score));
$metrics = [
'events' => $events->count(),
'activeLearners' => $events->pluck('learner_id')->unique()->count(),
'sessions' => $events->pluck('session_id')->filter()->unique()->count(),
'learningMinutes' => round($durations / 60, 1),
'coursesOpened' => $events->where('event_type', 'course.opened')->count(),
'coursesCompleted' => $events->where('event_type', 'course.completed')->count(),
'lessonsStarted' => $events->where('event_type', 'lesson.started')->count(),
'lessonsCompleted' => $events->where('event_type', 'lesson.completed')->count(),
'blocksViewed' => $events->where('event_type', 'block.viewed')->count(),
'blocksInteracted' => $events->whereIn('event_type', ['block.interacted', 'block.completed'])->count(),
'videoStarted' => $events->where('event_type', 'video.started')->count(),
'videoCompleted' => $events->where('event_type', 'video.completed')->count(),
'assessmentsCompleted' => $assessmentScores->count(),
'assessmentAverage' => $assessmentScores->isEmpty() ? null : round((float) $assessmentScores->average(), 1),
];
DB::table('analytics_daily_metrics')->updateOrInsert(
['organization_id' => $organizationId, 'metric_date' => $date, 'scope_type' => 'organization', 'scope_id' => $organizationId],
['id' => (string) str()->ulid(), 'metrics' => json_encode($metrics, JSON_THROW_ON_ERROR), 'calculated_at' => now(), 'created_at' => now(), 'updated_at' => now()],
);
}
}