refactor(core): decouple gin from pkg/util and reduce code duplication

This commit is contained in:
ryan
2026-08-28 20:15:47 +08:00
parent c52b7c4abf
commit df351cbd33
32 changed files with 394 additions and 432 deletions
+7 -20
View File
@@ -19,27 +19,26 @@ type MigrationEntry struct {
// MigrationExtension defines the interface for registering and querying plugin migrations. // MigrationExtension defines the interface for registering and querying plugin migrations.
type MigrationExtension interface { type MigrationExtension interface {
Register(pluginID string, fsys fs.FS, dir ...string) Register(pluginID string, fsys fs.FS, dir ...string)
Unregister(pluginID string) bool
Entries() []MigrationEntry Entries() []MigrationEntry
Get(pluginID string) (MigrationEntry, bool) Get(pluginID string) (MigrationEntry, bool)
Unregister(pluginID string) bool
} }
// MigrationRegistry collects and stores migration entries from plugins. // MigrationRegistry implements MigrationExtension.
type MigrationRegistry struct { type MigrationRegistry struct {
mu sync.RWMutex mu sync.RWMutex
entries []MigrationEntry entries []MigrationEntry
lookup map[string]MigrationEntry lookup map[string]MigrationEntry
} }
// NewMigrationRegistry creates a new migration registry. // NewMigrationRegistry creates a new MigrationRegistry.
func NewMigrationRegistry() *MigrationRegistry { func NewMigrationRegistry() *MigrationRegistry {
return &MigrationRegistry{ return &MigrationRegistry{
lookup: make(map[string]MigrationEntry), lookup: make(map[string]MigrationEntry),
} }
} }
// Register adds a migration entry for a plugin. // Register registers an embedded migration filesystem for a plugin.
// If dir is not specified, it defaults to "migrations".
func (m *MigrationRegistry) Register(pluginID string, fsys fs.FS, dir ...string) { func (m *MigrationRegistry) Register(pluginID string, fsys fs.FS, dir ...string) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
@@ -72,21 +71,9 @@ func (m *MigrationRegistry) Register(pluginID string, fsys fs.FS, dir ...string)
// Unregister removes a registered migration entry by plugin ID. // Unregister removes a registered migration entry by plugin ID.
func (m *MigrationRegistry) Unregister(pluginID string) bool { func (m *MigrationRegistry) Unregister(pluginID string) bool {
m.mu.Lock() return unregisterEntry(&m.mu, m.lookup, &m.entries, pluginID, func(e MigrationEntry) bool {
defer m.mu.Unlock() return e.PluginID == pluginID
})
if _, exists := m.lookup[pluginID]; !exists {
return false
}
delete(m.lookup, pluginID)
for i, e := range m.entries {
if e.PluginID == pluginID {
m.entries = append(m.entries[:i], m.entries[i+1:]...)
break
}
}
return true
} }
// Entries returns a copy of all registered migration entries in registration order. // Entries returns a copy of all registered migration entries in registration order.
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package extpoints
import (
"slices"
"sync"
)
func unregisterEntry[T any](mu *sync.RWMutex, lookup map[string]T, list *[]T, key string, matches func(T) bool) bool {
mu.Lock()
defer mu.Unlock()
if _, exists := lookup[key]; !exists {
return false
}
delete(lookup, key)
*list = slices.DeleteFunc(*list, matches)
return true
}
+3 -15
View File
@@ -88,21 +88,9 @@ func (s *ScheduleRegistry) RegisterCron(spec, taskType string, payload any, opts
// Unregister removes a registered schedule definition by its task type. // Unregister removes a registered schedule definition by its task type.
func (s *ScheduleRegistry) Unregister(taskType string) bool { func (s *ScheduleRegistry) Unregister(taskType string) bool {
s.mu.Lock() return unregisterEntry(&s.mu, s.lookup, &s.schedules, taskType, func(item ScheduleDefinition) bool {
defer s.mu.Unlock() return item.TaskType == taskType
})
if _, exists := s.lookup[taskType]; !exists {
return false
}
delete(s.lookup, taskType)
for i, item := range s.schedules {
if item.TaskType == taskType {
s.schedules = append(s.schedules[:i], s.schedules[i+1:]...)
break
}
}
return true
} }
// Schedules returns a copy of all registered ScheduleDefinitions. // Schedules returns a copy of all registered ScheduleDefinitions.
+3 -15
View File
@@ -65,21 +65,9 @@ func (s *SettingRegistry) Register(schema SettingSchema) {
// Unregister removes a registered SettingSchema by its key. // Unregister removes a registered SettingSchema by its key.
func (s *SettingRegistry) Unregister(key string) bool { func (s *SettingRegistry) Unregister(key string) bool {
s.mu.Lock() return unregisterEntry(&s.mu, s.lookup, &s.schemas, key, func(item SettingSchema) bool {
defer s.mu.Unlock() return item.Key == key
})
if _, exists := s.lookup[key]; !exists {
return false
}
delete(s.lookup, key)
for i, item := range s.schemas {
if item.Key == key {
s.schemas = append(s.schemas[:i], s.schemas[i+1:]...)
break
}
}
return true
} }
// Schemas returns a copy of all registered SettingSchemas. // Schemas returns a copy of all registered SettingSchemas.
+3 -15
View File
@@ -107,21 +107,9 @@ func (t *TaskRegistry) Register(pattern string, handler any, opts ...TaskOption)
// Unregister removes a registered task definition by its pattern. // Unregister removes a registered task definition by its pattern.
func (t *TaskRegistry) Unregister(pattern string) bool { func (t *TaskRegistry) Unregister(pattern string) bool {
t.mu.Lock() return unregisterEntry(&t.mu, t.lookup, &t.tasks, pattern, func(item TaskDefinition) bool {
defer t.mu.Unlock() return item.Pattern == pattern
})
if _, exists := t.lookup[pattern]; !exists {
return false
}
delete(t.lookup, pattern)
for i, item := range t.tasks {
if item.Pattern == pattern {
t.tasks = append(t.tasks[:i], t.tasks[i+1:]...)
break
}
}
return true
} }
// Tasks returns a copy of all registered TaskDefinitions. // Tasks returns a copy of all registered TaskDefinitions.
+4 -1
View File
@@ -5,6 +5,7 @@
package disk package disk
import ( import (
"Wavelet/pkg/util"
"container/list" "container/list"
"encoding/binary" "encoding/binary"
"errors" "errors"
@@ -100,7 +101,9 @@ var (
func Default() *Cache { func Default() *Cache {
defaultCacheOnce.Do(func() { defaultCacheOnce.Do(func() {
defaultCache = New("uploads/diskcache") defaultCache = New("uploads/diskcache")
go defaultCache.StartCleanupWorker(defaultCleanupInterval) util.Go(func() {
defaultCache.StartCleanupWorker(defaultCleanupInterval)
})
}) })
return defaultCache return defaultCache
} }
+11 -24
View File
@@ -5,15 +5,12 @@ package disk
import ( import (
"bytes" "bytes"
"os"
"testing" "testing"
"time" "time"
) )
func TestDiskCacheBasic(t *testing.T) { func TestDiskCacheBasic(t *testing.T) {
testDir := "uploads/test_diskcache_basic" testDir := t.TempDir()
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir) c := New(testDir)
defer func() { _ = c.Clear() }() defer func() { _ = c.Clear() }()
@@ -55,9 +52,7 @@ func TestDiskCacheBasic(t *testing.T) {
} }
func TestDiskCacheTTL(t *testing.T) { func TestDiskCacheTTL(t *testing.T) {
testDir := "uploads/test_diskcache_ttl" testDir := t.TempDir()
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir) c := New(testDir)
defer func() { _ = c.Clear() }() defer func() { _ = c.Clear() }()
@@ -91,9 +86,7 @@ func TestDiskCacheTTL(t *testing.T) {
} }
func TestDiskCacheExpirationPolicies(t *testing.T) { func TestDiskCacheExpirationPolicies(t *testing.T) {
testDir := "uploads/test_diskcache_expiration_policies" testDir := t.TempDir()
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir) c := New(testDir)
defer func() { _ = c.Clear() }() defer func() { _ = c.Clear() }()
@@ -102,14 +95,14 @@ func TestDiskCacheExpirationPolicies(t *testing.T) {
if err := c.Set("default", []byte("default"), DefaultExpiration); err != nil { if err := c.Set("default", []byte("default"), DefaultExpiration); err != nil {
t.Fatalf("Set(default, DefaultExpiration) returned error: %v", err) t.Fatalf("Set(default, DefaultExpiration) returned error: %v", err)
} }
if err := c.Set("custom", []byte("custom"), 100*time.Millisecond); err != nil { if err := c.Set("custom", []byte("custom"), 150*time.Millisecond); err != nil {
t.Fatalf("Set(custom, 100ms) returned error: %v", err) t.Fatalf("Set(custom, 150ms) returned error: %v", err)
} }
if err := c.Set("permanent", []byte("permanent"), NoExpiration); err != nil { if err := c.Set("permanent", []byte("permanent"), NoExpiration); err != nil {
t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err) t.Fatalf("Set(permanent, NoExpiration) returned error: %v", err)
} }
time.Sleep(75 * time.Millisecond) time.Sleep(80 * time.Millisecond)
if _, err := c.Get("default"); err != ErrCacheMiss { if _, err := c.Get("default"); err != ErrCacheMiss {
t.Errorf("Get(default) error = %v, want ErrCacheMiss", err) t.Errorf("Get(default) error = %v, want ErrCacheMiss", err)
@@ -121,7 +114,7 @@ func TestDiskCacheExpirationPolicies(t *testing.T) {
t.Errorf("Get(permanent) returned error: %v", err) t.Errorf("Get(permanent) returned error: %v", err)
} }
time.Sleep(50 * time.Millisecond) time.Sleep(100 * time.Millisecond)
if _, err := c.Get("custom"); err != ErrCacheMiss { if _, err := c.Get("custom"); err != ErrCacheMiss {
t.Errorf("Get(custom) error = %v, want ErrCacheMiss", err) t.Errorf("Get(custom) error = %v, want ErrCacheMiss", err)
@@ -132,9 +125,7 @@ func TestDiskCacheExpirationPolicies(t *testing.T) {
} }
func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) { func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) {
testDir := "uploads/test_diskcache_no_expiration_reload" testDir := t.TempDir()
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir) c := New(testDir)
if err := c.Set("permanent", []byte("value"), NoExpiration); err != nil { if err := c.Set("permanent", []byte("value"), NoExpiration); err != nil {
@@ -154,9 +145,7 @@ func TestDiskCacheNoExpirationSurvivesReload(t *testing.T) {
} }
func TestDiskCacheLRUEviction(t *testing.T) { func TestDiskCacheLRUEviction(t *testing.T) {
testDir := "uploads/test_diskcache_lru" testDir := t.TempDir()
defer func() { _ = os.RemoveAll(testDir) }()
_ = os.RemoveAll(testDir)
c := New(testDir) c := New(testDir)
defer func() { _ = c.Clear() }() defer func() { _ = c.Clear() }()
@@ -186,10 +175,8 @@ func TestDiskCacheLRUEviction(t *testing.T) {
t.Errorf("k2 should exist: %v", err) t.Errorf("k2 should exist: %v", err)
} }
// Write item 3: 8 + 2 = 10 bytes -> total size would be 30, exceeding 20. // Access k1 again to make it MRU, k2 becomes LRU
// This should evict the oldest item. Since k1 was accessed, but then k2 was accessed, _, _ = c.Get("k1")
// wait, let's access k1 again to make it the most recently used, so k2 becomes oldest!
_, _ = c.Get("k1") // k1 is now MRU, k2 is LRU
err = c.Set("k3", []byte("v3"), DefaultExpiration) err = c.Set("k3", []byte("v3"), DefaultExpiration)
if err != nil { if err != nil {
@@ -1,7 +1,8 @@
// Copyright 2026 Arctel.net // Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
package util // Package ginutil provides helper utilities for Gin web framework contexts.
package ginutil
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
@@ -438,8 +438,16 @@ func maskSensitiveConfig(key, value string) string {
case ConfigKeySMTPPassword: case ConfigKeySMTPPassword:
return maskedConfigValue return maskedConfigValue
case ConfigKeyStorageConfig: case ConfigKeyStorageConfig:
return maskStorageConfig(value)
}
return value
}
func maskStorageConfig(value string) string {
var cfg contracts.StorageConfigDTO var cfg contracts.StorageConfigDTO
if err := json.Unmarshal([]byte(value), &cfg); err == nil { if err := json.Unmarshal([]byte(value), &cfg); err != nil {
return value
}
if cfg.S3.SecretAccessKey != "" { if cfg.S3.SecretAccessKey != "" {
cfg.S3.SecretAccessKey = maskedConfigValue cfg.S3.SecretAccessKey = maskedConfigValue
} }
@@ -455,13 +463,12 @@ func maskSensitiveConfig(key, value string) string {
if cfg.WebDAV.Password != "" { if cfg.WebDAV.Password != "" {
cfg.WebDAV.Password = maskedConfigValue cfg.WebDAV.Password = maskedConfigValue
} }
if val, err := json.Marshal(cfg); err == nil { val, err := json.Marshal(cfg)
return string(val) if err != nil {
}
}
}
return value return value
} }
return string(val)
}
// validateAndMergeStorageConfig parses, merges unmasked secrets, validates parameter values, // validateAndMergeStorageConfig parses, merges unmasked secrets, validates parameter values,
// and tests connectivity of the new storage configuration. // and tests connectivity of the new storage configuration.
@@ -5,9 +5,9 @@ package admin
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/logger" "Wavelet/pkg/logger"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/util"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@@ -262,7 +262,7 @@ func DeleteUser(c *gin.Context) {
return return
} }
currUser, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
if currUser == nil { if currUser == nil {
response.AbortUnauthorized(c, AdminRequired) response.AbortUnauthorized(c, AdminRequired)
return return
@@ -373,7 +373,7 @@ func UpdateUser(c *gin.Context) {
return return
} }
currUser, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
if currUser == nil { if currUser == nil {
response.AbortUnauthorized(c, AdminRequired) response.AbortUnauthorized(c, AdminRequired)
return return
+4 -4
View File
@@ -5,10 +5,10 @@ package admin
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/logger" "Wavelet/pkg/logger"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/trace" "Wavelet/pkg/trace"
"Wavelet/pkg/util"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -19,15 +19,15 @@ func LoginAdminRequired() gin.HandlerFunc {
ctx, span := trace.Start(c.Request.Context(), "LoginAdminRequired") ctx, span := trace.Start(c.Request.Context(), "LoginAdminRequired")
defer span.End() defer span.End()
user, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) user, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
if user == nil { if user == nil {
response.AbortNotFound(c, AdminRequired) response.AbortNotFound(c, AdminRequired)
return return
} }
// 如果是通过 Access Token 鉴权,需要检查令牌本身是否具有管理员权限 // 如果是通过 Access Token 鉴权,需要检查令牌本身是否具有管理员权限
if tokenAuth, _ := util.GetFromContext[bool](c, contracts.AuthTokenAuthKey); tokenAuth { if tokenAuth, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAuthKey); tokenAuth {
tokenAdmin, _ := util.GetFromContext[bool](c, contracts.AuthTokenAdminKey) tokenAdmin, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAdminKey)
if !tokenAdmin { if !tokenAdmin {
response.AbortNotFound(c, TokenAdminRequired) response.AbortNotFound(c, TokenAdminRequired)
return return
+2 -1
View File
@@ -5,6 +5,7 @@ package auth
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/idgen" "Wavelet/pkg/idgen"
"Wavelet/pkg/logger" "Wavelet/pkg/logger"
"Wavelet/pkg/response" "Wavelet/pkg/response"
@@ -438,7 +439,7 @@ func handleCallbackRegister(ctx context.Context, c *gin.Context, source *AuthSou
// UserInfo 获取当前登录用户信息 // UserInfo 获取当前登录用户信息
func UserInfo(c *gin.Context) { func UserInfo(c *gin.Context) {
user, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) user, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
session := sessions.Default(c) session := sessions.Default(c)
needChange := session.Get("need_change_password") == true needChange := session.Get("need_change_password") == true
+26 -24
View File
@@ -5,9 +5,9 @@ package auth
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/trace" "Wavelet/pkg/trace"
"Wavelet/pkg/util"
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
@@ -25,13 +25,7 @@ func hashToken(token string) string {
func getUserByToken(ctx context.Context, tokenStr string) (*contracts.UserDTO, *CachedToken, error) { func getUserByToken(ctx context.Context, tokenStr string) (*contracts.UserDTO, *CachedToken, error) {
tokenHash := hashToken(tokenStr) tokenHash := hashToken(tokenStr)
tokenRecord, err := GetCachedToken(ctx, tokenHash) tokenRecord, err := GetCachedToken(ctx, tokenHash)
if err == nil { if err != nil || tokenRecord == nil {
user, err := GetCachedUser(ctx, tokenRecord.UserID)
if err == nil && user != nil && user.IsActive {
return user, tokenRecord, nil
}
}
var tokenRow struct { var tokenRow struct {
ID uint64 ID uint64
UserID uint64 UserID uint64
@@ -46,22 +40,30 @@ func getUserByToken(ctx context.Context, tokenStr string) (*contracts.UserDTO, *
IsAdmin: tokenRow.IsAdmin, IsAdmin: tokenRow.IsAdmin,
} }
SetCachedToken(ctx, tokenHash, tokenRecord) SetCachedToken(ctx, tokenHash, tokenRecord)
}
user, err := GetCachedUser(ctx, tokenRecord.UserID)
if err != nil || user == nil || !user.IsActive {
var userRow contracts.UserDTO var userRow contracts.UserDTO
if err := getDB(ctx).Table("w_users").Where("id = ? AND is_active = ?", tokenRow.UserID, true).First(&userRow).Error; err != nil { if err := getDB(ctx).Table("w_users").Where("id = ? AND is_active = ?", tokenRecord.UserID, true).First(&userRow).Error; err != nil {
return nil, nil, err return nil, nil, err
} }
SetCachedUser(ctx, userRow.ID, &userRow) user = &userRow
return &userRow, tokenRecord, nil SetCachedUser(ctx, tokenRecord.UserID, user)
} }
// GetUserFromRequest 校验 Access Token 或 Session 并返回用户对象,如果未登录或用户失效则返回 error return user, tokenRecord, nil
}
// GetUserFromRequest 从请求中获取当前用户(优先 Access Token,其次 Session)
func GetUserFromRequest(c *gin.Context) (*contracts.UserDTO, error) { func GetUserFromRequest(c *gin.Context) (*contracts.UserDTO, error) {
ctx := c.Request.Context() ctx := c.Request.Context()
var tokenStr string
// Check token in headers tokenFromQuery := c.Query("token")
tokenStr := c.GetHeader("X-Access-Token") if tokenFromQuery != "" {
if tokenStr == "" { tokenStr = tokenFromQuery
} else {
authHeader := c.GetHeader("Authorization") authHeader := c.GetHeader("Authorization")
if len(authHeader) > 7 && authHeader[:7] == "Bearer " { if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
tokenStr = authHeader[7:] tokenStr = authHeader[7:]
@@ -74,8 +76,8 @@ func GetUserFromRequest(c *gin.Context) (*contracts.UserDTO, error) {
if user.Username == SystemUsername { if user.Username == SystemUsername {
return nil, errors.New("system user is not allowed to login") return nil, errors.New("system user is not allowed to login")
} }
util.SetToContext(c, contracts.AuthTokenAuthKey, true) ginutil.SetToContext(c, contracts.AuthTokenAuthKey, true)
util.SetToContext(c, contracts.AuthTokenAdminKey, tokenRecord.IsAdmin) ginutil.SetToContext(c, contracts.AuthTokenAdminKey, tokenRecord.IsAdmin)
return user, nil return user, nil
} }
} }
@@ -96,8 +98,8 @@ func GetUserFromRequest(c *gin.Context) (*contracts.UserDTO, error) {
SetCachedUser(ctx, userID, user) SetCachedUser(ctx, userID, user)
} }
util.SetToContext(c, contracts.AuthTokenAuthKey, false) ginutil.SetToContext(c, contracts.AuthTokenAuthKey, false)
util.SetToContext(c, contracts.AuthTokenAdminKey, false) ginutil.SetToContext(c, contracts.AuthTokenAdminKey, false)
if user.Username == "system" { if user.Username == "system" {
return nil, errors.New("system user is not allowed to login") return nil, errors.New("system user is not allowed to login")
@@ -119,7 +121,7 @@ func LoginRequired() gin.HandlerFunc {
} }
LogForAudit(c.Request.Context(), user, c) LogForAudit(c.Request.Context(), user, c)
util.SetToContext(c, contracts.AuthUserObjKey, user) ginutil.SetToContext(c, contracts.AuthUserObjKey, user)
c.Next() c.Next()
} }
} }
@@ -136,8 +138,8 @@ func AdminRequired() gin.HandlerFunc {
return return
} }
isTokenAuth, _ := util.GetFromContext[bool](c, contracts.AuthTokenAuthKey) isTokenAuth, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAuthKey)
isTokenAdmin, _ := util.GetFromContext[bool](c, contracts.AuthTokenAdminKey) isTokenAdmin, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAdminKey)
// 如果是通过 Token 鉴权,要求该 Token 具备管理员权限或者用户本身为管理员 // 如果是通过 Token 鉴权,要求该 Token 具备管理员权限或者用户本身为管理员
if isTokenAuth && !isTokenAdmin && !user.IsAdmin { if isTokenAuth && !isTokenAdmin && !user.IsAdmin {
@@ -152,7 +154,7 @@ func AdminRequired() gin.HandlerFunc {
} }
LogForAudit(c.Request.Context(), user, c) LogForAudit(c.Request.Context(), user, c)
util.SetToContext(c, contracts.AuthUserObjKey, user) ginutil.SetToContext(c, contracts.AuthUserObjKey, user)
c.Next() c.Next()
} }
} }
@@ -165,7 +167,7 @@ func LoginAdminRequired() gin.HandlerFunc {
// DisallowTokenAuth 拒绝使用 Access Token 进行身份验证的请求访问该端点 // DisallowTokenAuth 拒绝使用 Access Token 进行身份验证的请求访问该端点
func DisallowTokenAuth() gin.HandlerFunc { func DisallowTokenAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if tokenAuth, _ := util.GetFromContext[bool](c, contracts.AuthTokenAuthKey); tokenAuth { if tokenAuth, _ := ginutil.GetFromContext[bool](c, contracts.AuthTokenAuthKey); tokenAuth {
response.AbortForbidden(c, ErrTokenAuthNotAllowed) response.AbortForbidden(c, ErrTokenAuthNotAllowed)
return return
} }
+2 -2
View File
@@ -5,7 +5,7 @@ package auth
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/util" "Wavelet/pkg/ginutil"
"context" "context"
"errors" "errors"
"sync" "sync"
@@ -29,7 +29,7 @@ func (s *authServiceImpl) RequireAdminMiddleware() any {
func (s *authServiceImpl) GetCurrentUser(ctx context.Context) (*contracts.UserDTO, error) { func (s *authServiceImpl) GetCurrentUser(ctx context.Context) (*contracts.UserDTO, error) {
if ginCtx, ok := ctx.(*gin.Context); ok { if ginCtx, ok := ctx.(*gin.Context); ok {
if u, ok := util.GetFromContext[*contracts.UserDTO](ginCtx, contracts.AuthUserObjKey); ok && u != nil { if u, ok := ginutil.GetFromContext[*contracts.UserDTO](ginCtx, contracts.AuthUserObjKey); ok && u != nil {
return u, nil return u, nil
} }
} }
@@ -40,6 +40,23 @@ func ListAdminChannels(c *gin.Context) {
c.JSON(http.StatusOK, response.OK(rows)) c.JSON(http.StatusOK, response.OK(rows))
} }
func parseAdminChannelID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.AbortBadRequest(c, "invalid channel id")
return 0, false
}
return id, true
}
func handleAdminChannelError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
if err.Error() == errChannelNotFound {
response.AbortNotFound(c, err.Error())
return
}
fallback(c, err.Error())
}
// CreateAdminChannel creates a messaging channel. // CreateAdminChannel creates a messaging channel.
// @Summary Create message gateway channel // @Summary Create message gateway channel
// @Description Creates a Telegram or QQ channel with encrypted credentials // @Description Creates a Telegram or QQ channel with encrypted credentials
@@ -52,17 +69,7 @@ func ListAdminChannels(c *gin.Context) {
// @Failure 400 {object} response.Any // @Failure 400 {object} response.Any
// @Router /api/v1/admin/message-gateway/channels [post] // @Router /api/v1/admin/message-gateway/channels [post]
func CreateAdminChannel(c *gin.Context) { func CreateAdminChannel(c *gin.Context) {
var req CreateChannelRequest handleJSONRequest(c, createChannel)
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
dto, err := createChannel(c.Request.Context(), req)
if err != nil {
response.AbortBadRequest(c, err.Error())
return
}
c.JSON(http.StatusOK, response.OK(dto))
} }
// UpdateAdminChannel patches a messaging channel. Empty secrets keep the previous values. // UpdateAdminChannel patches a messaging channel. Empty secrets keep the previous values.
@@ -79,26 +86,9 @@ func CreateAdminChannel(c *gin.Context) {
// @Failure 404 {object} response.Any // @Failure 404 {object} response.Any
// @Router /api/v1/admin/message-gateway/channels/{id} [patch] // @Router /api/v1/admin/message-gateway/channels/{id} [patch]
func UpdateAdminChannel(c *gin.Context) { func UpdateAdminChannel(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) handleEntityUpdate(c, parseAdminChannelID, updateChannel, func(c *gin.Context, err error) {
if err != nil { handleAdminChannelError(c, err, response.AbortBadRequest)
response.AbortBadRequest(c, "invalid channel id") })
return
}
var req UpdateChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
dto, err := updateChannel(c.Request.Context(), id, req)
if err != nil {
if err.Error() == errChannelNotFound {
response.AbortNotFound(c, err.Error())
return
}
response.AbortBadRequest(c, err.Error())
return
}
c.JSON(http.StatusOK, response.OK(dto))
} }
// DeleteAdminChannel removes a channel and its bindings/pairing codes. // DeleteAdminChannel removes a channel and its bindings/pairing codes.
@@ -112,17 +102,12 @@ func UpdateAdminChannel(c *gin.Context) {
// @Failure 404 {object} response.Any // @Failure 404 {object} response.Any
// @Router /api/v1/admin/message-gateway/channels/{id} [delete] // @Router /api/v1/admin/message-gateway/channels/{id} [delete]
func DeleteAdminChannel(c *gin.Context) { func DeleteAdminChannel(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, ok := parseAdminChannelID(c)
if err != nil { if !ok {
response.AbortBadRequest(c, "invalid channel id")
return return
} }
if err := deleteChannel(c.Request.Context(), id); err != nil { if err := deleteChannel(c.Request.Context(), id); err != nil {
if err.Error() == errChannelNotFound { handleAdminChannelError(c, err, response.AbortInternal)
response.AbortNotFound(c, err.Error())
return
}
response.AbortInternal(c, err.Error())
return return
} }
c.JSON(http.StatusOK, response.OKNil()) c.JSON(http.StatusOK, response.OKNil())
@@ -140,17 +125,12 @@ func DeleteAdminChannel(c *gin.Context) {
// @Failure 404 {object} response.Any // @Failure 404 {object} response.Any
// @Router /api/v1/admin/message-gateway/channels/{id}/test [post] // @Router /api/v1/admin/message-gateway/channels/{id}/test [post]
func TestAdminChannel(c *gin.Context) { func TestAdminChannel(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, ok := parseAdminChannelID(c)
if err != nil { if !ok {
response.AbortBadRequest(c, "invalid channel id")
return return
} }
if err := probeChannel(c.Request.Context(), id); err != nil { if err := probeChannel(c.Request.Context(), id); err != nil {
if err.Error() == errChannelNotFound { handleAdminChannelError(c, err, response.AbortBadRequest)
response.AbortNotFound(c, err.Error())
return
}
response.AbortBadRequest(c, err.Error())
return return
} }
c.JSON(http.StatusOK, response.OKNil()) c.JSON(http.StatusOK, response.OKNil())
@@ -0,0 +1,49 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package message_gateway
import (
"Wavelet/pkg/response"
"context"
"net/http"
"github.com/gin-gonic/gin"
)
func handleJSONRequest[Req any, Res any](c *gin.Context, handler func(ctx context.Context, req Req) (Res, error)) {
var req Req
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
res, err := handler(c.Request.Context(), req)
if err != nil {
response.AbortBadRequest(c, err.Error())
return
}
c.JSON(http.StatusOK, response.OK(res))
}
func handleEntityUpdate[Req any, Res any](
c *gin.Context,
parseID func(*gin.Context) (uint64, bool),
updater func(ctx context.Context, id uint64, req Req) (Res, error),
onErr func(*gin.Context, error),
) {
id, ok := parseID(c)
if !ok {
return
}
var req Req
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
dto, err := updater(c.Request.Context(), id, req)
if err != nil {
onErr(c, err)
return
}
c.JSON(http.StatusOK, response.OK(dto))
}
@@ -5,8 +5,8 @@ package message_gateway
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/util"
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
@@ -15,7 +15,7 @@ import (
) )
func currentUser(c *gin.Context) (*contracts.UserDTO, bool) { func currentUser(c *gin.Context) (*contracts.UserDTO, bool) {
return util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) return ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
} }
// ListChannels lists enabled channels a user can bind. // ListChannels lists enabled channels a user can bind.
@@ -214,20 +214,26 @@ type CreatePushChannelRequest struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }
// CreatePushChannel creates a push channel. func parsePushChannelID(c *gin.Context) (uint64, bool) {
func CreatePushChannel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64)
var req CreatePushChannelRequest if err != nil {
if err := c.ShouldBindJSON(&req); err != nil { response.AbortBadRequest(c, "invalid channel id")
response.AbortBadRequest(c, err.Error()) return 0, false
return }
return id, true
} }
channel, err := createPushChannel(c.Request.Context(), req) func handlePushChannelNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortBadRequest(c, err.Error()) response.AbortNotFound(c, "channel not found")
return return
} }
c.JSON(http.StatusOK, response.OK(channel)) fallback(c, err.Error())
}
// CreatePushChannel creates a push channel.
func CreatePushChannel(c *gin.Context) {
handleJSONRequest(c, createPushChannel)
} }
// UpdatePushChannelRequest is the update channel request payload. // UpdatePushChannelRequest is the update channel request payload.
@@ -242,44 +248,20 @@ type UpdatePushChannelRequest struct {
// UpdatePushChannel updates a push channel. // UpdatePushChannel updates a push channel.
func UpdatePushChannel(c *gin.Context) { func UpdatePushChannel(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) handleEntityUpdate(c, parsePushChannelID, updatePushChannel, func(c *gin.Context, err error) {
if err != nil { handlePushChannelNotFoundError(c, err, response.AbortInternal)
response.AbortBadRequest(c, "invalid channel id") })
return
}
var req UpdatePushChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
channel, err := updatePushChannel(c.Request.Context(), id, req)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortNotFound(c, "channel not found")
return
}
response.AbortInternal(c, err.Error())
return
}
c.JSON(http.StatusOK, response.OK(channel))
} }
// DeletePushChannel deletes a push channel. // DeletePushChannel deletes a push channel.
func DeletePushChannel(c *gin.Context) { func DeletePushChannel(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, ok := parsePushChannelID(c)
if err != nil { if !ok {
response.AbortBadRequest(c, "invalid channel id")
return return
} }
if err := deletePushChannel(c.Request.Context(), id); err != nil { if err := deletePushChannel(c.Request.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { handlePushChannelNotFoundError(c, err, response.AbortInternal)
response.AbortNotFound(c, "channel not found")
return
}
response.AbortInternal(c, err.Error())
return return
} }
c.JSON(http.StatusOK, response.OKNil()) c.JSON(http.StatusOK, response.OKNil())
@@ -56,36 +56,37 @@ func ListBuiltInPushEvents(c *gin.Context) {
c.JSON(http.StatusOK, response.OK(GetBuiltInEvents())) c.JSON(http.StatusOK, response.OK(GetBuiltInEvents()))
} }
// CreatePushEvent creates a new push event configuration. func parsePushEventID(c *gin.Context) (uint64, bool) {
func CreatePushEvent(c *gin.Context) {
var req CreatePushEventRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, err.Error())
return
}
event, err := createPushEvent(c.Request.Context(), req)
if err != nil {
response.AbortBadRequest(c, err.Error())
return
}
c.JSON(http.StatusOK, response.OK(event))
}
// DeletePushEvent deletes a push event configuration by ID.
func DeletePushEvent(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil { if err != nil {
response.AbortBadRequest(c, "invalid event id") response.AbortBadRequest(c, "invalid event id")
return return 0, false
}
return id, true
} }
if err := deletePushEvent(c.Request.Context(), id); err != nil { func handlePushEventNotFoundError(c *gin.Context, err error, fallback func(c *gin.Context, msg string)) {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
response.AbortNotFound(c, "notification event not found") response.AbortNotFound(c, "notification event not found")
return return
} }
response.AbortInternal(c, err.Error()) fallback(c, err.Error())
}
// CreatePushEvent creates a new push event configuration.
func CreatePushEvent(c *gin.Context) {
handleJSONRequest(c, createPushEvent)
}
// DeletePushEvent deletes a push event configuration by ID.
func DeletePushEvent(c *gin.Context) {
id, ok := parsePushEventID(c)
if !ok {
return
}
if err := deletePushEvent(c.Request.Context(), id); err != nil {
handlePushEventNotFoundError(c, err, response.AbortInternal)
return return
} }
c.JSON(http.StatusOK, response.OKNil()) c.JSON(http.StatusOK, response.OKNil())
@@ -93,9 +94,8 @@ func DeletePushEvent(c *gin.Context) {
// UpdatePushEvent updates an existing push event. // UpdatePushEvent updates an existing push event.
func UpdatePushEvent(c *gin.Context) { func UpdatePushEvent(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, ok := parsePushEventID(c)
if err != nil { if !ok {
response.AbortBadRequest(c, "invalid event id")
return return
} }
@@ -106,11 +106,7 @@ func UpdatePushEvent(c *gin.Context) {
} }
if err := updatePushEvent(c.Request.Context(), id, req); err != nil { if err := updatePushEvent(c.Request.Context(), id, req); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { handlePushEventNotFoundError(c, err, response.AbortBadRequest)
response.AbortNotFound(c, "notification event not found")
return
}
response.AbortBadRequest(c, err.Error())
return return
} }
c.JSON(http.StatusOK, response.OKNil()) c.JSON(http.StatusOK, response.OKNil())
@@ -118,19 +114,14 @@ func UpdatePushEvent(c *gin.Context) {
// TogglePushEvent toggles the enabled state of a push event. // TogglePushEvent toggles the enabled state of a push event.
func TogglePushEvent(c *gin.Context) { func TogglePushEvent(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64) id, ok := parsePushEventID(c)
if err != nil { if !ok {
response.AbortBadRequest(c, "invalid event id")
return return
} }
enabled, err := togglePushEvent(c.Request.Context(), id) enabled, err := togglePushEvent(c.Request.Context(), id)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { handlePushEventNotFoundError(c, err, response.AbortBadRequest)
response.AbortNotFound(c, "notification event not found")
return
}
response.AbortBadRequest(c, err.Error())
return return
} }
c.JSON(http.StatusOK, response.OK(enabled)) c.JSON(http.StatusOK, response.OK(enabled))
@@ -217,25 +217,31 @@ func DeletePushChannelRecord(ctx context.Context, channel *PushChannel) error {
return nil return nil
} }
// GetActivePushChannelByName 根据名称获取启用的消息通道 (优先从 Redis 缓存获取)。 func getCachedOrQuery[T any](ctx context.Context, cacheKey string, ttl time.Duration, query func(db *gorm.DB, dest *T) error) (*T, error) {
func GetActivePushChannelByName(ctx context.Context, name string) (*PushChannel, error) { var val T
cacheKey := "push:channel:active:" + name
var channel PushChannel
if cache := getCache(ctx); cache != nil { if cache := getCache(ctx); cache != nil {
if err := cache.Get(ctx, cacheKey, &channel); err == nil { if err := cache.Get(ctx, cacheKey, &val); err == nil {
return &channel, nil return &val, nil
} }
} }
if err := getDB(ctx).Where("name = ? AND enabled = ?", name, true).First(&channel).Error; err != nil { db := getDB(ctx)
if err := query(db, &val); err != nil {
return nil, err return nil, err
} }
if cache := getCache(ctx); cache != nil { if cache := getCache(ctx); cache != nil {
_ = cache.Set(ctx, cacheKey, channel, activePushChannelCacheTTL) _ = cache.Set(ctx, cacheKey, val, ttl)
} }
return &channel, nil return &val, nil
}
// GetActivePushChannelByName 根据名称获取启用的消息通道 (优先从 Redis 缓存获取)。
func GetActivePushChannelByName(ctx context.Context, name string) (*PushChannel, error) {
return getCachedOrQuery(ctx, "push:channel:active:"+name, activePushChannelCacheTTL, func(db *gorm.DB, dest *PushChannel) error {
return db.Where("name = ? AND enabled = ?", name, true).First(dest).Error
})
} }
// DeleteActivePushChannelCache 清理启用消息通道的缓存。 // DeleteActivePushChannelCache 清理启用消息通道的缓存。
@@ -329,23 +335,9 @@ func ListActivePushEventsByTaskTypeRecord(ctx context.Context, taskType string)
// GetActivePushEventByKey 获取启用的通知事件 (优先从 Redis 缓存获取)。 // GetActivePushEventByKey 获取启用的通知事件 (优先从 Redis 缓存获取)。
func GetActivePushEventByKey(ctx context.Context, key string) (*PushEvent, error) { func GetActivePushEventByKey(ctx context.Context, key string) (*PushEvent, error) {
cacheKey := "push:event:active:" + key return getCachedOrQuery(ctx, "push:event:active:"+key, activePushEventCacheTTL, func(db *gorm.DB, dest *PushEvent) error {
var event PushEvent return db.Where("event_key = ? AND enabled = ?", key, true).First(dest).Error
if cache := getCache(ctx); cache != nil { })
if err := cache.Get(ctx, cacheKey, &event); err == nil {
return &event, nil
}
}
if err := getDB(ctx).Where("event_key = ? AND enabled = ?", key, true).First(&event).Error; err != nil {
return nil, err
}
if cache := getCache(ctx); cache != nil {
_ = cache.Set(ctx, cacheKey, event, activePushEventCacheTTL)
}
return &event, nil
} }
// DeleteActivePushEventCache 清理启用通知事件的缓存。 // DeleteActivePushEventCache 清理启用通知事件的缓存。
@@ -7,9 +7,9 @@ package risk_control
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/config" "Wavelet/pkg/config"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/idgen" "Wavelet/pkg/idgen"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/util"
"Wavelet/plugins/domain/risk_control/logstore" "Wavelet/plugins/domain/risk_control/logstore"
"encoding/json" "encoding/json"
"net/http" "net/http"
@@ -42,7 +42,7 @@ func RiskControlMiddleware() gin.HandlerFunc {
c.Next() c.Next()
// 3. 后置身份检查:仅记录通过认证的请求 // 3. 后置身份检查:仅记录通过认证的请求
userObj, exists := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) userObj, exists := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
if !exists || userObj == nil { if !exists || userObj == nil {
return return
} }
@@ -7,8 +7,8 @@ import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/batchwriter" "Wavelet/pkg/batchwriter"
"Wavelet/pkg/config" "Wavelet/pkg/config"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/testhelper" "Wavelet/pkg/testhelper"
"Wavelet/pkg/util"
"Wavelet/plugins/domain/risk_control" "Wavelet/plugins/domain/risk_control"
"Wavelet/plugins/domain/risk_control/logstore" "Wavelet/plugins/domain/risk_control/logstore"
"context" "context"
@@ -99,7 +99,7 @@ func TestRiskControlMiddleware(t *testing.T) {
r := gin.New() r := gin.New()
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
user := &contracts.UserDTO{ID: 12345} user := &contracts.UserDTO{ID: 12345}
util.SetToContext[*contracts.UserDTO](c, contracts.AuthUserObjKey, user) ginutil.SetToContext[*contracts.UserDTO](c, contracts.AuthUserObjKey, user)
c.Next() c.Next()
}) })
r.Use(risk_control.RiskControlMiddleware()) r.Use(risk_control.RiskControlMiddleware())
@@ -23,8 +23,7 @@ import (
"sync" "sync"
pkgcache "Wavelet/pkg/cache/disk" pkgcache "Wavelet/pkg/cache/disk"
"Wavelet/pkg/ginutil"
pkgutil "Wavelet/pkg/util"
uploadstorage "Wavelet/plugins/domain/upload/storage" uploadstorage "Wavelet/plugins/domain/upload/storage"
@@ -301,7 +300,7 @@ func getOriginalFileBytes(ctx context.Context, upload *models.Upload) ([]byte, e
func checkPrivateFileOwner(c *gin.Context, ownerID uint64) error { func checkPrivateFileOwner(c *gin.Context, ownerID uint64) error {
var currUserID uint64 var currUserID uint64
var isAdmin bool var isAdmin bool
if u, ok := pkgutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey); ok && u != nil { if u, ok := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey); ok && u != nil {
currUserID = u.ID currUserID = u.ID
isAdmin = u.IsAdmin isAdmin = u.IsAdmin
} else if authSvc := shared.GetAuthService(c); authSvc != nil { } else if authSvc := shared.GetAuthService(c); authSvc != nil {
@@ -329,14 +328,19 @@ func CheckFileAccessPermission(c *gin.Context, upload *models.Upload) error {
return checkPrivateFileOwner(c, upload.UserID) return checkPrivateFileOwner(c, upload.UserID)
} }
if !cache.IsFilePublic(c.Request.Context(), upload.Type) { if cache.IsFilePublic(c.Request.Context(), upload.Type) {
if _, ok := pkgutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey); !ok {
if authSvc := shared.GetAuthService(c); authSvc != nil {
if _, err := authSvc.GetCurrentUser(c); err != nil {
return err
}
}
}
}
return nil return nil
} }
if _, ok := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey); ok {
return nil
}
authSvc := shared.GetAuthService(c)
if authSvc == nil {
return nil
}
_, err := authSvc.GetCurrentUser(c)
return err
}
@@ -5,8 +5,8 @@ package handler
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/util"
"Wavelet/plugins/domain/upload/ingest" "Wavelet/plugins/domain/upload/ingest"
"Wavelet/plugins/domain/upload/models" "Wavelet/plugins/domain/upload/models"
"Wavelet/plugins/domain/upload/repository" "Wavelet/plugins/domain/upload/repository"
@@ -168,7 +168,7 @@ type listMyFilesResponse struct {
// @Failure 401 {object} response.Any "未登录" // @Failure 401 {object} response.Any "未登录"
// @Router /api/v1/upload/my [get] // @Router /api/v1/upload/my [get]
func ListMyFiles(c *gin.Context) { func ListMyFiles(c *gin.Context) {
currUser, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context() ctx := c.Request.Context()
var req listMyFilesRequest var req listMyFilesRequest
@@ -215,7 +215,7 @@ func ListMyFiles(c *gin.Context) {
// @Failure 404 {object} response.Any "文件不存在" // @Failure 404 {object} response.Any "文件不存在"
// @Router /api/v1/upload/{id} [delete] // @Router /api/v1/upload/{id} [delete]
func DeleteMyFile(c *gin.Context) { func DeleteMyFile(c *gin.Context) {
currUser, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context() ctx := c.Request.Context()
if uploadstorage.ReadOnly(ctx) { if uploadstorage.ReadOnly(ctx) {
response.AbortConflict(c, shared.ErrStorageReadOnly) response.AbortConflict(c, shared.ErrStorageReadOnly)
@@ -262,7 +262,7 @@ type updateMyFileRequest struct {
// @Failure 404 {object} response.Any "文件不存在" // @Failure 404 {object} response.Any "文件不存在"
// @Router /api/v1/upload/{id} [put] // @Router /api/v1/upload/{id} [put]
func UpdateMyFile(c *gin.Context) { func UpdateMyFile(c *gin.Context) {
currUser, _ := util.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context() ctx := c.Request.Context()
if uploadstorage.ReadOnly(ctx) { if uploadstorage.ReadOnly(ctx) {
response.AbortConflict(c, shared.ErrStorageReadOnly) response.AbortConflict(c, shared.ErrStorageReadOnly)
@@ -29,11 +29,11 @@ import (
"strconv" "strconv"
"strings" "strings"
"Wavelet/pkg/ginutil"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
pkgutil "Wavelet/pkg/util"
uploadstorage "Wavelet/plugins/domain/upload/storage" uploadstorage "Wavelet/plugins/domain/upload/storage"
) )
@@ -64,7 +64,7 @@ func UploadFile(c *gin.Context) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, shared.MaxUploadSize) c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, shared.MaxUploadSize)
currUser, _ := pkgutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey) currUser, _ := ginutil.GetFromContext[*contracts.UserDTO](c, contracts.AuthUserObjKey)
ctx := c.Request.Context() ctx := c.Request.Context()
header, err := c.FormFile("file") header, err := c.FormFile("file")
@@ -5,8 +5,8 @@ package handler
import ( import (
"Wavelet/core/contracts" "Wavelet/core/contracts"
"Wavelet/pkg/ginutil"
"Wavelet/pkg/response" "Wavelet/pkg/response"
"Wavelet/pkg/util"
"Wavelet/plugins/domain/upload/models" "Wavelet/plugins/domain/upload/models"
"Wavelet/plugins/domain/upload/shared" "Wavelet/plugins/domain/upload/shared"
"archive/zip" "archive/zip"
@@ -42,7 +42,7 @@ func setupTestRouter(authUser *contracts.UserDTO) *gin.Engine {
authMiddleware := func(c *gin.Context) { authMiddleware := func(c *gin.Context) {
if authUser != nil { if authUser != nil {
util.SetToContext[*contracts.UserDTO](c, contracts.AuthUserObjKey, authUser) ginutil.SetToContext[*contracts.UserDTO](c, contracts.AuthUserObjKey, authUser)
} }
c.Next() c.Next()
} }
@@ -139,34 +139,7 @@ func (p *Plugin) Start(_ context.Context) error {
} }
if p.scheduler == nil { if p.scheduler == nil {
opts := p.schedulerOpts p.scheduler = p.initScheduler()
if opts == nil {
opts = &asynq.SchedulerOpts{
Location: p.location,
}
} else if opts.Location == nil && p.location != nil {
opts.Location = p.location
}
opt := p.redisOpt
if opt == nil {
if RedisOpt != nil {
opt = RedisOpt
} else {
redisCfg := config.Config.Redis
addr := "127.0.0.1:6379"
if len(redisCfg.Addrs) > 0 && redisCfg.Addrs[0] != "" {
addr = redisCfg.Addrs[0]
}
opt = asynq.RedisClientOpt{
Addr: addr,
Username: redisCfg.Username,
Password: redisCfg.Password,
DB: redisCfg.DB,
}
}
}
p.scheduler = asynq.NewScheduler(opt, opts)
} }
if p.coreCtx != nil && p.coreCtx.Schedules() != nil { if p.coreCtx != nil && p.coreCtx.Schedules() != nil {
@@ -268,3 +241,37 @@ func buildAsynqOptions(opts map[string]any) []asynq.Option {
return res return res
} }
func (p *Plugin) initScheduler() *asynq.Scheduler {
opts := p.schedulerOpts
if opts == nil {
opts = &asynq.SchedulerOpts{
Location: p.location,
}
} else if opts.Location == nil && p.location != nil {
opts.Location = p.location
}
opt := p.resolveRedisOpt()
return asynq.NewScheduler(opt, opts)
}
func (p *Plugin) resolveRedisOpt() asynq.RedisConnOpt {
if p.redisOpt != nil {
return p.redisOpt
}
if RedisOpt != nil {
return RedisOpt
}
redisCfg := config.Config.Redis
addr := "127.0.0.1:6379"
if len(redisCfg.Addrs) > 0 && redisCfg.Addrs[0] != "" {
addr = redisCfg.Addrs[0]
}
return asynq.RedisClientOpt{
Addr: addr,
Username: redisCfg.Username,
Password: redisCfg.Password,
DB: redisCfg.DB,
}
}
@@ -111,21 +111,7 @@ func ReloadScheduler() error {
// 4. 遍历并注册任务 // 4. 遍历并注册任务
taskSvc := getTaskService() taskSvc := getTaskService()
for _, s := range schedules { for _, s := range schedules {
taskName := s.TaskType taskName, maxRetry, queue := resolveTaskScheduleMeta(taskSvc, s.TaskType)
maxRetry := 3
queue := "default"
if taskSvc != nil {
if meta, ok := taskSvc.GetTaskMeta(s.TaskType); ok {
taskName = meta.Name
if meta.MaxRetry > 0 {
maxRetry = meta.MaxRetry
}
if meta.Queue != "" {
queue = meta.Queue
}
}
}
// 构造 Asynq 载荷 // 构造 Asynq 载荷
t := asynq.NewTask(taskName, []byte(s.Payload)) t := asynq.NewTask(taskName, []byte(s.Payload))
@@ -160,3 +146,24 @@ func waitForStop(done, signals <-chan struct{}) bool {
return true return true
} }
} }
func resolveTaskScheduleMeta(taskSvc contracts.TaskService, taskType string) (name string, maxRetry int, queue string) {
name = taskType
maxRetry = 3
queue = "default"
if taskSvc == nil {
return
}
meta, ok := taskSvc.GetTaskMeta(taskType)
if !ok {
return
}
name = meta.Name
if meta.MaxRetry > 0 {
maxRetry = meta.MaxRetry
}
if meta.Queue != "" {
queue = meta.Queue
}
return
}
@@ -366,27 +366,8 @@ func (s *taskServiceImpl) ListExecutions(ctx context.Context, taskType, status s
return nil, 0, err return nil, 0, err
} }
res := make([]contracts.TaskExecutionDTO, 0, len(rows)) res := make([]contracts.TaskExecutionDTO, 0, len(rows))
for _, r := range rows { for i := range rows {
res = append(res, contracts.TaskExecutionDTO{ res = append(res, toTaskExecutionDTO(&rows[i]))
ID: r.ID,
TaskID: r.TaskID,
TaskType: r.TaskType,
TaskName: r.TaskName,
Status: string(r.Status),
Retryable: r.Retryable,
MaxRetry: r.MaxRetry,
RetryCount: r.RetryCount,
Log: r.Log,
ErrorMessage: r.ErrorMessage,
Result: r.Result,
StartedAt: r.StartedAt,
FinishedAt: r.FinishedAt,
Duration: r.Duration,
Payload: r.Payload,
TriggeredBy: r.TriggeredBy,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
} }
return res, total, nil return res, total, nil
} }
@@ -416,7 +397,12 @@ func (s *taskServiceImpl) GetExecution(ctx context.Context, id uint64) (*contrac
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &contracts.TaskExecutionDTO{ dto := toTaskExecutionDTO(exec)
return &dto, nil
}
func toTaskExecutionDTO(exec *TaskExecution) contracts.TaskExecutionDTO {
return contracts.TaskExecutionDTO{
ID: exec.ID, ID: exec.ID,
TaskID: exec.TaskID, TaskID: exec.TaskID,
TaskType: exec.TaskType, TaskType: exec.TaskType,
@@ -435,5 +421,5 @@ func (s *taskServiceImpl) GetExecution(ctx context.Context, id uint64) (*contrac
TriggeredBy: exec.TriggeredBy, TriggeredBy: exec.TriggeredBy,
CreatedAt: exec.CreatedAt, CreatedAt: exec.CreatedAt,
UpdatedAt: exec.UpdatedAt, UpdatedAt: exec.UpdatedAt,
}, nil }
} }
@@ -12,6 +12,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@@ -78,7 +79,7 @@ func (s *inprocScheduler) registerJob(ctx context.Context, def extpoints.Schedul
spec := def.Spec spec := def.Spec
taskType := def.TaskType taskType := def.TaskType
fields := len(cronFields(spec)) fields := len(strings.Fields(spec))
cronSpec := spec cronSpec := spec
if fields == standardCronFields { if fields == standardCronFields {
cronSpec = "0 " + spec cronSpec = "0 " + spec
@@ -141,22 +142,3 @@ func invokeHandler(ctx context.Context, handler any, payload []byte) error {
return fmt.Errorf("unsupported handler type: %T", handler) return fmt.Errorf("unsupported handler type: %T", handler)
} }
} }
func cronFields(s string) []string {
var fields []string
var current []rune
for _, r := range s {
if r == ' ' || r == '\t' {
if len(current) > 0 {
fields = append(fields, string(current))
current = nil
}
} else {
current = append(current, r)
}
}
if len(current) > 0 {
fields = append(fields, string(current))
}
return fields
}
@@ -101,7 +101,7 @@ func (q *InprocQueue) Start(ctx context.Context) {
q.wg.Add(1) q.wg.Add(1)
util.Go(func() { util.Go(func() {
defer q.wg.Done() defer q.wg.Done()
q.workerLoop() q.workerLoop(ctx)
}) })
} }
} }
@@ -128,21 +128,23 @@ func (q *InprocQueue) Stop(ctx context.Context) error {
} }
} }
func (q *InprocQueue) workerLoop() { func (q *InprocQueue) workerLoop(ctx context.Context) {
for { for {
select { select {
case <-q.stopCh: case <-q.stopCh:
return return
case <-ctx.Done():
return
case msg, ok := <-q.queue: case msg, ok := <-q.queue:
if !ok { if !ok {
return return
} }
q.executeTask(msg) q.executeTask(ctx, msg)
} }
} }
} }
func (q *InprocQueue) executeTask(msg TaskMessage) { func (q *InprocQueue) executeTask(ctx context.Context, msg TaskMessage) {
if q.taskReg == nil { if q.taskReg == nil {
return return
} }
@@ -157,7 +159,7 @@ func (q *InprocQueue) executeTask(msg TaskMessage) {
timeout = 5 * time.Minute timeout = 5 * time.Minute
} }
taskCtx, cancel := context.WithTimeout(q.baseCtx, timeout) taskCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
err := invokeHandler(taskCtx, td.Handler, msg.Payload) err := invokeHandler(taskCtx, td.Handler, msg.Payload)
@@ -165,7 +167,13 @@ func (q *InprocQueue) executeTask(msg TaskMessage) {
msg.RetryLeft-- msg.RetryLeft--
// Retry with backoff // Retry with backoff
util.Go(func() { util.Go(func() {
time.Sleep(defaultRetryBackoff) select {
case <-time.After(defaultRetryBackoff):
case <-q.stopCh:
return
case <-ctx.Done():
return
}
if q.running.Load() { if q.running.Load() {
select { select {
case q.queue <- msg: case q.queue <- msg:
+3 -3
View File
@@ -106,12 +106,12 @@ else
log_pass "backend/pkg/ 零插件依赖" log_pass "backend/pkg/ 零插件依赖"
fi fi
# 3.2 pkg/util/ 严禁导入 ORM / Session 框架 # 3.2 pkg/util/ 严禁导入 Gin / ORM / Session 框架
UTIL_FRAMEWORK_IMPORTS=$(rg -n '"gorm.io/gorm"|"github.com/gorilla/sessions"' \ UTIL_FRAMEWORK_IMPORTS=$(rg -n '"gorm.io/gorm"|"github.com/gorilla/sessions"|"github.com/gin-gonic/gin"' \
"${BACKEND_DIR}/pkg/util/" --glob '*.go' -g '!*_test.go' || true) "${BACKEND_DIR}/pkg/util/" --glob '*.go' -g '!*_test.go' || true)
if [ -n "${UTIL_FRAMEWORK_IMPORTS}" ]; then if [ -n "${UTIL_FRAMEWORK_IMPORTS}" ]; then
log_fail "backend/pkg/util/ 必须保持纯粹,禁止导入 gorm、sessions 等数据库/会话框架包:" log_fail "backend/pkg/util/ 必须保持纯粹,禁止导入 gin、gorm、sessions 等 Web/数据库/会话框架包:"
echo "${UTIL_FRAMEWORK_IMPORTS}" >&2 echo "${UTIL_FRAMEWORK_IMPORTS}" >&2
else else
log_pass "backend/pkg/util/ 保持纯净无状态" log_pass "backend/pkg/util/ 保持纯净无状态"