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、字节数、回滚清单、向量占位
84 lines
2.9 KiB
Go
84 lines
2.9 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|