Files
projet_gestion_commande/backend/gestion/db/redis_queue_optimization.go
T

344 lines
10 KiB
Go

package db
import (
"encoding/json"
"fmt"
"gestion/models"
"gestion/services"
"log"
"math"
"sort"
"strconv"
"time"
"github.com/redis/go-redis/v9"
)
// CompleteDeliveryAndProcessNext marque une livraison comme terminée et optimise la queue
func (d *Database) CompleteDeliveryAndProcessNext(deliveryman string, completedCommandID int) error {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
// Retirer la commande complétée de la queue
Redis.ZRem(RedisCtx, queueKey, strconv.Itoa(completedCommandID))
// Supprimer les données de la commande
commandKey := fmt.Sprintf("queue:pending:%d", completedCommandID)
Redis.Del(RedisCtx, commandKey)
// Supprimer le cache de destination
destCacheKey := fmt.Sprintf("command:destination:%d", completedCommandID)
Redis.Del(RedisCtx, destCacheKey)
// Supprimer l'ETA
etaKey := fmt.Sprintf("command:eta:%d", completedCommandID)
Redis.Del(RedisCtx, etaKey)
// Décrémenter le compteur
counterKey := fmt.Sprintf("queue:deliveryman:%s:count", deliveryman)
Redis.Decr(RedisCtx, counterKey)
log.Printf("✅ Livraison %d complétée par %s", completedCommandID, deliveryman)
// Vérifier s'il reste des commandes
remainingCount, _ := Redis.ZCard(RedisCtx, queueKey).Result()
if remainingCount == 0 {
d.SetDeliveryPersonStatus(deliveryman, "available", 0)
log.Printf("🔓 Livreur %s libéré - Plus de commandes en queue", deliveryman)
// Chercher dans la queue générale
go d.ProcessNextCommandInQueue(deliveryman)
return nil
}
// ============================================
// 🔄 OPTIMISATION PAR PROXIMITÉ
// ============================================
log.Printf("🔄 Optimisation queue de %s: %d commande(s) restante(s)", deliveryman, remainingCount)
err := d.OptimizeDeliverymanQueueByProximity(deliveryman)
if err != nil {
log.Printf("⚠️ Erreur optimisation queue: %v", err)
// Fallback: recalculer les ETAs sans réorganiser
d.RecalculateQueueETAs(deliveryman)
}
// Récupérer la prochaine commande (maintenant la plus proche)
nextCommand, nextETA, err := d.FindNearestCommandInQueue(deliveryman)
if err == nil && nextCommand != nil {
d.SetDeliveryPersonStatus(deliveryman, "busy", nextCommand.CommandID)
log.Printf("📍 Prochaine livraison: Commande %d (ETA: %d min)", nextCommand.CommandID, nextETA)
}
return nil
}
func (d *Database) OptimizeDeliverymanQueueByProximity(deliveryman string) error {
livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman)
if err != nil {
log.Printf("⚠️ Position livreur %s non disponible, recalcul ETAs simple", deliveryman)
return d.RecalculateQueueETAs(deliveryman)
}
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result()
if err != nil || len(commandIDs) == 0 {
return nil
}
// Collecter les informations de chaque commande avec sa distance
var commandsWithDistance []CommandWithDistance
for _, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr)
if commandID <= 0 {
continue
}
// 1. Essayer de récupérer depuis queue:pending:{id}
commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result()
var lat, lng float64
var address string
if err == nil && data != "" {
var queueItem models.CommandQueue
if err := json.Unmarshal([]byte(data), &queueItem); err == nil {
lat = queueItem.Lat
lng = queueItem.Lng
address = queueItem.Address
}
}
// 2. Si coordonnées à 0, essayer le cache command:destination:{id}
if lat == 0 && lng == 0 {
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, err := Redis.Get(RedisCtx, destCacheKey).Result()
if err == nil && destData != "" {
var coords struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
if err := json.Unmarshal([]byte(destData), &coords); err == nil {
lat = coords.Lat
lng = coords.Lon
log.Printf("📍 Coordonnées récupérées depuis cache destination pour commande %d: (%.6f, %.6f)", commandID, lat, lng)
}
}
}
// 3. Si toujours 0, récupérer depuis la DB
if lat == 0 && lng == 0 {
command, err := d.GetCommandByID(commandID)
if err == nil {
if dLat, ok := command["dest_latitude"].(float64); ok && dLat != 0 {
lat = dLat
}
if dLng, ok := command["dest_longitude"].(float64); ok && dLng != 0 {
lng = dLng
}
if address == "" {
if addr, ok := command["adresse"].(string); ok {
address = addr
}
}
}
}
// 4. Si toujours 0, utiliser une distance très grande
if lat == 0 && lng == 0 {
log.Printf("⚠️ Coordonnées non disponibles pour commande %d, utilisation position par défaut", commandID)
commandsWithDistance = append(commandsWithDistance, CommandWithDistance{
CommandID: commandID,
Address: address,
Lat: 0,
Lng: 0,
Distance: 999999,
EstimatedETA: 120,
QueueItem: models.CommandQueue{
CommandID: commandID,
Address: address,
Lat: 0,
Lng: 0,
},
})
continue
}
// ✅ MODIFIÉ: Distance depuis la position ACTUELLE du livreur (pas chaînée)
distance := d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, lat, lng)
commandsWithDistance = append(commandsWithDistance, CommandWithDistance{
CommandID: commandID,
Address: address,
Lat: lat,
Lng: lng,
Distance: distance,
EstimatedETA: services.CalculateETA(distance),
QueueItem: models.CommandQueue{
CommandID: commandID,
Address: address,
Lat: lat,
Lng: lng,
},
})
}
if len(commandsWithDistance) == 0 {
return nil
}
// Trier par distance (la plus proche en premier)
sort.Slice(commandsWithDistance, func(i, j int) bool {
return commandsWithDistance[i].Distance < commandsWithDistance[j].Distance
})
log.Printf("🔄 Optimisation queue de %s: %d commandes triées par proximité", deliveryman, len(commandsWithDistance))
// Vider la queue actuelle
Redis.Del(RedisCtx, queueKey)
Redis.Del(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman))
// ✅ MODIFIÉ: Recréer la queue avec ETA = distance directe depuis position livreur
for i, cmd := range commandsWithDistance {
var travelTime int
var distance float64
if cmd.Lat != 0 && cmd.Lng != 0 {
// ✅ Distance depuis la position ACTUELLE du livreur (pas cumulative)
distance = d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, cmd.Lat, cmd.Lng)
travelTime = services.CalculateETA(distance)
} else {
distance = 0
travelTime = 10
}
// ✅ ETA = temps de trajet direct uniquement
cmd.QueueItem.EstimatedETA = travelTime
score := float64(i + 1)
data, _ := json.Marshal(cmd.QueueItem)
commandKey := fmt.Sprintf("queue:pending:%d", cmd.CommandID)
Redis.Set(RedisCtx, commandKey, data, 24*time.Hour)
Redis.ZAdd(RedisCtx, queueKey, redis.Z{
Score: score,
Member: cmd.CommandID,
})
d.SetCommandETAWithDetails(cmd.CommandID, travelTime, i+1)
log.Printf(" 📍 Position %d: Commande %d - %.2f km - ETA trajet: %d min",
i+1, cmd.CommandID, distance, travelTime)
}
Redis.Set(RedisCtx, fmt.Sprintf("queue:deliveryman:%s:count", deliveryman), len(commandsWithDistance), 0)
log.Printf("✅ Queue de %s optimisée: %d commandes réorganisées par proximité", deliveryman, len(commandsWithDistance))
return nil
}
func (d *Database) RecalculateQueueETAs(deliveryman string) error {
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result()
if err != nil || len(commandIDs) == 0 {
return nil
}
// ✅ Récupérer la position actuelle du livreur
livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman)
if err != nil {
log.Printf("⚠️ Position livreur %s non disponible pour recalcul ETA", deliveryman)
return err
}
for i, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr)
if commandID <= 0 {
continue
}
commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil {
continue
}
var queueItem models.CommandQueue
if err := json.Unmarshal([]byte(data), &queueItem); err != nil {
continue
}
// ✅ MODIFIÉ: ETA = distance directe depuis position livreur
travelTime := d.CalculateETABetweenPoints(livreurLat, livreurLng, queueItem.Lat, queueItem.Lng)
d.SetCommandETAWithDetails(commandID, travelTime, i+1)
queueItem.EstimatedETA = travelTime
updatedData, _ := json.Marshal(queueItem)
Redis.Set(RedisCtx, commandKey, updatedData, 24*time.Hour)
}
log.Printf("🔄 ETAs recalculés pour %d commandes de %s (trajet direct)", len(commandIDs), deliveryman)
return nil
}
func (d *Database) UpdateQueueETAsAfterCompletion(deliveryman string) error {
return d.RecalculateQueueETAs(deliveryman)
}
// FindNearestCommandInQueue trouve la commande la plus proche du livreur
func (d *Database) FindNearestCommandInQueue(deliveryman string) (*models.CommandQueue, int, error) {
livreurLat, livreurLng, err := d.GetDeliveryPersonLocation(deliveryman)
if err != nil {
return nil, 0, fmt.Errorf("position livreur non disponible: %w", err)
}
queueKey := fmt.Sprintf("queue:deliveryman:%s", deliveryman)
commandIDs, err := Redis.ZRange(RedisCtx, queueKey, 0, -1).Result()
if err != nil || len(commandIDs) == 0 {
return nil, 0, fmt.Errorf("queue vide")
}
var nearestCommand *models.CommandQueue
var nearestDistance float64 = math.MaxFloat64
var nearestETA int
for _, cmdIDStr := range commandIDs {
commandID := extractCommandID(cmdIDStr)
if commandID <= 0 {
continue
}
commandKey := fmt.Sprintf("queue:pending:%d", commandID)
data, err := Redis.Get(RedisCtx, commandKey).Result()
if err != nil {
continue
}
var queueItem models.CommandQueue
if err := json.Unmarshal([]byte(data), &queueItem); err != nil {
continue
}
distance := d.CalculateDistanceBetweenPoints(livreurLat, livreurLng, queueItem.Lat, queueItem.Lng)
if distance < nearestDistance {
nearestDistance = distance
nearestCommand = &queueItem
nearestETA = services.CalculateETA(distance)
}
}
if nearestCommand == nil {
return nil, 0, fmt.Errorf("aucune commande trouvée")
}
return nearestCommand, nearestETA, nil
}