优化结构
This commit is contained in:
parent
acaee94858
commit
7643152c1e
27
cmd/main.go
27
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})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -45,14 +45,15 @@
|
|||
|
||||
```text
|
||||
internal/
|
||||
app/ # 运行时组合根
|
||||
app/ # 应用组合根
|
||||
modules/ # 静态 catalog、业务模块定义及其模块级贡献
|
||||
biz/
|
||||
system/ # 系统领域
|
||||
payment/ # 支付领域
|
||||
integration/# 集成配置领域
|
||||
task/ # 定时任务领域
|
||||
conf/ # 配置 proto/runtime
|
||||
config/ # Viper 配置、快照和热更新
|
||||
global/ # 进程级共享资源入口
|
||||
data/
|
||||
system/ # 系统表与系统仓储
|
||||
integration/# 集成配置表与仓储
|
||||
|
|
|
|||
|
|
@ -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 就能自动发现。
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue