chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user