79 خطوط
3.7 KiB
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()],
|
|
);
|
|
}
|
|
}
|