liuxiulin 2 år sedan
incheckning
d5e5f70cb1
68 ändrade filer med 4482 tillägg och 0 borttagningar
  1. 8 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 9 0
      .idea/gxt-file-server.iml
  4. 8 0
      .idea/modules.xml
  5. 6 0
      .idea/vcs.xml
  6. 109 0
      README.md
  7. 261 0
      app/agent/agent.go
  8. 22 0
      app/bll/b_demo.go
  9. 20 0
      app/bll/b_file.go
  10. 22 0
      app/bll/b_file_chunk.go
  11. 9 0
      app/bll/b_trans.go
  12. 18 0
      app/bll/impl/impl.go
  13. 43 0
      app/bll/impl/internal/b_common.go
  14. 112 0
      app/bll/impl/internal/b_demo.go
  15. 323 0
      app/bll/impl/internal/b_file.go
  16. 105 0
      app/bll/impl/internal/b_file_chunk.go
  17. 23 0
      app/bll/impl/internal/b_trans.go
  18. 118 0
      app/context/context.go
  19. 24 0
      app/errors/error.go
  20. 93 0
      app/errors/response.go
  21. 0 0
      app/model/.gitkeep
  22. 65 0
      app/model/entity/e_demo.go
  23. 78 0
      app/model/entity/e_file_chunk.go
  24. 71 0
      app/model/entity/e_file_history.go
  25. 25 0
      app/model/entity/entity.go
  26. 110 0
      app/model/impl/model/m_demo.go
  27. 120 0
      app/model/impl/model/m_file_chunk.go
  28. 101 0
      app/model/impl/model/m_file_history.go
  29. 54 0
      app/model/impl/model/m_trans.go
  30. 123 0
      app/model/impl/model/model.go
  31. 22 0
      app/model/m_demo.go
  32. 22 0
      app/model/m_file_chunk.go
  33. 20 0
      app/model/m_file_history.go
  34. 13 0
      app/model/m_trans.go
  35. 33 0
      app/schema/s_demo.go
  36. 58 0
      app/schema/s_file.go
  37. 47 0
      app/schema/s_file_chunk.go
  38. 40 0
      app/schema/s_file_history.go
  39. 54 0
      app/schema/schema.go
  40. 202 0
      boot/boot.go
  41. 73 0
      boot/gorm.go
  42. 110 0
      config/config.toml
  43. 21 0
      go.mod
  44. 183 0
      go.sum
  45. 77 0
      gxt-file-server_package/file_server.go
  46. 18 0
      gxt-file-server_package/request_file_test.go
  47. 12 0
      main.go
  48. 124 0
      pkg/auth/jwt_auth.go
  49. 182 0
      pkg/gplus/gplus.go
  50. 64 0
      pkg/logger/gorm_log.go
  51. 196 0
      pkg/logger/logger.go
  52. 49 0
      pkg/middleware/middleware.go
  53. 39 0
      pkg/middleware/mw_auth.go
  54. 16 0
      pkg/middleware/mw_cros.go
  55. 100 0
      pkg/middleware/mw_file.go
  56. 71 0
      pkg/middleware/mw_rate_limiter.go
  57. 20 0
      pkg/middleware/mw_trace.go
  58. 36 0
      pkg/redis/redis.go
  59. 28 0
      pkg/store/store.go
  60. 167 0
      pkg/store/store_minio.go
  61. 39 0
      pkg/utils/util.go
  62. 41 0
      router/api/api.go
  63. 41 0
      router/api/controllers/c_demo.go
  64. 131 0
      router/api/controllers/c_file.go
  65. 9 0
      router/api/controllers/ctl.go
  66. 15 0
      router/router.go
  67. 18 0
      router/swagger.go
  68. 3 0
      发开发.sh

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+/server2

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/

+ 9 - 0
.idea/gxt-file-server.iml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="Go" enabled="true" />
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/gxt-file-server.iml" filepath="$PROJECT_DIR$/.idea/gxt-file-server.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 109 - 0
README.md

@@ -0,0 +1,109 @@
+## 高新通文件中间件服务
+
+### 功能清单
+
+* 支持多文件、大文件拆分上传
+* 支持已存在文件急速上传
+* 支持文件断点续传
+* 文件下载
+* 文件过期机制
+* 支持对象存储服务组件的扩展
+* 图片资源支持缩略图
+
+## 接口定义
+
+### 1.文件上传接口
+
+* 请求URL:`api/v1/files`
+* 请求方法: `POST`
+* 请求参数: `form-data`
+
+#### header参数
+
+_header参数可以不传,特殊需求下可以使用_
+
+|参数|类型|说明|
+|---|---|---|
+|FILE-EXPIRE|数字|文件过期时间,如果为空则按服务配置中的默认过期时间(300秒), -1为不过期|
+|FILE-HASH|字符串|文件hash值,如文件的md5|
+
+#### 请求form-data参数
+
+|参数|类型|说明|
+|---|---|---|
+|base_url|字符串|基本路径,对象桶的名称|
+|form_key|字符串|form-data中的文件键值,如**data**|
+|**data**|字符串|同form_key中配置的值|
+
+### 2.请求文件
+
+_对于文件的请求分两种情况,一种是直接显示文件,如图片、视频、pdf等, 一种下返回文件的二进制流(content-type:application/octet-stream),
+前者的content-type是根据文件的类型返回的,后者固定为application/octet-stream,对于浏览器无法解析的content-type,会直接下载_
+
+#### 2.1 打开文件(pdf\图片\视频)
+
+* 请求URL: `文件的url`
+* 请求方法: `GET`
+* 请求参数: `见下方`
+
+#### 缩略图参数说明
+
+_在请求的url中拼接如下参数_
+`?thumb=1&w=100&h=100`
+
+`thumb`: 为1则说明开启缩略图模式
+
+`w`: 缩略图宽度,数字
+
+`h`: 缩略图高度,数字
+
+#### 2.2 下载文件(返回文件的二进制流)
+
+* 请求URL: `api/v1/files`
+* 请求方法: `GET`
+* 请求参数:
+
+|参数|类型|说明|
+|---|---|---|
+|path|字符串|文件路径|
+|name|字符串|要下载的文件名|
+
+#### 2.3 文件本地化
+
+_默认文件上传后为临时文件,不调用本地化则会过期删除(设置FILE-EXPIRE为-1的情况除外)_
+
+* 请求URL: `/api/v1/files/persistent`
+* 请求方法: `PUT`
+* 请求参数: `JSON Body`
+
+|参数|类型|说明|
+|---|---|----|
+|hash|字符串|文件的hash值|
+
+### 3.文件分块上传
+
+* 请求URL: `/api/v1/files/chunk`
+* 请求方法: `POST`
+* 请求参数: `form-data`
+
+|参数|类型|是否必填|说明|
+|---|---|-----|---|
+|base_url|字符串|Y|基本路径,对象桶的名称|
+|form_key|字符串|Y|form-data中的文件键值,如**data**|
+|**data**|字符串|Y|同form_key中配置的值|
+|index|数字|Y|当前文件块的索引,从1开始|
+|total|数字|Y|当前文件总块数|
+|hash|字符串|Y|目标文件的md5,非文件块md5|
+
+## 用到的服务组件
+
+### MYSQL
+
+存储平台已经上传的文件信息,实现文件极速秒传
+
+_上传文件时,把文件的文件名,大小,路径写入file_history表中,如果文件是默认过期的,设置is_persistent字段为2(非持久),反之设置为1(持久),服务初始化时可以检测扫描此字段的值和
+数据创建时间是否大于过期时间实现统一删除,这样可以处理防止服务重启时收不到部分文件过期消息的情况_
+
+### REDIS
+
+储存临时上传的文件hash,并做key过期消息发布。

+ 261 - 0
app/agent/agent.go

@@ -0,0 +1,261 @@
+package agent
+
+import (
+	"context"
+	"errors"
+	"github.com/go-redis/redis/v8"
+	"github.com/gogf/gf/os/grpool"
+	context2 "gxt-file-server/app/context"
+	"gxt-file-server/app/model"
+	"gxt-file-server/app/schema"
+	"gxt-file-server/pkg/logger"
+	"gxt-file-server/pkg/store"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"time"
+)
+
+var (
+	// ErrMissingFile no such file
+	ErrMissingFile = errors.New("no such file")
+	defaultAgent   = NewAgent()
+)
+
+// Agent 文件服务代理
+type Agent struct {
+	config   *Config
+	backend  store.Backend
+	redisCli *redis.Client
+	workPool *grpool.Pool
+	fhm      model.IFileHistory
+}
+type Config struct {
+	DefaultExpireTime int // 默认临时文件过期时间(小时)
+	MaxMemory         int64
+}
+
+const (
+	defaultMaxMemory = 32 << 20 // 32 MB
+)
+
+func DefaultAgent() *Agent {
+	return defaultAgent
+}
+
+func NewAgent() *Agent {
+	return &Agent{
+		workPool: grpool.New(50),
+	}
+}
+
+func (a *Agent) SetConfig(config *Config) {
+	a.config = config
+}
+
+func (a *Agent) SetFileHistoryModel(fhm model.IFileHistory) {
+	a.fhm = fhm
+}
+
+func (a *Agent) SetRedisClient(rdc *redis.Client) {
+	a.redisCli = rdc
+}
+
+func (a *Agent) SetBackend(backend store.Backend) {
+	a.backend = backend
+}
+
+func (a *Agent) maxMemory() int64 {
+	if mm := a.config.MaxMemory; mm > 0 {
+		return mm
+	}
+	return defaultMaxMemory
+}
+
+func (a *Agent) Upload(ctx context.Context, r *http.Request, key string) ([]store.FileInfo, error) {
+	if r.MultipartForm == nil {
+		err := r.ParseMultipartForm(a.maxMemory())
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if r.MultipartForm == nil || r.MultipartForm.File == nil {
+		return nil, ErrMissingFile
+	}
+
+	var infos []store.FileInfo
+	for _, file := range r.MultipartForm.File[key] {
+		info, err := a.doUpload(ctx, file)
+		if err != nil {
+			return infos, err
+		}
+		infos = append(infos, info)
+	}
+	return infos, nil
+}
+
+func (a *Agent) doUpload(ctx context.Context, fheader *multipart.FileHeader) (store.FileInfo, error) {
+
+	file, err := fheader.Open()
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	size, err := a.fileSize(file)
+	if err != nil {
+		return nil, err
+	}
+
+	var fullName string
+	if h, ok := context2.FromFileNameContext(ctx); ok {
+		fullName = h(fheader.Filename)
+	} else {
+		fullName = fheader.Filename
+	}
+	hash, err := a.backend.Store(ctx, fullName, file, size)
+	if err != nil {
+		return nil, err
+	}
+
+	a.SetDefaultExpireTime(ctx, hash, fullName)
+
+	return &fileInfo{
+		fullName: fullName,
+		name:     fheader.Filename,
+		size:     size,
+		hash:     hash,
+	}, nil
+}
+
+//SetDefaultExpireTime redis设置文件过期
+func (a *Agent) SetDefaultExpireTime(ctx context.Context, hash, fullName string) {
+	var defaultExpireTime time.Duration
+	if ctx == nil {
+		ctx = context.Background()
+		defaultExpireTime = time.Duration(a.config.DefaultExpireTime) * time.Hour
+	} else {
+		if v, b := context2.FromFileExpireContext(ctx); b {
+			nV := v.(int)
+			defaultExpireTime = time.Duration(nV) * time.Hour
+		} else {
+			defaultExpireTime = time.Duration(a.config.DefaultExpireTime) * time.Hour
+		}
+	}
+	if defaultExpireTime > 0 {
+		a.redisCli.Set(ctx, hash, fullName, defaultExpireTime)
+	}
+}
+
+func (a *Agent) fileSize(file multipart.File) (int64, error) {
+	var size int64
+
+	if fsize, ok := file.(fsize); ok {
+		size = fsize.Size()
+	} else if fstat, ok := file.(fstat); ok {
+		stat, err := fstat.Stat()
+		if err != nil {
+			return 0, err
+		}
+		size = stat.Size()
+	}
+
+	return size, nil
+}
+
+// Persistent persistent file
+func (a *Agent) Persistent(ctx context.Context, hash string) error {
+	return a.redisCli.Del(ctx, hash).Err()
+}
+
+func (a *Agent) Start(ctx context.Context) error {
+	channel := "__keyevent@0__:expired"
+	pubSub := a.redisCli.PSubscribe(ctx, channel)
+	go func() {
+		_, err := pubSub.Receive(ctx)
+		if err != nil {
+			panic(err)
+		}
+		ch := pubSub.Channel()
+		for msg := range ch {
+			//
+			fileHash := msg.Payload
+			logger.Debugf(ctx, "文件过期删除:[%s]", fileHash)
+			_ = a.workPool.Add(func() {
+				data, err := a.fhm.Query(ctx, schema.FileHistoryQueryParam{FileHash: msg.Payload})
+				if err != nil {
+					return
+				}
+				if len(data.Data) > 0 {
+					v := data.Data[0]
+					_ = a.backend.Delete(ctx, v.Path)
+					_ = a.fhm.Delete(ctx, v.RecordID)
+				}
+			})
+		}
+	}()
+	return nil
+}
+
+func (a *Agent) Get(ctx context.Context, filePath string) ([]byte, string, error) {
+	return a.backend.Get(ctx, filePath)
+}
+
+type fsize interface {
+	Size() int64
+}
+
+type fstat interface {
+	Stat() (os.FileInfo, error)
+}
+
+type fileInfo struct {
+	fullName string
+	name     string
+	size     int64
+	hash     string
+}
+
+func (fi *fileInfo) Hash() string {
+	return fi.hash
+}
+
+func (fi *fileInfo) FullName() string {
+	return fi.fullName
+}
+
+func (fi *fileInfo) Name() string {
+	return fi.name
+}
+
+func (fi *fileInfo) Size() int64 {
+	return fi.size
+}
+
+// ComposeObject 通过使用服务端拷贝实现钭多个源对象合并创建成一个新的对象。
+func (a *Agent) ComposeObject(ctx context.Context, pathS []string, filePath string) error {
+	return a.backend.ComposeObject(ctx, pathS, filePath)
+}
+
+//RemoveObject 删除文件
+func (a *Agent) RemoveObject(ctx context.Context, filePath string) error {
+	return a.backend.RemoveObject(ctx, filePath)
+}
+
+//Stat 查看文件信息
+func (a *Agent) Stat(ctx context.Context, filePath string) (*schema.FileInfo, error) {
+	stat, err := a.backend.Stat(filePath)
+	if err != nil {
+		return nil, err
+	}
+	if stat.Err != nil {
+		return nil, errors.New("未找到文件信息")
+	}
+	return &schema.FileInfo{
+		URL:  filePath,
+		Name: stat.Key,
+		Hash: stat.ETag,
+		Size: stat.Size,
+	}, nil
+}

+ 22 - 0
app/bll/b_demo.go

@@ -0,0 +1,22 @@
+package bll
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+)
+
+// IDemo demo业务逻辑接口
+type IDemo interface {
+	// Query 查询数据
+	Query(ctx context.Context, params schema.DemoQueryParam, opts ...schema.DemoQueryOptions) (*schema.DemoQueryResult, error)
+	// Get 查询指定数据
+	Get(ctx context.Context, recordID string, opts ...schema.DemoQueryOptions) (*schema.Demo, error)
+	// Create 创建数据
+	Create(ctx context.Context, item schema.Demo) (*schema.Demo, error)
+	// Update 更新数据
+	Update(ctx context.Context, recordID string, item schema.Demo) (*schema.Demo, error)
+	// Delete 删除数据
+	Delete(ctx context.Context, recordID string) error
+	// UpdateStatus 更新状态
+	UpdateStatus(ctx context.Context, recordID string, status int) error
+}

+ 20 - 0
app/bll/b_file.go

@@ -0,0 +1,20 @@
+package bll
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+	"net/http"
+)
+
+type IFile interface {
+	// Upload 文件上传
+	Upload(ctx context.Context, r *http.Request, formKey, fileName string) (*schema.FileInfo, error)
+	// Download 文件下载
+	Download(ctx context.Context, filePath string) ([]byte, string, error)
+	// Persistent 持久化文件
+	Persistent(ctx context.Context, fileName string) error
+	// ChunkUpload 文件分块上传
+	ChunkUpload(ctx context.Context, req schema.FileChunkParams) (*schema.FileChunkInfo, error)
+	//FileMerge 文件合并
+	FileMerge(ctx context.Context, req schema.FileMergeParams) (*schema.FileInfo, error)
+}

+ 22 - 0
app/bll/b_file_chunk.go

@@ -0,0 +1,22 @@
+package bll
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+)
+
+// IFileChunk demo业务逻辑接口
+type IFileChunk interface {
+	// Query 查询数据
+	Query(ctx context.Context, params schema.FileChunkQueryParam, opts ...schema.FileChunkQueryOptions) (*schema.FileChunkQueryResult, error)
+	// Get 查询指定数据
+	Get(ctx context.Context, recordID string, opts ...schema.FileChunkQueryOptions) (*schema.FileChunk, error)
+	// Create 创建数据
+	Create(ctx context.Context, item schema.FileChunk) (*schema.FileChunk, error)
+	// Update 更新数据
+	Update(ctx context.Context, recordID string, item schema.FileChunk) (*schema.FileChunk, error)
+	// Delete 删除数据
+	Delete(ctx context.Context, recordID string) error
+	//ClearChunks 清理过期的文件分块
+	ClearChunks(ctx context.Context) error
+}

+ 9 - 0
app/bll/b_trans.go

@@ -0,0 +1,9 @@
+package bll
+
+import "context"
+
+// ITrans 事务管理接口
+type ITrans interface {
+	// Exec 执行事务
+	Exec(ctx context.Context, fn func(context.Context) error) error
+}

+ 18 - 0
app/bll/impl/impl.go

