diff --git a/cmd/serve.go b/cmd/serve.go index 2968cd6..ba2fd0f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -133,6 +133,23 @@ func executeBrand(ctx context.Context, taskID uint32, brandUID, brandName string if config.GlobalConfig.Media.Upload && IngestClient != nil { s.Media = media.New(IngestClient, "runway", config.GlobalConfig.Media.Concurrency) } + // 心跳:单场秀可能有几百张图,一抓就是几十分钟,期间进度数字不会变。 + // 没有它,后台会按「progress_at 超过 10 分钟没变」把正常抓取误判成卡死。 + stop := make(chan struct{}) + go func() { + t := time.NewTicker(60 * time.Second) + defer t.Stop() + for { + select { + case <-stop: + return + case <-t.C: + s.Heartbeat() + } + } + }() + defer close(stop) + shows = s.RunBrand(brandUID, brandName, maxShows) return shows, "" } diff --git a/internal/spider/vogue.go b/internal/spider/vogue.go index 99d48e0..abeab72 100644 --- a/internal/spider/vogue.go +++ b/internal/spider/vogue.go @@ -52,6 +52,8 @@ type VogueSpider struct { // 「3/25 场 · Fall 2020 Ready-to-Wear」。并发抓取,故用原子操作。 progressDone int32 progressTotal int32 + // progressText 最近一次上报的秀标题,心跳重发时复用它。 + progressText atomic.Value // string // Media 媒体机直传器(可为 nil:为 nil 时走旧路径,上报 URL 由后端下载)。 Media *media.Uploader // submitted 本次品牌执行中成功上报的场次数(服务化模式回报给后台用)。 @@ -345,6 +347,9 @@ func (s *VogueSpider) reportProgress(title string, done bool) { if s.Ingest == nil || s.TaskID == 0 { return } + if title != "" { + s.progressText.Store(title) + } n := int(atomic.LoadInt32(&s.progressDone)) if done { n = int(atomic.AddInt32(&s.progressDone, 1)) @@ -355,6 +360,16 @@ func (s *VogueSpider) reportProgress(title string, done bool) { } } +// Heartbeat 只刷新后台的心跳时间,进度数字不变。 +// +// 为什么需要:一场秀可能有几百张图,光下载就要几十分钟, +// 期间 done 一直是同一个数。没有心跳,后台的「10 分钟没推进」判定 +// 会把「正常但很慢」误报成「卡死」。 +func (s *VogueSpider) Heartbeat() { + title, _ := s.progressText.Load().(string) + s.reportProgress(title, false) +} + // getDetail 获取并解析单个发布会详情 func (s *VogueSpider) getDetail(brandUID string, info gjson.Result) { hed := info.Get("hed").String()