package handler import ( "crypto/sha256" "encoding/hex" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" "affiliate_dash/internal/pkg/response" "affiliate_dash/internal/service" "github.com/gin-gonic/gin" ) // DeliveryHandler 提供用户侧公开发货页面使用的接口。 type DeliveryHandler struct { deliverySvc *service.DeliveryService } func NewDeliveryHandler(deliverySvc *service.DeliveryService) *DeliveryHandler { return &DeliveryHandler{deliverySvc: deliverySvc} } func (h *DeliveryHandler) GetOrder(c *gin.Context) { order, err := h.deliverySvc.GetOrder(c.Param("order_no"), parseDeliveryAuth(c)) if err != nil { writeDeliveryError(c, err) return } response.OK(c, order) } type deliveryBindReq struct { GameAccount string `json:"game_account" binding:"required"` } func (h *DeliveryHandler) Bind(c *gin.Context) { var req deliveryBindReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请输入玩家编号") return } result, err := h.deliverySvc.Bind(c.Param("order_no"), req.GameAccount, parseDeliveryAuth(c)) if err != nil { writeDeliveryError(c, err) return } response.OK(c, result) } type deliverySubmitReq struct { GameAccount string `json:"game_account" binding:"required"` BindUUID string `json:"bind_uuid" binding:"required"` } func (h *DeliveryHandler) Submit(c *gin.Context) { var req deliverySubmitReq if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "玩家编号和绑定凭证不能为空") return } result, err := h.deliverySvc.Submit(c.Param("order_no"), req.GameAccount, req.BindUUID, parseDeliveryAuth(c)) if err != nil { writeDeliveryError(c, err) return } response.OK(c, result) } // ImageProxy 代理图片并支持本地持久化缓存,杜绝跨域污染和远端加载失败。 func (h *DeliveryHandler) ImageProxy(c *gin.Context) { rawURL := strings.TrimSpace(c.Query("url")) if rawURL == "" || (!strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://")) { response.BadRequest(c, "无效的图片地址") return } // 1. 优先检查本地官方商品图片缓存 for _, base := range []string{"data/uploads/products", "../data/uploads/products", "uploads/products"} { if strings.Contains(rawURL, "/products/") { parts := strings.Split(rawURL, "/products/") if len(parts) > 1 { sub := strings.Split(parts[1], "/") if len(sub) > 0 && sub[0] != "" { localPath := filepath.Join(base, sub[0]+".webp") if info, err := os.Stat(localPath); err == nil && info.Size() > 0 { c.Header("Cache-Control", "public, max-age=2592000, immutable") c.File(localPath) return } } } } // 按 URL SHA256 哈希查本地缓存 hash := sha256.Sum256([]byte(rawURL)) hashPath := filepath.Join(base, hex.EncodeToString(hash[:])+".webp") if info, err := os.Stat(hashPath); err == nil && info.Size() > 0 { c.Header("Cache-Control", "public, max-age=2592000, immutable") c.File(hashPath) return } } // 2. 本地无缓存时请求远端 req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, rawURL, nil) if err != nil { response.BadRequest(c, "构建请求失败") return } client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { response.Fail(c, http.StatusBadGateway, http.StatusBadGateway, "图片下载失败") return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { response.Fail(c, resp.StatusCode, resp.StatusCode, "远端图片响应异常") return } data, err := io.ReadAll(resp.Body) if err != nil { response.Fail(c, http.StatusBadGateway, http.StatusBadGateway, "读取图片内容失败") return } // 3. 异步/同步回写到本地磁盘缓存 cacheDir := "data/uploads/products" _ = os.MkdirAll(cacheDir, 0755) if strings.Contains(rawURL, "/products/") { parts := strings.Split(rawURL, "/products/") if len(parts) > 1 { sub := strings.Split(parts[1], "/") if len(sub) > 0 && sub[0] != "" { _ = os.WriteFile(filepath.Join(cacheDir, sub[0]+".webp"), data, 0644) } } } hash := sha256.Sum256([]byte(rawURL)) _ = os.WriteFile(filepath.Join(cacheDir, hex.EncodeToString(hash[:])+".webp"), data, 0644) contentType := resp.Header.Get("Content-Type") if contentType == "" { contentType = "image/webp" } c.Header("Cache-Control", "public, max-age=2592000, immutable") c.Data(http.StatusOK, contentType, data) } func parseDeliveryAuth(c *gin.Context) service.DeliveryLinkAuth { exp := int64(0) if value := strings.TrimSpace(c.Query("exp")); value != "" { if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { exp = parsed } } return service.DeliveryLinkAuth{ Exp: exp, Sign: strings.TrimSpace(c.Query("sign")), } } func writeDeliveryError(c *gin.Context, err error) { type statusCoder interface { HTTPStatus() int } if sc, ok := err.(statusCoder); ok { status := sc.HTTPStatus() response.Fail(c, status, status, strings.TrimSpace(err.Error())) return } msg := strings.TrimSpace(err.Error()) if msg == "订单不存在" { response.NotFound(c, msg) return } if msg == "链接参数缺失" || msg == "发货链接未生成" || msg == "发货链接已失效" || msg == "发货链接已过期" || msg == "发货链接签名无效" { response.Forbidden(c, msg) return } response.BadRequest(c, msg) }