@@ -0,0 +1,18 @@
+package impl
+
+import (
+	"go.uber.org/dig"
+	"gxt-file-server/app/bll"
+	"gxt-file-server/app/bll/impl/internal"
+)
+
+func Inject(container *dig.Container) {
+	_ = container.Provide(internal.NewTrans)
+	_ = container.Provide(func(b *internal.Trans) bll.ITrans { return b })
+	_ = container.Provide(internal.NewDemo)
+	_ = container.Provide(func(b *internal.Demo) bll.IDemo { return b })
+	_ = container.Provide(internal.NewFile)
+	_ = container.Provide(func(b *internal.File) bll.IFile { return b })
+	_ = container.Provide(internal.NewFileChunk)
+	_ = container.Provide(func(b *internal.FileChunk) bll.IFileChunk { return b })
+}

+ 43 - 0
app/bll/impl/internal/b_common.go

@@ -0,0 +1,43 @@
+package internal
+
+import (
+	"context"
+	iContext "gxt-file-server/app/context"
+	"gxt-file-server/app/model"
+)
+
+// TransFunc 定义事务执行函数
+type TransFunc func(context.Context) error
+
+// ExecTrans 执行事务
+func ExecTrans(ctx context.Context, transModel model.ITrans, fn TransFunc) error {
+	if _, ok := iContext.FromTrans(ctx); ok {
+		return fn(ctx)
+	}
+	trans, err := transModel.Begin(ctx)
+	if err != nil {
+		return err
+	}
+
+	defer func() {
+		if r := recover(); r != nil {
+			_ = transModel.Rollback(ctx, trans)
+			panic(r)
+		}
+	}()
+
+	err = fn(iContext.NewTrans(ctx, trans))
+	if err != nil {
+		_ = transModel.Rollback(ctx, trans)
+		return err
+	}
+	return transModel.Commit(ctx, trans)
+}
+
+// ExecTransWithLock 执行事务(加锁)
+func ExecTransWithLock(ctx context.Context, transModel model.ITrans, fn TransFunc) error {
+	if !iContext.FromTransLock(ctx) {
+		ctx = iContext.NewTransLock(ctx)
+	}
+	return ExecTrans(ctx, transModel, fn)
+}

+ 112 - 0
app/bll/impl/internal/b_demo.go

@@ -0,0 +1,112 @@
+package internal
+
+import (
+	"context"
+	"github.com/gogf/gf/util/guid"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model"
+	"gxt-file-server/app/schema"
+)
+
+// NewDemo 创建demo
+func NewDemo(mDemo model.IDemo) *Demo {
+	return &Demo{
+		DemoModel: mDemo,
+	}
+}
+
+// Demo 示例程序
+type Demo struct {
+	DemoModel model.IDemo
+}
+
+// Query 查询数据
+func (a *Demo) Query(ctx context.Context, params schema.DemoQueryParam, opts ...schema.DemoQueryOptions) (*schema.DemoQueryResult, error) {
+	return a.DemoModel.Query(ctx, params, opts...)
+}
+
+// Get 查询指定数据
+func (a *Demo) Get(ctx context.Context, recordID string, opts ...schema.DemoQueryOptions) (*schema.Demo, error) {
+	item, err := a.DemoModel.Get(ctx, recordID, opts...)
+	if err != nil {
+		return nil, err
+	} else if item == nil {
+		return nil, errors.ErrNotFound
+	}
+	return item, nil
+}
+
+func (a *Demo) checkCode(ctx context.Context, code string) error {
+	result, err := a.DemoModel.Query(ctx, schema.DemoQueryParam{
+		Code: code,
+	}, schema.DemoQueryOptions{
+		PageParam: &schema.PaginationParam{PageSize: -1},
+	})
+	if err != nil {
+		return err
+	} else if result.PageResult.Total > 0 {
+		return errors.New400Response("编号已经存在")
+	}
+	return nil
+}
+
+func (a *Demo) getUpdate(ctx context.Context, recordID string) (*schema.Demo, error) {
+	return a.Get(ctx, recordID)
+}
+
+// Create 创建数据
+func (a *Demo) Create(ctx context.Context, item schema.Demo) (*schema.Demo, error) {
+	err := a.checkCode(ctx, item.Code)
+	if err != nil {
+		return nil, err
+	}
+	item.RecordID = guid.S()
+	err = a.DemoModel.Create(ctx, item)
+	if err != nil {
+		return nil, err
+	}
+	return a.getUpdate(ctx, item.RecordID)
+}
+
+// Update 更新数据
+func (a *Demo) Update(ctx context.Context, recordID string, item schema.Demo) (*schema.Demo, error) {
+	oldItem, err := a.DemoModel.Get(ctx, recordID)
+	if err != nil {
+		return nil, err
+	} else if oldItem == nil {
+		return nil, errors.ErrNotFound
+	} else if oldItem.Code != item.Code {
+		err := a.checkCode(ctx, item.Code)
+		if err != nil {
+			return nil, err
+		}
+	}
+	err = a.DemoModel.Update(ctx, recordID, item)
+	if err != nil {
+		return nil, err
+	}
+	return a.getUpdate(ctx, recordID)
+}
+
+// Delete 删除数据
+func (a *Demo) Delete(ctx context.Context, recordID string) error {
+	oldItem, err := a.DemoModel.Get(ctx, recordID)
+	if err != nil {
+		return err
+	} else if oldItem == nil {
+		return errors.ErrNotFound
+	}
+
+	return a.DemoModel.Delete(ctx, recordID)
+}
+
+// UpdateStatus 更新状态
+func (a *Demo) UpdateStatus(ctx context.Context, recordID string, status int) error {
+	oldItem, err := a.DemoModel.Get(ctx, recordID)
+	if err != nil {
+		return err
+	} else if oldItem == nil {
+		return errors.ErrNotFound
+	}
+	return a.DemoModel.UpdateStatus(ctx, recordID, status)
+}

+ 323 - 0
app/bll/impl/internal/b_file.go

