package adtracking import ( "crypto/rand" "encoding/hex" "errors" "net/http" "strconv" "strings" "hfb_sys/backend/internal/middleware" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" ) type Handler struct { service *Service } func NewHandler(service *Service) *Handler { return &Handler{service: service} } // Redirect 公开短链 302 智能重定向 func (h *Handler) Redirect(c *gin.Context) { code := strings.TrimSpace(c.Param("code")) if code == "" { c.Redirect(http.StatusFound, "https://hao858.com/") return } visitorID, err := c.Cookie("hfb_v_id") if err != nil || visitorID == "" { visitorID = generateVisitorID() isSecure := c.Request.TLS != nil || c.Request.Header.Get("X-Forwarded-Proto") == "https" c.SetSameSite(http.SameSiteLaxMode) c.SetCookie("hfb_v_id", visitorID, 31536000, "/", "", isSecure, true) } ip := c.ClientIP() ua := c.Request.UserAgent() referer := c.Request.Referer() targetURL, err := h.service.ResolveRedirect(c.Request.Context(), code, ip, ua, referer, visitorID) if err != nil { // 链接不存在或已停用时,优雅兜底跳转至官网首页,附带失效短码参数以备提示 c.Redirect(http.StatusFound, "https://hao858.com/?ref_invalid="+urlEscape(code)) return } c.Redirect(http.StatusFound, targetURL) } // List 推广链接列表查询(带多维度汇总数据) func (h *Handler) List(c *gin.Context) { var query ListTrackingLinksQuery if err := c.ShouldBindQuery(&query); err != nil { response.BadRequest(c, "查询参数有误") return } baseURL := getRequestBaseURL(c) items, total, err := h.service.ListLinks(c.Request.Context(), query, baseURL) if err != nil { response.InternalServerError(c, "获取推广链接列表失败") return } response.OK(c, gin.H{ "items": items, "total": total, }) } // Overview 推广大盘统计汇总 func (h *Handler) Overview(c *gin.Context) { stats, err := h.service.GetOverview(c.Request.Context()) if err != nil { response.InternalServerError(c, "获取推广统计数据失败") return } response.OK(c, stats) } // Create 创建推广链接 func (h *Handler) Create(c *gin.Context) { var req CreateTrackingLinkRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请求参数不正确: "+err.Error()) return } adminID, _ := currentAdminID(c) item, err := h.service.CreateLink(c.Request.Context(), adminID, req) if err != nil { writeAdTrackingError(c, err) return } response.Created(c, item) } // Update 更新推广链接 func (h *Handler) Update(c *gin.Context) { id, ok := parseID(c) if !ok { return } var req UpdateTrackingLinkRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请求参数不正确: "+err.Error()) return } if err := h.service.UpdateLink(c.Request.Context(), id, req); err != nil { writeAdTrackingError(c, err) return } response.OK(c, gin.H{"updated": true}) } // UpdateStatus 启停推广链接 func (h *Handler) UpdateStatus(c *gin.Context) { id, ok := parseID(c) if !ok { return } var req UpdateLinkStatusRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "状态参数不合法") return } if err := h.service.UpdateStatus(c.Request.Context(), id, req.Status); err != nil { writeAdTrackingError(c, err) return } response.OK(c, gin.H{"updated": true}) } func parseID(c *gin.Context) (uint64, bool) { val := c.Param("id") id, err := strconv.ParseUint(val, 10, 64) if err != nil || id == 0 { response.BadRequest(c, "无效的ID") return 0, false } return id, true } func currentAdminID(c *gin.Context) (uint64, bool) { val, exists := c.Get(middleware.ContextAdminID) if !exists { return 0, false } id, ok := val.(uint64) if !ok { return 0, false } return id, true } func generateVisitorID() string { b := make([]byte, 16) _, _ = rand.Read(b) return hex.EncodeToString(b) } func urlEscape(s string) string { return strings.ReplaceAll(s, " ", "%20") } func getRequestBaseURL(c *gin.Context) string { scheme := "https" if proto := c.Request.Header.Get("X-Forwarded-Proto"); proto != "" { scheme = proto } else if c.Request.TLS == nil { scheme = "http" } host := c.Request.Host if host == "" { return "https://hao858.com" } // 如果是本地开发环境端口,保留域名 return scheme + "://" + host } func writeAdTrackingError(c *gin.Context, err error) { response.RecordError(c, err) switch { case errors.Is(err, ErrLinkNotFound): response.Error(c, http.StatusNotFound, "not_found", err.Error()) case errors.Is(err, ErrCodeAlreadyExists): response.Error(c, http.StatusConflict, "already_exists", err.Error()) case errors.Is(err, ErrInvalidCodeFormat), errors.Is(err, ErrReservedCode), errors.Is(err, ErrLinkDisabled), errors.Is(err, ErrDisallowedTargetDomain): response.BadRequest(c, err.Error()) default: response.InternalServerError(c, err.Error()) } }