github.com/SupenBysz/gf-admin-community@v0.7.4/internal/logic/sys_file/sys_file.go (about)

     1  package sys_file
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"github.com/SupenBysz/gf-admin-community/sys_consts"
     7  	"github.com/SupenBysz/gf-admin-community/sys_model"
     8  	"github.com/SupenBysz/gf-admin-community/sys_model/sys_do"
     9  	"github.com/SupenBysz/gf-admin-community/sys_model/sys_enum"
    10  	"github.com/SupenBysz/gf-admin-community/sys_model/sys_hook"
    11  	"github.com/kysion/base-library/utility/crypto"
    12  	"github.com/kysion/base-library/utility/daoctl"
    13  	"github.com/kysion/base-library/utility/kconv"
    14  	"github.com/kysion/base-library/utility/kmap"
    15  	"github.com/kysion/oss-library/api/oss_api"
    16  	"github.com/kysion/oss-library/oss_controller"
    17  	"github.com/kysion/oss-library/oss_global"
    18  	"github.com/kysion/oss-library/oss_model"
    19  	"io"
    20  	"net/http"
    21  	"os"
    22  	"time"
    23  
    24  	"github.com/gogf/gf/v2/encoding/gbase64"
    25  	"github.com/gogf/gf/v2/encoding/gjson"
    26  	"github.com/gogf/gf/v2/errors/gerror"
    27  	"github.com/gogf/gf/v2/frame/g"
    28  	"github.com/gogf/gf/v2/os/gfile"
    29  	"github.com/gogf/gf/v2/os/gtime"
    30  	"github.com/gogf/gf/v2/text/gstr"
    31  	"github.com/gogf/gf/v2/util/gconv"
    32  	"github.com/yitter/idgenerator-go/idgen"
    33  
    34  	"github.com/SupenBysz/gf-admin-community/sys_model/sys_dao"
    35  	"github.com/SupenBysz/gf-admin-community/sys_model/sys_entity"
    36  	"github.com/SupenBysz/gf-admin-community/sys_service"
    37  )
    38  
    39  type hookInfo sys_model.KeyValueT[int64, sys_hook.FileHookInfo]
    40  
    41  type sFile struct {
    42  	cachePrefix   string
    43  	hookArr       []hookInfo
    44  	CacheDuration time.Duration
    45  }
    46  
    47  func init() {
    48  	sys_service.RegisterFile(New())
    49  }
    50  
    51  func New() *sFile {
    52  	return &sFile{
    53  		cachePrefix:   "upload",
    54  		hookArr:       make([]hookInfo, 0),
    55  		CacheDuration: time.Hour * 2,
    56  	}
    57  }
    58  
    59  // InstallHook 安装Hook
    60  func (s *sFile) InstallHook(state sys_enum.UploadEventState, hookFunc sys_hook.FileHookFunc) int64 {
    61  	item := hookInfo{Key: idgen.NextId(), Value: sys_hook.FileHookInfo{Key: state, Value: hookFunc}}
    62  	s.hookArr = append(s.hookArr, item)
    63  	return item.Key
    64  }
    65  
    66  // UnInstallHook 卸载Hook
    67  func (s *sFile) UnInstallHook(savedHookId int64) {
    68  	newFuncArr := make([]hookInfo, 0)
    69  	for _, item := range s.hookArr {
    70  		if item.Key != savedHookId {
    71  			newFuncArr = append(newFuncArr, item)
    72  			continue
    73  		}
    74  	}
    75  	s.hookArr = newFuncArr
    76  }
    77  
    78  // CleanAllHook 清除Hook
    79  func (s *sFile) CleanAllHook() {
    80  	s.hookArr = make([]hookInfo, 0)
    81  }
    82  
    83  // Upload 统一上传文件
    84  func (s *sFile) Upload(ctx context.Context, in sys_model.FileUploadInput) (*sys_entity.SysFile, error) {
    85  	sessionUser := sys_service.SysSession().Get(ctx).JwtClaimsUser
    86  	uploadPath := g.Cfg().MustGet(ctx, "upload.tempPath").String()
    87  	// 获取系统默认的临时文件的存储路径
    88  	tmpPath := gfile.Temp("upload")
    89  	{
    90  		// 上传文件夹初始化
    91  		if uploadPath == "" {
    92  			uploadPath = tmpPath
    93  		}
    94  
    95  		// temp/upload/  ---> temp/upload
    96  		if len(uploadPath) > 0 && gstr.HasSuffix(uploadPath, "/") {
    97  			uploadPath = uploadPath[0 : len(uploadPath)-1]
    98  		}
    99  
   100  		// 为了下面存储userCacheJson
   101  		tmpPath = uploadPath
   102  
   103  		uploadPath = uploadPath + "/" + gtime.Now().Format("Ymd")
   104  		// 目录不存在则创建
   105  		if !gfile.Exists(uploadPath) {
   106  			gfile.Mkdir(uploadPath)
   107  			gfile.Chmod(uploadPath, gfile.DefaultPermCopy)
   108  		}
   109  	}
   110  
   111  	{
   112  		// 清理2天前上传的临时文件,释放空间
   113  		uploadExpirePath := tmpPath + "/" + gtime.Now().AddDate(0, 0, -2).Format("Ymd")
   114  		if gfile.Exists(uploadExpirePath) {
   115  			gfile.Remove(uploadExpirePath)
   116  		}
   117  	}
   118  
   119  	newUserUploadItemsCache := kmap.New[int64, *sys_model.FileInfo]()
   120  	strUserId := gconv.String(sessionUser.Id)
   121  	// 逻辑修改
   122  	userCacheJsonPath := gfile.Join(tmpPath, s.cachePrefix+"_"+strUserId+".json") // tmpPath : 没有年月日
   123  	//userCacheJsonPath := gfile.Join(uploadPath, s.cachePrefix+"_"+strUserId+".json") // uploadPath:有年月日
   124  	{
   125  		// 用户指定时间内上传文件最大数量限制
   126  		userUploadInfoCache := kmap.New[int64, *sys_model.FileInfo]()
   127  		jsonString := gfile.GetContents(userCacheJsonPath)
   128  
   129  		g.Try(ctx, func(ctx context.Context) {
   130  			gjson.DecodeTo(jsonString, &userUploadInfoCache)
   131  		})
   132  
   133  		now := gtime.Now()
   134  		userUploadInfoCache.Iterator(func(k int64, item *sys_model.FileInfo) bool {
   135  			if item.ExpiresAt.Add(s.CacheDuration).After(now) {
   136  				newUserUploadItemsCache.Set(item.Id, item)
   137  			}
   138  			return true
   139  		})
   140  
   141  		fileMaxUploadCountMinute := g.Cfg().MustGet(ctx, "service.fileMaxUploadCountMinute", 10).Int()
   142  		// 限定1分钟内允许上传的最大数量
   143  		if newUserUploadItemsCache.Size() >= fileMaxUploadCountMinute {
   144  			return nil, sys_service.SysLogs().ErrorSimple(ctx, gerror.New("您上传得太频繁,请稍后再操作"), "", sys_dao.SysFile.Table())
   145  		}
   146  	}
   147  
   148  	if in.Name != "" {
   149  		in.File.Filename = in.Name
   150  	}
   151  	// 将文件写入临时目录
   152  	id := idgen.NextId()
   153  	idStr := gconv.String(id)
   154  	dateDirName := uploadPath
   155  	gfile.Chmod(dateDirName, gfile.DefaultPermCopy)
   156  	savePath := gfile.Join(dateDirName, idStr)
   157  	fileName, err := in.File.Save(savePath, in.RandomName)
   158  	if err != nil {
   159  		return nil, sys_service.SysLogs().ErrorSimple(ctx, err, "文件保存失败", sys_dao.SysFile.Table())
   160  	}
   161  
   162  	absPath := gfile.Join(savePath, fileName)
   163  	data := &sys_model.FileInfo{
   164  		SysFile: sys_entity.SysFile{
   165  			Id:          id,
   166  			Name:        fileName,
   167  			Src:         absPath,
   168  			Url:         absPath,
   169  			LocalPath:   absPath,
   170  			Ext:         gfile.Ext(absPath),
   171  			Size:        in.File.Size,
   172  			Category:    "",
   173  			UserId:      sessionUser.Id,
   174  			UnionMainId: sessionUser.UnionMainId,
   175  			CreatedAt:   gtime.Now(),
   176  			UpdatedAt:   nil,
   177  		},
   178  		ExpiresAt: gtime.Now().Add(s.CacheDuration),
   179  	}
   180  
   181  	// 缓存前的Hook广播
   182  	g.Try(ctx, func(ctx context.Context) {
   183  		for _, hook := range s.hookArr {
   184  			if hook.Value.Key.Code()&sys_enum.Upload.EventState.BeforeCache.Code() == sys_enum.Upload.EventState.BeforeCache.Code() {
   185  				hook.Value.Value(ctx, sys_enum.Upload.EventState.BeforeCache, &data.SysFile)
   186  			}
   187  		}
   188  	})
   189  
   190  	// 追加到缓存队列
   191  	newUserUploadItemsCache.Set(data.Id, data)
   192  
   193  	// 写入缓存文件到指定的文件路径
   194  	gfile.PutContents(userCacheJsonPath, gjson.MustEncodeString(newUserUploadItemsCache)) // 路径
   195  
   196  	// 缓存后的Hook广播
   197  	g.Try(ctx, func(ctx context.Context) {
   198  		for _, hook := range s.hookArr {
   199  			if hook.Value.Key.Code()&sys_enum.Upload.EventState.AfterCache.Code() == sys_enum.Upload.EventState.AfterCache.Code() {
   200  				hook.Value.Value(ctx, sys_enum.Upload.EventState.AfterCache, &data.SysFile)
   201  			}
   202  		}
   203  	})
   204  
   205  	//data.Url = s.MakeFileUrl(ctx,data.Id)
   206  	// data.Url = 远程接口地址
   207  	return &data.SysFile, nil
   208  }
   209  
   210  // GetUploadFile 根据上传ID 获取上传文件信息
   211  func (s *sFile) GetUploadFile(ctx context.Context, uploadId int64, userId int64, message ...string) (*sys_model.FileInfo, error) {
   212  	strUserId := gconv.String(userId)
   213  	tempUploadPath := g.Cfg().MustGet(ctx, "upload.tempPath").String()
   214  	// tempUploadPath := g.Cfg().MustGet(ctx, "upload.tempPath").String() + "/" + gtime.Now().Format("Ymd") + "/"
   215  	// 获取系统默认的临时文件的存储路径
   216  	tmpPath := gfile.Temp("upload")
   217  	if tempUploadPath == "" {
   218  		tempUploadPath = tmpPath
   219  	}
   220  
   221  	userCacheJsonPath := gfile.Join(tempUploadPath, s.cachePrefix+"_"+strUserId+".json")
   222  
   223  	userUploadInfoCache := kmap.New[int64, *sys_model.FileInfo]()
   224  
   225  	gjson.DecodeTo(gfile.GetContents(userCacheJsonPath), &userUploadInfoCache)
   226  
   227  	// 从oss获取: upload_8610476923551813.json
   228  	if userUploadInfoCache.IsEmpty() {
   229  		//bucketName := g.Cfg().MustGet(ctx, "oss.bucketName").String() // 各端的oss
   230  		bucketName := g.Cfg().MustGet(ctx, "oss.masterBucketName").String() // 平台的oss
   231  		url, _ := s.GetOssFileSingUrl(ctx, bucketName, userCacheJsonPath)
   232  
   233  		if url != "" {
   234  			// 3、根据文件的签名URL,直接io读取文件,然后解析使用
   235  			v, err := http.Get(url)
   236  			if err != nil {
   237  				return nil, sys_service.SysLogs().WarnSimple(ctx, err, "Http get ["+url+"] failed!", sys_dao.SysFile.Table())
   238  			}
   239  			defer v.Body.Close()
   240  			// 读取文件字节流
   241  			content, err := io.ReadAll(v.Body)
   242  			if err != nil {
   243  				return nil, sys_service.SysLogs().WarnSimple(ctx, err, "Read http response failed! "+url, sys_dao.SysFile.Table())
   244  			}
   245  			if string(content) != "" {
   246  				// 将云存储中下载到本地的文件读取内容出来,序列化到userUploadInfoCache
   247  				gjson.DecodeTo(string(content), &userUploadInfoCache)
   248  			}
   249  
   250  			// 3、根据文件的签名URL,下载文件到本地 (Pass,本地带宽压力大)
   251  			// 临时的转载路径
   252  			//filePath := "temp/download" + "/" + gtime.Now().Format("Ymd") ( userCacheJson.json路径不需要加Ymd)
   253  			//filePath := g.Cfg().MustGet(ctx, "download.tempPath", "temp/download").String()
   254  			//
   255  			//// 目录不存在则创建
   256  			//if !gfile.Exists(filePath) {
   257  			//	gfile.Mkdir(filePath)
   258  			//	gfile.Chmod(filePath, gfile.DefaultPermCopy)
   259  			//}
   260  			//
   261  			//filePath += "/" + s.cachePrefix + "_" + strUserId + ".json"
   262  
   263  			//info.LocalPath = filePath
   264  
   265  			//withURL, err := s.GetOssFileWithURL(ctx, bucketName, filePath, url)
   266  			//if err != nil {
   267  			//	fmt.Println(err)
   268  			//}
   269  			//if withURL {
   270  			//	// 将云存储中下载到本地的文件读取内容出来,序列化到userUploadInfoCache
   271  			//	gjson.DecodeTo(gfile.GetContents(filePath), &userUploadInfoCache)
   272  			//}
   273  		}
   274  	}
   275  
   276  	messageStr := "文件不存在"
   277  	if len(message) > 0 {
   278  		messageStr = message[0]
   279  	}
   280  
   281  	item, has := userUploadInfoCache.Search(uploadId)
   282  	if item != nil && has {
   283  		return item, nil
   284  	}
   285  
   286  	return nil, sys_service.SysLogs().ErrorSimple(ctx, nil, messageStr, sys_dao.SysFile.Table())
   287  }
   288  
   289  // SaveFile 保存文件
   290  func (s *sFile) SaveFile(ctx context.Context, storageAddr string, info *sys_model.FileInfo) (*sys_model.FileInfo, error) {
   291  
   292  	if gfile.GetContents(info.Src) == "" { // oss云存储 (TODO 注意:如果部署了NFS,那么获取文件就是获取得到的)
   293  		// 1.获取远端的临时temp文件,2.进行拉取下载到本地temp目录,3.然后将持久化 oss + 本地
   294  		//bucketName := g.Cfg().MustGet(ctx, "oss.bucketName").String()
   295  		bucketName := g.Cfg().MustGet(ctx, "oss.masterBucketName").String()
   296  		url, _ := s.GetOssFileSingUrl(ctx, bucketName, info.Src)
   297  
   298  		if url != "" { // TODO 优化:不要下载,直接通过url读取文件内容,然后写到目标路径 storageAddr (暂未优化)
   299  			filePath := g.Cfg().MustGet(ctx, "download.tempPath", "temp/download").String()
   300  			filePath += "/" + gtime.Now().Format("Ymd") + "/" + gconv.String(info.Id)
   301  
   302  			// 目录不存在则创建
   303  			if !gfile.Exists(filePath) {
   304  				gfile.Mkdir(filePath)
   305  				gfile.Chmod(filePath, gfile.DefaultPermCopy)
   306  			}
   307  
   308  			filePath += "/" + info.Name
   309  
   310  			{
   311  				// 清理2天前下载的临时文件,释放空间
   312  				downloadExpirePath := g.Cfg().MustGet(ctx, "download.tempPath", "temp/download").String() + "/" + gtime.Now().AddDate(0, 0, -2).Format("Ymd")
   313  				if gfile.Exists(downloadExpirePath) {
   314  					gfile.Remove(downloadExpirePath)
   315  				}
   316  			}
   317  
   318  			withURL, err := s.GetOssFileWithURL(ctx, bucketName, filePath, url)
   319  			if err != nil {
   320  				fmt.Println(err)
   321  			}
   322  			if withURL {
   323  				info.Src = filePath
   324  			}
   325  		}
   326  	}
   327  
   328  	if !gfile.Exists(info.Src) { // oss 不存在, 本地也不存在
   329  		return nil, sys_service.SysLogs().ErrorSimple(ctx, nil, "文件不存在", sys_dao.SysFile.Table())
   330  	}
   331  
   332  	if storageAddr == info.Src {
   333  		return info, nil
   334  	}
   335  
   336  	// 文件保存前的Hook
   337  	g.Try(ctx, func(ctx context.Context) {
   338  		for _, hook := range s.hookArr {
   339  			if hook.Value.Key.Code()&sys_enum.Upload.EventState.BeforeSave.Code() == sys_enum.Upload.EventState.BeforeSave.Code() {
   340  				hook.Value.Value(ctx, sys_enum.Upload.EventState.BeforeSave, &info.SysFile)
   341  			}
   342  		}
   343  	})
   344  
   345  	gfile.Chmod(gfile.Dir(storageAddr), gfile.DefaultPermCopy)
   346  	if err := gfile.CopyFile(info.Src, storageAddr); err != nil {
   347  		return nil, sys_service.SysLogs().ErrorSimple(ctx, err, "文件保存失败", sys_dao.SysFile.Table())
   348  	}
   349  
   350  	// 记录到数据表
   351  	data := kconv.Struct(info.SysFile, &sys_do.SysFile{})
   352  	data.Src = storageAddr
   353  	//data.Url = storageAddr // 优化于2024年0509,URL一般会放远端的文件存储空间的URL,通过Hook修改
   354  
   355  	count, err := sys_dao.SysFile.Ctx(ctx).Where(sys_do.SysFile{Id: data.Id}).Count()
   356  	if count == 0 {
   357  		_, err = sys_dao.SysFile.Ctx(ctx).Data(data).OmitEmpty().Insert()
   358  	} else {
   359  		_, err = sys_dao.SysFile.Ctx(ctx).Data(data).OmitEmpty().Where(sys_do.SysFile{Id: data.Id}).Update()
   360  	}
   361  
   362  	if err != nil {
   363  		return nil, sys_service.SysLogs().ErrorSimple(ctx, err, "文件保存失败", sys_dao.SysFile.Table())
   364  	}
   365  
   366  	// 文件保存后的Hook
   367  	g.Try(ctx, func(ctx context.Context) {
   368  		for _, hook := range s.hookArr { // 微服务模式:同步持久化到oss
   369  			if hook.Value.Key.Code()&sys_enum.Upload.EventState.AfterSave.Code() == sys_enum.Upload.EventState.AfterSave.Code() {
   370  				hook.Value.Value(ctx, sys_enum.Upload.EventState.AfterSave, kconv.Struct(data, &sys_entity.SysFile{}))
   371  			}
   372  		}
   373  	})
   374  	return info, nil
   375  }
   376  
   377  // UploadIDCard 上传身份证照片
   378  func (s *sFile) UploadIDCard(ctx context.Context, in sys_model.OCRIDCardFileUploadInput) (*sys_model.IDCardWithOCR, error) {
   379  
   380  	result, err := s.Upload(ctx, in.FileUploadInput)
   381  
   382  	if err != nil {
   383  		return nil, err
   384  	}
   385  
   386  	ret := sys_model.IDCardWithOCR{
   387  		SysFile: *result,
   388  	}
   389  
   390  	fileBase64, err := gbase64.EncodeFileToString(result.Src)
   391  
   392  	if err != nil {
   393  		return &ret, sys_service.SysLogs().ErrorSimple(ctx, nil, "解析证照信息失败", sys_dao.SysFile.Table())
   394  	}
   395  
   396  	imageBase64 := fileBase64
   397  
   398  	OCRInfo, err := sys_service.SdkBaidu().OCRIDCard(ctx, imageBase64, in.DetectRisk, in.IDCardSide)
   399  
   400  	if err != nil {
   401  		return &ret, err
   402  	}
   403  
   404  	//	ret.Id = result.Id
   405  	ret.OCRIDCardA = OCRInfo.OCRIDCardA
   406  	ret.OCRIDCardB = OCRInfo.OCRIDCardB
   407  
   408  	return &ret, nil
   409  }
   410  
   411  // UploadBankCard 上传银行卡照片
   412  func (s *sFile) UploadBankCard(ctx context.Context, in sys_model.BankCardWithOCRInput) (*sys_model.BankCardWithOCR, error) {
   413  	result, err := s.Upload(ctx, in.FileUploadInput)
   414  
   415  	if err != nil {
   416  		return nil, err
   417  	}
   418  
   419  	ret := sys_model.BankCardWithOCR{
   420  		SysFile: *result,
   421  	}
   422  
   423  	// 图片数据进行base64编码
   424  	fileBase64, err := gbase64.EncodeFileToString(result.Src)
   425  
   426  	if err != nil {
   427  		return &ret, sys_service.SysLogs().ErrorSimple(ctx, nil, "解析证照信息失败", sys_dao.SysFile.Table())
   428  	}
   429  
   430  	bankCard, err := sys_service.SdkBaidu().OCRBankCard(ctx, fileBase64)
   431  
   432  	if err != nil {
   433  		return &ret, err
   434  	}
   435  
   436  	// 给返回数据赋值
   437  	ret.BaiduSdkOCRBankCard.OCRBankCard = *bankCard
   438  	// ret.Id = result.Id
   439  	return &ret, nil
   440  }
   441  
   442  // UploadBusinessLicense 上传营业执照照片
   443  func (s *sFile) UploadBusinessLicense(ctx context.Context, in sys_model.OCRBusinessLicense) (*sys_model.BusinessLicenseWithOCR, error) {
   444  	result, err := s.Upload(ctx, in.FileUploadInput)
   445  
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  
   450  	ret := sys_model.BusinessLicenseWithOCR{
   451  		SysFile: *result,
   452  	}
   453  
   454  	fileBase64, err := gbase64.EncodeFileToString(result.Src)
   455  
   456  	if err != nil {
   457  		return &ret, sys_service.SysLogs().ErrorSimple(ctx, gerror.New("解析证照信息失败"), "", sys_dao.SysFile.Table())
   458  	}
   459  
   460  	imageBase64 := fileBase64
   461  
   462  	OCRInfo, err := sys_service.SdkBaidu().OCRBusinessLicense(ctx, imageBase64)
   463  
   464  	if err != nil {
   465  		return &ret, err
   466  	}
   467  
   468  	ret.BusinessLicenseOCR = *OCRInfo
   469  	//	ret.Id = result.Id
   470  	return &ret, nil
   471  }
   472  
   473  // DownLoadFile 下载文件
   474  func (s *sFile) DownLoadFile(ctx context.Context, savePath string, url string) (string, error) { // 文件获取url
   475  	// 校验目标存储路径是否存在
   476  	if !gfile.Exists(gfile.Dir(savePath)) {
   477  		return "", sys_service.SysLogs().WarnSimple(ctx, nil, "The save path does not exist! "+savePath, sys_dao.SysFile.Table())
   478  	}
   479  
   480  	tmpPath := g.Cfg().MustGet(ctx, "download.tempPath", "temp/download").String()
   481  
   482  	gfile.Mkdir(tmpPath)
   483  	gfile.Chmod(tmpPath, 755)
   484  
   485  	tmpPath = gfile.Join(tmpPath, gconv.String(idgen.NextId()))
   486  
   487  	// 通过http获取文件
   488  	v, err := http.Get(url)
   489  	if err != nil {
   490  		return "", sys_service.SysLogs().WarnSimple(ctx, err, "Http get ["+url+"] failed!", sys_dao.SysFile.Table())
   491  	}
   492  	defer v.Body.Close()
   493  	// 读取文件字节流
   494  	content, err := io.ReadAll(v.Body)
   495  	if err != nil {
   496  		return "", sys_service.SysLogs().WarnSimple(ctx, err, "Read http response failed! "+url, sys_dao.SysFile.Table())
   497  	}
   498  	// 写入临时路径
   499  	err = os.WriteFile(tmpPath, content, 755)
   500  	if err != nil {
   501  		return "", sys_service.SysLogs().WarnSimple(ctx, err, "Save to sys_file failed! "+url, sys_dao.SysFile.Table())
   502  	}
   503  	// 将临时文件的文件拷贝到目标路径中
   504  	if nil != gfile.CopyFile(tmpPath, savePath) {
   505  		return "", sys_service.SysLogs().WarnSimple(ctx, err, "Copy sys_file failed! "+savePath, sys_dao.SysFile.Table())
   506  	}
   507  
   508  	return savePath, nil
   509  }
   510  
   511  // GetFileById 根据id获取并返回文件信息
   512  func (s *sFile) GetFileById(ctx context.Context, id int64, errorMessage string) (*sys_model.FileInfo, error) { // 获取图片可以是id、token、路径
   513  	sessionUser := sys_service.SysSession().Get(ctx).JwtClaimsUser
   514  
   515  	{
   516  		// 优先尝试从缓存获取图片 (缓存查找)
   517  		cacheFile, _ := s.GetUploadFile(ctx, id, sessionUser.Id, errorMessage)
   518  
   519  		if cacheFile != nil {
   520  			//cacheFile.Url = s.MakeFileUrl(ctx, cacheFile.Id)
   521  			//cacheFile.LocalPath = s.MakeFileUrl(ctx, cacheFile.Id) // TODO 20240506
   522  			//cacheFile.LocalPath = s.MakeFileUrlByPath(ctx, cacheFile.Src) // 20240509 没什么必要吧,SRC就是本地的路径
   523  			cacheFile.LocalPath = cacheFile.Src // 20240509 本地路径 == src
   524  			return cacheFile, nil
   525  		}
   526  	}
   527  	{
   528  		file := &sys_entity.SysFile{}
   529  		model := sys_dao.SysFile.Ctx(ctx).
   530  			Where(sys_do.SysFile{Id: id})
   531  
   532  		if sessionUser.IsAdmin == false {
   533  			// 判断用户是否有权限
   534  			can, _ := sys_service.SysPermission().CheckPermission(ctx, sys_enum.File.PermissionType.ViewDetail)
   535  			if can == false {
   536  				model = model.Where(sys_do.SysFile{UnionMainId: sessionUser.UnionMainId})
   537  			}
   538  		}
   539  		err := model.Scan(file)
   540  
   541  		if err != nil {
   542  			return nil, sys_service.SysLogs().WarnSimple(ctx, err, errorMessage, sys_dao.SysFile.Table())
   543  		}
   544  
   545  		//file.Url = s.GetUrlById(file.Id)
   546  		file.LocalPath = s.MakeFileUrl(ctx, file.Id)
   547  
   548  		return &sys_model.FileInfo{
   549  			SysFile:   *file,
   550  			ExpiresAt: nil,
   551  		}, nil
   552  	}
   553  }
   554  
   555  // MakeFileUrl 图像id换取url: 拼接三个参数,缓存fileInfo、然后返回url + 三参
   556  func (s *sFile) MakeFileUrl(ctx context.Context, id int64) string {
   557  	file := &sys_entity.SysFile{}
   558  	err := sys_dao.SysFile.Ctx(ctx).
   559  		Where(sys_do.SysFile{Id: id}).Scan(file)
   560  
   561  	if err != nil {
   562  		return ""
   563  	}
   564  
   565  	sign := makeSign(file.Src, file.Id)
   566  	srcBase64 := string(gbase64.Encode([]byte(file.Src)))
   567  	fileId := gconv.String(file.Id)
   568  
   569  	// 获取到api接口前缀
   570  	apiPrefix := sys_consts.Global.ApiPreFix
   571  
   572  	// 拼接请求url
   573  	return apiPrefix + "/common/getFile?sign=" + sign + "&path=" + srcBase64 + "&id=" + fileId // id: oss.masterBucketName
   574  }
   575  
   576  // MakeFileUrlByPath 文件path换取url: 拼接三个参数,缓存签名数据、然后返回url + 三参
   577  func (s *sFile) MakeFileUrlByPath(ctx context.Context, path string) string {
   578  	cId := idgen.NextId()
   579  	sign := makeSign(path, cId)
   580  
   581  	srcBase64 := string(gbase64.Encode([]byte(path)))
   582  	fileId := gconv.String(cId)
   583  
   584  	// 获取到api接口前缀
   585  	apiPrefix := sys_consts.Global.ApiPreFix
   586  
   587  	// 拼接请求url
   588  	return apiPrefix + "/common/getFile?sign=" + sign + "&path=" + srcBase64 + "&cid=" + fileId // cid: oss.bucketName
   589  }
   590  
   591  // GetFile 获取图片 公开  (srcBase64 + srcMd5 + fileId) ==> md5加密
   592  func (s *sFile) GetFile(ctx context.Context, sign, srcBase64 string, id int64, cId int64) (*sys_model.FileInfo, error) {
   593  	// oss --> id: oss.masterBucketName  cid: oss.bucketName
   594  	if cId != 0 { // 缓存
   595  		// 验签
   596  		srcDecode, _ := gbase64.DecodeToString(srcBase64)
   597  		checkSign := makeSign(srcDecode, cId)
   598  		// 校验签名
   599  		if sign != checkSign {
   600  			return nil, sys_service.SysLogs().WarnSimple(ctx, nil, "签名校验失败", sys_dao.SysFile.Table())
   601  		}
   602  
   603  		// 资源:本地|oss远端
   604  		//if !gfile.IsFile(srcDecode) { // 跨进程资源,就会获取不到资源,所以需要查询oss的云存储空间
   605  		if gfile.GetContents(srcDecode) == "" { // 跨进程资源,就会获取不到资源,所以需要查询oss的云存储空间
   606  
   607  			//s.GetUploadFile(ctx, cId,) userId 拿不到
   608  			//split := gstr.Split(srcDecode, "/")
   609  
   610  			// 从oss 获取
   611  			bucketName := g.Cfg().MustGet(ctx, "oss.masterBucketName").String()
   612  			url, _ := s.GetOssFileSingUrl(ctx, bucketName, srcDecode)
   613  
   614  			if url != "" {
   615  				srcDecode = url
   616  
   617  				/*
   618  					oos 目的就是:降低服务器的带宽压力,所以不要下载到本地。直接使用url跳转
   619  					filePath := "temp/download"
   620  					filePath += "/" + gtime.Now().Format("Ymd") + "/" + gconv.String(cId)
   621  
   622  					//filePath := srcDecode
   623  
   624  					// 目录不存在则创建
   625  					if !gfile.Exists(filePath) {
   626  						gfile.Mkdir(filePath)
   627  						gfile.Chmod(filePath, gfile.DefaultPermCopy)
   628  					}
   629  
   630  					filePath += "/" + split[len(split)-1]
   631  
   632  					withURL, err := s.GetOssFileWithURL(ctx, bucketName, filePath, url)
   633  					if err != nil {
   634  						fmt.Println(err)
   635  					}
   636  
   637  					if withURL {
   638  						srcDecode = filePath
   639  					}
   640  				*/
   641  			}
   642  		}
   643  
   644  		//if !gfile.IsFile(srcDecode) {
   645  		//	return nil, sys_service.SysLogs().WarnSimple(ctx, nil, "文件不存在", sys_dao.SysFile.Table())
   646  		//}
   647  
   648  		// 签名通过,直接根据src返回
   649  		return &sys_model.FileInfo{
   650  			SysFile: sys_entity.SysFile{
   651  				Src: srcDecode,
   652  			},
   653  		}, nil
   654  	}
   655  
   656  	// 优先从缓存获取,缓存要是获取不到,那么从数据库加载文件信息,从而加载文件
   657  	// 先获取图片,进行签名、验签,验签通过查找图片,如果不在缓存中的图片从数据库查询后进行缓存起来  缓存key == sign
   658  	fileInfo := daoctl.GetById[sys_entity.SysFile](sys_dao.SysFile.Ctx(ctx), id)
   659  	if fileInfo == nil {
   660  		return nil, sys_service.SysLogs().WarnSimple(ctx, nil, "文件不存在,请检查id", sys_dao.SysFile.Table())
   661  	}
   662  
   663  	{
   664  		srcDecode, _ := gbase64.DecodeToString(srcBase64)
   665  		checkSign := makeSign(srcDecode, id)
   666  		// 校验签名
   667  		if sign != checkSign {
   668  			return nil, sys_service.SysLogs().WarnSimple(ctx, nil, "签名校验失败", sys_dao.SysFile.Table())
   669  		}
   670  	}
   671  
   672  	{
   673  		// 优先尝试从缓存获取图片 (缓存查找,sign为key)
   674  		//cacheFile, _ := g.Redis().Get(ctx, sign) // redis 缓存
   675  		cacheFile, _ := g.DB().GetCache().Get(ctx, sign) // 内存缓存
   676  		if cacheFile.Val() != nil {
   677  			data := sys_model.FileInfo{}
   678  
   679  			gconv.Struct(cacheFile, &data)
   680  			return &data, nil
   681  		}
   682  	}
   683  
   684  	{
   685  		// 从数据库找,找到后缓存起来
   686  
   687  		/*
   688  			拉取图片方案,根据src判断是本地还是远端的图片:
   689  				如果是存在于本地的图片,直接拉出来,然后进行缓存,将Src赋值到cachePath上面
   690  				如果是存在于远端的图片,根据src从远端获取至本地的temp目录下面,然后将cachePath缓存本地的临时路径
   691  		*/
   692  
   693  		// 判断是否是本地图片
   694  		file := gfile.GetContents(fileInfo.Src) // 后期这里应该是cachePath
   695  
   696  		if file != "" { // 本地
   697  			data := sys_model.FileInfo{
   698  				SysFile:   *fileInfo,
   699  				ExpiresAt: gtime.New(time.Hour * 24),
   700  			}
   701  
   702  			g.DB().GetCache().Set(ctx, sign, data, time.Hour*24)
   703  
   704  			return &data, nil
   705  		} else { // 远端
   706  			// 获取远端图片,进行拉取到本地temp目录,然后将src赋值 (Pref:不需要下载到本地,直接使用oss的url)
   707  
   708  			// bucketName := g.Cfg().MustGet(ctx, "oss.bucketName").String() // 各端资源目录
   709  			// TODO 问题:不能直接这样拿,活动图片是商家上传的,如果直接这样的话,取资源的oss路径就是operator了,实际上应该是merchant的oss资源路径。
   710  
   711  			// 方案1: 需要有一个主体&oss-bucket的映射表
   712  			// 方案2: fileInfo,存储bucket的bucketDomain 或者bucket-name
   713  			split := gstr.Split(fileInfo.Url, ".") // mlj-merchant-service.oss-cn-shenzhen.aliyuncs.com
   714  			bucketName := split[0]
   715  			url, _ := s.GetOssFileSingUrl(ctx, bucketName, fileInfo.Src)
   716  
   717  			// TODO 本来我想直接使用url作为src输出,但是这个方法g.RequestFromCtx(ctx).Response.ServeFile(file.Src) 不支持输出网络路径的图片,所以需要临时下载 (Pref 不需要,直接使用这个url,进行Redirect跳转即可)
   718  			if url != "" {
   719  				fileInfo.Src = url
   720  
   721  				//filePath := "temp/download"
   722  				//filePath += "/" + gtime.Now().Format("Ymd") + "/" + gconv.String(cId)
   723  				//
   724  				//// 目录不存在则创建
   725  				//if !gfile.Exists(filePath) {
   726  				//	gfile.Mkdir(filePath)
   727  				//	gfile.Chmod(filePath, gfile.DefaultPermCopy)
   728  				//}
   729  				//
   730  				//filePath += "/" + fileInfo.Name
   731  				//
   732  				//withURL, err := s.GetOssFileWithURL(ctx, bucketName, filePath, url)
   733  				//if err != nil {
   734  				//	fmt.Println(err)
   735  				//}
   736  				//
   737  				//if withURL {
   738  				//	fileInfo.Src = filePath
   739  				//}
   740  			}
   741  
   742  			data := sys_model.FileInfo{
   743  				SysFile:   *fileInfo,
   744  				ExpiresAt: gtime.New(time.Hour * 24),
   745  			}
   746  
   747  			g.DB().GetCache().Set(ctx, sign, data, time.Hour*24)
   748  
   749  			return &data, nil
   750  			//fileInfo.LocalPath = fileInfo.Src
   751  
   752  			//daoctl.UpdateWithError(sys_dao.SysFile.Ctx(ctx).Where(sys_dao.SysFile.Columns().Id, fileInfo.Id).Data(sys_do.SysFile{CachePath: fileInfo.CachePath}))
   753  		}
   754  	}
   755  
   756  	return nil, nil
   757  }
   758  
   759  // 签名数据,组成部分:(srcBase64 + srcMd5 + fileId) ==> md5加密
   760  func makeSign(fileSrc string, id int64) string {
   761  	srcBase64 := string(gbase64.Encode([]byte(fileSrc)))
   762  	srcMd5 := crypto.Md5Hash(fileSrc)
   763  	fileId := string(id)
   764  
   765  	cryptoData := srcBase64 + srcMd5 + fileId
   766  	checkSign := crypto.Md5Hash(cryptoData)
   767  
   768  	return checkSign
   769  }
   770  
   771  // UploadPicture 上传图片并审核
   772  func (s *sFile) UploadPicture(ctx context.Context, input sys_model.PictureWithOCRInput) (*sys_model.PictureWithOCR, error) {
   773  
   774  	result, err := s.Upload(ctx, input.FileUploadInput)
   775  
   776  	if err != nil {
   777  		return nil, err
   778  	}
   779  
   780  	ret := sys_model.PictureWithOCR{
   781  		SysFile: *result,
   782  		Data:    make([]sys_model.DescriptionData, 0),
   783  	}
   784  
   785  	fileBase64, err := gbase64.EncodeFileToString(result.Src)
   786  
   787  	if err != nil {
   788  		return &ret, sys_service.SysLogs().ErrorSimple(ctx, nil, "图片审核失败", sys_dao.SysFile.Table())
   789  	}
   790  
   791  	imageBase64 := fileBase64
   792  
   793  	PictureInfo, err := sys_service.SdkBaidu().AuditPicture(ctx, imageBase64, input.ImageType)
   794  
   795  	if err != nil {
   796  		return &ret, err
   797  	}
   798  
   799  	ret.Conclusion = PictureInfo.Conclusion
   800  	ret.ConclusionType = PictureInfo.ConclusionType
   801  	ret.Data = PictureInfo.Data
   802  
   803  	return &ret, err
   804  }
   805  
   806  // GetOssFileSingUrl 获取文件的签名访问URL
   807  func (s *sFile) GetOssFileSingUrl(ctx context.Context, bucketName, objectKey string) (string, error) {
   808  	modules := oss_global.Global.Modules
   809  
   810  	// 1、获取默认的渠道商 (优先级最高的渠道商)
   811  	provider, err := modules.OssServiceProviderConfig().GetProviderByPriority(ctx, 1)
   812  	if err != nil {
   813  		return "", err
   814  	}
   815  
   816  	// 2、获取存储对象Bucket
   817  	bucketConfig, err := modules.OssBucketConfig().GetByBucketNameAndProviderNo(ctx, bucketName, provider.ProviderNo, 1)
   818  	if err != nil {
   819  		return "", err
   820  	}
   821  
   822  	// 24小时过期
   823  	duration := gconv.Int64(60 * 60 * 24)
   824  
   825  	reqInfo := oss_api.GetFileSingURLReq{
   826  		GetFileSingURL: oss_model.GetFileSingURL{
   827  			MustInfo: oss_model.MustInfo{
   828  				ProviderId: provider.Id,
   829  				ProviderNo: provider.ProviderNo,
   830  				BucketName: bucketConfig.BucketName,
   831  			},
   832  			ObjectKey:    objectKey,
   833  			ExpiredInSec: duration,
   834  		},
   835  	}
   836  
   837  	// 2、调用oss 进行请求
   838  	ret, err := oss_controller.OssFile(modules).GetFileSingURL(ctx, &reqInfo)
   839  
   840  	return (string)(ret), err
   841  }
   842  
   843  // GetOssFileWithURL 根据文件的签名访问URL获取文件
   844  func (s *sFile) GetOssFileWithURL(ctx context.Context, bucketName, filePath, singUrl string) (bool, error) {
   845  	modules := oss_global.Global.Modules
   846  
   847  	// 1、获取默认的渠道商 (优先级最高的渠道商)
   848  	provider, err := modules.OssServiceProviderConfig().GetProviderByPriority(ctx, 1)
   849  	if err != nil {
   850  		return false, err
   851  	}
   852  
   853  	// 2、获取存储对象Bucket
   854  	bucketConfig, err := modules.OssBucketConfig().GetByBucketNameAndProviderNo(ctx, bucketName, provider.ProviderNo, 1)
   855  	if err != nil {
   856  		return false, err
   857  	}
   858  
   859  	reqInfo := oss_api.GetObjectToFileWithURLReq{
   860  		GetObjectToFileWithURL: oss_model.GetObjectToFileWithURL{
   861  			MustInfo: oss_model.MustInfo{
   862  				ProviderId: provider.Id,
   863  				ProviderNo: provider.ProviderNo,
   864  				BucketName: bucketConfig.BucketName,
   865  			},
   866  			FilePath: filePath,
   867  			SingUrl:  singUrl,
   868  		},
   869  	}
   870  
   871  	// 2、调用oss 进行请求
   872  	ret, err := oss_controller.OssFile(modules).GetObjectToFileWithURL(ctx, &reqInfo)
   873  
   874  	return ret == true, err
   875  }