优化结构
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"kra/pkg/database/migration"
|
||||
platformmodule "kra/pkg/module"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{
|
||||
ID: "202608220001_task_schema",
|
||||
Migrate: func(db *gorm.DB) error {
|
||||
return migration.CreateMissingTables(db, &TaskPO{}, &TaskLogPO{})
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// SeedDefaults inserts module-contributed timed tasks without making the
|
||||
// system data package own task table shapes.
|
||||
func SeedDefaults(ctx context.Context, db *gorm.DB, defaults []platformmodule.TimedTask) error {
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
if len(defaults) == 0 {
|
||||
defaults = []platformmodule.TimedTask{
|
||||
{Name: "ClearDB", Description: "定时清理数据库过期日志", Spec: "@daily", MethodName: "ClearDB", Enabled: true},
|
||||
{Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", MethodName: "CleanStaleUploads", Enabled: true},
|
||||
}
|
||||
}
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, item := range defaults {
|
||||
if strings.TrimSpace(item.Name) == "" {
|
||||
continue
|
||||
}
|
||||
row := TaskPO{Name: item.Name, Description: item.Description, Spec: item.Spec, WithSeconds: item.WithSeconds, ExecutorType: "method", MethodName: item.MethodName, Enabled: item.Enabled}
|
||||
if err := tx.Where("name = ?", row.Name).FirstOrCreate(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package task
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
// Provider is the minimal database seam required by the task tables.
|
||||
// Task persistence must not depend on the full data runtime or system repos.
|
||||
type Provider interface {
|
||||
DB() *gorm.DB
|
||||
DatabaseReady() bool
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package task
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewTaskRepo)
|
||||
@@ -0,0 +1,155 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
taskbiz "kra/internal/biz/task"
|
||||
"kra/pkg/database/gormkit"
|
||||
"kra/pkg/database/pagination"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TaskPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
Name string `gorm:"index"`
|
||||
Description, Spec string
|
||||
WithSeconds bool
|
||||
ExecutorType, MethodName string
|
||||
Params gormkit.JSON
|
||||
HTTPURL string
|
||||
HTTPMethod string
|
||||
HTTPHeader gormkit.JSON
|
||||
HTTPBody string `gorm:"type:text"`
|
||||
HTTPAllowPrivate, Enabled bool
|
||||
}
|
||||
|
||||
func (TaskPO) TableName() string { return "sys_timed_tasks" }
|
||||
|
||||
type TaskLogPO struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
TaskID uint `gorm:"index"`
|
||||
TaskName, TriggerType string
|
||||
StartedAt, FinishedAt time.Time
|
||||
DurationMS int64
|
||||
Status string
|
||||
ErrorMsg, Output string `gorm:"type:text"`
|
||||
}
|
||||
|
||||
func (TaskLogPO) TableName() string { return "sys_timed_task_logs" }
|
||||
|
||||
type taskRepo struct{ data Provider }
|
||||
|
||||
func NewTaskRepo(data Provider) taskbiz.TaskRepo { return &taskRepo{data: data} }
|
||||
func taskToPO(v *taskbiz.TimedTask) TaskPO {
|
||||
return TaskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
||||
}
|
||||
func taskFromPO(v TaskPO) *taskbiz.TimedTask {
|
||||
return &taskbiz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
|
||||
}
|
||||
func (r *taskRepo) CreateTask(ctx context.Context, v *taskbiz.TimedTask) error {
|
||||
po := taskToPO(v)
|
||||
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
v.ID = po.ID
|
||||
return nil
|
||||
}
|
||||
func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID uint) (bool, error) {
|
||||
var count int64
|
||||
db := r.data.DB().WithContext(ctx).Model(&TaskPO{}).Where("name = ?", name)
|
||||
if excludeID > 0 {
|
||||
db = db.Where("id <> ?", excludeID)
|
||||
}
|
||||
err := db.Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
func (r *taskRepo) UpdateTask(ctx context.Context, v *taskbiz.TimedTask) error {
|
||||
po := taskToPO(v)
|
||||
return r.data.DB().WithContext(ctx).Model(&TaskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
|
||||
}
|
||||
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
|
||||
return r.data.DB().WithContext(ctx).Delete(&TaskPO{}, id).Error
|
||||
}
|
||||
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*taskbiz.TimedTask, error) {
|
||||
var po TaskPO
|
||||
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return taskFromPO(po), nil
|
||||
}
|
||||
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *taskbiz.TimedTask) ([]*taskbiz.TimedTask, int64, error) {
|
||||
// During first-install the data layer intentionally serves a bootstrap
|
||||
// database without system tables. The scheduler starts before /init/initdb
|
||||
// and should remain idle instead of logging a missing-table SQL error.
|
||||
if !r.data.DatabaseReady() {
|
||||
return []*taskbiz.TimedTask{}, 0, nil
|
||||
}
|
||||
db := r.data.DB().WithContext(ctx).Model(&TaskPO{})
|
||||
if q != nil {
|
||||
if q.Name != "" {
|
||||
db = db.Where("name LIKE ?", "%"+q.Name+"%")
|
||||
}
|
||||
if q.ExecutorType != "" {
|
||||
db = db.Where("executor_type = ?", q.ExecutorType)
|
||||
}
|
||||
if q.EnabledFilter != nil {
|
||||
db = db.Where("enabled = ?", *q.EnabledFilter)
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []TaskPO
|
||||
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*taskbiz.TimedTask, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, taskFromPO(po))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
|
||||
return r.data.DB().WithContext(ctx).Model(&TaskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
|
||||
}
|
||||
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *taskbiz.TimedTaskLog) error {
|
||||
return r.data.DB().WithContext(ctx).Create(&TaskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
|
||||
}
|
||||
func taskLogFromPO(v TaskLogPO) *taskbiz.TimedTaskLog {
|
||||
return &taskbiz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
|
||||
}
|
||||
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*taskbiz.TimedTaskLog, int64, error) {
|
||||
db := r.data.DB().WithContext(ctx).Model(&TaskLogPO{})
|
||||
if taskID != 0 {
|
||||
db = db.Where("task_id = ?", taskID)
|
||||
}
|
||||
if status != "" {
|
||||
db = db.Where("status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var pos []TaskLogPO
|
||||
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
out := make([]*taskbiz.TimedTaskLog, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
out = append(out, taskLogFromPO(po))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
func (r *taskRepo) CleanupLogs(ctx context.Context) error {
|
||||
return r.data.DB().WithContext(ctx).Unscoped().
|
||||
Where("created_at < ?", time.Now().Add(-720*time.Hour)).
|
||||
Delete(&TaskLogPO{}).Error
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTaskRepoListIsEmptyBeforeDatabaseInitialization(t *testing.T) {
|
||||
repo := NewTaskRepo(&Data{})
|
||||
items, total, err := repo.ListTasks(context.Background(), 0, 0, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 0 || len(items) != 0 {
|
||||
t.Fatalf("bootstrap tasks = (%d, %d), want empty", total, len(items))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
gormDB *reloadableDB
|
||||
databaseReady atomic.Bool
|
||||
}
|
||||
|
||||
func (d *Data) DB() *gorm.DB {
|
||||
if d == nil || d.gormDB == nil {
|
||||
return nil
|
||||
}
|
||||
return d.gormDB.DB()
|
||||
}
|
||||
|
||||
func (d *Data) DatabaseReady() bool { return d != nil && d.databaseReady.Load() }
|
||||
|
||||
type reloadableDB struct{ db *gorm.DB }
|
||||
|
||||
func newReloadableDB(db *gorm.DB) *reloadableDB { return &reloadableDB{db: db} }
|
||||
func (r *reloadableDB) DB() *gorm.DB {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.db
|
||||
}
|
||||
func (r *reloadableDB) WithContext(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx) }
|
||||
|
||||
func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||
if driver != "sqlite" {
|
||||
return nil, fmt.Errorf("unsupported test database driver %q", driver)
|
||||
}
|
||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
}
|
||||
Reference in New Issue
Block a user