mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
feat(push): implement system notification and push framework
This commit is contained in:
@@ -115,7 +115,7 @@ func ListFiles(c *gin.Context) {
|
||||
// @Router /api/v1/admin/uploads/{id} [delete]
|
||||
func DeleteFile(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if storageReadOnly(ctx) {
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func ListMyFiles(c *gin.Context) {
|
||||
func DeleteMyFile(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if storageReadOnly(ctx) {
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
@@ -313,7 +313,7 @@ type updateMyFileRequest struct {
|
||||
func UpdateMyFile(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
if storageReadOnly(ctx) {
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if storageReadOnly(ctx) {
|
||||
if StorageReadOnly(ctx) {
|
||||
c.JSON(http.StatusConflict, util.Err(ErrStorageReadOnly))
|
||||
return true, nil
|
||||
}
|
||||
@@ -396,7 +396,7 @@ func tryInstantUpload(ctx context.Context, c *gin.Context, currUser *model.User,
|
||||
|
||||
// storeUploadFile 将文件写入当前活动存储驱动。
|
||||
func storeUploadFile(ctx context.Context, subPath string, size int64, mimeType string, buf *bytes.Buffer, meta *model.UploadMetadata) (string, string, string) {
|
||||
if storageReadOnly(ctx) {
|
||||
if StorageReadOnly(ctx) {
|
||||
return "", "", ErrStorageReadOnly
|
||||
}
|
||||
driver, backend, err := storage.Active(ctx)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
)
|
||||
|
||||
func storageReadOnly(ctx context.Context) bool {
|
||||
func StorageReadOnly(ctx context.Context) bool {
|
||||
execution, ok, err := latestStorageMigrationExecution(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "读取存储维护状态失败: %v", err)
|
||||
@@ -66,5 +66,3 @@ func currentMigrationTargetConfig(ctx context.Context) (storage.Config, bool, er
|
||||
}
|
||||
return target, true, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,21 +11,14 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 异步任务名称与管理类型定义
|
||||
const (
|
||||
// CleanupUnusedUploadsTask 清理未使用上传任务标识
|
||||
CleanupUnusedUploadsTask = "upload:cleanup_unused"
|
||||
// TaskTypeCleanupUploads 清理未使用上传管理类型
|
||||
TaskTypeCleanupUploads = "cleanup_unused_uploads"
|
||||
// WarmImageCacheTask 图片压缩缓存预热任务标识
|
||||
WarmImageCacheTask = "upload:warm_image_cache"
|
||||
// TaskTypeWarmImageCache 图片压缩缓存预热管理类型
|
||||
@@ -34,18 +27,6 @@ const (
|
||||
|
||||
var warmImageCacheMu sync.Mutex
|
||||
|
||||
// CleanupUnusedUploadsMeta represents the task metadata.
|
||||
var CleanupUnusedUploadsMeta = task.TaskMeta{
|
||||
Type: TaskTypeCleanupUploads,
|
||||
AsynqTask: CleanupUnusedUploadsTask,
|
||||
Name: "清理未使用上传",
|
||||
Description: "清理超过1小时未使用的上传文件",
|
||||
SupportsTime: false,
|
||||
MaxRetry: task.DefaultMaxRetry,
|
||||
Queue: task.QueueDefault,
|
||||
Retryable: true,
|
||||
}
|
||||
|
||||
// WarmImageCacheMeta represents the image cache warmup task metadata.
|
||||
var WarmImageCacheMeta = task.TaskMeta{
|
||||
Type: TaskTypeWarmImageCache,
|
||||
@@ -73,86 +54,12 @@ type WarmImageCachePayload struct {
|
||||
Quality string `json:"quality"`
|
||||
}
|
||||
|
||||
// CleanupUnusedUploadsHandler 清理未使用上传文件的异步任务处理器
|
||||
type CleanupUnusedUploadsHandler struct{}
|
||||
// SystemCleanupHandler 系统定期垃圾清理异步任务处理器
|
||||
|
||||
// WarmImageCacheHandler serially warms compressed image cache entries.
|
||||
type WarmImageCacheHandler struct{}
|
||||
|
||||
// Execute 执行清理未使用上传文件的业务逻辑
|
||||
func (h *CleanupUnusedUploadsHandler) Execute(ctx context.Context, _ []byte) (*task.TaskResult, error) {
|
||||
if storageReadOnly(ctx) {
|
||||
return nil, errors.New(ErrStorageReadOnly)
|
||||
}
|
||||
const batchSize = 100 // 每批处理100个文件
|
||||
var lastID uint64
|
||||
var totalProcessed int
|
||||
var totalDeleted int
|
||||
|
||||
// 计算1小时前的时间
|
||||
oneHourAgo := time.Now().Add(-1 * time.Hour)
|
||||
|
||||
task.AppendLog(ctx, "开始扫描未使用上传文件,阈值: %s", oneHourAgo.Format(time.RFC3339))
|
||||
|
||||
for {
|
||||
// 使用游标分页查询未使用且超过1小时的上传记录
|
||||
var unusedUploads []model.Upload
|
||||
if err := db.DB(ctx).
|
||||
Where("id > ? AND status = ? AND created_at < ?", lastID, model.UploadStatusPending, oneHourAgo).
|
||||
Order("id ASC").
|
||||
Limit(batchSize).
|
||||
Find(&unusedUploads).Error; err != nil {
|
||||
task.AppendLog(ctx, "查询未使用的上传文件失败: %v", err)
|
||||
return nil, fmt.Errorf(ErrQueryUnusedUploadsFailed, err)
|
||||
}
|
||||
|
||||
// 没有更多数据,退出循环
|
||||
if len(unusedUploads) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
task.AppendLog(ctx, "本批次找到 %d 个需要清理的上传文件", len(unusedUploads))
|
||||
|
||||
// 处理每个未使用的上传文件
|
||||
for _, upload := range unusedUploads {
|
||||
totalProcessed++
|
||||
|
||||
if err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 更新上传记录状态
|
||||
if err := tx.Model(&model.Upload{}).
|
||||
Where("id = ? AND status = ?", upload.ID, model.UploadStatusPending).
|
||||
Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
driver := storage.Driver(upload.StorageDriver)
|
||||
if driver == "" {
|
||||
driver = storage.DriverLocal
|
||||
}
|
||||
backend, err := storage.ForDriver(ctx, driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := backend.Delete(ctx, upload.FilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
task.AppendLog(ctx, "清理上传文件失败 [ID:%d]: %v", upload.ID, err)
|
||||
lastID = upload.ID
|
||||
continue
|
||||
}
|
||||
|
||||
totalDeleted++
|
||||
lastID = upload.ID
|
||||
}
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("共处理 %d 个文件,成功删除 %d 个", totalProcessed, totalDeleted)
|
||||
task.AppendLog(ctx, "%s", msg)
|
||||
return &task.TaskResult{Message: msg}, nil
|
||||
}
|
||||
// Execute 执行系统清理(包含文件清理和历史消息推送日志清理)
|
||||
|
||||
// ValidatePayload validates and normalizes image cache warmup parameters.
|
||||
func (h *WarmImageCacheHandler) ValidatePayload(payload []byte) ([]byte, error) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/diskcache"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/service"
|
||||
"github.com/Rain-kl/Wavelet/internal/storage"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"github.com/Rain-kl/Wavelet/internal/testhelper"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCleanupUnusedUploadsHandler_Execute(t *testing.T) {
|
||||
func TestSystemCleanupHandler_Execute(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -42,6 +43,8 @@ func TestCleanupUnusedUploadsHandler_Execute(t *testing.T) {
|
||||
defer storageMock()
|
||||
|
||||
ctx := context.Background()
|
||||
err := db.DB(ctx).AutoMigrate(&model.PushHistory{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 准备测试数据:创建一些上传记录
|
||||
now := time.Now()
|
||||
@@ -81,14 +84,40 @@ func TestCleanupUnusedUploadsHandler_Execute(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 准备推送历史测试数据:1个旧的(应删除),1个新的(应保留)
|
||||
oldPush := &model.PushHistory{
|
||||
EventKey: "admin_login",
|
||||
Channel: "email",
|
||||
Target: "admin@test.com",
|
||||
Title: "Old Login",
|
||||
Content: "Old Content",
|
||||
Level: "INFO",
|
||||
Status: "success",
|
||||
CreatedAt: now.AddDate(0, 0, -10),
|
||||
}
|
||||
newPush := &model.PushHistory{
|
||||
EventKey: "admin_login",
|
||||
Channel: "lark",
|
||||
Target: "http://webhook.com",
|
||||
Title: "New Login",
|
||||
Content: "New Content",
|
||||
Level: "INFO",
|
||||
Status: "success",
|
||||
CreatedAt: now,
|
||||
}
|
||||
err = db.DB(ctx).Create(oldPush).Error
|
||||
require.NoError(t, err)
|
||||
err = db.DB(ctx).Create(newPush).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// 执行 handler
|
||||
handler := &CleanupUnusedUploadsHandler{}
|
||||
handler := &service.SystemCleanupHandler{}
|
||||
result, err := handler.Execute(ctx, nil)
|
||||
|
||||
// 验证结果
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Contains(t, result.Message, "共处理 2 个文件,成功删除 2 个")
|
||||
assert.Contains(t, result.Message, "系统清理完成。成功清理未使用的上传文件 2/2 个;清理历史推送审计日志 1 条。")
|
||||
|
||||
// 验证数据库状态:pending 且超过1小时的应被标记为 deleted
|
||||
var pendingCount int64
|
||||
@@ -102,9 +131,19 @@ func TestCleanupUnusedUploadsHandler_Execute(t *testing.T) {
|
||||
var usedCount int64
|
||||
db.DB(ctx).Model(&model.Upload{}).Where("status = ?", model.UploadStatusUsed).Count(&usedCount)
|
||||
assert.Equal(t, int64(1), usedCount, "used 状态的文件不应受影响")
|
||||
|
||||
// 验证推送历史数据状态:10天前的应被删除,今天的应保留
|
||||
var pushCount int64
|
||||
db.DB(ctx).Model(&model.PushHistory{}).Count(&pushCount)
|
||||
assert.Equal(t, int64(1), pushCount, "应只剩1条推送历史记录")
|
||||
|
||||
var remainingPush model.PushHistory
|
||||
err = db.DB(ctx).First(&remainingPush).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "New Login", remainingPush.Title)
|
||||
}
|
||||
|
||||
func TestCleanupUnusedUploadsHandler_ExecuteNoFiles(t *testing.T) {
|
||||
func TestSystemCleanupHandler_ExecuteNoFiles(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
@@ -119,19 +158,21 @@ func TestCleanupUnusedUploadsHandler_ExecuteNoFiles(t *testing.T) {
|
||||
defer storageMock()
|
||||
|
||||
ctx := context.Background()
|
||||
err := db.DB(ctx).AutoMigrate(&model.PushHistory{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 没有任何上传记录
|
||||
handler := &CleanupUnusedUploadsHandler{}
|
||||
handler := &service.SystemCleanupHandler{}
|
||||
result, err := handler.Execute(ctx, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
assert.Contains(t, result.Message, "共处理 0 个文件,成功删除 0 个")
|
||||
assert.Contains(t, result.Message, "系统清理完成。成功清理未使用的上传文件 0/0 个;清理历史推送审计日志 0 条。")
|
||||
}
|
||||
|
||||
func TestCleanupUnusedUploadsHandler_ImplementsTaskHandler(t *testing.T) {
|
||||
// 编译期验证 CleanupUnusedUploadsHandler 实现了 TaskHandler 接口
|
||||
var _ task.TaskHandler = (*CleanupUnusedUploadsHandler)(nil)
|
||||
func TestSystemCleanupHandler_ImplementsTaskHandler(t *testing.T) {
|
||||
// 编译期验证 SystemCleanupHandler 实现了 TaskHandler 接口
|
||||
var _ task.TaskHandler = (*service.SystemCleanupHandler)(nil)
|
||||
}
|
||||
|
||||
func TestWarmImageCacheHandlerValidatePayload(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user