package handler import ( "log" "net/http" "strings" "github.com/gin-gonic/gin" "infogenie-backend/internal/cache" "infogenie-backend/internal/database" "infogenie-backend/internal/model" ) var featureCardSections = map[string]struct{}{ "60sapi": {}, "smallgame": {}, "toolbox": {}, "aimodel": {}, } func normalizeFeatureSection(s string) (string, bool) { key := strings.ToLower(strings.TrimSpace(s)) if key == "" { return "", false } if _, ok := featureCardSections[key]; !ok { return "", false } return key, true } func sanitizeFeatureItemID(raw string) (string, bool) { id := strings.TrimSpace(raw) if id == "" || len(id) > 128 { return "", false } return id, true } // GetFeatureCardClicks 公开:按板块返回各功能 id 的点击次数(未出现过的 id 前端视为 0) func (h *SiteConfigHandler) GetFeatureCardClicks(c *gin.Context) { section, ok := normalizeFeatureSection(c.Query("section")) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_section"}) return } ctx := c.Request.Context() key := cache.KeySiteFeatureClicks(section) var cached struct { Section string `json:"section"` Counts map[string]uint64 `json:"counts"` } if hit, err := cache.GetJSON(ctx, key, &cached); err != nil { log.Printf("redis GET %s: %v", key, err) } else if hit { c.JSON(http.StatusOK, gin.H{"section": cached.Section, "counts": cached.Counts}) return } var rows []model.SiteFeatureCardClick if err := database.DB.Where("section = ?", section).Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } counts := make(map[string]uint64, len(rows)) for _, r := range rows { counts[r.ItemID] = r.ClickCount } payload := gin.H{"section": section, "counts": counts} if err := cache.SetJSON(ctx, key, payload, 0); err != nil { log.Printf("redis SET %s: %v", key, err) } c.JSON(http.StatusOK, payload) } type postFeatureCardClickBody struct { Section string `json:"section"` ItemID string `json:"item_id"` } // PostFeatureCardClickIncrement 公开:记录一次卡片点击并返回最新计数 func (h *SiteConfigHandler) PostFeatureCardClickIncrement(c *gin.Context) { var body postFeatureCardClickBody if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"}) return } section, ok := normalizeFeatureSection(body.Section) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_section"}) return } itemID, ok := sanitizeFeatureItemID(body.ItemID) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_item_id"}) return } const q = `INSERT INTO site_feature_card_clicks (section, item_id, click_count, updated_at) VALUES (?, ?, 1, NOW()) ON DUPLICATE KEY UPDATE click_count = click_count + 1, updated_at = NOW()` if err := database.DB.Exec(q, section, itemID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } var row model.SiteFeatureCardClick if err := database.DB.Where("section = ? AND item_id = ?", section, itemID).First(&row).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } cache.Delete(c.Request.Context(), cache.KeySiteFeatureClicks(section)) c.JSON(http.StatusOK, gin.H{"section": section, "item_id": itemID, "count": row.ClickCount}) }