Remove InfoGenie-frontend and Go .env from version control; add .env.example templates; ignore .claude local settings. Admin UI reads site gate from env only. Note: rotate secrets if repo history was ever public. Made-with: Cursor
116 lines
3.4 KiB
Go
116 lines
3.4 KiB
Go
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})
|
||
}
|