feat: rbac完善,file接入完成

This commit is contained in:
Blizzard
2026-05-01 12:56:08 +08:00
parent bbd3f834b9
commit a93477ea8e
81 changed files with 5470 additions and 371 deletions
@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/common/response"
)
// 创建存储配置
func CreateStorageConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateStorageConfigReq
if err := httpx.Parse(r, &req); err != nil {
response.Fail(w, err.Error())
return
}
l := file.NewCreateStorageConfigLogic(r.Context(), svcCtx)
err := l.CreateStorageConfig(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.OkWithData(w, nil)
}
}
}
@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/common/response"
)
// 删除存储配置
func DeleteStorageConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdsReq
if err := httpx.Parse(r, &req); err != nil {
response.Fail(w, err.Error())
return
}
l := file.NewDeleteStorageConfigLogic(r.Context(), svcCtx)
err := l.DeleteStorageConfig(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.OkWithData(w, nil)
}
}
}
@@ -0,0 +1,30 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
)
// 下载文件
func DownloadFileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.FileIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := file.NewDownloadFileLogic(r.Context(), svcCtx)
err := l.DownloadFile(w, &req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
}
}
}
@@ -23,11 +23,11 @@ func GetFileListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
l := file.NewGetFileListLogic(r.Context(), svcCtx)
err := l.GetFileList(&req)
resp, err := l.GetFileList(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.Ok(w)
response.OkWithData(w, resp)
}
}
}
@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/common/response"
)
// 获取存储配置列表
func GetStorageConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.StorageConfigListReq
if err := httpx.Parse(r, &req); err != nil {
response.Fail(w, err.Error())
return
}
l := file.NewGetStorageConfigListLogic(r.Context(), svcCtx)
resp, err := l.GetStorageConfigList(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.OkWithData(w, resp)
}
}
}
@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/common/response"
)
// 设置默认存储配置
func SetDefaultStorageConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SetDefaultStorageConfigReq
if err := httpx.Parse(r, &req); err != nil {
response.Fail(w, err.Error())
return
}
l := file.NewSetDefaultStorageConfigLogic(r.Context(), svcCtx)
err := l.SetDefaultStorageConfig(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.OkWithData(w, nil)
}
}
}
@@ -0,0 +1,33 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"sundynix-micro-go/app/file/api/internal/logic/file"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/common/response"
)
// 更新存储配置
func UpdateStorageConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateStorageConfigReq
if err := httpx.Parse(r, &req); err != nil {
response.Fail(w, err.Error())
return
}
l := file.NewUpdateStorageConfigLogic(r.Context(), svcCtx)
err := l.UpdateStorageConfig(&req)
if err != nil {
response.Fail(w, err.Error())
} else {
response.OkWithData(w, nil)
}
}
}
@@ -15,7 +15,7 @@ import (
func UploadFileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := file.NewUploadFileLogic(r.Context(), svcCtx)
resp, err := l.UploadFile()
resp, err := l.UploadFile(r)
if err != nil {
response.Fail(w, err.Error())
} else {
+37 -1
View File
@@ -21,12 +21,48 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
Path: "/:id",
Handler: file.GetFileByIdHandler(serverCtx),
},
{
// 创建存储配置
Method: http.MethodPost,
Path: "/config/create",
Handler: file.CreateStorageConfigHandler(serverCtx),
},
{
// 删除存储配置
Method: http.MethodPost,
Path: "/config/delete",
Handler: file.DeleteStorageConfigHandler(serverCtx),
},
{
// 获取存储配置列表
Method: http.MethodPost,
Path: "/config/list",
Handler: file.GetStorageConfigListHandler(serverCtx),
},
{
// 设置默认存储配置
Method: http.MethodPost,
Path: "/config/setDefault",
Handler: file.SetDefaultStorageConfigHandler(serverCtx),
},
{
// 更新存储配置
Method: http.MethodPost,
Path: "/config/update",
Handler: file.UpdateStorageConfigHandler(serverCtx),
},
{
// 删除文件
Method: http.MethodDelete,
Method: http.MethodPost,
Path: "/delete",
Handler: file.DeleteFileHandler(serverCtx),
},
{
// 下载文件
Method: http.MethodGet,
Path: "/download/:id",
Handler: file.DownloadFileHandler(serverCtx),
},
{
// 文件列表
Method: http.MethodPost,
@@ -0,0 +1,48 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type CreateStorageConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 创建存储配置
func NewCreateStorageConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateStorageConfigLogic {
return &CreateStorageConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateStorageConfigLogic) CreateStorageConfig(req *types.CreateStorageConfigReq) error {
_, err := l.svcCtx.FileRpc.CreateStorageConfig(l.ctx, &fileservice.CreateStorageConfigReq{
Type: req.Type,
Name: req.Name,
Endpoint: req.Endpoint,
AccessKeyId: req.AccessKeyId,
AccessKeySecret: req.AccessKeySecret,
BucketName: req.BucketName,
BucketUrl: req.BucketUrl,
Region: req.Region,
Status: int32(req.Status),
Remark: req.Remark,
})
if err != nil {
return err
}
return nil
}
@@ -6,10 +6,11 @@ package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/oss_core"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type DeleteFileLogic struct {
@@ -28,7 +29,33 @@ func NewDeleteFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete
}
func (l *DeleteFileLogic) DeleteFile(req *types.IdsReq) error {
// todo: add your logic here and delete this line
if len(req.Ids) == 0 {
return nil
}
return nil
// 1. 查询文件记录获取OSS Key
resp, err := l.svcCtx.FileRpc.GetFilesByIds(l.ctx, &fileservice.GetFilesByIdsReq{
Ids: req.Ids,
})
if err != nil {
return err
}
// 2. 从OSS物理删除
factory := oss_core.NewOSSFactory(l.svcCtx.FileRpc)
uploader, err := factory.GetActiveUploader(l.ctx)
if err == nil && uploader != nil {
for _, file := range resp.Files {
if file.Key != "" {
_ = uploader.DeleteFile(l.ctx, file.Key)
}
}
}
// 3. 从数据库删除
_, err = l.svcCtx.FileRpc.DeleteFiles(l.ctx, &fileservice.DeleteFilesReq{
Ids: req.Ids,
})
return err
}
@@ -0,0 +1,39 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type DeleteStorageConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 删除存储配置
func NewDeleteStorageConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteStorageConfigLogic {
return &DeleteStorageConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteStorageConfigLogic) DeleteStorageConfig(req *types.IdsReq) error {
_, err := l.svcCtx.FileRpc.DeleteStorageConfig(l.ctx, &fileservice.DeleteFilesReq{
Ids: req.Ids,
})
if err != nil {
return err
}
return nil
}
@@ -0,0 +1,62 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"io"
"net/http"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/oss_core"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type DownloadFileLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 下载文件
func NewDownloadFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DownloadFileLogic {
return &DownloadFileLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DownloadFileLogic) DownloadFile(w http.ResponseWriter, req *types.FileIdReq) error {
// 1. 获取文件记录
resp, err := l.svcCtx.FileRpc.GetFileById(l.ctx, &fileservice.GetFileByIdReq{
Id: req.Id,
})
if err != nil {
return err
}
// 2. 获取具体底层client
factory := oss_core.NewOSSFactory(l.svcCtx.FileRpc)
uploader, err := factory.GetActiveUploader(l.ctx)
if err != nil {
return err
}
// 3. 读取流
reader, err := uploader.DownloadFile(l.ctx, resp.File.Key)
if err != nil {
return err
}
defer reader.Close()
// 4. 返回文件流
w.Header().Set("Content-Disposition", "attachment; filename=\""+resp.File.Name+"\"")
w.Header().Set("Content-Type", "application/octet-stream")
_, err = io.Copy(w, reader)
return err
}
@@ -6,10 +6,10 @@ package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type GetFileByIdLogic struct {
@@ -28,7 +28,20 @@ func NewGetFileByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFi
}
func (l *GetFileByIdLogic) GetFileById(req *types.FileIdReq) (resp *types.FileInfo, err error) {
// todo: add your logic here and delete this line
respRpc, err := l.svcCtx.FileRpc.GetFileById(l.ctx, &fileservice.GetFileByIdReq{
Id: req.Id,
})
if err != nil {
return nil, err
}
return
return &types.FileInfo{
Id: respRpc.File.Id,
Name: respRpc.File.Name,
Url: respRpc.File.Url,
Tag: respRpc.File.Tag,
Key: respRpc.File.Key,
Suffix: respRpc.File.Suffix,
Md5: respRpc.File.Md5,
}, nil
}
@@ -6,10 +6,10 @@ package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type GetFileListLogic struct {
@@ -27,8 +27,31 @@ func NewGetFileListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFi
}
}
func (l *GetFileListLogic) GetFileList(req *types.FileListReq) error {
// todo: add your logic here and delete this line
func (l *GetFileListLogic) GetFileList(req *types.FileListReq) (resp *types.FileListResp, err error) {
respRpc, err := l.svcCtx.FileRpc.GetFileList(l.ctx, &fileservice.GetFileListReq{
Current: int32(req.Current),
PageSize: int32(req.PageSize),
Name: req.Name,
})
if err != nil {
return nil, err
}
return nil
var list []types.FileInfo
for _, item := range respRpc.List {
list = append(list, types.FileInfo{
Id: item.Id,
Name: item.Name,
Url: item.Url,
Tag: item.Tag,
Key: item.Key,
Suffix: item.Suffix,
Md5: item.Md5,
})
}
return &types.FileListResp{
List: list,
Total: respRpc.Total,
}, nil
}
@@ -0,0 +1,63 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type GetStorageConfigListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 获取存储配置列表
func NewGetStorageConfigListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetStorageConfigListLogic {
return &GetStorageConfigListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetStorageConfigListLogic) GetStorageConfigList(req *types.StorageConfigListReq) (resp *types.StorageConfigListResp, err error) {
respRpc, err := l.svcCtx.FileRpc.GetStorageConfigList(l.ctx, &fileservice.StorageConfigListReq{
Current: int32(req.Current),
PageSize: int32(req.PageSize),
Type: req.Type,
Name: req.Name,
})
if err != nil {
return nil, err
}
var list []types.StorageConfigInfo
for _, item := range respRpc.List {
list = append(list, types.StorageConfigInfo{
Id: item.Id,
Type: item.Type,
Name: item.Name,
Endpoint: item.Endpoint,
AccessKeyId: item.AccessKeyId,
AccessKeySecret: item.AccessKeySecret,
BucketName: item.BucketName,
BucketUrl: item.BucketUrl,
Region: item.Region,
IsDefault: int(item.IsDefault),
Status: int(item.Status),
Remark: item.Remark,
})
}
return &types.StorageConfigListResp{
List: list,
Total: respRpc.Total,
}, nil
}
@@ -0,0 +1,39 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type SetDefaultStorageConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 设置默认存储配置
func NewSetDefaultStorageConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetDefaultStorageConfigLogic {
return &SetDefaultStorageConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *SetDefaultStorageConfigLogic) SetDefaultStorageConfig(req *types.SetDefaultStorageConfigReq) error {
_, err := l.svcCtx.FileRpc.SetDefaultStorageConfig(l.ctx, &fileservice.SetDefaultStorageConfigReq{
Id: req.Id,
})
if err != nil {
return err
}
return nil
}
@@ -0,0 +1,49 @@
// Code scaffolded by goctl. Safe to edit.
// goctl 1.10.1
package file
import (
"context"
"github.com/zeromicro/go-zero/core/logx"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
)
type UpdateStorageConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// 更新存储配置
func NewUpdateStorageConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateStorageConfigLogic {
return &UpdateStorageConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateStorageConfigLogic) UpdateStorageConfig(req *types.UpdateStorageConfigReq) error {
_, err := l.svcCtx.FileRpc.UpdateStorageConfig(l.ctx, &fileservice.UpdateStorageConfigReq{
Id: req.Id,
Type: req.Type,
Name: req.Name,
Endpoint: req.Endpoint,
AccessKeyId: req.AccessKeyId,
AccessKeySecret: req.AccessKeySecret,
BucketName: req.BucketName,
BucketUrl: req.BucketUrl,
Region: req.Region,
Status: int32(req.Status),
Remark: req.Remark,
})
if err != nil {
return err
}
return nil
}
@@ -5,9 +5,17 @@ package file
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"net/http"
"path/filepath"
"sundynix-micro-go/app/file/api/internal/oss_core"
"sundynix-micro-go/app/file/api/internal/svc"
"sundynix-micro-go/app/file/api/internal/types"
"sundynix-micro-go/app/file/rpc/fileservice"
"github.com/zeromicro/go-zero/core/logx"
)
@@ -27,8 +35,84 @@ func NewUploadFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Upload
}
}
func (l *UploadFileLogic) UploadFile() (resp *types.FileInfo, err error) {
// todo: add your logic here and delete this line
func (l *UploadFileLogic) UploadFile(r *http.Request) (resp *types.FileInfo, err error) {
err = r.ParseMultipartForm(32 << 20) // 32MB max memory
if err != nil {
return nil, fmt.Errorf("解析表单失败: %v", err)
}
file, fileHeader, err := r.FormFile("file")
if err != nil {
return nil, fmt.Errorf("获取文件失败: %v", err)
}
defer file.Close()
return
// 计算文件 MD5
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return nil, fmt.Errorf("计算文件MD5失败: %v", err)
}
fileMd5 := hex.EncodeToString(hash.Sum(nil))
// 调用 RPC 检查文件是否已存在(秒传)
checkResp, err := l.svcCtx.FileRpc.CheckFileByMd5(l.ctx, &fileservice.CheckFileByMd5Req{Md5: fileMd5})
if err != nil {
l.Logger.Errorf("调用FileRpc检查MD5失败: %v", err)
return nil, fmt.Errorf("服务器内部异常")
}
if checkResp.Exists && checkResp.File != nil {
l.Logger.Infof("文件已存在,触发秒传: %s", fileMd5)
return &types.FileInfo{
Id: checkResp.File.Id,
Name: checkResp.File.Name,
Url: checkResp.File.Url,
Tag: checkResp.File.Tag,
Key: checkResp.File.Key,
Suffix: checkResp.File.Suffix,
Md5: checkResp.File.Md5,
}, nil
}
// 此时需要真正上传,先将文件指针拨回开头
if _, err := file.Seek(0, 0); err != nil {
return nil, fmt.Errorf("读取文件流失败: %v", err)
}
factory := oss_core.NewOSSFactory(l.svcCtx.FileRpc)
uploader, err := factory.GetActiveUploader(l.ctx)
if err != nil {
l.Logger.Errorf("获取OSS客户端失败: %v", err)
return nil, fmt.Errorf("存储服务未配置或不可用")
}
url, key, err := uploader.UploadFile(l.ctx, file, fileHeader)
if err != nil {
l.Logger.Errorf("上传文件失败: %v", err)
return nil, err
}
ext := filepath.Ext(fileHeader.Filename)
// 把记录存入数据库
rpcResp, err := l.svcCtx.FileRpc.CreateFile(l.ctx, &fileservice.CreateFileReq{
Name: fileHeader.Filename,
Url: url,
Tag: "default",
Key: key,
Suffix: ext,
Md5: fileMd5, // 保存计算出的MD5
})
if err != nil {
l.Logger.Errorf("调用FileRpc创建文件记录失败: %v", err)
return nil, fmt.Errorf("保存文件记录失败")
}
return &types.FileInfo{
Id: rpcResp.File.Id,
Name: rpcResp.File.Name,
Url: rpcResp.File.Url,
Tag: rpcResp.File.Tag,
Key: rpcResp.File.Key,
Suffix: rpcResp.File.Suffix,
Md5: rpcResp.File.Md5,
}, nil
}
+58
View File
@@ -0,0 +1,58 @@
package oss_core
import (
"context"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"strings"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"sundynix-micro-go/app/file/rpc/file"
"sundynix-micro-go/common/utils/hash"
)
type AliyunUploader struct {
bucket *oss.Bucket
config *file.StorageConfigInfo
}
func NewAliyunUploader(conf *file.StorageConfigInfo) (Uploader, error) {
client, err := oss.New(conf.Endpoint, conf.AccessKeyId, conf.AccessKeySecret)
if err != nil {
return nil, err
}
bucket, err := client.Bucket(conf.BucketName)
if err != nil {
return nil, err
}
return &AliyunUploader{
bucket: bucket,
config: conf,
}, nil
}
func (a *AliyunUploader) UploadFile(ctx context.Context, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) {
ext := filepath.Ext(fileHeader.Filename)
filename := hash.MD5([]byte(strings.TrimSuffix(fileHeader.Filename, ext))) + ext
timestr := fmt.Sprintf("%d", time.Now().UnixMicro())
objectKey := time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename
err := a.bucket.PutObject(objectKey, file)
if err != nil {
return "", "", fmt.Errorf("上传阿里云OSS失败: %v", err)
}
url := a.config.BucketUrl + "/" + objectKey
return url, objectKey, nil
}
func (a *AliyunUploader) DeleteFile(ctx context.Context, key string) error {
return a.bucket.DeleteObject(key)
}
func (a *AliyunUploader) DownloadFile(ctx context.Context, key string) (io.ReadCloser, error) {
return a.bucket.GetObject(key)
}
+89
View File
@@ -0,0 +1,89 @@
package oss_core
import (
"context"
"fmt"
"reflect"
"sundynix-micro-go/app/file/rpc/file"
"sundynix-micro-go/app/file/rpc/fileservice"
"sync"
)
type CachedUploader struct {
Uploader Uploader
Config *file.StorageConfigInfo
}
var (
uploaderCache sync.Map // map[string]*CachedUploader
cacheMutex sync.Mutex
)
// OSSFactory 工厂结构体
type OSSFactory struct {
fileRpc fileservice.FileService
}
func NewOSSFactory(fileRpc fileservice.FileService) *OSSFactory {
return &OSSFactory{
fileRpc: fileRpc,
}
}
// GetActiveUploader 获取当前激活的存储上传实例
func (f *OSSFactory) GetActiveUploader(ctx context.Context) (Uploader, error) {
resp, err := f.fileRpc.GetDefaultStorageConfig(ctx, &fileservice.GetDefaultStorageConfigReq{})
if err != nil || resp.Config == nil {
return nil, fmt.Errorf("未找到激活的存储配置: %v", err)
}
conf := resp.Config
// 1. 尝试从缓存获取
if cachedVal, ok := uploaderCache.Load(conf.Id); ok {
cachedItem := cachedVal.(*CachedUploader)
// 优雅之处:深度对比配置是否发生变化,完美支持前端动态修改配置后实时热加载
if reflect.DeepEqual(cachedItem.Config, conf) {
return cachedItem.Uploader, nil
}
}
// 2. 加锁防止高并发下的重复实例化
cacheMutex.Lock()
defer cacheMutex.Unlock()
// 3. Double-Check
if cachedVal, ok := uploaderCache.Load(conf.Id); ok {
cachedItem := cachedVal.(*CachedUploader)
if reflect.DeepEqual(cachedItem.Config, conf) {
return cachedItem.Uploader, nil
}
}
// 4. 实例化新的 Uploader
var uploader Uploader
switch conf.Type {
case "minio":
uploader, err = NewMinioUploader(conf)
case "aliyun":
uploader, err = NewAliyunUploader(conf)
case "tencent":
uploader, err = NewTencentUploader(conf)
case "qiniu":
uploader, err = NewQiniuUploader(conf)
default:
return nil, fmt.Errorf("不支持的存储类型: %s", conf.Type)
}
if err != nil {
return nil, err
}
// 5. 更新缓存
uploaderCache.Store(conf.Id, &CachedUploader{
Uploader: uploader,
Config: conf,
})
return uploader, nil
}
+95
View File
@@ -0,0 +1,95 @@
package oss_core
import (
"context"
"fmt"
"io"
"mime/multipart"
"path/filepath"
"strings"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"sundynix-micro-go/app/file/rpc/file"
"sundynix-micro-go/common/utils/hash"
)
type MinioUploader struct {
client *minio.Client
config *file.StorageConfigInfo
}
func NewMinioUploader(conf *file.StorageConfigInfo) (Uploader, error) {
// 判断如果是 https 或者是类似云服务,可以根据 endpoint 后缀猜测 secure
useSSL := strings.HasPrefix(conf.Endpoint, "https://") || strings.Contains(conf.Endpoint, "aliyuncs.com") || conf.Endpoint == "oss-cn-hangzhou.aliyuncs.com" // 这里只是示例
if strings.HasPrefix(conf.Endpoint, "http://") || strings.HasPrefix(conf.Endpoint, "https://") {
// 移除协议头给 minio 客户端
conf.Endpoint = strings.TrimPrefix(conf.Endpoint, "http://")
conf.Endpoint = strings.TrimPrefix(conf.Endpoint, "https://")
} else {
// 默认非 SSL
useSSL = false
}
client, err := minio.New(conf.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(conf.AccessKeyId, conf.AccessKeySecret, ""),
Secure: useSSL,
})
if err != nil {
return nil, err
}
// 这里可以加上检查 bucket
return &MinioUploader{
client: client,
config: conf,
}, nil
}
func (m *MinioUploader) UploadFile(ctx context.Context, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) {
// 直接重置文件指针(保险起见)
file.Seek(0, 0)
ext := filepath.Ext(fileHeader.Filename)
filename := hash.MD5([]byte(strings.TrimSuffix(fileHeader.Filename, ext))) + ext
timestr := fmt.Sprintf("%d", time.Now().UnixMicro())
objectKey := time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename
bucketName := m.config.BucketName
// 直接执行 PutObject(流式上传,不再全部读入内存)
info, err := m.client.PutObject(ctx, bucketName, objectKey, file, fileHeader.Size, minio.PutObjectOptions{
ContentType: "application/octet-stream",
})
if err != nil {
// 如果是因为 bucket 不存在,则尝试创建并重试
if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
m.client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{})
// 重置文件流位置,准备重试
file.Seek(0, 0)
info, err = m.client.PutObject(ctx, bucketName, objectKey, file, fileHeader.Size, minio.PutObjectOptions{
ContentType: "application/octet-stream",
})
}
if err != nil {
return "", "", fmt.Errorf("上传Minio失败: %v", err)
}
}
url := m.config.BucketUrl + "/" + info.Key
return url, info.Key, nil
}
func (m *MinioUploader) DeleteFile(ctx context.Context, key string) error {
return m.client.RemoveObject(ctx, m.config.BucketName, key, minio.RemoveObjectOptions{})
}
func (m *MinioUploader) DownloadFile(ctx context.Context, key string) (io.ReadCloser, error) {
obj, err := m.client.GetObject(ctx, m.config.BucketName, key, minio.GetObjectOptions{})
if err != nil {
return nil, err
}
return obj, nil
}
+84
View File
@@ -0,0 +1,84 @@
package oss_core
import (
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"sundynix-micro-go/app/file/rpc/file"
"sundynix-micro-go/common/utils/hash"
)
type QiniuUploader struct {
mac *qbox.Mac
config *file.StorageConfigInfo
}
func NewQiniuUploader(conf *file.StorageConfigInfo) (Uploader, error) {
mac := qbox.NewMac(conf.AccessKeyId, conf.AccessKeySecret)
return &QiniuUploader{
mac: mac,
config: conf,
}, nil
}
func (q *QiniuUploader) UploadFile(ctx context.Context, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) {
ext := filepath.Ext(fileHeader.Filename)
filename := hash.MD5([]byte(strings.TrimSuffix(fileHeader.Filename, ext))) + ext
timestr := fmt.Sprintf("%d", time.Now().UnixMicro())
objectKey := time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename
putPolicy := storage.PutPolicy{
Scope: q.config.BucketName,
}
upToken := putPolicy.UploadToken(q.mac)
cfg := storage.Config{}
// 根据配置中的 Region 判断
// 这里简单写死,如果要自适应可以根据 config.Region 给定
cfg.Zone = &storage.ZoneHuadong
cfg.UseHTTPS = false
cfg.UseCdnDomains = false
formUploader := storage.NewFormUploader(&cfg)
ret := storage.PutRet{}
err := formUploader.Put(ctx, &ret, upToken, objectKey, file, fileHeader.Size, nil)
if err != nil {
return "", "", fmt.Errorf("上传七牛云失败: %v", err)
}
url := q.config.BucketUrl + "/" + ret.Key
return url, ret.Key, nil
}
func (q *QiniuUploader) DeleteFile(ctx context.Context, key string) error {
cfg := storage.Config{}
cfg.Zone = &storage.ZoneHuadong
bucketManager := storage.NewBucketManager(q.mac, &cfg)
return bucketManager.Delete(q.config.BucketName, key)
}
func (q *QiniuUploader) DownloadFile(ctx context.Context, key string) (io.ReadCloser, error) {
mac := qbox.NewMac(q.config.AccessKeyId, q.config.AccessKeySecret)
domain := q.config.BucketUrl
deadline := time.Now().Add(time.Second * 3600).Unix() // 1小时有效期
privateAccessURL := storage.MakePrivateURL(mac, domain, key, deadline)
resp, err := http.Get(privateAccessURL)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("qiniu download failed with status: %s", resp.Status)
}
return resp.Body, nil
}
+73
View File
@@ -0,0 +1,73 @@
package oss_core
import (
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/tencentyun/cos-go-sdk-v5"
"sundynix-micro-go/app/file/rpc/file"
"sundynix-micro-go/common/utils/hash"
)
type TencentUploader struct {
client *cos.Client
config *file.StorageConfigInfo
}
func NewTencentUploader(conf *file.StorageConfigInfo) (Uploader, error) {
// Endpoint should be something like https://bucket-appid.cos.ap-guangzhou.myqcloud.com
// But usually users just put bucket name and region, here we assume endpoint is full bucket url
// Or we use bucketUrl as the endpoint for client initialization if endpoint is not formatted well
u, err := url.Parse(conf.Endpoint)
if err != nil {
return nil, fmt.Errorf("解析Endpoint失败: %v", err)
}
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: conf.AccessKeyId,
SecretKey: conf.AccessKeySecret,
},
})
return &TencentUploader{
client: client,
config: conf,
}, nil
}
func (t *TencentUploader) UploadFile(ctx context.Context, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) {
ext := filepath.Ext(fileHeader.Filename)
filename := hash.MD5([]byte(strings.TrimSuffix(fileHeader.Filename, ext))) + ext
timestr := fmt.Sprintf("%d", time.Now().UnixMicro())
objectKey := time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename
_, err := t.client.Object.Put(ctx, objectKey, file, nil)
if err != nil {
return "", "", fmt.Errorf("上传腾讯云COS失败: %v", err)
}
fileUrl := t.config.BucketUrl + "/" + objectKey
return fileUrl, objectKey, nil
}
func (t *TencentUploader) DeleteFile(ctx context.Context, key string) error {
_, err := t.client.Object.Delete(ctx, key)
return err
}
func (t *TencentUploader) DownloadFile(ctx context.Context, key string) (io.ReadCloser, error) {
resp, err := t.client.Object.Get(ctx, key, nil)
if err != nil {
return nil, err
}
return resp.Body, nil
}
@@ -0,0 +1,17 @@
package oss_core
import (
"context"
"io"
"mime/multipart"
)
// Uploader 统一存储上传接口
type Uploader interface {
// UploadFile 接收文件流并上传,返回存储的具体url,标识key和可能的错误
UploadFile(ctx context.Context, file multipart.File, fileHeader *multipart.FileHeader) (url string, key string, err error)
// DeleteFile 删除远端存储的文件
DeleteFile(ctx context.Context, key string) error
// DownloadFile 获取文件下载的数据流
DownloadFile(ctx context.Context, key string) (io.ReadCloser, error)
}
+63
View File
@@ -3,6 +3,19 @@
package types
type CreateStorageConfigReq struct {
Type string `json:"type"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
BucketName string `json:"bucketName"`
BucketUrl string `json:"bucketUrl"`
Region string `json:"region,optional"`
Status int `json:"status,optional"`
Remark string `json:"remark,optional"`
}
type FileIdReq struct {
Id string `path:"id"`
}
@@ -23,6 +36,56 @@ type FileListReq struct {
Name string `json:"name,optional"`
}
type FileListResp struct {
List []FileInfo `json:"list"`
Total int64 `json:"total"`
}
type IdsReq struct {
Ids []string `json:"ids"`
}
type SetDefaultStorageConfigReq struct {
Id string `json:"id"`
}
type StorageConfigInfo struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
BucketName string `json:"bucketName"`
BucketUrl string `json:"bucketUrl"`
Region string `json:"region"`
IsDefault int `json:"isDefault"`
Status int `json:"status"`
Remark string `json:"remark"`
}
type StorageConfigListReq struct {
Current int `json:"current,optional"`
PageSize int `json:"pageSize,optional"`
Type string `json:"type,optional"`
Name string `json:"name,optional"`
}
type StorageConfigListResp struct {
List []StorageConfigInfo `json:"list"`
Total int64 `json:"total"`
}
type UpdateStorageConfigReq struct {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Endpoint string `json:"endpoint"`
AccessKeyId string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
BucketName string `json:"bucketName"`
BucketUrl string `json:"bucketUrl"`
Region string `json:"region,optional"`
Status int `json:"status,optional"`
Remark string `json:"remark,optional"`
}