feat: Calculer automatiquement les moyennes après chaque saisie de notes
Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Repository\StudentAverageRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineStudentAverageRepository implements StudentAverageRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function saveSubjectAverage(
|
||||
TenantId $tenantId,
|
||||
UserId $studentId,
|
||||
SubjectId $subjectId,
|
||||
string $periodId,
|
||||
float $average,
|
||||
int $gradeCount,
|
||||
): void {
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO student_averages (id, tenant_id, student_id, subject_id, period_id, average, grade_count, updated_at)
|
||||
VALUES (gen_random_uuid(), :tenant_id, :student_id, :subject_id, :period_id, :average, :grade_count, NOW())
|
||||
ON CONFLICT (student_id, subject_id, period_id) DO UPDATE SET
|
||||
average = EXCLUDED.average,
|
||||
grade_count = EXCLUDED.grade_count,
|
||||
updated_at = NOW()',
|
||||
[
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'student_id' => (string) $studentId,
|
||||
'subject_id' => (string) $subjectId,
|
||||
'period_id' => $periodId,
|
||||
'average' => $average,
|
||||
'grade_count' => $gradeCount,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function saveGeneralAverage(
|
||||
TenantId $tenantId,
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
float $average,
|
||||
): void {
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO student_general_averages (id, tenant_id, student_id, period_id, average, updated_at)
|
||||
VALUES (gen_random_uuid(), :tenant_id, :student_id, :period_id, :average, NOW())
|
||||
ON CONFLICT (student_id, period_id) DO UPDATE SET
|
||||
average = EXCLUDED.average,
|
||||
updated_at = NOW()',
|
||||
[
|
||||
'tenant_id' => (string) $tenantId,
|
||||
'student_id' => (string) $studentId,
|
||||
'period_id' => $periodId,
|
||||
'average' => $average,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findSubjectAveragesForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT average FROM student_averages
|
||||
WHERE student_id = :student_id
|
||||
AND period_id = :period_id
|
||||
AND tenant_id = :tenant_id',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'period_id' => $periodId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map(
|
||||
/** @param array<string, mixed> $row */
|
||||
static function (array $row): float {
|
||||
/** @var string|float $avg */
|
||||
$avg = $row['average'];
|
||||
|
||||
return (float) $avg;
|
||||
},
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findDetailedSubjectAveragesForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): array {
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT sa.subject_id, sa.average, sa.grade_count, s.name as subject_name
|
||||
FROM student_averages sa
|
||||
LEFT JOIN subjects s ON s.id = sa.subject_id
|
||||
WHERE sa.student_id = :student_id
|
||||
AND sa.period_id = :period_id
|
||||
AND sa.tenant_id = :tenant_id
|
||||
ORDER BY s.name ASC',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'period_id' => $periodId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
return array_map(
|
||||
/** @param array<string, mixed> $row */
|
||||
static function (array $row): array {
|
||||
/** @var string $subjectId */
|
||||
$subjectId = $row['subject_id'];
|
||||
/** @var string|null $subjectName */
|
||||
$subjectName = $row['subject_name'];
|
||||
/** @var string|float $average */
|
||||
$average = $row['average'];
|
||||
/** @var string|int $gradeCount */
|
||||
$gradeCount = $row['grade_count'];
|
||||
|
||||
return [
|
||||
'subjectId' => $subjectId,
|
||||
'subjectName' => $subjectName,
|
||||
'average' => (float) $average,
|
||||
'gradeCount' => (int) $gradeCount,
|
||||
];
|
||||
},
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findGeneralAverageForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): ?float {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT average FROM student_general_averages
|
||||
WHERE student_id = :student_id
|
||||
AND period_id = :period_id
|
||||
AND tenant_id = :tenant_id',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'period_id' => $periodId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var string|float $average */
|
||||
$average = $row['average'];
|
||||
|
||||
return (float) $average;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function deleteSubjectAverage(
|
||||
UserId $studentId,
|
||||
SubjectId $subjectId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): void {
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM student_averages
|
||||
WHERE student_id = :student_id
|
||||
AND subject_id = :subject_id
|
||||
AND period_id = :period_id
|
||||
AND tenant_id = :tenant_id',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'subject_id' => (string) $subjectId,
|
||||
'period_id' => $periodId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function deleteGeneralAverage(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): void {
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM student_general_averages
|
||||
WHERE student_id = :student_id
|
||||
AND period_id = :period_id
|
||||
AND tenant_id = :tenant_id',
|
||||
[
|
||||
'student_id' => (string) $studentId,
|
||||
'period_id' => $periodId,
|
||||
'tenant_id' => (string) $tenantId,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user