feat: update multiple tomtom keys and switch tomtom key and add comming soon button

This commit is contained in:
2026-05-19 17:35:39 +02:00
parent 48bdba750e
commit 93120f6bfe
18 changed files with 341 additions and 69 deletions
+5
View File
@@ -236,6 +236,11 @@ func InitDB() *Database {
log.Fatalf("❌ Erreur migration clients.points_extra: %v", err)
}
// Migration: flag "à venir" sur les produits
if _, err = database.Exec(`ALTER TABLE products ADD COLUMN IF NOT EXISTS coming_soon BOOLEAN NOT NULL DEFAULT FALSE`); err != nil {
log.Fatalf("❌ Erreur migration products.coming_soon: %v", err)
}
// Lancer le nettoyage périodique des tokens expirés
go database.cleanExpiredTokensPeriodically()
+12
View File
@@ -211,6 +211,18 @@ func (d *Database) SetProductStock(productID int, stock float64) error {
return nil
}
func (d *Database) SetProductComingSoon(productID int, comingSoon bool) error {
result := d.GDB.Exec(`UPDATE products SET coming_soon = ?, updated_at = ? WHERE id = ?`,
comingSoon, time.Now(), productID)
if result.Error != nil {
return fmt.Errorf("erreur mise à jour coming_soon: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("produit non trouvé")
}
return nil
}
// DeleteProduct supprime un produit
func (d *Database) DeleteProduct(productID int) error {
result := d.GDB.Exec(`DELETE FROM products WHERE id = ?`, productID)
+10
View File
@@ -278,6 +278,8 @@ func CreateProduct(c *gin.Context) {
log.Printf("✅ [CreateProduct] %s crée produit: %s", username, name)
comingSoon := c.PostForm("coming_soon") == "true"
// ✅ CRÉER LE PRODUIT
product := models.Product{
Name: name,
@@ -285,6 +287,7 @@ func CreateProduct(c *gin.Context) {
Description: description,
Stock: stock,
Unit: unit,
ComingSoon: comingSoon,
Prices: prices,
}
@@ -573,6 +576,7 @@ func UpdateProduct(c *gin.Context) {
Unit string `json:"unit"`
Prices []models.ProductPrice `json:"prices"`
Stock *float64 `json:"stock"`
ComingSoon *bool `json:"coming_soon"`
}
if err := c.ShouldBindJSON(&updateData); err != nil {
@@ -638,6 +642,12 @@ func UpdateProduct(c *gin.Context) {
}
}
if updateData.ComingSoon != nil {
if err := database.SetProductComingSoon(id, *updateData.ComingSoon); err != nil {
log.Printf("⚠️ [UpdateProduct] Erreur mise à jour coming_soon: %v", err)
}
}
// ✅ RÉCUPÉRER LE PRODUIT MIS À JOUR
updatedProduct, _ := database.GetProductByID(id)
media, _ := database.GetMediaByProductID(id)
+10 -9
View File
@@ -3,16 +3,17 @@ package models
import "time"
type Product struct {
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"column:name" binding:"required"`
Category string `json:"category" gorm:"column:category" binding:"required"`
Description string `json:"description" gorm:"column:description"`
Stock float64 `json:"stock" gorm:"column:stock"`
Unit string `json:"unit" gorm:"column:unit"`
Prices []ProductPrice `json:"prices" gorm:"foreignKey:ProductID"`
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"column:name" binding:"required"`
Category string `json:"category" gorm:"column:category" binding:"required"`
Description string `json:"description" gorm:"column:description"`
Stock float64 `json:"stock" gorm:"column:stock"`
Unit string `json:"unit" gorm:"column:unit"`
ComingSoon bool `json:"coming_soon" gorm:"column:coming_soon;default:false"`
Prices []ProductPrice `json:"prices" gorm:"foreignKey:ProductID"`
Media []Media `json:"media,omitempty" gorm:"foreignKey:ProductID"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
func (Product) TableName() string { return "products" }
+18 -30
View File
@@ -10,7 +10,6 @@ import (
"math"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -266,44 +265,37 @@ func CalculateETA(distanceKm float64) int {
// CalculateETAWithTomTom calcule l'ETA via TomTom API (précis avec trafic réel)
// Retourne (etaMinutes, distanceKm, error)
func CalculateETAWithTomTom(from, to Coordinates) (int, float64, error) {
apiKey := os.Getenv("TOMTOM_API_KEY")
if apiKey == "" {
// Fallback sur calcul local si pas de clé API
distance := CalculateDistance(from, to)
return CalculateETA(distance), distance, nil
}
// API TomTom Routing: Calculate Route avec trafic
u := &url.URL{
Scheme: "https",
Host: "api.tomtom.com",
Path: fmt.Sprintf("/routing/1/calculateRoute/%f,%f:%f,%f/json", from.Latitude, from.Longitude, to.Latitude, to.Longitude),
}
q := url.Values{}
q.Set("key", apiKey)
q.Set("traffic", "true")
q.Set("travelMode", "car")
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
if len(tomTomKeys.keys) == 0 {
distance := CalculateDistance(from, to)
return CalculateETA(distance), distance, nil
}
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Do(req)
buildReq := func(key string) (*http.Request, error) {
u := &url.URL{
Scheme: "https",
Host: "api.tomtom.com",
Path: fmt.Sprintf("/routing/1/calculateRoute/%f,%f:%f,%f/json", from.Latitude, from.Longitude, to.Latitude, to.Longitude),
}
q := url.Values{}
q.Set("key", key)
q.Set("traffic", "true")
q.Set("travelMode", "car")
u.RawQuery = q.Encode()
return http.NewRequest(http.MethodGet, u.String(), nil)
}
resp, err := tomTomKeys.Do(client, buildReq)
if err != nil {
// Fallback sur calcul local en cas d'erreur réseau
distance := CalculateDistance(from, to)
eta := CalculateETA(distance)
fmt.Printf("⚠️ TomTom timeout, fallback: %.2f km -> %d min\n", distance, eta)
fmt.Printf("⚠️ TomTom indisponible, fallback: %.2f km -> %d min (%v)\n", distance, eta, err)
return eta, distance, nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
// Fallback sur calcul local en cas d'erreur API
distance := CalculateDistance(from, to)
eta := CalculateETA(distance)
fmt.Printf("⚠️ TomTom API error %d, fallback: %.2f km -> %d min\n", resp.StatusCode, distance, eta)
@@ -328,18 +320,14 @@ func CalculateETAWithTomTom(from, to Coordinates) (int, float64, error) {
}
summary := routeResponse.Routes[0].Summary
// Calculer ETA en minutes (arrondi supérieur)
etaMinutes := (summary.TravelTimeInSeconds + 59) / 60
distanceKm := float64(summary.LengthInMeters) / 1000.0
// Appliquer minimum
if etaMinutes < MinETA {
etaMinutes = MinETA
}
fmt.Printf("🛣️ TomTom: %.2f km -> %d min (trafic réel)\n", distanceKm, etaMinutes)
return etaMinutes, distanceKm, nil
}
+17 -27
View File
@@ -12,36 +12,29 @@ import (
"log"
"net/http"
"net/url"
"os"
"time"
)
func GetETAWithTraffic(from, to Coordinates) (etaMinutes int, distanceKm float64, err error) {
apiKey := os.Getenv("TOMTOM_API_KEY")
if apiKey == "" {
return 0, 0, fmt.Errorf("TOMTOM_API_KEY non configurée")
}
u := &url.URL{
Scheme: "https",
Host: "api.tomtom.com",
Path: fmt.Sprintf("/routing/1/calculateRoute/%f,%f:%f,%f/json", from.Latitude, from.Longitude, to.Latitude, to.Longitude),
}
q := url.Values{}
q.Set("key", apiKey)
q.Set("traffic", "true")
q.Set("travelMode", "car")
u.RawQuery = q.Encode()
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return 0, 0, fmt.Errorf("erreur construction requête TomTom: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
buildReq := func(key string) (*http.Request, error) {
u := &url.URL{
Scheme: "https",
Host: "api.tomtom.com",
Path: fmt.Sprintf("/routing/1/calculateRoute/%f,%f:%f,%f/json", from.Latitude, from.Longitude, to.Latitude, to.Longitude),
}
q := url.Values{}
q.Set("key", key)
q.Set("traffic", "true")
q.Set("travelMode", "car")
u.RawQuery = q.Encode()
return http.NewRequest(http.MethodGet, u.String(), nil)
}
resp, err := tomTomKeys.Do(client, buildReq)
if err != nil {
return 0, 0, fmt.Errorf("erreur requête TomTom: %w", err)
return 0, 0, fmt.Errorf("erreur TomTom: %w", err)
}
defer resp.Body.Close()
@@ -65,12 +58,9 @@ func GetETAWithTraffic(from, to Coordinates) (etaMinutes int, distanceKm float64
}
summary := routeResponse.Routes[0].Summary
// Calculer ETA en minutes (arrondi supérieur)
etaMinutes = (summary.TravelTimeInSeconds + 59) / 60
distanceKm = float64(summary.LengthInMeters) / 1000.0
log.Printf("🛣️ TomTom Routing: %.2f km → %d min (trafic inclus)", distanceKm, etaMinutes)
return etaMinutes, distanceKm, nil
}
+96
View File
@@ -0,0 +1,96 @@
package services
import (
"fmt"
"io"
"log"
"net/http"
"os"
"sync/atomic"
)
type tomTomKeyManager struct {
keys []string
current atomic.Int32
}
var tomTomKeys = initTomTomKeyManager()
func initTomTomKeyManager() *tomTomKeyManager {
m := &tomTomKeyManager{}
seen := map[string]bool{}
candidates := []string{
os.Getenv("TOMTOM_API_KEY"),
os.Getenv("TOMTOM_API_KEY_1"),
os.Getenv("TOMTOM_API_KEY_2"),
os.Getenv("TOMTOM_API_KEY_3"),
}
for _, k := range candidates {
if k != "" && !seen[k] {
seen[k] = true
m.keys = append(m.keys, k)
}
}
log.Printf("🔑 [TOMTOM] %d clé(s) API configurée(s)", len(m.keys))
return m
}
// currentKey retourne la clé active et son index.
func (m *tomTomKeyManager) currentKey() (string, int) {
n := len(m.keys)
if n == 0 {
return "", -1
}
idx := int(m.current.Load()) % n
return m.keys[idx], idx
}
// rotate passe à la clé suivante.
func (m *tomTomKeyManager) rotate(fromIdx int) {
n := len(m.keys)
if n <= 1 {
return
}
next := int32((fromIdx + 1) % n)
m.current.CompareAndSwap(int32(fromIdx), next)
log.Printf("🔄 [TOMTOM] Rotation clé %d → clé %d (quota atteint)", fromIdx+1, next+1)
}
// Do exécute la requête en rotant automatiquement sur 403/429.
// buildReq doit construire une nouvelle *http.Request pour la clé donnée.
func (m *tomTomKeyManager) Do(client *http.Client, buildReq func(key string) (*http.Request, error)) (*http.Response, error) {
n := len(m.keys)
if n == 0 {
return nil, fmt.Errorf("aucune clé TomTom configurée (TOMTOM_API_KEY / TOMTOM_API_KEY_1..3)")
}
_, startIdx := m.currentKey()
for attempt := 0; attempt < n; attempt++ {
idx := (startIdx + attempt) % n
key := m.keys[idx]
req, err := buildReq(key)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
m.rotate(idx)
continue
}
return resp, nil
}
return nil, fmt.Errorf("toutes les clés TomTom ont atteint leur quota (%d clé(s) testée(s))", n)
}