添加阿里云oss上传接口
This commit is contained in:
203
controllers/file/picUpload.go
Normal file
203
controllers/file/picUpload.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 阿里云OSS配置 - 请替换为你的实际配置
|
||||||
|
const (
|
||||||
|
ossEndpoint = "oss-cn-shanghai.aliyuncs.com" // OSS地域节点
|
||||||
|
ossAccessKeyID = "LTAI5tQDJnnajQS8uCXJLKfP" // 你的AccessKeyID
|
||||||
|
ossAccessKeySecret = "4wmJNcnYg5wKgBWePTQ9x4CPfUqgzn" // 你的AccessKeySecret
|
||||||
|
ossBucketName = "ttk-userdata" // 你的Bucket名称
|
||||||
|
ossBasePath = "/" // 图片存储基础路径
|
||||||
|
)
|
||||||
|
|
||||||
|
// 允许的图片文件扩展名
|
||||||
|
var allowedImageExts = map[string]bool{
|
||||||
|
".jpg": true,
|
||||||
|
".jpeg": true,
|
||||||
|
".png": true,
|
||||||
|
".gif": true,
|
||||||
|
".webp": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传响应结构
|
||||||
|
type UploadResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
URL string `json:"url"` // 图片访问URL
|
||||||
|
Filename string `json:"filename"` // OSS中的文件名
|
||||||
|
Size int64 `json:"size"` // 图片大小(字节)
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PicUpload 处理图片上传请求
|
||||||
|
func PicUpload(c *gin.Context) {
|
||||||
|
// 1. 获取上传的文件
|
||||||
|
file, err := c.FormFile("image")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, UploadResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: "获取上传文件失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 验证文件
|
||||||
|
if err := validateImageFile(file); err != nil {
|
||||||
|
c.JSON(400, UploadResponse{
|
||||||
|
Code: 400,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 打开文件
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, UploadResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "打开文件失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
// 4. 生成OSS中的文件名
|
||||||
|
objectName, err := generateObjectName(file.Filename)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, UploadResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "生成文件名失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 上传到OSS
|
||||||
|
ossURL, err := uploadToOSS(srcFile, objectName, file.Size)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, UploadResponse{
|
||||||
|
Code: 500,
|
||||||
|
Message: "上传到OSS失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 返回成功响应
|
||||||
|
c.JSON(200, UploadResponse{
|
||||||
|
Code: 200,
|
||||||
|
Message: "上传成功",
|
||||||
|
Data: struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}{
|
||||||
|
URL: ossURL,
|
||||||
|
Filename: objectName,
|
||||||
|
Size: file.Size,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证图片文件
|
||||||
|
func validateImageFile(file *multipart.FileHeader) error {
|
||||||
|
// 验证大小(10MB)
|
||||||
|
if file.Size > 10*1024*1024 {
|
||||||
|
return errors.New("文件大小不能超过10MB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证扩展名
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||||
|
if !allowedImageExts[ext] {
|
||||||
|
return errors.New("不支持的图片格式,仅支持jpg、jpeg、png、gif、webp")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一文件名
|
||||||
|
func generateObjectName(originalName string) (string, error) {
|
||||||
|
// 步骤1:提取原始文件的扩展名(如 .jpg .png)
|
||||||
|
ext := strings.ToLower(filepath.Ext(originalName))
|
||||||
|
if ext == "" {
|
||||||
|
return "", errors.New("文件没有扩展名,无法识别文件类型")
|
||||||
|
}
|
||||||
|
// 验证扩展名是否为允许的图片格式(避免非法扩展名)
|
||||||
|
if !allowedImageExts[ext] {
|
||||||
|
return "", errors.New("不支持的图片格式,仅支持jpg、jpeg、png、gif、webp")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤2:过滤原始文件名中的非法字符(仅保留 a-z A-Z 0-9 - _ .)
|
||||||
|
// 先移除扩展名,只处理文件名部分
|
||||||
|
rawFileName := strings.TrimSuffix(originalName, ext)
|
||||||
|
// 正则表达式:保留合法字符,其他替换为空
|
||||||
|
reg := regexp.MustCompile(`[^a-zA-Z0-9_\-.]`)
|
||||||
|
filteredFileName := reg.ReplaceAllString(rawFileName, "")
|
||||||
|
// 若过滤后文件名为空,用"unknown"代替(避免生成空文件名)
|
||||||
|
if filteredFileName == "" {
|
||||||
|
filteredFileName = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤3:生成唯一文件名(时间戳 + 过滤后的文件名 + 扩展名)
|
||||||
|
// 时间戳用秒级+毫秒级,确保唯一性(避免同一秒上传相同文件名)
|
||||||
|
timestamp := time.Now().Format("20060102150405.000") // 格式:年月日时分秒.毫秒
|
||||||
|
uniqueFileName := fmt.Sprintf("%s-%s%s", timestamp, filteredFileName, ext)
|
||||||
|
|
||||||
|
// 步骤4:拼接最终 ObjectName(确保路径规范:无首尾/、无连续/)
|
||||||
|
// 清理 ossBasePath(如 "images/" 转为 "images",避免结尾/)
|
||||||
|
cleanBasePath := strings.TrimSuffix(ossBasePath, "/")
|
||||||
|
// 拼接路径(如 "images" + "20240520143000.123-unknown.jpg" → "images/20240520143000.123-unknown.jpg")
|
||||||
|
objectName := fmt.Sprintf("%s/%s", cleanBasePath, uniqueFileName)
|
||||||
|
|
||||||
|
// 最终验证:确保 ObjectName 无首尾/、无连续/
|
||||||
|
objectName = strings.Trim(objectName, "/")
|
||||||
|
objectName = regexp.MustCompile(`/+`).ReplaceAllString(objectName, "/") // 多/替换为单/
|
||||||
|
|
||||||
|
return objectName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传到OSS
|
||||||
|
func uploadToOSS(reader io.Reader, objectName string, fileSize int64) (string, error) {
|
||||||
|
// 1. 创建OSS客户端(旧版SDK的创建方式)
|
||||||
|
client, err := oss.New(ossEndpoint, ossAccessKeyID, ossAccessKeySecret)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建OSS客户端失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 获取Bucket
|
||||||
|
bucket, err := client.Bucket(ossBucketName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取Bucket失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 上传文件(旧版SDK的PutObject方法)
|
||||||
|
err = bucket.PutObject(objectName, reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("上传文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 生成访问URL
|
||||||
|
// 公开Bucket可以直接使用此URL
|
||||||
|
url := fmt.Sprintf("https://%s.%s/%s", ossBucketName, ossEndpoint, objectName)
|
||||||
|
|
||||||
|
// 如果是私有Bucket,需要生成带签名的URL(有效期1小时)
|
||||||
|
// signedURL, err := bucket.SignURL(objectName, oss.HTTPGet, 3600)
|
||||||
|
// if err != nil {
|
||||||
|
// return "", fmt.Errorf("生成签名URL失败: %w", err)
|
||||||
|
// }
|
||||||
|
// return signedURL, nil
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
2
go.mod
2
go.mod
@@ -28,6 +28,7 @@ require (
|
|||||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect
|
||||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||||
github.com/bitly/go-simplejson v0.5.0 // indirect
|
github.com/bitly/go-simplejson v0.5.0 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
github.com/bits-and-blooms/bitset v1.24.0 // indirect
|
||||||
@@ -127,6 +128,7 @@ require (
|
|||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/text v0.29.0 // indirect
|
||||||
|
golang.org/x/time v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"toutoukan/controllers/article"
|
"toutoukan/controllers/article"
|
||||||
"toutoukan/controllers/article/getArticleNum"
|
"toutoukan/controllers/article/getArticleNum"
|
||||||
"toutoukan/controllers/comments/getcomments"
|
"toutoukan/controllers/comments/getcomments"
|
||||||
"toutoukan/controllers/comments/publishComments"
|
"toutoukan/controllers/comments/publishComments"
|
||||||
|
"toutoukan/controllers/file"
|
||||||
"toutoukan/controllers/goods"
|
"toutoukan/controllers/goods"
|
||||||
"toutoukan/controllers/kills"
|
"toutoukan/controllers/kills"
|
||||||
"toutoukan/controllers/notifications/getnotifications"
|
"toutoukan/controllers/notifications/getnotifications"
|
||||||
"toutoukan/controllers/search"
|
"toutoukan/controllers/search"
|
||||||
"toutoukan/controllers/system"
|
"toutoukan/controllers/system"
|
||||||
"toutoukan/controllers/user"
|
"toutoukan/controllers/user"
|
||||||
|
"toutoukan/controllers/user/setting"
|
||||||
"toutoukan/init/ratelimit"
|
"toutoukan/init/ratelimit"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
func SetupRouter() *gin.Engine {
|
||||||
@@ -24,6 +27,7 @@ func SetupRouter() *gin.Engine {
|
|||||||
apiGroup.POST("/kill", ratelimit.RateLimitMiddleware(), kills.Userkill)
|
apiGroup.POST("/kill", ratelimit.RateLimitMiddleware(), kills.Userkill)
|
||||||
apiGroup.POST("/getscore", user.GetScore)
|
apiGroup.POST("/getscore", user.GetScore)
|
||||||
apiGroup.POST("/getInfo", user.GetUserInfo)
|
apiGroup.POST("/getInfo", user.GetUserInfo)
|
||||||
|
apiGroup.POST("/change", setting.ChangeUserSetting)
|
||||||
|
|
||||||
}
|
}
|
||||||
//r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) {
|
//r.GET("/socket", jwt.JWTAuthMiddleware(), func(c *gin.Context) {
|
||||||
@@ -60,6 +64,10 @@ func SetupRouter() *gin.Engine {
|
|||||||
{
|
{
|
||||||
notificationGroup.POST("/get", getnotifications.GetNotifications)
|
notificationGroup.POST("/get", getnotifications.GetNotifications)
|
||||||
}
|
}
|
||||||
|
fileGroup := r.Group("/file")
|
||||||
|
{
|
||||||
|
fileGroup.POST("/upload", file.PicUpload)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user