feat: 媒体库 blob 去重存储与品牌/注册等前端调整

新增:
- blob_store 二进制去重存储 + blob_dedup 测试

其它:
- 后端 settings/auth/media_library/upload/operations 服务与 handler 调整
- 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
2026-10-03 02:38:11 +08:00
parent 8cfdfcbbf0
commit 46e0cdc0b3
32 changed files with 1242 additions and 228 deletions

View File

@@ -25,7 +25,7 @@ func newMediaLibraryService(t *testing.T) (*UploadService, string) {
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.Attachment{}, &model.User{}, &model.Post{}, &model.Comment{}); err != nil {
if err := db.AutoMigrate(&model.Attachment{}, &model.Blob{}, &model.User{}, &model.Post{}, &model.Comment{}); err != nil {
t.Fatalf("migrate: %v", err)
}
dir := t.TempDir()
@@ -315,6 +315,82 @@ func TestAdminMediaLibraryBookCategory(t *testing.T) {
}
}
func TestAdminMediaLibraryDedupAggregation(t *testing.T) {
s, dir := newMediaLibraryService(t)
writeMediaFile(t, dir, "images/dup.webp", []byte("fake-webp-bytes"))
mkUser := func(username, nickname string) model.User {
if err := s.db.Create(&model.User{Username: username, Nickname: nickname}).Error; err != nil {
t.Fatalf("create user: %v", err)
}
var u model.User
if err := s.db.Where("username = ?", username).First(&u).Error; err != nil {
t.Fatalf("load user: %v", err)
}
return u
}
alice := mkUser("alice", "爱丽丝")
bob := mkUser("bob", "阿Bob")
early := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
late := early.Add(2 * time.Hour)
// 同一张图被两人先后上传(内容寻址去重后共享同一文件/对象)
mkAtt := func(u model.User, url string, at time.Time) {
if err := s.db.Create(&model.Attachment{
UserID: u.ID, Kind: model.AttachmentKindImage,
URL: url, MIME: "image/webp",
Size: 100, Width: 10, Height: 10, CreatedAt: at,
}).Error; err != nil {
t.Fatalf("create attachment %s: %v", url, err)
}
}
mkAtt(alice, "/uploads/images/dup.webp", early)
mkAtt(bob, "/uploads/images/dup.webp", late)
mkAtt(bob, "/api/media/rdup", early)
mkAtt(alice, "/api/media/rdup", late)
items, _, err := s.AdminMediaLibrary()
if err != nil {
t.Fatalf("AdminMediaLibrary: %v", err)
}
countByURL := map[string]int{}
byURL := map[string]MediaLibraryItem{}
for _, it := range items {
countByURL[it.URL]++
byURL[it.URL] = it
}
// 本地去重文件:单个条目,上传者/时间取最早上传的爱丽丝,引用计数 2
local, ok := byURL["/uploads/images/dup.webp"]
if !ok {
t.Fatal("dedup local file missing")
}
if countByURL["/uploads/images/dup.webp"] != 1 {
t.Fatal("dedup local file should appear exactly once")
}
if local.Uploader != "爱丽丝" || local.RefCount != 2 {
t.Fatalf("local dedup meta wrong: %+v", local)
}
if local.UploadedAt == nil || !local.UploadedAt.Equal(early) {
t.Fatalf("local dedup time should be earliest upload, got %v", local.UploadedAt)
}
// 远程对象同 URL 两条记录:也只出一个条目,取最早的 bob
remote, ok := byURL["/api/media/rdup"]
if !ok {
t.Fatal("dedup remote object missing")
}
if countByURL["/api/media/rdup"] != 1 {
t.Fatal("dedup remote object should appear exactly once")
}
if remote.Uploader != "阿Bob" || remote.RefCount != 2 {
t.Fatalf("remote dedup meta wrong: %+v", remote)
}
}
func TestMediaThumb(t *testing.T) {
s, dir := newMediaLibraryService(t)