package handler import ( "errors" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/gin-gonic/gin" "wx_service/internal/middleware" "wx_service/internal/model" "wx_service/internal/remove_watermark/service" ) type VideoHandler struct { videoService *service.VideoService } func NewVideoHandler(videoService *service.VideoService) *VideoHandler { return &VideoHandler{ videoService: videoService, } } type removeWatermarkRequest struct { Content string `json:"content" binding:"required"` } type reportDownloadFailureRequest struct { Domain string `json:"domain"` FailedURL string `json:"failedUrl" binding:"required"` ErrorMessage string `json:"errorMessage"` Timestamp int64 `json:"timestamp"` UserAgent string `json:"userAgent"` } type proxyMediaRequest struct { URL string `form:"url" binding:"required"` } func (h *VideoHandler) RemoveWatermark(c *gin.Context) { var req removeWatermarkRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误")) return } user := middleware.MustCurrentUser(c) result, err := h.videoService.RemoveWatermark(c.Request.Context(), user, req.Content) if err != nil { switch { case errors.Is(err, service.ErrURLNotFound): c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请检查分享链接是否正确")) return case errors.Is(err, service.ErrDailyQuotaExceeded): c.JSON(http.StatusForbidden, model.Error(http.StatusForbidden, "今日免费次数已用完,观看广告后今天可无限制使用")) return case errors.Is(err, service.ErrShortVideoAPIKey): c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "服务暂不可用,请联系管理员")) return default: var thirdPartyErr *service.ThirdPartyError if errors.As(err, &thirdPartyErr) { c.JSON(http.StatusBadGateway, model.Error(http.StatusBadGateway, "解析服务异常,请稍后重试")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "去水印失败,请稍后重试")) return } } c.JSON(http.StatusOK, model.Success(gin.H{ "provider": result.Provider, "raw": result.Raw, "free_quota_used": result.FreeQuotaUsed, })) } func (h *VideoHandler) UnlockQuota(c *gin.Context) { user := middleware.MustCurrentUser(c) if err := h.videoService.UnlockForToday(c.Request.Context(), user); err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "解锁失败,请稍后重试")) return } c.JSON(http.StatusOK, model.Success(gin.H{ "unlocked": true, })) } func (h *VideoHandler) ReportDownloadFailure(c *gin.Context) { var req reportDownloadFailureRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误")) return } failedURL := strings.TrimSpace(req.FailedURL) if failedURL == "" { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "failedUrl 不能为空")) return } reportedAt := time.Now() if req.Timestamp > 0 { reportedAt = time.UnixMilli(req.Timestamp) } domain := strings.TrimSpace(req.Domain) if domain == "" { if parsed, err := url.Parse(failedURL); err == nil { domain = parsed.Host } } userAgent := strings.TrimSpace(req.UserAgent) if userAgent == "" { userAgent = c.Request.UserAgent() } report := service.DownloadFailureReport{ Domain: domain, FailedURL: failedURL, ErrorMessage: req.ErrorMessage, ReportedAt: reportedAt, UserAgent: userAgent, ClientIP: c.ClientIP(), } if err := h.videoService.ReportDownloadFailure(c.Request.Context(), report); err != nil { c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "上报失败,请稍后重试")) return } c.JSON(http.StatusOK, model.Success(gin.H{ "reported": true, })) } // ProxyMedia 代理媒体资源下载 // 该接口用于中转视频/图片等媒体资源,使小程序可以通过当前域名下载,避免微信域名限制 func (h *VideoHandler) ProxyMedia(c *gin.Context) { var req proxyMediaRequest if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "请求参数错误,缺少 url 参数")) return } targetURL := strings.TrimSpace(req.URL) if targetURL == "" { c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "url 参数不能为空")) return } // 调用 service 层进行代理 result, err := h.videoService.ProxyMedia(c.Request.Context(), targetURL) if err != nil { switch { case errors.Is(err, service.ErrProxyDisabled): c.JSON(http.StatusServiceUnavailable, model.Error(http.StatusServiceUnavailable, "代理服务未启用")) return case errors.Is(err, service.ErrProxyInvalidURL): c.JSON(http.StatusBadRequest, model.Error(http.StatusBadRequest, "无效的代理地址")) return case errors.Is(err, service.ErrProxyDomainBlocked): c.JSON(http.StatusForbidden, model.Error(http.StatusForbidden, "该域名不在允许列表中")) return case errors.Is(err, service.ErrProxyFileTooLarge): c.JSON(http.StatusRequestEntityTooLarge, model.Error(http.StatusRequestEntityTooLarge, "文件过大,超出限制")) return default: var thirdPartyErr *service.ThirdPartyError if errors.As(err, &thirdPartyErr) { c.JSON(http.StatusBadGateway, model.Error(http.StatusBadGateway, "上游服务返回错误")) return } c.JSON(http.StatusInternalServerError, model.Error(http.StatusInternalServerError, "代理请求失败")) return } } // 确保关闭响应体 defer result.Body.Close() // 设置响应头 c.Header("Content-Type", result.ContentType) if result.ContentLength > 0 { c.Header("Content-Length", strconv.FormatInt(result.ContentLength, 10)) } // 允许跨域(如果需要) c.Header("Access-Control-Allow-Origin", "*") // 设置缓存头,减少重复请求 c.Header("Cache-Control", "public, max-age=86400") // 流式输出响应体 c.Status(result.StatusCode) // 使用 io.Copy 进行流式传输,避免一次性加载到内存 _, _ = io.Copy(c.Writer, result.Body) }