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,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Console;
|
||||
|
||||
use App\Scolarite\Application\Service\RecalculerMoyennesService;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use App\Shared\Infrastructure\Tenant\TenantDatabaseSwitcher;
|
||||
use App\Shared\Infrastructure\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Tenant\TenantRegistry;
|
||||
|
||||
use function count;
|
||||
|
||||
use Override;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Recalcule toutes les projections de moyennes à partir des données existantes.
|
||||
*
|
||||
* Usage:
|
||||
* php bin/console app:recalculer-moyennes # Tous les tenants
|
||||
* php bin/console app:recalculer-moyennes --tenant=UUID # Un seul tenant
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'app:recalculer-moyennes',
|
||||
description: 'Recalcule les statistiques évaluations et moyennes élèves depuis les notes publiées',
|
||||
)]
|
||||
final class RecalculerToutesMoyennesCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EvaluationRepository $evaluationRepository,
|
||||
private readonly TenantRegistry $tenantRegistry,
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly TenantDatabaseSwitcher $databaseSwitcher,
|
||||
private readonly RecalculerMoyennesService $service,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Limiter à un tenant spécifique (UUID)');
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Recalcul des moyennes et statistiques');
|
||||
|
||||
/** @var string|null $tenantOption */
|
||||
$tenantOption = $input->getOption('tenant');
|
||||
|
||||
if ($tenantOption !== null) {
|
||||
$configs = [$this->tenantRegistry->getConfig(TenantId::fromString($tenantOption))];
|
||||
} else {
|
||||
$configs = $this->tenantRegistry->getAllConfigs();
|
||||
}
|
||||
|
||||
$totalEvals = 0;
|
||||
$totalErrors = 0;
|
||||
|
||||
foreach ($configs as $config) {
|
||||
[$processed, $errors] = $this->processTenant($config, $io);
|
||||
$totalEvals += $processed;
|
||||
$totalErrors += $errors;
|
||||
}
|
||||
|
||||
if ($totalErrors > 0) {
|
||||
$io->warning(sprintf(
|
||||
'%d évaluation(s) traitée(s), %d erreur(s).',
|
||||
$totalEvals,
|
||||
$totalErrors,
|
||||
));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->success(sprintf('%d évaluation(s) traitée(s) avec succès.', $totalEvals));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{int, int} [processed, errors]
|
||||
*/
|
||||
private function processTenant(TenantConfig $config, SymfonyStyle $io): array
|
||||
{
|
||||
$this->tenantContext->setCurrentTenant($config);
|
||||
$this->databaseSwitcher->useTenantDatabase($config->databaseUrl);
|
||||
|
||||
$tenantId = \App\Shared\Domain\Tenant\TenantId::fromString((string) $config->tenantId);
|
||||
|
||||
$evaluations = $this->evaluationRepository->findAllWithPublishedGrades($tenantId);
|
||||
|
||||
if ($evaluations === []) {
|
||||
$io->text(sprintf(' Tenant %s : aucune évaluation publiée.', $config->subdomain));
|
||||
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
$io->text(sprintf(' Tenant %s : %d évaluation(s) publiée(s)', $config->subdomain, count($evaluations)));
|
||||
|
||||
$processed = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($evaluations as $evaluation) {
|
||||
try {
|
||||
$this->service->recalculerStatistiquesEvaluation($evaluation->id, $tenantId);
|
||||
$this->service->recalculerTousElevesPourEvaluation($evaluation->id, $tenantId);
|
||||
++$processed;
|
||||
} catch (Throwable $e) {
|
||||
$io->error(sprintf(' Erreur évaluation %s : %s', $evaluation->id, $e->getMessage()));
|
||||
++$errors;
|
||||
}
|
||||
}
|
||||
|
||||
$io->text(sprintf(' → %d traitée(s), %d erreur(s)', $processed, $errors));
|
||||
|
||||
return [$processed, $errors];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user