From cc629bd8b727679d474cbf14c17d547527f5d85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=AF=BB=E6=AC=A2?= Date: Wed, 23 Apr 2025 10:32:09 +0800 Subject: [PATCH] =?UTF-8?q?:new:=20=E6=B7=BB=E5=8A=A0MinIO=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=92=8C=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E4=B8=8B=E8=BD=BD=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E5=92=8C=E8=A7=86=E9=A2=91=E6=B6=88=E6=81=AF=E5=B9=B6=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E5=88=B0MinIO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/config.yaml | 9 +++ go.mod | 11 +++- go.sum | 15 +++++ internal/config/config.go | 7 ++- internal/config/minio.go | 37 ++++++++++++ internal/docker/robot.go | 4 +- internal/handler/robot.go | 8 +-- internal/middleware/debug.go | 43 -------------- internal/minio/funcs.go | 98 ++++++++++++++++++++++++++++++++ internal/minio/minio.go | 50 ++++++++++++++++ internal/model/db.go | 10 ++-- internal/server/server.go | 4 +- internal/tasks/message.go | 46 +++++++++++++++ internal/tasks/tasks.go | 16 +++--- internal/types/appmessage.go | 25 ++++++++ internal/wechat/app_message.go | 79 +++++++++++++++++++++++++ internal/wechat/media_message.go | 56 ++++++++++++++++++ main.go | 16 ++++-- 18 files changed, 462 insertions(+), 72 deletions(-) create mode 100644 internal/config/minio.go delete mode 100644 internal/middleware/debug.go create mode 100644 internal/minio/funcs.go create mode 100644 internal/minio/minio.go create mode 100644 internal/types/appmessage.go create mode 100644 internal/wechat/app_message.go create mode 100644 internal/wechat/media_message.go diff --git a/configs/config.yaml b/configs/config.yaml index 9da7dbc..5a80cdb 100644 --- a/configs/config.yaml +++ b/configs/config.yaml @@ -53,3 +53,12 @@ auth: logger: level: "info" file: "./logs/app.log" + +# MinIO配置示例 +minio: + endpoint: http://10.0.0.5:9000 + host: http://10.0.0.5:9000 + accessKey: xxx + secretKey: xxx + bucket: wechat + useSSL: false diff --git a/go.mod b/go.mod index 18c90e6..717747e 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,12 @@ require ( github.com/goccy/go-json v0.10.5 github.com/gofiber/fiber/v2 v2.52.6 github.com/gofiber/template/html/v2 v2.1.3 + github.com/gofiber/websocket/v2 v2.2.1 github.com/google/uuid v1.6.0 github.com/logto-io/go/v2 v2.0.0 + github.com/minio/minio-go/v7 v7.0.91 github.com/spf13/viper v1.20.1 + github.com/valyala/fasthttp v1.60.0 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.11 gorm.io/driver/sqlite v1.5.7 @@ -27,9 +30,11 @@ require ( github.com/containerd/log v0.1.0 // 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 github.com/fasthttp/websocket v1.5.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -38,7 +43,6 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/utils v1.1.0 // indirect - github.com/gofiber/websocket/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -48,10 +52,13 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/minio/crc64nvme v1.0.1 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/atomicwriter v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect @@ -62,6 +69,7 @@ require ( github.com/pkg/errors v0.9.1 // 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 github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -70,7 +78,6 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect diff --git a/go.sum b/go.sum index 2d33260..1d316d8 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fasthttp/websocket v1.5.3 h1:TPpQuLwJYfd4LJPXvHDYPMFWbLjsT91n3GpWtCQtdek= github.com/fasthttp/websocket v1.5.3/go.mod h1:46gg/UBmTU1kUaTcwQXpUxtRwG2PvIZYeA8oL6vF3Fs= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -35,6 +37,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo= github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -93,6 +97,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -108,6 +115,12 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= +github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -135,6 +148,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= diff --git a/internal/config/config.go b/internal/config/config.go index c100abb..f14ebfc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "os" "strings" @@ -14,6 +15,7 @@ type Config struct { Docker DockerConfig `mapstructure:"docker"` Auth AuthConfig `mapstructure:"auth"` Logger LoggerConfig `mapstructure:"logger"` + Minio MinioConfig `mapstructure:"minio"` } // Load 加载配置文件和环境变量 @@ -36,7 +38,7 @@ func Load() (*Config, error) { // 读取配置文件 if err := viper.ReadInConfig(); err != nil { // 配置文件不存在时不返回错误 - if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return nil, err } } @@ -66,5 +68,8 @@ func (c *Config) Validate() error { if err := c.Logger.Validate(); err != nil { return err } + if err := c.Minio.Validate(); err != nil { + return err + } return nil } diff --git a/internal/config/minio.go b/internal/config/minio.go new file mode 100644 index 0000000..4780725 --- /dev/null +++ b/internal/config/minio.go @@ -0,0 +1,37 @@ +package config + +import "fmt" + +// Minio +// @description: Minio配置 +type MinioConfig struct { + Endpoint string `mapstructure:"endpoint"` // 接口地址 + Host string `mapstructure:"host"` // 自定义域名 + AccessKeyID string `mapstructure:"accessKey"` // 账号 + SecretAccessKey string `mapstructure:"secretKey"` // 密码 + BucketName string `mapstructure:"bucket"` // 桶名称 + UseSsl bool `mapstructure:"useSSL"` // 是否使用SSL +} + +// Validate +// @description: 验证参数 +// @receiver c +// @return error +func (c *MinioConfig) Validate() error { + if c.Endpoint == "" { + return fmt.Errorf("endpoint is required") + } + if c.Host == "" { + return fmt.Errorf("host is required") + } + if c.AccessKeyID == "" { + return fmt.Errorf("accessKeyId is required") + } + if c.SecretAccessKey == "" { + return fmt.Errorf("secretAccessKey is required") + } + if c.BucketName == "" { + c.BucketName = "wechat" + } + return nil +} diff --git a/internal/docker/robot.go b/internal/docker/robot.go index f4764c7..b174792 100644 --- a/internal/docker/robot.go +++ b/internal/docker/robot.go @@ -3,7 +3,7 @@ package docker import ( "context" "gitee.ltd/lxh/wechat-robot/internal/config" - "log" + "github.com/gofiber/fiber/v2/log" ) const ( @@ -43,7 +43,7 @@ func CreateRobotContainer(ctx context.Context, cfg *config.DockerConfig, robotNa // 获取容器访问地址 containerHost, err := GetContainerHost(ctx, containerID, cfg) if err != nil { - log.Printf("警告: 无法获取容器访问地址: %v", err) + log.Warnf("警告: 无法获取容器访问地址: %v", err) containerHost = "localhost:9000" // 使用默认值 } diff --git a/internal/handler/robot.go b/internal/handler/robot.go index ab6edf3..9432305 100644 --- a/internal/handler/robot.go +++ b/internal/handler/robot.go @@ -4,7 +4,7 @@ import ( "context" "errors" "gitee.ltd/lxh/xybot" - "log" + "github.com/gofiber/fiber/v2/log" "strconv" "strings" "time" @@ -173,19 +173,19 @@ func DeleteRobot(c *fiber.Ctx) error { robotCli, err := xybot.NewClient(robot.WechatID, robot.ContainerHost, false) if err != nil { - log.Printf("创建微信客户端失败: %v", err) + log.Errorf("创建微信客户端失败: %v", err) } if robot.Status == model.RobotStatusOnline { if err = robotCli.Login.Logout(); err != nil { - log.Printf("登出机器人失败: %v", err) + log.Errorf("登出机器人失败: %v", err) // 继续删除流程,不因登出失败而中断 } } // 删除容器 if err = docker.RemoveContainer(ctx, robot.ContainerID, true); err != nil { - log.Printf("删除容器失败: %v", err) + log.Errorf("删除容器失败: %v", err) // 继续删除流程,不因容器删除失败而中断 } diff --git a/internal/middleware/debug.go b/internal/middleware/debug.go deleted file mode 100644 index 2bfe7e1..0000000 --- a/internal/middleware/debug.go +++ /dev/null @@ -1,43 +0,0 @@ -package middleware - -import ( - "log" - "time" - - "github.com/gofiber/fiber/v2" -) - -// DebugMiddleware 用于记录更详细的请求信息以便排查问题 -func DebugMiddleware() fiber.Handler { - return func(c *fiber.Ctx) error { - // 记录开始时间 - start := time.Now() - - // 输出请求详情 - log.Printf("[DEBUG] 收到请求: %s %s", c.Method(), c.Path()) - log.Printf("[DEBUG] 查询参数: %s", c.Request().URI().QueryArgs().String()) - log.Printf("[DEBUG] 请求头: %v", c.GetReqHeaders()) - - // 处理请求 - err := c.Next() - - // 请求结束,计算耗时 - duration := time.Since(start) - status := c.Response().StatusCode() - - if err != nil { - log.Printf("[DEBUG] 请求错误: %s %s - %d - %v - %s", - c.Method(), c.Path(), status, err, duration) - } else { - log.Printf("[DEBUG] 请求完成: %s %s - %d - %s", - c.Method(), c.Path(), status, duration) - } - - if status == 500 { - log.Printf("[ERROR] 内部服务器错误: %s %s - %v", - c.Method(), c.Path(), err) - } - - return err - } -} diff --git a/internal/minio/funcs.go b/internal/minio/funcs.go new file mode 100644 index 0000000..fd22f1f --- /dev/null +++ b/internal/minio/funcs.go @@ -0,0 +1,98 @@ +package minio + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" + "gitee.ltd/lxh/wechat-robot/internal/config" + "github.com/gofiber/fiber/v2/log" + "github.com/minio/minio-go/v7" + "net/http" + "strings" + "time" +) + +// SaveBytes +// @description: 保存文件到OSS +// @param b +// @param md5 +// @param suffix +// @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, ";") { + contentType = contentType[:strings.Index(contentType, ";")] + } + // 重新设置文件名 + suf := "" + if len(suffix) == 0 { + suf = contentType[strings.Index(contentType, "/")+1:] + } else { + suf = suffix[0] + } + today := time.Now().Local().Format("2006/01/02") + fileName := fmt.Sprintf("%s/%s/%s", contentType, today, md5) + if suf != "" { + fileName = fmt.Sprintf("%s.%s", fileName, suf) + } + log.Debugf("开始上传文件: %v", fileName) + reader := bytes.NewBuffer(b) + _, err = minioClient.PutObject(ctx, cfg.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 { + protocol = "https" + } + url = fmt.Sprintf("%s://%s/%s/%s", protocol, cfg.Minio.Host, cfg.Minio.BucketName, fileName) + // 异步数据入库 + //go func() { + // wf := db.WeChatFileEntity{Url: fileUrl, HashCode: md5, FileType: contentType} + // if err = wf.Save(); err != nil { + // log.Errorf("文件资源信息保存入库失败: %v", err) + // } + //}() + return +} + +// SaveBase64 +// @description: 保存Base64字符串到OSS +// @param bs53Str +// @param md5Str +// @param suffix +// @return url +// @return err +func SaveBase64(bs64Str string, md5Str string, suffix ...string) (url string, err error) { + bs64Bytes, err := base64.StdEncoding.DecodeString(bs64Str) // 解密base64字符串 + if err != nil { + log.Errorf("解密失败: %v", err) + return + } + + // 如果md5为空,计算一下 + if md5Str == "" { + hasher := md5.New() + if _, err = hasher.Write(bs64Bytes); err != nil { + log.Errorf("计算md5失败: %v", err) + return + } + md5Str = hex.EncodeToString(hasher.Sum(nil)) + //log.Debugf("计算出的md5: %v", md5Str) + } + + return SaveBytes(bs64Bytes, md5Str, suffix...) +} diff --git a/internal/minio/minio.go b/internal/minio/minio.go new file mode 100644 index 0000000..df53c01 --- /dev/null +++ b/internal/minio/minio.go @@ -0,0 +1,50 @@ +package minio + +import ( + "context" + "gitee.ltd/lxh/wechat-robot/internal/config" + "github.com/gofiber/fiber/v2/log" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +var minioClient *minio.Client + +// Init +// @description: 初始化Minio连接 +func Init() { + cfg, err := config.Load() + if err != nil { + log.Panicf("加载配置失败: %s", err.Error()) + return + } + + 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, + }) + if err != nil { + log.Panicf("OSS初始化失败: %v", err.Error()) + } + log.Debug("OSS连接成功,开始判断桶是否存在") + // 判断捅是否存在,不存在就创建 + var exists bool + exists, err = minioClient.BucketExists(ctx, cfg.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"}) + if err != nil { + log.Panicf("OSS桶创建失败: %v", err.Error()) + } + log.Debug("桶创建成功") + } else { + log.Debug("桶已存在") + } + log.Info("OSS初始化成功") +} diff --git a/internal/model/db.go b/internal/model/db.go index 9c0d39f..0686620 100644 --- a/internal/model/db.go +++ b/internal/model/db.go @@ -1,8 +1,9 @@ package model import ( + "database/sql" "fmt" - "log" + "github.com/gofiber/fiber/v2/log" "sync" "gorm.io/driver/mysql" @@ -57,14 +58,15 @@ func InitDB(cfg *config.DatabaseConfig) error { // 对于SQLite,执行一些特定的优化 if cfg.Type == config.SQLite { - sqlDB, err := db.DB() + var sqlDB *sql.DB + sqlDB, err = db.DB() if err != nil { - log.Printf("Warning: Could not get underlying SQL DB: %v", err) + log.Errorf("Warning: Could not get underlying SQL DB: %v", err) return } // 启用外键约束 - sqlDB.Exec("PRAGMA foreign_keys = ON") + _, _ = sqlDB.Exec("PRAGMA foreign_keys = ON") // 设置连接池大小 sqlDB.SetMaxOpenConns(1) // SQLite建议使用单连接 } diff --git a/internal/server/server.go b/internal/server/server.go index c02c5ec..7c5a125 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,7 +3,7 @@ package server import ( "errors" "fmt" - "log" + "github.com/gofiber/fiber/v2/log" "os" "strings" @@ -69,7 +69,7 @@ func New(cfg *config.Config) *Server { JSONDecoder: json.Unmarshal, ErrorHandler: func(c *fiber.Ctx, err error) error { // 详细错误日志 - log.Printf("错误: %+v\n请求路径: %s\n", err, c.Path()) + log.Errorf("错误: %+v\n请求路径: %s\n", err, c.Path()) code := fiber.StatusInternalServerError var e *fiber.Error diff --git a/internal/tasks/message.go b/internal/tasks/message.go index e093119..f20b5ba 100644 --- a/internal/tasks/message.go +++ b/internal/tasks/message.go @@ -1,9 +1,13 @@ package tasks import ( + "encoding/xml" + "gitee.ltd/lxh/wechat-robot/internal/minio" "gitee.ltd/lxh/wechat-robot/internal/model" "gitee.ltd/lxh/wechat-robot/internal/types" + "gitee.ltd/lxh/wechat-robot/internal/wechat" "gitee.ltd/lxh/xybot" + "github.com/gofiber/fiber/v2/log" "strings" "time" ) @@ -66,6 +70,48 @@ func syncMessage(client *xybot.Client, robotId uint) { m.Content = strings.Join(strings.Split(m.Content, "\n")[1:], "\n") } + // 处理一下,如果是图片、文件、表情包等信息,直接下载下来存到OSS + if m.Type == types.MsgTypeImage || m.Type == types.MsgTypeVideo { + // 解析消息xml文件 + // 解析为结构体 + var media wechat.MediaMessage + if err = xml.Unmarshal([]byte(m.Content), &media); err != nil { + log.Errorf("%v解析消息失败: %v", m.Type, err.Error()) + continue + } + + var md5Str, bs64Str string + switch m.Type { + case types.MsgTypeImage: + fileUrl := "" + md5Str = media.Img.Md5 + if media.Img.CdnBigImgUrl != "" { + fileUrl = media.Img.CdnBigImgUrl + } else if media.Img.CdnMidImgUrl != "" { + fileUrl = media.Img.CdnMidImgUrl + } else if media.Img.CdnThumbUrl != "" { + fileUrl = media.Img.CdnThumbUrl + } else { + continue + } + if bs64Str, err = client.Tool.CdnDownloadImg(media.Img.AesKey, fileUrl); err != nil { + log.Errorf("图片文件下载失败: %s", err.Error()) + continue + } + case types.MsgTypeVideo: + if bs64Str, err = client.Tool.DownloadVideo(m.ClientMsgId); err != nil { + log.Errorf("视频文件下载失败: %s", err.Error()) + continue + } + } + + // 下载完成,保存到OSS + if m.FileUrl, err = minio.SaveBase64(bs64Str, md5Str); err != nil { + log.Errorf("文件保存到Minio失败: %s", err.Error()) + continue + } + } + msg = append(msg, m) } // 保存入库 diff --git a/internal/tasks/tasks.go b/internal/tasks/tasks.go index 326d4a5..a069cf4 100644 --- a/internal/tasks/tasks.go +++ b/internal/tasks/tasks.go @@ -4,8 +4,8 @@ import ( "gitee.ltd/lxh/wechat-robot/internal/model" "gitee.ltd/lxh/xybot" "github.com/go-co-op/gocron/v2" + "github.com/gofiber/fiber/v2/log" "github.com/google/uuid" - "log" "sync" ) @@ -39,13 +39,13 @@ func Start() { // 启动定时任务 scheduler.Start() - log.Println("定时任务已启动") + log.Info("定时任务已启动") } // AddJob // @description: 添加任务 func AddJob(robot model.Robot) { - log.Printf("开始添加【%s[%s]】的定时任务", robot.Nickname, robot.WechatID) + log.Debugf("开始添加【%s[%s]】的定时任务", robot.Nickname, robot.WechatID) // 初始化微信客户端 robotCli, err := xybot.NewClient(robot.WechatID, robot.ContainerHost, false) if err != nil { @@ -57,7 +57,7 @@ func AddJob(robot model.Robot) { gocron.NewTask(syncMessage, robotCli, robot.ID), ) if err != nil { - log.Printf("添加定时任务失败: %v", err) + log.Errorf("添加定时任务失败: %v", err) return } // 添加到已启动的任务列表 @@ -82,11 +82,11 @@ func DeleteJob(robotId uint) { // 先取出任务Id jobId, ok := enabledSyncMessageMap.Load(robotId) if !ok { - log.Printf("定时任务不存在,robotId: %d", robotId) + log.Errorf("定时任务不存在,robotId: %d", robotId) return } if err := scheduler.RemoveJob(jobId.(uuid.UUID)); err != nil { - log.Printf("删除定时任务失败: %v", err) + log.Errorf("删除定时任务失败: %v", err) return } // 删除已启动的任务列表 @@ -95,11 +95,11 @@ func DeleteJob(robotId uint) { // 删除联系人同步任务 jobId, ok = enabledSyncContactMap.Load(robotId) if !ok { - log.Printf("联系人同步任务不存在,robotId: %d", robotId) + log.Errorf("联系人同步任务不存在,robotId: %d", robotId) return } if err := scheduler.RemoveJob(jobId.(uuid.UUID)); err != nil { - log.Printf("删除联系人同步任务失败: %v", err) + log.Errorf("删除联系人同步任务失败: %v", err) return } // 删除已启动的任务列表 diff --git a/internal/types/appmessage.go b/internal/types/appmessage.go new file mode 100644 index 0000000..055ee00 --- /dev/null +++ b/internal/types/appmessage.go @@ -0,0 +1,25 @@ +package types + +// AppMessageType 以Go惯用形式定义了微信所有的官方App消息类型。 +type AppMessageType int + +const ( + AppMsgTypeText AppMessageType = 1 // 文本消息 + AppMsgTypeImg AppMessageType = 2 // 图片消息 + AppMsgTypeAudio AppMessageType = 3 // 语音消息 + AppMsgTypeVideo AppMessageType = 4 // 视频消息 + AppMsgTypeUrl AppMessageType = 5 // 文章消息 + AppMsgTypeAttach AppMessageType = 6 // 附件消息 + AppMsgTypeOpen AppMessageType = 7 // Open + AppMsgTypeEmoji AppMessageType = 8 // 表情消息 + AppMsgTypeVoiceRemind AppMessageType = 9 // VoiceRemind + AppMsgTypeScanGood AppMessageType = 10 // ScanGood + AppMsgTypeGood AppMessageType = 13 // Good + AppMsgTypeEmotion AppMessageType = 15 // Emotion + AppMsgTypeCardTicket AppMessageType = 16 // 名片消息 + AppMsgTypeRealtimeShareLocation AppMessageType = 17 // 地理位置消息 + AppMsgTypeReferences AppMessageType = 57 // 引用回复 + AppMsgTypeTransfers AppMessageType = 2000 // 转账消息 + AppMsgTypeRedEnvelopes AppMessageType = 2001 // 红包消息 + AppMsgTypeReaderType AppMessageType = 100001 //自定义的消息 +) diff --git a/internal/wechat/app_message.go b/internal/wechat/app_message.go new file mode 100644 index 0000000..e91e3f4 --- /dev/null +++ b/internal/wechat/app_message.go @@ -0,0 +1,79 @@ +package wechat + +import ( + "encoding/xml" + "gitee.ltd/lxh/wechat-robot/internal/types" +) + +// AppMessage APP消息 +type AppMessage struct { + XMLName xml.Name `xml:"msg"` + Text string `xml:",chardata"` + AppMsg struct { + Text string `xml:",chardata"` + Appid string `xml:"appid,attr"` + SdkVer string `xml:"sdkver,attr"` + Title string `xml:"title"` + Des string `xml:"des"` + Action string `xml:"action"` + Type types.AppMessageType `xml:"type"` + ShowType string `xml:"showtype"` + SoundType string `xml:"soundtype"` + MediaTagName string `xml:"mediatagname"` + MessageExt string `xml:"messageext"` + MessageAction string `xml:"messageaction"` + Content string `xml:"content"` + ContentAttr string `xml:"contentattr"` + URL string `xml:"url"` + LowUrl string `xml:"lowurl"` + DataUrl string `xml:"dataurl"` + LowDataUrl string `xml:"lowdataurl"` + SongAlbumUrl string `xml:"songalbumurl"` + SongLyric string `xml:"songlyric"` + AppAttach struct { + Text string `xml:",chardata"` + TotalLen string `xml:"totallen"` + AttachId string `xml:"attachid"` + EmoticonMd5 string `xml:"emoticonmd5"` + FileExt string `xml:"fileext"` + CdnThumbAeskey string `xml:"cdnthumbaeskey"` + CdnAttachUrl string `xml:"cdnattachurl"` + FileKey string `xml:"filekey"` + AesKey string `xml:"aeskey"` + } `xml:"appattach"` + ExtInfo string `xml:"extinfo"` + SourceUsername string `xml:"sourceusername"` + SourceDisplayName string `xml:"sourcedisplayname"` + ThumbUrl string `xml:"thumburl"` + Md5 string `xml:"md5"` + StaTextStr string `xml:"statextstr"` + DirectShare string `xml:"directshare"` + ReferMsg struct { + Text string `xml:",chardata"` + Type string `xml:"type"` + SvrId int64 `xml:"svrid"` + FromUsr string `xml:"fromusr"` + ChatUsr string `xml:"chatusr"` + DisplayName string `xml:"displayname"` + Content string `xml:"content"` + MsgSource struct { + Text string `xml:",chardata"` + MsgSource struct { + Text string `xml:",chardata"` + SequenceID string `xml:"sequence_id"` + Silence string `xml:"silence"` + MemberCount string `xml:"membercount"` + Signature string `xml:"signature"` + } `xml:"msgsource"` + } `xml:"msgsource"` + } `xml:"refermsg"` + } `xml:"appmsg"` + FromUsername string `xml:"fromusername"` + Scene string `xml:"scene"` + AppInfo struct { + Text string `xml:",chardata"` + Version string `xml:"version"` + AppName string `xml:"appname"` + } `xml:"appinfo"` + CommentUrl string `xml:"commenturl"` +} diff --git a/internal/wechat/media_message.go b/internal/wechat/media_message.go new file mode 100644 index 0000000..2fa7801 --- /dev/null +++ b/internal/wechat/media_message.go @@ -0,0 +1,56 @@ +package wechat + +import "encoding/xml" + +// MediaMessage 多媒体消息(视频、图片) +type MediaMessage struct { + XMLName xml.Name `xml:"msg" json:"msg"` + Text string `xml:",chardata" json:"-"` + VideoMsg *VideoMessage `xml:"videomsg" json:"videoMsg,omitempty"` + Img *ImgMessage `xml:"img" json:"img,omitempty"` +} + +// ImgMessage 图片消息 +type ImgMessage struct { + Text string `xml:",chardata"` + AesKey string `xml:"aeskey,attr"` + EnCryVer string `xml:"encryver,attr"` + CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"` + CdnThumbUrl string `xml:"cdnthumburl,attr"` + CdnThumbLength string `xml:"cdnthumblength,attr"` + CdnThumbHeight string `xml:"cdnthumbheight,attr"` + CdnThumbWidth string `xml:"cdnthumbwidth,attr"` + CdnMidHeight string `xml:"cdnmidheight,attr"` + CdnMidWidth string `xml:"cdnmidwidth,attr"` + CdnMidImgUrl string `xml:"cdnmidimgurl,attr"` + CdnHdHeight string `xml:"cdnhdheight,attr"` + CdnHdWidth string `xml:"cdnhdwidth,attr"` + CdnBigImgUrl string `xml:"cdnbigimgurl,attr"` + Length string `xml:"length,attr"` + Md5 string `xml:"md5,attr"` + HevcMidSize string `xml:"hevc_mid_size,attr"` +} + +// VideoMessage 视频消息 +type VideoMessage struct { + Text string `xml:",chardata"` + AesKey string `xml:"aeskey,attr"` + CdnVideoUrl string `xml:"cdnvideourl,attr"` + CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"` + CdnThumbUrl string `xml:"cdnthumburl,attr"` + Length string `xml:"length,attr"` + PlayLength string `xml:"playlength,attr"` + CdnThumbLength string `xml:"cdnthumblength,attr"` + CdnThumbWidth string `xml:"cdnthumbwidth,attr"` + CdnThumbHeight string `xml:"cdnthumbheight,attr"` + FromUsername string `xml:"fromusername,attr"` + Md5 string `xml:"md5,attr"` + NewMd5 string `xml:"newmd5,attr"` + IsPlaceholder string `xml:"isplaceholder,attr"` + RawMd5 string `xml:"rawmd5,attr"` + RawLength string `xml:"rawlength,attr"` + CdnRawVideoUrl string `xml:"cdnrawvideourl,attr"` + CdnRawVideoAesKey string `xml:"cdnrawvideoaeskey,attr"` + OverWriteNewMsgId string `xml:"overwritenewmsgid,attr"` + IsAd string `xml:"isad,attr"` +} diff --git a/main.go b/main.go index 7711506..df02306 100644 --- a/main.go +++ b/main.go @@ -2,8 +2,9 @@ package main import ( "context" + "gitee.ltd/lxh/wechat-robot/internal/minio" "gitee.ltd/lxh/wechat-robot/internal/tasks" - "log" + "github.com/gofiber/fiber/v2/log" "os" "os/signal" "syscall" @@ -26,6 +27,9 @@ func main() { log.Fatalf("配置无效: %v", err) } + // 初始化Minio + minio.Init() + // 初始化数据库 err = model.InitDB(&cfg.Database) if err != nil { @@ -36,7 +40,7 @@ func main() { // 初始化Docker客户端 err = docker.InitClient(&cfg.Docker) if err != nil { - log.Printf("初始化Docker客户端失败: %v", err) + log.Errorf("初始化Docker客户端失败: %v", err) } defer docker.CloseClient() @@ -55,11 +59,11 @@ func main() { // 启动HTTP服务器 go func() { if err = srv.Start(); err != nil { - log.Printf("Server error: %v", err) + log.Errorf("Server error: %v", err) } }() - log.Println("Server started successfully") + log.Debug("Server started successfully") // 启动定时任务 tasks.Start() @@ -69,7 +73,7 @@ func main() { signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit - log.Println("Shutting down...") + log.Warn("Shutting down...") cancel() // 停止容器监控 // 关闭HTTP服务器 @@ -77,5 +81,5 @@ func main() { log.Fatalf("Server shutdown failed: %v", err) } - log.Println("Server exited properly") + log.Warn("Server exited properly") }