From 0956bef9dbf82059411e32f9205928eac5e510d7 Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 20 Aug 2025 10:47:17 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE=E9=87=8D?= =?UTF-8?q?=E6=9E=84=EF=BC=8C=E6=94=AF=E6=8C=81=E5=90=8E=E5=8F=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E9=9D=A2=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/core/types/config.go | 16 + api/handler/admin/config_handler.go | 99 ++-- api/main.go | 47 +- api/service/config_migration.go | 275 +++++++++ api/service/config_service.go | 146 +++++ api/service/types.go | 4 + api/service/xxl_job_service.go | 64 --- web/src/components/admin/AdminSidebar.vue | 58 +- web/src/router.js | 62 ++- web/src/views/admin/AgreementConfig.vue | 96 ++++ web/src/views/admin/ApiConfig.vue | 60 ++ web/src/views/admin/CommunicationConfig.vue | 108 ++++ web/src/views/admin/LicenseConfig.vue | 149 +++++ web/src/views/admin/MenuConfig.vue | 25 + web/src/views/admin/NoticeConfig.vue | 91 +++ web/src/views/admin/PaymentConfig.vue | 129 +++++ web/src/views/admin/PrivacyConfig.vue | 96 ++++ web/src/views/admin/StorageConfig.vue | 132 +++++ web/src/views/admin/SystemConfig.vue | 588 ++++++++++++++++++++ 19 files changed, 2107 insertions(+), 138 deletions(-) create mode 100644 api/service/config_migration.go create mode 100644 api/service/config_service.go delete mode 100644 api/service/xxl_job_service.go create mode 100644 web/src/views/admin/AgreementConfig.vue create mode 100644 web/src/views/admin/ApiConfig.vue create mode 100644 web/src/views/admin/CommunicationConfig.vue create mode 100644 web/src/views/admin/LicenseConfig.vue create mode 100644 web/src/views/admin/MenuConfig.vue create mode 100644 web/src/views/admin/NoticeConfig.vue create mode 100644 web/src/views/admin/PaymentConfig.vue create mode 100644 web/src/views/admin/PrivacyConfig.vue create mode 100644 web/src/views/admin/StorageConfig.vue create mode 100644 web/src/views/admin/SystemConfig.vue diff --git a/api/core/types/config.go b/api/core/types/config.go index ccff3678..503e9078 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -172,3 +172,19 @@ type SystemConfig struct { MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB } + +// 配置键名常量 +const ( + ConfigKeySystem = "system" + ConfigKeyNotice = "notice" + ConfigKeyAgreement = "agreement" + ConfigKeyPrivacy = "privacy" + ConfigKeyApi = "api" + ConfigKeySms = "sms" + ConfigKeySmtp = "smtp" + ConfigKeyOss = "oss" + ConfigKeyAlipay = "alipay" + ConfigKeyWechat = "wechat" + ConfigKeyHuPi = "hupi" + ConfigKeyGeekpay = "geekpay" +) diff --git a/api/handler/admin/config_handler.go b/api/handler/admin/config_handler.go index fa940fde..29a458fe 100644 --- a/api/handler/admin/config_handler.go +++ b/api/handler/admin/config_handler.go @@ -8,6 +8,8 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "encoding/json" + "errors" "geekai/core" "geekai/core/types" "geekai/handler" @@ -26,13 +28,15 @@ type ConfigHandler struct { handler.BaseHandler levelDB *store.LevelDB licenseService *service.LicenseService + configService *service.ConfigService } -func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler { +func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService, configService *service.ConfigService) *ConfigHandler { return &ConfigHandler{ BaseHandler: handler.BaseHandler{App: app, DB: db}, levelDB: levelDB, licenseService: licenseService, + configService: configService, } } @@ -42,70 +46,52 @@ func (h *ConfigHandler) RegisterRoutes() { group.POST("update", h.Update) group.GET("get", h.Get) group.POST("active", h.Active) + group.POST("test", h.Test) group.GET("fixData", h.FixData) group.GET("license", h.GetLicense) } func (h *ConfigHandler) Update(c *gin.Context) { - var data struct { - Key string `json:"key"` - Config struct { - types.SystemConfig - Content string `json:"content,omitempty"` - Updated bool `json:"updated,omitempty"` - } `json:"config"` + var payload struct { + Key string `json:"key"` + Config json.RawMessage `json:"config"` ConfigBak types.SystemConfig `json:"config_bak,omitempty"` } - if err := c.ShouldBindJSON(&data); err != nil { + if err := c.ShouldBindJSON(&payload); err != nil { logger.Errorf("Update config failed: %v", err) resp.ERROR(c, types.InvalidArgs) return } - // ONLY authorized user can change the copyright - if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy { - resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权") - return - } - - // 如果要启用图形验证码功能,则检查是否配置了 API 服务 - if data.Config.EnabledVerify && h.App.Config.ApiConfig.AppId == "" { - resp.ERROR(c, "启用验证码服务需要先配置 GeekAI 官方 API 服务 AppId 和 Token") - return - } - - value := utils.JsonEncode(&data.Config) - config := model.Config{Name: data.Key, Value: value} - res := h.DB.FirstOrCreate(&config, model.Config{Name: data.Key}) - if res.Error != nil { - resp.ERROR(c, res.Error.Error()) - return - } - - if config.Id > 0 { - config.Value = value - res := h.DB.Updates(&config) - if res.Error != nil { - resp.ERROR(c, res.Error.Error()) + if payload.Key == "system" { + var sys types.SystemConfig + if err := json.Unmarshal(payload.Config, &sys); err != nil { + resp.ERROR(c, "系统配置解析失败: "+err.Error()) return } - - // update config cache for AppServer - var cfg model.Config - h.DB.Where("name", data.Key).First(&cfg) - var err error - if data.Key == "system" { - err = utils.JsonDecode(cfg.Value, &h.App.SysConfig) - } - if err != nil { - resp.ERROR(c, "Failed to update config cache: "+err.Error()) + if (sys.Copyright != payload.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy { + resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权") + return + } + if sys.EnabledVerify && h.App.Config.ApiConfig.AppId == "" { + resp.ERROR(c, "启用验证码服务需要先配置 GeekAI 官方 API 服务 AppId 和 Token") return } - logger.Infof("Update AppServer's config successfully: %v", config.Value) } - resp.SUCCESS(c, config) + // 使用统一配置服务写入与广播 + if err := h.configService.Set(payload.Key, payload.Config); err != nil { + resp.ERROR(c, err.Error()) + return + } + if payload.Key == "system" { + var sys types.SystemConfig + if err := json.Unmarshal(payload.Config, &sys); err == nil { + h.App.SysConfig = &sys + } + } + resp.SUCCESS(c) } // Get 获取指定的系统配置 @@ -114,6 +100,10 @@ func (h *ConfigHandler) Get(c *gin.Context) { var config model.Config res := h.DB.Where("name", key).First(&config) if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + resp.SUCCESS(c, map[string]interface{}{}) + return + } resp.ERROR(c, res.Error.Error()) return } @@ -128,6 +118,23 @@ func (h *ConfigHandler) Get(c *gin.Context) { resp.SUCCESS(c, value) } +// Test 配置测试(占位) +func (h *ConfigHandler) Test(c *gin.Context) { + var data struct { + Key string `json:"key"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + msg, err := h.configService.Test(data.Key) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + resp.SUCCESS(c, msg) +} + // Active 激活系统 func (h *ConfigHandler) Active(c *gin.Context) { var data struct { diff --git a/api/main.go b/api/main.go index f6ade46b..35305a39 100644 --- a/api/main.go +++ b/api/main.go @@ -143,6 +143,15 @@ func main() { fx.Provide(handler.NewPowerLogHandler), fx.Provide(handler.NewJimengHandler), + fx.Provide(service.NewConfigService), + fx.Provide(service.NewConfigMigrationService), + fx.Invoke(func(migrationService *service.ConfigMigrationService, config *types.AppConfig, redisClient *redis.Client) { + if err := migrationService.MigrateFromConfig(config); err != nil { + logger.Errorf("配置迁移失败: %v", err) + } + }), + + // 管理后台控制器 fx.Provide(admin.NewConfigHandler), fx.Provide(admin.NewAdminHandler), fx.Provide(admin.NewApiKeyHandler), @@ -153,23 +162,9 @@ func main() { fx.Provide(admin.NewChatModelHandler), fx.Provide(admin.NewProductHandler), fx.Provide(admin.NewOrderHandler), - fx.Provide(admin.NewChatHandler), fx.Provide(admin.NewPowerLogHandler), fx.Provide(admin.NewAdminJimengHandler), - // 创建服务 - fx.Provide(sms.NewSendServiceManager), - fx.Provide(func(config *types.AppConfig) *service.CaptchaService { - return service.NewCaptchaService(config.ApiConfig) - }), - fx.Provide(oss.NewUploaderManager), - fx.Provide(dalle.NewService), - fx.Invoke(func(s *dalle.Service) { - s.Run() - s.DownloadImages() - s.CheckTaskStatus() - }), - // 邮件服务 fx.Provide(service.NewSmtpService), // License 服务 @@ -178,6 +173,14 @@ func main() { licenseService.SyncLicense() }), + // Dalle 服务 + fx.Provide(dalle.NewService), + fx.Invoke(func(s *dalle.Service) { + s.Run() + s.DownloadImages() + s.CheckTaskStatus() + }), + // MidJourney service pool fx.Provide(mj.NewService), fx.Provide(mj.NewClient), @@ -218,14 +221,13 @@ func main() { fx.Provide(payment.NewJPayService), fx.Provide(payment.NewWechatService), fx.Provide(service.NewSnowflake), - fx.Provide(service.NewXXLJobExecutor), - fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) { - if config.XXLConfig.Enabled { - go func() { - log.Fatal(exec.Run()) - }() - } + + // 创建服务 + fx.Provide(sms.NewSendServiceManager), + fx.Provide(func(config *types.AppConfig) *service.CaptchaService { + return service.NewCaptchaService(config.ApiConfig) }), + fx.Provide(oss.NewUploaderManager), // 注册路由 fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { @@ -259,7 +261,7 @@ func main() { h.RegisterRoutes() }), - // 管理后台控制器 + // 管理后台路由注册 fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { h.RegisterRoutes() }), @@ -322,6 +324,7 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) { h.RegisterRoutes() }), + fx.Provide(admin.NewChatHandler), fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) { h.RegisterRoutes() }), diff --git a/api/service/config_migration.go b/api/service/config_migration.go new file mode 100644 index 00000000..eff6760a --- /dev/null +++ b/api/service/config_migration.go @@ -0,0 +1,275 @@ +package service + +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// Copyright 2023 The Geek-AI Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 license +// that can be found in the LICENSE file. +// @Author yangjian102621@163.com +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "context" + "encoding/json" + "geekai/core/types" + "geekai/store/model" + + "github.com/go-redis/redis/v8" + "gorm.io/gorm" +) + +const ( + // 迁移状态Redis key + MigrationStatusKey = "config_migration:status" + // 迁移完成标志 + MigrationCompleted = "completed" +) + +// ConfigMigrationService 配置迁移服务 +type ConfigMigrationService struct { + db *gorm.DB + redisClient *redis.Client +} + +func NewConfigMigrationService(db *gorm.DB, redisClient *redis.Client) *ConfigMigrationService { + return &ConfigMigrationService{ + db: db, + redisClient: redisClient, + } +} + +// MigrateFromConfig 从 config.toml 迁移配置到数据库(仅首次启动时执行) +func (s *ConfigMigrationService) MigrateFromConfig(config *types.AppConfig) error { + // 检查是否已经迁移过 + if s.isMigrationCompleted() { + logger.Info("配置迁移已完成,跳过迁移") + return nil + } + + logger.Info("开始迁移配置到数据库...") + + // 迁移支付配置 + if err := s.migratePaymentConfig(config); err != nil { + logger.Errorf("迁移支付配置失败: %v", err) + return err + } + + // 迁移存储配置 + if err := s.migrateStorageConfig(config); err != nil { + logger.Errorf("迁移存储配置失败: %v", err) + return err + } + + // 迁移通信配置 + if err := s.migrateCommunicationConfig(config); err != nil { + logger.Errorf("迁移通信配置失败: %v", err) + return err + } + + // 迁移API配置 + if err := s.migrateApiConfig(config); err != nil { + logger.Errorf("迁移API配置失败: %v", err) + return err + } + + // 标记迁移完成 + if err := s.markMigrationCompleted(); err != nil { + logger.Errorf("标记迁移完成失败: %v", err) + return err + } + + logger.Info("配置迁移完成") + return nil +} + +// 检查是否已经迁移完成 +func (s *ConfigMigrationService) isMigrationCompleted() bool { + ctx := context.Background() + status, err := s.redisClient.Get(ctx, MigrationStatusKey).Result() + if err != nil { + // Redis中没有找到标志,说明未迁移过 + return false + } + return status == MigrationCompleted +} + +// 标记迁移完成 +func (s *ConfigMigrationService) markMigrationCompleted() error { + ctx := context.Background() + // 设置迁移完成标志,永不过期 + return s.redisClient.Set(ctx, MigrationStatusKey, MigrationCompleted, 0).Err() +} + +// 迁移支付配置 +func (s *ConfigMigrationService) migratePaymentConfig(config *types.AppConfig) error { + // 支付宝配置 + alipayConfig := map[string]any{ + "enabled": config.AlipayConfig.Enabled, + "sand_box": config.AlipayConfig.SandBox, + "app_id": config.AlipayConfig.AppId, + "private_key": config.AlipayConfig.PrivateKey, + "alipay_public_key": config.AlipayConfig.AlipayPublicKey, + "notify_url": config.AlipayConfig.NotifyURL, + "return_url": config.AlipayConfig.ReturnURL, + } + if err := s.saveConfig("alipay", alipayConfig); err != nil { + return err + } + + // 微信支付配置 + wechatConfig := map[string]any{ + "enabled": config.WechatPayConfig.Enabled, + "app_id": config.WechatPayConfig.AppId, + "mch_id": config.WechatPayConfig.MchId, + "serial_no": config.WechatPayConfig.SerialNo, + "private_key": config.WechatPayConfig.PrivateKey, + "api_v3_key": config.WechatPayConfig.ApiV3Key, + "notify_url": config.WechatPayConfig.NotifyURL, + } + if err := s.saveConfig("wechat", wechatConfig); err != nil { + return err + } + + // 虎皮椒配置 + hupiConfig := map[string]any{ + "enabled": config.HuPiPayConfig.Enabled, + "app_id": config.HuPiPayConfig.AppId, + "app_secret": config.HuPiPayConfig.AppSecret, + "api_url": config.HuPiPayConfig.ApiURL, + "notify_url": config.HuPiPayConfig.NotifyURL, + "return_url": config.HuPiPayConfig.ReturnURL, + } + if err := s.saveConfig("hupi", hupiConfig); err != nil { + return err + } + + // GeekPay配置 + geekpayConfig := map[string]any{ + "enabled": config.GeekPayConfig.Enabled, + "app_id": config.GeekPayConfig.AppId, + "private_key": config.GeekPayConfig.PrivateKey, + "api_url": config.GeekPayConfig.ApiURL, + "notify_url": config.GeekPayConfig.NotifyURL, + "return_url": config.GeekPayConfig.ReturnURL, + "methods": config.GeekPayConfig.Methods, + } + if err := s.saveConfig("geekpay", geekpayConfig); err != nil { + return err + } + + return nil +} + +// 迁移存储配置 +func (s *ConfigMigrationService) migrateStorageConfig(config *types.AppConfig) error { + ossConfig := map[string]any{ + "active": config.OSS.Active, + "local": map[string]any{ + "base_path": config.OSS.Local.BasePath, + "base_url": config.OSS.Local.BaseURL, + }, + "minio": map[string]any{ + "endpoint": config.OSS.Minio.Endpoint, + "access_key": config.OSS.Minio.AccessKey, + "access_secret": config.OSS.Minio.AccessSecret, + "bucket": config.OSS.Minio.Bucket, + "use_ssl": config.OSS.Minio.UseSSL, + "domain": config.OSS.Minio.Domain, + }, + "qiniu": map[string]any{ + "zone": config.OSS.QiNiu.Zone, + "access_key": config.OSS.QiNiu.AccessKey, + "access_secret": config.OSS.QiNiu.AccessSecret, + "bucket": config.OSS.QiNiu.Bucket, + "domain": config.OSS.QiNiu.Domain, + }, + "aliyun": map[string]any{ + "endpoint": config.OSS.AliYun.Endpoint, + "access_key": config.OSS.AliYun.AccessKey, + "access_secret": config.OSS.AliYun.AccessSecret, + "bucket": config.OSS.AliYun.Bucket, + "sub_dir": config.OSS.AliYun.SubDir, + "domain": config.OSS.AliYun.Domain, + }, + } + return s.saveConfig("oss", ossConfig) +} + +// 迁移通信配置 +func (s *ConfigMigrationService) migrateCommunicationConfig(config *types.AppConfig) error { + // SMTP配置 + smtpConfig := map[string]any{ + "use_tls": config.SmtpConfig.UseTls, + "host": config.SmtpConfig.Host, + "port": config.SmtpConfig.Port, + "app_name": config.SmtpConfig.AppName, + "from": config.SmtpConfig.From, + "password": config.SmtpConfig.Password, + } + if err := s.saveConfig("smtp", smtpConfig); err != nil { + return err + } + + // 短信配置 + smsConfig := map[string]any{ + "active": config.SMS.Active, + "ali": map[string]any{ + "access_key": config.SMS.Ali.AccessKey, + "access_secret": config.SMS.Ali.AccessSecret, + "product": config.SMS.Ali.Product, + "domain": config.SMS.Ali.Domain, + "sign": config.SMS.Ali.Sign, + "code_temp_id": config.SMS.Ali.CodeTempId, + }, + "bao": map[string]any{ + "username": config.SMS.Bao.Username, + "password": config.SMS.Bao.Password, + "domain": config.SMS.Bao.Domain, + "sign": config.SMS.Bao.Sign, + "code_template": config.SMS.Bao.CodeTemplate, + }, + } + return s.saveConfig("sms", smsConfig) +} + +// 迁移API配置 +func (s *ConfigMigrationService) migrateApiConfig(config *types.AppConfig) error { + apiConfig := map[string]any{ + "api_url": config.ApiConfig.ApiURL, + "app_id": config.ApiConfig.AppId, + "token": config.ApiConfig.Token, + "jimeng_config": map[string]any{ + "access_key": config.ApiConfig.JimengConfig.AccessKey, + "secret_key": config.ApiConfig.JimengConfig.SecretKey, + }, + } + return s.saveConfig("api", apiConfig) +} + +// 保存配置到数据库 +func (s *ConfigMigrationService) saveConfig(key string, config any) error { + // 检查是否已存在 + var existingConfig model.Config + if err := s.db.Where("name", key).First(&existingConfig).Error; err == nil { + // 配置已存在,跳过 + logger.Infof("配置 %s 已存在,跳过迁移", key) + return nil + } + + // 序列化配置 + configJSON, err := json.Marshal(config) + if err != nil { + return err + } + + // 保存到数据库 + newConfig := model.Config{ + Name: key, + Value: string(configJSON), + } + if err := s.db.Create(&newConfig).Error; err != nil { + return err + } + + logger.Infof("成功迁移配置 %s", key) + return nil +} diff --git a/api/service/config_service.go b/api/service/config_service.go new file mode 100644 index 00000000..88c076e3 --- /dev/null +++ b/api/service/config_service.go @@ -0,0 +1,146 @@ +package service + +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// Copyright 2023 The Geek-AI Authors. All rights reserved. +// Use of this source code is governed by a Apache-2.0 license +// that can be found in the LICENSE file. +// @Author yangjian102621@163.com +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +import ( + "context" + "encoding/json" + "fmt" + "geekai/store/model" + "sync" + + "github.com/go-redis/redis/v8" + "gorm.io/gorm" +) + +// ConfigService 统一的配置访问、缓存与通知服务 +type ConfigService struct { + db *gorm.DB + rdb *redis.Client + mu sync.RWMutex + cache map[string]json.RawMessage + watchers map[string][]chan struct{} +} + +func NewConfigService(db *gorm.DB, rdb *redis.Client) *ConfigService { + s := &ConfigService{ + db: db, + rdb: rdb, + cache: make(map[string]json.RawMessage), + watchers: make(map[string][]chan struct{}), + } + go s.subscribe() + return s +} + +// Get 以原始 JSON 获取配置(带本地缓存) +func (s *ConfigService) Get(key string) (json.RawMessage, error) { + s.mu.RLock() + if v, ok := s.cache[key]; ok { + s.mu.RUnlock() + return v, nil + } + s.mu.RUnlock() + + var cfg model.Config + if err := s.db.Where("name", key).First(&cfg).Error; err != nil { + return nil, err + } + s.mu.Lock() + s.cache[key] = json.RawMessage(cfg.Value) + s.mu.Unlock() + return json.RawMessage(cfg.Value), nil +} + +// GetInto 将配置解析进传入结构体 +func (s *ConfigService) GetInto(key string, dest interface{}) error { + data, err := s.Get(key) + if err != nil { + return err + } + if len(data) == 0 { + return nil + } + return json.Unmarshal(data, dest) +} + +// Set 设置配置并写入数据库,同时触发通知 +func (s *ConfigService) Set(key string, config json.RawMessage) error { + value := string(config) + cfg := model.Config{Name: key, Value: value} + if err := s.db.Where("name", key).FirstOrCreate(&cfg, model.Config{Name: key}).Error; err != nil { + return err + } + if cfg.Id > 0 { + cfg.Value = value + if err := s.db.Updates(&cfg).Error; err != nil { + return err + } + } + s.mu.Lock() + s.cache[key] = json.RawMessage(value) + s.mu.Unlock() + s.notifyLocal(key) + s.publish(key) + return nil +} + +// Watch 返回一个通道,当指定 key 发生变化时收到事件 +func (s *ConfigService) Watch(key string) <-chan struct{} { + ch := make(chan struct{}, 1) + s.mu.Lock() + s.watchers[key] = append(s.watchers[key], ch) + s.mu.Unlock() + return ch +} + +func (s *ConfigService) notifyLocal(key string) { + s.mu.RLock() + list := s.watchers[key] + s.mu.RUnlock() + for _, ch := range list { + select { // 非阻塞通知 + case ch <- struct{}{}: + default: + } + } +} + +// 通过 Redis 发布配置变更,便于多实例同步 +func (s *ConfigService) publish(key string) { + if s.rdb == nil { + return + } + channel := "config:changed" + if err := s.rdb.Publish(context.Background(), channel, key).Err(); err != nil { + logger.Warnf("publish config change failed: %v", err) + } +} + +func (s *ConfigService) subscribe() { + if s.rdb == nil { + return + } + channel := "config:changed" + sub := s.rdb.Subscribe(context.Background(), channel) + for msg := range sub.Channel() { + key := msg.Payload + logger.Infof("config changed: %s", key) + // 失效本地缓存并本地广播 + s.mu.Lock() + delete(s.cache, key) + s.mu.Unlock() + s.notifyLocal(key) + } +} + +// Test 预留统一测试入口,根据 key 执行连通性检查 +func (s *ConfigService) Test(key string) (string, error) { + // TODO: 实现各配置类型的测试逻辑 + return fmt.Sprintf("%s ok", key), nil +} diff --git a/api/service/types.go b/api/service/types.go index f39bf73c..25863471 100644 --- a/api/service/types.go +++ b/api/service/types.go @@ -1,5 +1,7 @@ package service +import logger2 "geekai/logger" + const FailTaskProgress = 101 const ( TaskStatusRunning = "RUNNING" @@ -15,6 +17,8 @@ type NotifyMessage struct { Type string `json:"type"` } +var logger = logger2.GetLogger() + const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]" const ImagePromptOptimizeTemplate = ` diff --git a/api/service/xxl_job_service.go b/api/service/xxl_job_service.go deleted file mode 100644 index 6fae6fad..00000000 --- a/api/service/xxl_job_service.go +++ /dev/null @@ -1,64 +0,0 @@ -package service - -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -// * Copyright 2023 The Geek-AI Authors. All rights reserved. -// * Use of this source code is governed by a Apache-2.0 license -// * that can be found in the LICENSE file. -// * @Author yangjian102621@163.com -// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -import ( - "context" - "geekai/core/types" - logger2 "geekai/logger" - - "github.com/xxl-job/xxl-job-executor-go" - "gorm.io/gorm" -) - -var logger = logger2.GetLogger() - -type XXLJobExecutor struct { - executor xxl.Executor - db *gorm.DB -} - -func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor { - if !config.XXLConfig.Enabled { - logger.Info("XXL-JOB service is disabled") - return nil - } - - exec := xxl.NewExecutor( - xxl.ServerAddr(config.XXLConfig.ServerAddr), - xxl.AccessToken(config.XXLConfig.AccessToken), //请求令牌(默认为空) - xxl.ExecutorIp(config.XXLConfig.ExecutorIp), //可自动获取 - xxl.ExecutorPort(config.XXLConfig.ExecutorPort), //默认9999(非必填) - xxl.RegistryKey(config.XXLConfig.RegistryKey), //执行器名称 - xxl.SetLogger(&customLogger{}), //自定义日志 - ) - exec.Init() - return &XXLJobExecutor{executor: exec, db: db} -} - -func (e *XXLJobExecutor) Run() error { - e.executor.RegTask("ClearOrders", e.ClearOrders) - return e.executor.Run() -} - -// ClearOrders 清理未支付的订单,如果没有抛出异常则表示执行成功 -func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (msg string) { - logger.Info("执行清理未支付订单...") - - return "success" -} - -type customLogger struct{} - -func (l *customLogger) Info(format string, a ...interface{}) { - logger.Debugf(format, a...) -} - -func (l *customLogger) Error(format string, a ...interface{}) { - logger.Errorf(format, a...) -} diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index ee493360..9a3a57ce 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -187,8 +187,60 @@ const items = [ }, { icon: 'config', - index: '/admin/system', + index: 'config-center', title: '系统设置', + subs: [ + { + icon: 'config', + index: '/admin/config/system', + title: '系统配置', + }, + { + icon: 'config', + index: '/admin/config/notice', + title: '公告配置', + }, + { + icon: 'config', + index: '/admin/config/agreement', + title: '用户协议', + }, + { + icon: 'config', + index: '/admin/config/privacy', + title: '隐私声明', + }, + { + icon: 'config', + index: '/admin/config/menu', + title: '菜单配置', + }, + { + icon: 'config', + index: '/admin/config/license', + title: '授权激活', + }, + { + icon: 'recharge', + index: '/admin/config/payment', + title: '支付配置', + }, + { + icon: 'menu', + index: '/admin/config/storage', + title: '存储配置', + }, + { + icon: 'log', + index: '/admin/config/communication', + title: '通信配置', + }, + { + icon: 'api-key', + index: '/admin/config/api', + title: 'API配置', + }, + ], }, { icon: 'log', @@ -242,6 +294,7 @@ setMenuItems(items) top: 0; bottom: 0; overflow-y: scroll; + background-color: #324157; .logo { display: flex; @@ -267,7 +320,8 @@ setMenuItems(items) } ul { - height: 100%; + height: auto; + min-height: 100%; .el-menu-item, .el-sub-menu { diff --git a/web/src/router.js b/web/src/router.js index 0b5132f7..cf6055c2 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -168,10 +168,64 @@ const routes = [ component: () => import('@/views/admin/Dashboard.vue'), }, { - path: '/admin/system', - name: 'admin-system', - meta: { title: '系统设置' }, - component: () => import('@/views/admin/SysConfig.vue'), + path: '/admin/config/system', + name: 'admin-config-system', + meta: { title: '系统配置' }, + component: () => import('@/views/admin/SystemConfig.vue'), + }, + { + path: '/admin/config/payment', + name: 'admin-config-payment', + meta: { title: '支付配置' }, + component: () => import('@/views/admin/PaymentConfig.vue'), + }, + { + path: '/admin/config/storage', + name: 'admin-config-storage', + meta: { title: '存储配置' }, + component: () => import('@/views/admin/StorageConfig.vue'), + }, + { + path: '/admin/config/communication', + name: 'admin-config-communication', + meta: { title: '通信配置' }, + component: () => import('@/views/admin/CommunicationConfig.vue'), + }, + { + path: '/admin/config/api', + name: 'admin-config-api', + meta: { title: 'API配置' }, + component: () => import('@/views/admin/ApiConfig.vue'), + }, + { + path: '/admin/config/notice', + name: 'admin-config-notice', + meta: { title: '公告配置' }, + component: () => import('@/views/admin/NoticeConfig.vue'), + }, + { + path: '/admin/config/agreement', + name: 'admin-config-agreement', + meta: { title: '用户协议' }, + component: () => import('@/views/admin/AgreementConfig.vue'), + }, + { + path: '/admin/config/privacy', + name: 'admin-config-privacy', + meta: { title: '隐私声明' }, + component: () => import('@/views/admin/PrivacyConfig.vue'), + }, + { + path: '/admin/config/menu', + name: 'admin-config-menu', + meta: { title: '菜单配置' }, + component: () => import('@/views/admin/MenuConfig.vue'), + }, + { + path: '/admin/config/license', + name: 'admin-config-license', + meta: { title: '授权激活' }, + component: () => import('@/views/admin/LicenseConfig.vue'), }, { path: '/admin/user', diff --git a/web/src/views/admin/AgreementConfig.vue b/web/src/views/admin/AgreementConfig.vue new file mode 100644 index 00000000..aee1be16 --- /dev/null +++ b/web/src/views/admin/AgreementConfig.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/web/src/views/admin/ApiConfig.vue b/web/src/views/admin/ApiConfig.vue new file mode 100644 index 00000000..272018cb --- /dev/null +++ b/web/src/views/admin/ApiConfig.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/web/src/views/admin/CommunicationConfig.vue b/web/src/views/admin/CommunicationConfig.vue new file mode 100644 index 00000000..d2fb0a9d --- /dev/null +++ b/web/src/views/admin/CommunicationConfig.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/web/src/views/admin/LicenseConfig.vue b/web/src/views/admin/LicenseConfig.vue new file mode 100644 index 00000000..a123757d --- /dev/null +++ b/web/src/views/admin/LicenseConfig.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/web/src/views/admin/MenuConfig.vue b/web/src/views/admin/MenuConfig.vue new file mode 100644 index 00000000..6ea7bf30 --- /dev/null +++ b/web/src/views/admin/MenuConfig.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/web/src/views/admin/NoticeConfig.vue b/web/src/views/admin/NoticeConfig.vue new file mode 100644 index 00000000..91346b27 --- /dev/null +++ b/web/src/views/admin/NoticeConfig.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/web/src/views/admin/PaymentConfig.vue b/web/src/views/admin/PaymentConfig.vue new file mode 100644 index 00000000..d6cda075 --- /dev/null +++ b/web/src/views/admin/PaymentConfig.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/web/src/views/admin/PrivacyConfig.vue b/web/src/views/admin/PrivacyConfig.vue new file mode 100644 index 00000000..e9e905c4 --- /dev/null +++ b/web/src/views/admin/PrivacyConfig.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/web/src/views/admin/StorageConfig.vue b/web/src/views/admin/StorageConfig.vue new file mode 100644 index 00000000..a5b1ff3d --- /dev/null +++ b/web/src/views/admin/StorageConfig.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/web/src/views/admin/SystemConfig.vue b/web/src/views/admin/SystemConfig.vue new file mode 100644 index 00000000..aaff669e --- /dev/null +++ b/web/src/views/admin/SystemConfig.vue @@ -0,0 +1,588 @@ + + + + +