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

@@ -16,6 +16,8 @@ import (
"path/filepath"
"strings"
"time"
"gorm.io/gorm"
)
func (o *Operations) s3(c StorageConfig) (*minio.Client, error) {
@@ -137,6 +139,65 @@ func (o *Operations) StoreFile(path, mimeType string, public bool) (string, erro
}
return id, nil
}
// blobObjectID 内容寻址对象的稳定 ID:公开/私有加不同前缀,同字节也不共享可见性。
func blobObjectID(hash string, public bool) string {
if public {
return "pub_" + hash
}
return "prv_" + hash
}
// StoreFileHashed 按内容 hash 幂等上传:同一 (hash, public) 只存一份,重复上传跳过传输。
// 本地后端返回空 ID(文件留在本地 blob 目录,由调用方做引用计数)。
func (o *Operations) StoreFileHashed(path, mimeType string, public bool, hash string) (string, error) {
v, version, e := o.read(o.db, "storage")
if e != nil {
return "", errors.New("读取存储配置失败")
}
c := *v.(*StorageConfig)
if c.Backend == "local" {
return "", nil
}
id := blobObjectID(hash, public)
// hash 相同字节必然相同:对象已存在时直接复用,省掉整次上传
var existing model.StoredObject
if e := o.db.First(&existing, "id = ?", id).Error; e == nil {
return id, nil
} else if !errors.Is(e, gorm.ErrRecordNotFound) {
return "", e
}
client, e := o.s3(c)
if e != nil {
return "", e
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
f, e := os.Open(path)
if e != nil {
return "", e
}
stat, e := f.Stat()
if e != nil {
_ = f.Close()
return "", e
}
key := c.Prefix + "objects/" + id
if _, e = client.PutObject(ctx, c.Bucket, key, f, stat.Size(), minio.PutObjectOptions{ContentType: mimeType, DisableMultipart: true}); e != nil {
_ = f.Close()
return "", errors.New("上传到 S3 失败,当前目标未自动切换")
}
_ = f.Close()
obj := model.StoredObject{ID: id, ConfigName: fmt.Sprintf("storage-%d", version), Key: key, MIME: mimeType, Public: public}
if e = o.db.Create(&obj).Error; e != nil {
if isUniqueConflict(e) { // 并发:另一请求已写入同内容对象,直接复用
return id, nil
}
_ = client.RemoveObject(ctx, c.Bucket, key, minio.RemoveObjectOptions{})
return "", errors.New("保存文件记录失败")
}
return id, nil
}
func (o *Operations) OpenObject(ctx context.Context, id string, requirePublic bool) (io.ReadCloser, string, error) {
var obj model.StoredObject
if e := o.db.First(&obj, "id = ?", id).Error; e != nil {