refactor(admin): decouple system cleanup with event bus and enforce single owner principle

This commit is contained in:
ryan
2026-09-03 08:50:37 +08:00
parent 1e19d8114a
commit 6b28adfacf
14 changed files with 419 additions and 50 deletions
+12 -1
View File
@@ -21,6 +21,12 @@ import (
// SystemConfig aliases model.SystemConfig for external compatibility.
type SystemConfig = model.SystemConfig
// TaskExecution aliases model.TaskExecution for external compatibility.
type TaskExecution = model.TaskExecution
// Schedule aliases model.Schedule for external compatibility.
type Schedule = model.Schedule
//go:embed migrations/*/*.sql
var adminMigrations embed.FS
@@ -132,12 +138,17 @@ func (p *Plugin) Apply(ctx *core.Context) error {
ctx.Router().RegisterWhitelist("/robots.txt")
// 2. Register Background Tasks
const defaultCleanupRetry = 3
ctx.Task().Register(service.LogDBSwitchTask, &service.LogDBSwitchHandler{}, extpoints.WithTaskMeta(service.LogDBSwitchMeta))
ctx.Task().Register(service.SystemCleanupTask, &service.SystemCleanupHandler{}, extpoints.WithTaskMeta(service.SystemCleanupMeta), extpoints.WithTaskRetry(defaultCleanupRetry))
// 2.1 Register Cron Schedule
ctx.Schedule().RegisterCron("0 3 * * *", service.SystemCleanupTask, nil)
// 3. Register Settings Schemas
ctx.Settings().Register(extpoints.SettingSchema{
Key: "admin.system_cleanup_cron",
Default: "0 4 * * *",
Default: "0 3 * * *",
Description: "Cron expression for nightly system logs and expired tokens cleanup",
Type: "string",
Category: "maintenance",
+3 -1
View File
@@ -36,11 +36,13 @@ func TestAdminPluginUnit(t *testing.T) {
// Verify tasks
_, ok := ctx.Tasks().Get("logs:db_switch")
require.True(t, ok)
_, ok = ctx.Tasks().Get("system:cleanup")
require.True(t, ok)
// Verify settings
setting, ok := ctx.Settings().Get("admin.system_cleanup_cron")
require.True(t, ok)
assert.Equal(t, "0 4 * * *", setting.Default)
assert.Equal(t, "0 3 * * *", setting.Default)
provider, err := core.Inject[contracts.PublicConfigProvider](ctx)
require.NoError(t, err)
@@ -0,0 +1,70 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package service
import (
"Wavelet/core/contracts"
"Wavelet/pkg/logger"
"Wavelet/plugins/domain/admin/model"
"context"
"errors"
"fmt"
"time"
)
const (
// SystemCleanupTask 系统定期垃圾清理任务标识
SystemCleanupTask = "system:cleanup"
// TaskTypeSystemCleanup 系统定期垃圾清理管理类型
TaskTypeSystemCleanup = "system_cleanup"
taskQueueDefault = "default"
)
// SystemCleanupMeta describes the system-wide cleanup task metadata.
var SystemCleanupMeta = contracts.TaskMetaDTO{
Type: TaskTypeSystemCleanup,
AsynqTask: SystemCleanupTask,
Name: "系统垃圾清理",
DisplayName: "系统垃圾清理",
Description: "定期清理过期任务执行记录,并通过领域事件广播触发各业务域自治清理(临时文件、历史推送等)",
Category: "maintenance",
SupportsTime: false,
MaxRetry: 3,
Queue: taskQueueDefault,
Retryable: true,
}
// SystemCleanupHandler handles the system-wide garbage cleanup task.
type SystemCleanupHandler struct{}
// Execute executes system cleanup: clears old task executions and emits EventTopicSystemCleanup.
func (h *SystemCleanupHandler) Execute(ctx context.Context, _ []byte) (*contracts.TaskResultDTO, error) {
db := GetDB(ctx)
if db == nil {
return nil, errors.New("database service not available")
}
// 1. 清理自身域(admin 域)的过期任务执行记录(7 天前)
var deletedExecutions int64
sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
res := db.Where("created_at < ?", sevenDaysAgo).Delete(&model.TaskExecution{})
if err := res.Error; err != nil {
logger.WarnF(ctx, "清理过期任务执行日志失败: %v", err)
} else {
deletedExecutions = res.RowsAffected
logger.InfoF(ctx, "已清理 7 天前任务执行日志,共 %d 条", deletedExecutions)
}
// 2. 广播 EventTopicSystemCleanup 领域事件,由各业务域插件(upload, msg_gateway, user 等)自治执行各自的清理逻辑
nowStr := time.Now().Format(time.RFC3339)
if err := EmitEvent(ctx, contracts.EventTopicSystemCleanup, contracts.SystemCleanupEvent{
TriggeredAt: nowStr,
}); err != nil {
logger.WarnF(ctx, "广播系统清理领域事件失败: %v", err)
}
msg := fmt.Sprintf("系统垃圾清理完成,已清理过期任务执行日志 %d 条,并已广播领域清理事件", deletedExecutions)
logger.InfoF(ctx, "%s", msg)
return &contracts.TaskResultDTO{Message: msg}, nil
}
@@ -0,0 +1,78 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package service_test
import (
"Wavelet/core/contracts"
"Wavelet/plugins/domain/admin/model"
"Wavelet/plugins/domain/admin/service"
"context"
"sync/atomic"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestSystemCleanupHandler_Execute(t *testing.T) {
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, sqliteDB.AutoMigrate(&model.TaskExecution{}))
service.SetDBService(&testDBService{db: sqliteDB})
defer service.ResetServices()
now := time.Now()
oldTime := now.Add(-10 * 24 * time.Hour)
recentTime := now.Add(-1 * time.Hour)
// Seed old execution (should be cleaned)
oldExec := model.TaskExecution{
ID: 1,
TaskID: "task-old",
TaskType: "sample_task",
Status: "success",
CreatedAt: oldTime,
}
require.NoError(t, sqliteDB.Create(&oldExec).Error)
// Seed recent execution (should remain)
recentExec := model.TaskExecution{
ID: 2,
TaskID: "task-recent",
TaskType: "sample_task",
Status: "success",
CreatedAt: recentTime,
}
require.NoError(t, sqliteDB.Create(&recentExec).Error)
var eventFired atomic.Bool
service.SetEventEmitter(func(ctx context.Context, topic string, payload any) error {
if topic == contracts.EventTopicSystemCleanup {
eventFired.Store(true)
}
return nil
})
handler := &service.SystemCleanupHandler{}
res, err := handler.Execute(context.Background(), nil)
require.NoError(t, err)
require.NotNil(t, res)
assert.Contains(t, res.Message, "系统垃圾清理完成")
// Verify old execution is deleted and recent remains
var count int64
sqliteDB.Model(&model.TaskExecution{}).Count(&count)
assert.Equal(t, int64(1), count)
var remaining model.TaskExecution
sqliteDB.First(&remaining)
assert.Equal(t, uint64(2), remaining.ID)
assert.True(t, eventFired.Load(), "EventTopicSystemCleanup must be emitted")
}