diff --git a/cmd/main.go b/cmd/main.go index 777745f..c6641aa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,6 +10,11 @@ import ( "kra/internal/app" "kra/internal/config" + "kra/internal/data" + "kra/internal/global" + mqintegration "kra/internal/integration/mq" + "kra/internal/integration/storage" + websocketintegration "kra/internal/integration/websocket" "kra/internal/server/router" "kra/internal/service" "kra/internal/service/dto" @@ -51,7 +56,27 @@ func runtimeContributions(systemRoutes *router.Routes, systemTasks *worker.TaskM } } -func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskScheduler, audit *service.AuditRecorder, loggerControl *logging.ReloadableLogger, _ mq.Client) *kratos.App { +func installGlobalResources(logger *slog.Logger, dataStore *data.Data, fileStorage *storage.Reloadable, broker *mqintegration.Reloadable, websocket *websocketintegration.Server, scheduler *worker.TaskScheduler) *global.ResourceRegistry { + install := func() { + global.Install(global.Resources{ + Logger: logger, + DB: dataStore.DB(), + NamedDBs: dataStore.NamedDatabases(), + Redis: dataStore.RedisClient(), + NamedRedis: dataStore.NamedRedisClients(), + Mongo: dataStore.MongoClient(), + Storage: fileStorage, + MQ: broker, + WebSocket: websocket, + Scheduler: scheduler, + }) + } + dataStore.SetResourceHook(install) + install() + return global.DefaultRegistry() +} + +func newApp(logger *slog.Logger, hs *kratoshttp.Server, scheduler *worker.TaskScheduler, audit *service.AuditRecorder, loggerControl *logging.ReloadableLogger, _ mq.Client, _ *global.ResourceRegistry) *kratos.App { if audit != nil && loggerControl != nil { loggerControl.SetErrorSink(logging.ErrorSinkFunc(func(ctx context.Context, entry logging.ErrorEntry) error { return audit.CreateErrorRequest(ctx, &dto.ErrorRecordRequest{Form: entry.Form, Info: entry.Info, Level: entry.Level, RequestID: entry.RequestID, TraceID: entry.TraceID}) diff --git a/cmd/wire.go b/cmd/wire.go index 5172fca..12f468f 100644 --- a/cmd/wire.go +++ b/cmd/wire.go @@ -50,6 +50,7 @@ func wireApp(*config.Server, *config.Store, *slog.Logger, *logging.ReloadableLog wire.Bind(new(taskbiz.TaskMethodRegistry), new(*platformtask.Registry)), biz.ProviderSet, service.ProviderSet, + installGlobalResources, newApp, )) } diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 7117174..49948d3 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -184,7 +184,8 @@ func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logg cleanup() return nil, nil, err } - kratosApp := newApp(logger, httpServer, taskScheduler, v3, reloadableLogger, mqReloadable) + resourceRegistry := installGlobalResources(logger, dataData, reloadable, mqReloadable, websocketServer, taskScheduler) + kratosApp := newApp(logger, httpServer, taskScheduler, v3, reloadableLogger, mqReloadable, resourceRegistry) return kratosApp, func() { cleanup3() cleanup2() diff --git a/docs/system-pkg-audit.md b/docs/system-pkg-audit.md index 44165f2..2fe1ca9 100644 --- a/docs/system-pkg-audit.md +++ b/docs/system-pkg-audit.md @@ -45,14 +45,15 @@ ```text internal/ - app/ # 运行时组合根 + app/ # 应用组合根 modules/ # 静态 catalog、业务模块定义及其模块级贡献 biz/ system/ # 系统领域 payment/ # 支付领域 integration/# 集成配置领域 task/ # 定时任务领域 - conf/ # 配置 proto/runtime + config/ # Viper 配置、快照和热更新 + global/ # 进程级共享资源入口 data/ system/ # 系统表与系统仓储 integration/# 集成配置表与仓储 diff --git a/internal/README.md b/internal/README.md index ee59c12..8814ee9 100644 --- a/internal/README.md +++ b/internal/README.md @@ -3,13 +3,14 @@ 系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确 生命周期或分层职责的包: -- `app`:运行时组合根,负责依赖注入后的任务/路由组合 +- `app`:应用组合根,负责依赖注入后的任务/路由组合 - `modules`:静态模块 catalog,按 system/integration/task/payment 维护 Definition - `biz/system`:用户、权限、菜单、审计、媒体和系统配置领域 - `biz/payment`:支付订单、支付流程、支付接口和支付日志 - `biz/integration`:集成配置定义、校验和连接测试边界 - `biz/task`:定时任务模型、用例和任务注册协议 -- `conf`:基础配置 proto 与运行时配置解析 +- `config`:Viper 配置模型、加载、快照和热更新 +- `global`:进程级 DB、Redis、Mongo、Storage、MQ、WebSocket 和 Scheduler 入口 - `data`:共享数据库生命周期;仓储按 `data/system`、`data/integration`、`data/task`、`data/payment` 隔离 - `initialize`:数据库首次初始化和系统种子数据编排 - `integration`:Redis、邮件、对象存储、支付、WebSocket、EMQX 和 RabbitMQ 适配器 @@ -30,7 +31,7 @@ `internal/modules/catalog.go` 是静态模块 catalog 的唯一注册点,负责按依赖顺序 汇总各模块 Definition。`internal/app/runtime.go` 只负责依赖注入后的任务注册 -和路由运行时组合。这样新增模块只需在 modules catalog 注册一次,app 不再重复 +和路由组合。这样新增模块只需在 modules catalog 注册一次,app 不再重复 维护模块声明。 系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入 @@ -41,5 +42,5 @@ system 通过 `modules/system.Definition()` 提供系统迁移;integration 通 `modules/task.Definition()` 提供定时任务迁移和默认任务; payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过 `worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供 -路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire +路由。静态模块贡献可由 catalog 汇总;带构造依赖的路由和任务仍需在 cmd/Wire 中显式装配,不应误认为只添加 Definition 就能自动发现。 diff --git a/internal/config/runtime.go b/internal/config/runtime.go index ad7cf1b..a487ca7 100644 --- a/internal/config/runtime.go +++ b/internal/config/runtime.go @@ -63,7 +63,8 @@ func NewStore(config *Config) *Store { return store } -// LoadStore loads the initial snapshot and starts watching its source file. +// LoadStore loads the initial snapshot and starts the single process-wide +// Viper-backed watcher. func LoadStore(path string) (*Store, error) { config, err := Load(path) if err != nil { @@ -260,13 +261,29 @@ func (r *Store) watchLoop(watcher *fsnotify.Watcher, stop <-chan struct{}, done timerC = timer.C case <-timerC: timerC = nil - config, err := Load(path) + next, err := Load(path) if err != nil { // Keep the last valid snapshot. A partially-written file must not // take down a running process or publish invalid state. continue } - r.Replace(config) + current := r.Snapshot() + if next.Admin == nil { + next.Admin = &Admin{} + } + // Storage and Email move to database-backed integration settings after + // first initialization. External config file edits must not erase the + // active values when those sections are absent from config.yaml. + if current != nil && current.Admin != nil { + if next.Admin.Storage == nil { + next.Admin.Storage = cloneStorage(current.Admin.Storage) + } + if next.Admin.Email == nil && current.Admin.Email != nil { + email := *current.Admin.Email + next.Admin.Email = &email + } + } + r.Replace(next) case _, ok := <-errors: // fsnotify errors are intentionally non-fatal; the watcher remains // useful for subsequent events and Close always terminates it. diff --git a/internal/config/runtime_test.go b/internal/config/runtime_test.go index ddde237..a5aebcf 100644 --- a/internal/config/runtime_test.go +++ b/internal/config/runtime_test.go @@ -116,3 +116,32 @@ func TestStoreWatchReloadAndClose(t *testing.T) { } t.Fatalf("watcher did not reload configuration: %#v", store.Snapshot()) } + +func TestStoreWatchPreservesDatabaseBackedIntegrationSettings(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + writeConfig(t, path, "data: {}\nadmin: {}\n") + store, err := LoadStore(path) + if err != nil { + t.Fatal(err) + } + defer store.Close() + current := store.Snapshot() + current.Admin.Storage = &Storage{Type: "local"} + current.Admin.Email = &Email{Host: "smtp.example.com"} + store.Replace(current) + + writeConfig(t, path, "data:\n redis:\n addr: 127.0.0.1:6379\nadmin: {}\n") + deadline := time.Now().Add(3 * time.Second) + for time.Now().Before(deadline) { + value := store.Snapshot() + if value != nil && value.Data != nil && value.Data.Redis != nil && value.Data.Redis.Addr != "" { + if value.Admin == nil || value.Admin.Storage == nil || value.Admin.Storage.Type != "local" || value.Admin.Email == nil || value.Admin.Email.Host != "smtp.example.com" { + t.Fatalf("database-backed settings were lost: %#v", value.Admin) + } + return + } + time.Sleep(20 * time.Millisecond) + } + t.Fatal("watcher did not publish the changed file") +} diff --git a/internal/data/config_store.go b/internal/data/config_store.go index 087da85..17ff6db 100644 --- a/internal/data/config_store.go +++ b/internal/data/config_store.go @@ -377,6 +377,14 @@ func (d *Data) reloadConfig(ctx context.Context) error { _ = candidateRedis.Close() } }() + useRedisList := useRedis && next.Admin.System.UseMultipoint + candidateRedisList := openRedisList(next.Data.RedisList, useRedisList, d.logger()) + candidateRedisListAccepted := false + defer func() { + if !candidateRedisListAccepted { + closeRedisList(candidateRedisList) + } + }() useMongo := next.Admin.System != nil && next.Admin.System.UseMongo candidateMongo, mongoErr := openMongo(next.Data.Mongo, useMongo) if mongoErr != nil { @@ -410,6 +418,7 @@ func (d *Data) reloadConfig(ctx context.Context) error { } d.replaceDatabaseList(candidateDBList) d.redis.replace(candidateRedis) + d.replaceRedisList(candidateRedisList) if mongoErr == nil { d.mongo.replace(candidateMongo) mongoAccepted = true @@ -417,6 +426,7 @@ func (d *Data) reloadConfig(ctx context.Context) error { closeCandidate = false candidateDBListAccepted = true candidateRedisAccepted = true + candidateRedisListAccepted = true d.runtime.Replace(next) if d.integrations != nil { d.integrations.Replace(integrationConfigs) @@ -424,6 +434,7 @@ func (d *Data) reloadConfig(ctx context.Context) error { if d.storage != nil { d.storage.Replace(candidateStorage) } + d.notifyResources() return nil } diff --git a/internal/data/config_watch.go b/internal/data/config_watch.go deleted file mode 100644 index 784b32e..0000000 --- a/internal/data/config_watch.go +++ /dev/null @@ -1,123 +0,0 @@ -package data - -import ( - "path/filepath" - "sync" - "time" - - "github.com/fsnotify/fsnotify" - "kra/internal/config" -) - -// watchConfig refreshes the in-memory config snapshot on file changes, while -// /system/reloadSystem remains responsible -// for rebuilding database, Redis, storage, and scheduled tasks. -func (d *Data) watchConfig() func() { - logger := d.logger() - configPath := d.runtime.ConfigPath() - if configPath == "" { - return func() {} - } - absolute, err := filepath.Abs(configPath) - if err != nil { - logger.Error("resolve config watch path", "mod", "system", "error", err) - return func() {} - } - watcher, err := fsnotify.NewWatcher() - if err != nil { - logger.Error("create config watcher", "mod", "system", "error", err) - return func() {} - } - if err = watcher.Add(filepath.Dir(absolute)); err != nil { - logger.Error("watch config directory", "mod", "system", "error", err) - _ = watcher.Close() - return func() {} - } - done := make(chan struct{}) - finished := make(chan struct{}) - var once sync.Once - go func() { - defer close(finished) - var timer *time.Timer - var timerC <-chan time.Time - events := watcher.Events - watchErrors := watcher.Errors - for { - select { - case event, ok := <-events: - if !ok { - events = nil - continue - } - if filepath.Clean(event.Name) != absolute || event.Op&(fsnotify.Write|fsnotify.Create|fsnotify.Rename) == 0 { - continue - } - if timer != nil { - if !timer.Stop() { - select { - case <-timer.C: - default: - } - } - } - if timer == nil { - timer = time.NewTimer(100 * time.Millisecond) - } else { - timer.Reset(100 * time.Millisecond) - } - timerC = timer.C - case <-timerC: - // The debounce callback runs on this goroutine so shutdown can - // wait for it to finish without racing runtime replacement. - timerC = nil - select { - case <-done: - continue - default: - } - next, loadErr := readBootstrap(absolute) - if loadErr != nil { - logger.Error("reload changed config", "mod", "system", "error", loadErr) - continue - } - if next.Data == nil { - logger.Error("reload changed config: data configuration is required", "mod", "system") - continue - } - if next.Admin == nil { - next.Admin = &config.Admin{} - } - if current := d.runtime.Admin(); current != nil { - next.Admin.Storage = current.Storage - next.Admin.Email = current.Email - } - next.Admin.ConfigPath = absolute - d.runtime.Replace(next) - logger.Info("config file changed", "mod", "system", "path", absolute) - case watchErr, ok := <-watchErrors: - if !ok { - watchErrors = nil - continue - } - logger.Error("config watcher error", "mod", "system", "error", watchErr) - case <-done: - if timer != nil { - if !timer.Stop() { - select { - case <-timer.C: - default: - } - } - } - return - } - } - }() - return func() { - once.Do(func() { - close(done) - _ = watcher.Close() - <-finished - }) - } -} diff --git a/internal/data/config_watch_test.go b/internal/data/config_watch_test.go deleted file mode 100644 index 2f9bcc0..0000000 --- a/internal/data/config_watch_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package data - -import ( - "os" - "path/filepath" - "sync" - "testing" - "time" - - "kra/internal/config" -) - -func TestConfigWatcherStopWaitsForDebouncedReload(t *testing.T) { - root := t.TempDir() - configPath := filepath.Join(root, "config.yaml") - rawConfig := []byte("data:\n database:\n driver: sqlite\n path: " + filepath.ToSlash(root) + "\nadmin: {}\n") - if err := os.WriteFile(configPath, rawConfig, 0o600); err != nil { - t.Fatal(err) - } - runtime := config.NewStore(&config.Config{Data: &config.Data{}, Admin: &config.Admin{ConfigPath: configPath}}) - started := make(chan struct{}) - release := make(chan struct{}) - var startOnce sync.Once - stopListener := runtime.Subscribe(func(*config.Config) { - startOnce.Do(func() { close(started) }) - <-release - }) - defer stopListener() - data := &Data{runtime: runtime} - stop := data.watchConfig() - - // A write event schedules the 100ms debounce reload. - if err := os.WriteFile(configPath, rawConfig, 0o600); err != nil { - t.Fatal(err) - } - select { - case <-started: - case <-time.After(3 * time.Second): - stop() - t.Fatal("debounced reload did not start") - } - - stopped := make(chan struct{}) - go func() { - stop() - close(stopped) - }() - select { - case <-stopped: - t.Fatal("watcher stop returned before reload callback finished") - case <-time.After(100 * time.Millisecond): - } - close(release) - select { - case <-stopped: - case <-time.After(time.Second): - t.Fatal("watcher stop did not wait for callback completion") - } -} diff --git a/internal/data/data.go b/internal/data/data.go index 32b0ff4..ad8cbb8 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -10,6 +10,7 @@ import ( "github.com/google/wire" "github.com/redis/go-redis/v9" + "go.mongodb.org/mongo-driver/mongo" "gorm.io/gorm" "kra/internal/config" dataintegration "kra/internal/data/integration" @@ -45,9 +46,12 @@ func NewIntegrationRuntime(data *Data) *runtimeconfig.Store { type Data struct { initMu sync.Mutex configMu sync.Mutex + resourceMu sync.RWMutex databaseReady atomic.Bool gormDB *reloadableDB redis *reloadableRedis + redisListMu sync.RWMutex + redisList map[string]redis.UniversalClient mongo *reloadableMongo runtime *config.Store integrations *runtimeconfig.Store @@ -57,6 +61,7 @@ type Data struct { appLogger *slog.Logger auditLog *dataScopeAuditWriter catalog module.Catalog + resourceHook func() } // DB exposes the active primary database to narrowly scoped data submodules. @@ -105,6 +110,79 @@ func (d *Data) RedisClient() redis.UniversalClient { return d.redis.load() } +// MongoClient returns the currently active Mongo client. The client remains +// owned by Data and is only exposed here for framework-level resource wiring. +func (d *Data) MongoClient() *mongo.Client { + if d == nil || d.mongo == nil { + return nil + } + return d.mongo.load() +} + +// NamedDatabases returns a copy of configured secondary database handles for +// the process-wide resource registry. Data retains lifecycle ownership. +func (d *Data) NamedDatabases() map[string]*gorm.DB { + if d == nil { + return nil + } + d.dbListMu.RLock() + defer d.dbListMu.RUnlock() + if len(d.dbList) == 0 { + return nil + } + result := make(map[string]*gorm.DB, len(d.dbList)) + for name, db := range d.dbList { + if name != "" && db != nil { + result[name] = db + } + } + return result +} + +// NamedRedisClients returns a copy of configured secondary Redis handles for +// the process-wide resource registry. Data retains lifecycle ownership. +func (d *Data) NamedRedisClients() map[string]redis.UniversalClient { + if d == nil { + return nil + } + d.redisListMu.RLock() + defer d.redisListMu.RUnlock() + if len(d.redisList) == 0 { + return nil + } + result := make(map[string]redis.UniversalClient, len(d.redisList)) + for name, client := range d.redisList { + if name != "" && client != nil { + result[name] = client + } + } + return result +} + +// SetResourceHook registers a composition-root callback invoked after active +// database, Redis, or Mongo handles change. A callback keeps data independent +// from the global package while allowing the root to refresh shared handles. +func (d *Data) SetResourceHook(hook func()) { + if d == nil { + return + } + d.resourceMu.Lock() + d.resourceHook = hook + d.resourceMu.Unlock() +} + +func (d *Data) notifyResources() { + if d == nil { + return + } + d.resourceMu.RLock() + hook := d.resourceHook + d.resourceMu.RUnlock() + if hook != nil { + hook() + } +} + func (d *Data) logger() *slog.Logger { if d != nil && d.appLogger != nil { return d.appLogger @@ -176,13 +254,9 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor c.Database = &config.Database{} } d := &Data{runtime: runtime, integrations: runtimeconfig.NewStore(), appLogger: appLogger, storage: storageManager, catalog: catalog} - var stopConfigWatcher func() var cleanupOnce sync.Once cleanup := func() { cleanupOnce.Do(func() { - if stopConfigWatcher != nil { - stopConfigWatcher() - } if d.auditLog != nil { d.auditLog.Close() } @@ -193,6 +267,7 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor if d.redis != nil { d.redis.close() } + closeRedisList(d.redisList) if d.mongo != nil { d.mongo.close() } @@ -275,6 +350,8 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor } useRedis := admin != nil && admin.System != nil && admin.System.UseRedis d.redis = newReloadableRedis(openRedis(c.Redis, useRedis, appLogger)) + useRedisList := useRedis && admin.System.UseMultipoint + d.redisList = openRedisList(c.RedisList, useRedisList, appLogger) useMongo := admin != nil && admin.System != nil && admin.System.UseMongo mongoClient, err := openMongo(c.Mongo, useMongo) if err != nil { @@ -282,7 +359,6 @@ func NewData(runtime *config.Store, appLogger *slog.Logger, storageManager *stor mongoClient = nil } d.mongo = newReloadableMongo(mongoClient) - stopConfigWatcher = d.watchConfig() initialized = true return d, cleanup, nil } @@ -322,8 +398,47 @@ func openRedis(config *config.Redis, enabled bool, appLogger ...*slog.Logger) re return candidate } +func openRedisList(configs []*config.Redis, enabled bool, appLogger ...*slog.Logger) map[string]redis.UniversalClient { + if !enabled || len(configs) == 0 { + return nil + } + clients := make(map[string]redis.UniversalClient) + for _, item := range configs { + if item == nil || item.Name == "" { + continue + } + if client := openRedis(item, true, appLogger...); client != nil { + clients[item.Name] = client + } + } + if len(clients) == 0 { + return nil + } + return clients +} + +func closeRedisList(clients map[string]redis.UniversalClient) { + for _, client := range clients { + if client != nil { + _ = client.Close() + } + } +} + +func (d *Data) replaceRedisList(clients map[string]redis.UniversalClient) { + if d == nil { + return + } + d.redisListMu.Lock() + old := d.redisList + d.redisList = clients + d.redisListMu.Unlock() + closeRedisList(old) +} + func (d *Data) activateDatabase(db *gorm.DB, config *config.Database) { d.gormDB.replace(db, d.enqueueDataScopeAudit) d.runtime.UpdateDatabase(config) d.databaseReady.Store(true) + d.notifyResources() } diff --git a/internal/data/initialization_backend.go b/internal/data/initialization_backend.go index 4531b25..2eeb920 100644 --- a/internal/data/initialization_backend.go +++ b/internal/data/initialization_backend.go @@ -105,6 +105,7 @@ func (d *Data) PersistRuntimeConfig(ctx context.Context, value *configpkg.Config if d.storage != nil { d.storage.Replace(candidateStorage) } + d.notifyResources() return nil } func (d *Data) ReloadConfig(ctx context.Context) error { @@ -212,5 +213,6 @@ func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseCon if d.integrations != nil { d.integrations.Replace(integrationConfigs) } + d.notifyResources() return nil } diff --git a/internal/data/runtime_clients.go b/internal/data/runtime_clients.go index 0eac0fc..fd5babd 100644 --- a/internal/data/runtime_clients.go +++ b/internal/data/runtime_clients.go @@ -36,6 +36,17 @@ func (r *reloadableMongo) replace(client *mongo.Client) { } r.mu.Unlock() } + +func (r *reloadableMongo) load() *mongo.Client { + if r == nil { + return nil + } + r.mu.RLock() + client := r.current + r.mu.RUnlock() + return client +} + func (r *reloadableMongo) close() { r.mu.Lock() all := append([]*mongo.Client{r.current}, r.retired...) diff --git a/internal/global/resources.go b/internal/global/resources.go index c7808cc..93973b0 100644 --- a/internal/global/resources.go +++ b/internal/global/resources.go @@ -238,6 +238,15 @@ func normalizeInterface[T any](value T) T { var defaultResources = NewResourceRegistry() +// Install publishes resources assembled by the composition root and returns +// the process-wide registry for lifecycle wiring. +func Install(resources Resources) *ResourceRegistry { + defaultResources.Replace(resources) + return defaultResources +} + +func DefaultRegistry() *ResourceRegistry { return defaultResources } + func ResourceSnapshot() Resources { return defaultResources.Snapshot() } func ReplaceResources(resources Resources) { defaultResources.Replace(resources) } diff --git a/web/src/view/systemTools/timedTask/index.vue b/web/src/view/systemTools/timedTask/index.vue index 75c9fed..9e31d4c 100644 --- a/web/src/view/systemTools/timedTask/index.vue +++ b/web/src/view/systemTools/timedTask/index.vue @@ -299,9 +299,9 @@ const headerText = ref('') const methodOptions = ref([]) const formTitle = computed(() => (form.value.ID ? '编辑定时任务' : '新增定时任务')) -// 注册方法代码模板: 引导二开在 worker 的统一注册入口追加注册。 -const methodRegisterTemplate = `// 位置: internal/worker/task_registry.go -> registerTaskMethods() -biz.RegisterTaskMethod("MyTask", "任务说明(展示在方法下拉中)", func(ctx context.Context, params json.RawMessage) error { +// 注册方法代码模板: 引导二开通过模块或 worker contributor 显式注册。 +const methodRegisterTemplate = `// 在模块 Definition.Tasks 或 TaskMethods.RegisterTasks 中注册 +registry.Register(task.Method{Name: "MyTask", Description: "任务说明(展示在方法下拉中)", Run: func(ctx context.Context, params json.RawMessage) error { // params 为面板「参数(JSON)」原文, 按需解析; 无参数可忽略 // var p struct { // Days int \`json:"days"\` @@ -313,7 +313,7 @@ biz.RegisterTaskMethod("MyTask", "任务说明(展示在方法下拉中)", func( // } // TODO: 调用注入到 TaskExecutor 的 biz usecase 执行业务逻辑 return nil -})` +}})` const copyRegisterTemplate = () => { const input = document.createElement('textarea') @@ -322,7 +322,7 @@ const copyRegisterTemplate = () => { input.select() document.execCommand('copy') document.body.removeChild(input) - ElMessage.success('已复制,请粘贴到 internal/worker/task_registry.go 的 registerTaskMethods() 中,重启后生效') + ElMessage.success('已复制,请添加到模块任务定义或 TaskMethods.RegisterTasks,重启后生效') } const openForm = async (row) => {