🆕 添加MinIO配置和初始化功能,新增下载图片和视频消息并保存到MinIO

This commit is contained in:
李寻欢 2025-04-23 10:32:09 +08:00
parent 65d245ddaf
commit cc629bd8b7
18 changed files with 462 additions and 72 deletions

View File

@ -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

11
go.mod
View File

@ -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

15
go.sum
View File

@ -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=

View File

@ -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
}

37
internal/config/minio.go Normal file
View File

@ -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
}

View File

@ -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" // 使用默认值
}

View File

@ -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)
// 继续删除流程,不因容器删除失败而中断
}

View File

@ -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
}
}

98
internal/minio/funcs.go Normal file
View File

@ -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...)
}

50
internal/minio/minio.go Normal file
View File

@ -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初始化成功")
}

View File

@ -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建议使用单连接
}

View File

@ -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

View File

@ -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)
}
// 保存入库

View File

@ -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
}
// 删除已启动的任务列表

View File

@ -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 //自定义的消息
)

View File

@ -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"`
}

View File

@ -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"`
}

16
main.go
View File

@ -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")
}