Add video download failure reporting feature

- Introduced a new API endpoint `POST /api/v1/video/remove_watermark/report_failure` for reporting download failures.
- Added a new database table `video_download_failures` to store details about failed downloads, including domain, URL, error message, and reporting metadata.
- Updated the video handler to process failure reports and save them to the database.
- Enhanced documentation to include details about the new reporting feature and its usage.
This commit is contained in:
nepiedg
2026-01-03 23:50:30 +00:00
parent 1ad775be63
commit 49b709df9f
9 changed files with 168 additions and 5 deletions
@@ -3,6 +3,9 @@ package handler
import (
"errors"
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -25,6 +28,14 @@ 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"`
}
func (h *VideoHandler) RemoveWatermark(c *gin.Context) {
var req removeWatermarkRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -84,3 +95,52 @@ func (h *VideoHandler) UnlockQuota(c *gin.Context) {
"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,
}))
}
@@ -50,3 +50,25 @@ func (VideoParseUnlock) TableName() string {
func (VideoParseUnlock) TableComment() string {
return "短视频去水印-每日广告解锁"
}
type VideoDownloadFailure struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `gorm:"comment:创建时间" json:"created_at"`
UpdatedAt time.Time `gorm:"comment:更新时间" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
Domain string `gorm:"size:255;index;comment:来源域名" json:"domain"`
FailedURL string `gorm:"size:1000;comment:下载失败的URL" json:"failed_url"`
ErrorMessage string `gorm:"type:text;comment:失败原因" json:"error_message"`
ReportedAt time.Time `gorm:"index;comment:失败发生时间" json:"reported_at"`
UserAgent string `gorm:"size:255;comment:上报端UA" json:"user_agent"`
ClientIP string `gorm:"size:64;comment:上报IP" json:"client_ip"`
}
func (VideoDownloadFailure) TableName() string {
return "video_download_failures"
}
func (VideoDownloadFailure) TableComment() string {
return "短视频去水印-下载失败上报"
}
@@ -19,7 +19,7 @@ import (
rmmodel "wx_service/internal/remove_watermark/model"
)
const removeWatermarkEndpoint = "https://api.23bt.cn/api/d1w/index"
const removeWatermarkEndpoint = "https://api.23bt.cn/api/dsp/index"
var (
// 从用户输入的文本里“抓取 URL”的简单正则(找到第一个 http/https 链接)
@@ -56,6 +56,15 @@ func (e *ThirdPartyError) Error() string {
return fmt.Sprintf("third-party api error: status=%d message=%s", e.StatusCode, e.Message)
}
type DownloadFailureReport struct {
Domain string
FailedURL string
ErrorMessage string
ReportedAt time.Time
UserAgent string
ClientIP string
}
func NewVideoService(db *gorm.DB, cfg config.ShortVideoConfig) (*VideoService, error) {
timeout := cfg.RequestTimeout
if timeout <= 0 {
@@ -253,3 +262,33 @@ func truncateString(input string, max int) string {
}
return input[:max]
}
func (s *VideoService) ReportDownloadFailure(ctx context.Context, report DownloadFailureReport) error {
if report.ReportedAt.IsZero() {
report.ReportedAt = time.Now()
}
if report.Domain == "" && report.FailedURL != "" {
if parsed, err := url.Parse(report.FailedURL); err == nil {
report.Domain = parsed.Host
}
}
entry := rmmodel.VideoDownloadFailure{
Domain: report.Domain,
FailedURL: report.FailedURL,
ErrorMessage: truncateString(report.ErrorMessage, 2000),
ReportedAt: report.ReportedAt,
UserAgent: truncateString(report.UserAgent, 255),
ClientIP: truncateString(report.ClientIP, 64),
}
if entry.FailedURL == "" {
return errors.New("failed_url is required")
}
if err := s.db.WithContext(ctx).Create(&entry).Error; err != nil {
return fmt.Errorf("save download failure report: %w", err)
}
return nil
}