feat: 新增删除文件接口并支持多文件上传

- 新增 DeleteFile 接口,支持通过文件 URL 删除文件
- UploadFile 接口支持多文件上传,返回结果包含文件列表
- DownloadToBrowser 改为流式读取,避免大文件占用内存
- 移除 UploadFileBytes 字节流上传接口
- 修复租户存储容量校验顺序,先校验容量再写入 Redis
This commit is contained in:
2026-06-11 09:14:45 +08:00
parent 99cbdec579
commit 625ec05599
4 changed files with 172 additions and 191 deletions

View File

@@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"io"
"net/url"
"oss/consts"
"oss/dao"
@@ -31,42 +32,137 @@ func (f *file) DownloadToFile(ctx context.Context, req *dto.DownloadToFileReq) (
// DownloadToBrowser 下载文件到浏览器
func (f *file) DownloadToBrowser(ctx context.Context, req *dto.DownloadToBrowserReq) (err error) {
fileBytes, fileName, contentType, err := minio.DownloadToBrowser(ctx, req.FileURL)
// 拿到流,而不是全量字节数组
reader, fileName, contentType, fileSize, err := minio.DownloadToBrowser(ctx, req.FileURL)
if err != nil {
return err
}
// 重要:确保流最后关闭
if closer, ok := reader.(io.Closer); ok {
defer closer.Close()
}
r := ghttp.RequestFromCtx(ctx)
// 设置响应头
r.Response.Header().Set("Content-Type", contentType)
r.Response.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, fileName, url.PathEscape(fileName)))
r.Response.Header().Set("Content-Length", fmt.Sprintf("%d", len(fileBytes)))
// 写出二进制流
r.Response.Write(fileBytes)
resp := r.Response
resp.Header().Set("Content-Type", contentType)
resp.Header().Set("Content-Disposition", fmt.Sprintf(
`attachment; filename="%s"; filename*=UTF-8''%s`,
fileName, url.PathEscape(fileName),
))
resp.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize))
resp.Header().Del("Content-Encoding")
resp.Header().Del("Transfer-Encoding")
data, err := io.ReadAll(reader)
if err != nil {
return err
}
resp.WriteExit(data)
return
}
func (f *file) DeleteFile(ctx context.Context, req *dto.DeleteFileReq) (err error) {
return minio.DeleteFile(ctx, req.FileURL)
}
func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
fileSize := gconv.Int(req.File.Size)
totalFileSize := 0
// 获取租户id
user, err := utils.GetUserInfo(ctx)
fileReq := new(dto.UploadFileReq)
if !g.IsEmpty(req.File) {
fileReq.Files = &ghttp.UploadFiles{req.File}
files, err := f.UploadFiles(ctx, fileReq)
if err != nil {
return nil, err
}
res = files.Items[0]
} else if !g.IsEmpty(req.Files) {
fileReq.Files = req.Files
res, err = f.UploadFiles(ctx, fileReq)
} else {
return nil, gerror.New("上传内容不能为空")
}
return
}
// UploadFiles 上传多个文件(新接口)
func (f *file) UploadFiles(ctx context.Context, req *dto.UploadFileReq) (res *dto.UploadFileRes, err error) {
var files = *req.Files
totalAdd := 0
for _, file := range files {
if file == nil {
continue
}
totalAdd += gconv.Int(file.Size)
}
tenantId, err := f.reserveTenantOssSize(ctx, totalAdd)
if err != nil {
return nil, err
}
prefix, _ := utils.GetFileAddressPrefix(ctx)
items := make([]*dto.UploadFileRes, 0, len(files))
for _, file := range files {
if file == nil {
continue
}
fileURL, fileName, fileFormat, e := minio.UploadFile(ctx, file)
if e != nil {
glog.Errorf(ctx, "上传文件失败: %v", e)
return nil, e
}
ossEntity := &dto.UploadFile{
TenantId: tenantId,
FileURL: fileURL,
FileSize: gconv.Int(file.Size),
}
if _, e = dao.File.Insert(ctx, ossEntity); e != nil {
return nil, e
}
items = append(items, &dto.UploadFileRes{
FileURL: fileURL,
FileSize: gconv.Int(file.Size),
FileName: fileName,
FileFormat: fileFormat,
FileAddressPrefix: prefix,
})
}
fileRes := &dto.UploadFileRes{
FileAddressPrefix: prefix,
Items: items,
}
return fileRes, nil
}
func (f *file) reserveTenantOssSize(ctx context.Context, addSize int) (tenantId uint64, err error) {
if addSize < 0 {
err = gerror.New("addSize 不能为负数")
return
}
// 获取租户id
user, e := utils.GetUserInfo(ctx)
if e != nil {
err = e
glog.Errorf(ctx, "获取用户信息失败: %v", err)
return
}
tenantId := user.TenantId
tenantId = user.TenantId
// 获取redis-租户存储容量总数key
tenantOssTotalKey := fmt.Sprintf(consts.TenantOssTotalKey, gconv.String(user.TenantId))
// 获取redis-租户存储-锁key
fileLockKey := fmt.Sprintf(consts.FileLockKey, gconv.String(user.TenantId))
success, err := utils.Lock(ctx, fileLockKey, gconv.Int64(time.Minute*1), func(ctx context.Context) error {
success, e := utils.Lock(ctx, fileLockKey, gconv.Int64(time.Minute*1), func(ctx context.Context) error {
// 获取redis-租户存储容量总数
get, err := g.Redis().Get(ctx, tenantOssTotalKey)
if err != nil {
glog.Errorf(ctx, "获取redis-租户存储容量总数失败: %v", err)
return err
get, e := g.Redis().Get(ctx, tenantOssTotalKey)
if e != nil {
glog.Errorf(ctx, "获取redis-租户存储容量总数失败: %v", e)
return e
}
tenantOssTotalEntity := &entity.TenantOssTotal{}
if g.IsEmpty(get) {
@@ -74,10 +170,10 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
getByTenantIdReq := &dto.GetByTenantIdReq{
TenantId: user.TenantId,
}
tenantOssTotalRes, err := dao.TenantOssTotal.GetOneByTenantId(ctx, getByTenantIdReq)
if err != nil {
glog.Errorf(ctx, "查询数据库-获取租户存储容量总数失败: %v", err)
return err
tenantOssTotalRes, e := dao.TenantOssTotal.GetOneByTenantId(ctx, getByTenantIdReq)
if e != nil {
glog.Errorf(ctx, "查询数据库-获取租户存储容量总数失败: %v", e)
return e
}
if tenantOssTotalRes == nil || g.IsEmpty(tenantOssTotalRes.Id) {
// 数据库中没有该租户的记录,创建默认配置
@@ -89,102 +185,40 @@ func (f *file) UploadFile(ctx context.Context, req *dto.UploadFileReq) (res *dto
}
} else {
// 反序列化-redis获取租户存储容量总数
if err = gconv.Struct(get, tenantOssTotalEntity); err != nil {
glog.Errorf(ctx, "反序列化-redis获取租户存储容量总数失败: %v", err)
return err
if e = gconv.Struct(get, tenantOssTotalEntity); e != nil {
glog.Errorf(ctx, "反序列化-redis获取租户存储容量总数失败: %v", e)
return e
}
}
tenantId = tenantOssTotalEntity.TenantId
fileSize = tenantOssTotalEntity.UsedOssSize + fileSize
totalFileSize = tenantOssTotalEntity.TotalOssSize
// 设置redis-租户存储容量总数
usedAfter := tenantOssTotalEntity.UsedOssSize + addSize
total := tenantOssTotalEntity.TotalOssSize
if usedAfter > total {
return gerror.New("存储服务内存不足")
}
// 设置redis-租户存储容量总数超时时间10分钟
tenantOssTotalKeyMap := dto.UpdateUsedOssReq{
TenantId: tenantId,
UsedOssSize: fileSize,
TotalOssSize: totalFileSize,
UsedOssSize: usedAfter,
TotalOssSize: total,
Creator: user.UserName,
Updater: user.UserName,
}
// 修改redis-租户存储容量总数 超时时间10分钟
if err = g.Redis().SetEX(ctx, tenantOssTotalKey, tenantOssTotalKeyMap, gconv.Int64(time.Minute*10)); err != nil {
glog.Errorf(ctx, "修改redis-租户存储容量总数 超时时间10分钟失败: %v", err)
return err
}
if fileSize > totalFileSize {
return gerror.New("存储服务内存不足")
if e = g.Redis().SetEX(ctx, tenantOssTotalKey, tenantOssTotalKeyMap, gconv.Int64(time.Minute*10)); e != nil {
glog.Errorf(ctx, "修改redis-租户存储容量总数失败: %v", e)
return e
}
return nil
})
if err != nil {
return nil, err
if e != nil {
err = e
return
}
if !success {
return nil, gerror.New("存储服务内存不足")
}
// 上传图片
var fileURL, fileName, fileFormat string
fileURL, fileName, fileFormat, err = minio.UploadFile(ctx, req.File)
if err != nil {
glog.Errorf(ctx, "上传图片失败: %v", err)
return nil, err
}
// 插入数据库
ossEntity := &dto.UploadFile{
TenantId: tenantId,
FileURL: fileURL,
FileSize: fileSize,
}
if _, err = dao.File.Insert(ctx, ossEntity); err != nil {
return nil, err
}
// 返回图片url
res = &dto.UploadFileRes{
FileURL: fileURL,
FileSize: fileSize,
FileName: fileName,
FileFormat: fileFormat,
}
url, err := utils.GetFileAddressPrefix(ctx)
if err != nil {
err = gerror.New("存储服务内存不足")
return
}
res.FileAddressPrefix = url
return
}
// UploadFileBytes 上传文件(字节流)
func (f *file) UploadFileBytes(ctx context.Context, req *dto.UploadFileBytesReq) (res *dto.UploadFileRes, err error) {
// 获取用户信息
user, err := utils.GetUserInfo(ctx)
if err != nil {
glog.Errorf(ctx, "获取用户信息失败: %v", err)
return
}
tenantId := user.TenantId
// 上传到 MinIO
fileURL, fileFormat, err := minio.UploadFileBytes(ctx, req.FileName, req.FileBytes, req.FileStoreURL)
if err != nil {
glog.Errorf(ctx, "上传文件失败: %v", err)
return nil, err
}
// 插入数据库记录
ossEntity := &dto.UploadFile{
TenantId: tenantId,
FileURL: fileURL,
FileSize: len(req.FileBytes),
}
if _, err = dao.File.Insert(ctx, ossEntity); err != nil {
return nil, err
}
res = &dto.UploadFileRes{
FileURL: fileURL,
FileSize: len(req.FileBytes),
FileName: req.FileName,
FileFormat: fileFormat,
}
res.FileAddressPrefix, _ = utils.GetFileAddressPrefix(ctx)
return
}