@@ -240,7 +240,7 @@ func (d *Database) CreateCommandWithAddress(username, deliveryAddress string) (*
|
|||||||
// Stock déjà déduit à l'ajout au panier — ne pas déduire une seconde fois ici.
|
// Stock déjà déduit à l'ajout au panier — ne pas déduire une seconde fois ici.
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.GDB.Delete(&models.Panier{}, "username = ?", username).Error; err != nil {
|
if err := d.GDB.Exec(`DELETE FROM baskets WHERE username = ?`, username).Error; err != nil {
|
||||||
log.Printf("⚠️ Erreur vidage panier: %v", err)
|
log.Printf("⚠️ Erreur vidage panier: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ func (d *Database) AssignDeliveryPerson(commandID int, livreurUsername string) e
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDeliveryPersonCommands récupère les commandes assignées à un livreur
|
|
||||||
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) {
|
func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status string) ([]map[string]any, error) {
|
||||||
query := `SELECT id, username, status, adresse, total_prix::float8 as total_prix, livreur_assign, created_at, updated_at
|
query := `SELECT id, username, status, adresse, total_prix::float8 as total_prix, livreur_assign, created_at, updated_at
|
||||||
FROM commandes
|
FROM commandes
|
||||||
@@ -106,6 +105,10 @@ func (d *Database) GetDeliveryPersonCommands(livreurUsername string, status stri
|
|||||||
if status != "" {
|
if status != "" {
|
||||||
query += " AND status = ?"
|
query += " AND status = ?"
|
||||||
args = append(args, status)
|
args = append(args, status)
|
||||||
|
} else {
|
||||||
|
// Ne retourner que les commandes actives — exclure l'historique terminé
|
||||||
|
// pour éviter le N+1 sur 66+ commandes qui fait timeout le mobile
|
||||||
|
query += " AND status IN ('assigned', 'en_route', 'arrived', 'livre')"
|
||||||
}
|
}
|
||||||
|
|
||||||
query += " ORDER BY created_at DESC"
|
query += " ORDER BY created_at DESC"
|
||||||
|
|||||||
@@ -33,12 +33,20 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifJSON, _ := json.Marshal(notification)
|
notifJSON, _ := json.Marshal(notification)
|
||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
pipe := Redis.Pipeline()
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
pipe.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
|
pipe.LTrim(RedisCtx, notifKey, 0, 199)
|
||||||
|
pipe.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
pipe.Exec(RedisCtx) //nolint
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
|
if chatID, ok, err := d.GetClientTelegramChatID(username); err == nil && ok {
|
||||||
|
dedupKey := fmt.Sprintf("notif:dedup:%d:%s", commandID, notifType)
|
||||||
|
if set, _ := Redis.SetNX(RedisCtx, dedupKey, "1", 5*time.Minute).Result(); set {
|
||||||
go sendTelegramNotif(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
go sendTelegramNotif(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
||||||
|
} else {
|
||||||
|
log.Printf("⚠️ [NOTIF] Doublon détecté (cmd=%d, type=%s) — Telegram ignoré", commandID, notifType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +54,6 @@ func (d *Database) NotifyClient(username string, commandID int, notifType, messa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyLivreur envoie une notification in-app (Redis) à un livreur
|
|
||||||
func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error {
|
func (d *Database) NotifyLivreur(username string, commandID int, notifType, message string) error {
|
||||||
notifKey := fmt.Sprintf("notifications:%s", username)
|
notifKey := fmt.Sprintf("notifications:%s", username)
|
||||||
|
|
||||||
@@ -59,12 +66,20 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
|
|||||||
}
|
}
|
||||||
|
|
||||||
notifJSON, _ := json.Marshal(notification)
|
notifJSON, _ := json.Marshal(notification)
|
||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
pipe2 := Redis.Pipeline()
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
pipe2.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
|
pipe2.LTrim(RedisCtx, notifKey, 0, 199)
|
||||||
|
pipe2.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
pipe2.Exec(RedisCtx) //nolint
|
||||||
|
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(username); err == nil && ok {
|
||||||
|
dedupKey := fmt.Sprintf("notif:dedup:livreur:%d:%s", commandID, notifType)
|
||||||
|
if set, _ := Redis.SetNX(RedisCtx, dedupKey, "1", 5*time.Minute).Result(); set {
|
||||||
go sendTelegramNotif(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
go sendTelegramNotif(chatID, fmt.Sprintf("🔔 <b>Notification</b>\n\n%s", message))
|
||||||
|
} else {
|
||||||
|
log.Printf("⚠️ [NOTIF] Doublon livreur détecté (cmd=%d, type=%s) — Telegram ignoré", commandID, notifType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +87,6 @@ func (d *Database) NotifyLivreur(username string, commandID int, notifType, mess
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyAllAdminCabine stocke une notification Redis pour tous les admins/cabines
|
|
||||||
func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) {
|
func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryAddr string) {
|
||||||
var users []struct {
|
var users []struct {
|
||||||
Username string `gorm:"column:username"`
|
Username string `gorm:"column:username"`
|
||||||
@@ -93,24 +107,27 @@ func (d *Database) NotifyAllAdminCabine(commandID int, clientUsername, deliveryA
|
|||||||
}
|
}
|
||||||
notifJSON, _ := json.Marshal(notification)
|
notifJSON, _ := json.Marshal(notification)
|
||||||
|
|
||||||
count := 0
|
pipe := Redis.Pipeline()
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
notifKey := fmt.Sprintf("notifications:%s", u.Username)
|
notifKey := fmt.Sprintf("notifications:%s", u.Username)
|
||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
pipe.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
pipe.LTrim(RedisCtx, notifKey, 0, 199)
|
||||||
|
pipe.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
}
|
||||||
|
pipe.Exec(RedisCtx) //nolint
|
||||||
|
|
||||||
|
count := len(users)
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
|
for _, u := range users {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
||||||
capturedChatID := chatID
|
capturedChatID := chatID
|
||||||
go sendTelegramNotif(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", msg))
|
go sendTelegramNotif(capturedChatID, fmt.Sprintf("🔔 <b>Nouvelle commande</b>\n\n%s", msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count++
|
|
||||||
}
|
}
|
||||||
log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID)
|
log.Printf("📬 [ADMIN_NOTIF] Notif Redis (%d users) pour commande #%d", count, commandID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyAllAdminCabineAlert envoie une notification Redis à tous les admins/cabines lors d'une alerte
|
|
||||||
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
|
func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alertMessage string) {
|
||||||
var users []struct {
|
var users []struct {
|
||||||
Username string `gorm:"column:username"`
|
Username string `gorm:"column:username"`
|
||||||
@@ -131,19 +148,23 @@ func (d *Database) NotifyAllAdminCabineAlert(alertID int, livreurUsername, alert
|
|||||||
}
|
}
|
||||||
notifJSON, _ := json.Marshal(notification)
|
notifJSON, _ := json.Marshal(notification)
|
||||||
|
|
||||||
count := 0
|
pipe := Redis.Pipeline()
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
notifKey := fmt.Sprintf("notifications:%s", u.Username)
|
notifKey := fmt.Sprintf("notifications:%s", u.Username)
|
||||||
Redis.LPush(RedisCtx, notifKey, notifJSON)
|
pipe.LPush(RedisCtx, notifKey, notifJSON)
|
||||||
Redis.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
pipe.LTrim(RedisCtx, notifKey, 0, 199)
|
||||||
|
pipe.Expire(RedisCtx, notifKey, 7*24*time.Hour)
|
||||||
|
}
|
||||||
|
pipe.Exec(RedisCtx) //nolint
|
||||||
|
|
||||||
|
count := len(users)
|
||||||
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
if services.TelegramBot != nil && services.TelegramBot.IsNotificationsEnabled() {
|
||||||
|
for _, u := range users {
|
||||||
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
if chatID, ok, err := d.GetUserTelegramChatID(u.Username); err == nil && ok {
|
||||||
capturedChatID := chatID
|
capturedChatID := chatID
|
||||||
go sendTelegramNotif(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", body))
|
go sendTelegramNotif(capturedChatID, fmt.Sprintf("🚨 <b>Alerte livreur</b>\n\n%s", body))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
count++
|
|
||||||
}
|
}
|
||||||
log.Printf("🚨 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
|
log.Printf("🚨 [ALERT_NOTIF] Notif Redis (%d users) pour alerte #%d de %s", count, alertID, livreurUsername)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CleanupInvalidQueueCommands supprime toutes les commandes avec des données manquantes
|
|
||||||
func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
||||||
log.Println("🧹 [CLEANUP] Démarrage du nettoyage des commandes invalides...")
|
log.Println("🧹 [CLEANUP] Démarrage du nettoyage des commandes invalides...")
|
||||||
|
|
||||||
keys, err := Redis.Keys(RedisCtx, "queue:pending:*").Result()
|
keys, err := scanRedisKeys("queue:pending:*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("erreur récupération des clés: %w", err)
|
return 0, fmt.Errorf("erreur récupération des clés: %w", err)
|
||||||
}
|
}
|
||||||
@@ -82,7 +81,6 @@ func (d *Database) CleanupInvalidQueueCommands() (int, error) {
|
|||||||
return removedCount, nil
|
return removedCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeInvalidCommand supprime une commande invalide de toutes les queues
|
|
||||||
func (d *Database) removeInvalidCommand(key string, commandID int, reason string) {
|
func (d *Database) removeInvalidCommand(key string, commandID int, reason string) {
|
||||||
commandIDStr := fmt.Sprintf("%d", commandID)
|
commandIDStr := fmt.Sprintf("%d", commandID)
|
||||||
|
|
||||||
@@ -94,7 +92,7 @@ func (d *Database) removeInvalidCommand(key string, commandID int, reason string
|
|||||||
Redis.ZRem(RedisCtx, "queue:priority:sorted", commandIDStr)
|
Redis.ZRem(RedisCtx, "queue:priority:sorted", commandIDStr)
|
||||||
|
|
||||||
// 3. Supprimer des queues de livreurs
|
// 3. Supprimer des queues de livreurs
|
||||||
livreurKeys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
|
livreurKeys, _ := scanRedisKeys("queue:deliveryman:*")
|
||||||
for _, queueKey := range livreurKeys {
|
for _, queueKey := range livreurKeys {
|
||||||
if len(queueKey) > 6 && queueKey[len(queueKey)-6:] == ":count" {
|
if len(queueKey) > 6 && queueKey[len(queueKey)-6:] == ":count" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -68,8 +68,9 @@ func (d *Database) GetAllQueuesOverview() (map[string]any, error) {
|
|||||||
overview["general_queue"] = generalQueueSize
|
overview["general_queue"] = generalQueueSize
|
||||||
|
|
||||||
deliverymanQueues := make(map[string]any)
|
deliverymanQueues := make(map[string]any)
|
||||||
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
|
keys, _ := scanRedisKeys("queue:deliveryman:*")
|
||||||
|
|
||||||
|
var totalPending int64 = generalQueueSize
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if len(key) > 6 && key[len(key)-6:] == ":count" {
|
if len(key) > 6 && key[len(key)-6:] == ":count" {
|
||||||
continue
|
continue
|
||||||
@@ -83,29 +84,19 @@ func (d *Database) GetAllQueuesOverview() (map[string]any, error) {
|
|||||||
"can_accept_more": queueSize < MAX_COMMANDS_PER_DELIVERYMAN,
|
"can_accept_more": queueSize < MAX_COMMANDS_PER_DELIVERYMAN,
|
||||||
"capacity": fmt.Sprintf("%d/%d", queueSize, MAX_COMMANDS_PER_DELIVERYMAN),
|
"capacity": fmt.Sprintf("%d/%d", queueSize, MAX_COMMANDS_PER_DELIVERYMAN),
|
||||||
}
|
}
|
||||||
}
|
totalPending += queueSize
|
||||||
|
|
||||||
overview["deliveryman_queues"] = deliverymanQueues
|
|
||||||
var totalPending int64 = generalQueueSize
|
|
||||||
for _, key := range keys {
|
|
||||||
if len(key) > 6 && key[len(key)-6:] == ":count" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
size, _ := Redis.ZCard(RedisCtx, key).Result()
|
|
||||||
totalPending += size
|
|
||||||
}
|
}
|
||||||
overview["total_pending"] = totalPending
|
overview["total_pending"] = totalPending
|
||||||
|
|
||||||
return overview, nil
|
return overview, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQueueStats - Statistiques détaillées
|
|
||||||
func (d *Database) GetQueueStats() (map[string]any, error) {
|
func (d *Database) GetQueueStats() (map[string]any, error) {
|
||||||
normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
|
normalCount, _ := Redis.ZCard(RedisCtx, "queue:pending:sorted").Result()
|
||||||
priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result()
|
priorityCount, _ := Redis.ZCard(RedisCtx, "queue:priority:sorted").Result()
|
||||||
|
|
||||||
var deliverymanQueueCount int64
|
var deliverymanQueueCount int64
|
||||||
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
|
keys, _ := scanRedisKeys("queue:deliveryman:*")
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if len(key) > 6 && key[len(key)-6:] == ":count" {
|
if len(key) > 6 && key[len(key)-6:] == ":count" {
|
||||||
continue
|
continue
|
||||||
@@ -117,7 +108,8 @@ func (d *Database) GetQueueStats() (map[string]any, error) {
|
|||||||
var totalWaitTime int64
|
var totalWaitTime int64
|
||||||
var commandCount int64
|
var commandCount int64
|
||||||
|
|
||||||
normalResults, _ := Redis.ZRangeWithScores(RedisCtx, "queue:pending:sorted", 0, -1).Result()
|
// Limité aux 100 premières entrées pour ne pas bloquer Redis sur une grande queue
|
||||||
|
normalResults, _ := Redis.ZRangeWithScores(RedisCtx, "queue:pending:sorted", 0, 99).Result()
|
||||||
for _, result := range normalResults {
|
for _, result := range normalResults {
|
||||||
commandID := extractCommandID(result.Member)
|
commandID := extractCommandID(result.Member)
|
||||||
if commandID <= 0 {
|
if commandID <= 0 {
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ func (d *Database) AddToGeneralQueue(queueItem models.CommandQueue) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveCommandFromQueue - VERSION AMÉLIORÉE avec auto-update du statut
|
|
||||||
func (d *Database) RemoveCommandFromQueue(commandID int) error {
|
func (d *Database) RemoveCommandFromQueue(commandID int) error {
|
||||||
key := fmt.Sprintf("queue:pending:%d", commandID)
|
key := fmt.Sprintf("queue:pending:%d", commandID)
|
||||||
commandIDStr := strconv.Itoa(commandID)
|
commandIDStr := strconv.Itoa(commandID)
|
||||||
@@ -153,7 +152,7 @@ func (d *Database) RemoveCommandFromQueue(commandID int) error {
|
|||||||
pipe.ZRem(RedisCtx, "queue:priority:sorted", commandIDStr)
|
pipe.ZRem(RedisCtx, "queue:priority:sorted", commandIDStr)
|
||||||
|
|
||||||
// Trouver et retirer de la queue du livreur
|
// Trouver et retirer de la queue du livreur
|
||||||
keys, _ := Redis.Keys(RedisCtx, "queue:deliveryman:*").Result()
|
keys, _ := scanRedisKeys("queue:deliveryman:*")
|
||||||
var affectedDeliveryman string
|
var affectedDeliveryman string
|
||||||
|
|
||||||
for _, queueKey := range keys {
|
for _, queueKey := range keys {
|
||||||
@@ -320,3 +319,20 @@ func (d *Database) iterDeliveryStatuses(fn func(models.DeliveryPersonStatus)) er
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scanRedisKeys(pattern string) ([]string, error) {
|
||||||
|
var all []string
|
||||||
|
cursor := uint64(0)
|
||||||
|
for {
|
||||||
|
batch, next, err := Redis.Scan(RedisCtx, cursor, pattern, 200).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
all = append(all, batch...)
|
||||||
|
cursor = next
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -838,6 +838,14 @@ func CreateUser(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashed, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ [CREATE_USER] Erreur bcrypt: %v", err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur traitement mot de passe"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.Password = string(hashed)
|
||||||
|
|
||||||
if err := database.CreateUser(&user); err != nil {
|
if err := database.CreateUser(&user); err != nil {
|
||||||
log.Printf("❌ [CREATE_USER] Erreur: %v", err)
|
log.Printf("❌ [CREATE_USER] Erreur: %v", err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur création"})
|
||||||
|
|||||||
Reference in New Issue
Block a user