chore: add ansible backend docker frontend-prep

This commit is contained in:
2026-01-21 13:05:13 +01:00
parent 5a280b6b01
commit 943fe4de7d
14930 changed files with 2341433 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
package workers
import (
"fmt"
"gestion/db"
"gestion/services"
"log"
"time"
)
// StartAutoAssignmentCron démarre le cron job d'auto-assignation
// Tourne toutes les 1 minute pour assigner les commandes pending
func StartAutoAssignmentCron(database *db.Database, geoService *services.GeoService) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
log.Println("⏰ [CRON] Auto-Assignment Worker démarré (1 min) avec système de priorisation")
// Exécution immédiate au démarrage
go processAutoAssignmentWithPriority(database, geoService)
for range ticker.C {
go processAutoAssignmentWithPriority(database, geoService)
}
}
// processAutoAssignmentWithPriority traite les commandes pending par ordre de priorité
func processAutoAssignmentWithPriority(database *db.Database, geoService *services.GeoService) {
log.Println("🔄 [CRON] === Début du cycle d'auto-assignation ===")
// 1. Récupérer les stats
stats, err := database.GetPendingCommandsStats()
if err != nil {
log.Printf("⚠️ [CRON] Erreur récupération stats: %v", err)
} else {
log.Printf("📊 [CRON] Stats: %d commandes pending, attente moyenne: %d min",
stats["total_pending"], stats["avg_waiting_minutes"])
}
// 2. Récupérer toutes les commandes pending triées par ancienneté
commands, err := database.GetAllCommandsOldestFirst("pending", "")
if err != nil {
log.Printf("❌ [CRON] Erreur récupération commandes: %v", err)
return
}
if len(commands) == 0 {
log.Println("✅ [CRON] Aucune commande en attente")
return
}
log.Printf("📋 [CRON] %d commande(s) pending à traiter (ordre: PLUS ANCIENNES → plus récentes)", len(commands))
// 3. Traiter les commandes dans l'ordre de priorité
assignedCount := 0
failedCount := 0
skippedCount := 0
for i, cmd := range commands {
commandID, ok := cmd["id"].(int)
if !ok {
if idFloat, ok := cmd["id"].(float64); ok {
commandID = int(idFloat)
} else {
skippedCount++
continue
}
}
// Log la position dans la queue de priorité
createdAt, _ := cmd["created_at"].(time.Time)
waitingTime := time.Since(createdAt)
priority := i + 1 // Position dans la queue (1 = plus prioritaire)
log.Printf("🎯 [CRON] [PRIORITÉ #%d/%d] Commande ID:%d | Créée: %s | Attente: %d min",
priority, len(commands),
commandID,
createdAt.Format("2006-01-02 15:04:05"),
int(waitingTime.Minutes()))
// Récupérer l'adresse
address, ok := cmd["adresse"].(string)
if !ok || address == "" || address == "Adresse non spécifiée" {
log.Printf("⚠️ [CRON] Commande %d - Adresse invalide, SKIP", commandID)
failedCount++
continue
}
// Tenter l'assignation
success := tryAssignCommandWithPriority(database, geoService, commandID, address, priority, int(waitingTime.Minutes()))
if success {
assignedCount++
} else {
failedCount++
}
// Petite pause entre les assignations
time.Sleep(500 * time.Millisecond)
}
log.Printf("✅ [CRON] === Cycle terminé: %d assignées, %d échouées, %d skipped ===",
assignedCount, failedCount, skippedCount)
}
// tryAssignCommandWithPriority tente d'assigner une commande avec info de priorité
func tryAssignCommandWithPriority(
database *db.Database,
geoService *services.GeoService,
commandID int,
address string,
priority int,
waitingMinutes int,
) bool {
log.Printf("🎯 [CRON] Traitement commande %d (priorité #%d, attente: %d min)",
commandID, priority, waitingMinutes)
// 1. Géocoder l'adresse
location, err := geoService.GeocodeAddress(address)
if err != nil {
log.Printf("❌ [CRON] Cmd %d - Géocodage échoué: %v", commandID, err)
return false
}
targetCoords := services.Coordinates{
Latitude: location.Latitude,
Longitude: location.Longitude,
}
// 2. Récupérer livreurs actifs
activeLivreurs, err := database.GetAllActiveDeliveryPersons()
if err != nil || len(activeLivreurs) == 0 {
log.Printf("⚠️ [CRON] Cmd %d - Aucun livreur disponible", commandID)
return false
}
usernames := make([]string, len(activeLivreurs))
for i, livreur := range activeLivreurs {
usernames[i] = livreur.Username
}
// 3. Trouver le plus proche
nearest, err := geoService.FindNearestDeliveryPersonFast(targetCoords, usernames)
if err != nil {
log.Printf("❌ [CRON] Cmd %d - Aucun livreur avec GPS", commandID)
return false
}
// 4. Calculer ETA
travelTime := nearest.EstimatedTime
distance := nearest.Distance
// Utiliser TomTom pour les commandes urgentes (> 30 min d'attente)
if waitingMinutes > 30 {
etaTraffic, distTraffic, err := services.GetETAWithTraffic(nearest.Location, targetCoords)
if err == nil {
travelTime = etaTraffic
distance = distTraffic
log.Printf("🚨 [CRON] Cmd %d - Commande urgente, ETA précis calculé", commandID)
}
}
// 5. Assigner à la queue
err = database.AssignCommandToDeliverymanQueueWithCoords(
commandID,
nearest.Username,
travelTime,
location.Latitude,
location.Longitude,
address,
)
if err != nil {
log.Printf("❌ [CRON] Cmd %d - Assignation échouée: %v", commandID, err)
return false
}
// 6. Mettre à jour statut livreur
database.SetDeliveryPersonStatus(nearest.Username, "busy", commandID)
// 7. Log avec info de priorité
logMessage := fmt.Sprintf("Auto-assigné (CRON) à %s (%.2f km, ~%d min) | Priorité: #%d | Attente: %d min",
nearest.Username, distance, travelTime, priority, waitingMinutes)
database.AddCommandLog(commandID, "assigned", logMessage, "system-cron")
log.Printf("✅ [CRON] Cmd %d → %s (%.2f km) | Priorité #%d | Attente: %d min",
commandID, nearest.Username, distance, priority, waitingMinutes)
return true
}
+315
View File
@@ -0,0 +1,315 @@
// ============================================
// workers/redis_worker.go - TÂCHES ARRIÈRE-PLAN
// ============================================
package workers
import (
"encoding/json"
"gestion/db"
"log"
"time"
)
// ============================================
// WORKER PRINCIPAL
// ============================================
// StartRedisWorkers démarre tous les workers Redis
func StartRedisWorkers(database *db.Database) {
log.Println("🚀 Démarrage des workers Redis...")
// Worker pour les notifications programmées
go NotificationWorker(database)
// Worker pour l'auto-assignation des commandes
go AutoAssignWorker(database)
// Worker pour le nettoyage des réservations expirées
go StockCleanupWorker(database)
// Worker pour la synchronisation des points
go PointsSyncWorker(database)
log.Println("✅ Tous les workers Redis sont démarrés")
}
// ============================================
// WORKER NOTIFICATIONS
// ============================================
// NotificationWorker traite les notifications programmées
func NotificationWorker(database *db.Database) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
log.Println("📢 Worker Notifications démarré (check toutes les 30s)")
for range ticker.C {
err := database.ProcessScheduledNotifications()
if err != nil {
log.Printf("⚠️ Erreur traitement notifications: %v", err)
}
}
}
// ============================================
// WORKER AUTO-ASSIGNATION
// ============================================
// AutoAssignWorker assigne automatiquement les commandes
func AutoAssignWorker(database *db.Database) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
log.Println("🤖 Worker Auto-Assignation démarré (check toutes les minutes)")
for range ticker.C {
nextCommand, err := database.GetNextCommandInQueue()
if err != nil {
continue // Pas de commande
}
shouldAssign := false
if shouldAssign {
err = database.AutoAssignCommand(nextCommand.CommandID)
if err != nil {
log.Printf("⚠️ Échec auto-assignation commande %d: %v",
nextCommand.CommandID, err)
} else {
log.Printf("✅ Commande %d auto-assignée", nextCommand.CommandID)
database.RemoveCommandFromQueue(nextCommand.CommandID)
}
}
}
}
// ============================================
// WORKER NETTOYAGE STOCK
// ============================================
// StockCleanupWorker nettoie les réservations expirées
func StockCleanupWorker(database *db.Database) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
log.Println("🧹 Worker Nettoyage Stock démarré (check toutes les 5 min)")
for range ticker.C {
cleanedCount := 0
// Récupérer toutes les clés de réservation
keys, err := db.Redis.Keys(db.RedisCtx, "stock:reserve:*").Result()
if err != nil {
log.Printf("⚠️ Erreur récupération réservations: %v", err)
continue
}
for _, key := range keys {
// Vérifier si la réservation est expirée
ttl, err := db.Redis.TTL(db.RedisCtx, key).Result()
if err != nil {
continue
}
// Si TTL <= 0, la réservation est expirée
if ttl <= 0 {
// Redis va automatiquement supprimer la clé
// Mais on peut log pour traçabilité
cleanedCount++
}
}
if cleanedCount > 0 {
log.Printf("🧹 %d réservations de stock expirées nettoyées", cleanedCount)
}
}
}
// ============================================
// WORKER SYNCHRONISATION POINTS
// ============================================
// PointsSyncWorker synchronise les points Redis avec la DB
func PointsSyncWorker(database *db.Database) {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
log.Println("🔄 Worker Synchronisation Points démarré (check toutes les 10 min)")
for range ticker.C {
syncedCount := 0
// Récupérer toutes les clés de points
keys, err := db.Redis.Keys(db.RedisCtx, "client:points:*").Result()
if err != nil {
log.Printf("⚠️ Erreur récupération points: %v", err)
continue
}
for _, key := range keys {
// Extraire le username
username := key[len("client:points:"):]
// Récupérer les points depuis Redis
points, err := db.Redis.Get(db.RedisCtx, key).Int()
if err != nil {
continue
}
// Mettre à jour dans la DB principale
err = database.AddClientPoints(username, points)
if err != nil {
log.Printf("⚠️ Erreur sync points pour %s: %v", username, err)
continue
}
// Réinitialiser dans Redis après sync
db.Redis.Set(db.RedisCtx, key, 0, 0)
syncedCount++
}
if syncedCount > 0 {
log.Printf("🔄 %d comptes clients synchronisés avec la DB", syncedCount)
}
}
}
// ============================================
// WORKER MISE À JOUR ETA
// ============================================
// ETAUpdateWorker met à jour automatiquement les ETAs
func ETAUpdateWorker(database *db.Database) {
ticker := time.NewTicker(2 * time.Minute)
defer ticker.Stop()
log.Println("⏱️ Worker Mise à Jour ETA démarré (check toutes les 2 min)")
for range ticker.C {
// Récupérer toutes les commandes en cours de livraison
commands, err := database.GetAllCommands("support", "")
if err != nil {
continue
}
for _, command := range commands {
commandID := command["id"].(int)
// Recalculer l'ETA basé sur la position du livreur
// (logique à implémenter selon vos besoins)
// Exemple: réduire l'ETA de 2 minutes
// database.SetCommandETA(commandID, newETA)
log.Printf("🔄 ETA mis à jour pour commande %d", commandID)
}
}
}
// ============================================
// WORKER ALERTES RETARD
// ============================================
// DelayAlertWorker envoie des alertes en cas de retard
func DelayAlertWorker(database *db.Database) {
ticker := time.NewTicker(3 * time.Minute)
defer ticker.Stop()
log.Println("⚠️ Worker Alertes Retard démarré (check toutes les 3 min)")
for range ticker.C {
// Récupérer les ETAs de toutes les commandes
keys, err := db.Redis.Keys(db.RedisCtx, "command:eta:*").Result()
if err != nil {
continue
}
for _, key := range keys {
data, err := db.Redis.Get(db.RedisCtx, key).Result()
if err != nil {
continue
}
// Désérialiser le JSON dans eta
var eta map[string]interface{}
if err := json.Unmarshal([]byte(data), &eta); err != nil {
log.Printf("⚠️ Impossible de parser ETA pour %s: %v", key, err)
continue
}
// Extraire l'heure d'arrivée
arrivalTimeFloat, ok := eta["arrival_time"].(float64)
if !ok {
continue
}
arrivalTime := int64(arrivalTimeFloat)
// Vérifier retard
now := time.Now().Unix()
if now > arrivalTime {
commandIDFloat, ok := eta["command_id"].(float64)
if !ok {
continue
}
commandID := int(commandIDFloat)
delay := (now - arrivalTime) / 60 // en minutes
log.Printf("⚠️ ALERTE: Commande %d en retard de %d minutes", commandID, delay)
// Notifier l'admin
database.PublishCommandEvent(commandID, "delay_alert", "Commande en retard")
if delay > 15 {
command, _ := database.GetCommandByID(commandID)
if livreur, ok := command["livreur_assign"].(string); ok {
log.Printf("⚠️ Pénalité appliquée au livreur %s", livreur)
}
}
}
}
}
}
// ============================================
// WORKER STATISTIQUES TEMPS RÉEL
// ============================================
// StatsWorker calcule des statistiques en temps réel
func StatsWorker(database *db.Database) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
log.Println("📊 Worker Statistiques démarré (check toutes les 5 min)")
for range ticker.C {
// Nombre de commandes en attente
queueSize, _ := db.Redis.ZCard(db.RedisCtx, "queue:pending:sorted").Result()
// Nombre de livreurs disponibles
livreurs, _ := database.GetAvailableDeliveryPersonsRedis()
availableCount := len(livreurs)
// Nombre de livraisons en cours
commands, _ := database.GetAllCommands("support", "")
inProgressCount := len(commands)
stats := map[string]interface{}{
"queue_size": queueSize,
"available_drivers": availableCount,
"in_progress": inProgressCount,
"timestamp": time.Now().Unix(),
}
// Stocker dans Redis
db.Redis.HSet(db.RedisCtx, "stats:realtime",
"queue_size", stats["queue_size"],
"available_drivers", stats["available_drivers"],
"in_progress", stats["in_progress"],
"timestamp", stats["timestamp"],
)
log.Printf("📊 Stats: %d en attente | %d livreurs dispo | %d en cours",
queueSize, availableCount, inProgressCount)
}
}