@@ -0,0 +1,323 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	"github.com/go-redis/redis/v8"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/util/guid"
+	"gxt-file-server/app/agent"
+	context2 "gxt-file-server/app/context"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model"
+	"gxt-file-server/app/schema"
+	"log"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+type File struct {
+	historyModel model.IFileHistory
+	chunkModel   model.IFileChunk
+	redisCli     *redis.Client
+	transModel   model.ITrans
+}
+
+func NewFile(
+	hm model.IFileHistory,
+	cm model.IFileChunk,
+	redisCli *redis.Client,
+	tm model.ITrans,
+) *File {
+	return &File{
+		historyModel: hm,
+		chunkModel:   cm,
+		redisCli:     redisCli,
+		transModel:   tm,
+	}
+}
+
+//TODO 文件上传时候的文件名是否重命名
+
+// Upload 文件上传
+func (f *File) Upload(ctx context.Context, r *http.Request, formKey, basePath string) (*schema.FileInfo, error) {
+	uuid := guid.S()
+	base := g.Cfg().GetString("agent.DefaultFilePathPrefix")
+	ctx = context2.NewFileNameContext(ctx, func(fileName string) string {
+		return fmt.Sprintf("%s/%s/%s/%s",
+			base, strings.ToLower(basePath), uuid, fileName)
+	})
+	if hash, b := context2.FromFileHashContext(ctx); b {
+		result, err := f.historyModel.Query(ctx, schema.FileHistoryQueryParam{Hash: hash.(string)})
+		if err != nil {
+			return nil, err
+		}
+		if len(result.Data) > 0 {
+			v := result.Data[0]
+			return &schema.FileInfo{
+				URL:  v.Path,
+				Size: v.FileSize,
+				Name: v.FileName,
+				Hash: v.Hash,
+			}, nil
+		}
+	}
+	infos, err := agent.DefaultAgent().Upload(ctx, r, formKey)
+	if err != nil {
+		return nil, err
+	} else if len(infos) == 0 {
+		return nil, nil
+	}
+	fullName := fmt.Sprintf("%s/%s/%s/%s", base, strings.ToLower(basePath), uuid, url.PathEscape(f.getFileName(infos[0].Name())))
+	if fullName[0] != '/' {
+		fullName = "/" + fullName
+	}
+	isPersistent := schema.FALSE
+	if v, b := context2.FromFileExpireContext(ctx); b {
+		nV := v.(int)
+		if nV == -1 {
+			isPersistent = schema.TRUE
+		}
+	}
+	err = f.historyModel.Create(ctx, schema.FileHistory{
+		RecordID:     guid.S(),
+		Hash:         infos[0].Hash(),
+		Path:         fullName,
+		Creator:      "",
+		IsPersistent: isPersistent,
+		FileSize:     infos[0].Size(),
+		FileName:     infos[0].Name(),
+		FileHash:     infos[0].Hash(),
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	info := &schema.FileInfo{
+		URL:  fullName,
+		Name: infos[0].Name(),
+		Size: infos[0].Size(),
+		Hash: infos[0].Hash(),
+	}
+	return info, nil
+}
+
+// Download 获取文件二进制流
+func (f *File) Download(ctx context.Context, filePath string) ([]byte, string, error) {
+	return agent.DefaultAgent().Get(ctx, filePath)
+}
+
+// 修正文件名,将半角 % 替换为全角 %(不替换的话文件将无法从浏览器中打开)
+func (f *File) getFileName(fileName string) string {
+	return strings.ReplaceAll(fileName, "%", "%")
+}
+
+// Persistent 持久化文件
+func (f *File) Persistent(ctx context.Context, hash string) error {
+	if err := agent.DefaultAgent().Persistent(ctx, hash); err != nil {
+		return err
+	}
+
+	result, err := f.historyModel.Query(ctx, schema.FileHistoryQueryParam{
+		Hash:         hash,
+		IsPersistent: schema.FALSE,
+	})
+	if err != nil {
+		return err
+	}
+	if len(result.Data) > 0 {
+		v := result.Data[0]
+		v.IsPersistent = schema.TRUE
+		err := f.historyModel.Update(ctx, v.RecordID, *v)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// ChunkUpload 文件分块上传
+func (f *File) ChunkUpload(ctx context.Context, req schema.FileChunkParams) (*schema.FileChunkInfo, error) {
+	base := g.Cfg().GetString("agent.DefaultFilePathPrefix")
+	chunkBase := g.Cfg().GetString("agent.DefaultChunkExpireTime")
+	ctx = context2.NewFileNameContext(ctx, func(fileName string) string {
+		return fmt.Sprintf("%s/%s/%s/%s_%d",
+			base, strings.ToLower(req.BaseUrl), req.Hash, req.Hash, req.Index)
+	})
+	// check file exist
+	result, err := f.historyModel.Query(ctx, schema.FileHistoryQueryParam{Hash: req.Hash})
+	if err != nil {
+		return nil, err
+	}
+	if len(result.Data) > 0 {
+		v := result.Data[0]
+		return &schema.FileChunkInfo{
+			URL:        v.Path,
+			Name:       v.FileName,
+			Hash:       v.Hash,
+			IsComplete: 2,
+		}, nil
+	}
+	// check file chunk exist
+	chunks, err := f.chunkModel.Query(ctx, schema.FileChunkQueryParam{Hash: req.Hash, Current: req.Index})
+	if err != nil {
+		return nil, err
+	}
+	if len(chunks.Data) > 0 {
+		v := chunks.Data[0]
+		return &schema.FileChunkInfo{
+			Total:      v.Total,
+			Current:    v.Current,
+			URL:        v.Path,
+			Name:       v.Name,
+			Hash:       v.Hash,
+			IsComplete: 1,
+		}, nil
+	}
+	//分块上传不经过redis,每天扫表查询过期的文件块
+	ctx = context2.NewFileExpireContext(ctx, -1)
+	infos, err := agent.DefaultAgent().Upload(ctx, req.HttpRequest, req.FormKey)
+	if err != nil {
+		return nil, err
+	} else if len(infos) == 0 {
+		return nil, nil
+	}
+	fullName := fmt.Sprintf("%s/%s/%s/%s_%d", base, strings.ToLower(req.BaseUrl), req.Hash, req.Hash, req.Index)
+	if fullName[0] != '/' {
+		fullName = "/" + fullName
+	}
+	//设置块的过期时间
+	pastTime, err := time.ParseDuration(chunkBase + "s")
+	if err != nil {
+		return nil, err
+	}
+
+	data := &schema.FileChunk{
+		RecordID: guid.S(),
+		Hash:     req.Hash,
+		Total:    req.Total,
+		Current:  req.Index,
+		Name:     infos[0].Name(),
+		Path:     fullName,
+		Size:     infos[0].Size(),
+		FileHash: infos[0].Hash(),
+		PastTime: time.Now().Add(pastTime),
+	}
+	err = f.chunkModel.Create(ctx, *data)
+	if err != nil {
+		return nil, err
+	}
+	return &schema.FileChunkInfo{
+		Current:    data.Current,
+		Total:      data.Total,
+		URL:        data.Path,
+		Name:       data.Name,
+		Hash:       data.Hash,
+		IsComplete: 1,
+	}, nil
+}
+
+//FileMerge 文件合并
+func (f *File) FileMerge(ctx context.Context, req schema.FileMergeParams) (*schema.FileInfo, error) {
+	if req.Total <= 0 || req.Total > 1000 {
+		//minio中最多可操作1000个对象
+		return nil, errors.New("非法的块数")
+	}
+	// check file exist
+	hisResult, err := f.historyModel.Query(ctx, schema.FileHistoryQueryParam{Hash: req.Hash})
+	if err != nil {
+		return nil, err
+	}
+	if len(hisResult.Data) > 0 {
+		v := hisResult.Data[0]
+		return &schema.FileInfo{
+			URL:  v.Path,
+			Name: v.FileName,
+			Hash: v.Hash,
+			Size: v.FileSize,
+		}, nil
+	}
+	result, err := f.chunkModel.Query(ctx, schema.FileChunkQueryParam{
+		Hash: req.Hash,
+	})
+	if err != nil {
+		return nil, err
+	}
+	if len(result.Data) < req.Total {
+		return nil, errors.New400Response("不完整的文件对象")
+	}
+
+	filePaths := result.Data.FileChunkToPath()
+	base := g.Cfg().GetString("agent.DefaultFilePathPrefix")
+	fullName := fmt.Sprintf("%s/%s/%s/%s", base, req.BaseUrl, req.Hash, req.FileName)
+
+	//分片合并
+	err = agent.DefaultAgent().ComposeObject(ctx, filePaths, fullName)
+	if err != nil {
+		return nil, err
+	}
+
+	//分片删除
+	for i := range filePaths {
+		err = agent.DefaultAgent().RemoveObject(ctx, filePaths[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	fileInfo, err := agent.DefaultAgent().Stat(ctx, fullName)
+	if err != nil {
+		return nil, err
+	}
+
+	err = ExecTrans(ctx, f.transModel, func(ctx context.Context) error {
+		err = f.chunkModel.DeleteHash(ctx, req.Hash)
+		if err != nil {
+			return err
+		}
+
+		return f.historyModel.Create(ctx, schema.FileHistory{
+			RecordID:     guid.S(),
+			Hash:         req.Hash,
+			Path:         fullName,
+			Creator:      "",
+			IsPersistent: 2,
+			FileSize:     fileInfo.Size,
+			FileName:     req.FileName,
+			FileHash:     fileInfo.Hash,
+		})
+	})
+
+	//设置文件过期
+	agent.DefaultAgent().SetDefaultExpireTime(ctx, fileInfo.Hash, fullName)
+
+	if err != nil {
+		return nil, err
+	}
+	return fileInfo, nil
+}
+
+//ClearChunks 清理过期的文件分块
+func (f *File) ClearChunks(ctx context.Context) {
+	results, err := f.chunkModel.Query(ctx, schema.FileChunkQueryParam{
+		IsClear: true,
+	})
+	if err != nil {
+		log.Fatalln(err)
+	}
+	for _, v := range results.Data {
+		//删除文件
+		err = agent.DefaultAgent().RemoveObject(ctx, v.Path)
+		if err != nil {
+			log.Fatalln(err)
+		}
+		//删除数据
+		err = f.chunkModel.Delete(ctx, v.RecordID)
+		if err != nil {
+			log.Fatalln(err)
+			return
+		}
+	}
+}

+ 105 - 0
app/bll/impl/internal/b_file_chunk.go

@@ -0,0 +1,105 @@
+package internal
+
+import (
+	"context"
+	"github.com/gogf/gf/util/guid"
+	"gxt-file-server/app/agent"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model"
+	"gxt-file-server/app/schema"
+	"log"
+)
+
+// NewFileChunk 创建demo
+func NewFileChunk(mFileChunk model.IFileChunk) *FileChunk {
+	return &FileChunk{
+		FileChunkModel: mFileChunk,
+	}
+}
+
+// FileChunk 示例程序
+type FileChunk struct {
+	FileChunkModel model.IFileChunk
+}
+
+// Query 查询数据
+func (a *FileChunk) Query(ctx context.Context, params schema.FileChunkQueryParam, opts ...schema.FileChunkQueryOptions) (*schema.FileChunkQueryResult, error) {
+	return a.FileChunkModel.Query(ctx, params, opts...)
+}
+
+// Get 查询指定数据
+func (a *FileChunk) Get(ctx context.Context, recordID string, opts ...schema.FileChunkQueryOptions) (*schema.FileChunk, error) {
+	item, err := a.FileChunkModel.Get(ctx, recordID, opts...)
+	if err != nil {
+		return nil, err
+	} else if item == nil {
+		return nil, errors.ErrNotFound
+	}
+	return item, nil
+}
+
+func (a *FileChunk) getUpdate(ctx context.Context, recordID string) (*schema.FileChunk, error) {
+	return a.Get(ctx, recordID)
+}
+
+// Create 创建数据
+func (a *FileChunk) Create(ctx context.Context, item schema.FileChunk) (*schema.FileChunk, error) {
+	item.RecordID = guid.S()
+	err := a.FileChunkModel.Create(ctx, item)
+	if err != nil {
+		return nil, err
+	}
+	return a.getUpdate(ctx, item.RecordID)
+}
+
+// Update 更新数据
+func (a *FileChunk) Update(ctx context.Context, recordID string, item schema.FileChunk) (*schema.FileChunk, error) {
+	oldItem, err := a.FileChunkModel.Get(ctx, recordID)
+	if err != nil {
+		return nil, err
+	} else if oldItem == nil {
+		return nil, errors.ErrNotFound
+	}
+	err = a.FileChunkModel.Update(ctx, recordID, item)
+	if err != nil {
+		return nil, err
+	}
+	return a.getUpdate(ctx, recordID)
+}
+
+// Delete 删除数据
+func (a *FileChunk) Delete(ctx context.Context, recordID string) error {
+	oldItem, err := a.FileChunkModel.Get(ctx, recordID)
+	if err != nil {
+		return err
+	} else if oldItem == nil {
+		return errors.ErrNotFound
+	}
+
+	return a.FileChunkModel.Delete(ctx, recordID)
+}
+
+//ClearChunks 清理过期的文件分块
+func (f *FileChunk) ClearChunks(ctx context.Context) error {
+	results, err := f.FileChunkModel.Query(ctx, schema.FileChunkQueryParam{
+		IsClear: true,
+	})
+	if err != nil {
+		return err
+	}
+	log.Printf("共检测出%d块过期文件", len(results.Data))
+	for _, v := range results.Data {
+		//删除文件
+		err = agent.DefaultAgent().RemoveObject(ctx, v.Path)
+		if err != nil {
+			return err
+		}
+		//删除数据
+		err = f.FileChunkModel.Delete(ctx, v.RecordID)
+		if err != nil {
+			return err
+		}
+		log.Printf("删除文件:%v\n", v.Path)
+	}
+	return nil
+}

+ 23 - 0
app/bll/impl/internal/b_trans.go

@@ -0,0 +1,23 @@
+package internal
+
+import (
+	"context"
+	"gxt-file-server/app/model"
+)
+
+// NewTrans 创建事务管理实例
+func NewTrans(trans model.ITrans) *Trans {
+	return &Trans{
+		TransModel: trans,
+	}
+}
+
+// Trans 事务管理
+type Trans struct {
+	TransModel model.ITrans
+}
+
+// Exec 执行事务
+func (a *Trans) Exec(ctx context.Context, fn func(context.Context) error) error {
+	return ExecTrans(ctx, a.TransModel, fn)
+}

+ 118 - 0
app/context/context.go

@@ -0,0 +1,118 @@
+package context
+
+import (
+	"context"
+)
+
+// 定义全局上下文中的键
+type (
+	transCtx     struct{}
+	transLockCtx struct{}
+	userIDCtx    struct{}
+	traceIDCtx   struct{}
+
+	fileSizeLimitKey struct{}
+	fileNameKey      struct{}
+	fileExpireKey    struct{}
+	fileHashKey      struct{}
+
+	// FileSizeLimitHandle file size limit
+	FileSizeLimitHandle func() bool
+
+	// FileNameHandle the file name
+	FileNameHandle func(fileName string) string
+)
+
+// NewTrans 创建事务的上下文
+func NewTrans(ctx context.Context, trans interface{}) context.Context {
+	return context.WithValue(ctx, transCtx{}, trans)
+}
+
+func NewFileExpireContext(ctx context.Context, v interface{}) context.Context {
+	return context.WithValue(ctx, fileExpireKey{}, v)
+}
+
+func NewFileHashContext(ctx context.Context, v interface{}) context.Context {
+	return context.WithValue(ctx, fileHashKey{}, v)
+}
+
+func FromFileHashContext(ctx context.Context) (interface{}, bool) {
+	v := ctx.Value(fileHashKey{})
+	return v, v != nil
+}
+
+func FromFileExpireContext(ctx context.Context) (interface{}, bool) {
+	v := ctx.Value(fileExpireKey{})
+	return v, v != nil
+}
+
+// FromTrans 从上下文中获取事务
+func FromTrans(ctx context.Context) (interface{}, bool) {
+	v := ctx.Value(transCtx{})
+	return v, v != nil
+}
+
+// NewTransLock 创建事务锁的上下文
+func NewTransLock(ctx context.Context) context.Context {
+	return context.WithValue(ctx, transLockCtx{}, struct{}{})
+}
+
+// FromTransLock 从上下文中获取事务锁
+func FromTransLock(ctx context.Context) bool {
+	v := ctx.Value(transLockCtx{})
+	return v != nil
+}
+
+// NewUserID 创建用户ID的上下文
+func NewUserID(ctx context.Context, userID string) context.Context {
+	return context.WithValue(ctx, userIDCtx{}, userID)
+}
+
+// FromUserID 从上下文中获取用户ID
+func FromUserID(ctx context.Context) (string, bool) {
+	v := ctx.Value(userIDCtx{})
+	if v != nil {
+		if s, ok := v.(string); ok {
+			return s, s != ""
+		}
+	}
+	return "", false
+}
+
+// NewTraceID 创建追踪ID的上下文
+func NewTraceID(ctx context.Context, traceID string) context.Context {
+	return context.WithValue(ctx, traceIDCtx{}, traceID)
+}
+
+// FromTraceID 从上下文中获取追踪ID
+func FromTraceID(ctx context.Context) (string, bool) {
+	v := ctx.Value(traceIDCtx{})
+	if v != nil {
+		if s, ok := v.(string); ok {
+			return s, s != ""
+		}
+	}
+	return "", false
+}
+
+// NewFileSizeLimitContext returns a new Context that carries value fsl.
+func NewFileSizeLimitContext(ctx context.Context, fsl FileSizeLimitHandle) context.Context {
+	return context.WithValue(ctx, fileSizeLimitKey{}, fsl)
+}
+
+// FromFileSizeLimitContext returns the FileSizeLimitHandle value stored in ctx, if any.
+func FromFileSizeLimitContext(ctx context.Context) (FileSizeLimitHandle, bool) {
+	handle, ok := ctx.Value(fileSizeLimitKey{}).(FileSizeLimitHandle)
+	return handle, ok
+}
+
+// NewFileNameContext returns a new Context that carries value fn.
+func NewFileNameContext(ctx context.Context, fn FileNameHandle) context.Context {
+	return context.WithValue(ctx, fileNameKey{}, fn)
+}
+
+// FromFileNameContext returns the FileNameHandle value stored in ctx, if any.
+func FromFileNameContext(ctx context.Context) (FileNameHandle, bool) {
+	handle, ok := ctx.Value(fileNameKey{}).(FileNameHandle)
+	return handle, ok
+}

+ 24 - 0
app/errors/error.go

@@ -0,0 +1,24 @@
+package errors
+
+import "github.com/gogf/gf/errors/gerror"
+
+// 定义别名
+var (
+	New       = gerror.New
+	Wrap      = gerror.Wrap
+	Wrapf     = gerror.Wrapf
+	WithStack = gerror.Stack
+)
+
+var (
+	ErrBadRequest = New400Response("请求发生错误")
+
+	ErrNoPerm                = NewResponse(401, "无访问权限", 401)
+	ErrInvalidToken          = NewResponse(9999, "令牌失效", 401)
+	ErrNotFound              = NewResponse(404, "资源不存在", 404)
+	ErrTooManyRequests       = NewResponse(429, "请求过于频繁", 429)
+	ErrInternalServer        = NewResponse(500, "服务器发生错误", 500)
+	ErrDBServerInternalError = NewResponse(50001, "数据库发生错误", 500)
+	ErrHeaderParamsError     = NewResponse(400, "请求头参数格式错误", 400)
+	ErrFileNotFound          = NewResponse(404, "文件不存在", 404)
+)

+ 93 - 0
app/errors/response.go

@@ -0,0 +1,93 @@
+package errors
+
+import "github.com/gogf/gf/net/ghttp"
+
+// ResponseError 响应错误
+type ResponseError struct {
+	Code       int    // 错误码
+	Message    string // 错误消息
+	StatusCode int    // 响应错误码
+	ERR        error  // 响应错误
+}
+
+func (r *ResponseError) Error() string {
+	if r.ERR != nil {
+		return r.ERR.Error()
+	}
+	return r.Message
+}
+
+// UnWrapResponse 解包响应错误
+func UnWrapResponse(err error) *ResponseError {
+	if v, ok := err.(*ResponseError); ok {
+		return v
+	}
+	return nil
+}
+
+// WrapResponse 包装响应错误
+func WrapResponse(err error, code int, msg string, status ...int) error {
+	res := &ResponseError{
+		Code:    code,
+		Message: msg,
+		ERR:     err,
+	}
+	if len(status) > 0 {
+		res.StatusCode = status[0]
+	}
+	return res
+}
+
+// Wrap400Response 包装错误码为400的响应错误
+func Wrap400Response(err error, msg ...string) error {
+	m := "请求发生错误"
+	if len(msg) > 0 {
+		m = msg[0]
+	}
+	return WrapResponse(err, 400, m, 400)
+}
+
+// Wrap500Response 包装错误码为500的响应错误
+func Wrap500Response(err error, msg ...string) error {
+	m := "服务器发生错误"
+	if len(msg) > 0 {
+		m = msg[0]
+	}
+	return WrapResponse(err, 500, m, 500)
+}
+
+// NewResponse 创建响应错误
+func NewResponse(code int, msg string, status ...int) error {
+	res := &ResponseError{
+		Code:    code,
+		Message: msg,
+	}
+	if len(status) > 0 {
+		res.StatusCode = status[0]
+	}
+	return res
+}
+
+// New400Response 创建错误码为400的响应错误
+func New400Response(msg string) error {
+	return NewResponse(400, msg, 400)
+}
+
+// New500Response 创建错误码为500的响应错误
+func New500Response(msg string) error {
+	return NewResponse(500, msg, 500)
+}
+
+func Json(r *ghttp.Request, code int, v interface{}) {
+	if code == 500 || code == 401 {
+		r.Response.WriteStatus(code)
+		r.Response.ClearBuffer()
+	}
+	_ = r.Response.WriteJson(v)
+}
+
+// 返回JSON数据并退出当前HTTP执行函数。
+func JsonExit(r *ghttp.Request, err int, data interface{}) {
+	Json(r, err, data)
+	r.Exit()
+}

+ 0 - 0
app/model/.gitkeep


+ 65 - 0
app/model/entity/e_demo.go

@@ -0,0 +1,65 @@
+package entity
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/schema"
+)
+
+// GetDemoDB 获取demo存储
+func GetDemoDB(ctx context.Context, defDB *gorm.DB) *gorm.DB {
+	return getDBWithModel(ctx, defDB, Demo{})
+}
+
+// SchemaDemo demo对象
+type SchemaDemo schema.Demo
+
+// ToDemo 转换为demo实体
+func (a SchemaDemo) ToDemo() *Demo {
+	item := &Demo{
+		RecordID: a.RecordID,
+		Code:     &a.Code,
+		Name:     &a.Name,
+		Memo:     &a.Memo,
+		Status:   &a.Status,
+		Creator:  &a.Creator,
+	}
+	return item
+}
+
+// ToSchemaDemo 转换为demo对象
+func (a Demo) ToSchemaDemo() *schema.Demo {
+	item := &schema.Demo{
+		RecordID:  a.RecordID,
+		Code:      *a.Code,
+		Name:      *a.Name,
+		Memo:      *a.Memo,
+		Status:    *a.Status,
+		Creator:   *a.Creator,
+		CreatedAt: a.CreatedAt,
+	}
+	return item
+}
+
+// Demo demo实体
+type Demo struct {
+	gorm.Model
+	RecordID string  `gorm:"column:record_id;size:32;index;"` // 记录内码
+	Code     *string `gorm:"column:code;size:50;index;"`      // 编号
+	Name     *string `gorm:"column:name;size:100;index;"`     // 名称
+	Memo     *string `gorm:"column:memo;size:200;"`           // 备注
+	Status   *int    `gorm:"column:status;index;"`            // 状态(1:启用 2:停用)
+	Creator  *string `gorm:"column:creator;size:32;"`         // 创建者
+}
+
+// Demos demo列表
+type Demos []*Demo
+
+// ToSchemaDemos 转换为demo对象列表
+func (a Demos) ToSchemaDemos() []*schema.Demo {
+	list := make([]*schema.Demo, len(a))
+	for i, item := range a {
+		list[i] = item.ToSchemaDemo()
+	}
+	return list
+}

+ 78 - 0
app/model/entity/e_file_chunk.go

@@ -0,0 +1,78 @@
+package entity
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/schema"
+	"time"
+)
+
+// GetFileChunkDB 获取demo存储
+func GetFileChunkDB(ctx context.Context, defDB *gorm.DB) *gorm.DB {
+	return getDBWithModel(ctx, defDB, FileChunk{})
+}
+
+// SchemaFileChunk demo对象
+type SchemaFileChunk schema.FileChunk
+
+// ToFileChunk 转换为demo实体
+func (a SchemaFileChunk) ToFileChunk() *FileChunk {
+	item := &FileChunk{
+		RecordID: a.RecordID,
+		Hash:     a.Hash,
+		Path:     a.Path,
+		Name:     a.Name,
+		Total:    a.Total,
+		Current:  a.Current,
+		Size:     a.Size,
+		Creator:  &a.Creator,
+		FileHash: a.FileHash,
+		PastTime: a.PastTime,
+	}
+	return item
+}
+
+// ToSchemaFileChunk 转换为demo对象
+func (a FileChunk) ToSchemaFileChunk() *schema.FileChunk {
+	item := &schema.FileChunk{
+		RecordID:  a.RecordID,
+		Hash:      a.Hash,
+		Path:      a.Path,
+		Name:      a.Name,
+		Total:     a.Total,
+		Current:   a.Current,
+		Size:      a.Size,
+		Creator:   *a.Creator,
+		CreatedAt: a.CreatedAt,
+		FileHash:  a.FileHash,
+		PastTime:  a.PastTime,
+	}
+	return item
+}
+
+// FileChunk demo实体
+type FileChunk struct {
+	gorm.Model
+	RecordID string    `gorm:"column:record_id;size:32;index;"` // 记录内码
+	Hash     string    `gorm:"column:hash;size:36;index"`
+	FileHash string    `gorm:"column:file_hash;size:36;index"`
+	Path     string    `gorm:"column:path;size:250"`
+	Name     string    `gorm:"column:name;size:200"`
+	Total    int       `gorm:"column:total"`
+	Current  int       `gorm:"column:current"`
+	Size     int64     `gorm:"column:size"`
+	Creator  *string   `gorm:"column:creator;size:32;"` // 创建者
+	PastTime time.Time `gorm:"column:past_time;"`       //文件块过期的时间
+}
+
+// FileChunks demo列表
+type FileChunks []*FileChunk
+
+// ToSchemaFileChunks 转换为demo对象列表
+func (a FileChunks) ToSchemaFileChunks() []*schema.FileChunk {
+	list := make([]*schema.FileChunk, len(a))
+	for i, item := range a {
+		list[i] = item.ToSchemaFileChunk()
+	}
+	return list
+}

+ 71 - 0
app/model/entity/e_file_history.go

@@ -0,0 +1,71 @@
+package entity
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/schema"
+)
+
+// GetFileHistoryDB 获取demo存储
+func GetFileHistoryDB(ctx context.Context, defDB *gorm.DB) *gorm.DB {
+	return getDBWithModel(ctx, defDB, FileHistory{})
+}
+
+// SchemaFileHistory demo对象
+type SchemaFileHistory schema.FileHistory
+
+// ToFileHistory 转换为demo实体
+func (a SchemaFileHistory) ToFileHistory() *FileHistory {
+	item := &FileHistory{
+		RecordID:     a.RecordID,
+		Hash:         a.Hash,
+		Path:         a.Path,
+		FileName:     a.FileName,
+		FileSize:     a.FileSize,
+		IsPersistent: a.IsPersistent,
+		Creator:      &a.Creator,
+		FileHash:     a.FileHash,
+	}
+	return item
+}
+
+// ToSchemaFileHistory 转换为demo对象
+func (a FileHistory) ToSchemaFileHistory() *schema.FileHistory {
+	item := &schema.FileHistory{
+		RecordID:     a.RecordID,
+		Creator:      *a.Creator,
+		Hash:         a.Hash,
+		Path:         a.Path,
+		FileName:     a.FileName,
+		FileSize:     a.FileSize,
+		IsPersistent: a.IsPersistent,
+		CreatedAt:    a.CreatedAt,
+		FileHash:     a.FileHash,
+	}
+	return item
+}
+
+// FileHistory demo实体
+type FileHistory struct {
+	gorm.Model
+	RecordID     string  `gorm:"column:record_id;size:32;index;"` // 记录内码
+	Hash         string  `gorm:"column:hash;index;"`
+	Path         string  `gorm:"column:path;size:250"`
+	FileSize     int64   `gorm:"column:file_size"`
+	FileName     string  `gorm:"column:file_name;size:200"`
+	IsPersistent int     `gorm:"column:is_persistent"`    // 是否持久化
+	Creator      *string `gorm:"column:creator;size:32;"` // 创建者
+	FileHash     string  `gorm:"column:file_hash;"`
+}
+
+// FileHistories demo列表
+type FileHistories []*FileHistory
+
+// ToSchemaFileHistories 转换为demo对象列表
+func (a FileHistories) ToSchemaFileHistories() []*schema.FileHistory {
+	list := make([]*schema.FileHistory, len(a))
+	for i, item := range a {
+		list[i] = item.ToSchemaFileHistory()
+	}
+	return list
+}

+ 25 - 0
app/model/entity/entity.go

@@ -0,0 +1,25 @@
+package entity
+
+import (
+	"context"
+	"gorm.io/gorm"
+	iContext "gxt-file-server/app/context"
+)
+
+func getDB(ctx context.Context, defDB *gorm.DB) *gorm.DB {
+	trans, ok := iContext.FromTrans(ctx)
+	if ok {
+		db, ok := trans.(*gorm.DB)
+		if ok {
+			if iContext.FromTransLock(ctx) {
+				db = db.Set("gorm:query_option", "FOR UPDATE")
+			}
+			return db
+		}
+	}
+	return defDB
+}
+
+func getDBWithModel(ctx context.Context, defDB *gorm.DB, m interface{}) *gorm.DB {
+	return getDB(ctx, defDB).Model(m).WithContext(ctx)
+}

+ 110 - 0
app/model/impl/model/m_demo.go

@@ -0,0 +1,110 @@
+package model
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model/entity"
+	"gxt-file-server/app/schema"
+)
+
+// NewDemo 创建demo存储实例
+func NewDemo(db *gorm.DB) *Demo {
+	return &Demo{db}
+}
+
+// Demo demo存储
+type Demo struct {
+	db *gorm.DB
+}
+
+func (a *Demo) getQueryOption(opts ...schema.DemoQueryOptions) schema.DemoQueryOptions {
+	var opt schema.DemoQueryOptions
+	if len(opts) > 0 {
+		opt = opts[0]
+	}
+	return opt
+}
+
+// Query 查询数据
+func (a *Demo) Query(ctx context.Context, params schema.DemoQueryParam, opts ...schema.DemoQueryOptions) (*schema.DemoQueryResult, error) {
+	db := entity.GetDemoDB(ctx, a.db)
+	if v := params.Code; v != "" {
+		db = db.Where("code=?", v)
+	}
+	if v := params.LikeCode; v != "" {
+		db = db.Where("code LIKE ?", "%"+v+"%")
+	}
+	if v := params.LikeName; v != "" {
+		db = db.Where("name LIKE ?", "%"+v+"%")
+	}
+	if v := params.Status; v > 0 {
+		db = db.Where("status=?", v)
+	}
+	db = db.Order("id DESC")
+
+	opt := a.getQueryOption(opts...)
+	var list entity.Demos
+	pr, err := WrapPageQuery(ctx, db, opt.PageParam, &list)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	}
+	qr := &schema.DemoQueryResult{
+		PageResult: pr,
+		Data:       list.ToSchemaDemos(),
+	}
+
+	return qr, nil
+}
+
+// Get 查询指定数据
+func (a *Demo) Get(ctx context.Context, recordID string, opts ...schema.DemoQueryOptions) (*schema.Demo, error) {
+	db := entity.GetDemoDB(ctx, a.db).Where("record_id=?", recordID)
+	var item entity.Demo
+	ok, err := FindOne(ctx, db, &item)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	} else if !ok {
+		return nil, nil
+	}
+
+	return item.ToSchemaDemo(), nil
+}
+
+// Create 创建数据
+func (a *Demo) Create(ctx context.Context, item schema.Demo) error {
+	demo := entity.SchemaDemo(item).ToDemo()
+	result := entity.GetDemoDB(ctx, a.db).Create(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Update 更新数据
+func (a *Demo) Update(ctx context.Context, recordID string, item schema.Demo) error {
+	demo := entity.SchemaDemo(item).ToDemo()
+	result := entity.GetDemoDB(ctx, a.db).Where("record_id=?", recordID).Omit("record_id", "creator").Updates(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Delete 删除数据
+func (a *Demo) Delete(ctx context.Context, recordID string) error {
+	result := entity.GetDemoDB(ctx, a.db).Where("record_id=?", recordID).Delete(entity.Demo{})
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// UpdateStatus 更新状态
+func (a *Demo) UpdateStatus(ctx context.Context, recordID string, status int) error {
+	result := entity.GetDemoDB(ctx, a.db).Where("record_id=?", recordID).Update("status", status)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}

+ 120 - 0
app/model/impl/model/m_file_chunk.go

@@ -0,0 +1,120 @@
+package model
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model/entity"
+	"gxt-file-server/app/schema"
+	"time"
+)
+
+// NewFileChunk 创建demo存储实例
+func NewFileChunk(db *gorm.DB) *FileChunk {
+	return &FileChunk{db}
+}
+
+// FileChunk demo存储
+type FileChunk struct {
+	db *gorm.DB
+}
+
+func (a *FileChunk) getQueryOption(opts ...schema.FileChunkQueryOptions) schema.FileChunkQueryOptions {
+	var opt schema.FileChunkQueryOptions
+	if len(opts) > 0 {
+		opt = opts[0]
+	}
+	return opt
+}
+
+// Query 查询数据
+func (a *FileChunk) Query(ctx context.Context, params schema.FileChunkQueryParam, opts ...schema.FileChunkQueryOptions) (*schema.FileChunkQueryResult, error) {
+	db := entity.GetFileChunkDB(ctx, a.db)
+	if v := params.Hash; v != "" {
+		db = db.Where("hash = ?", v)
+	}
+	if v := params.Current; v > 0 {
+		db = db.Where("current = ?", v)
+	}
+	if v := params.IsClear; v {
+		//精确到秒 已备定时扫表的定时任务为秒/分钟
+		timeNowString := time.Now().Format("2006-01-02 15:04:05")
+		db = db.Where("past_time < ?", timeNowString)
+	}
+	//按照块的顺序排序 不可变更 !!! guo
+	db = db.Order("current ASC")
+
+	opt := a.getQueryOption(opts...)
+	var list entity.FileChunks
+	pr, err := WrapPageQuery(ctx, db, opt.PageParam, &list)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	}
+	qr := &schema.FileChunkQueryResult{
+		PageResult: pr,
+		Data:       list.ToSchemaFileChunks(),
+	}
+
+	return qr, nil
+}
+
+// Get 查询指定数据
+func (a *FileChunk) Get(ctx context.Context, recordID string, opts ...schema.FileChunkQueryOptions) (*schema.FileChunk, error) {
+	db := entity.GetFileChunkDB(ctx, a.db).Where("record_id=?", recordID)
+	var item entity.FileChunk
+	ok, err := FindOne(ctx, db, &item)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	} else if !ok {
+		return nil, nil
+	}
+
+	return item.ToSchemaFileChunk(), nil
+}
+
+// Create 创建数据
+func (a *FileChunk) Create(ctx context.Context, item schema.FileChunk) error {
+	demo := entity.SchemaFileChunk(item).ToFileChunk()
+	result := entity.GetFileChunkDB(ctx, a.db).Create(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Update 更新数据
+func (a *FileChunk) Update(ctx context.Context, recordID string, item schema.FileChunk) error {
+	demo := entity.SchemaFileChunk(item).ToFileChunk()
+	result := entity.GetFileChunkDB(ctx, a.db).Where("record_id=?", recordID).Omit("record_id", "creator").Updates(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Delete 真实删除数据
+func (a *FileChunk) Delete(ctx context.Context, recordID string) error {
+	result := entity.GetFileChunkDB(ctx, a.db).Where("record_id=?", recordID).Unscoped().Delete(entity.FileChunk{})
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// DeleteHash 根据HASH真实删除数据
+func (a *FileChunk) DeleteHash(ctx context.Context, hash string) error {
+	result := entity.GetFileChunkDB(ctx, a.db).Where("hash=?", hash).Unscoped().Delete(entity.FileChunk{})
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// UpdateStatus 更新状态
+func (a *FileChunk) UpdateStatus(ctx context.Context, recordID string, status int) error {
+	result := entity.GetFileChunkDB(ctx, a.db).Where("record_id=?", recordID).Update("status", status)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}

+ 101 - 0
app/model/impl/model/m_file_history.go

@@ -0,0 +1,101 @@
+package model
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/model/entity"
+	"gxt-file-server/app/schema"
+)
+
+// NewFileHistory 创建demo存储实例
+func NewFileHistory(db *gorm.DB) *FileHistory {
+	return &FileHistory{db}
+}
+
+// FileHistory demo存储
+type FileHistory struct {
+	db *gorm.DB
+}
+
+func (a *FileHistory) getQueryOption(opts ...schema.FileHistoryQueryOptions) schema.FileHistoryQueryOptions {
+	var opt schema.FileHistoryQueryOptions
+	if len(opts) > 0 {
+		opt = opts[0]
+	}
+	return opt
+}
+
+// Query 查询数据
+func (a *FileHistory) Query(ctx context.Context, params schema.FileHistoryQueryParam, opts ...schema.FileHistoryQueryOptions) (*schema.FileHistoryQueryResult, error) {
+	db := entity.GetFileHistoryDB(ctx, a.db)
+	if v := params.FileName; v != "" {
+		db = db.Where("file_name = ?", v)
+	}
+	if v := params.Hash; v != "" {
+		db = db.Where("hash = ?", v)
+	}
+	if v := params.FileHash; v != "" {
+		db = db.Where("file_hash = ?", v)
+	}
+	if v := params.IsPersistent; v > 0 {
+		db = db.Where("is_persistent = ?", v)
+	}
+	db = db.Order("id DESC")
+
+	opt := a.getQueryOption(opts...)
+	var list entity.FileHistories
+	pr, err := WrapPageQuery(ctx, db, opt.PageParam, &list)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	}
+	qr := &schema.FileHistoryQueryResult{
+		PageResult: pr,
+		Data:       list.ToSchemaFileHistories(),
+	}
+
+	return qr, nil
+}
+
+// Get 查询指定数据
+func (a *FileHistory) Get(ctx context.Context, recordID string, opts ...schema.FileHistoryQueryOptions) (*schema.FileHistory, error) {
+	db := entity.GetFileHistoryDB(ctx, a.db).Where("record_id=?", recordID)
+	var item entity.FileHistory
+	ok, err := FindOne(ctx, db, &item)
+	if err != nil {
+		return nil, errors.ErrDBServerInternalError
+	} else if !ok {
+		return nil, nil
+	}
+
+	return item.ToSchemaFileHistory(), nil
+}
+
+// Create 创建数据
+func (a *FileHistory) Create(ctx context.Context, item schema.FileHistory) error {
+	demo := entity.SchemaFileHistory(item).ToFileHistory()
+	result := entity.GetFileHistoryDB(ctx, a.db).Create(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Delete 删除数据
+func (a *FileHistory) Delete(ctx context.Context, recordId string) error {
+	result := entity.GetFileHistoryDB(ctx, a.db).Where("record_id =?", recordId).Unscoped().Delete(entity.FileHistory{})
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Update 更新数据
+func (a *FileHistory) Update(ctx context.Context, recordID string, item schema.FileHistory) error {
+	demo := entity.SchemaFileHistory(item).ToFileHistory()
+	result := entity.GetFileHistoryDB(ctx, a.db).Where("record_id=?", recordID).Omit("record_id", "creator").Updates(demo)
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}

+ 54 - 0
app/model/impl/model/m_trans.go

@@ -0,0 +1,54 @@
+package model
+
+import (
+	"context"
+	"gorm.io/gorm"
+	"gxt-file-server/app/errors"
+)
+
+// NewTrans 创建事务管理实例
+func NewTrans(db *gorm.DB) *Trans {
+	return &Trans{db}
+}
+
+// Trans 事务管理
+type Trans struct {
+	db *gorm.DB
+}
+
+// Begin 开启事务
+func (a *Trans) Begin(ctx context.Context) (interface{}, error) {
+	result := a.db.Begin()
+	if err := result.Error; err != nil {
+		return nil, errors.ErrDBServerInternalError
+	}
+	return result, nil
+}
+
+// Commit 提交事务
+func (a *Trans) Commit(ctx context.Context, trans interface{}) error {
+	db, ok := trans.(*gorm.DB)
+	if !ok {
+		return errors.New("unknow trans")
+	}
+
+	result := db.Commit()
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}
+
+// Rollback 回滚事务
+func (a *Trans) Rollback(ctx context.Context, trans interface{}) error {
+	db, ok := trans.(*gorm.DB)
+	if !ok {
+		return errors.New("unknow trans")
+	}
+
+	result := db.Rollback()
+	if err := result.Error; err != nil {
+		return errors.ErrDBServerInternalError
+	}
+	return nil
+}

+ 123 - 0
app/model/impl/model/model.go

@@ -0,0 +1,123 @@
+package model
+
+import (
+	"context"
+	"gorm.io/gorm"
+	iContext "gxt-file-server/app/context"
+	"gxt-file-server/app/schema"
+)
+
+// TransFunc 定义事务执行函数
+type TransFunc func(context.Context) error
+
+// ExecTrans 执行事务
+func ExecTrans(ctx context.Context, db *gorm.DB, fn TransFunc) error {
+	if _, ok := iContext.FromTrans(ctx); ok {
+		return fn(ctx)
+	}
+
+	transModel := NewTrans(db)
+	trans, err := transModel.Begin(ctx)
+	if err != nil {
+		return err
+	}
+
+	defer func() {
+		if r := recover(); r != nil {
+			_ = transModel.Rollback(ctx, trans)
+			panic(r)
+		}
+	}()
+
+	ctx = iContext.NewTrans(ctx, trans)
+	err = fn(ctx)
+	if err != nil {
+		_ = transModel.Rollback(ctx, trans)
+		return err
+	}
+	return transModel.Commit(ctx, trans)
+}
+
+// ExecTransWithLock 执行事务(加锁)
+func ExecTransWithLock(ctx context.Context, db *gorm.DB, fn TransFunc) error {
+	if !iContext.FromTransLock(ctx) {
+		ctx = iContext.NewTransLock(ctx)
+	}
+	return ExecTrans(ctx, db, fn)
+}
+
+// WrapPageQuery 包装带有分页的查询
+func WrapPageQuery(ctx context.Context, db *gorm.DB, pp *schema.PaginationParam, out interface{}) (*schema.PaginationResult, error) {
+	if pp != nil {
+		total, err := FindPage(ctx, db, pp.PageIndex, pp.PageSize, out)
+		if err != nil {
+			return nil, err
+		}
+		return &schema.PaginationResult{
+			Total: total,
+		}, nil
+	}
+
+	result := db.Find(out)
+	return nil, result.Error
+}
+
+// FindPage 查询分页数据
+func FindPage(ctx context.Context, db *gorm.DB, pageIndex, pageSize int, out interface{}) (int, error) {
+	selectTmp := db.Statement.Clauses["SELECT"]
+	var count int64
+	result := db.Count(&count)
+	db.Statement.Clauses["SELECT"] = selectTmp
+	if err := result.Error; err != nil {
+		return 0, err
+	} else if count == 0 {
+		return 0, nil
+	}
+
+	// 如果分页大小小于0或者分页索引小于0,则不查询数据
+	if pageSize < 0 || pageIndex < 0 {
+		return int(count), nil
+	}
+
+	if pageIndex > 0 && pageSize > 0 {
+		db = db.Offset((pageIndex - 1) * pageSize)
+	}
+	if pageSize > 0 {
+		db = db.Limit(pageSize)
+	}
+	result = db.Find(out)
+	if err := result.Error; err != nil {
+		return 0, err
+	}
+
+	return int(count), nil
+}
+
+// FindOne 查询单条数据
+func FindOne(ctx context.Context, db *gorm.DB, out interface{}) (bool, error) {
+	result := db.First(out)
+	if err := result.Error; err != nil {
+		if err == gorm.ErrRecordNotFound {
+			return false, nil
+		}
+		return false, err
+	}
+	return true, nil
+}
+
+// Check 检查数据是否存在
+func Check(ctx context.Context, db *gorm.DB) (bool, error) {
+	var count int64
+	result := db.Count(&count)
+	if err := result.Error; err != nil {
+		return false, err
+	}
+	return count > 0, nil
+}
+
+const maxNumberOfBatchParameters = 65535
+
+// OwnDb 实例化DB
+type OwnDb struct {
+	*gorm.DB
+}

+ 22 - 0
app/model/m_demo.go

@@ -0,0 +1,22 @@
+package model
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+)
+
+// IDemo demo存储接口
+type IDemo interface {
+	// Query 查询数据
+	Query(ctx context.Context, params schema.DemoQueryParam, opts ...schema.DemoQueryOptions) (*schema.DemoQueryResult, error)
+	// Get 查询指定数据
+	Get(ctx context.Context, recordID string, opts ...schema.DemoQueryOptions) (*schema.Demo, error)
+	// Create 创建数据
+	Create(ctx context.Context, item schema.Demo) error
+	// Update 更新数据
+	Update(ctx context.Context, recordID string, item schema.Demo) error
+	// Delete 删除数据
+	Delete(ctx context.Context, recordID string) error
+	// UpdateStatus 更新状态
+	UpdateStatus(ctx context.Context, recordID string, status int) error
+}

+ 22 - 0
app/model/m_file_chunk.go

@@ -0,0 +1,22 @@
+package model
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+)
+
+// IFileChunk demo存储接口
+type IFileChunk interface {
+	// Query 查询数据
+	Query(ctx context.Context, params schema.FileChunkQueryParam, opts ...schema.FileChunkQueryOptions) (*schema.FileChunkQueryResult, error)
+	// Get 查询指定数据
+	Get(ctx context.Context, recordID string, opts ...schema.FileChunkQueryOptions) (*schema.FileChunk, error)
+	// Create 创建数据
+	Create(ctx context.Context, item schema.FileChunk) error
+	// Update 更新数据
+	Update(ctx context.Context, recordID string, item schema.FileChunk) error
+	// Delete 真实删除数据
+	Delete(ctx context.Context, recordID string) error
+	//DeleteHash 根据HASH真实删除数据
+	DeleteHash(ctx context.Context, hash string) error
+}

+ 20 - 0
app/model/m_file_history.go

@@ -0,0 +1,20 @@
+package model
+
+import (
+	"context"
+	"gxt-file-server/app/schema"
+)
+
+// IFileHistory demo存储接口
+type IFileHistory interface {
+	// Query 查询数据
+	Query(ctx context.Context, params schema.FileHistoryQueryParam, opts ...schema.FileHistoryQueryOptions) (*schema.FileHistoryQueryResult, error)
+	// Get 查询指定数据
+	Get(ctx context.Context, recordID string, opts ...schema.FileHistoryQueryOptions) (*schema.FileHistory, error)
+	// Create 创建数据
+	Create(ctx context.Context, item schema.FileHistory) error
+	// Delete 删除数据
+	Delete(ctx context.Context, recordID string) error
+	// Update 更新数据
+	Update(ctx context.Context, recordId string, data schema.FileHistory) error
+}

+ 13 - 0
app/model/m_trans.go

@@ -0,0 +1,13 @@
+package model
+
+import "context"
+
+// ITrans 事务管理接口
+type ITrans interface {
+	// Begin 开始事务
+	Begin(ctx context.Context) (interface{}, error)
+	// Commit 提交事务
+	Commit(ctx context.Context, trans interface{}) error
+	// Rollback 回滚事务
+	Rollback(ctx context.Context, trans interface{}) error
+}

+ 33 - 0
app/schema/s_demo.go

@@ -0,0 +1,33 @@
+package schema
+
+import "time"
+
+// Demo demo对象
+type Demo struct {
+	RecordID  string    `json:"record_id"`                       // 记录ID
+	Code      string    `json:"code" v:"required"`               // 编号
+	Name      string    `json:"name" v:"required"`               // 名称
+	Memo      string    `json:"memo"`                            // 备注
+	Status    int       `json:"status" v:"required|max:2|min:1"` // 状态(1:启用 2:停用)
+	Creator   string    `json:"creator"`                         // 创建者
+	CreatedAt time.Time `json:"created_at"`                      // 创建时间
+}
+
+// DemoQueryParam 查询条件
+type DemoQueryParam struct {
+	Code     string `form:"-"`        // 编号
+	Status   int    `form:"status"`   // 状态(1:启用 2:停用)
+	LikeCode string `form:"likeCode"` // 编号(模糊查询)
+	LikeName string `form:"likeName"` // 名称(模糊查询)
+}
+
+// DemoQueryOptions demo对象查询可选参数项
+type DemoQueryOptions struct {
+	PageParam *PaginationParam // 分页参数
+}
+
+// DemoQueryResult demo对象查询结果
+type DemoQueryResult struct {
+	Data       []*Demo
+	PageResult *PaginationResult
+}

+ 58 - 0
app/schema/s_file.go

@@ -0,0 +1,58 @@
+package schema
+
+import "net/http"
+
+// FileInfo 文件信息
+type FileInfo struct {
+	URL  string `json:"url"`
+	Name string `json:"name"`
+	Hash string `json:"hash"`
+	Size int64  `json:"size"`
+}
+
+// FileChunkInfo 文件块信息
+type FileChunkInfo struct {
+	Current    int    `json:"current"`     // 当前块
+	Total      int    `json:"total"`       // 总块数
+	URL        string `json:"url"`         // 路径
+	Name       string `json:"name"`        // 文件名
+	Hash       string `json:"hash"`        // 文件hash
+	IsComplete int    `json:"is_complete"` // 是否完成上传(1:未完成2:已完成)
+}
+
+// UploadParams 上传文件时的参数
+type UploadParams struct {
+	BaseUrl string `json:"base_url" v:"required#必须输入base_url"`
+	FormKey string `json:"form_key" v:"required#必须输入file字段的名称form_key"`
+}
+
+type PersistentFileRequest struct {
+	Hash string `json:"hash" v:"required#必须输入hash"` // 文件的哈希值
+}
+
+type FileChunkUploadReq struct {
+	BaseUrl string `json:"base_url" v:"required#必须输入base_url"`
+	FormKey string `json:"form_key" v:"required#必须输入file字段的名称form_key"`
+	Index   int    `json:"index" v:"required#当前块索引不能为空(index)|min:1"`
+	Total   int    `json:"total" v:"required#必须传总块数total|min:1"`
+	Hash    string `json:"hash" v:"required#必须输入文件的md5值"`
+}
+
+// FileChunkParams 文件分块上传参数
+type FileChunkParams struct {
+	HttpRequest *http.Request // http request object
+	FormKey     string        // form-data file key
+	BaseUrl     string        // base url like bucket name
+	Index       int           // current file chunk index
+	Total       int           // total chunks
+	Hash        string
+}
+
+// FileMergeParams 文件合并参数
+type FileMergeParams struct {
+	HttpRequest *http.Request // http request object
+	BaseUrl     string        // base url like bucket name
+	Total       int           // total chunks
+	Hash        string
+	FileName    string
+}

+ 47 - 0
app/schema/s_file_chunk.go

@@ -0,0 +1,47 @@
+package schema
+
+import "time"
+
+// FileChunk 文件分块
+type FileChunk struct {
+	RecordID  string    `json:"record_id"`  // 记录ID
+	Hash      string    `json:"hash"`       //
+	Total     int       `json:"total"`      // 文件块总数
+	Current   int       `json:"current"`    // 当前上传的块数
+	Name      string    `json:"name"`       // 文件名
+	Path      string    `json:"path"`       // 文件路径
+	Size      int64     `json:"size"`       // 文件大小
+	Creator   string    `json:"creator"`    // 创建者
+	CreatedAt time.Time `json:"created_at"` // 创建时间
+	FileHash  string    `json:"file_hash"`  // 文件的md5
+	PastTime  time.Time `json:"past_time"`  //文件块过期的时间
+}
+
+// FileChunkQueryParam 查询条件
+type FileChunkQueryParam struct {
+	Hash    string `form:"hash"`
+	Current int    `form:"current"`
+	Name    string `form:"name"`
+	IsClear bool   `form:"is_clear"`
+}
+
+// FileChunkQueryOptions demo对象查询可选参数项
+type FileChunkQueryOptions struct {
+	PageParam *PaginationParam // 分页参数
+}
+
+type FileChunkS []*FileChunk
+
+// FileChunkQueryResult demo对象查询结果
+type FileChunkQueryResult struct {
+	Data       FileChunkS
+	PageResult *PaginationResult
+}
+
+func (a FileChunkS) FileChunkToPath() []string {
+	pathS := make([]string, 0, len(a))
+	for i := range a {
+		pathS = append(pathS, a[i].Path)
+	}
+	return pathS
+}

+ 40 - 0
app/schema/s_file_history.go

@@ -0,0 +1,40 @@
+package schema
+
+import "time"
+
+// FileHistory demo对象
+type FileHistory struct {
+	RecordID     string    `json:"record_id"`     // 记录ID
+	Hash         string    `json:"hash"`          // 文件hash
+	Path         string    `json:"path"`          // 路径
+	Creator      string    `json:"creator"`       // 创建者,三方服务
+	FileSize     int64     `json:"file_size"`     // 文件大小
+	FileName     string    `json:"file_name"`     // 文件名
+	IsPersistent int       `json:"is_persistent"` // 是否固化(1:持久化2:临时)
+	CreatedAt    time.Time `json:"created_at"`
+	FileHash     string    `json:"file_hash"` //文件hash
+}
+
+const (
+	TRUE  = 1
+	FALSE = 2
+)
+
+// FileHistoryQueryParam 查询条件
+type FileHistoryQueryParam struct {
+	Hash         string `form:"hash"`
+	FileName     string `form:"file_name"`
+	IsPersistent int    `form:"is_persistent"`
+	FileHash     string `form:"file_hash"` //文件hash
+}
+
+// FileHistoryQueryOptions demo对象查询可选参数项
+type FileHistoryQueryOptions struct {
+	PageParam *PaginationParam // 分页参数
+}
+
+// FileHistoryQueryResult demo对象查询结果
+type FileHistoryQueryResult struct {
+	Data       []*FileHistory
+	PageResult *PaginationResult
+}

+ 54 - 0
app/schema/schema.go

@@ -0,0 +1,54 @@
+package schema
+
+// HTTPStatusText 定义HTTP状态文本
+type HTTPStatusText string
+
+func (t HTTPStatusText) String() string {
+	return string(t)
+}
+
+// HTTPError HTTP响应错误
+type HTTPError struct {
+	Error HTTPErrorItem `json:"error"` // 错误项
+}
+
+// HTTPErrorItem HTTP响应错误项
+type HTTPErrorItem struct {
+	Code    int    `json:"code"`     // 错误码
+	Message string `json:"message"`  // 错误信息
+	TraceId string `json:"trace_id"` // 追踪Id,用于快速定位错误
+}
+
+// HTTPStatus HTTP响应状态
+type HTTPStatus struct {
+	Status string `json:"status"` // 状态(OK)
+}
+
+// HTTPList HTTP响应列表数据
+type HTTPList struct {
+	List       interface{}     `json:"list"`
+	Pagination *HTTPPagination `json:"pagination,omitempty"`
+}
+
+// HTTPPagination HTTP分页数据
+type HTTPPagination struct {
+	Total    int `json:"total"`
+	Current  int `json:"current"`
+	PageSize int `json:"pageSize"`
+}
+
+// PaginationParam 分页查询条件
+type PaginationParam struct {
+	PageIndex int // 页索引
+	PageSize  int // 页大小
+}
+
+// PaginationResult 分页查询结果
+type PaginationResult struct {
+	Total int // 总数据条数
+}
+
+//VerifyToken 验证码令牌
+type VerifyToken struct {
+	Token string `json:"token"` //令牌
+}

+ 202 - 0
boot/boot.go

@@ -0,0 +1,202 @@
+package boot
+
+import (
+	"context"
+	"fmt"
+	redisLib "github.com/go-redis/redis/v8"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/net/ghttp"
+	"github.com/gogf/gf/os/gcron"
+	"go.uber.org/dig"
+	"gorm.io/gorm"
+	agent2 "gxt-file-server/app/agent"
+	"gxt-file-server/app/bll"
+	"gxt-file-server/app/bll/impl"
+	"gxt-file-server/app/model"
+	"gxt-file-server/pkg/gplus"
+	"gxt-file-server/pkg/logger"
+	"gxt-file-server/pkg/middleware"
+	"gxt-file-server/pkg/redis"
+	"gxt-file-server/pkg/store"
+	"gxt-file-server/pkg/utils"
+	"gxt-file-server/router"
+	"log"
+	"os"
+)
+
+// VERSION 定义应用版本号
+const VERSION = "1.0.0"
+
+func init() {
+	// 初始化logger
+	logger.Init(g.Cfg().GetString("common.run_mode"))
+	logger.SetVersion(VERSION)
+	logger.SetTraceIdFunc(utils.NewTraceID)
+	ctx := logger.NewTraceIDContext(context.Background(), utils.NewTraceID())
+	Init(ctx)
+}
+
+// Init 初始化App,
+func Init(ctx context.Context) func() {
+	logger.Printf(ctx, "服务启动,运行模式:%s,版本号:%s,进程号:%d", g.Cfg().Get("common.run_mode"), VERSION, os.Getpid())
+	// 初始化依赖注入容器
+	container, call := buildContainer(ctx)
+	// 初始化路由注册
+	s := g.Server()
+	// 每个请求生成新的追踪Id,如果上下文件中没有trace-id
+	s.Use(middleware.TraceIdMiddleware())
+	// file middleware
+	filePrefix := fmt.Sprintf("/%s/", g.Cfg().GetString("agent.DefaultFilePathPrefix"))
+	s.Use(middleware.FileMiddleWare(middleware.AllowPathPrefixNoSkipper(filePrefix)))
+	// 统一处理内部错误
+	s.Use(func(r *ghttp.Request) {
+		r.Middleware.Next()
+		if err := r.GetError(); err != nil {
+			gplus.ResError(r, err)
+		}
+	})
+	initCron(ctx, container)
+	router.InitRouters(s, container)
+	return func() {
+		if call != nil {
+			call()
+		}
+	}
+
+}
+
+// 初始化redis
+func initRedis(ctx context.Context, container *dig.Container) func() {
+	addr := g.Cfg().GetString("redis.addr")
+	password := g.Cfg().GetString("redis.password")
+	db := g.Cfg().GetInt("redis.db")
+	redisCli := redis.Init(ctx, addr, password, db)
+	redisCli.ConfigSet(ctx, "set notify-keyspace-events", "Ex")
+	logger.Printf(ctx, "REDIS初始化成功,当前服务器[%s, %d]", addr, db)
+	// 注入redis client
+	_ = container.Provide(func() *redisLib.Client {
+		return redisCli
+	})
+	return func() {
+		_ = redisCli.Close()
+	}
+}
+
+func initAgent(ctx context.Context, container *dig.Container) func() {
+	_ = container.Invoke(func(rCli *redisLib.Client, fhm model.IFileHistory) {
+		addr := g.Cfg().GetString("minio.addr")
+		aKey := g.Cfg().GetString("minio.assess_key")
+		sKey := g.Cfg().GetString("minio.secret_key")
+		m := store.MiniStoreInit(addr, aKey, sKey)
+		agent2.DefaultAgent().SetBackend(m)
+		agent2.DefaultAgent().SetConfig(&agent2.Config{
+			DefaultExpireTime: g.Cfg().GetInt("agent.DefaultExpireTime"),
+		})
+		agent2.DefaultAgent().SetRedisClient(rCli)
+		agent2.DefaultAgent().SetFileHistoryModel(fhm)
+		_ = agent2.DefaultAgent().Start(ctx)
+	})
+	return func() {
+
+	}
+}
+
+// 初始化存储,目前只初始化gorm
+func initStore(ctx context.Context, container *dig.Container) (func(), error) {
+	var storeCall func()
+	db, err := initGorm()
+	if err != nil {
+		return storeCall, err
+	}
+	// 如果自动映射数据表
+	if g.Cfg().GetBool("gorm.enable_auto_migrate") {
+		err = autoMigrate(db)
+		if err != nil {
+			return storeCall, err
+		}
+	}
+	// 注入DB
+	_ = container.Provide(func() *gorm.DB { return db })
+	// 注入model接口
+	_ = InjectModel(container)
+	storeCall = func() {
+		sqlDb, _ := db.DB()
+		_ = sqlDb.Close()
+	}
+	logger.Printf(ctx, "MYSQL初始化成功, 服务器[%s], 数据库[%s]",
+		g.Cfg().GetString("mysql.host"),
+		g.Cfg().GetString("mysql.db_name"))
+	return storeCall, nil
+}
+
+// 构建依赖注入容器
+func buildContainer(ctx context.Context) (*dig.Container, func()) {
+	container := dig.New()
+	// 初始化存储模块
+	storeCall, err := initStore(ctx, container)
+	if err != nil {
+		panic(err)
+	}
+	// 初始化redis模块
+	var redisCall func()
+	if g.Cfg().GetBool("redis.enable") {
+		redisCall = initRedis(ctx, container)
+	}
+	agentCall := initAgent(ctx, container)
+	// 注入bll
+	impl.Inject(container)
+	return container, func() {
+		if storeCall != nil {
+			storeCall()
+		}
+		if redisCall != nil {
+			redisCall()
+		}
+		if agentCall != nil {
+			agentCall()
+		}
+	}
+}
+
+func initCron(ctx context.Context, container *dig.Container) {
+	//秒 分 时  日 月 周
+	runMode := g.Cfg().GetString("common.run_mode")
+	_ = container.Invoke(func(
+		chunk bll.IFileChunk,
+	) {
+		switch runMode {
+		case "debug", "test":
+			initCronDebug(ctx, chunk) //定时清理过期文件块
+		case "release":
+			initCronRelease(ctx, chunk) //定时清理过期文件块
+		}
+		logger.Printf(ctx, "gCorn初始化成功\n")
+	})
+}
+
+func initCronDebug(ctx context.Context, chunk bll.IFileChunk) {
+	_, err := gcron.Add("0 5 * * * *", func() {
+		err := chunk.ClearChunks(ctx)
+		if err != nil {
+			log.Fatalln(err)
+			return
+		}
+	})
+	if err != nil {
+		logger.Errorf(ctx, err.Error())
+	}
+}
+
+func initCronRelease(ctx context.Context, chunk bll.IFileChunk) {
+	clearChunk := g.Cfg().GetString("corn.clear_chunk")
+	_, err := gcron.Add(clearChunk, func() {
+		err := chunk.ClearChunks(ctx)
+		if err != nil {
+			log.Fatalln(err)
+			return
+		}
+	})
+	if err != nil {
+		logger.Errorf(ctx, err.Error())
+	}
+}

+ 73 - 0
boot/gorm.go

@@ -0,0 +1,73 @@
+package boot
+
+import (
+	"fmt"
+	"gxt-file-server/app/model"
+	"gxt-file-server/app/model/entity"
+	iModel "gxt-file-server/app/model/impl/model"
+	"gxt-file-server/pkg/logger"
+	"time"
+
+	"github.com/gogf/gf/frame/g"
+	"go.uber.org/dig"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
+)
+
+// 初始化gorm
+func initGorm() (*gorm.DB, error) {
+	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s",
+		g.Cfg().GetString("mysql.user"),
+		g.Cfg().GetString("mysql.password"),
+		g.Cfg().GetString("mysql.host"),
+		g.Cfg().GetInt("mysql.port"),
+		g.Cfg().GetString("mysql.db_name"),
+		g.Cfg().GetString("mysql.parameters"))
+
+	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+		SkipDefaultTransaction: true, // 跳过默认事务
+		NamingStrategy: schema.NamingStrategy{
+			TablePrefix:   g.Cfg().GetString("gorm.table_prefix"),
+			SingularTable: true, // 使用单数表名
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	db.Logger = logger.NewEntry(logger.GetLogger())
+	if g.Cfg().GetString("common.run_mode") == "debug" {
+		db.Debug()
+	}
+
+	sqlDb, err := db.DB()
+	if err != nil {
+		return nil, err
+	}
+	sqlDb.SetMaxIdleConns(g.Cfg().GetInt("gorm.max_idle_conns"))
+	sqlDb.SetMaxOpenConns(g.Cfg().GetInt("gorm.max_open_conns"))
+	sqlDb.SetConnMaxLifetime(time.Duration(g.Cfg().GetInt("gorm.max_open_conns")) * time.Second)
+	return db, nil
+}
+
+// 自动创建数据表映射
+func autoMigrate(db *gorm.DB) error {
+	return db.AutoMigrate(
+		new(entity.Demo),
+		new(entity.FileHistory),
+		new(entity.FileChunk),
+	)
+}
+
+// InjectModel inject models
+func InjectModel(container *dig.Container) error {
+	_ = container.Provide(iModel.NewTrans)
+	_ = container.Provide(func(m *iModel.Trans) model.ITrans { return m })
+	_ = container.Provide(iModel.NewDemo)
+	_ = container.Provide(func(m *iModel.Demo) model.IDemo { return m })
+	_ = container.Provide(iModel.NewFileHistory)
+	_ = container.Provide(func(m *iModel.FileHistory) model.IFileHistory { return m })
+	_ = container.Provide(iModel.NewFileChunk)
+	_ = container.Provide(func(m *iModel.FileChunk) model.IFileChunk { return m })
+	return nil
+}

+ 110 - 0
config/config.toml

@@ -0,0 +1,110 @@
+# 通用配置
+[common]
+    # 运行模式(debug:开发,test:测试,release:正式)
+    run_mode = "debug"
+
+# HTTP Server
+[server]
+	Address     = ":18199"
+	ServerAgent = "file-server"
+	LogPath     = "gf-app/server"
+	# 请求读取超时时间
+	ReadTimeout = "300s"
+	# 客户端最大Body上传限制大小,默认为8*1024*1024=8MB
+    ClientMaxBodySize = "1024MB"
+    # 是否开启平滑重启特性,开启时将会在本地增加10000的本地TCP端口用于进程间通信。默认false
+    Graceful = false
+
+	# 静态服务配置
+	# 开关
+	FileServerEnable = false
+	# 静态文件目录
+	ServerRoot = ""
+	# 默认首页检索
+	IndexFiles = ["index.html"]
+	# PProf配置
+	# 是否开启PProf性能调试特性。默认为false
+    PProfEnabled = false
+    # 开启PProf时有效,表示PProf特性的页面访问路径,对当前Server绑定的所有域名有效。
+    PProfPattern = ""
+    # 反向代理的前缀, 如果有反向代理,则可以添加路由前缀
+    RoutePrefix = ""
+
+# Logger.
+[logger]
+    Path        = "/tmp/log/gf-app"
+    Level       = "all"
+    Stdout      = true
+    CtxKeys = ["user_id", "trace_id", "span_title", "span_function", "version"]
+# 请求频率限制(需要启用redis配置)
+[rate_limiter]
+    # 是否启用
+    enable = true
+    # 每分钟每个用户允许的最大请求数量
+    count = 10
+    # redis数据库(如果存储方式是redis,则指定存储的数据库)
+    redis_db = 10
+# 跨域请求
+[cors]
+    # 是否启用
+    enable = true
+    # 允许跨域请求的域名列表(*表示全部允许)
+    allow_origins = ["*"]
+    # 允许跨域请求的请求方式列表
+    allow_methods = ["GET","POST","PUT","DELETE","PATCH"]
+    # 允许客户端与跨域请求一起使用的非简单标头的列表
+    allow_headers = ["Access-Control-Allow-Origin"]
+    # 请求是否可以包含cookie,HTTP身份验证或客户端SSL证书等用户凭据
+    allow_credentials = true
+    # 可以缓存预检请求结果的时间(以秒为单位)
+    max_age = 7200
+# redis配置
+[redis]
+    # 开关
+    enable = true
+    # 地址
+    addr = "127.0.0.1:6379"
+    # 密码
+    password = ""
+    # 默认库
+    db = 0
+# mysql数据库配置
+[mysql]
+    # 连接地址
+    host = "127.0.0.1"
+    # 连接端口
+    port= 3306
+    # 用户名
+    user = "root"
+    # 密码
+    password = "zJv4DwFL6G2MgSvP@"
+    # 数据库
+    db_name = "file-server"
+    # 连接参数
+    parameters = "charset=utf8mb4&parseTime=True&loc=Local&allowNativePasswords=true"
+# gorm配置
+[gorm]
+    # 设置连接可以重用的最长时间(单位:秒)
+    max_lifetime = 7200
+    # 设置数据库的最大打开连接数
+    max_open_conns = 150
+    # 设置空闲连接池中的最大连接数
+    max_idle_conns = 50
+    # 数据库表名前缀
+    table_prefix = "m_"
+    # 是否启用自动映射数据库表结构
+    enable_auto_migrate = false
+    # 慢查询阀值(单位:毫秒)
+    slow_sql_limit = 2000
+# minio 配置
+[minio]
+    addr = "127.0.0.1:9000"
+    assess_key = "administrator"
+    secret_key = "it2^wNKE$mVhr!hJozge@WauS+"
+[agent]
+    DefaultChunkExpireTime = 3600 #默认文件块的过期时间(秒)
+    DefaultExpireTime = 1 # 默认临时文件过期时间(小时)
+    DefaultFilePathPrefix = "s" # 默认文件上传路径前缀
+[corn]
+    clear_chunk = "0 0 1 * * *"
+

+ 21 - 0
go.mod

@@ -0,0 +1,21 @@
+module gxt-file-server
+
+go 1.15
+
+require (
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/go-ini/ini v1.62.0 // indirect
+	github.com/go-redis/redis/v8 v8.11.1
+	github.com/go-redis/redis_rate/v9 v9.1.1
+	github.com/gogf/gf v1.14.2
+	github.com/minio/minio-go v6.0.14+incompatible
+	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
+	github.com/smartystreets/goconvey v1.6.4 // indirect
+	go.uber.org/dig v1.12.0
+	golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
+	golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect
+	gopkg.in/ini.v1 v1.62.0 // indirect
+	gorm.io/driver/mysql v1.0.3
+	gorm.io/gorm v1.20.6
+)

+ 183 - 0
go.sum

@@ -0,0 +1,183 @@
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUaonAaAKArG3pyC67kGL3YY+6hGG8G4=
+github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU=
+github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-redis/redis/v8 v8.3.4/go.mod h1:jszGxBCez8QA1HWSmQxJO9Y82kNibbUmeYhKWrBejTU=
+github.com/go-redis/redis/v8 v8.11.1 h1:Aqf/1y2eVfE9zrySM++/efzwv3mkLH7n/T96//gbo94=
+github.com/go-redis/redis/v8 v8.11.1/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
+github.com/go-redis/redis_rate/v9 v9.1.1 h1:7SIrbnhQ7zsTNEgIvprFhJf7/+l3wSpZc2iRVwUmaq8=
+github.com/go-redis/redis_rate/v9 v9.1.1/go.mod h1:jjU9YxOSZ3cz0yj1QJVAJiy5ueKmL9o4AySJHcKyTSE=
+github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/gogf/gf v1.14.2 h1:xIgfq2YcMYAm1ZcbVD/u0y5qZDDdARY4u7261w8QlHA=
+github.com/gogf/gf v1.14.2/go.mod h1:7b21qQKDyIwJO4PkBCxVci5C62tm89MANGV2wJgAf50=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gqcn/structs v1.1.1 h1:cyzGRwfmn3d1d54fwW3KUNyG9QxR0ldIeqwFGeBt638=
+github.com/gqcn/structs v1.1.1/go.mod h1:/aBhTBSsKQ2Ec9pbnYdGphtdWXHFn4KrCL0fXM/Adok=
+github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA=
+github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E=
+github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o=
+github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
+github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
+github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ=
+github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
+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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opentelemetry.io/otel v0.13.0/go.mod h1:dlSNewoRYikTkotEnxdmuBHgzT+k/idJSfDv/FxEnOY=
+go.uber.org/dig v1.12.0 h1:l1GQeZpEbss0/M4l/ZotuBndCrkMdjnygzgcuOjAdaY=
+go.uber.org/dig v1.12.0/go.mod h1:X34SnWGr8Fyla9zQNO2GSO2D+TIuqB14OS8JhYocIyw=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20191030062658-86caa796c7ab/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.0.3 h1:+JKBYPfn1tygR1/of/Fh2T8iwuVwzt+PEJmKaXzMQXg=
+gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI=
+gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.20.6 h1:qa7tC1WcU+DBI/ZKMxvXy1FcrlGsvxlaKufHrT2qQ08=
+gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=

+ 77 - 0
gxt-file-server_package/file_server.go

@@ -0,0 +1,77 @@
+package file_server_sdk
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"sync"
+)
+
+var (
+	persistent = "api/v1/files/persistent" //文件持久化路径
+)
+
+type persistentStruct struct {
+	Hash string `json:"hash"`
+}
+
+type Config struct {
+	Host   string
+}
+
+type ConfigHandle struct {
+	Host   string
+}
+
+var (
+	once sync.Once
+	internalHandle  *ConfigHandle
+)
+
+func (c *Config) Init() {
+	//代码运行中需要的时候执行,且只执行一次
+	once.Do(func() {
+		internalHandle = &ConfigHandle{
+			Host: c.Host,
+		}
+	})
+}
+
+// GetHandle get handle
+func GetHandle() *ConfigHandle {
+	return internalHandle
+}
+
+//HandlePersistent 设置文件持久化
+func (c *ConfigHandle) HandlePersistent(hash string) error {
+	if hash == "" {
+		return errors.New("缺少请求参数")
+	}
+	param := persistentStruct{
+		Hash: hash,
+	}
+	s, err := json.Marshal(param)
+	if err != nil {
+		return err
+	}
+	url := fmt.Sprintf("%s/%s", c.Host, persistent)
+	reqBody := strings.NewReader(string(s))
+	req, err := http.NewRequest(http.MethodPut, url, reqBody)
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Content-type", "application/json")
+	client := &http.Client{}
+	response, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+	_, err = ioutil.ReadAll(response.Body)
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 18 - 0
gxt-file-server_package/request_file_test.go

@@ -0,0 +1,18 @@
+package file_server_sdk
+
+import (
+	"fmt"
+	"testing"
+)
+
+func init() {
+	i := Config{
+		Host: "http://127.0.0.1:18199",
+	}
+	i.Init()
+}
+
+func Test_Persistent_file(t *testing.T) {
+	var hash = "sss"
+	fmt.Println(GetHandle().HandlePersistent(hash))
+}

+ 12 - 0
main.go

@@ -0,0 +1,12 @@
+package main
+
+import (
+	"github.com/gogf/gf/frame/g"
+	_ "gxt-file-server/boot"
+	_ "gxt-file-server/router"
+)
+
+
+func main()  {
+	g.Server().Run()
+}

+ 124 - 0
pkg/auth/jwt_auth.go

@@ -0,0 +1,124 @@
+package auth
+
+import (
+	"encoding/json"
+	"github.com/dgrijalva/jwt-go"
+	"gxt-file-server/app/errors"
+	"time"
+)
+
+// TokenInfo 令牌信息
+type TokenInfo interface {
+	// GetAccessToken 获取访问令牌
+	GetAccessToken() string
+	// GetTokenType 获取令牌类型
+	GetTokenType() string
+	// GetExpiresAt 获取令牌到期时间戳
+	GetExpiresAt() int64
+	// EncodeToJSON JSON编码
+	EncodeToJSON() ([]byte, error)
+}
+
+// tokenInfo 令牌信息
+type tokenInfo struct {
+	AccessToken string `json:"access_token"` // 访问令牌
+	TokenType   string `json:"token_type"`   // 令牌类型
+	ExpiresAt   int64  `json:"expires_at"`   // 令牌到期时间
+}
+
+func (t *tokenInfo) GetAccessToken() string {
+	return t.AccessToken
+}
+
+func (t *tokenInfo) GetTokenType() string {
+	return t.TokenType
+}
+
+func (t *tokenInfo) GetExpiresAt() int64 {
+	return t.ExpiresAt
+}
+
+func (t *tokenInfo) EncodeToJSON() ([]byte, error) {
+	return json.Marshal(t)
+}
+
+type options struct {
+	signingMethod jwt.SigningMethod
+	signingKey    interface{}
+	keyfunc       jwt.Keyfunc
+	expired       int
+	tokenType     string
+}
+
+const defaultKey = "gxt-file-server"
+
+var defaultOptions = options{
+	tokenType:     "Bearer",
+	expired:       7200,
+	signingMethod: jwt.SigningMethodHS512,
+	signingKey:    []byte(defaultKey),
+	keyfunc: func(t *jwt.Token) (interface{}, error) {
+		if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
+			return nil, errors.ErrInvalidToken
+		}
+		return []byte(defaultKey), nil
+	},
+}
+
+type Option func(*options)
+
+type JWTAuth struct {
+	opts *options
+}
+
+func New(opts ...Option) *JWTAuth {
+	o := defaultOptions
+	for _, opt := range opts {
+		opt(&o)
+	}
+	return &JWTAuth{opts: &o}
+}
+
+// GenerateToken 生成令牌
+func (a *JWTAuth) GenerateToken(userID string) (TokenInfo, error) {
+	now := time.Now()
+	expiresAt := now.Add(time.Duration(a.opts.expired) * time.Second).Unix()
+	token := jwt.NewWithClaims(a.opts.signingMethod, &jwt.StandardClaims{
+		IssuedAt:  now.Unix(),
+		ExpiresAt: expiresAt,
+		NotBefore: now.Unix(),
+		Subject:   userID,
+	})
+
+	tokenString, err := token.SignedString(a.opts.signingKey)
+	if err != nil {
+		return nil, err
+	}
+
+	tokenInfo := &tokenInfo{
+		ExpiresAt:   expiresAt,
+		TokenType:   a.opts.tokenType,
+		AccessToken: tokenString,
+	}
+	return tokenInfo, nil
+}
+
+// 解析令牌
+func (a *JWTAuth) parseToken(tokenString string) (*jwt.StandardClaims, error) {
+	token, _ := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, a.opts.keyfunc)
+	if !token.Valid {
+		return nil, errors.ErrInvalidToken
+	}
+
+	return token.Claims.(*jwt.StandardClaims), nil
+}
+
+// ParseUserID 解析用户ID
+func (a *JWTAuth) ParseUserID(tokenString string) (string, error) {
+	claims, err := a.parseToken(tokenString)
+	if err != nil {
+		return "", err
+	}
+
+	return claims.Subject, nil
+}

+ 182 - 0
pkg/gplus/gplus.go

@@ -0,0 +1,182 @@
+package gplus
+
+import (
+	"context"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/net/ghttp"
+	iContext "gxt-file-server/app/context"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/schema"
+	"gxt-file-server/pkg/logger"
+	"gxt-file-server/pkg/utils"
+	"net/http"
+	"strings"
+)
+
+// 定义上下文中的键
+const (
+	prefix = "gao-xin-tong"
+	// UserIDKey 存储上下文中的键(用户ID)
+	UserIDKey = prefix + "/user-id"
+	// UserTypeKey 存储上下文中的键(用户类型)
+	UserTypeKey = prefix + "/user-type"
+	// TraceIDKey 存储上下文中的键(跟踪ID)
+	TraceIDKey = prefix + "/trace-id"
+	// ResBodyKey 存储上下文中的键(响应Body数据)
+	ResBodyKey = prefix + "/res-body"
+)
+
+// ParseJson 解析请求参数Json
+func ParseJson(r *ghttp.Request, out interface{}) error {
+	if err := r.Parse(out); err != nil {
+		m := "解析请求参数发生错误"
+		if g.Cfg().GetString("common.run_mode") == "debug" {
+			m += "[" + err.Error() + "]"
+		}
+		return errors.Wrap400Response(err, m)
+	}
+	return nil
+}
+
+// GetPageIndex 获取当前页
+func GetPageIndex(r *ghttp.Request) int {
+	defaultVal := 1
+	if v := r.GetQueryInt("current"); v > 0 {
+		return v
+	}
+	return defaultVal
+}
+
+// GetPageSize 获取分页的页大小(最大50)
+func GetPageSize(r *ghttp.Request) int {
+	defaultVal := 10
+	if v := r.GetQueryInt("pageSize"); v > 0 {
+		if v > 50 {
+			v = 50
+		}
+		return v
+	}
+	return defaultVal
+}
+
+// ResPage 分页响应
+func ResPage(r *ghttp.Request, v interface{}, pr *schema.PaginationResult) {
+	result := schema.HTTPList{
+		List: v,
+		Pagination: &schema.HTTPPagination{
+			Current:  GetPageIndex(r),
+			PageSize: GetPageSize(r),
+		},
+	}
+	if pr != nil {
+		result.Pagination.Total = pr.Total
+	}
+	ResSuccess(r, result)
+}
+
+// ResSuccess 响应成功
+func ResSuccess(c *ghttp.Request, v interface{}) {
+	ResJSON(c, http.StatusOK, v)
+}
+
+// ResOK 响应OK
+func ResOK(c *ghttp.Request) {
+	ResSuccess(c, schema.HTTPStatus{Status: "OK"})
+}
+
+// ResList 响应列表数据
+func ResList(c *ghttp.Request, v interface{}) {
+	ResSuccess(c, schema.HTTPList{List: v})
+}
+
+// ResJSON 响应JSON结果
+func ResJSON(r *ghttp.Request, code int, v interface{}) {
+	errors.JsonExit(r, code, v)
+}
+
+// ResError 响应错误
+func ResError(r *ghttp.Request, err error, status ...int) {
+	var res *errors.ResponseError
+	if err != nil {
+		if e, ok := err.(*errors.ResponseError); ok {
+			res = e
+		} else {
+			res = errors.UnWrapResponse(errors.Wrap500Response(err))
+		}
+	} else {
+
+		res = errors.UnWrapResponse(errors.ErrInternalServer)
+	}
+
+	if len(status) > 0 {
+		res.StatusCode = status[0]
+	}
+
+	if err := res.ERR; err != nil {
+		if res.StatusCode >= 500 {
+			logger.Errorf(NewContext(r), "%s", err)
+		}
+	}
+
+	eitem := schema.HTTPErrorItem{
+		Code:    res.Code,
+		Message: res.Message,
+		TraceId: GetTraceID(r),
+	}
+	ResJSON(r, res.StatusCode, schema.HTTPError{Error: eitem})
+}
+
+// SetUserId 上下文中设置用户Id
+func SetUserId(r *ghttp.Request, userId string) {
+	r.SetCtxVar(UserIDKey, userId)
+}
+
+// GetUserId 获取上下文中的用户Id
+func GetUserId(r *ghttp.Request) string {
+	return r.GetCtxVar(UserIDKey).String()
+}
+
+// GetToken 获取token
+func GetToken(r *ghttp.Request) string {
+	var token string
+	auth := r.GetHeader("Authorization")
+	prefix := "Bearer "
+	if auth != "" && strings.HasPrefix(auth, prefix) {
+		token = auth[len(prefix):]
+	}
+	return token
+}
+
+// NewContext 封装上下文入口
+func NewContext(r *ghttp.Request) context.Context {
+	parent := context.Background()
+
+	traceId := GetTraceID(r)
+	if traceId == "" {
+		traceId = utils.NewTraceID()
+	}
+	parent = iContext.NewTraceID(parent, traceId)
+	parent = logger.NewTraceIDContext(parent, traceId)
+
+	if v := GetUserID(r); v != "" {
+		parent = iContext.NewUserID(parent, v)
+		parent = logger.NewUserIDContext(parent, v)
+	}
+
+	return parent
+}
+
+// GetTraceID 获取追踪ID
+func GetTraceID(c *ghttp.Request) string {
+	return c.GetCtxVar(TraceIDKey).String()
+}
+
+// GetUserID 获取用户ID
+func GetUserID(c *ghttp.Request) string {
+	return c.GetCtxVar(UserIDKey).String()
+}
+
+// GetUserType 获取用户类型
+func GetUserType(c *ghttp.Request) string {
+	return c.GetCtxVar(UserTypeKey).String()
+}

+ 64 - 0
pkg/logger/gorm_log.go

@@ -0,0 +1,64 @@
+package logger
+
+import (
+	"context"
+	"fmt"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/os/glog"
+	"gorm.io/gorm/logger"
+	"gorm.io/gorm/utils"
+	"time"
+)
+
+func (e *Entry) LogMode(lv logger.LogLevel) logger.Interface {
+	switch lv {
+	case logger.Error:
+		e.entry.SetLevel(e.entry.GetLevel() | glog.LEVEL_ERRO)
+		break
+	case logger.Info:
+		e.entry.SetLevel(e.entry.GetLevel() | glog.LEVEL_INFO)
+		break
+	}
+	return e
+}
+
+func (e *Entry) Info(ctx context.Context, format string, args ...interface{}) {
+	e.entry.Ctx(ctx).Infof(format, args...)
+}
+
+func (e *Entry) Warn(ctx context.Context, format string, args ...interface{}) {
+	e.entry.Ctx(ctx).Warningf(format, args...)
+}
+
+func (e *Entry) Error(ctx context.Context, format string, args ...interface{}) {
+	e.entry.Ctx(ctx).Errorf(format, args...)
+}
+
+func (e *Entry) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
+	limit := g.Cfg().GetInt("gorm.slow_sql_limit")
+	if e.entry.GetLevel() > 0 {
+		elapsed := time.Since(begin)
+		switch {
+		case err != nil && e.entry.GetLevel() >= glog.LEVEL_ERRO:
+			{
+				sql, _ := fc()
+				e.Info(ctx, "%s\nSQL查询出错:%s\n执行SQL:%s", utils.FileWithLineNum(), err, sql)
+			}
+		case elapsed > time.Duration(limit)*time.Millisecond && e.entry.GetLevel() >= glog.LEVEL_WARN:
+			sql, rows := fc()
+			slowLog := fmt.Sprintf("执行时间 %v", elapsed)
+			if rows == -1 {
+				e.Warnf("%s\n慢查询SQL:%s\n%s \n影响行数:[%s]", utils.FileWithLineNum(), sql, slowLog, "-")
+			} else {
+				e.Warnf("%s\n慢查询SQL:%s\n%s\n影响行数:[%d]", utils.FileWithLineNum(), sql, slowLog, rows)
+			}
+		case e.entry.GetLevel() >= glog.LEVEL_INFO:
+			sql, rows := fc()
+			if rows == -1 {
+				e.Infof("执行SQL:[%s],影响行数:[%s]", sql, "-")
+			} else {
+				e.Infof("执行SQL:[%s], 影响行数:[%d]", sql, rows)
+			}
+		}
+	}
+}

+ 196 - 0
pkg/logger/logger.go

@@ -0,0 +1,196 @@
+package logger
+
+import (
+	"context"
+	"errors"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/os/glog"
+)
+
+// 定义键名
+const (
+	TraceIDKey      = "trace_id"
+	UserIDKey       = "user_id"
+	SpanTitleKey    = "span_title"
+	SpanFunctionKey = "span_function"
+	VersionKey      = "version"
+)
+
+var log *glog.Logger
+
+// TraceIDFunc 定义获取跟踪ID的函数
+type TraceIDFunc func() string
+
+var (
+	version     string
+	traceIDFunc TraceIDFunc
+)
+
+// SetTraceIdFunc 设置获取追踪Id的生成函数
+func SetTraceIdFunc(fn TraceIDFunc) {
+	traceIDFunc = fn
+}
+
+// SetVersion 设置版本号
+func SetVersion(ver string) {
+	version = ver
+}
+
+// FromTraceIDContext 从上下文中获取跟踪ID
+func FromTraceIDContext(ctx context.Context) string {
+	v := ctx.Value(TraceIDKey)
+	if v != nil {
+		if s, ok := v.(string); ok {
+			return s
+		}
+	}
+	return getTraceID()
+}
+
+// NewUserIDContext 创建用户ID上下文
+func NewUserIDContext(ctx context.Context, userID string) context.Context {
+	return context.WithValue(ctx, UserIDKey, userID)
+}
+
+// FromUserIDContext 从上下文中获取用户ID
+func FromUserIDContext(ctx context.Context) string {
+	v := ctx.Value(UserIDKey)
+	if v != nil {
+		if s, ok := v.(string); ok {
+			return s
+		}
+	}
+	return ""
+}
+
+func getTraceID() string {
+	if traceIDFunc != nil {
+		return traceIDFunc()
+	}
+	return ""
+}
+
+// GetLogger 获取logger
+func GetLogger() *glog.Logger {
+	return log
+}
+
+// NewTraceIDContext 创建跟踪ID上下文
+func NewTraceIDContext(ctx context.Context, traceID string) context.Context {
+	return context.WithValue(ctx, TraceIDKey, traceID)
+}
+
+// Init 初始化日志配置
+func Init(runMode string) {
+	if log != nil {
+		panic(errors.New("重复初始化logger"))
+	}
+	log = g.Log()
+	var lv string
+	switch runMode {
+	case "debug":
+		lv = "DEV"
+	case "test":
+		lv = "DEV"
+	case "release":
+		lv = "PRODUCT"
+	}
+	err := log.SetLevelStr(lv)
+	if err != nil {
+		panic(err)
+	}
+}
+
+type spanOptions struct {
+	Title    string
+	FuncName string
+}
+
+// SpanOption 定义跟踪单元的数据项
+type SpanOption func(*spanOptions)
+
+// SetSpanTitle 设置跟踪单元的标题
+func SetSpanTitle(title string) SpanOption {
+	return func(o *spanOptions) {
+		o.Title = title
+	}
+}
+
+// SetSpanFuncName 设置跟踪单元的函数名
+func SetSpanFuncName(funcName string) SpanOption {
+	return func(o *spanOptions) {
+		o.FuncName = funcName
+	}
+}
+
+// StartSpan 开始一个追踪单元
+func StartSpan(ctx context.Context, opts ...SpanOption) *Entry {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+
+	var o spanOptions
+	for _, opt := range opts {
+		opt(&o)
+	}
+	return NewEntry(log)
+}
+
+// Entry 定义统一的日志写入方式
+type Entry struct {
+	entry *glog.Logger
+}
+
+func NewEntry(entry *glog.Logger) *Entry {
+	return &Entry{entry: entry}
+}
+
+// Debugf 写入调试日志
+func Debugf(ctx context.Context, format string, args ...interface{}) {
+	StartSpan(ctx).entry.Ctx(ctx).Debugf(format, args)
+}
+
+// Printf 写入消息日志
+func Printf(ctx context.Context, format string, args ...interface{}) {
+	StartSpan(ctx).entry.Ctx(ctx).Printf(format, args...)
+}
+
+// Warnf 写入警告日志
+func Warnf(ctx context.Context, format string, args ...interface{}) {
+	StartSpan(ctx).entry.Ctx(ctx).Warningf(format, args...)
+}
+
+// Fatalf 写入重大错误日志
+func Fatalf(ctx context.Context, format string, args ...interface{}) {
+	StartSpan(ctx).entry.Ctx(ctx).Fatalf(format, args...)
+}
+
+// Errorf 错误日志
+func Errorf(ctx context.Context, format string, args ...interface{}) {
+	StartSpan(ctx).entry.Ctx(ctx).Errorf(format, args...)
+}
+
+// Errorf 错误日志
+func (e *Entry) Errorf(format string, args ...interface{}) {
+	e.entry.Errorf(format, args...)
+}
+
+// Warnf 警告日志
+func (e *Entry) Warnf(format string, args ...interface{}) {
+	e.entry.Warningf(format, args...)
+}
+
+// Infof 消息日志
+func (e *Entry) Infof(format string, args ...interface{}) {
+	e.entry.Infof(format, args...)
+}
+
+// Printf 消息日志
+func (e *Entry) Printf(format string, args ...interface{}) {
+	e.entry.Printf(format, args...)
+}
+
+// Debugf 写入调试日志
+func (e *Entry) Debugf(format string, args ...interface{}) {
+	e.entry.Debugf(format, args...)
+}

+ 49 - 0
pkg/middleware/middleware.go

@@ -0,0 +1,49 @@
+package middleware
+
+import "github.com/gogf/gf/net/ghttp"
+
+// EmptyMiddleware 不执行业务处理的中间件
+func EmptyMiddleware(r *ghttp.Request) {
+	r.Middleware.Next()
+}
+
+type SkipperFunc func(request *ghttp.Request) bool
+
+// AllowPathPrefixSkipper 检查请求路径是否包含指定的前缀,如果包含则跳过
+func AllowPathPrefixSkipper(prefixes ...string) SkipperFunc {
+	return func(request *ghttp.Request) bool {
+		path := request.URL.Path
+		pathLen := len(path)
+		for _, p := range prefixes {
+			if pl := len(p); pathLen >= pl && path[:pl] == p {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+// AllowPathPrefixNoSkipper 检查请求路径是否包含指定的前缀,如果包含则不跳过
+func AllowPathPrefixNoSkipper(prefixes ...string) SkipperFunc {
+	return func(request *ghttp.Request) bool {
+		path := request.URL.Path
+		pathLen := len(path)
+
+		for _, p := range prefixes {
+			if pl := len(p); pathLen >= pl && path[:pl] == p {
+				return false
+			}
+		}
+		return true
+	}
+}
+
+// SkipHandler 统一处理跳过函数
+func SkipHandler(r *ghttp.Request, skippers ...SkipperFunc) bool {
+	for _, skipper := range skippers {
+		if skipper(r) {
+			return true
+		}
+	}
+	return false
+}

+ 39 - 0
pkg/middleware/mw_auth.go

@@ -0,0 +1,39 @@
+package middleware
+
+import (
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/net/ghttp"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/pkg/auth"
+	"gxt-file-server/pkg/gplus"
+)
+
+func UserAuthMiddleware(skippers ...SkipperFunc) ghttp.HandlerFunc {
+	jwt := auth.New()
+	return func(r *ghttp.Request) {
+		if len(skippers) > 0 && skippers[0](r) {
+			r.Middleware.Next()
+			return
+		}
+		var userId string
+		if t := gplus.GetToken(r); t != "" {
+			id, err := jwt.ParseUserID(t)
+			if err != nil {
+				gplus.ResError(r, err)
+			}
+			userId = id
+		}
+		if userId != "" {
+			gplus.SetUserId(r, userId)
+		}
+		if userId == "" {
+			if g.Cfg().GetString("common.RunMode") == "debug" {
+				gplus.SetUserId(r, g.Cfg().GetString("root.user_name"))
+				r.Middleware.Next()
+				return
+			}
+			gplus.ResError(r, errors.ErrNoPerm)
+		}
+		r.Middleware.Next()
+	}
+}

+ 16 - 0
pkg/middleware/mw_cros.go

@@ -0,0 +1,16 @@
+package middleware
+
+import "github.com/gogf/gf/net/ghttp"
+
+// CROSMiddleWare 跨域请求中间件
+// TODO:未完成配置信息对应
+func CROSMiddleWare(skippers ...SkipperFunc) ghttp.HandlerFunc {
+	return func(r *ghttp.Request) {
+		if len(skippers) > 0 && skippers[0](r) {
+			r.Middleware.Next()
+			return
+		}
+		r.Response.CORSDefault()
+		r.Middleware.Next()
+	}
+}

+ 100 - 0
pkg/middleware/mw_file.go

@@ -0,0 +1,100 @@
+package middleware
+
+import (
+	"bytes"
+	"github.com/gogf/gf/net/ghttp"
+	"github.com/nfnt/resize"
+	"gxt-file-server/app/agent"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/pkg/gplus"
+	"gxt-file-server/pkg/logger"
+	"image"
+	"image/jpeg"
+	"image/png"
+	"net/url"
+	"path"
+	"strconv"
+	"strings"
+)
+
+// FileMiddleWare 文件服务中间件
+func FileMiddleWare(skippers ...SkipperFunc) ghttp.HandlerFunc {
+	return func(r *ghttp.Request) {
+		if len(skippers) > 0 && skippers[0](r) {
+			r.Middleware.Next()
+			return
+		}
+		ctx := gplus.NewContext(r)
+		filePath, err := url.PathUnescape(r.Request.URL.Path)
+		if err != nil {
+			logger.Errorf(ctx, err.Error())
+			gplus.ResError(r, err)
+		}
+		var (
+			thumb string
+			w     uint
+			h     uint
+		)
+		suffix := path.Ext(filePath)
+		contentType := getContentType(suffix)
+		fileData, _, err := agent.DefaultAgent().Get(ctx, filePath)
+		if err != nil {
+			gplus.ResError(r, errors.ErrFileNotFound)
+		}
+		thumb = r.GetString("thumb")
+		if thumb == "1" && (contentType == "image/png" || contentType == "image/jpeg") {
+			w = r.GetUint("w")
+			h = r.GetUint("h")
+			if w > 0 || h > 0 {
+				img, format, err := image.Decode(bytes.NewBuffer(fileData))
+				if err != nil {
+					return
+				}
+
+				if origWidth := uint(img.Bounds().Dx()); w > origWidth {
+					w = origWidth
+				}
+				if origHeight := uint(img.Bounds().Dy()); h > origHeight {
+					h = origHeight
+				}
+
+				img = resize.Resize(w, h, img, resize.Lanczos3)
+				buf := new(bytes.Buffer)
+				if format == "png" {
+					png.Encode(buf, img)
+				} else {
+					jpeg.Encode(buf, img, nil)
+				}
+
+				if l := buf.Len(); l > 0 {
+					fileData = buf.Bytes()
+				}
+			}
+		}
+
+		if strings.HasPrefix(contentType, "text/html") ||
+			strings.HasPrefix(contentType, "application/javascript") {
+			contentType = "text/plain; charset=utf-8"
+		}
+
+		r.Response.Header().Set("Content-Type", contentType)
+		r.Response.Header().Set("Cache-Control", "max-age=31536000")
+		r.Response.Header().Set("Content-Length", strconv.FormatInt(int64(len(fileData)), 10))
+		r.Response.Write(fileData)
+	}
+}
+
+func getContentType(suffix string) string{
+	fileContentType := make(map[string]string)
+	fileContentType[".jpg"] = "image/jpeg"
+	fileContentType[".jpeg"] = "image/jpeg"
+	fileContentType[".gif"] = "image/gif"
+	fileContentType[".png"] = "image/png"
+	fileContentType[".mp4"] = "video/mp4"
+	fileContentType[".mpg4"] = "video/mp4"
+	fileContentType[".pdf"] = "application/pdf"
+	if r,ok := fileContentType[suffix];ok{
+		return r
+	}
+	return "application/octet-stream"
+}

+ 71 - 0
pkg/middleware/mw_rate_limiter.go

@@ -0,0 +1,71 @@
+package middleware
+
+import (
+	"github.com/go-redis/redis/v8"
+	"github.com/go-redis/redis_rate/v9"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/net/ghttp"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/pkg/gplus"
+	"gxt-file-server/pkg/logger"
+	"strconv"
+)
+
+// RateLimiterMiddleware 请求频率限制中间件
+func RateLimiterMiddleware(skippers ...SkipperFunc) ghttp.HandlerFunc {
+	if !g.Cfg().GetBool("rate_limiter.enable") {
+		return EmptyMiddleware
+	}
+	// check enable redis
+	if !g.Cfg().GetBool("redis.enable") {
+		return func(r *ghttp.Request) {
+			logger.Warnf(gplus.NewContext(r), "限流中间件无法正常使用,请启用redis配置[redis.enable]")
+			r.Middleware.Next()
+		}
+	}
+
+	addr := g.Cfg().GetString("redis.addr")
+	password := g.Cfg().GetString("redis.password")
+	db := g.Cfg().GetInt("redis.db")
+	ring := redis.NewRing(&redis.RingOptions{
+		Addrs: map[string]string{
+			"server1": addr,
+		},
+		Password: password,
+		DB:       db,
+	})
+
+	limiter := redis_rate.NewLimiter(ring)
+
+	return func(r *ghttp.Request) {
+		if SkipHandler(r, skippers...) {
+			r.Middleware.Next()
+			return
+		}
+
+		userID := gplus.GetUserID(r)
+		if userID == "" {
+			r.Middleware.Next()
+			return
+		}
+		ctx := gplus.NewContext(r)
+		limit := g.Cfg().GetInt("rate_limiter.count")
+		result, err := limiter.Allow(ctx,
+			userID, redis_rate.PerMinute(limit))
+		if err != nil {
+			gplus.ResError(r, errors.ErrInternalServer)
+		}
+		if result != nil {
+			if result.Allowed == 0 {
+				h := r.Response.Header()
+				h.Set("X-RateLimit-Limit", strconv.FormatInt(int64(result.Limit.Burst), 10))
+				h.Set("X-RateLimit-Remaining", strconv.FormatInt(int64(result.Remaining), 10))
+				h.Set("X-RateLimit-Reset", strconv.FormatInt(int64(result.ResetAfter.Seconds()), 10))
+				gplus.ResError(r, errors.ErrTooManyRequests)
+				return
+			}
+		}
+
+		r.Middleware.Next()
+	}
+}

+ 20 - 0
pkg/middleware/mw_trace.go

@@ -0,0 +1,20 @@
+package middleware
+
+import (
+	"github.com/gogf/gf/net/ghttp"
+	"gxt-file-server/pkg/gplus"
+	"gxt-file-server/pkg/utils"
+)
+
+func TraceIdMiddleware(skippers ...SkipperFunc) ghttp.HandlerFunc {
+	return func(r *ghttp.Request) {
+		if len(skippers) > 0 && skippers[0](r) {
+			r.Middleware.Next()
+			return
+		}
+		if r.GetCtxVar(gplus.TraceIDKey).String() == "" {
+			r.SetCtxVar(gplus.TraceIDKey, utils.NewTraceID())
+		}
+		r.Middleware.Next()
+	}
+}

+ 36 - 0
pkg/redis/redis.go

@@ -0,0 +1,36 @@
+package redis
+
+import (
+	"context"
+	"github.com/go-redis/redis/v8"
+	"sync"
+)
+
+var (
+	once sync.Once
+)
+
+// Init 初始化redis客户端
+func Init(ctx context.Context, addr, password string, db int) *redis.Client {
+	var internalClient *redis.Client
+	once.Do(func() {
+		internalClient = newCli(ctx, addr, password, db)
+	})
+	return internalClient
+}
+
+// New 创建redis客户端实例
+func newCli(ctx context.Context, addr, password string, db int) *redis.Client {
+	cli := redis.NewClient(&redis.Options{
+		Addr:     addr,
+		Password: password,
+		DB:       db,
+	})
+
+	cmd := cli.Ping(ctx)
+	if err := cmd.Err(); err != nil {
+		panic(err)
+	}
+
+	return cli
+}

+ 28 - 0
pkg/store/store.go

@@ -0,0 +1,28 @@
+package store
+
+import (
+	"context"
+	"github.com/minio/minio-go"
+	"io"
+	"net/http"
+)
+
+type Backend interface {
+	Store(ctx context.Context, filename string, data io.Reader, size int64) (string, error)
+	Get(ctx context.Context, fileName string) ([]byte, string, error)
+	Delete(ctx context.Context, fileName string) error
+	ComposeObject(ctx context.Context,pathS []string,filePath string) error
+	RemoveObject(ctx context.Context,filePath string) error
+	Stat(filename string) (minio.ObjectInfo, error)
+}
+
+type FileInfo interface {
+	FullName() string
+	Name() string
+	Size() int64
+	Hash() string
+}
+
+type Uploader interface {
+	Upload(ctx context.Context, r *http.Request, key string) ([]FileInfo, error)
+}

+ 167 - 0
pkg/store/store_minio.go

@@ -0,0 +1,167 @@
+package store
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"github.com/minio/minio-go"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+)
+
+var once sync.Once
+
+// ErrorInvalidName 无效的文件名
+var ErrorInvalidName = errors.New("invalid file name")
+
+// MinioStore minio 对象存储
+type MinioStore struct {
+	cli *minio.Client
+}
+
+// ComposeObject 通过使用服务端拷贝实现钭多个源对象合并创建成一个新的对象。
+func (m *MinioStore) ComposeObject(ctx context.Context,pathS []string,filePath string) error {
+	bucketName,fileName,err := m.parseFilename(filePath)
+	if err != nil{
+		return err
+	}
+	dst, err := minio.NewDestinationInfo(bucketName, fileName, nil, nil)
+	if err != nil {
+		return  err
+	}
+
+	srcs := make([]minio.SourceInfo,0,len(pathS))
+	for i:=range pathS{
+		bucketName,fileName,err := m.parseFilename(pathS[i])
+		if err != nil{
+			return err
+		}
+		srcs = append(srcs,minio.NewSourceInfo(bucketName, fileName, nil))
+	}
+	return m.cli.ComposeObject(dst, srcs)
+}
+
+//RemoveObject 删除minio中的文件
+func (m *MinioStore) RemoveObject(ctx context.Context,filePath string) error {
+	bucketName,fileName,err := m.parseFilename(filePath)
+	if err != nil{
+		return err
+	}
+	return m.cli.RemoveObject(bucketName, fileName)
+}
+
+
+func (m *MinioStore) Delete(ctx context.Context, fileName string) error {
+	b, o, err := m.parseFilename(fileName)
+	if err != nil {
+		return err
+	}
+	n, err := url.PathUnescape(o)
+	if err != nil {
+		return err
+	}
+	return m.cli.RemoveObject(b, n)
+}
+
+// Stat 文件状态信息
+func (m *MinioStore) Stat(filename string) (minio.ObjectInfo, error) {
+	bucketName, objectName, err := m.parseFilename(filename)
+	if err != nil {
+		return minio.ObjectInfo{}, err
+	}
+	return m.cli.StatObject(bucketName, objectName, minio.StatObjectOptions{})
+}
+
+// Get get file buffers
+func (m *MinioStore) Get(ctx context.Context, fileName string) ([]byte, string, error) {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+
+	stat, err := m.Stat(fileName)
+	if err != nil {
+		return nil, "", errors.New("文件不存在:" + fileName)
+	}
+	bucketName, objectName, err := m.parseFilename(fileName)
+	if err != nil {
+		return nil, "", err
+	}
+
+	obj, err := m.cli.GetObjectWithContext(ctx, bucketName, objectName, minio.GetObjectOptions{})
+	if err != nil {
+		return nil, "", err
+	}
+	buf, err := ioutil.ReadAll(obj)
+	if err != nil {
+		return nil, "", err
+	}
+	return buf, stat.ContentType, nil
+}
+
+// Store save file
+func (m *MinioStore) Store(ctx context.Context, filename string, data io.Reader, size int64) (fileHash string, err error) {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	bucket, objName, err := m.parseFilename(filename)
+	if err != nil {
+		return
+	}
+	exists, err := m.cli.BucketExists(bucket)
+	if err != nil {
+		return
+	} else if !exists {
+		err = m.cli.MakeBucket(bucket, "local")
+		if err != nil {
+			return
+		}
+	}
+	buf, err := ioutil.ReadAll(data)
+	if err != nil {
+		return
+	}
+	rd := bytes.NewBuffer(buf)
+	if size == 0 {
+		size = int64(rd.Len())
+	}
+	_, err = m.cli.PutObjectWithContext(ctx, bucket, objName, rd, size, minio.PutObjectOptions{
+		ContentType: http.DetectContentType(buf),
+		NumThreads:  2,
+	})
+	stat, err := m.Stat(filename)
+	if err != nil {
+		return
+	}
+	fileHash = stat.ETag
+	return
+}
+
+// 解析文件名 `prefix/bucket/uuid/filename`
+func (m *MinioStore) parseFilename(filename string) (string, string, error) {
+	if len(filename) > 0 && filename[0] == '/' {
+		filename = filename[1:]
+	}
+	names := strings.Split(filename, "/")
+	if len(names) < 3 {
+		return "", "", ErrorInvalidName
+	}
+
+	return strings.ToLower(names[1]), strings.Join(names[2:], "/"), nil
+}
+
+// MiniStoreInit minio init
+func MiniStoreInit(addr, accessKey, secretKey string) *MinioStore {
+	ms := new(MinioStore)
+	once.Do(func() {
+		cli, err := minio.New(addr, accessKey, secretKey, false)
+		if err != nil {
+			panic(err)
+		}
+		ms.cli = cli
+	})
+	return ms
+}

+ 39 - 0
pkg/utils/util.go

@@ -0,0 +1,39 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"runtime"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var (
+	pid           = os.Getpid()
+	gormSourceDir string
+)
+
+func init() {
+	_, file, _, _ := runtime.Caller(0)
+	gormSourceDir = regexp.MustCompile(`utils.utils\.go`).ReplaceAllString(file, "")
+}
+
+// NewTraceID 创建追踪ID
+func NewTraceID() string {
+	return fmt.Sprintf("trace-id-%d-%s",
+		pid,
+		time.Now().Format("2006.01.02.15.04.05.999999"))
+}
+
+func FileWithLineNum() string {
+	for i := 2; i < 15; i++ {
+		_, file, line, ok := runtime.Caller(i)
+
+		if ok && (!strings.HasPrefix(file, gormSourceDir) || strings.HasSuffix(file, "_test.go")) {
+			return file + ":" + strconv.FormatInt(int64(line), 10)
+		}
+	}
+	return ""
+}

+ 41 - 0
router/api/api.go

@@ -0,0 +1,41 @@
+package api
+
+import (
+	"gxt-file-server/pkg/middleware"
+	"gxt-file-server/router/api/controllers"
+
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/net/ghttp"
+	"go.uber.org/dig"
+)
+
+// RegisterRouters 注册路由
+func RegisterRouters(s *ghttp.Server, container *dig.Container) error {
+	controllers.Inject(container)
+	// 路由根
+	gr := s.Group(g.Cfg().GetString("server.RoutePrefix")).
+		Group("/api")
+	// 注册请求限制中间件
+	gr.Middleware(middleware.RateLimiterMiddleware())
+	gr.Middleware(middleware.CROSMiddleWare())
+	return container.Invoke(func(
+		cDemo *controllers.Demo,
+		cFile *controllers.File,
+	) {
+		v1 := gr.Group("/v1")
+		{
+			gDemo := v1.Group("/demos")
+			{
+				gDemo.POST("/", cDemo.Create)
+			}
+			gFile := v1.Group("/files")
+			{
+				gFile.POST("/", cFile.Upload)
+				gFile.GET("/", cFile.Download)
+				gFile.PUT("/persistent", cFile.Persistent)
+				gFile.POST("/chunk", cFile.Chunk)
+				gFile.POST("/merge", cFile.Merge)
+			}
+		}
+	})
+}

+ 41 - 0
router/api/controllers/c_demo.go

@@ -0,0 +1,41 @@
+package controllers
+
+import (
+	"github.com/gogf/gf/net/ghttp"
+	"gxt-file-server/app/bll"
+	"gxt-file-server/app/schema"
+	"gxt-file-server/pkg/gplus"
+)
+
+type Demo struct {
+	cBll bll.IDemo
+}
+
+func NewDemo(cb bll.IDemo) *Demo {
+	return &Demo{
+		cBll: cb,
+	}
+}
+
+// Create 创建数据
+// @Tags API-Demo
+// @Summary 创建数据
+// @Param body body schema.Demo true "提交的数据"
+// @Success 200 {object} schema.Demo
+// @Failure 400 {object} schema.HTTPError "{error:{code:0,message:无效的请求参数}}"
+// @Failure 401 {object} schema.HTTPError "{error:{code:0,message:未授权}}"
+// @Failure 500 {object} schema.HTTPError "{error:{code:0,message:服务器错误}}"
+// @Router /api/v1/demos/ [post]
+func (a *Demo) Create(r *ghttp.Request) {
+	var data schema.Demo
+	if err := gplus.ParseJson(r, &data); err != nil {
+		gplus.ResError(r, err)
+	}
+
+	ctx := gplus.NewContext(r)
+	result, err := a.cBll.Create(ctx, data)
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	gplus.ResSuccess(r, result)
+}

+ 131 - 0
router/api/controllers/c_file.go

@@ -0,0 +1,131 @@
+package controllers
+
+import (
+	"fmt"
+	"github.com/gogf/gf/net/ghttp"
+	"gxt-file-server/app/bll"
+	context2 "gxt-file-server/app/context"
+	"gxt-file-server/app/errors"
+	"gxt-file-server/app/schema"
+	"gxt-file-server/pkg/gplus"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+// NewFile 创建文件管理控制器
+func NewFile(bFile bll.IFile) *File {
+	return &File{
+		FileBll: bFile,
+	}
+}
+
+// File 文件管理
+// @Name File
+// @Description 文件管理
+type File struct {
+	FileBll bll.IFile
+}
+
+// Upload 上传文件
+func (a *File) Upload(r *ghttp.Request) {
+	var params schema.UploadParams
+	if err := gplus.ParseJson(r, &params); err != nil {
+		gplus.ResError(r, err)
+	}
+	ctx := gplus.NewContext(r)
+	if v := r.Header.Get("FILE-EXPIRE"); v != "" {
+		m, err := strconv.Atoi(v)
+		if err != nil {
+			gplus.ResError(r, errors.ErrHeaderParamsError)
+		}
+		ctx = context2.NewFileExpireContext(ctx, m)
+	}
+	if v := r.Header.Get("FILE-HASH"); v != "" {
+		ctx = context2.NewFileHashContext(ctx, v)
+	}
+	info, err := a.FileBll.Upload(ctx, r.Request, params.FormKey, params.BaseUrl)
+	if err != nil {
+		gplus.ResError(r, err)
+		return
+	}
+
+	gplus.ResSuccess(r, info)
+}
+
+// Download 下载文件
+func (a *File) Download(r *ghttp.Request) {
+	if r.GetString("path") == "" || r.GetString("name") == "" {
+		gplus.ResError(r, errors.ErrBadRequest)
+	}
+	path, err := url.PathUnescape(r.GetString("path"))
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	fileData, contentType, err := a.FileBll.Download(gplus.NewContext(r), path)
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	if strings.HasPrefix(contentType, "text/html") ||
+		strings.HasPrefix(contentType, "application/javascript") {
+		contentType = "text/plain; charset=utf-8"
+	}
+	r.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", r.GetString("name")))
+	r.Response.Header().Set("Content-Type", "application/octet-stream")
+	// r.Response.Header().Set("Content-Type", contentType)
+	r.Response.Header().Set("Cache-Control", "max-age=31536000")
+	r.Response.Header().Set("Content-Length", strconv.FormatInt(int64(len(fileData)), 10))
+	r.Response.Write(fileData)
+}
+
+// Persistent 设置文件为持久化
+func (a *File) Persistent(r *ghttp.Request) {
+	var param schema.PersistentFileRequest
+	if err := gplus.ParseJson(r, &param); err != nil {
+		gplus.ResError(r, err)
+	}
+	err := a.FileBll.Persistent(gplus.NewContext(r), param.Hash)
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	gplus.ResOK(r)
+}
+
+// Chunk 分块上传
+func (a *File) Chunk(r *ghttp.Request) {
+	var param schema.FileChunkUploadReq
+	if err := gplus.ParseJson(r, &param); err != nil {
+		gplus.ResError(r, err)
+	}
+	result, err := a.FileBll.ChunkUpload(gplus.NewContext(r), schema.FileChunkParams{
+		HttpRequest: r.Request,
+		FormKey:     param.FormKey,
+		BaseUrl:     param.BaseUrl,
+		Index:       param.Index,
+		Total:       param.Total,
+		Hash:        param.Hash,
+	})
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	gplus.ResSuccess(r, result)
+}
+
+// Merge 文件分块合并
+func (a *File) Merge(r *ghttp.Request) {
+	var param schema.FileMergeParams
+	if err := gplus.ParseJson(r, &param); err != nil {
+		gplus.ResError(r, err)
+	}
+	result, err := a.FileBll.FileMerge(gplus.NewContext(r), schema.FileMergeParams{
+		HttpRequest: r.Request,
+		BaseUrl:     param.BaseUrl,
+		Total:       param.Total,
+		Hash:        param.Hash,
+		FileName: param.FileName,
+	})
+	if err != nil {
+		gplus.ResError(r, err)
+	}
+	gplus.ResSuccess(r, result)
+}

+ 9 - 0
router/api/controllers/ctl.go

@@ -0,0 +1,9 @@
+package controllers
+
+import "go.uber.org/dig"
+
+// Inject 注入 controllers
+func Inject(container *dig.Container) {
+	_ = container.Provide(NewDemo)
+	_ = container.Provide(NewFile)
+}

+ 15 - 0
router/router.go

@@ -0,0 +1,15 @@
+package router
+
+import (
+	"github.com/gogf/gf/net/ghttp"
+	"go.uber.org/dig"
+	"gxt-file-server/router/api"
+)
+// InitRouters 初始化路由注册
+func InitRouters(s *ghttp.Server, container *dig.Container) {
+	// 注册api路由组
+	err := api.RegisterRouters(s, container)
+	if err != nil {
+		panic(err)
+	}
+}

+ 18 - 0
router/swagger.go

@@ -0,0 +1,18 @@
+/*
+Package routers 生成swagger文档
+
+文档规则请参考:https://github.com/swaggo/swag#declarative-comments-format
+
+使用方式:
+
+	go get -u github.com/swaggo/swag/cmd/swag
+	swag init -g ./internal/app/routers/swagger.go -o ./docs/swagger*/
+package router
+
+// @title 高新通后台开发框架
+// @version 1.0.0
+// @description 高新通内部开发框架,基于gf+gorm+dig。
+// @schemes http https
+// @host 39.98.250.155:10076
+// host 127.0.0.1:10076
+// @basePath /

+ 3 - 0
发开发.sh

@@ -0,0 +1,3 @@
+GOOS=linux GOARCH=amd64 go build -ldflags "-w -s" -o ./server2  ./
+scp ./server2 root@39.98.250.155:/root/services/gxt-file-server/gxt-file-server2
+read -p "按回车继续..."