mirror of
				https://gitee.com/dromara/mayfly-go
				synced 2025-11-04 08:20:25 +08:00 
			
		
		
		
	feat: 新增统一文件模块,统一文件操作
This commit is contained in:
		
							
								
								
									
										53
									
								
								server/internal/file/api/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								server/internal/file/api/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/file/api/vo"
 | 
			
		||||
	"mayfly-go/internal/file/application"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/model"
 | 
			
		||||
	"mayfly-go/pkg/req"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type File struct {
 | 
			
		||||
	FileApp application.File `inject:""`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) GetFileByKeys(rc *req.Ctx) {
 | 
			
		||||
	keysStr := rc.PathParam("keys")
 | 
			
		||||
	biz.NotEmpty(keysStr, "keys不能为空")
 | 
			
		||||
 | 
			
		||||
	var files []vo.SimpleFile
 | 
			
		||||
	err := f.FileApp.ListByCondToAny(model.NewCond().In("file_key", strings.Split(keysStr, ",")), &files)
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = files
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) GetFileContent(rc *req.Ctx) {
 | 
			
		||||
	key := rc.PathParam("key")
 | 
			
		||||
	biz.NotEmpty(key, "key不能为空")
 | 
			
		||||
 | 
			
		||||
	filename, reader, err := f.FileApp.GetReader(rc.MetaCtx, key)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		rc.GetWriter().Write([]byte(err.Error()))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer reader.Close()
 | 
			
		||||
	rc.Download(reader, filename)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) Upload(rc *req.Ctx) {
 | 
			
		||||
	multipart, err := rc.GetRequest().MultipartReader()
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
 | 
			
		||||
	file, err := multipart.NextPart()
 | 
			
		||||
	biz.ErrIsNilAppendErr(err, "读取文件失败: %s")
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
 | 
			
		||||
	fileKey, err := f.FileApp.Upload(rc.MetaCtx, rc.Query("fileKey"), file.FileName(), file)
 | 
			
		||||
	biz.ErrIsNil(err)
 | 
			
		||||
	rc.ResData = fileKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) Remove(rc *req.Ctx) {
 | 
			
		||||
	biz.ErrIsNil(f.FileApp.Remove(rc.MetaCtx, rc.PathParam("key")))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								server/internal/file/api/vo/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/internal/file/api/vo/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
package vo
 | 
			
		||||
 | 
			
		||||
type SimpleFile struct {
 | 
			
		||||
	Filename string `json:"filename"`
 | 
			
		||||
	FileKey  string `json:"fileKey"`
 | 
			
		||||
	Size     int64  `json:"size"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								server/internal/file/application/application.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/internal/file/application/application.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/pkg/ioc"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InitIoc() {
 | 
			
		||||
	ioc.Register(new(fileAppImpl), ioc.WithComponentName("FileApp"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								server/internal/file/application/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								server/internal/file/application/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
package application
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"mayfly-go/internal/file/config"
 | 
			
		||||
	"mayfly-go/internal/file/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/file/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
	"mayfly-go/pkg/errorx"
 | 
			
		||||
	"mayfly-go/pkg/logx"
 | 
			
		||||
	"mayfly-go/pkg/utils/stringx"
 | 
			
		||||
	"mayfly-go/pkg/utils/writerx"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type File interface {
 | 
			
		||||
	base.App[*entity.File]
 | 
			
		||||
 | 
			
		||||
	// Upload 上传文件
 | 
			
		||||
	//
 | 
			
		||||
	// @param fileKey 文件key,若存在则使用存在的文件key,否则生成新的文件key。
 | 
			
		||||
	//
 | 
			
		||||
	// @param filename 文件名,带文件后缀
 | 
			
		||||
	//
 | 
			
		||||
	// @return fileKey 文件key
 | 
			
		||||
	Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error)
 | 
			
		||||
 | 
			
		||||
	// NewWriter 创建文件writer
 | 
			
		||||
	//
 | 
			
		||||
	// @param canEmptyFileKey 文件key,若不为空则使用该文件key,否则生成新的文件key。
 | 
			
		||||
	//
 | 
			
		||||
	// @param filename 文件名,带文件后缀
 | 
			
		||||
	//
 | 
			
		||||
	// @return fileKey 文件key
 | 
			
		||||
	//
 | 
			
		||||
	// @return writer 文件writer
 | 
			
		||||
	//
 | 
			
		||||
	// @return saveFunc 保存文件信息的回调函数 (必须要defer中调用才会入库保存该文件信息)
 | 
			
		||||
	NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error)
 | 
			
		||||
 | 
			
		||||
	// GetReader 获取文件reader
 | 
			
		||||
	//
 | 
			
		||||
	// @return filename 文件名
 | 
			
		||||
	//
 | 
			
		||||
	// @return reader 文件reader
 | 
			
		||||
	//
 | 
			
		||||
	// @return err 错误
 | 
			
		||||
	GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error)
 | 
			
		||||
 | 
			
		||||
	// Remove 删除文件
 | 
			
		||||
	Remove(ctx context.Context, fileKey string) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type fileAppImpl struct {
 | 
			
		||||
	base.AppImpl[*entity.File, repository.File]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) InjectFileRepo(repo repository.File) {
 | 
			
		||||
	f.Repo = repo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) Upload(ctx context.Context, fileKey string, filename string, r io.Reader) (string, error) {
 | 
			
		||||
	fileKey, writer, saveFileFunc, err := f.NewWriter(ctx, fileKey, filename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fileKey, err
 | 
			
		||||
	}
 | 
			
		||||
	defer saveFileFunc()
 | 
			
		||||
 | 
			
		||||
	if _, err := io.Copy(writer, r); err != nil {
 | 
			
		||||
		return fileKey, err
 | 
			
		||||
	}
 | 
			
		||||
	return fileKey, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) NewWriter(ctx context.Context, canEmptyFileKey string, filename string) (fileKey string, writer *writerx.CountingWriteCloser, saveFunc func() error, err error) {
 | 
			
		||||
	isNewFile := true
 | 
			
		||||
	file := &entity.File{}
 | 
			
		||||
 | 
			
		||||
	if canEmptyFileKey == "" {
 | 
			
		||||
		canEmptyFileKey = stringx.RandUUID()
 | 
			
		||||
		file.FileKey = canEmptyFileKey
 | 
			
		||||
	} else {
 | 
			
		||||
		file.FileKey = canEmptyFileKey
 | 
			
		||||
		if err := f.GetByCond(file); err == nil {
 | 
			
		||||
			isNewFile = false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	file.Filename = filename
 | 
			
		||||
 | 
			
		||||
	if !isNewFile {
 | 
			
		||||
		// 先删除旧文件
 | 
			
		||||
		f.remove(ctx, file)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 生产新的文件名
 | 
			
		||||
	newFilename := canEmptyFileKey + filepath.Ext(filename)
 | 
			
		||||
	filepath, w, err := f.newWriter(newFilename)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	file.Path = filepath
 | 
			
		||||
 | 
			
		||||
	fileKey = canEmptyFileKey
 | 
			
		||||
	writer = writerx.NewCountingWriteCloser(w)
 | 
			
		||||
	// 创建回调函数
 | 
			
		||||
	saveFunc = func() error {
 | 
			
		||||
		// 获取已写入的字节数
 | 
			
		||||
		file.Size = writer.BytesWritten()
 | 
			
		||||
		writer.Close()
 | 
			
		||||
		// 保存文件信息
 | 
			
		||||
		return f.Save(ctx, file)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fileKey, writer, saveFunc, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) GetReader(ctx context.Context, fileKey string) (string, io.ReadCloser, error) {
 | 
			
		||||
	file := &entity.File{FileKey: fileKey}
 | 
			
		||||
	if err := f.GetByCond(file); err != nil {
 | 
			
		||||
		return "", nil, errorx.NewBiz("文件不存在")
 | 
			
		||||
	}
 | 
			
		||||
	r, err := os.Open(filepath.Join(config.GetFileConfig().BasePath, file.Path))
 | 
			
		||||
	return file.Filename, r, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) Remove(ctx context.Context, fileKey string) error {
 | 
			
		||||
	file := &entity.File{FileKey: fileKey}
 | 
			
		||||
	if err := f.GetByCond(file); err != nil {
 | 
			
		||||
		return errorx.NewBiz("文件不存在")
 | 
			
		||||
	}
 | 
			
		||||
	f.DeleteById(ctx, file.Id)
 | 
			
		||||
	return f.remove(ctx, file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) newWriter(filename string) (string, io.WriteCloser, error) {
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	filePath := filepath.Join(cast.ToString(now.Year()), cast.ToString(int(now.Month())), cast.ToString(now.Day()), cast.ToString(now.Hour()), filename)
 | 
			
		||||
	fileAbsPath := filepath.Join(config.GetFileConfig().BasePath, filePath)
 | 
			
		||||
 | 
			
		||||
	// 目录不存在则创建
 | 
			
		||||
	fileDir := filepath.Dir(fileAbsPath)
 | 
			
		||||
	if _, err := os.Stat(fileDir); os.IsNotExist(err) {
 | 
			
		||||
		err = os.MkdirAll(fileDir, os.ModePerm)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建目标文件
 | 
			
		||||
	out, err := os.OpenFile(fileAbsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0766)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return filePath, out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *fileAppImpl) remove(ctx context.Context, file *entity.File) error {
 | 
			
		||||
	if err := os.Remove(filepath.Join(config.GetFileConfig().BasePath, file.Path)); err != nil {
 | 
			
		||||
		logx.ErrorfContext(ctx, "删除旧文件[%s] 失败: %s", file.Path, err.Error())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								server/internal/file/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								server/internal/file/config/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	sysapp "mayfly-go/internal/sys/application"
 | 
			
		||||
 | 
			
		||||
	"github.com/may-fly/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ConfigKeyFile string = "FileConfig" // 文件配置key
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type FileConfig struct {
 | 
			
		||||
	BasePath string // 文件基础路径
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetFileConfig() *FileConfig {
 | 
			
		||||
	c := sysapp.GetConfigApp().GetConfig(ConfigKeyFile)
 | 
			
		||||
	jm := c.GetJsonMap()
 | 
			
		||||
 | 
			
		||||
	fc := new(FileConfig)
 | 
			
		||||
	fc.BasePath = cast.ToStringD(jm["basePath"], "./file")
 | 
			
		||||
	return fc
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								server/internal/file/domain/entity/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/internal/file/domain/entity/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
import "mayfly-go/pkg/model"
 | 
			
		||||
 | 
			
		||||
type File struct {
 | 
			
		||||
	model.Model
 | 
			
		||||
 | 
			
		||||
	FileKey  string `json:"fikeKey"`  // 文件key
 | 
			
		||||
	Filename string `json:"filename"` // 文件名
 | 
			
		||||
	Path     string `json:"path" `    // 文件路径
 | 
			
		||||
	Size     int64  `json:"size"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *File) TableName() string {
 | 
			
		||||
	return "t_sys_file"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								server/internal/file/domain/entity/query.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								server/internal/file/domain/entity/query.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
package entity
 | 
			
		||||
 | 
			
		||||
type FileQuery struct {
 | 
			
		||||
	Keys []string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								server/internal/file/domain/repository/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/internal/file/domain/repository/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
package repository
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/file/domain/entity"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type File interface {
 | 
			
		||||
	base.Repo[*entity.File]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								server/internal/file/infrastructure/persistence/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								server/internal/file/infrastructure/persistence/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/file/domain/entity"
 | 
			
		||||
	"mayfly-go/internal/file/domain/repository"
 | 
			
		||||
	"mayfly-go/pkg/base"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type fileRepoImpl struct {
 | 
			
		||||
	base.RepoImpl[*entity.File]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newFileRepo() repository.File {
 | 
			
		||||
	return &fileRepoImpl{base.RepoImpl[*entity.File]{M: new(entity.File)}}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package persistence
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/pkg/ioc"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InitIoc() {
 | 
			
		||||
	ioc.Register(newFileRepo(), ioc.WithComponentName("FileRepo"))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								server/internal/file/init/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/internal/file/init/init.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
package init
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/initialize"
 | 
			
		||||
	"mayfly-go/internal/file/application"
 | 
			
		||||
	"mayfly-go/internal/file/infrastructure/persistence"
 | 
			
		||||
	"mayfly-go/internal/file/router"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	initialize.AddInitIocFunc(func() {
 | 
			
		||||
		persistence.InitIoc()
 | 
			
		||||
		application.InitIoc()
 | 
			
		||||
	})
 | 
			
		||||
	initialize.AddInitRouterFunc(router.Init)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								server/internal/file/router/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								server/internal/file/router/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"mayfly-go/internal/file/api"
 | 
			
		||||
	"mayfly-go/pkg/biz"
 | 
			
		||||
	"mayfly-go/pkg/ioc"
 | 
			
		||||
	"mayfly-go/pkg/req"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InitFileRouter(router *gin.RouterGroup) {
 | 
			
		||||
	file := router.Group("sys/files")
 | 
			
		||||
	f := new(api.File)
 | 
			
		||||
	biz.ErrIsNil(ioc.Inject(f))
 | 
			
		||||
 | 
			
		||||
	reqs := [...]*req.Conf{
 | 
			
		||||
 | 
			
		||||
		req.NewGet("/detail/:keys", f.GetFileByKeys).DontNeedToken(),
 | 
			
		||||
 | 
			
		||||
		req.NewGet("/:key", f.GetFileContent).DontNeedToken().NoRes(),
 | 
			
		||||
 | 
			
		||||
		req.NewPost("/upload", f.Upload).Log(req.NewLogSave("file-文件上传")),
 | 
			
		||||
 | 
			
		||||
		req.NewDelete("/:key", f.Remove).Log(req.NewLogSave("file-文件删除")),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req.BatchSetGroup(file, reqs[:])
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								server/internal/file/router/router.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/internal/file/router/router.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package router
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Init(router *gin.RouterGroup) {
 | 
			
		||||
	InitFileRouter(router)
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user