调度中心嵌入cms

This commit is contained in:
vet
2026-05-28 13:29:24 +08:00
parent 52446ccf3f
commit 5f93d025d2
17 changed files with 262 additions and 20 deletions

1
.env
View File

@@ -8,4 +8,5 @@ MONGO_AUTHSOURCE=admin
MONGO_DATABASE=openim_v3 MONGO_DATABASE=openim_v3
SCHEDULER_MONGO_DATABASE=scheduler_center SCHEDULER_MONGO_DATABASE=scheduler_center
REDIS_ADDR=127.0.0.1:6379 REDIS_ADDR=127.0.0.1:6379
REDIS_USERNAME=
REDIS_PASSWORD= REDIS_PASSWORD=

View File

@@ -8,4 +8,5 @@ MONGO_AUTHSOURCE=admin
MONGO_DATABASE=openim_v3 MONGO_DATABASE=openim_v3
SCHEDULER_MONGO_DATABASE=scheduler_center SCHEDULER_MONGO_DATABASE=scheduler_center
REDIS_ADDR=127.0.0.1:6379 REDIS_ADDR=127.0.0.1:6379
REDIS_USERNAME=
REDIS_PASSWORD= REDIS_PASSWORD=

View File

@@ -7,3 +7,6 @@ MONGO_PASSWORD=rI57PJsJhnz_qlRkfnTa0RPT
MONGO_AUTHSOURCE=admin MONGO_AUTHSOURCE=admin
MONGO_DATABASE=openim_v3 MONGO_DATABASE=openim_v3
SCHEDULER_MONGO_DATABASE=scheduler_center_local SCHEDULER_MONGO_DATABASE=scheduler_center_local
REDIS_ADDR=127.0.0.1:6379
REDIS_USERNAME=
REDIS_PASSWORD=

View File

@@ -7,3 +7,6 @@ MONGO_PASSWORD=rI57PJsJhnz_qlRkfnTa0RPT
MONGO_AUTHSOURCE=admin MONGO_AUTHSOURCE=admin
MONGO_DATABASE=openim_v3 MONGO_DATABASE=openim_v3
SCHEDULER_MONGO_DATABASE=scheduler_center SCHEDULER_MONGO_DATABASE=scheduler_center
REDIS_ADDR=127.0.0.1:6379
REDIS_USERNAME=
REDIS_PASSWORD=

View File

@@ -56,6 +56,9 @@ MONGO_PASSWORD=
MONGO_AUTHSOURCE=admin MONGO_AUTHSOURCE=admin
MONGO_DATABASE=openim_v3 MONGO_DATABASE=openim_v3
SCHEDULER_MONGO_DATABASE=scheduler_center SCHEDULER_MONGO_DATABASE=scheduler_center
REDIS_ADDR=127.0.0.1:6379
REDIS_USERNAME=
REDIS_PASSWORD=
``` ```
## 启动方式 ## 启动方式

BIN
api

Binary file not shown.

View File

@@ -14,6 +14,7 @@ import (
storemongo "scheduler-backend/internal/store/mongo" storemongo "scheduler-backend/internal/store/mongo"
"scheduler-backend/pkg/config" "scheduler-backend/pkg/config"
"scheduler-backend/pkg/log" "scheduler-backend/pkg/log"
redisclient "scheduler-backend/pkg/redis"
) )
func main() { func main() {
@@ -31,6 +32,11 @@ func main() {
logger.Error("scheduler api mongo connect failed", "error", err) logger.Error("scheduler api mongo connect failed", "error", err)
os.Exit(1) os.Exit(1)
} }
redisConn, err := redisclient.Connect(rootCtx, cfg)
if err != nil {
logger.Error("scheduler api redis connect failed", "error", err)
os.Exit(1)
}
jobConfigStore := storemongo.NewJobConfigStore(databases.MetaDB) jobConfigStore := storemongo.NewJobConfigStore(databases.MetaDB)
if err := jobdef.SyncJobConfigs(rootCtx, "job-config-list", jobConfigStore, logger); err != nil { if err := jobdef.SyncJobConfigs(rootCtx, "job-config-list", jobConfigStore, logger); err != nil {
logger.Error("sync job configs failed", "error", err) logger.Error("sync job configs failed", "error", err)
@@ -42,16 +48,18 @@ func main() {
Logger: logger, Logger: logger,
MetaDB: databases.MetaDB, MetaDB: databases.MetaDB,
BusinessDB: databases.BusinessDB, BusinessDB: databases.BusinessDB,
Redis: redisConn,
}) })
router := api.NewRouter(api.RouterDeps{ router := api.NewRouter(api.RouterDeps{
Registry: registry, Registry: registry,
JobConfigStore: jobConfigStore, JobConfigStore: jobConfigStore,
ExecutionStore: storemongo.NewJobExecutionStore(databases.MetaDB), ExecutionStore: storemongo.NewJobExecutionStore(databases.MetaDB),
ProfileStore: storemongo.NewAdminProfileStore(databases.MetaDB), ProfileStore: storemongo.NewAdminProfileStore(databases.MetaDB),
AdminUserStore: storemongo.NewAdminUserStore(databases.MetaDB), AdminUserStore: storemongo.NewAdminUserStore(databases.MetaDB),
ConfigStore: storemongo.NewSystemConfigStore(databases.MetaDB), ConfigStore: storemongo.NewSystemConfigStore(databases.MetaDB),
Executor: execSvc, GlobalConfigStore: storemongo.NewGlobalConfigStore(databases.MetaDB),
Executor: execSvc,
}) })
logger.Info("scheduler api starting", "addr", addr) logger.Info("scheduler api starting", "addr", addr)
@@ -62,6 +70,7 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
_ = redisConn.Close()
_ = databases.Client.Disconnect(ctx) _ = databases.Client.Disconnect(ctx)
os.Exit(0) os.Exit(0)
}() }()

