From 7e282b36d7b9a821576b28bc0fa08be57f557f0c Mon Sep 17 00:00:00 2001 From: Blizzard Date: Sat, 11 Oct 2025 15:13:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0oss=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/system/enter.go | 2 + api/v1/system/oss.go | 116 +++++++++++++++++++++++++++++++++++ config-dev.yaml | 1 + config/config.go | 4 ++ config/oss_minio.go | 11 ++++ config/oss_tencent.go | 10 +++ config/rocket_mq.go | 12 ++++ config/system.go | 1 + go.mod | 15 ++++- go.sum | 36 ++++++++++- initialize/gorm.go | 6 +- model/system/oss.go | 20 ++++++ model/system/request/oss.go | 8 +++ model/system/response/oss.go | 7 +++ router/system/enter.go | 2 + router/system/oss_router.go | 16 +++++ service/system/enter.go | 1 + service/system/oss.go | 88 ++++++++++++++++++++++++++ utils/upload/minio_oss.go | 103 +++++++++++++++++++++++++++++++ utils/upload/oss_instance.go | 31 ++++++++++ utils/upload/tencent_cos.go | 60 ++++++++++++++++++ 21 files changed, 545 insertions(+), 5 deletions(-) create mode 100644 api/v1/system/oss.go create mode 100644 config/oss_minio.go create mode 100644 config/oss_tencent.go create mode 100644 config/rocket_mq.go create mode 100644 model/system/oss.go create mode 100644 model/system/request/oss.go create mode 100644 model/system/response/oss.go create mode 100644 router/system/oss_router.go create mode 100644 service/system/oss.go create mode 100644 utils/upload/minio_oss.go create mode 100644 utils/upload/oss_instance.go create mode 100644 utils/upload/tencent_cos.go diff --git a/api/v1/system/enter.go b/api/v1/system/enter.go index b63ef81..fc52c4e 100644 --- a/api/v1/system/enter.go +++ b/api/v1/system/enter.go @@ -9,6 +9,7 @@ type ApiGroup struct { RoleApi MenuApi OperationRecordApi + OssApi } var ( @@ -18,4 +19,5 @@ var ( roleService = service.ServiceGroupApp.SystemServiceGroup.RoleService menuService = service.ServiceGroupApp.SystemServiceGroup.MenuService operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService + ossService = service.ServiceGroupApp.SystemServiceGroup.OssService ) diff --git a/api/v1/system/oss.go b/api/v1/system/oss.go new file mode 100644 index 0000000..f166602 --- /dev/null +++ b/api/v1/system/oss.go @@ -0,0 +1,116 @@ +package system + +import ( + "sundynix-go/global" + "sundynix-go/model/commom/request" + "sundynix-go/model/commom/response" + "sundynix-go/model/system" + sysReq "sundynix-go/model/system/request" + sysResp "sundynix-go/model/system/response" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type OssApi struct { +} + +// UploadFile +// @tags 文件相关 +// @Summary 文件上传 +// @Security ApiKeyAuth +// @accept multipart/form-data +// @Produce application/json +// @Param file formData file true "上传文件" +// @Success 200 {object} response.Response{msg=string} "上传文件" +// @router /api/oss/upload [post] +func (o *OssApi) UploadFile(c *gin.Context) { + var file system.Oss + _, header, err := c.Request.FormFile("file") + if err != nil { + global.Logger.Error("接收文件失败!", zap.Error(err)) + response.FailWithMsg("接收文件失败!", c) + return + } + file, err = ossService.Upload(header) //上传完成后拿到文件信息 + if err != nil { + global.Logger.Error("上传文件失败!", zap.Error(err)) + response.FailWithMsg("上传文件失败!", c) + return + } + response.OkWithData(sysResp.UploadFileResponse{File: file}, c) +} + +// DeleteFile +// @tags 文件相关 +// @Summary 删除文件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除文件" +// @Success 200 {object} response.Response{msg=string} "删除文件" +// @router /api/oss/delete [post] +func (o *OssApi) DeleteFile(c *gin.Context) { + var ids request.IdsReq + err := c.ShouldBindJSON(&ids) + if err != nil { + response.FailWithMsg(err.Error(), c) + return + } + err = ossService.DeleteFileByIds(ids) + if err != nil { + global.Logger.Error("删除文件失败!", zap.Error(err)) + response.FailWithMsg("删除文件失败!", c) + return + } + response.OkWithMsg("删除文件成功!", c) +} + +// GetFileList +// @tags 文件相关 +// @Summary 文件列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body sysReq.GetOssFileList true "文件列表" +// @Success 200 {object} response.Response{data=string} "文件列表" +// @router /api/oss/getFileList [post] +func (o *OssApi) GetFileList(c *gin.Context) { + var pageInfo sysReq.GetOssFileList + err := c.ShouldBindJSON(&pageInfo) + if err != nil { + response.FailWithMsg(err.Error(), c) + return + } + list, total, err := ossService.GetFileList(pageInfo) + if err != nil { + global.Logger.Error("获取文件列表失败!", zap.Error(err)) + response.FailWithMsg("获取文件列表失败!", c) + return + } + response.OkWithData(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Current, + PageSize: pageInfo.PageSize, + }, c) +} + +// Detail +// @tags 文件相关 +// @Summary 文件详情 +// @Security ApiKeyAuth +// @Produce application/json +// @Param id query string true "文件id" +// @Success 200 {object} response.Response{data=string} "文件详情" +// @router /api/oss/detail [get] +func (o *OssApi) Detail(c *gin.Context) { + id := c.Query("id") + file, err := ossService.GetById(id) + if err != nil { + global.Logger.Error("获取文件详情失败!", zap.Error(err)) + response.FailWithMsg(err.Error(), c) + return + } + response.OkWithData(file, c) +} diff --git a/config-dev.yaml b/config-dev.yaml index d22349d..c437f91 100644 --- a/config-dev.yaml +++ b/config-dev.yaml @@ -3,6 +3,7 @@ system: db-type: mysql router-prefix: "api" enable-captcha: 0 + oss-type: minio jwt: buffer-time: 1d diff --git a/config/config.go b/config/config.go index 1937d72..e0a9964 100644 --- a/config/config.go +++ b/config/config.go @@ -8,4 +8,8 @@ type Config struct { Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"` Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"` Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"` + + Minio Minio `mapstructure:"minio" json:"minio" yaml:"minio"` + RocketMQConfig RocketMQConfig `mapstructure:"rocket-mq" json:"rocket-mq" yaml:"rocket-mq"` + TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"` } diff --git a/config/oss_minio.go b/config/oss_minio.go new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/config/oss_minio.go @@ -0,0 +1,11 @@ +package config + +type Minio struct { + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + AccessKeyId string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id"` + AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret"` + BucketName string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"` + UseSSL bool `mapstructure:"use-ssl" json:"use-ssl" yaml:"use-ssl"` + BasePath string `mapstructure:"base-path" json:"base-path" yaml:"base-path"` + BucketUrl string `mapstructure:"bucket-url" json:"bucket-url" yaml:"bucket-url"` +} diff --git a/config/oss_tencent.go b/config/oss_tencent.go new file mode 100644 index 0000000..39a29d1 --- /dev/null +++ b/config/oss_tencent.go @@ -0,0 +1,10 @@ +package config + +type TencentCOS struct { + Bucket string `mapstructure:"bucket" json:"bucket" yaml:"bucket"` + Region string `mapstructure:"region" json:"region" yaml:"region"` + SecretID string `mapstructure:"secret-id" json:"secret-id" yaml:"secret-id"` + SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` + BaseURL string `mapstructure:"base-url" json:"base-url" yaml:"base-url"` + PathPrefix string `mapstructure:"path-prefix" json:"path-prefix" yaml:"path-prefix"` +} diff --git a/config/rocket_mq.go b/config/rocket_mq.go new file mode 100644 index 0000000..5841bbe --- /dev/null +++ b/config/rocket_mq.go @@ -0,0 +1,12 @@ +package config + +type RocketMQConfig struct { + NameSpace string `mapstructure:"name-space" json:"nameSpace" yaml:"name-space"` + Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` + ConsumerGroup string `mapstructure:"consumer-group" json:"consumerGroup" yaml:"consumer-group"` + AccessKey string `mapstructure:"access-key" json:"accessKey" yaml:"access-key"` + SecretKey string `mapstructure:"secret-key" json:"secretKey" yaml:"secret-key"` + Topic string `mapstructure:"topic" json:"topic" yaml:"topic"` + EnableSSL bool `mapstructure:"enable-ssl" json:"enableSSL" yaml:"enable-ssl"` + LogEnabled bool `mapstructure:"log-enabled" json:"logEnabled" yaml:"log-enabled"` +} diff --git a/config/system.go b/config/system.go index 6123e71..092626a 100644 --- a/config/system.go +++ b/config/system.go @@ -5,4 +5,5 @@ type System struct { DbType string `mapstructure:"db-type" json:"db-type" yaml:"db-type"` RouterPrefix string `mapstructure:"router-prefix" json:"router-prefix" yaml:"router-prefix"` EnableCaptcha int `mapstructure:"enable-captcha" json:"enable-captcha" yaml:"enable-captcha"` + OssType string `mapstructure:"oss-type" json:"oss-type" yaml:"oss-type"` } diff --git a/go.mod b/go.mod index 40a9005..ed5e088 100644 --- a/go.mod +++ b/go.mod @@ -8,14 +8,16 @@ require ( github.com/fsnotify/fsnotify v1.8.0 github.com/gin-gonic/gin v1.10.1 github.com/glebarez/sqlite v1.11.0 - github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/golang-jwt/jwt/v5 v5.2.3 github.com/google/uuid v1.6.0 + github.com/minio/minio-go/v7 v7.0.95 github.com/mojocn/base64Captcha v1.3.8 github.com/redis/go-redis/v9 v9.7.3 github.com/spf13/viper v1.20.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 + github.com/tencentyun/cos-go-sdk-v5 v0.7.70 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.42.0 golang.org/x/sync v0.17.0 @@ -31,12 +33,14 @@ require ( github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clbanning/mxj v1.8.4 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/jsonreference v0.21.1 // indirect github.com/go-openapi/spec v0.21.0 // indirect @@ -59,6 +63,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/google/go-querystring v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.4 // indirect @@ -67,21 +72,29 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/crc64nvme v1.0.2 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/go-httpheader v0.2.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tinylib/msgp v1.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 970f016..ec356d8 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -39,6 +41,8 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +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-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= @@ -84,16 +88,19 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= +github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -112,6 +119,9 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -124,6 +134,14 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8 github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= +github.com/minio/crc64nvme v1.0.2/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.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= +github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -131,10 +149,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mojocn/base64Captcha v1.3.8 h1:rrN9BhCwXKS8ht1e21kvR3iTaMgf4qPC9sRoV52bqEg= github.com/mojocn/base64Captcha v1.3.8/go.mod h1:QFZy927L8HVP3+VV5z2b1EAEiv1KxVJKZbAucVgLUy4= +github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= +github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 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= @@ -143,6 +165,9 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +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.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -173,6 +198,13 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentyun/cos-go-sdk-v5 v0.7.70 h1:gkBkSfrDvUg4ZIjwYAfjbNCCclen9LCRNHhBNz+yjEQ= +github.com/tencentyun/cos-go-sdk-v5 v0.7.70/go.mod h1:STbTNaNKq03u+gscPEGOahKzLcGSYOj6Dzc5zNay7Pg= +github.com/tencentyun/qcloud-cos-sts-sdk v0.0.0-20250515025012-e0eec8a5d123/go.mod h1:b18KQa4IxHbxeseW1GcZox53d7J0z39VNONTxvvlkXw= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= diff --git a/initialize/gorm.go b/initialize/gorm.go index d31529f..ceca5f6 100644 --- a/initialize/gorm.go +++ b/initialize/gorm.go @@ -1,11 +1,12 @@ package initialize import ( - "go.uber.org/zap" - "gorm.io/gorm" "os" "sundynix-go/global" "sundynix-go/model/system" + + "go.uber.org/zap" + "gorm.io/gorm" ) // Gorm 根据全局配置中的数据库类型返回对应的 *gorm.DB 实例。 @@ -35,6 +36,7 @@ func MigrateTable() { system.Role{}, system.Menu{}, system.SysOperationRecord{}, + system.Oss{}, ) if err != nil { global.Logger.Error("Migrate table failed,err:", zap.Error(err)) diff --git a/model/system/oss.go b/model/system/oss.go new file mode 100644 index 0000000..c0f7962 --- /dev/null +++ b/model/system/oss.go @@ -0,0 +1,20 @@ +package system + +import ( + "time" + + "gorm.io/gorm" +) + +type Oss struct { + //global.BaseModel + Id string `gorm:"size:50;primaryKey" json:"id"` // 主键ID + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间 + Name string `json:"name" form:"name" gorm:"column:name;comment:文件名"` + Url string `json:"url" form:"url" gorm:"column:url;comment:文件地址"` + Tag string `json:"tag" form:"tag" gorm:"column:tag;comment:文件标签"` + Key string `json:"key" form:"key" gorm:"column:key;comment:文件key"` + Suffix string `json:"suffix" form:"suffix" gorm:"column:suffix;comment:文件后缀"` +} diff --git a/model/system/request/oss.go b/model/system/request/oss.go new file mode 100644 index 0000000..d0d6573 --- /dev/null +++ b/model/system/request/oss.go @@ -0,0 +1,8 @@ +package request + +import common "sundynix-go/model/commom/request" + +type GetOssFileList struct { + common.PageInfo + Name string `json:"name" form:"name"` +} diff --git a/model/system/response/oss.go b/model/system/response/oss.go new file mode 100644 index 0000000..56b90e4 --- /dev/null +++ b/model/system/response/oss.go @@ -0,0 +1,7 @@ +package response + +import "sundynix-go/model/system" + +type UploadFileResponse struct { + File system.Oss `json:"file"` +} diff --git a/router/system/enter.go b/router/system/enter.go index 1a133ed..f4c0a68 100644 --- a/router/system/enter.go +++ b/router/system/enter.go @@ -9,6 +9,7 @@ type RouterGroup struct { RoleRouter MenuRouter OperationRecordRouter + OssRouter } // 初始化路由 @@ -19,4 +20,5 @@ var ( roleApi = v1.ApiGroupApp.SystemApiGroup.RoleApi menuApi = v1.ApiGroupApp.SystemApiGroup.MenuApi operationRecordApi = v1.ApiGroupApp.SystemApiGroup.OperationRecordApi + ossApi = v1.ApiGroupApp.SystemApiGroup.OssApi ) diff --git a/router/system/oss_router.go b/router/system/oss_router.go new file mode 100644 index 0000000..1408478 --- /dev/null +++ b/router/system/oss_router.go @@ -0,0 +1,16 @@ +package system + +import "github.com/gin-gonic/gin" + +type OssRouter struct { +} + +func (f *OssRouter) InitOssRouter(Router *gin.RouterGroup) { + ossRouter := Router.Group("oss") + { + ossRouter.POST("upload", ossApi.UploadFile) + ossRouter.POST("delete", ossApi.DeleteFile) + ossRouter.POST("getFileList", ossApi.GetFileList) + ossRouter.GET("getFile", ossApi.Detail) + } +} diff --git a/service/system/enter.go b/service/system/enter.go index aaa776c..f9f4f48 100644 --- a/service/system/enter.go +++ b/service/system/enter.go @@ -7,4 +7,5 @@ type ServiceGroup struct { RoleService MenuService OperationRecordService + OssService } diff --git a/service/system/oss.go b/service/system/oss.go new file mode 100644 index 0000000..f29e4b6 --- /dev/null +++ b/service/system/oss.go @@ -0,0 +1,88 @@ +package system + +import ( + "errors" + "mime/multipart" + "strings" + "sundynix-go/global" + common "sundynix-go/model/commom/request" + "sundynix-go/model/system" + sysReq "sundynix-go/model/system/request" + "sundynix-go/utils/uniqueid" + "sundynix-go/utils/upload" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type OssService struct { +} + +var OssServiceApp = new(OssService) + +func (o *OssService) Save(file system.Oss) error { + return global.DB.Create(&file).Error +} + +func (o *OssService) Upload(header *multipart.FileHeader) (file system.Oss, err error) { + instance := upload.OssInstance() + filepath, key, uploadErr := instance.UploadFile(header) + if uploadErr != nil { + return file, uploadErr + } + //文件后缀 + s := strings.Split(header.Filename, ".") + f := system.Oss{ + Id: uniqueid.GenerateId(), + Key: key, // uploads/2025-09-17/ + Name: header.Filename, + Suffix: s[len(s)-1], + Tag: s[len(s)-1], + Url: filepath, // http://127.0.0.1:9000/planting-fun/uploads/2025-09-17/211476f3837fc7acbaebf0f901c1bd68.png + } + return f, o.Save(f) + +} + +func (o *OssService) DeleteFileByIds(ids common.IdsReq) error { + //循环删除 + instance := upload.OssInstance() + for _, id := range ids.Ids { + file, err := o.GetById(id) + if err != nil { + return err + } + if err = instance.DeleteFile(file.Key); err != nil { + global.Logger.Error("删除文件失败!", zap.Error(err)) + return err + } + } + err := global.DB.Where("id IN (?)", ids.Ids).Delete(&system.Oss{}).Error + return err +} + +func (o *OssService) GetById(id string) (system.Oss, error) { + var file system.Oss + err := global.DB.Where("id = ?", id).First(&file).Error + //不存在的时候不要返回错误,而是返回nil + if errors.Is(err, gorm.ErrRecordNotFound) { + return file, nil + } + return file, err +} + +func (o *OssService) GetFileList(info sysReq.GetOssFileList) (list interface{}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Current - 1) + db := global.DB.Model(&system.Oss{}) + var files []system.Oss + if info.Name != "" { + db = db.Where("name LIKE ?", "%"+info.Name+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&files).Error + return files, total, err +} diff --git a/utils/upload/minio_oss.go b/utils/upload/minio_oss.go new file mode 100644 index 0000000..a08effa --- /dev/null +++ b/utils/upload/minio_oss.go @@ -0,0 +1,103 @@ +package upload + +import ( + "bytes" + "context" + "errors" + "io" + "mime/multipart" + "path/filepath" + "strings" + "sundynix-go/global" + "sundynix-go/utils" + "time" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "go.uber.org/zap" +) + +var MinioClient *Minio // 优化性能,但是不支持动态配置 +type Minio struct { + Client *minio.Client + bucket string +} + +func GetMinio(endpoint, accessKey, secretKey, bucketName string, useSSL bool) (*Minio, error) { + if MinioClient != nil { + return MinioClient, nil + } + // Initialize minio client object. + minioClient, err := minio.New(endpoint, &minio.Options{ + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Secure: useSSL, // Set to true if using https + }) + if err != nil { + return nil, err + } + + // 创建bucket + err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{}) + if err != nil { + // 判断是否已经存在 + exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName) + if errBucketExists == nil && exists { + global.Logger.Info("Bucket already exists") + } else { + return nil, err + } + } + MinioClient = &Minio{ + Client: minioClient, + bucket: bucketName, + } + return MinioClient, nil +} + +func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) { + f, openErr := file.Open() + // mutipart.File to os.File + if openErr != nil { + global.Logger.Error("function file.Open() Failed", zap.Any("err", openErr.Error())) + return "", "", errors.New("function file.Open() Failed, err:" + openErr.Error()) + } + buffer := bytes.Buffer{} + _, err := io.Copy(&buffer, f) + if err != nil { + global.Logger.Error("读取文件失败", zap.Any("err", err.Error())) + return "", "", errors.New("读取文件失败, err:" + err.Error()) + } + f.Close() // 创建文件 defer 关闭 + + //对文件名进行加密存储 + ext := filepath.Ext(file.Filename) + filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext + if global.Config.Minio.BasePath == "" { + filePathres = "uploads/" + time.Now().Format("2006-01-02") + "/" + filename // uploads/2025-09-17/xxxx.png + } else { + filePathres = global.Config.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + filename + } + // 设置超时10分钟 + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + + //大文件自动切换为分片上传 + info, err := m.Client.PutObject(ctx, global.Config.Minio.BucketName, filePathres, &buffer, file.Size, minio.PutObjectOptions{ + ContentType: "application/octet-stream", + }) + if err != nil { + global.Logger.Error("上传文件到minio失败", zap.Any("err", err.Error())) + return "", "", errors.New("上传文件到minio失败, err:" + err.Error()) + } + //http://127.0.0.1:9000/planting-fun/uploads/2025-09-17/211476f3837fc7acbaebf0f901c1bd68.png + return global.Config.Minio.BucketUrl + "/" + info.Key, filePathres, nil + +} + +func (m *Minio) DeleteFile(key string) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{}) + return err +} diff --git a/utils/upload/oss_instance.go b/utils/upload/oss_instance.go new file mode 100644 index 0000000..c3c2a26 --- /dev/null +++ b/utils/upload/oss_instance.go @@ -0,0 +1,31 @@ +package upload + +import ( + "fmt" + "mime/multipart" + "sundynix-go/global" +) + +// oss 对象存储接口 +type Oss interface { + UploadFile(file *multipart.FileHeader) (string, string, error) + DeleteFile(key string) error +} + +// OssInstance 实例化oos方法 +func OssInstance() Oss { + switch global.Config.System.OssType { + case "local": + fmt.Println("local") + case "tencent-cos": + return &TencentCOS{} + case "minio": + minioClient, err := GetMinio(global.Config.Minio.Endpoint, global.Config.Minio.AccessKeyId, global.Config.Minio.AccessKeySecret, global.Config.Minio.BucketName, global.Config.Minio.UseSSL) + if err != nil { + global.Logger.Warn("minio初始化失败,请检查minio可用性或安全配置:" + err.Error()) + panic("minio初始化失败,请检查minio可用性或安全配置") + } + return minioClient + } + return nil +} diff --git a/utils/upload/tencent_cos.go b/utils/upload/tencent_cos.go new file mode 100644 index 0000000..440f7b6 --- /dev/null +++ b/utils/upload/tencent_cos.go @@ -0,0 +1,60 @@ +package upload + +import ( + "context" + "errors" + "fmt" + "mime/multipart" + "net/http" + "net/url" + "sundynix-go/global" + "time" + + "github.com/tencentyun/cos-go-sdk-v5" + "go.uber.org/zap" +) + +type TencentCOS struct{} + +// NewClient 创建一个腾讯云COS客户端 +func NewClient() *cos.Client { + urlStr, _ := url.Parse("https://" + global.Config.TencentCOS.Bucket + ".cos." + global.Config.TencentCOS.Region + ".myqcloud.com") + baseURL := &cos.BaseURL{BucketURL: urlStr} + client := cos.NewClient(baseURL, &http.Client{ + Transport: &cos.AuthorizationTransport{ + SecretID: global.Config.TencentCOS.SecretID, + SecretKey: global.Config.TencentCOS.SecretKey, + }, + }) + return client +} + +// UploadFile upload file to COS +func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) { + client := NewClient() + f, openError := file.Open() + if openError != nil { + global.Logger.Error("function file.Open() failed", zap.Any("err", openError.Error())) + return "", "", errors.New("function file.Open() failed, err:" + openError.Error()) + } + defer f.Close() // 创建文件 defer 关闭 + fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) + + _, err := client.Object.Put(context.Background(), global.Config.TencentCOS.PathPrefix+"/"+fileKey, f, nil) + if err != nil { + panic(err) + } + return global.Config.TencentCOS.BaseURL + "/" + global.Config.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil +} + +// DeleteFile delete file form COS +func (*TencentCOS) DeleteFile(key string) error { + client := NewClient() + name := global.Config.TencentCOS.PathPrefix + "/" + key + _, err := client.Object.Delete(context.Background(), name) + if err != nil { + global.Logger.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error())) + return errors.New("function bucketManager.Delete() failed, err:" + err.Error()) + } + return nil +}