This commit is contained in:
ryan
2026-07-13 15:17:45 +08:00
parent 08e61ce833
commit e0eb4e5975
334 changed files with 17212 additions and 12321 deletions
@@ -4,7 +4,8 @@
package auth_source
import ("bytes"
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -15,7 +16,8 @@ import ("bytes"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
func setupTestRouter(authUser *model.User) *gin.Engine {
r := testhelper.NewTestGinEngine()
+1 -1
View File
@@ -11,4 +11,4 @@ import (
func saveOrUpdateConfig(ctx context.Context, key, value string) error {
return repository.SaveOrUpdateSystemConfig(ctx, key, value)
}
}
+1 -1
View File
@@ -97,4 +97,4 @@ func ClearCache(c *gin.Context) {
return
}
c.JSON(http.StatusOK, response.OKNil())
}
}
+4 -2
View File
@@ -6,7 +6,8 @@
// overview information, and executing custom SQL queries.
package db_manage
import ("database/sql"
import (
"database/sql"
"fmt"
"math"
"net/http"
@@ -19,7 +20,8 @@ import ("database/sql"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
const (
binaryKB = 0
+1 -1
View File
@@ -427,4 +427,4 @@ func GetLogsAnalytics(c *gin.Context) {
Browsers: browserList,
TopUsers: topUsers,
}))
}
}
+4 -4
View File
@@ -49,10 +49,10 @@ func TestWebSocketCheckOrigin(t *testing.T) {
}
tests := []struct {
name string
origin string
host string
wantOK bool
name string
origin string
host string
wantOK bool
}{
{
name: "empty origin (non-browser clients)",
+1 -1
View File
@@ -275,4 +275,4 @@ func renderCustomPayload(template string, req CustomPushRequest) string {
result = strings.ReplaceAll(result, "$url", escapeJSONString(req.URL))
result = strings.ReplaceAll(result, "$to", escapeJSONString(req.To))
return result
}
}
@@ -35,4 +35,4 @@ func handleAdminLogin(ctx context.Context, event listener.AdminLoggedIn) {
"time": time.Now().Format("2006-01-02 15:04:05"),
}
push.DefaultTrigger.Trigger(ctx, AdminLogin, body)
}
}
@@ -14,4 +14,4 @@ import (
func Register() {
push.RegisterBuiltInEvent(AdminLogin)
listener.OnAdminLoggedIn(handleAdminLogin)
}
}
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"strings"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/task"
@@ -339,4 +340,4 @@ func resolveDynamicKeyword(target string, flatBody map[string]any) string {
}
}
return target
}
}
+1 -1
View File
@@ -410,4 +410,4 @@ func getEventInfo(req CreateEventRequest) (string, string, []byte, error) {
return "", "", nil, err
}
return req.EventKey, meta.Name, defaultTemplateBytes, nil
}
}
+1 -1
View File
@@ -289,4 +289,4 @@ func TestPush(c *gin.Context) {
return
}
c.JSON(http.StatusOK, response.OKNil())
}
}
+1 -1
View File
@@ -121,4 +121,4 @@ func extractUsername(data map[string]any) string {
}
}
return ""
}
}
+4 -2
View File
@@ -5,7 +5,8 @@
// Package status 提供系统状态查询接口
package status
import ("context"
import (
"context"
"fmt"
"log"
"math"
@@ -19,7 +20,8 @@ import ("context"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
// startTime 记录服务启动时间
var startTime = time.Now()
+4 -4
View File
@@ -7,9 +7,9 @@ package system_config
// 系统配置错误消息常量
const (
SystemConfigNotFound = "系统配置不存在"
ConfigKeyRequired = "配置键不能为空"
ConfigValueRequired = "配置值不能为空"
ConfigKeyExists = "配置键已存在"
SystemConfigNotFound = "系统配置不存在"
ConfigKeyRequired = "配置键不能为空"
ConfigValueRequired = "配置值不能为空"
ConfigKeyExists = "配置键已存在"
StorageDriverSwitchRequiresMigration = "存在存量文件,请通过存储迁移任务切换存储引擎"
)
+1 -1
View File
@@ -125,4 +125,4 @@ func resolveStorageMigrationTasksOnDirectDriverUpdate(
}).Error; err != nil {
logger.ErrorF(ctx, "自动更新迁移任务状态失败: %v", err)
}
}
}
+4 -2
View File
@@ -4,7 +4,8 @@
package task
import ("fmt"
import (
"fmt"
"net/http"
"strconv"
"strings"
@@ -18,7 +19,8 @@ import ("fmt"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
// ListTaskTypes 获取支持的任务类型列表
// @Summary 获取支持的任务类型
+4 -2
View File
@@ -4,7 +4,8 @@
package task
import ("bytes"
import (
"bytes"
"context"
"encoding/json"
"fmt"
@@ -25,7 +26,8 @@ import ("bytes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
func setupTaskTestEnvironment(t *testing.T) func() {
_, mr, cleanup := testhelper.SetupTestEnvironment(t)
+1 -1
View File
@@ -75,4 +75,4 @@ func deleteTemplate(ctx context.Context, key string) error {
return errors.New(SystemTemplateCannotDelete)
}
return repository.DeleteTemplate(ctx, &tmpl)
}
}
+1 -1
View File
@@ -173,4 +173,4 @@ func DeleteTemplate(c *gin.Context) {
}
c.JSON(http.StatusOK, response.OKNil())
}
}
+4 -2
View File
@@ -3,7 +3,8 @@
package template
import ("bytes"
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -14,7 +15,8 @@ import ("bytes"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
func setupTestRouter(authUser *model.User) *gin.Engine {
r := testhelper.NewTestGinEngine()
+4 -2
View File
@@ -3,14 +3,16 @@
package updater
import ("context"
import (
"context"
"net/http"
"time"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
// GetUpdateStatus 获取应用更新状态
// @Summary 获取应用更新状态
+9 -9
View File
@@ -6,15 +6,15 @@
package user
const (
userNotFound = "用户不存在"
cannotDisable = "不能禁用管理员用户"
cannotDelete = "不能删除管理员用户"
cannotDeleteSelf = "不能删除当前登录用户"
updateUserFailed = "更新用户状态失败"
deleteUserFailed = "删除用户失败"
usernameExists = "用户名已存在"
usernameRequired = "用户名不能为空"
passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
userNotFound = "用户不存在"
cannotDisable = "不能禁用管理员用户"
cannotDelete = "不能删除管理员用户"
cannotDeleteSelf = "不能删除当前登录用户"
updateUserFailed = "更新用户状态失败"
deleteUserFailed = "删除用户失败"
usernameExists = "用户名已存在"
usernameRequired = "用户名不能为空"
passwordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials
createUserFailed = "创建用户失败"
emailRequired = "邮箱不能为空"
emailExists = "邮箱已被注册"
+1 -1
View File
@@ -193,4 +193,4 @@ func GetDefaultManager() *Manager {
defaultManager = NewManager(secret, store)
})
return defaultManager
}
}
+1 -1
View File
@@ -159,4 +159,4 @@ func TestVerifyTokenConcurrentRace(t *testing.T) {
if got := success.Load(); got != 1 {
t.Fatalf("successful VerifyToken count = %d, want %d", got, 1)
}
}
}
+1 -1
View File
@@ -31,4 +31,4 @@ func VerifyMiddleware(mgr *Manager, scope string) gin.HandlerFunc {
c.Next()
}
}
}
+1 -1
View File
@@ -7,4 +7,4 @@ import "github.com/Rain-kl/Wavelet/internal/testhelper"
func init() {
testhelper.RegisterCleanup(ResetRuntimeSettingsForTest)
}
}
@@ -18,7 +18,6 @@ func TestListVisibleSystemConfigsUsesStoreCache(t *testing.T) {
defer cleanup()
ctx := context.Background()
repository.ResetSystemConfigRAMCacheForTest()
if err := repository.InvalidateVisibleSystemConfigsCache(ctx); err != nil {
t.Fatalf("InvalidateVisibleSystemConfigsCache() error = %v", err)
+4 -2
View File
@@ -4,7 +4,8 @@
package config
import ("encoding/json"
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
@@ -13,7 +14,8 @@ import ("encoding/json"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
func TestGetPublicConfigUsesVisibility(t *testing.T) {
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
@@ -19,7 +19,6 @@ func TestSystemConfigRAMCacheServesUntilInvalidated(t *testing.T) {
defer cleanup()
ctx := context.Background()
repository.ResetSystemConfigRAMCacheForTest()
if err := repository.InvalidateAllSystemConfigCaches(ctx); err != nil {
t.Fatalf("InvalidateAllSystemConfigCaches() error = %v", err)
@@ -72,7 +71,6 @@ func TestInvalidateSystemConfigCacheBroadcast(t *testing.T) {
defer cleanup()
ctx := context.Background()
repository.ResetSystemConfigRAMCacheForTest()
// Initially seed in cache
@@ -109,4 +107,4 @@ func TestInvalidateSystemConfigCacheBroadcast(t *testing.T) {
// Just a sanity check: we can publish a new manual update and verify subscription triggers
// which was done implicitly above.
}
}
}
+4 -2
View File
@@ -4,11 +4,13 @@
// Package custom provides custom business handlers
package custom
import ("net/http"
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
// Hello is a sample handler for custom business logic
// @Summary Sample Hello API
+4 -2
View File
@@ -5,11 +5,13 @@
// Package health 提供健康检查端点
package health
import ("net/http"
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
// Health 健康检查
// @Summary 健康检查
+7 -8
View File
@@ -11,13 +11,13 @@ import (
// Session 用户信息字段 Key
const (
UserNameKey = "username"
UserIDKey = "user_id"
UserObjKey = "user_obj"
TokenAuthKey = "token_auth" // 标记当前请求是否通过 Access Token 鉴权
TokenAdminKey = "token_admin" // Access Token 本身是否具有管理员权限
SessionTokenKey = "oauth_session_token" //nolint:gosec // false positive: this is a session key, not hardcoded credentials
PasswordHashKey = "password_hash"
UserNameKey = "username"
UserIDKey = "user_id"
UserObjKey = "user_obj"
TokenAuthKey = "token_auth" // 标记当前请求是否通过 Access Token 鉴权
TokenAdminKey = "token_admin" // Access Token 本身是否具有管理员权限
SessionTokenKey = "oauth_session_token" //nolint:gosec // false positive: this is a session key, not hardcoded credentials
PasswordHashKey = "password_hash"
)
// OAuth State 缓存 Key 格式与过期时间
@@ -54,4 +54,3 @@ func decodeOAuthStatePayload(value string) (oauthStatePayload, error) {
}
return payload, nil
}
+1 -1
View File
@@ -19,4 +19,4 @@ func GetFromContext[T any](c *gin.Context, key string) (T, bool) {
// SetToContext 设置值到 Gin 请求上下文。
func SetToContext[T any](c *gin.Context, key string, value T) {
c.Set(key, value)
}
}
+1 -1
View File
@@ -170,4 +170,4 @@ func Authorize(c *gin.Context) {
return
}
c.JSON(http.StatusOK, response.OK(OAuthAuthorizeResponse{AuthorizeURL: authorizeURL}))
}
}
@@ -62,4 +62,4 @@ func DeleteExternalAccount(c *gin.Context) {
return
}
c.JSON(http.StatusOK, response.OKNil())
}
}
+1 -1
View File
@@ -140,4 +140,4 @@ func parseCommaSeparatedWhitelist(value string) []string {
}
}
return whitelist
}
}
+1 -1
View File
@@ -284,4 +284,4 @@ func TestGetUploadByIDWorksWithRedisDisabled(t *testing.T) {
if gotCached.ID != upload.ID {
t.Fatal("expected RAM cache hit when redis is disabled")
}
}
}
+1 -1
View File
@@ -35,4 +35,4 @@ const (
ErrS3KeyStartsWithSlash = shared.ErrS3KeyStartsWithSlash
ErrS3KeyContainsNullBytes = shared.ErrS3KeyContainsNullBytes
ErrQueryUnusedUploadsFailed = shared.ErrQueryUnusedUploadsFailed
)
)
@@ -22,15 +22,15 @@ func TestGetDistinctUploadTypes(t *testing.T) {
dbConn.Create(&user)
customUpload := model.Upload{
ID: 9001,
UserID: user.ID,
FileName: "custom.txt",
FilePath: "uploads/custom.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Type: "custom_type_xyz",
Status: model.UploadStatusUsed,
ID: 9001,
UserID: user.ID,
FileName: "custom.txt",
FilePath: "uploads/custom.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Type: "custom_type_xyz",
Status: model.UploadStatusUsed,
}
dbConn.Create(&customUpload)
@@ -61,4 +61,4 @@ func TestGetDistinctUploadTypes(t *testing.T) {
if len(resp.Data) != 1 || resp.Data[0] != "custom_type_xyz" {
t.Errorf("expected only custom_type_xyz in types list, got: %v", resp.Data)
}
}
}
+1 -1
View File
@@ -83,4 +83,4 @@ func loadUploadStats(ctx context.Context) ([]model.UploadStat, error) {
func isRecordNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
}
+112 -112
View File
@@ -343,14 +343,14 @@ func TestDownloadFile(t *testing.T) {
// Seed upload records in DB
localUpload := model.Upload{
ID: 2001,
UserID: 1001,
FileName: "中文文件名.txt",
FilePath: "uploads/test_download.txt",
FileSize: 12,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2001,
UserID: 1001,
FileName: "中文文件名.txt",
FilePath: "uploads/test_download.txt",
FileSize: 12,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
}
// Create local file
@@ -410,44 +410,44 @@ func TestListFiles(t *testing.T) {
uploads := []model.Upload{
{
ID: 2101,
UserID: authUser.ID,
FileName: "first-report.txt",
FilePath: "uploads/first-report.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2101,
UserID: authUser.ID,
FileName: "first-report.txt",
FilePath: "uploads/first-report.txt",
FileSize: 10,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 2102,
UserID: authUser.ID,
FileName: "Second-Photo.PNG",
FilePath: "uploads/second-photo.png",
FileSize: 20,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
ID: 2102,
UserID: authUser.ID,
FileName: "Second-Photo.PNG",
FilePath: "uploads/second-photo.png",
FileSize: 20,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
},
{
ID: 2103,
UserID: authUser.ID,
FileName: "third-notes.md",
FilePath: "uploads/third-notes.md",
FileSize: 30,
MimeType: "text/markdown",
Extension: "md",
Status: model.UploadStatusUsed,
ID: 2103,
UserID: authUser.ID,
FileName: "third-notes.md",
FilePath: "uploads/third-notes.md",
FileSize: 30,
MimeType: "text/markdown",
Extension: "md",
Status: model.UploadStatusUsed,
},
{
ID: 2104,
UserID: 2002,
FileName: "other-user.txt",
FilePath: "uploads/other-user.txt",
FileSize: 40,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 2104,
UserID: 2002,
FileName: "other-user.txt",
FilePath: "uploads/other-user.txt",
FileSize: 40,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
}
for i := range uploads {
@@ -562,34 +562,34 @@ func TestBatchDownloadFiles(t *testing.T) {
// Seed upload records. Note f2 and f3 have the same FileName "file_a.txt" to trigger name collision resolution.
uploads := []model.Upload{
{
ID: 3001,
UserID: 1001,
FileName: "file_a.txt",
FilePath: "uploads/f1.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3001,
UserID: 1001,
FileName: "file_a.txt",
FilePath: "uploads/f1.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 3002,
UserID: 1001,
FileName: "file_b.txt",
FilePath: "uploads/f2.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3002,
UserID: 1001,
FileName: "file_b.txt",
FilePath: "uploads/f2.txt",
FileSize: 13,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 3003,
UserID: 1001,
FileName: "file_a.txt", // COLLISION with 3001!
FilePath: "uploads/f3.txt",
FileSize: 28,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 3003,
UserID: 1001,
FileName: "file_a.txt", // COLLISION with 3001!
FilePath: "uploads/f3.txt",
FileSize: 28,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
}
@@ -750,40 +750,40 @@ func TestGetFileStats(t *testing.T) {
// Insert some dummy uploads
uploads := []model.Upload{
{
ID: 3101,
UserID: authUser.ID,
FileName: "photo.png",
FilePath: "uploads/photo.png",
FileSize: 100,
MimeType: "image/png",
Extension: "png",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 3101,
UserID: authUser.ID,
FileName: "photo.png",
FilePath: "uploads/photo.png",
FileSize: 100,
MimeType: "image/png",
Extension: "png",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
},
{
ID: 3102,
UserID: authUser.ID,
FileName: "video.mp4",
FilePath: "uploads/video.mp4",
FileSize: 500,
MimeType: "video/mp4",
Extension: "mp4",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
ID: 3102,
UserID: authUser.ID,
FileName: "video.mp4",
FilePath: "uploads/video.mp4",
FileSize: 500,
MimeType: "video/mp4",
Extension: "mp4",
Type: "generic",
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -2), // 2 days ago
},
{
ID: 3103,
UserID: authUser.ID,
FileName: "document.pdf",
FilePath: "uploads/document.pdf",
FileSize: 200,
MimeType: "application/pdf",
Extension: "pdf",
Type: "avatar", // different type
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
ID: 3103,
UserID: authUser.ID,
FileName: "document.pdf",
FilePath: "uploads/document.pdf",
FileSize: 200,
MimeType: "application/pdf",
Extension: "pdf",
Type: "avatar", // different type
Status: model.UploadStatusUsed,
CreatedAt: time.Now().AddDate(0, 0, -10), // older than 7 days
},
}
@@ -865,26 +865,26 @@ func TestUserUploadManagement(t *testing.T) {
// Seed upload records
upload1 := model.Upload{
ID: 4001,
UserID: 1001,
FileName: "user1-file.txt",
FilePath: "uploads/user1-file.txt",
FileSize: 100,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 4001,
UserID: 1001,
FileName: "user1-file.txt",
FilePath: "uploads/user1-file.txt",
FileSize: 100,
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
}
upload2 := model.Upload{
ID: 4002,
UserID: 1002,
FileName: "user2-file.png",
FilePath: "uploads/user2-file.png",
FileSize: 200,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
ID: 4002,
UserID: 1002,
FileName: "user2-file.png",
FilePath: "uploads/user2-file.png",
FileSize: 200,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
CreatedAt: time.Now(),
}
_ = dbConn.Create(&upload1)
+1 -1
View File
@@ -123,4 +123,4 @@ func GetFileStats(c *gin.Context) {
Categories: categories,
Types: types,
}))
}
}
+1 -1
View File
@@ -13,4 +13,4 @@ import (
var ErrForbidden = errors.New("upload forbidden")
// ErrStorageReadOnly indicates the storage backend is in migration read-only mode.
var ErrStorageReadOnly = errors.New(shared.ErrStorageReadOnly)
var ErrStorageReadOnly = errors.New(shared.ErrStorageReadOnly)
+1 -1
View File
@@ -195,4 +195,4 @@ func createNewUpload(ctx context.Context, req Request) (Result, error) {
Created: true,
Stored: true,
}, nil
}
}
+1 -1
View File
@@ -61,4 +61,4 @@ func ingestWithHashPolicy(ctx context.Context, req Request) (Result, error) {
}
return createNewUpload(ctx, req)
}
}
+1 -1
View File
@@ -326,4 +326,4 @@ func setupMockStorage(t *testing.T, putCount *int) (restore func(), disable func
storage.ResetCache()
}
return restore, disable
}
}
+1 -1
View File
@@ -55,4 +55,4 @@ func softDeleteUploadWithStats(ctx context.Context, upload *model.Upload) error
}
uploadcache.InvalidateUploadMetaCache(ctx, upload.ID)
return nil
}
}
+1 -1
View File
@@ -57,4 +57,4 @@ type Result struct {
Created bool
Stored bool
Resolved bool
}
}
+9 -9
View File
@@ -5,16 +5,16 @@ package shared
// Upload size, path, media quality, and cache constants shared across subpackages.
const (
MaxUploadSize = 32 * 1024 * 1024 // 32MB
DetectContentBytes = 512 // http.DetectContentType 需要的最小字节数
UploadDirPerm = 0755 // 上传目录权限
UploadFilePerm = 0644 // 上传文件权限
ImageQualityLow = "low"
ImageQualityMedium = "medium"
ImageQualityHigh = "high"
ImageQualityOrigin = "origin"
MaxUploadSize = 32 * 1024 * 1024 // 32MB
DetectContentBytes = 512 // http.DetectContentType 需要的最小字节数
UploadDirPerm = 0755 // 上传目录权限
UploadFilePerm = 0644 // 上传文件权限
ImageQualityLow = "low"
ImageQualityMedium = "medium"
ImageQualityHigh = "high"
ImageQualityOrigin = "origin"
DefaultPublicUploadType = "avatar"
FileStatsTrendDays = 7
MaxS3KeyLength = 1024
AccessCacheTTL = 5 // seconds; multiplied by time.Second at use site
)
)
+1 -1
View File
@@ -38,4 +38,4 @@ const (
ErrInvalidImageCacheWarmupQuality = "图片质量仅支持 low、medium、high"
ErrParseImageCacheWarmupPayload = "解析图片缓存预热参数失败: %w"
ErrQueryImagesForCacheWarmup = "查询待预热图片失败: %w"
)
)
+1 -1
View File
@@ -40,4 +40,4 @@ func GetFileCategory(mimeType, ext string) string {
return catDocument
}
return catOther
}
}
+1 -1
View File
@@ -128,4 +128,4 @@ func RecordUploadStatsRemove(ctx context.Context, upload *model.Upload) {
func isActiveUploadStatus(status model.UploadStatus) bool {
return status == model.UploadStatusPending || status == model.UploadStatusUsed
}
}
@@ -100,4 +100,4 @@ func loadUploadStats(ctx context.Context) (uploadStatsSnapshot, error) {
TotalCount: rows[0].FileCount,
TotalSize: rows[0].FileSize,
}, nil
}
}
+1 -1
View File
@@ -85,4 +85,4 @@ func buildMigrationAccessState(ctx context.Context) MigrationAccessState {
state.Target = target
state.HasTarget = true
return state
}
}
+1 -1
View File
@@ -90,4 +90,4 @@ func NormalizeMigrationPayload(ctx context.Context, payload []byte) ([]byte, sto
return nil, storage.Config{}, fmt.Errorf("marshal storage migration payload: %w", err)
}
return normalized, target, nil
}
}
+1 -1
View File
@@ -28,4 +28,4 @@ func OpenStoredObject(ctx context.Context, upload *model.Upload) (*storage.Objec
return nil, err
}
return backend.Get(ctx, upload.FilePath)
}
}
@@ -374,4 +374,4 @@ func isNotFoundError(err error) bool {
}
}
return false
}
}
@@ -60,16 +60,16 @@ func TestMigrationHandlerExecute(t *testing.T) {
}
upload := model.Upload{
ID: 99101,
UserID: 1,
FileName: "test.txt",
FilePath: "uploads/test.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "hash",
Type: "attachment",
Status: model.UploadStatusUsed,
ID: 99101,
UserID: 1,
FileName: "test.txt",
FilePath: "uploads/test.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "hash",
Type: "attachment",
Status: model.UploadStatusUsed,
}
if err := dbConn.Create(&upload).Error; err != nil {
t.Fatalf("Create(upload) returned error: %v", err)
@@ -157,16 +157,16 @@ func TestMigrationHandlerExecuteWithHashValidation(t *testing.T) {
// Case 1: Incorrect Hash (should fail validation)
uploadIncorrect := model.Upload{
ID: 99102,
UserID: 1,
FileName: "test-hash.txt",
FilePath: "uploads/test-hash.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
Type: "attachment",
Status: model.UploadStatusUsed,
ID: 99102,
UserID: 1,
FileName: "test-hash.txt",
FilePath: "uploads/test-hash.txt",
FileSize: int64(len(content)),
MimeType: "text/plain",
Extension: "txt",
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // Invalid hash
Type: "attachment",
Status: model.UploadStatusUsed,
}
if err := dbConn.Create(&uploadIncorrect).Error; err != nil {
t.Fatalf("Create(uploadIncorrect) returned error: %v", err)
+1 -1
View File
@@ -181,4 +181,4 @@ func (h *WarmImageCacheHandler) Execute(ctx context.Context, payload []byte) (*t
)
task.AppendLog(ctx, "%s", msg)
return &task.TaskResult{Message: msg}, nil
}
}
+28 -28
View File
@@ -280,40 +280,40 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) {
records := []model.Upload{
{
ID: 4101,
UserID: 1001,
FileName: "first.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
ID: 4101,
UserID: 1001,
FileName: "first.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusUsed,
},
{
ID: 4102,
UserID: 1001,
FileName: "second.jpg",
FilePath: secondPath,
MimeType: "application/octet-stream",
Extension: "jpg",
Status: model.UploadStatusPending,
ID: 4102,
UserID: 1001,
FileName: "second.jpg",
FilePath: secondPath,
MimeType: "application/octet-stream",
Extension: "jpg",
Status: model.UploadStatusPending,
},
{
ID: 4103,
UserID: 1001,
FileName: "notes.txt",
FilePath: filepath.Join(testDir, "notes.txt"),
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
ID: 4103,
UserID: 1001,
FileName: "notes.txt",
FilePath: filepath.Join(testDir, "notes.txt"),
MimeType: "text/plain",
Extension: "txt",
Status: model.UploadStatusUsed,
},
{
ID: 4104,
UserID: 1001,
FileName: "deleted.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusDeleted,
ID: 4104,
UserID: 1001,
FileName: "deleted.png",
FilePath: firstPath,
MimeType: "image/png",
Extension: "png",
Status: model.UploadStatusDeleted,
},
}
for i := range records {
+1 -1
View File
@@ -47,4 +47,4 @@ func NormalizeImageQuality(quality string) string {
default:
return shared.ImageQualityOrigin
}
}
}
+1 -1
View File
@@ -72,4 +72,4 @@ func CompressImageToWebP(srcReader io.Reader, quality string) ([]byte, error) {
}
return buf.Bytes(), nil
}
}
+1 -1
View File
@@ -49,4 +49,4 @@ func TestInitSyncsPushEventsOnce(t *testing.T) {
if adminLogin.Name != "管理员登录" {
t.Fatalf("admin_login name = %q, want %q", adminLogin.Name, "管理员登录")
}
}
}
+1 -1
View File
@@ -14,4 +14,4 @@ func runBootstrap(opts bootstrap.Options) {
ctx, span := trace.Start(context.Background(), "bootstrap.Init")
defer span.End()
bootstrap.Init(ctx, opts)
}
}
+1 -2
View File
@@ -51,7 +51,7 @@ func TestResetPasswdCmd_WithUserAndPassword(t *testing.T) {
// Execute command with args
rootCmd.SetArgs([]string{"reset-passwd", "--user", "testuser1", "--password", "newpassword123"})
// Capture output
oldStdout := os.Stdout
r, w, _ := os.Pipe()
@@ -225,4 +225,3 @@ func TestResetPasswdCmd_InteractiveMode(t *testing.T) {
t.Errorf("expected password to change, but it matches the old one")
}
}
+1 -1
View File
@@ -42,4 +42,4 @@ func AbortTooManyRequests(c *gin.Context, msg string) {
// AbortConflict 以 409 中断请求并将错误挂载到 Gin Error 链。
func AbortConflict(c *gin.Context, msg string) {
AbortWithError(c, http.StatusConflict, msg)
}
}
+1 -1
View File
@@ -37,4 +37,4 @@ func ErrorHandlerMiddleware() gin.HandlerFunc {
c.JSON(http.StatusInternalServerError, Err("内部系统错误"))
}
}
}
+1 -1
View File
@@ -165,4 +165,4 @@ func TestErrorHandlerMiddleware_RecordsSpanOnAPIError(t *testing.T) {
assert.Equal(t, codes.Error, spans[0].Status().Code)
assert.Equal(t, "bad request", spans[0].Status().Description)
require.NotEmpty(t, spans[0].Events())
}
}
+3 -3
View File
@@ -6,7 +6,7 @@ package db
const (
errRedisHashSetFailed = "failed to set redis hash: %w"
errRedisHashDeleteFailed = "failed to delete redis hash field: %w"
errUnmarshalDataFailed = "failed to unmarshal data: %w"
errMarshalDataFailed = "failed to marshal data: %w"
errRedisKeySetFailed = "failed to set redis key: %w"
errUnmarshalDataFailed = "failed to unmarshal data: %w"
errMarshalDataFailed = "failed to marshal data: %w"
errRedisKeySetFailed = "failed to set redis key: %w"
)
+1 -1
View File
@@ -43,4 +43,4 @@ func NextUint64ID() uint64 {
log.Printf("[Snowflake] generated negative ID: %d (attempt %d/%d)", id, attempt, maxNegativeIDRetries)
}
panic(fmt.Sprintf("[Snowflake] generated negative ID after %d attempts", maxNegativeIDRetries))
}
}
+1 -1
View File
@@ -12,4 +12,4 @@ import (
func TestNextUint64ID(t *testing.T) {
id := NextUint64ID()
assert.NotZero(t, id)
}
}
+1 -1
View File
@@ -48,4 +48,4 @@ func TestMigrateClickHouseSkipsWhenDisabled(t *testing.T) {
})
MigrateClickHouse()
}
}
+1 -1
View File
@@ -39,4 +39,4 @@ func EmitAdminLoggedIn(ctx context.Context, user *model.User, ip string) {
for _, handler := range adminLoggedInHandlers {
handler(ctx, event)
}
}
}
+1 -1
View File
@@ -41,4 +41,4 @@ func (UserAccessLog) InsertColumns() string {
// BatchInsertSQL returns the INSERT prefix used by native batch writers.
func (UserAccessLog) BatchInsertSQL() string {
return fmt.Sprintf("INSERT INTO %s (%s)", userAccessLogTableName, userAccessLogInsertColumns)
}
}
+1 -1
View File
@@ -96,4 +96,4 @@ func validateJSON(s string) error {
return nil
}
return errors.New("payload schema must be a valid JSON format")
}
}
+7 -7
View File
@@ -12,13 +12,13 @@ import (
// PushEvent 系统通知事件模型
type PushEvent struct {
ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"` // 如 admin_login
Name string `json:"name" gorm:"size:100;not null"` // 如 管理员登录
EventKey string `json:"event_key" gorm:"uniqueIndex;size:80;not null"` // 如 admin_login
Name string `json:"name" gorm:"size:100;not null"` // 如 管理员登录
TaskType string `json:"task_type" gorm:"size:100;index;not null;default:''"` // 关联的异步任务类型
Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"` // 推送渠道列表,如 ["lark"]
Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"` // 推送目标用户/邮箱列表
Template string `json:"template" gorm:"type:text;not null"` // 消息模板 JSON
Enabled bool `json:"enabled" gorm:"index;not null;default:false"` // 是否启用
Channels []string `json:"channels" gorm:"type:text;serializer:json;not null"` // 推送渠道列表,如 ["lark"]
Targets []string `json:"targets" gorm:"type:text;serializer:json;not null"` // 推送目标用户/邮箱列表
Template string `json:"template" gorm:"type:text;not null"` // 消息模板 JSON
Enabled bool `json:"enabled" gorm:"index;not null;default:false"` // 是否启用
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
}
@@ -47,4 +47,4 @@ func (pe *PushEvent) Validate() error {
return errors.New("cannot enable event without any push channels configured")
}
return nil
}
}
+1 -1
View File
@@ -60,4 +60,4 @@ type SystemConfig struct {
// TableName 表名
func (SystemConfig) TableName() string {
return "w_system_configs"
}
}
+1 -1
View File
@@ -25,4 +25,4 @@ type UploadStat struct {
// TableName returns the upload stats table name.
func (UploadStat) TableName() string {
return "w_upload_stats"
}
}
+14 -14
View File
@@ -32,20 +32,20 @@ type UploadMetadata struct {
// Upload 上传文件记录
type Upload struct {
ID uint64 `json:"id,string" gorm:"primaryKey"`
UserID uint64 `json:"user_id,string" gorm:"index;not null"`
FileName string `json:"file_name" gorm:"size:255;not null"` // 原始文件名 (例如: image.png)
FilePath string `json:"file_path" gorm:"size:500;not null;index"` // 文件相对路径 / S3 Key
FileSize int64 `json:"file_size" gorm:"not null"` // 文件大小(字节)
MimeType string `json:"mime_type" gorm:"size:100;not null"` // 媒体类型 (MIME, 如 image/png)
Extension string `json:"extension" gorm:"size:50;not null"` // 文件后缀名 (不含点,如 png, pdf)
Hash string `json:"hash" gorm:"size:64;index"` // 文件哈希 (SHA-256/MD5,可用于排重)
Type string `json:"type" gorm:"column:type;size:50;not null;index"` // 业务标识类型 (如 avatar, doc, attachment)
Status UploadStatus `json:"status" gorm:"type:varchar(20);not null"` // 状态
AccessMode int `json:"access_mode" gorm:"column:access_mode;not null;default:0"`
Metadata UploadMetadata `json:"metadata" gorm:"serializer:json;type:jsonb"` // 业务扩展元数据
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ID uint64 `json:"id,string" gorm:"primaryKey"`
UserID uint64 `json:"user_id,string" gorm:"index;not null"`
FileName string `json:"file_name" gorm:"size:255;not null"` // 原始文件名 (例如: image.png)
FilePath string `json:"file_path" gorm:"size:500;not null;index"` // 文件相对路径 / S3 Key
FileSize int64 `json:"file_size" gorm:"not null"` // 文件大小(字节)
MimeType string `json:"mime_type" gorm:"size:100;not null"` // 媒体类型 (MIME, 如 image/png)
Extension string `json:"extension" gorm:"size:50;not null"` // 文件后缀名 (不含点,如 png, pdf)
Hash string `json:"hash" gorm:"size:64;index"` // 文件哈希 (SHA-256/MD5,可用于排重)
Type string `json:"type" gorm:"column:type;size:50;not null;index"` // 业务标识类型 (如 avatar, doc, attachment)
Status UploadStatus `json:"status" gorm:"type:varchar(20);not null"` // 状态
AccessMode int `json:"access_mode" gorm:"column:access_mode;not null;default:0"`
Metadata UploadMetadata `json:"metadata" gorm:"serializer:json;type:jsonb"` // 业务扩展元数据
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
// TableName 表名
+1 -1
View File
@@ -93,4 +93,4 @@ func applyFilter(query *gorm.DB, filter AccessLogFilter) *gorm.DB {
query = query.Where("created_at <= ?", *filter.EndTime)
}
return query
}
}
@@ -14,4 +14,4 @@ type AccessLogFilter struct {
StartTime *time.Time
// EndTime filters created_at <= EndTime when non-nil.
EndTime *time.Time
}
}
@@ -156,4 +156,4 @@ func GetTopActiveUsers(ctx context.Context, startTime time.Time, limit int) ([]T
return nil, fmt.Errorf("get top active users: %w", err)
}
return users, nil
}
}
@@ -141,10 +141,10 @@ func TestBatchInsert_UsesModelBatchSQL(t *testing.T) {
}
type mockConn struct {
batch driver.Batch
batchQuery string
prepareCalled bool
preparedQuery string
batch driver.Batch
batchQuery string
prepareCalled bool
preparedQuery string
}
func (m *mockConn) Contributors() []string { return nil }
@@ -204,4 +204,4 @@ func (m *mockBatch) Rows() int { return len(m.rows) }
func (m *mockBatch) Columns() []column.Interface { return nil }
func (m *mockBatch) Close() error { return nil }
func (m *mockBatch) Close() error { return nil }
@@ -46,4 +46,4 @@ func BatchInsert(ctx context.Context, logs []analyticsmodel.UserAccessLog) error
return fmt.Errorf("send clickhouse batch: %w", err)
}
return nil
}
}
+1 -1
View File
@@ -27,4 +27,4 @@ func ParseBrowserName(ua string) string {
return "Safari"
}
return "Other"
}
}
@@ -38,7 +38,7 @@ func setupAuthSourceCacheTest(t *testing.T) (*gorm.DB, *miniredis.Miniredis, fun
db.SetDB(sqliteDB)
db.Redis = redis.NewClient(&redis.Options{
Addr: miniRedis.Addr(),
Addr: miniRedis.Addr(),
MaintNotificationsConfig: &maintnotifications.Config{
Mode: maintnotifications.ModeDisabled,
},
@@ -237,4 +237,4 @@ func TestAuthSourceInvalidationPubSubClearsPeerRAM(t *testing.T) {
if _, ok := authSourceActiveRAM.GetIfPresent(authSourceActiveRAMKey); ok {
t.Fatal("expected peer RAM cache to be cleared by pub/sub")
}
}
}
+1 -1
View File
@@ -102,4 +102,4 @@ func DeleteActivePushChannelCache(ctx context.Context, name string) {
if db.Redis != nil {
_ = db.Redis.Del(ctx, db.PrefixedKey("push:channel:active:"+name)).Err()
}
}
}
+1 -1
View File
@@ -121,4 +121,4 @@ func DeleteActivePushEventCache(ctx context.Context, key string) {
if db.Redis != nil {
_ = db.Redis.Del(ctx, db.PrefixedKey("push:event:active:"+key)).Err()
}
}
}
+1 -1
View File
@@ -51,4 +51,4 @@ func CreatePushHistory(ctx context.Context, history *model.PushHistory) error {
// PushHistoryQuery returns a scoped query builder for push histories.
func PushHistoryQuery(ctx context.Context) *gorm.DB {
return db.DB(ctx).Model(&model.PushHistory{})
}
}
+5 -6
View File
@@ -21,10 +21,10 @@ import (
const (
configTypeSystem = "system"
errDatabaseNotInitialized = "database not initialized"
errConfigIntParseFailed = "配置 %s 的值 '%s' 无法转换为整数: %w"
errConfigDecimalParseFailed = "配置 %s 的值 '%s' 无法转换为decimal: %w"
errConfigBoolParseFailed = "配置 %s 的值 '%s' 无法转换为布尔值: %w"
errParseMenuDisplayConfigFailed = "解析目录显示配置失败: %w"
errConfigIntParseFailed = "配置 %s 的值 '%s' 无法转换为整数: %w"
errConfigDecimalParseFailed = "配置 %s 的值 '%s' 无法转换为decimal: %w"
errConfigBoolParseFailed = "配置 %s 的值 '%s' 无法转换为布尔值: %w"
errParseMenuDisplayConfigFailed = "解析目录显示配置失败: %w"
)
// PreheatSystemConfigs loads all system configs from database.
@@ -198,7 +198,6 @@ func ListVisibleSystemConfigs(ctx context.Context) ([]model.SystemConfig, error)
return configs, nil
}
// GetIntByKey queries config and converts to int.
func GetIntByKey(ctx context.Context, key string) (int, error) {
sc, err := GetSystemConfigByKey(ctx, key)
@@ -261,4 +260,4 @@ func GetMenuDisplayConfig(ctx context.Context) (map[string]bool, error) {
}
return config, nil
}
}
+1 -1
View File
@@ -82,4 +82,4 @@ func SaveOrUpdateSystemConfig(ctx context.Context, key, value string) error {
}
}
return InvalidateSystemConfigCache(ctx, key)
}
}
+1 -2
View File
@@ -56,7 +56,6 @@ func setupSystemConfigTest(t *testing.T) (*gorm.DB, func()) {
db.SetDB(sqliteDB)
db.Redis = redisClient
cleanup := func() {
StopSystemConfigCacheListener()
ResetSystemConfigRAMCacheForTest()
@@ -156,4 +155,4 @@ func TestGetSystemConfigByGroupAndInvalidation(t *testing.T) {
if updated.Value != "new_site_name" {
t.Fatalf("value = %q, want %q", updated.Value, "new_site_name")
}
}
}
+1 -1
View File
@@ -56,4 +56,4 @@ func SaveTemplate(ctx context.Context, tmpl *model.Template) error {
// DeleteTemplate removes a template record.
func DeleteTemplate(ctx context.Context, tmpl *model.Template) error {
return db.DB(ctx).Delete(tmpl).Error
}
}
+1 -1
View File
@@ -127,4 +127,4 @@ func ListUploadsByIDs(ctx context.Context, ids []uint64) ([]model.Upload, error)
// UploadQuery returns a scoped GORM query for uploads.
func UploadQuery(ctx context.Context) *gorm.DB {
return db.DB(ctx).Model(&model.Upload{})
}
}
+1 -1
View File
@@ -17,4 +17,4 @@ func ListUploadStats(ctx context.Context) ([]model.UploadStat, error) {
return nil, err
}
return stats, nil
}
}
-1
View File
@@ -108,4 +108,3 @@ func init() {
})
}
}
+2 -2
View File
@@ -5,12 +5,12 @@
package root
import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
// RegisterFrontend is a package-level variable overridden by frontend.go when built with embed_frontend.
var RegisterFrontend = func(_ *gin.Engine) {
// No-op by default
// No-op by default
}
// RegisterRootRoutes registers custom business routes that belong to the root path.
+9 -9
View File
@@ -4,16 +4,16 @@
package storage
import (
"context"
"encoding/json"
"io"
"sync"
"testing"
"time"
"context"
"encoding/json"
"io"
"sync"
"testing"
"time"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func TestStorageCache(t *testing.T) {
+1 -1
View File
@@ -14,4 +14,4 @@ func runExtraCleanups() {
for _, fn := range extraCleanups {
fn()
}
}
}

Some files were not shown because too many files have changed in this diff Show More