View File

@@ -15,6 +15,7 @@ import (
storemongo "scheduler-backend/internal/store/mongo" storemongo "scheduler-backend/internal/store/mongo"
"scheduler-backend/pkg/config" "scheduler-backend/pkg/config"
"scheduler-backend/pkg/log" "scheduler-backend/pkg/log"
redisclient "scheduler-backend/pkg/redis"
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
) )
@@ -33,6 +34,11 @@ func main() {
logger.Error("scheduler worker mongo connect failed", "error", err) logger.Error("scheduler worker mongo connect failed", "error", err)
os.Exit(1) os.Exit(1)
} }
redisConn, err := redisclient.Connect(rootCtx, cfg)
if err != nil {
logger.Error("scheduler worker redis connect failed", "error", err)
os.Exit(1)
}
jobConfigStore := storemongo.NewJobConfigStore(databases.MetaDB) jobConfigStore := storemongo.NewJobConfigStore(databases.MetaDB)
if err := jobdef.SyncJobConfigs(rootCtx, "job-config-list", jobConfigStore, logger); err != nil { if err := jobdef.SyncJobConfigs(rootCtx, "job-config-list", jobConfigStore, logger); err != nil {
@@ -45,6 +51,7 @@ func main() {
Logger: logger, Logger: logger,
MetaDB: databases.MetaDB, MetaDB: databases.MetaDB,
BusinessDB: databases.BusinessDB, BusinessDB: databases.BusinessDB,
Redis: redisConn,
} }
execSvc := executor.NewService(registry, runtime) execSvc := executor.NewService(registry, runtime)
g, err := gocron.NewScheduler() g, err := gocron.NewScheduler()
@@ -99,6 +106,9 @@ func main() {
if err := healthServer.Shutdown(shutdownCtx); err != nil { if err := healthServer.Shutdown(shutdownCtx); err != nil {
logger.Error("scheduler worker health server shutdown failed", "error", err) logger.Error("scheduler worker health server shutdown failed", "error", err)
} }
if err := redisConn.Close(); err != nil {
logger.Error("scheduler worker redis disconnect failed", "error", err)
}
if err := databases.Client.Disconnect(context.Background()); err != nil { if err := databases.Client.Disconnect(context.Background()); err != nil {
logger.Error("scheduler worker mongo disconnect failed", "error", err) logger.Error("scheduler worker mongo disconnect failed", "error", err)
} }

View File

@@ -51,6 +51,7 @@ type executionItem struct {
ID string `json:"id"` ID string `json:"id"`
JobName string `json:"jobName"` JobName string `json:"jobName"`
TriggerType string `json:"triggerType"` TriggerType string `json:"triggerType"`
ScheduleType string `json:"scheduleType,omitempty"`
Status string `json:"status"` Status string `json:"status"`
StartedAt string `json:"startedAt,omitempty"` StartedAt string `json:"startedAt,omitempty"`
FinishedAt string `json:"finishedAt,omitempty"` FinishedAt string `json:"finishedAt,omitempty"`
@@ -61,6 +62,7 @@ type executionDetailItem struct {
ID string `json:"id"` ID string `json:"id"`
JobConfigID string `json:"jobConfigId"` JobConfigID string `json:"jobConfigId"`
TriggerType string `json:"triggerType"` TriggerType string `json:"triggerType"`
ScheduleType string `json:"scheduleType,omitempty"`
Status string `json:"status"` Status string `json:"status"`
ParamsSnapshot string `json:"paramsSnapshot"` ParamsSnapshot string `json:"paramsSnapshot"`
StartedAt string `json:"startedAt,omitempty"` StartedAt string `json:"startedAt,omitempty"`
@@ -133,6 +135,12 @@ type dashboardOverviewResponse struct {
RecentExecutions []executionItem `json:"recentExecutions"` RecentExecutions []executionItem `json:"recentExecutions"`
} }
type globalConfigItem struct {
Config string `json:"config"`
UpdatedBy string `json:"updatedBy"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type JobConfigLister interface { type JobConfigLister interface {
List(ctx context.Context) ([]model.JobConfig, error) List(ctx context.Context) ([]model.JobConfig, error)
GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobConfig, error) GetByID(ctx context.Context, id primitive.ObjectID) (*model.JobConfig, error)
@@ -173,14 +181,20 @@ type SystemConfigStore interface {
ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error ToggleEnabled(ctx context.Context, id primitive.ObjectID, enabled bool) error
} }
type GlobalConfigStore interface {
Get(ctx context.Context, key string) (*model.GlobalConfig, error)
Upsert(ctx context.Context, item *model.GlobalConfig) error
}
type RouterDeps struct { type RouterDeps struct {
Registry *jobdef.Registry Registry *jobdef.Registry
JobConfigStore JobConfigLister JobConfigStore JobConfigLister
ExecutionStore JobExecutionLister ExecutionStore JobExecutionLister
ProfileStore AdminProfileStore ProfileStore AdminProfileStore
AdminUserStore AdminUserStore AdminUserStore AdminUserStore
ConfigStore SystemConfigStore ConfigStore SystemConfigStore
Executor *executor.Service GlobalConfigStore GlobalConfigStore
Executor *executor.Service
} }
type upsertJobRequest struct { type upsertJobRequest struct {
@@ -243,6 +257,12 @@ type toggleSystemConfigRequest struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
} }
type updateGlobalConfigRequest struct {
Config json.RawMessage `json:"config" binding:"required"`
}
const schedulerGlobalConfigKey = "scheduler"
func NewRouter(deps RouterDeps) *gin.Engine { func NewRouter(deps RouterDeps) *gin.Engine {
router := gin.Default() router := gin.Default()
router.Use(func(c *gin.Context) { router.Use(func(c *gin.Context) {
@@ -579,6 +599,35 @@ func NewRouter(deps RouterDeps) *gin.Engine {
"errorMessage": execRecord.ErrorMessage, "errorMessage": execRecord.ErrorMessage,
}) })
}) })
router.GET("/admin/scheduler/global-config", func(c *gin.Context) {
item, err := deps.GlobalConfigStore.Get(c.Request.Context(), schedulerGlobalConfigKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, toGlobalConfigItem(*item))
})
router.PUT("/admin/scheduler/global-config", func(c *gin.Context) {
var req updateGlobalConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !json.Valid(req.Config) {
c.JSON(http.StatusBadRequest, gin.H{"error": errInvalidJSON("config").Error()})
return
}
item := &model.GlobalConfig{
Key: schedulerGlobalConfigKey,
Config: string(req.Config),
UpdatedBy: "admin",
}
if err := deps.GlobalConfigStore.Upsert(c.Request.Context(), item); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, toGlobalConfigItem(*item))
})
router.GET("/admin/scheduler/executions", func(c *gin.Context) { router.GET("/admin/scheduler/executions", func(c *gin.Context) {
jobs, err := deps.JobConfigStore.List(c.Request.Context()) jobs, err := deps.JobConfigStore.List(c.Request.Context())
if err != nil { if err != nil {
@@ -592,16 +641,20 @@ func NewRouter(deps RouterDeps) *gin.Engine {
} }
jobNameMap := make(map[string]string, len(jobs)) jobNameMap := make(map[string]string, len(jobs))
jobScheduleTypeMap := make(map[string]string, len(jobs))
for _, item := range jobs { for _, item := range jobs {
jobNameMap[item.ID.Hex()] = item.Name jobNameMap[item.ID.Hex()] = item.Name
jobScheduleTypeMap[item.ID.Hex()] = item.ScheduleType
} }
responseItems := make([]executionItem, 0, len(items)) responseItems := make([]executionItem, 0, len(items))
for _, item := range items { for _, item := range items {
jobID := item.JobConfigID.Hex()
responseItems = append(responseItems, executionItem{ responseItems = append(responseItems, executionItem{
ID: objectIDToString(item.ID), ID: objectIDToString(item.ID),
JobName: jobDisplayName(jobNameMap, item.JobConfigID), JobName: jobDisplayName(jobNameMap, item.JobConfigID),
TriggerType: item.TriggerType, TriggerType: item.TriggerType,
ScheduleType: jobScheduleTypeMap[jobID],
Status: item.Status, Status: item.Status,
StartedAt: formatTime(item.StartedAt), StartedAt: formatTime(item.StartedAt),
FinishedAt: formatTime(item.FinishedAt), FinishedAt: formatTime(item.FinishedAt),
@@ -631,7 +684,17 @@ func NewRouter(deps RouterDeps) *gin.Engine {
return return
} }
c.JSON(http.StatusOK, toExecutionDetailItem(*item)) detail := toExecutionDetailItem(*item)
job, err := deps.JobConfigStore.GetByID(c.Request.Context(), item.JobConfigID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if job != nil {
detail.ScheduleType = job.ScheduleType
}
c.JSON(http.StatusOK, detail)
}) })
router.GET("/admin/scheduler/executions/:id/logfile", func(c *gin.Context) { router.GET("/admin/scheduler/executions/:id/logfile", func(c *gin.Context) {
id, err := primitive.ObjectIDFromHex(c.Param("id")) id, err := primitive.ObjectIDFromHex(c.Param("id"))
@@ -900,6 +963,14 @@ func toExecutionDetailItem(item model.JobExecution) executionDetailItem {
} }
} }
func toGlobalConfigItem(item model.GlobalConfig) globalConfigItem {
return globalConfigItem{
Config: item.Config,
UpdatedBy: item.UpdatedBy,
UpdatedAt: formatTime(&item.UpdatedAt),
}
}
func toProfileItem(item model.AdminProfile) profileItem { func toProfileItem(item model.AdminProfile) profileItem {
return profileItem{ return profileItem{
ID: objectIDToString(item.ID), ID: objectIDToString(item.ID),

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/redis/go-redis/v9"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"scheduler-backend/pkg/config" "scheduler-backend/pkg/config"
@@ -93,6 +94,7 @@ type Runtime struct {
Logger log.Logger Logger log.Logger
MetaDB *mongo.Database MetaDB *mongo.Database
BusinessDB *mongo.Database BusinessDB *mongo.Database
Redis *redis.Client
} }
type Handler interface { type Handler interface {

View File

@@ -36,7 +36,9 @@ func (URLRewriteHandler) Run(ctx context.Context, runtime Runtime, req ExecuteRe
redisCfg := urlrewrite.RedisConfig{ redisCfg := urlrewrite.RedisConfig{
Addr: runtime.Config.RedisAddr, Addr: runtime.Config.RedisAddr,
Username: runtime.Config.RedisUsername,
Password: runtime.Config.RedisPassword, Password: runtime.Config.RedisPassword,
Client: runtime.Redis,
} }
batchID, err := urlrewrite.Run(ctx, runtime.BusinessDB, params, redisCfg, logf) batchID, err := urlrewrite.Run(ctx, runtime.BusinessDB, params, redisCfg, logf)
if err != nil { if err != nil {

View File

@@ -0,0 +1,15 @@
package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type GlobalConfig struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Key string `bson:"key" json:"key"`
Config string `bson:"config" json:"config"`
UpdatedBy string `bson:"updatedBy" json:"updatedBy"`
UpdatedAt time.Time `bson:"updatedAt" json:"updatedAt"`
}

View File

@@ -0,0 +1,86 @@
package mongo
import (
"context"
"errors"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
mongodriver "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"scheduler-backend/internal/store/model"
)
const schedulerGlobalConfigKey = "scheduler"
const defaultSchedulerGlobalConfigJSON = `{
"maxConcurrentJobs": 5,
"defaultTimeoutSeconds": 300
}`
type GlobalConfigStore struct {
collection *mongodriver.Collection
}
func NewGlobalConfigStore(db *mongodriver.Database) *GlobalConfigStore {
return &GlobalConfigStore{
collection: db.Collection("global_config"),
}
}
func (s *GlobalConfigStore) Get(ctx context.Context, key string) (*model.GlobalConfig, error) {
findCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var item model.GlobalConfig
err := s.collection.FindOne(findCtx, bson.M{"key": key}).Decode(&item)
if err == nil {
return &item, nil
}
if !errors.Is(err, mongodriver.ErrNoDocuments) {
return nil, err
}
item = model.GlobalConfig{
ID: primitive.NewObjectID(),
Key: key,
Config: defaultConfigForKey(key),
UpdatedBy: "system",
UpdatedAt: time.Now(),
}
if _, err := s.collection.InsertOne(findCtx, item); err != nil {
return nil, err
}
return &item, nil
}
func (s *GlobalConfigStore) Upsert(ctx context.Context, item *model.GlobalConfig) error {
updateCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
item.UpdatedAt = time.Now()
if item.UpdatedBy == "" {
item.UpdatedBy = "system"
}
if item.Key == "" {
item.Key = schedulerGlobalConfigKey
}
_, err := s.collection.UpdateOne(updateCtx, bson.M{"key": item.Key}, bson.M{
"$set": bson.M{
"key": item.Key,
"config": item.Config,
"updatedBy": item.UpdatedBy,
"updatedAt": item.UpdatedAt,
},
}, options.Update().SetUpsert(true))
return err
}
func defaultConfigForKey(key string) string {
if key == schedulerGlobalConfigKey {
return defaultSchedulerGlobalConfigJSON
}
return "{}"
}

View File

@@ -6,9 +6,10 @@ import (
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
) )
func newRedisClient(ctx context.Context, addr, password string) (*redis.Client, error) { func newRedisClient(ctx context.Context, addr, username, password string) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: addr, Addr: addr,
Username: username,
Password: password, Password: password,
}) })
if err := rdb.Ping(ctx).Err(); err != nil { if err := rdb.Ping(ctx).Err(); err != nil {

View File

@@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/redis/go-redis/v9"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
@@ -35,7 +36,9 @@ type Params struct {
type RedisConfig struct { type RedisConfig struct {
Addr string Addr string
Username string
Password string Password string
Client *redis.Client
} }
type Summary struct { type Summary struct {
@@ -216,11 +219,19 @@ func invalidateBatchCache(ctx context.Context, db *mongo.Database, redisCfg Redi
return nil return nil
} }
rdb, err := newRedisClient(ctx, redisCfg.Addr, redisCfg.Password) rdb := redisCfg.Client
if err != nil { shouldClose := false
return fmt.Errorf("connect redis: %w", err) if rdb == nil {
var err error
rdb, err = newRedisClient(ctx, redisCfg.Addr, redisCfg.Username, redisCfg.Password)
if err != nil {
return fmt.Errorf("connect redis: %w", err)
}
shouldClose = true
}
if shouldClose {
defer rdb.Close()
} }
defer rdb.Close()
if err := rdb.Del(ctx, keys...).Err(); err != nil { if err := rdb.Del(ctx, keys...).Err(); err != nil {
return fmt.Errorf("redis del: %w", err) return fmt.Errorf("redis del: %w", err)

View File

@@ -20,6 +20,7 @@ type Config struct {
BusinessMongoDatabase string BusinessMongoDatabase string
SchedulerMongoDatabase string SchedulerMongoDatabase string
RedisAddr string RedisAddr string
RedisUsername string
RedisPassword string RedisPassword string
} }
@@ -37,6 +38,7 @@ func Load() Config {
BusinessMongoDatabase: getenv("MONGO_DATABASE", "openim_v3"), BusinessMongoDatabase: getenv("MONGO_DATABASE", "openim_v3"),
SchedulerMongoDatabase: getenv("SCHEDULER_MONGO_DATABASE", "scheduler_center"), SchedulerMongoDatabase: getenv("SCHEDULER_MONGO_DATABASE", "scheduler_center"),
RedisAddr: getenv("REDIS_ADDR", ""), RedisAddr: getenv("REDIS_ADDR", ""),
RedisUsername: getenv("REDIS_USERNAME", ""),
RedisPassword: getenv("REDIS_PASSWORD", ""), RedisPassword: getenv("REDIS_PASSWORD", ""),
} }
} }

22
pkg/redis/client.go Normal file
View File

@@ -0,0 +1,22 @@
package redis
import (
"context"
goredis "github.com/redis/go-redis/v9"
"scheduler-backend/pkg/config"
)
func Connect(ctx context.Context, cfg config.Config) (*goredis.Client, error) {
client := goredis.NewClient(&goredis.Options{
Addr: cfg.RedisAddr,
Username: cfg.RedisUsername,
Password: cfg.RedisPassword,
})
if err := client.Ping(ctx).Err(); err != nil {
_ = client.Close()
return nil, err
}
return client, nil
}