From 2e009e593fba385e409170d43d995926bbaa5d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AF=BB=E6=AC=A2?= Date: Mon, 28 Apr 2025 10:07:08 +0800 Subject: [PATCH] =?UTF-8?q?:refactor:=20=E9=87=8D=E6=9E=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84Load=E5=87=BD?= =?UTF-8?q?=E6=95=B0=EF=BC=8C=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=B9=B6=E4=BC=98=E5=8C=96Redis=E5=92=8CDocker?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/config.dev.yaml | 22 ++++-- configs/config.yaml | 10 ++- go.mod | 4 + go.sum | 16 +++- internal/config/README.md | 2 +- internal/config/auth.go | 9 +++ internal/config/config.go | 47 ++---------- internal/config/docker.go | 22 +++--- internal/config/redis.go | 22 ++++++ internal/docker/README.md | 67 +--------------- internal/docker/client.go | 42 +++++----- internal/docker/container.go | 38 +++++----- internal/docker/monitor.go | 139 ---------------------------------- internal/docker/robot.go | 7 +- internal/handler/api_login.go | 2 +- internal/handler/auth.go | 28 ++----- internal/handler/logto.go | 87 +++------------------ internal/handler/robot.go | 20 +---- internal/initialize/config.go | 67 ++++++++++++++++ internal/initialize/init.go | 18 +++++ internal/logto/logto.go | 47 ++++++++++++ internal/middleware/auth.go | 43 +++++++---- internal/minio/funcs.go | 12 +-- internal/minio/minio.go | 17 ++--- internal/model/README.md | 8 +- internal/model/db.go | 98 ++++++++++++------------ internal/redis/redis.go | 36 +++++++++ internal/server/server.go | 18 ++--- main.go | 60 +++------------ 29 files changed, 427 insertions(+), 581 deletions(-) create mode 100644 internal/config/redis.go delete mode 100644 internal/docker/monitor.go create mode 100644 internal/initialize/config.go create mode 100644 internal/initialize/init.go create mode 100644 internal/logto/logto.go create mode 100644 internal/redis/redis.go diff --git a/configs/config.dev.yaml b/configs/config.dev.yaml index 9519858..bba612d 100644 --- a/configs/config.dev.yaml +++ b/configs/config.dev.yaml @@ -13,19 +13,21 @@ database: dbname: "wechat_bot" charset: "utf8mb4" +redis: + host: "10.0.0.31" + port: 6379 + password: "pGhQKwj7DE7FbFL1" + db: 13 + docker: host: "http://10.0.0.243:2375" apiVersion: "1.41" imageName: "lxh01/xybotv2:latest" network: "bridge" - memory: 123 # 容器内存限制(MB) - redis: - host: "10.0.0.31" - password: "pGhQKwj7DE7FbFL1" - db: 2 + memory: 512 # 容器内存限制(MB) auth: - type: "password" # 支持 password 和 logto 两种 + type: "logto" # 支持 password 和 logto 两种 password: secretKey: "your-secret-key-change-me" # 加密密钥 adminToken: "admin-token-change-me" # 密码 @@ -38,3 +40,11 @@ auth: logger: level: "debug" file: "" # 空表示日志输出到控制台 + +minio: + endpoint: 10.0.0.5:9000 + host: 10.0.0.5:9000 + accessKey: ZsRIVPn3XZtjAeL5Mu50 + secretKey: h8uu621Z9YOvd19VWx84A2nPfIU4ND03aAo69Xlx + bucket: wechat + useSSL: false diff --git a/configs/config.yaml b/configs/config.yaml index 5a80cdb..14660f4 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -28,16 +28,18 @@ database: # type: "sqlite" # dbname: "./data/wechat_demo.db" +redis: + host: "10.0.0.31" + port: 6379 + password: "pGhQKwj7DE7FbFL1" + db: 13 + docker: host: "unix:///var/run/docker.sock" apiVersion: "1.41" imageName: "lxh01/xybotv2:latest" network: "bridge" memory: 123 # 容器内存限制(MB) - redis: - host: "10.0.0.31" - password: "pGhQKwj7DE7FbFL1" - db: 2 auth: type: "password" # 支持 password 和 logto 两种 diff --git a/go.mod b/go.mod index ed601ec..1eee617 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-co-op/gocron/v2 v2.16.1 github.com/goccy/go-json v0.10.5 github.com/gofiber/fiber/v2 v2.52.6 + github.com/gofiber/storage/redis/v3 v3.1.4 github.com/gofiber/template/html/v2 v2.1.3 github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 @@ -27,7 +28,9 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/containerd/log v0.1.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -67,6 +70,7 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/xid v1.6.0 // indirect diff --git a/go.sum b/go.sum index 156c604..98c2e09 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gitee.ltd/lxh/xybot v0.0.3 h1:/xRCnW2nMtx/hdV7TdpEcL3Sh8f9oBGHToONk0yGstA= -gitee.ltd/lxh/xybot v0.0.3/go.mod h1:jYfEAQ3WPsST/PY4fEEVFjU6KtMocxn3sQi78I+vdxc= -gitee.ltd/lxh/xybot v0.0.4 h1:sEs6ZOZud2oDWvW1MVpwHWJUq3AyxYA1G/SGP4En+/0= -gitee.ltd/lxh/xybot v0.0.4/go.mod h1:jYfEAQ3WPsST/PY4fEEVFjU6KtMocxn3sQi78I+vdxc= gitee.ltd/lxh/xybot v0.0.5 h1:kgwJktO/p7WbywUuAGTPH2V4VOta6dnYs1CXz6qVvZU= gitee.ltd/lxh/xybot v0.0.5/go.mod h1:jYfEAQ3WPsST/PY4fEEVFjU6KtMocxn3sQi78I+vdxc= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= @@ -14,13 +10,21 @@ github.com/agiledragon/gomonkey/v2 v2.12.0 h1:ek0dYu9K1rSV+TgkW5LvNNPRWyDZVIxGMC github.com/agiledragon/gomonkey/v2 v2.12.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= @@ -61,6 +65,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/storage/redis/v3 v3.1.4 h1:lmI+exp/u17zoD7qXtCdkaGwr7OB21F4m77tZtIhMhI= +github.com/gofiber/storage/redis/v3 v3.1.4/go.mod h1:SXHWcuzoFZlMxJ6Qnu50rvSWQOJYSxiLrOFE+8FpXAM= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o= @@ -145,6 +151,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/internal/config/README.md b/internal/config/README.md index cfc1850..e4f611d 100644 --- a/internal/config/README.md +++ b/internal/config/README.md @@ -16,4 +16,4 @@ ## 使用方法 -应用启动时,会通过`config.Load()`函数加载配置文件和环境变量。配置文件默认位于`configs/config.yaml`,环境变量可以覆盖配置文件中的设置。 +应用启动时,会通过`initialize.Init()`函数加载配置文件和环境变量。配置文件默认位于`configs/config.yaml`,环境变量可以覆盖配置文件中的设置。 diff --git a/internal/config/auth.go b/internal/config/auth.go index 42ee041..000a8f2 100644 --- a/internal/config/auth.go +++ b/internal/config/auth.go @@ -2,6 +2,7 @@ package config import ( "fmt" + logtoClient "github.com/logto-io/go/v2/client" ) // AuthConfig 认证配置 @@ -90,3 +91,11 @@ func (c *AuthLogto) Validate() error { return nil } + +// GetLogtoClient +// @description: 获取Logto客户端 +// @receiver c +// @return cli +func (c *AuthLogto) GetLogtoClient() (cli *logtoClient.LogtoConfig) { + return &logtoClient.LogtoConfig{Endpoint: c.Endpoint, AppId: c.AppId, AppSecret: c.AppSecret} +} diff --git a/internal/config/config.go b/internal/config/config.go index f14ebfc..7927952 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,56 +1,18 @@ package config -import ( - "errors" - "os" - "strings" - - "github.com/spf13/viper" -) +var Scd Config // Config 是应用程序的主配置结构 type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` + Redis RedisConfig `mapstructure:"redis"` Docker DockerConfig `mapstructure:"docker"` Auth AuthConfig `mapstructure:"auth"` Logger LoggerConfig `mapstructure:"logger"` Minio MinioConfig `mapstructure:"minio"` } -// Load 加载配置文件和环境变量 -func Load() (*Config, error) { - viper.SetConfigName("config") - - // 检查是否有环境变量指定使用开发配置 - if os.Getenv("APP_ENV") == "development" { - viper.SetConfigName("config.dev") - } - - viper.SetConfigType("yaml") - viper.AddConfigPath("./configs") - viper.AddConfigPath(".") - - // 环境变量覆盖 - viper.AutomaticEnv() - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - - // 读取配置文件 - if err := viper.ReadInConfig(); err != nil { - // 配置文件不存在时不返回错误 - if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return nil, err - } - } - - var cfg Config - if err := viper.Unmarshal(&cfg); err != nil { - return nil, err - } - - return &cfg, nil -} - // Validate 验证配置是否有效 func (c *Config) Validate() error { if err := c.Server.Validate(); err != nil { @@ -71,5 +33,10 @@ func (c *Config) Validate() error { if err := c.Minio.Validate(); err != nil { return err } + + if err := c.Redis.Validate(); err != nil { + return err + } + return nil } diff --git a/internal/config/docker.go b/internal/config/docker.go index 3fd581d..74f0846 100644 --- a/internal/config/docker.go +++ b/internal/config/docker.go @@ -4,21 +4,13 @@ import ( "fmt" ) -// RedisConfig 是Redis相关配置 -type RedisConfig struct { - Host string `mapstructure:"host"` - Password string `mapstructure:"password"` - DB int `mapstructure:"db"` -} - // DockerConfig Docker配置 type DockerConfig struct { - Host string `mapstructure:"host"` // Docker daemon 主机地址 - APIVersion string `mapstructure:"apiVersion"` // Docker API版本 - ImageName string `mapstructure:"imageName"` // 微信机器人Docker镜像名称 - Network string `mapstructure:"network"` // 容器网络 - Memory int64 `mapstructure:"memory"` // 内存限制 - Redis RedisConfig `mapstructure:"redis"` // Redis配置 + Host string `mapstructure:"host"` // Docker daemon 主机地址 + APIVersion string `mapstructure:"apiVersion"` // Docker API版本 + ImageName string `mapstructure:"imageName"` // 微信机器人Docker镜像名称 + Network string `mapstructure:"network"` // 容器网络 + Memory int64 `mapstructure:"memory"` // 内存限制 } // Validate 验证Docker配置 @@ -36,5 +28,9 @@ func (c *DockerConfig) Validate() error { c.Network = "bridge" } + if c.Memory == 0 { + c.Memory = 512 // 默认内存限制为512M + } + return nil } diff --git a/internal/config/redis.go b/internal/config/redis.go new file mode 100644 index 0000000..2b4df5b --- /dev/null +++ b/internal/config/redis.go @@ -0,0 +1,22 @@ +package config + +import "fmt" + +// RedisConfig 是Redis相关配置 +type RedisConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Password string `mapstructure:"password"` + DB int `mapstructure:"db"` +} + +// Validate 验证Redis配置 +func (r *RedisConfig) Validate() error { + if r.Host == "" { + return fmt.Errorf("redis主机不能为空") + } + if r.Port <= 0 { + r.Port = 6379 + } + return nil +} diff --git a/internal/docker/README.md b/internal/docker/README.md index 388cdd6..4a9f882 100644 --- a/internal/docker/README.md +++ b/internal/docker/README.md @@ -16,71 +16,6 @@ - 启动、停止和删除容器 - 获取容器状态和日志 -2. **微信机器人操作** - - 获取登录二维码 - - 微信登录状态监控 - - 获取联系人列表和群成员 - - 获取聊天记录 - - 微信登出 - -3. **状态监控** +2. **状态监控** - 定期检查微信机器人容器状态 - 自动更新数据库中的状态信息 - -## 使用方法 - -初始化Docker客户端: - -```go -import ( - "context" - "github.com/Lxh/wechat-demo/internal/config" - "github.com/Lxh/wechat-demo/internal/docker" -) - -func main() { - cfg, _ := config.Load() - err := docker.InitClient(&cfg.Docker) - if err != nil { - panic(err) - } - - // 创建微信机器人容器 - ctx := context.Background() - containerID, err := docker.CreateRobotContainer(ctx, &cfg.Docker, "robot1") - if err != nil { - panic(err) - } - - // 获取容器状态 - status, errMsg, err := docker.GetWechatBotStatus(ctx, containerID) - // ... -} -``` - -启动容器监控: - -```go -import ( - "context" - "time" - "github.com/Lxh/wechat-demo/internal/model" - "github.com/Lxh/wechat-demo/internal/docker" -) - -func main() { - db := model.GetDB() - - // 创建监控器,每分钟检查一次 - monitor := docker.NewContainerMonitor(db, time.Minute) - - // 启动监控 - monitor.Start(context.Background()) - - // 添加容器到监控列表 - monitor.AddRobot("container-id") - - // 停止监控 - // monitor.Stop() -} -``` diff --git a/internal/docker/client.go b/internal/docker/client.go index fcf6814..f6f5dc0 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -1,47 +1,47 @@ package docker import ( - "sync" - "github.com/docker/docker/client" + "github.com/gofiber/fiber/v2/log" "gitee.ltd/lxh/wechat-robot/internal/config" ) var ( dockerClient *client.Client - clientOnce sync.Once - clientErr error ) // InitClient 初始化Docker客户端 -func InitClient(cfg *config.DockerConfig) error { - clientOnce.Do(func() { - options := []client.Opt{ - client.WithAPIVersionNegotiation(), +func InitClient() { + var err error + defer func() { + if err != nil { + log.Panicf("Docker客户端初始化失败: %v", err) } + }() - // 如果指定了Docker主机地址 - if cfg.Host != "" { - options = append(options, client.WithHost(cfg.Host)) - } + options := []client.Opt{ + client.WithAPIVersionNegotiation(), + } - // 如果指定了API版本 - if cfg.APIVersion != "" { - options = append(options, client.WithVersion(cfg.APIVersion)) - } + // 如果指定了Docker主机地址 + if config.Scd.Docker.Host != "" { + options = append(options, client.WithHost(config.Scd.Docker.Host)) + } - // 创建Docker客户端 - dockerClient, clientErr = client.NewClientWithOpts(options...) - }) + // 如果指定了API版本 + if config.Scd.Docker.APIVersion != "" { + options = append(options, client.WithVersion(config.Scd.Docker.APIVersion)) + } - return clientErr + // 创建Docker客户端 + dockerClient, err = client.NewClientWithOpts(options...) } // GetClient 获取Docker客户端实例 func GetClient() *client.Client { if dockerClient == nil { - panic("Docker client not initialized, call InitClient first") + panic("Docker客户端未初始化,先调用InitClient") } return dockerClient } diff --git a/internal/docker/container.go b/internal/docker/container.go index f573f84..16505cf 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -30,7 +30,7 @@ type ContainerInfo struct { } // CreateContainer 创建容器 -func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, env []string, labels map[string]string, port int) (string, error) { +func CreateContainer(ctx context.Context, name string, env []string, labels map[string]string, port int) (string, error) { cli := GetClient() // 检测是否在容器内运行 @@ -42,7 +42,7 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, currentNetworkName = getCurrentContainerNetwork(ctx, cli) if currentNetworkName != "" { // 使用发现的网络替代配置中的网络 - cfg.Network = currentNetworkName + config.Scd.Docker.Network = currentNetworkName } } @@ -60,7 +60,7 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, // 如果没有指定端口,则自动分配 if port <= 0 { // 查找同镜像容器的最大端口号并加1 - maxPort, err := findMaxPortForImage(ctx, cli, cfg.ImageName) + maxPort, err := findMaxPortForImage(ctx, cli, config.Scd.Docker.ImageName) if err != nil { // 如果出错,使用默认端口9001 port = 9001 @@ -79,15 +79,18 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, } // 添加Redis环境变量 - if cfg.Redis.Host != "" { - env = append(env, fmt.Sprintf("REDIS_HOST=%s", cfg.Redis.Host)) - env = append(env, fmt.Sprintf("REDIS_PASSWORD=%s", cfg.Redis.Password)) - env = append(env, fmt.Sprintf("REDIS_DB=%d", cfg.Redis.DB)) + if config.Scd.Redis.Host != "" { + env = append(env, fmt.Sprintf("REDIS_HOST=%s", config.Scd.Redis.Host)) + env = append(env, fmt.Sprintf("REDIS_PASSWORD=%s", config.Scd.Redis.Password)) + env = append(env, fmt.Sprintf("REDIS_DB=%d", config.Scd.Redis.DB)) + if config.Scd.Redis.Port != 0 { + env = append(env, fmt.Sprintf("REDIS_PORT=%d", config.Scd.Redis.Port)) + } } // 设置容器配置 containerConfig := &container.Config{ - Image: cfg.ImageName, + Image: config.Scd.Docker.ImageName, Env: env, ExposedPorts: exposedPorts, Labels: labels, @@ -100,19 +103,16 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, Name: "unless-stopped", }, } - if cfg.Memory == 0 { - cfg.Memory = 512 // 默认内存限制为512M - } - hostConfig.Memory = cfg.Memory * 1024 * 1024 // 限制使用内存 + hostConfig.Memory = config.Scd.Docker.Memory * 1024 * 1024 // 限制使用内存 // 设置网络配置 networkingConfig := &network.NetworkingConfig{} - if cfg.Network != "" { + if config.Scd.Docker.Network != "" { // 首先检查网络类型,只在用户自定义网络上分配固定IP isUserNetwork := false - if cfg.Network != "bridge" && cfg.Network != "host" && cfg.Network != "none" { + if config.Scd.Docker.Network != "bridge" && config.Scd.Docker.Network != "host" && config.Scd.Docker.Network != "none" { // 检查网络是否存在 - _, err := cli.NetworkInspect(ctx, cfg.Network, network.InspectOptions{}) + _, err := cli.NetworkInspect(ctx, config.Scd.Docker.Network, network.InspectOptions{}) if err == nil { isUserNetwork = true } @@ -124,7 +124,7 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, // 只在用户自定义网络上尝试分配固定IP if isUserNetwork { // 自动为容器分配一个递增的IP地址 - nextIP, err := getNextAvailableIPInNetwork(ctx, cli, cfg.Network) + nextIP, err := getNextAvailableIPInNetwork(ctx, cli, config.Scd.Docker.Network) if err == nil && nextIP != "" { endpointSettings.IPAMConfig = &network.EndpointIPAMConfig{ IPv4Address: nextIP, @@ -132,7 +132,7 @@ func CreateContainer(ctx context.Context, cfg *config.DockerConfig, name string, } } - endpointsConfig[cfg.Network] = endpointSettings + endpointsConfig[config.Scd.Docker.Network] = endpointSettings networkingConfig.EndpointsConfig = endpointsConfig } @@ -650,7 +650,7 @@ func GetContainerIP(ctx context.Context, containerID string, cfg *config.DockerC } // GetContainerHost 获取容器的访问地址(格式:ip:port) -func GetContainerHost(ctx context.Context, containerID string, cfg *config.DockerConfig) (string, error) { +func GetContainerHost(ctx context.Context, containerID string) (string, error) { // 如果在Docker环境中运行,需要获取容器真实IP或容器名 if isRunningInContainer() { cli := GetClient() @@ -689,7 +689,7 @@ func GetContainerHost(ctx context.Context, containerID string, cfg *config.Docke return "", fmt.Errorf("无法检查容器: %w", err) } - hostIP := extractHostIP(cfg.Host) + hostIP := extractHostIP(config.Scd.Docker.Host) // 查找9000端口的映射 for _, port := range inspect.NetworkSettings.Ports["9000/tcp"] { diff --git a/internal/docker/monitor.go b/internal/docker/monitor.go deleted file mode 100644 index db82577..0000000 --- a/internal/docker/monitor.go +++ /dev/null @@ -1,139 +0,0 @@ -package docker - -import ( - "context" - "sync" - "time" - - "gorm.io/gorm" -) - -// ContainerMonitor 容器监控器 -type ContainerMonitor struct { - db *gorm.DB - interval time.Duration - robots map[string]struct{} // 记录正在监控的机器人容器ID - mutex sync.RWMutex - stopChan chan struct{} - monitorActive bool -} - -// NewContainerMonitor 创建容器监控器 -func NewContainerMonitor(db *gorm.DB, interval time.Duration) *ContainerMonitor { - if interval == 0 { - interval = 30 * time.Second // 默认30秒检查一次 - } - - return &ContainerMonitor{ - db: db, - interval: interval, - robots: make(map[string]struct{}), - stopChan: make(chan struct{}), - } -} - -// Start 启动监控 -func (m *ContainerMonitor) Start(ctx context.Context) { - //m.mutex.Lock() - //if m.monitorActive { - // m.mutex.Unlock() - // return - //} - //m.monitorActive = true - //m.mutex.Unlock() - // - //log.Println("Starting container monitor...") - // - //// 启动监控协程 - //go func() { - // ticker := time.NewTicker(m.interval) - // defer ticker.Stop() - // - // for { - // select { - // case <-ticker.C: - // m.checkRobots(context.Background()) - // case <-m.stopChan: - // log.Println("Container monitor stopped") - // return - // case <-ctx.Done(): - // log.Println("Container monitor stopped due to context cancellation") - // return - // } - // } - //}() -} - -// Stop 停止监控 -func (m *ContainerMonitor) Stop() { - m.mutex.Lock() - defer m.mutex.Unlock() - - if m.monitorActive { - close(m.stopChan) - m.monitorActive = false - } -} - -// AddRobot 添加机器人到监控列表 -func (m *ContainerMonitor) AddRobot(containerID string) { - m.mutex.Lock() - defer m.mutex.Unlock() - m.robots[containerID] = struct{}{} -} - -// RemoveRobot 从监控列表中移除机器人 -func (m *ContainerMonitor) RemoveRobot(containerID string) { - m.mutex.Lock() - defer m.mutex.Unlock() - delete(m.robots, containerID) -} - -// checkRobots 检查所有机器人状态 -//func (m *ContainerMonitor) checkRobots(ctx context.Context) { -// m.mutex.RLock() -// robotIDs := make([]string, 0, len(m.robots)) -// for id := range m.robots { -// robotIDs = append(robotIDs, id) -// } -// m.mutex.RUnlock() -// -// for _, containerID := range robotIDs { -// // 使用新的上下文,避免一个检查失败影响其他检查 -// checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second) -// defer cancel() -// -// // 获取机器人的当前状态 -// status, errMsg, err := GetWechatBotStatus(checkCtx, containerID) -// if err != nil { -// log.Printf("Error checking robot %s: %v", containerID, err) -// continue -// } -// -// // 更新数据库中的状态 -// robot := &model.Robot{} -// result := m.db.Where("container_id = ?", containerID).First(robot) -// if result.Error != nil { -// log.Printf("Error finding robot %s in database: %v", containerID, result.Error) -// continue -// } -// -// // 只有状态变化时才更新 -// if robot.Status != status { -// robot.Status = status -// robot.ErrorMessage = errMsg -// -// // 如果状态变为在线,更新登录时间 -// if status == model.RobotStatusOnline { -// now := time.Now() -// robot.LastLoginAt = &now -// } -// -// if err := m.db.Save(robot).Error; err != nil { -// log.Printf("Error updating robot %s status: %v", containerID, err) -// } else { -// log.Printf("Robot %s status updated to %s", containerID, status) -// } -// } -// } -//} diff --git a/internal/docker/robot.go b/internal/docker/robot.go index b174792..700e761 100644 --- a/internal/docker/robot.go +++ b/internal/docker/robot.go @@ -2,7 +2,6 @@ package docker import ( "context" - "gitee.ltd/lxh/wechat-robot/internal/config" "github.com/gofiber/fiber/v2/log" ) @@ -14,7 +13,7 @@ const ( ) // CreateRobotContainer 创建微信机器人容器 -func CreateRobotContainer(ctx context.Context, cfg *config.DockerConfig, robotName string, port int) (string, string, error) { +func CreateRobotContainer(ctx context.Context, robotName string, port int) (string, string, error) { // 创建容器标签 labels := map[string]string{ WechatBotLabelKey: WechatBotLabelValue, @@ -27,7 +26,7 @@ func CreateRobotContainer(ctx context.Context, cfg *config.DockerConfig, robotNa } // 创建容器 - containerID, err := CreateContainer(ctx, cfg, "wechat-bot-"+robotName, env, labels, port) + containerID, err := CreateContainer(ctx, "wechat-bot-"+robotName, env, labels, port) if err != nil { return "", "", err } @@ -41,7 +40,7 @@ func CreateRobotContainer(ctx context.Context, cfg *config.DockerConfig, robotNa } // 获取容器访问地址 - containerHost, err := GetContainerHost(ctx, containerID, cfg) + containerHost, err := GetContainerHost(ctx, containerID) if err != nil { log.Warnf("警告: 无法获取容器访问地址: %v", err) containerHost = "localhost:9000" // 使用默认值 diff --git a/internal/handler/api_login.go b/internal/handler/api_login.go index 579a0bd..fa77e9c 100644 --- a/internal/handler/api_login.go +++ b/internal/handler/api_login.go @@ -66,7 +66,7 @@ func CheckQRCodeStatus(c *fiber.Ctx) error { }) } - if cfg, _ := config.Load(); cfg.Server.Env == "development" { + if config.Scd.Server.Env == "development" { bs, _ := json.Marshal(response) log.Debugf("扫码返回结果: %s", bs) } diff --git a/internal/handler/auth.go b/internal/handler/auth.go index 9f6ec85..38dded0 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -12,7 +12,7 @@ import ( // LoginPage 显示登录页面 func LoginPage(c *fiber.Ctx) error { // 如果已经登录,则重定向到机器人列表 - if middleware.IsAuthenticated(c) { + if _, flag := middleware.IsAuthenticated(c); flag { return c.Redirect("/admin/robots") } @@ -20,7 +20,7 @@ func LoginPage(c *fiber.Ctx) error { errorMsg := c.Query("error") // 加载配置 - if cfg, _ := config.Load(); cfg.Auth.Type == "logto" { + if config.Scd.Auth.Type == "logto" { // 如果使用Logto认证,重定向到Logto登录页面 return c.Redirect("/auth/logto/login") } @@ -38,31 +38,25 @@ func LoginSubmit(c *fiber.Ctx) error { // 获取用户输入的密钥 token := c.FormValue("token") - // 检查凭据有效性 - cfg, err := config.Load() - if err != nil { - return c.Redirect("/login?error=系统错误,无法加载配置") - } - // 根据认证类型进行不同的验证 - if cfg.Auth.Type == "password" { + if config.Scd.Auth.Type == "password" { // 仅验证密钥是否与配置的AdminToken匹配 - if token != cfg.Auth.Password.AdminToken { + if token != config.Scd.Auth.Password.AdminToken { return c.Redirect("/login?error=访问密钥不正确") } // 登录成功,设置认证 Cookie cookie := new(fiber.Cookie) cookie.Name = "auth_token" - cookie.Value = cfg.Auth.Password.SecretKey // 在实际应用中,这应该是一个生成的会话令牌 - cookie.Expires = time.Now().Add(time.Hour * time.Duration(cfg.Auth.Password.TokenExpiry)) + cookie.Value = config.Scd.Auth.Password.SecretKey // 在实际应用中,这应该是一个生成的会话令牌 + cookie.Expires = time.Now().Add(time.Hour * time.Duration(config.Scd.Auth.Password.TokenExpiry)) cookie.HTTPOnly = true cookie.Path = "/" c.Cookie(cookie) // 重定向到机器人列表页面,而不是首页 return c.Redirect("/admin/robots") - } else if cfg.Auth.Type == "logto" { + } else if config.Scd.Auth.Type == "logto" { // 对于Logto登录,我们重定向到Logto登录页面 return c.Redirect("/auth/logto/login") } @@ -73,14 +67,8 @@ func LoginSubmit(c *fiber.Ctx) error { // Logout 处理退出登录 func Logout(c *fiber.Ctx) error { - // 加载配置 - cfg, err := config.Load() - if err != nil { - return c.Redirect("/login?error=系统错误,无法加载配置") - } - // 根据认证类型执行不同的登出逻辑 - if cfg.Auth.Type == "logto" { + if config.Scd.Auth.Type == "logto" { // 对于Logto登录,使用Logto的登出流程 return c.Redirect("/auth/logto/logout") } diff --git a/internal/handler/logto.go b/internal/handler/logto.go index 02df987..28fa1da 100644 --- a/internal/handler/logto.go +++ b/internal/handler/logto.go @@ -1,48 +1,23 @@ package handler import ( - "net/http" - "time" - - "gitee.ltd/lxh/wechat-robot/internal/types" + "gitee.ltd/lxh/wechat-robot/internal/logto" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/session" - logtoClient "github.com/logto-io/go/v2/client" "github.com/valyala/fasthttp/fasthttpadaptor" - - "gitee.ltd/lxh/wechat-robot/internal/config" + "net/http" ) -// 实现Logto会话存储接口 -var store = session.New(session.Config{ - KeyLookup: "cookie:logto-session", - CookieSecure: false, // 开发环境可设成false -}) - // LogtoLogin 重定向到Logto登录页面 func LogtoLogin(c *fiber.Ctx) error { - // 加载配置 - cfg, err := config.Load() - if err != nil { - return c.Redirect("/error?error=系统错误,无法加载配置") - } - // 构建回调URL callbackURL := c.Protocol() + "://" + c.Hostname() + "/auth/logto/callback" //callbackURL := "https://wechat.0bug.dev/auth/logto/callback" - // 创建Logto客户端配置 - logtoConfig := &logtoClient.LogtoConfig{ - Endpoint: cfg.Auth.Logto.Endpoint, - AppId: cfg.Auth.Logto.AppId, - AppSecret: cfg.Auth.Logto.AppSecret, - } - // 获取会话 - fiberSessionStorage := &types.LogtoSessionStorage{Store: store, Ctx: c} - - // 创建Logto客户端 - client := logtoClient.NewLogtoClient(logtoConfig, fiberSessionStorage) // 获取登录链接 + client, err := logto.GetLogtoClient(c) + if err != nil { + return c.Redirect("/error?error=Logto登录错误: " + err.Error()) + } signInUri, err := client.SignInWithRedirectUri(callbackURL) if err != nil { return c.Redirect("/error?error=Logto登录错误: " + err.Error() + "。请在Logto控制台确认回调URI为: " + callbackURL) @@ -53,23 +28,11 @@ func LogtoLogin(c *fiber.Ctx) error { // LogtoCallback 处理Logto登录回调 func LogtoCallback(c *fiber.Ctx) error { - // 加载配置 - cfg, err := config.Load() + client, err := logto.GetLogtoClient(c) if err != nil { - return c.Redirect("/error?error=系统错误,无法加载配置") + return c.Redirect("/error?error=Logto登录错误: " + err.Error()) } - // 创建Logto客户端配置 - logtoConfig := &logtoClient.LogtoConfig{ - Endpoint: cfg.Auth.Logto.Endpoint, - AppId: cfg.Auth.Logto.AppId, - AppSecret: cfg.Auth.Logto.AppSecret, - } - // 获取会话 - fiberSessionStorage := &types.LogtoSessionStorage{Store: store, Ctx: c} - // 创建Logto客户端 - client := logtoClient.NewLogtoClient(logtoConfig, fiberSessionStorage) - r := &http.Request{} if err = fasthttpadaptor.ConvertRequest(c.Context(), r, true); err != nil { return c.Redirect("/error?error=转换请求错误: " + err.Error()) @@ -80,38 +43,17 @@ func LogtoCallback(c *fiber.Ctx) error { return c.Redirect("/error?error=Logto回调处理错误: " + err.Error()) } - // 设置auth_token cookie,使用logto前缀 - cookie := new(fiber.Cookie) - cookie.Name = "auth_token" - cookie.Value = "logto:auth" // 设置auth_token带有logto前缀 - cookie.Expires = time.Now().Add(24 * time.Hour) // 24小时过期 - cookie.HTTPOnly = true - cookie.Path = "/" - c.Cookie(cookie) - // 重定向到机器人列表页面 return c.Redirect("/admin/robots") } // LogtoLogout 处理Logto退出登录 func LogtoLogout(c *fiber.Ctx) error { - // 加载配置 - cfg, err := config.Load() + client, err := logto.GetLogtoClient(c) if err != nil { - return c.Redirect("/error?error=系统错误,无法加载配置") + return c.Redirect("/error?error=Logto登录错误: " + err.Error()) } - // 创建Logto客户端配置 - logtoConfig := &logtoClient.LogtoConfig{ - Endpoint: cfg.Auth.Logto.Endpoint, - AppId: cfg.Auth.Logto.AppId, - AppSecret: cfg.Auth.Logto.AppSecret, - } - // 获取会话 - fiberSessionStorage := &types.LogtoSessionStorage{Store: store, Ctx: c} - // 创建Logto客户端 - client := logtoClient.NewLogtoClient(logtoConfig, fiberSessionStorage) - // 构建登出后重定向URL postLogoutRedirectURL := c.Protocol() + "://" + c.Hostname() @@ -121,15 +63,6 @@ func LogtoLogout(c *fiber.Ctx) error { return c.Redirect("/error?error=Logto登出错误: " + err.Error()) } - // 清除本地Cookie - cookie := new(fiber.Cookie) - cookie.Name = "auth_token" - cookie.Value = "" - cookie.Expires = time.Now().Add(-time.Hour) // 设置为过期 - cookie.HTTPOnly = true - cookie.Path = "/" - c.Cookie(cookie) - // 重定向到Logto登出页面 return c.Redirect(signOutUri) } diff --git a/internal/handler/robot.go b/internal/handler/robot.go index 914b211..9669808 100644 --- a/internal/handler/robot.go +++ b/internal/handler/robot.go @@ -12,13 +12,14 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" - "gitee.ltd/lxh/wechat-robot/internal/config" "gitee.ltd/lxh/wechat-robot/internal/docker" "gitee.ltd/lxh/wechat-robot/internal/model" ) // ListRobots 列出所有机器人 func ListRobots(c *fiber.Ctx) error { + log.Debugf("登录用户Id: %+v", c.Get("userId")) + db := model.GetDB() var robots []model.Robot @@ -73,17 +74,11 @@ func CreateRobot(c *fiber.Ctx) error { } } - // 加载配置 - cfg, err := config.Load() - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "加载配置失败") - } - // 创建Docker容器 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - containerID, containerHost, err := docker.CreateRobotContainer(ctx, &cfg.Docker, robotName, port) + containerID, containerHost, err := docker.CreateRobotContainer(ctx, robotName, port) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "创建容器失败: "+err.Error()) } @@ -105,11 +100,6 @@ func CreateRobot(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "保存机器人信息失败: "+err.Error()) } - // 添加到监控器 - // 注意:这里假设有一个全局可访问的监控器实例,实际中可能需要通过依赖注入或其他方式获取 - monitor := docker.NewContainerMonitor(db, time.Minute) - monitor.AddRobot(containerID) - return c.Redirect("/admin/robots/" + strconv.Itoa(int(robot.ID))) } @@ -189,10 +179,6 @@ func DeleteRobot(c *fiber.Ctx) error { // 继续删除流程,不因容器删除失败而中断 } - // 从监控器移除 - monitor := docker.NewContainerMonitor(db, time.Minute) - monitor.RemoveRobot(robot.ContainerID) - // 删除数据库记录 if err = db.Delete(&robot).Error; err != nil { // 针对API请求返回JSON diff --git a/internal/initialize/config.go b/internal/initialize/config.go new file mode 100644 index 0000000..19316db --- /dev/null +++ b/internal/initialize/config.go @@ -0,0 +1,67 @@ +package initialize + +import ( + "gitee.ltd/lxh/wechat-robot/internal/config" + "gitee.ltd/lxh/wechat-robot/internal/docker" + "gitee.ltd/lxh/wechat-robot/internal/minio" + "gitee.ltd/lxh/wechat-robot/internal/model" + "gitee.ltd/lxh/wechat-robot/internal/redis" + "github.com/fsnotify/fsnotify" + "github.com/gofiber/fiber/v2/log" + "github.com/spf13/viper" + "os" + "strings" +) + +// initConfig +// @description: 初始化配置文件 +func initConfig() { + viper.SetConfigName("config") + + // 检查是否有环境变量指定使用开发配置 + if os.Getenv("APP_ENV") == "development" { + viper.SetConfigName("config.dev") + } + + viper.SetConfigType("yaml") + viper.AddConfigPath("./configs") + viper.AddConfigPath(".") + + // 环境变量覆盖 + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // 读取配置文件 + if err := viper.ReadInConfig(); err != nil { + log.Panicf("配置文件读取失败: %v", err) + return + } + // 绑定到 + if err := viper.Unmarshal(&config.Scd); err != nil { + log.Panicf("配置文件解析失败: %v", err) + return + } + log.Debugf("配置文件解析成功: %+v", config.Scd) + if err := config.Scd.Validate(); err != nil { + log.Panicf("配置文件验证失败: %v", err) + return + } + + // 下面的代码是配置变动之后自动刷新的 + viper.OnConfigChange(func(e fsnotify.Event) { + // 绑定配置文件 + if err := viper.Unmarshal(&config.Scd); err != nil { + log.Errorf("配置文件更新失败: %v", err) + } else if err = config.Scd.Validate(); err != nil { + log.Errorf("配置文件验证失败: %v", err) + } else { + // 初始化数据库连接等操作 + model.InitDB() // 1. 初始化数据库 + redis.InitRedisClient() // 2. 初始化Redis + minio.Init() // 3. 初始化Minio + docker.InitClient() // 4. 初始化Docker客户端 + } + }) + // 监听变动 + viper.WatchConfig() +} diff --git a/internal/initialize/init.go b/internal/initialize/init.go new file mode 100644 index 0000000..37f6e82 --- /dev/null +++ b/internal/initialize/init.go @@ -0,0 +1,18 @@ +package initialize + +import ( + "gitee.ltd/lxh/wechat-robot/internal/docker" + "gitee.ltd/lxh/wechat-robot/internal/minio" + "gitee.ltd/lxh/wechat-robot/internal/model" + "gitee.ltd/lxh/wechat-robot/internal/redis" +) + +// Init +// @description: 系统初始化s +func Init() { + initConfig() // 1. 初始化配置文件 + model.InitDB() // 2. 初始化数据库 + redis.InitRedisClient() // 3. 初始化Redis + minio.Init() // 4. 初始化Minio + docker.InitClient() // 5. 初始化Docker客户端 +} diff --git a/internal/logto/logto.go b/internal/logto/logto.go new file mode 100644 index 0000000..17682e9 --- /dev/null +++ b/internal/logto/logto.go @@ -0,0 +1,47 @@ +package logto + +import ( + "gitee.ltd/lxh/wechat-robot/internal/config" + "gitee.ltd/lxh/wechat-robot/internal/types" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/storage/redis/v3" + logtoClient "github.com/logto-io/go/v2/client" +) + +var storage *redis.Storage + +// 实现Logto会话存储接口 +var store *session.Store + +// GetLogtoClient +// @description: 获取logto客户端 +func GetLogtoClient(c *fiber.Ctx) (client *logtoClient.LogtoClient, err error) { + if storage == nil { + storage = redis.New(redis.Config{ + Host: config.Scd.Redis.Host, + Port: config.Scd.Redis.Port, + Password: config.Scd.Redis.Password, + Database: config.Scd.Redis.DB, + Reset: false, // false:启动时不清除数据库,true:清除(请谨慎使用) + // 如果需要,可以配置其他选项,如 TLS、PoolSize 等 + }) + } + + if store == nil { + store = session.New(session.Config{ + Storage: storage, + KeyLookup: "cookie:logto-session", + CookieSecure: false, // 开发环境可设成false + }) + } + + // 创建Logto客户端配置 + logtoConfig := config.Scd.Auth.Logto.GetLogtoClient() + // 获取会话 + fiberSessionStorage := &types.LogtoSessionStorage{Store: store, Ctx: c} + + // 创建Logto客户端 + client = logtoClient.NewLogtoClient(logtoConfig, fiberSessionStorage) + return +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index fcde089..2ceaf6c 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -1,9 +1,10 @@ package middleware import ( - "strings" - + "context" "gitee.ltd/lxh/wechat-robot/internal/config" + "gitee.ltd/lxh/wechat-robot/internal/logto" + "gitee.ltd/lxh/wechat-robot/internal/redis" "github.com/gofiber/fiber/v2" ) @@ -11,29 +12,28 @@ import ( // @description: 检查用户是否已登录 // @param c // @return bool -func IsAuthenticated(c *fiber.Ctx) bool { +func IsAuthenticated(c *fiber.Ctx) (loginType string, flag bool) { token := c.Cookies("auth_token") if token == "" { - return false - } - - // 加载配置 - cfg, err := config.Load() - if err != nil { - return false + if token = c.Cookies("logto-session"); token == "" { + return + } } // 根据认证类型验证 - switch cfg.Auth.Type { + loginType = config.Scd.Auth.Type + switch config.Scd.Auth.Type { case "password": // 对比token (简单实现,实际应用可能需要更复杂的验证) - return token == cfg.Auth.Password.SecretKey + flag = token == config.Scd.Auth.Password.SecretKey case "logto": // 如果是Logto认证方式,检查token前缀,有前缀则认为已登录 - return strings.HasPrefix(token, "logto:") + flag = redis.Client.Exists(context.Background(), token).Val() > 0 default: - return false + // nothing } + + return } // Authenticate @@ -42,9 +42,22 @@ func IsAuthenticated(c *fiber.Ctx) bool { func Authenticate() fiber.Handler { return func(c *fiber.Ctx) error { // 检查是否已登录 - if !IsAuthenticated(c) { + loginType, flag := IsAuthenticated(c) + if !flag { return c.Redirect("/login") } + + // 获取Logto客户端 + if loginType == "logto" { + client, err := logto.GetLogtoClient(c) + if err != nil { + return c.Redirect("/error?error=Logto登录错误: " + err.Error()) + } + if userInfo, e := client.GetIdTokenClaims(); e == nil { + c.Set("userId", userInfo.Sub) + } + } + return c.Next() } } diff --git a/internal/minio/funcs.go b/internal/minio/funcs.go index fd22f1f..38b822b 100644 --- a/internal/minio/funcs.go +++ b/internal/minio/funcs.go @@ -23,12 +23,6 @@ import ( // @return url // @return err func SaveBytes(b []byte, md5 string, suffix ...string) (url string, err error) { - cfg, err := config.Load() - if err != nil { - log.Errorf("加载配置失败: %s", err.Error()) - return - } - ctx := context.Background() contentType := http.DetectContentType(b) if strings.Contains(contentType, ";") { @@ -48,17 +42,17 @@ func SaveBytes(b []byte, md5 string, suffix ...string) (url string, err error) { } log.Debugf("开始上传文件: %v", fileName) reader := bytes.NewBuffer(b) - _, err = minioClient.PutObject(ctx, cfg.Minio.BucketName, fileName, reader, -1, minio.PutObjectOptions{ContentType: contentType}) + _, err = minioClient.PutObject(ctx, config.Scd.Minio.BucketName, fileName, reader, -1, minio.PutObjectOptions{ContentType: contentType}) if err != nil { log.Errorf("文件上传错误: %v", err) return "", err } log.Debugf("文件上传完毕: %v", fileName) protocol := "http" - if cfg.Minio.UseSsl { + if config.Scd.Minio.UseSsl { protocol = "https" } - url = fmt.Sprintf("%s://%s/%s/%s", protocol, cfg.Minio.Host, cfg.Minio.BucketName, fileName) + url = fmt.Sprintf("%s://%s/%s/%s", protocol, config.Scd.Minio.Host, config.Scd.Minio.BucketName, fileName) // 异步数据入库 //go func() { // wf := db.WeChatFileEntity{Url: fileUrl, HashCode: md5, FileType: contentType} diff --git a/internal/minio/minio.go b/internal/minio/minio.go index df53c01..06b68c7 100644 --- a/internal/minio/minio.go +++ b/internal/minio/minio.go @@ -13,17 +13,12 @@ var minioClient *minio.Client // Init // @description: 初始化Minio连接 func Init() { - cfg, err := config.Load() - if err != nil { - log.Panicf("加载配置失败: %s", err.Error()) - return - } - + var err error ctx := context.Background() // 初使化 minio client对象。 - minioClient, err = minio.New(cfg.Minio.Endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(cfg.Minio.AccessKeyID, cfg.Minio.SecretAccessKey, ""), - Secure: cfg.Minio.UseSsl, + minioClient, err = minio.New(config.Scd.Minio.Endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(config.Scd.Minio.AccessKeyID, config.Scd.Minio.SecretAccessKey, ""), + Secure: config.Scd.Minio.UseSsl, }) if err != nil { log.Panicf("OSS初始化失败: %v", err.Error()) @@ -31,14 +26,14 @@ func Init() { log.Debug("OSS连接成功,开始判断桶是否存在") // 判断捅是否存在,不存在就创建 var exists bool - exists, err = minioClient.BucketExists(ctx, cfg.Minio.BucketName) + exists, err = minioClient.BucketExists(ctx, config.Scd.Minio.BucketName) if err != nil { log.Panicf("判断桶失败: %v", err) } if !exists { log.Debug("桶不存在,开始创建") // 创建桶 - err = minioClient.MakeBucket(ctx, cfg.Minio.BucketName, minio.MakeBucketOptions{Region: "us-east-1"}) + err = minioClient.MakeBucket(ctx, config.Scd.Minio.BucketName, minio.MakeBucketOptions{Region: "us-east-1"}) if err != nil { log.Panicf("OSS桶创建失败: %v", err.Error()) } diff --git a/internal/model/README.md b/internal/model/README.md index 3b27365..a43d062 100644 --- a/internal/model/README.md +++ b/internal/model/README.md @@ -23,16 +23,12 @@ ```go import ( - "github.com/Lxh/wechat-demo/internal/config" + "gitee.ltd/lxh/wechat-robot/internal/initialize" "github.com/Lxh/wechat-demo/internal/model" ) func main() { - cfg, _ := config.Load() - err := model.InitDB(&cfg.Database) - if err != nil { - panic(err) - } + initialize.Init() // 使用DB实例 db := model.GetDB() diff --git a/internal/model/db.go b/internal/model/db.go index 0686620..8cee754 100644 --- a/internal/model/db.go +++ b/internal/model/db.go @@ -4,8 +4,6 @@ import ( "database/sql" "fmt" "github.com/gofiber/fiber/v2/log" - "sync" - "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" @@ -16,69 +14,71 @@ import ( ) var ( - db *gorm.DB - dbOnce sync.Once + db *gorm.DB ) // InitDB 初始化数据库连接 -func InitDB(cfg *config.DatabaseConfig) error { +func InitDB() { var err error - - dbOnce.Do(func() { - gormConfig := &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), - } - - dsn := cfg.DSN() - - // 根据数据库类型选择对应的驱动 - switch cfg.Type { - case config.PostgreSQL: - db, err = gorm.Open(postgres.Open(dsn), gormConfig) - case config.MySQL: - db, err = gorm.Open(mysql.Open(dsn), gormConfig) - case config.SQLite: - db, err = gorm.Open(sqlite.Open(dsn), gormConfig) - default: - err = fmt.Errorf("unsupported database type: %s", cfg.Type) - return - } - + defer func() { if err != nil { - log.Fatalf("Failed to connect to database: %v", err) - return + log.Panicf("数据库初始化失败: %v", err) } + }() - // 自动迁移数据库模型 - err = migrateDB() + gormConfig := &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + } + + dsn := config.Scd.Database.DSN() + + // 根据数据库类型选择对应的驱动 + switch config.Scd.Database.Type { + case config.PostgreSQL: + db, err = gorm.Open(postgres.Open(dsn), gormConfig) + case config.MySQL: + db, err = gorm.Open(mysql.Open(dsn), gormConfig) + case config.SQLite: + db, err = gorm.Open(sqlite.Open(dsn), gormConfig) + default: + err = fmt.Errorf("不支持的数据库类型: %s", config.Scd.Database.Type) + return + } + + if err != nil { + log.Fatalf("无法连接到数据库: %v", err) + return + } + + // 自动迁移数据库模型 + err = migrateDB() + if err != nil { + log.Fatalf("迁移数据库失败: %v", err) + return + } + + // 对于SQLite,执行一些特定的优化 + if config.Scd.Database.Type == config.SQLite { + var sqlDB *sql.DB + sqlDB, err = db.DB() if err != nil { - log.Fatalf("Failed to migrate database: %v", err) + log.Errorf("无法获取基础SQL DB: %v", err) return } - // 对于SQLite,执行一些特定的优化 - if cfg.Type == config.SQLite { - var sqlDB *sql.DB - sqlDB, err = db.DB() - if err != nil { - log.Errorf("Warning: Could not get underlying SQL DB: %v", err) - return - } + // 启用外键约束 + _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") + // 设置连接池大小 + sqlDB.SetMaxOpenConns(1) // SQLite建议使用单连接 + } - // 启用外键约束 - _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") - // 设置连接池大小 - sqlDB.SetMaxOpenConns(1) // SQLite建议使用单连接 - } - }) - - return err + return } // GetDB 获取数据库连接实例 func GetDB() *gorm.DB { if db == nil { - panic("Database not initialized, call InitDB first") + panic("数据库未初始化,先调用InitDB") } return db } @@ -91,7 +91,7 @@ func CloseDB() error { sqlDB, err := db.DB() if err != nil { - return fmt.Errorf("get sql.DB instance error: %w", err) + return fmt.Errorf("获取sql.DB实例错误: %w", err) } return sqlDB.Close() diff --git a/internal/redis/redis.go b/internal/redis/redis.go new file mode 100644 index 0000000..05d5894 --- /dev/null +++ b/internal/redis/redis.go @@ -0,0 +1,36 @@ +package redis + +import ( + "context" + "fmt" + "gitee.ltd/lxh/wechat-robot/internal/config" + "github.com/gofiber/fiber/v2/log" + "github.com/redis/go-redis/v9" +) + +var Client *redis.Client + +// InitRedisClient +// @description: 初始化redis客户端 +func InitRedisClient() { + var err error + defer func() { + if err != nil { + log.Errorf("Redis连接初始化失败: %s", err) + } + }() + + // 初始化连接 + conn := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d", config.Scd.Redis.Host, config.Scd.Redis.Port), + Password: config.Scd.Redis.Password, + DB: config.Scd.Redis.DB, + }) + if err = conn.Ping(context.Background()).Err(); err != nil { + log.Errorf("Redis连接初始化失败: %s", err) + return + } + log.Debug("Redis连接初始化成功") + Client = conn + return +} diff --git a/internal/server/server.go b/internal/server/server.go index 7c5a125..ce09384 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,12 +22,11 @@ import ( // Server 表示HTTP服务器 type Server struct { - app *fiber.App - config *config.Config + app *fiber.App } // New 创建新的服务器实例 -func New(cfg *config.Config) *Server { +func New() *Server { // 确保视图目录存在 viewsDir := "./internal/view" if _, err := os.Stat(viewsDir); os.IsNotExist(err) { @@ -36,8 +35,8 @@ func New(cfg *config.Config) *Server { // 初始化模板引擎 engine := html.New(viewsDir, ".html") - engine.Reload(cfg.Server.Env == "development") // 开发环境下启用热重载 - engine.Debug(cfg.Server.Env == "development") // 增加Debug输出 + engine.Reload(config.Scd.Server.Env == "development") // 开发环境下启用热重载 + engine.Debug(config.Scd.Server.Env == "development") // 增加Debug输出 // 添加自定义模板函数 engine.AddFunc("sub", func(a, b int) int { @@ -64,7 +63,7 @@ func New(cfg *config.Config) *Server { app := fiber.New(fiber.Config{ Views: engine, ViewsLayout: "layouts/main", // 默认布局 - EnablePrintRoutes: cfg.Server.Env == "development", + EnablePrintRoutes: config.Scd.Server.Env == "development", JSONEncoder: json.Marshal, JSONDecoder: json.Unmarshal, ErrorHandler: func(c *fiber.Ctx, err error) error { @@ -107,8 +106,7 @@ func New(cfg *config.Config) *Server { app.Static("/public", "./public") return &Server{ - app: app, - config: cfg, + app: app, } } @@ -156,8 +154,8 @@ func (s *Server) SetupRoutes() { // Start 启动HTTP服务器 func (s *Server) Start() error { - addr := s.config.Server.Address() - fmt.Printf("Server starting on %s\n", addr) + addr := config.Scd.Server.Address() + fmt.Printf("服务开始启动,监听地址: %s\n", addr) return s.app.Listen(addr) } diff --git a/main.go b/main.go index df02306..995341e 100644 --- a/main.go +++ b/main.go @@ -1,69 +1,33 @@ package main import ( - "context" - "gitee.ltd/lxh/wechat-robot/internal/minio" + "gitee.ltd/lxh/wechat-robot/internal/initialize" + "gitee.ltd/lxh/wechat-robot/internal/server" "gitee.ltd/lxh/wechat-robot/internal/tasks" "github.com/gofiber/fiber/v2/log" "os" "os/signal" "syscall" - "time" - - "gitee.ltd/lxh/wechat-robot/internal/config" - "gitee.ltd/lxh/wechat-robot/internal/docker" - "gitee.ltd/lxh/wechat-robot/internal/model" - "gitee.ltd/lxh/wechat-robot/internal/server" ) func main() { - // 加载配置 - cfg, err := config.Load() - if err != nil { - log.Fatalf("无法加载配置: %v", err) - } - - if err = cfg.Validate(); err != nil { - log.Fatalf("配置无效: %v", err) - } - - // 初始化Minio - minio.Init() - - // 初始化数据库 - err = model.InitDB(&cfg.Database) - if err != nil { - log.Fatalf("初始化数据库失败: %v", err) - } - defer model.CloseDB() - - // 初始化Docker客户端 - err = docker.InitClient(&cfg.Docker) - if err != nil { - log.Errorf("初始化Docker客户端失败: %v", err) - } - defer docker.CloseClient() - - // 启动容器监控 - db := model.GetDB() - monitor := docker.NewContainerMonitor(db, time.Minute) - ctx, cancel := context.WithCancel(context.Background()) - monitor.Start(ctx) + // 初始化系统 + initialize.Init() // 创建HTTP服务器 - srv := server.New(cfg) + srv := server.New() // 设置路由 srv.SetupRoutes() // 启动HTTP服务器 go func() { - if err = srv.Start(); err != nil { + if err := srv.Start(); err != nil { log.Errorf("Server error: %v", err) } }() - log.Debug("Server started successfully") + log.Debug("服务器已成功启动") // 启动定时任务 tasks.Start() @@ -73,13 +37,11 @@ func main() { signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit - log.Warn("Shutting down...") - cancel() // 停止容器监控 - + log.Warn("正在关闭...") // 关闭HTTP服务器 - if err = srv.Shutdown(); err != nil { - log.Fatalf("Server shutdown failed: %v", err) + if err := srv.Shutdown(); err != nil { + log.Fatalf("服务器关闭失败: %v", err) } - log.Warn("Server exited properly") + log.Warn("服务器已正确退出") }