Files
backend_v2/internal/service/ingest_keypath_test.go
toom1996 151e09ab30 feat: 抓取任务队列 + 媒体机直传 + 向量回填
crawl(品牌抓取任务队列)
- /admin/internal/crawl/tasks 领取 / :id/result 回报,spider 可常驻拉任务
- 后台品牌列表支持「加入抓取队列」,队列页可看状态与重试
- 租约超时的 running 自动回收为 queued,避免 worker 崩溃后任务卡死

media(媒体机直传)
- ingest 载荷新增 main_key/detail_keys/main_size/detail_sizes:
  有 key 就直接落库,跳过下载与上传;与 URL 形态可逐图混用
- 细节图按 Details/DetailKeys 两者较长者遍历(只给 key 时不再丢图)
- 媒体机上传的 key 不进失败回滚清单(内容寻址、可能与他人共享)
- /admin/internal/upload-url 签发 S4 PUT 预签名(spider 不持有 AK/SK)

embeddings(向量回填)
- GET /admin/internal/embeddings/pending 派发缺向量的图(含可直接下载的 url)
- POST /admin/internal/embeddings 回写向量并重算近邻;失败项计入
  image_embedding_failures,连续失败到上限后退出队列(坏图不占位)
- 近邻重算改为异步串行:同步会让 100 张的批次 POST 超时(实测 >60s)
- 新增 key 分支单测:混用 key/url、细节图只给 key、字节数、回滚清单、向量占位
2026-10-25 00:37:13 +08:00

84 lines
2.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"testing"
"fashionapi/internal/dto"
)
// TestFetchLookImagesUsesProvidedKeys 验证「媒体机直传」分支:
// payload 里带 main_key / detail_keys 时,worker 不应下载、不应上传,
// 而是直接把 key 当作要落库的存储地址,且 cover 取首张主图。
//
// 这条路径是媒体拆分的核心保证:一旦这里退化成下载 URL,
// 后端又会重新背上带宽瓶颈,而错误会非常隐蔽(只是变慢,不报错)。
func TestFetchLookImagesUsesProvidedKeys(t *testing.T) {
s := &IngestService{} // 不装配 uploader/vision:走 key 分支时它们都不该被用到
looks := []dto.RunwayLook{
{
MainKey: "runway/aaaa.jpg",
MainSize: 100,
DetailKeys: []string{"runway/bbbb.jpg"},
DetailSizes: []int64{200},
},
{
// 第二张混用旧形态:没有 key,带 URL(这里只验主图,不触发网络)
MainKey: "runway/cccc.jpg",
MainSize: 300,
},
}
cover, rows, vecs, keys, failed, _ := s.fetchLookImages(context.Background(), looks, "runway")
if failed {
t.Fatal("key 分支不应被判定为失败")
}
if cover != "runway/aaaa.jpg" {
t.Fatalf("cover 应取首张主图 key,实际 %q", cover)
}
if len(rows) != 3 {
t.Fatalf("应落 3 行(2 主图 + 1 细节图),实际 %d", len(rows))
}
if rows[0].Image != "runway/aaaa.jpg" || rows[0].IsDetail != 0 {
t.Fatalf("第 1 行应为 aaaa 主图,实际 %+v", rows[0])
}
if rows[1].Image != "runway/bbbb.jpg" || rows[1].IsDetail != 1 {
t.Fatalf("第 2 行应为 bbbb 细节图,实际 %+v", rows[1])
}
if rows[1].ImageSize != 200 || rows[0].ImageSize != 100 {
t.Fatalf("字节数应来自 payload,实际 %d / %d", rows[0].ImageSize, rows[1].ImageSize)
}
// 媒体机上传的 key 不进回滚清单(内容寻址、可能与他人共享)
if len(keys) != 0 {
t.Fatalf("key 分支不应产生回滚清单,实际 %v", keys)
}
if len(vecs) != len(rows) {
t.Fatalf("vecs 应与 rows 对齐,实际 %d vs %d", len(vecs), len(rows))
}
for i, v := range vecs {
if v != nil {
t.Fatalf("第 %d 张不应带向量(向量由 CLIP 回填),实际 %v", i, v)
}
}
if got := countMainImages(rows); got != 2 {
t.Fatalf("主图数应为 2,实际 %d", got)
}
}
// TestSha1FromKey 锁住「从内容寻址 key 反推 sha1」的行为:批次内去重依赖它,
// 若解析退化(例如带上目录前缀),同一张图会被重复落库。
func TestSha1FromKey(t *testing.T) {
cases := map[string]string{
"runway/4080435af23d8f80bbbcf9e6f55a5f1d00c49685.jpg": "4080435af23d8f80bbbcf9e6f55a5f1d00c49685",
"street/abc123.png": "abc123",
"plain-key": "plain-key",
}
for in, want := range cases {
if got := sha1FromKey(in); got != want {
t.Fatalf("sha1FromKey(%q) = %q,期望 %q", in, got, want)
}
}
}