feat: Release v3.1.6 - Amélioration complète des flux de passages
- Optimisation des listes de passages (user/admin) - Amélioration du flux de création avec validation temps réel - Amélioration du flux de consultation avec export multi-formats - Amélioration du flux de modification avec suivi des changements - Ajout de la génération PDF pour les reçus - Migration de la structure des uploads - Implémentation de la file d'attente d'emails - Ajout des permissions de suppression de passages - Corrections de bugs et optimisations performances 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
317
api/scripts/cron/process_email_queue.php
Executable file
317
api/scripts/cron/process_email_queue.php
Executable file
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script CRON pour traiter la queue d'emails
|
||||
* Envoie les emails en attente dans la table email_queue
|
||||
*
|
||||
* À exécuter toutes les 5 minutes via crontab :
|
||||
* Exemple: [asterisk]/5 [asterisk] [asterisk] [asterisk] [asterisk] /usr/bin/php /path/to/api/scripts/cron/process_email_queue.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Configuration
|
||||
define('MAX_ATTEMPTS', 3);
|
||||
define('BATCH_SIZE', 50);
|
||||
define('LOCK_FILE', '/tmp/process_email_queue.lock');
|
||||
|
||||
// Empêcher l'exécution multiple simultanée
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
$lockTime = filemtime(LOCK_FILE);
|
||||
// Si le lock a plus de 30 minutes, on le supprime (processus probablement bloqué)
|
||||
if (time() - $lockTime > 1800) {
|
||||
unlink(LOCK_FILE);
|
||||
} else {
|
||||
die("Le processus est déjà en cours d'exécution\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Créer le fichier de lock
|
||||
file_put_contents(LOCK_FILE, getmypid());
|
||||
|
||||
// Enregistrer un handler pour supprimer le lock en cas d'arrêt
|
||||
register_shutdown_function(function() {
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
// Détecter l'environnement basé sur le hostname ou un paramètre
|
||||
$hostname = gethostname();
|
||||
if (strpos($hostname, 'prod') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'app.geosector.fr';
|
||||
} elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rapp') !== false) {
|
||||
$_SERVER['SERVER_NAME'] = 'rapp.geosector.fr';
|
||||
} else {
|
||||
$_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut
|
||||
}
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chargement de l'environnement
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
require_once __DIR__ . '/../../src/Services/LogService.php';
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
try {
|
||||
// Initialisation de la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
LogService::log('Démarrage du processeur de queue d\'emails', [
|
||||
'level' => 'info',
|
||||
'script' => 'process_email_queue.php'
|
||||
]);
|
||||
|
||||
// Récupérer les emails en attente
|
||||
$stmt = $db->prepare('
|
||||
SELECT id, fk_pass, to_email, subject, body, headers, attempts
|
||||
FROM email_queue
|
||||
WHERE status = ? AND attempts < ?
|
||||
ORDER BY created_at ASC
|
||||
LIMIT ?
|
||||
');
|
||||
|
||||
$stmt->execute(['pending', MAX_ATTEMPTS, BATCH_SIZE]);
|
||||
$emails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($emails)) {
|
||||
LogService::log('Aucun email en attente dans la queue', [
|
||||
'level' => 'debug'
|
||||
]);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
LogService::log('Emails à traiter', [
|
||||
'level' => 'info',
|
||||
'count' => count($emails)
|
||||
]);
|
||||
|
||||
// Configuration SMTP
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
|
||||
// Traiter chaque email
|
||||
foreach ($emails as $emailData) {
|
||||
$emailId = $emailData['id'];
|
||||
$passageId = $emailData['fk_pass'];
|
||||
|
||||
try {
|
||||
// Incrémenter le compteur de tentatives
|
||||
$stmt = $db->prepare('UPDATE email_queue SET attempts = attempts + 1 WHERE id = ?');
|
||||
$stmt->execute([$emailId]);
|
||||
|
||||
// Créer l'instance PHPMailer
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// Configuration du serveur SMTP
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $smtpConfig['host'];
|
||||
$mail->SMTPAuth = $smtpConfig['auth'] ?? true;
|
||||
$mail->Username = $smtpConfig['user'];
|
||||
$mail->Password = $smtpConfig['pass'];
|
||||
$mail->SMTPSecure = $smtpConfig['secure'];
|
||||
$mail->Port = $smtpConfig['port'];
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
// Configuration de l'expéditeur
|
||||
$fromName = 'Amicale Sapeurs-Pompiers'; // Nom par défaut
|
||||
$mail->setFrom($emailConfig['from'], $fromName);
|
||||
|
||||
// Destinataire
|
||||
$mail->addAddress($emailData['to_email']);
|
||||
|
||||
// Sujet
|
||||
$mail->Subject = $emailData['subject'];
|
||||
|
||||
// Headers personnalisés si présents
|
||||
if (!empty($emailData['headers'])) {
|
||||
// Les headers contiennent déjà les informations MIME pour la pièce jointe
|
||||
// On doit extraire le boundary et reconstruire le message
|
||||
if (preg_match('/boundary="([^"]+)"/', $emailData['headers'], $matches)) {
|
||||
$boundary = $matches[1];
|
||||
|
||||
// Le body contient déjà le message complet avec pièce jointe
|
||||
$mail->isHTML(false);
|
||||
$mail->Body = $emailData['body'];
|
||||
|
||||
// Extraire le contenu HTML et la pièce jointe
|
||||
$parts = explode("--$boundary", $emailData['body']);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (strpos($part, 'Content-Type: text/html') !== false) {
|
||||
// Extraire le contenu HTML
|
||||
$htmlContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $htmlContent;
|
||||
} elseif (strpos($part, 'Content-Type: application/pdf') !== false) {
|
||||
// Extraire le PDF encodé en base64
|
||||
if (preg_match('/filename="([^"]+)"/', $part, $fileMatches)) {
|
||||
$filename = $fileMatches[1];
|
||||
$pdfContent = trim(substr($part, strpos($part, "\r\n\r\n") + 4));
|
||||
// Supprimer les retours à la ligne du base64
|
||||
$pdfContent = str_replace(["\r", "\n"], '', $pdfContent);
|
||||
|
||||
// Ajouter la pièce jointe
|
||||
$mail->addStringAttachment(
|
||||
base64_decode($pdfContent),
|
||||
$filename,
|
||||
'base64',
|
||||
'application/pdf'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Email simple sans pièce jointe
|
||||
$mail->isHTML(true);
|
||||
$mail->Body = $emailData['body'];
|
||||
}
|
||||
|
||||
// Ajouter une copie si configuré
|
||||
if (!empty($emailConfig['contact'])) {
|
||||
$mail->addBCC($emailConfig['contact']);
|
||||
}
|
||||
|
||||
// Envoyer l'email
|
||||
if ($mail->send()) {
|
||||
// Marquer comme envoyé
|
||||
$stmt = $db->prepare('
|
||||
UPDATE email_queue
|
||||
SET status = ?, sent_at = NOW()
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute(['sent', $emailId]);
|
||||
|
||||
// Mettre à jour le passage si nécessaire
|
||||
if ($passageId > 0) {
|
||||
$stmt = $db->prepare('
|
||||
UPDATE ope_pass
|
||||
SET date_sent_recu = NOW(), chk_email_sent = 1
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$passageId]);
|
||||
}
|
||||
|
||||
$successCount++;
|
||||
|
||||
LogService::log('Email envoyé avec succès', [
|
||||
'level' => 'info',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId,
|
||||
'to' => $emailData['to_email']
|
||||
]);
|
||||
} else {
|
||||
throw new Exception('Échec de l\'envoi');
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$failureCount++;
|
||||
|
||||
LogService::log('Erreur lors de l\'envoi de l\'email', [
|
||||
'level' => 'error',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId,
|
||||
'error' => $e->getMessage(),
|
||||
'attempts' => $emailData['attempts'] + 1
|
||||
]);
|
||||
|
||||
// Si on a atteint le nombre max de tentatives, marquer comme échoué
|
||||
if ($emailData['attempts'] + 1 >= MAX_ATTEMPTS) {
|
||||
$stmt = $db->prepare('
|
||||
UPDATE email_queue
|
||||
SET status = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute(['failed', $e->getMessage(), $emailId]);
|
||||
|
||||
LogService::log('Email marqué comme échoué après ' . MAX_ATTEMPTS . ' tentatives', [
|
||||
'level' => 'warning',
|
||||
'emailId' => $emailId,
|
||||
'passageId' => $passageId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Pause courte entre chaque email pour éviter la surcharge
|
||||
usleep(500000); // 0.5 seconde
|
||||
}
|
||||
|
||||
LogService::log('Traitement de la queue terminé', [
|
||||
'level' => 'info',
|
||||
'success' => $successCount,
|
||||
'failures' => $failureCount,
|
||||
'total' => count($emails)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur fatale dans le processeur de queue', [
|
||||
'level' => 'critical',
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
// Supprimer le lock en cas d'erreur
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Nettoyer les vieux emails traités (optionnel)
|
||||
try {
|
||||
// Supprimer les emails envoyés de plus de 30 jours
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM email_queue
|
||||
WHERE status = ? AND sent_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
');
|
||||
$stmt->execute(['sent']);
|
||||
|
||||
$deleted = $stmt->rowCount();
|
||||
if ($deleted > 0) {
|
||||
LogService::log('Nettoyage des anciens emails', [
|
||||
'level' => 'info',
|
||||
'deleted' => $deleted
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors du nettoyage des anciens emails', [
|
||||
'level' => 'warning',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
||||
// Supprimer le lock
|
||||
if (file_exists(LOCK_FILE)) {
|
||||
unlink(LOCK_FILE);
|
||||
}
|
||||
|
||||
echo "Traitement terminé : $successCount envoyés, $failureCount échecs\n";
|
||||
exit(0);
|
||||
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
31
api/scripts/cron/process_email_queue_with_daily_log.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script wrapper pour process_email_queue avec logs journaliers
|
||||
# Crée automatiquement un nouveau fichier log chaque jour
|
||||
|
||||
# Configuration
|
||||
LOG_DIR="/var/www/geosector/api/logs"
|
||||
LOG_FILE="$LOG_DIR/email_queue_$(date +%Y%m%d).log"
|
||||
PHP_SCRIPT="/var/www/geosector/api/scripts/cron/process_email_queue.php"
|
||||
|
||||
# Créer le répertoire de logs s'il n'existe pas
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# Ajouter un timestamp au début de l'exécution
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Démarrage du processeur de queue d'emails" >> "$LOG_FILE"
|
||||
|
||||
# Exécuter le script PHP
|
||||
/usr/bin/php "$PHP_SCRIPT" >> "$LOG_FILE" 2>&1
|
||||
|
||||
# Ajouter le statut de sortie
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -eq 0 ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (succès)" >> "$LOG_FILE"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fin du traitement (erreur: $EXIT_CODE)" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Nettoyer les logs de plus de 30 jours
|
||||
find "$LOG_DIR" -name "email_queue_*.log" -type f -mtime +30 -delete 2>/dev/null
|
||||
|
||||
exit $EXIT_CODE
|
||||
186
api/scripts/cron/test_email_queue.php
Executable file
186
api/scripts/cron/test_email_queue.php
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Script de test pour vérifier le processeur de queue d'emails
|
||||
* Affiche les emails en attente sans les envoyer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Simuler l'environnement web pour AppConfig en CLI
|
||||
if (php_sapi_name() === 'cli') {
|
||||
$_SERVER['SERVER_NAME'] = $_SERVER['SERVER_NAME'] ?? 'dapp.geosector.fr'; // DVA par défaut
|
||||
$_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
$_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
|
||||
|
||||
// Définir getallheaders si elle n'existe pas (CLI)
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Core/Database.php';
|
||||
require_once __DIR__ . '/../../src/Config/AppConfig.php';
|
||||
|
||||
try {
|
||||
// Initialiser la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$dbConfig = $appConfig->getDatabaseConfig();
|
||||
|
||||
// Initialiser la base de données avec la configuration
|
||||
Database::init($dbConfig);
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "=== TEST DE LA QUEUE D'EMAILS ===\n\n";
|
||||
|
||||
// Statistiques générales
|
||||
$stmt = $db->query('
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
MIN(created_at) as oldest,
|
||||
MAX(created_at) as newest
|
||||
FROM email_queue
|
||||
GROUP BY status
|
||||
');
|
||||
|
||||
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "STATISTIQUES:\n";
|
||||
echo "-------------\n";
|
||||
foreach ($stats as $stat) {
|
||||
echo sprintf(
|
||||
"Status: %s - Nombre: %d (Plus ancien: %s, Plus récent: %s)\n",
|
||||
$stat['status'],
|
||||
$stat['count'],
|
||||
$stat['oldest'] ?? 'N/A',
|
||||
$stat['newest'] ?? 'N/A'
|
||||
);
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
// Emails en attente
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
eq.id,
|
||||
eq.fk_pass,
|
||||
eq.to_email,
|
||||
eq.subject,
|
||||
eq.created_at,
|
||||
eq.attempts,
|
||||
eq.status,
|
||||
p.fk_type,
|
||||
p.montant,
|
||||
p.nom_recu
|
||||
FROM email_queue eq
|
||||
LEFT JOIN ope_pass p ON eq.fk_pass = p.id
|
||||
WHERE eq.status = ?
|
||||
ORDER BY eq.created_at DESC
|
||||
LIMIT 10
|
||||
');
|
||||
|
||||
$stmt->execute(['pending']);
|
||||
$pendingEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($pendingEmails)) {
|
||||
echo "Aucun email en attente.\n";
|
||||
} else {
|
||||
echo "EMAILS EN ATTENTE (10 plus récents):\n";
|
||||
echo "------------------------------------\n";
|
||||
foreach ($pendingEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Créé le: %s | Tentatives: %d\n",
|
||||
$email['created_at'],
|
||||
$email['attempts']
|
||||
);
|
||||
if ($email['fk_pass'] > 0) {
|
||||
echo sprintf(
|
||||
" Passage - Type: %s | Montant: %.2f€ | Reçu: %s\n",
|
||||
$email['fk_type'] == 1 ? 'DON' : 'Autre',
|
||||
$email['montant'] ?? 0,
|
||||
$email['nom_recu'] ?? 'Non généré'
|
||||
);
|
||||
}
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Emails échoués
|
||||
$stmt = $db->prepare('
|
||||
SELECT
|
||||
id,
|
||||
fk_pass,
|
||||
to_email,
|
||||
subject,
|
||||
created_at,
|
||||
attempts,
|
||||
error_message
|
||||
FROM email_queue
|
||||
WHERE status = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
');
|
||||
|
||||
$stmt->execute(['failed']);
|
||||
$failedEmails = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($failedEmails)) {
|
||||
echo "\nEMAILS ÉCHOUÉS (5 plus récents):\n";
|
||||
echo "--------------------------------\n";
|
||||
foreach ($failedEmails as $email) {
|
||||
echo sprintf(
|
||||
"ID: %d | Passage: %d | Destinataire: %s\n",
|
||||
$email['id'],
|
||||
$email['fk_pass'],
|
||||
$email['to_email']
|
||||
);
|
||||
echo sprintf(
|
||||
" Sujet: %s\n",
|
||||
$email['subject']
|
||||
);
|
||||
echo sprintf(
|
||||
" Tentatives: %d | Erreur: %s\n",
|
||||
$email['attempts'],
|
||||
$email['error_message'] ?? 'Non spécifiée'
|
||||
);
|
||||
echo "---\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier la configuration SMTP
|
||||
echo "\nCONFIGURATION SMTP:\n";
|
||||
echo "-------------------\n";
|
||||
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
echo "Host: " . ($smtpConfig['host'] ?? 'Non configuré') . "\n";
|
||||
echo "Port: " . ($smtpConfig['port'] ?? 'Non configuré') . "\n";
|
||||
echo "Username: " . ($smtpConfig['user'] ?? 'Non configuré') . "\n";
|
||||
echo "Password: " . (isset($smtpConfig['pass']) ? '***' : 'Non configuré') . "\n";
|
||||
echo "Encryption: " . ($smtpConfig['secure'] ?? 'Non configuré') . "\n";
|
||||
echo "From Email: " . ($emailConfig['from'] ?? 'Non configuré') . "\n";
|
||||
echo "Contact Email: " . ($emailConfig['contact'] ?? 'Non configuré') . "\n";
|
||||
|
||||
echo "\n=== FIN DU TEST ===\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "ERREUR: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
exit(0);
|
||||
Reference in New Issue
Block a user