质量优化

This commit is contained in:
ryan
2026-06-11 15:36:46 +08:00
parent 6b93320404
commit 533f783268
7 changed files with 70 additions and 57 deletions
+6 -4
View File
@@ -16,6 +16,8 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const maskedConfigValue = "******"
// CreateSystemConfigRequest 创建系统配置请求 // CreateSystemConfigRequest 创建系统配置请求
type CreateSystemConfigRequest struct { type CreateSystemConfigRequest struct {
Key string `json:"key" binding:"required,max=64"` Key string `json:"key" binding:"required,max=64"`
@@ -117,7 +119,7 @@ func ListSystemConfigs(c *gin.Context) {
for i := range configs { for i := range configs {
if configs[i].Key == model.ConfigKeySMTPPassword && configs[i].Value != "" { if configs[i].Key == model.ConfigKeySMTPPassword && configs[i].Value != "" {
configs[i].Value = "******" configs[i].Value = maskedConfigValue
} }
} }
@@ -149,7 +151,7 @@ func GetSystemConfig(c *gin.Context) {
} }
if config.Key == model.ConfigKeySMTPPassword && config.Value != "" { if config.Key == model.ConfigKeySMTPPassword && config.Value != "" {
config.Value = "******" config.Value = maskedConfigValue
} }
c.JSON(http.StatusOK, util.OK(config)) c.JSON(http.StatusOK, util.OK(config))
@@ -200,7 +202,7 @@ func UpdateSystemConfig(c *gin.Context) {
updates["visibility"] = *req.Visibility updates["visibility"] = *req.Visibility
config.Visibility = *req.Visibility config.Visibility = *req.Visibility
} }
if key != model.ConfigKeySMTPPassword || req.Value != "******" { if key != model.ConfigKeySMTPPassword || req.Value != maskedConfigValue {
updates["value"] = req.Value updates["value"] = req.Value
config.Value = req.Value config.Value = req.Value
} }
@@ -256,7 +258,7 @@ func TestSMTP(c *gin.Context) {
} }
password := req.SMTPPassword password := req.SMTPPassword
if password == "******" { if password == maskedConfigValue {
var sc model.SystemConfig var sc model.SystemConfig
if err := sc.GetByKey(c.Request.Context(), model.ConfigKeySMTPPassword); err == nil { if err := sc.GetByKey(c.Request.Context(), model.ConfigKeySMTPPassword); err == nil {
password = sc.Value password = sc.Value
+5
View File
@@ -8,4 +8,9 @@ const (
detectContentBytes = 512 // http.DetectContentType 需要的最小字节数 detectContentBytes = 512 // http.DetectContentType 需要的最小字节数
uploadDirPerm = 0755 // 上传目录权限 uploadDirPerm = 0755 // 上传目录权限
uploadFilePerm = 0644 // 上传文件权限 uploadFilePerm = 0644 // 上传文件权限
imageQualityLow = "low"
imageQualityMedium = "medium"
imageQualityHigh = "high"
imageQualityOrigin = "origin"
storageDriverLocal = "local"
) )
+5 -5
View File
@@ -91,7 +91,7 @@ func ServeUpload(c *gin.Context, upload *model.Upload) {
quality := normalizeImageQuality(c.Query("quality")) quality := normalizeImageQuality(c.Query("quality"))
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension)) isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
if quality == "origin" || !isImage { if quality == imageQualityOrigin || !isImage {
serveOriginal(c, upload) serveOriginal(c, upload)
return return
} }
@@ -142,16 +142,16 @@ func imageCompressionCacheKey(upload *model.Upload, quality string) string {
func normalizeImageQuality(quality string) string { func normalizeImageQuality(quality string) string {
switch strings.ToLower(quality) { switch strings.ToLower(quality) {
case "low", "medium", "high": case imageQualityLow, imageQualityMedium, imageQualityHigh:
return strings.ToLower(quality) return strings.ToLower(quality)
default: default:
return "origin" return imageQualityOrigin
} }
} }
// serveOriginal 原始文件的流式响应逻辑 // serveOriginal 原始文件的流式响应逻辑
func serveOriginal(c *gin.Context, upload *model.Upload) { func serveOriginal(c *gin.Context, upload *model.Upload) {
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) { if upload.StorageDriver == storageDriverLocal || (upload.StorageDriver == "" && !storage.IsEnabled()) {
c.File(upload.FilePath) c.File(upload.FilePath)
return return
} }
@@ -178,7 +178,7 @@ func serveOriginal(c *gin.Context, upload *model.Upload) {
// getOriginalFileBytes 获取原始文件所有字节 // getOriginalFileBytes 获取原始文件所有字节
func getOriginalFileBytes(ctx context.Context, upload *model.Upload) ([]byte, error) { func getOriginalFileBytes(ctx context.Context, upload *model.Upload) ([]byte, error) {
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) { if upload.StorageDriver == storageDriverLocal || (upload.StorageDriver == "" && !storage.IsEnabled()) {
return os.ReadFile(upload.FilePath) return os.ReadFile(upload.FilePath)
} }
+5 -5
View File
@@ -292,7 +292,7 @@ func TestImageCompression(t *testing.T) {
t.Errorf("expected Content-Type image/webp, got %s", w.Header().Get("Content-Type")) t.Errorf("expected Content-Type image/webp, got %s", w.Header().Get("Content-Type"))
} }
cacheKey := imageCompressionCacheKey(&uploadRecord, "medium") cacheKey := imageCompressionCacheKey(&uploadRecord, imageQualityMedium)
cachedBytes, err := cache.Get(cacheKey) cachedBytes, err := cache.Get(cacheKey)
if err != nil { if err != nil {
t.Fatalf("disk cache Get(%q) returned error: %v", cacheKey, err) t.Fatalf("disk cache Get(%q) returned error: %v", cacheKey, err)
@@ -343,11 +343,11 @@ func TestNormalizeImageQuality(t *testing.T) {
quality string quality string
want string want string
}{ }{
{name: "low", quality: "low", want: "low"}, {name: imageQualityLow, quality: imageQualityLow, want: imageQualityLow},
{name: "medium", quality: "medium", want: "medium"}, {name: imageQualityMedium, quality: imageQualityMedium, want: imageQualityMedium},
{name: "high", quality: "high", want: "high"}, {name: imageQualityHigh, quality: imageQualityHigh, want: imageQualityHigh},
{name: "origin", quality: "origin", want: "origin"}, {name: "origin", quality: "origin", want: "origin"},
{name: "uppercase", quality: "LOW", want: "low"}, {name: "uppercase", quality: "LOW", want: imageQualityLow},
{name: "empty", quality: "", want: "origin"}, {name: "empty", quality: "", want: "origin"},
{name: "invalid", quality: "maximum", want: "origin"}, {name: "invalid", quality: "maximum", want: "origin"},
} }
+4 -4
View File
@@ -200,7 +200,7 @@ func DownloadFile(c *gin.Context) {
quality := normalizeImageQuality(c.Query("quality")) quality := normalizeImageQuality(c.Query("quality"))
isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension)) isImage := strings.HasPrefix(strings.ToLower(upload.MimeType), "image/") || isImageExtension(strings.ToLower(upload.Extension))
if quality != "origin" && isImage { if quality != imageQualityOrigin && isImage {
ext := filepath.Ext(fileName) ext := filepath.Ext(fileName)
if ext != "" { if ext != "" {
fileName = strings.TrimSuffix(fileName, ext) + ".webp" fileName = strings.TrimSuffix(fileName, ext) + ".webp"
@@ -290,7 +290,7 @@ func BatchDownloadFiles(c *gin.Context) {
// 打开底层文件数据源 // 打开底层文件数据源
var rc io.ReadCloser var rc io.ReadCloser
if upload.StorageDriver == "local" || (upload.StorageDriver == "" && !storage.IsEnabled()) { if upload.StorageDriver == storageDriverLocal || (upload.StorageDriver == "" && !storage.IsEnabled()) {
fileSrc, err := os.Open(upload.FilePath) fileSrc, err := os.Open(upload.FilePath)
if err != nil { if err != nil {
logger.ErrorF(ctx, "打包时读取本地文件失败: %v", err) logger.ErrorF(ctx, "打包时读取本地文件失败: %v", err)
@@ -516,7 +516,7 @@ func storeUploadFile(ctx context.Context, id uint64, ext, subPath string, size i
logger.ErrorF(ctx, "本地磁盘写入文件失败: %v", err) logger.ErrorF(ctx, "本地磁盘写入文件失败: %v", err)
return "", "", ErrSaveFileFailed return "", "", ErrSaveFileFailed
} }
return "local", localPath, "" return storageDriverLocal, localPath, ""
} }
// isImageExtension 判断文件扩展名是否属于常见图片格式 // isImageExtension 判断文件扩展名是否属于常见图片格式
@@ -556,7 +556,7 @@ func detectMimeType(buf *bytes.Buffer, header *multipart.FileHeader, size int64)
// saveUploadRecord 保存上传记录到数据库,失败时清理本地垃圾文件 // saveUploadRecord 保存上传记录到数据库,失败时清理本地垃圾文件
func saveUploadRecord(ctx context.Context, upload *model.Upload, storageDriver, filePath string) string { func saveUploadRecord(ctx context.Context, upload *model.Upload, storageDriver, filePath string) string {
if err := db.DB(ctx).Create(upload).Error; err != nil { if err := db.DB(ctx).Create(upload).Error; err != nil {
if storageDriver == "local" { if storageDriver == storageDriverLocal {
_ = os.Remove(filePath) _ = os.Remove(filePath)
} }
return ErrSaveUploadRecordFailed return ErrSaveUploadRecordFailed
+3 -3
View File
@@ -54,11 +54,11 @@ func CompressImageToWebP(srcReader io.Reader, quality string) ([]byte, error) {
// Determine quality // Determine quality
var qualityScore float32 var qualityScore float32
switch strings.ToLower(quality) { switch strings.ToLower(quality) {
case "low": case imageQualityLow:
qualityScore = 60 qualityScore = 60
case "medium": case imageQualityMedium:
qualityScore = 75 qualityScore = 75
case "high", "": case imageQualityHigh, "":
qualityScore = 85 qualityScore = 85
default: default:
qualityScore = 85 qualityScore = 85
+42 -36
View File
@@ -17,6 +17,12 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const (
configTypeSystem = "system"
configValueTrue = "true"
configValueFalse = "false"
)
// SetupTestEnvironment initializes an in-memory SQLite DB, seeds default configurations, // SetupTestEnvironment initializes an in-memory SQLite DB, seeds default configurations,
// starts miniredis, and overrides the global db/Redis clients. It returns a cleanup function. // starts miniredis, and overrides the global db/Redis clients. It returns a cleanup function.
func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) { func SetupTestEnvironment(t *testing.T) (*gorm.DB, *miniredis.Miniredis, func()) {
@@ -79,37 +85,37 @@ func getSeedConfigsPart1() []model.SystemConfig {
{ {
Key: model.ConfigKeyUploadAllowedExtensions, Key: model.ConfigKeyUploadAllowedExtensions,
Value: "jpg,png,webp", Value: "jpg,png,webp",
Type: "system", Type: configTypeSystem,
Description: "允许上传的图片扩展名(逗号分隔)", Description: "允许上传的图片扩展名(逗号分隔)",
}, },
{ {
Key: model.ConfigKeySiteName, Key: model.ConfigKeySiteName,
Value: "Wavelet", Value: "Wavelet",
Type: "system", Type: configTypeSystem,
Description: "系统平台的展示名称", Description: "系统平台的展示名称",
}, },
{ {
Key: model.ConfigKeyPasswordLoginEnabled, Key: model.ConfigKeyPasswordLoginEnabled,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "是否允许使用账号密码登录", Description: "是否允许使用账号密码登录",
}, },
{ {
Key: model.ConfigKeyRegistrationEnabled, Key: model.ConfigKeyRegistrationEnabled,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "控制普通用户是否可以自主注册(true/false)", Description: "控制普通用户是否可以自主注册(true/false)",
}, },
{ {
Key: model.ConfigKeyPasswordRegisterEnabled, Key: model.ConfigKeyPasswordRegisterEnabled,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "是否允许通过密码创建本地账号", Description: "是否允许通过密码创建本地账号",
}, },
{ {
Key: model.ConfigKeyOIDCLoginEnabled, Key: model.ConfigKeyOIDCLoginEnabled,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "是否允许使用第三方 OIDC 认证源登录", Description: "是否允许使用第三方 OIDC 认证源登录",
}, },
{ {
@@ -120,44 +126,44 @@ func getSeedConfigsPart1() []model.SystemConfig {
}, },
{ {
Key: model.ConfigKeyCapLoginEnabled, Key: model.ConfigKeyCapLoginEnabled,
Value: "false", Value: configValueFalse,
Type: "system", Type: configTypeSystem,
Description: "是否启用登录人机验证(true/false)", Description: "是否启用登录人机验证(true/false)",
}, },
{ {
Key: model.ConfigKeyCapAutoSolve, Key: model.ConfigKeyCapAutoSolve,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发", Description: "打开页面后是否自动开始计算,关闭则需用户手动点击触发",
}, },
{ {
Key: model.ConfigKeyCapChallengeCount, Key: model.ConfigKeyCapChallengeCount,
Value: "1", Value: "1",
Type: "system", Type: configTypeSystem,
Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5", Description: "客户端需求解的 PoW 难题总数,默认 1,推荐 1~5",
}, },
{ {
Key: model.ConfigKeyCapChallengeSize, Key: model.ConfigKeyCapChallengeSize,
Value: "32", Value: "32",
Type: "system", Type: configTypeSystem,
Description: "人机验证盐值长度", Description: "人机验证盐值长度",
}, },
{ {
Key: model.ConfigKeyCapChallengeDifficulty, Key: model.ConfigKeyCapChallengeDifficulty,
Value: "4", Value: "4",
Type: "system", Type: configTypeSystem,
Description: "人机验证 PoW 难度(目标前缀长度)", Description: "人机验证 PoW 难度(目标前缀长度)",
}, },
{ {
Key: model.ConfigKeyCapChallengeTTL, Key: model.ConfigKeyCapChallengeTTL,
Value: "600", Value: "600",
Type: "system", Type: configTypeSystem,
Description: "人机验证难题有效时间(秒)", Description: "人机验证难题有效时间(秒)",
}, },
{ {
Key: model.ConfigKeyCapTokenTTL, Key: model.ConfigKeyCapTokenTTL,
Value: "1200", Value: "1200",
Type: "system", Type: configTypeSystem,
Description: "人机验证兑换凭证有效时间(秒)", Description: "人机验证兑换凭证有效时间(秒)",
}, },
} }
@@ -168,79 +174,79 @@ func getSeedConfigsPart2() []model.SystemConfig {
{ {
Key: model.ConfigKeyServerAddress, Key: model.ConfigKeyServerAddress,
Value: "", Value: "",
Type: "system", Type: configTypeSystem,
Description: "服务器地址(用于跨域源控制,不设定则允许任意源)", Description: "服务器地址(用于跨域源控制,不设定则允许任意源)",
}, },
{ {
Key: model.ConfigKeySMTPHost, Key: model.ConfigKeySMTPHost,
Value: "", Value: "",
Type: "system", Type: configTypeSystem,
Description: "SMTP 服务器地址(例如 smtp.example.com)", Description: "SMTP 服务器地址(例如 smtp.example.com)",
}, },
{ {
Key: model.ConfigKeySMTPPort, Key: model.ConfigKeySMTPPort,
Value: "587", Value: "587",
Type: "system", Type: configTypeSystem,
Description: "SMTP 端口(例如 587 或 465)", Description: "SMTP 端口(例如 587 或 465)",
}, },
{ {
Key: model.ConfigKeySMTPUsername, Key: model.ConfigKeySMTPUsername,
Value: "", Value: "",
Type: "system", Type: configTypeSystem,
Description: "SMTP 账户(如 sender@example.com)", Description: "SMTP 账户(如 sender@example.com)",
}, },
{ {
Key: model.ConfigKeySMTPPassword, Key: model.ConfigKeySMTPPassword,
Value: "", Value: "",
Type: "system", Type: configTypeSystem,
Description: "SMTP 访问凭证(授权码/密码)", Description: "SMTP 访问凭证(授权码/密码)",
}, },
{ {
Key: model.ConfigKeyEmailLoginVerificationEnabled, Key: model.ConfigKeyEmailLoginVerificationEnabled,
Value: "false", Value: configValueFalse,
Type: "system", Type: configTypeSystem,
Description: "是否开启邮箱登录验证(true/false)", Description: "是否开启邮箱登录验证(true/false)",
}, },
{ {
Key: model.ConfigKeyEmailRegisterVerificationEnabled, Key: model.ConfigKeyEmailRegisterVerificationEnabled,
Value: "false", Value: configValueFalse,
Type: "system", Type: configTypeSystem,
Description: "是否开启邮箱注册验证(true/false)", Description: "是否开启邮箱注册验证(true/false)",
}, },
{ {
Key: model.ConfigKeyMenuDisplayConfig, Key: model.ConfigKeyMenuDisplayConfig,
Value: "{}", Value: "{}",
Type: "system", Type: configTypeSystem,
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})", Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
}, },
{ {
Key: model.ConfigKeySearchEngineIndexingEnabled, Key: model.ConfigKeySearchEngineIndexingEnabled,
Value: "false", Value: configValueFalse,
Type: "system", Type: configTypeSystem,
Description: "是否允许搜索引擎检索", Description: "是否允许搜索引擎检索",
}, },
{ {
Key: model.ConfigKeyFileAccessWhitelist, Key: model.ConfigKeyFileAccessWhitelist,
Value: `["avatar"]`, Value: `["avatar"]`,
Type: "system", Type: configTypeSystem,
Description: "免登录访问的文件业务类型白名单", Description: "免登录访问的文件业务类型白名单",
}, },
{ {
Key: model.ConfigKeyDiskCacheMaxSizeMB, Key: model.ConfigKeyDiskCacheMaxSizeMB,
Value: "100", Value: "100",
Type: "system", Type: configTypeSystem,
Description: "磁盘缓存最大空间大小 (MB)", Description: "磁盘缓存最大空间大小 (MB)",
}, },
{ {
Key: model.ConfigKeyDiskCacheTTLMinutes, Key: model.ConfigKeyDiskCacheTTLMinutes,
Value: "60", Value: "60",
Type: "system", Type: configTypeSystem,
Description: "磁盘缓存默认有效期 (分钟)", Description: "磁盘缓存默认有效期 (分钟)",
}, },
{ {
Key: model.ConfigKeyDiskCacheLRUEnabled, Key: model.ConfigKeyDiskCacheLRUEnabled,
Value: "true", Value: configValueTrue,
Type: "system", Type: configTypeSystem,
Description: "是否启用 LRU 淘汰机制", Description: "是否启用 LRU 淘汰机制",
}, },
} }