From db3e3efab98fc43b4f6bf24a941858df26911374 Mon Sep 17 00:00:00 2001 From: toom1996 <23cm.cn@gmail.com> Date: Sun, 25 Oct 2026 18:37:20 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BB=BA=E8=A1=8C=E5=89=8D=E6=8C=89?= =?UTF-8?q?=E5=90=8C=E6=A0=87=E9=A2=98=E5=85=9C=E5=BA=95=EF=BC=8C=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=AD=A3=E8=8A=82=E7=A0=81=E6=BC=82=E7=A7=BB=E9=80=A0?= =?UTF-8?q?=E5=87=BA=E9=87=8D=E5=A4=8D=E5=9B=BE=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 男装季节码由 FWyy 改为 SSyy 后,仍按旧规则算码的实例(如本机残留的旧后端) 会把同一场秀当成新场次再建一行,今天已产生 7 组重复(Acne/Celine/AMBUSH 等)。 - 新增 IngestRepository.RunwayTitleExists(品牌 + 英文标题) - processRunway 在「实体键未命中、准备新建」时再查一次同标题: 命中则跳过并记 outcome=skipped_existing,note 写明「季节码漂移保护」 - 宁可跳过也不产生重复;标题含年份,正常不会误伤不同场次 --- internal/repository/ingest_repository.go | 20 ++++++++++++++++++++ internal/service/ingest_image_count_test.go | 6 ++++++ internal/service/ingest_service.go | 11 +++++++++++ 3 files changed, 37 insertions(+) diff --git a/internal/repository/ingest_repository.go b/internal/repository/ingest_repository.go index 2d3aa0a..b6669e3 100644 --- a/internal/repository/ingest_repository.go +++ b/internal/repository/ingest_repository.go @@ -33,6 +33,12 @@ type IngestRepository interface { // outcome 取 model.IngestOutcome* 常量,note 例如「新建 42 张图」「已存在(status=published)」。 // 二者是后台「品牌更新记录」的全部信息源:只有 status=done 看不出这次到底新建还是跳过。 MarkDone(ctx context.Context, id uint32, outcome, note string) error + // RunwayTitleExists 按(品牌 + 英文标题)查已有图集,返回其 id 与是否存在。 + // + // 用途:季节码推导规则变更后,同一场秀可能被算出不同的 season_code(如 Spring 男装 + // 曾是 FWyy、现在是 SSyy)。老实例按旧规则写入会建出「同标题不同季节码」的重复图集。 + // 建行前用它兜底,把「标题相同」视为同一场秀。 + RunwayTitleExists(ctx context.Context, brandID uint32, titleEn string) (uint32, bool, error) // ListByBrand 取某品牌的入库记录(按 id 倒序),供品牌详情页展示更新历史。 ListByBrand(ctx context.Context, brandID uint32, limit int) ([]BrandIngestRecord, error) // MarkFailed 标记任务失败并记录错误(attempts 自增)。 @@ -183,6 +189,20 @@ func (r *ingestRepository) MarkDone(ctx context.Context, id uint32, outcome, not }).Error } +// RunwayTitleExists 按(品牌 + 英文标题)查已有图集。 +func (r *ingestRepository) RunwayTitleExists(ctx context.Context, brandID uint32, titleEn string) (uint32, bool, error) { + if brandID == 0 || titleEn == "" { + return 0, false, nil + } + var id uint32 + if err := r.db.WithContext(ctx).Raw( + "SELECT id FROM brand_runways WHERE brand_id = ? AND title_en = ? AND is_deleted = 0 LIMIT 1", + brandID, titleEn).Scan(&id).Error; err != nil { + return 0, false, err + } + return id, id > 0, nil +} + // BrandIngestRecord 是品牌详情页需要的「一条更新记录」。 // // 只取必要列:payload 可能几十 KB(含全部 look 的 key),一次拉 20 条会传上兆, diff --git a/internal/service/ingest_image_count_test.go b/internal/service/ingest_image_count_test.go index 0dcf6d9..dbcc0cd 100644 --- a/internal/service/ingest_image_count_test.go +++ b/internal/service/ingest_image_count_test.go @@ -44,6 +44,12 @@ func (c *captureIngestRepo) MarkDone(ctx context.Context, id uint32, outcome, no return nil } +// RunwayTitleExists 捕获型仓储没有真实数据,一律视为「不存在」, +// 免得测试路径走进「同标题已存在 → 跳过」分支。 +func (c *captureIngestRepo) RunwayTitleExists(ctx context.Context, brandID uint32, titleEn string) (uint32, bool, error) { + return 0, false, nil +} + // ListByBrand 捕获型仓储不需要品牌维度查询。 func (c *captureIngestRepo) ListByBrand(ctx context.Context, brandID uint32, limit int) ([]repository.BrandIngestRecord, error) { return nil, nil diff --git a/internal/service/ingest_service.go b/internal/service/ingest_service.go index abcf366..bff42b0 100644 --- a/internal/service/ingest_service.go +++ b/internal/service/ingest_service.go @@ -349,6 +349,17 @@ func (s *IngestService) processRunway(ctx context.Context, job model.IngestJob, } } action := decideIngestAction(exist, existStatus) + // 季节码漂移保护:实体键按 season_code 去重,但规则改过(Spring 男装由 FWyy 改为 SSyy)。 + // 若还有实例按旧规则算码,就会把同一场秀当成新场次再建一行。 + // 这里在建行前用「同品牌同标题」再兜一次底,宁可跳过也不产生重复。 + if !exist && action != ingestSkip { + if dupID, found, terr := s.repo.RunwayTitleExists(ctx, brandID, p.TitleEn); terr == nil && found { + log.Printf("[ingest] job=%d runway 已存在同标题图集 id=%d(季节码可能漂移),跳过", job.ID, dupID) + _ = s.repo.MarkDone(ctx, job.ID, model.IngestOutcomeSkippedExisting, + fmt.Sprintf("已存在同标题图集(id=%d),跳过(季节码漂移保护)", dupID)) + return + } + } if action == ingestSkip { log.Printf("[ingest] job=%d runway 实体已存在(status=%s),跳过(回填 published_at)", job.ID, existStatus) // 临时回填:仅更新发布时间与缺失年份,不重下图片(图片已在库)。