初始
This commit is contained in:
106
library/libResponse/response.go
Normal file
106
library/libResponse/response.go
Normal file
@ -0,0 +1,106 @@
|
||||
package libResponse
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gview"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
const (
|
||||
SuccessCode int = 0
|
||||
ErrorCode int = -1
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
// 代码
|
||||
Code int `json:"code" example:"200"`
|
||||
// 数据集
|
||||
Data interface{} `json:"data"`
|
||||
// 消息
|
||||
Msg string `json:"message"`
|
||||
}
|
||||
|
||||
var response = new(Response)
|
||||
|
||||
func JsonExit(r *ghttp.Request, code int, msg string, data ...interface{}) {
|
||||
response.JsonExit(r, code, msg, data...)
|
||||
}
|
||||
|
||||
func RJson(r *ghttp.Request, code int, msg string, data ...interface{}) {
|
||||
response.RJson(r, code, msg, data...)
|
||||
}
|
||||
|
||||
func SusJson(isExit bool, r *ghttp.Request, msg string, data ...interface{}) {
|
||||
response.SusJson(isExit, r, msg, data...)
|
||||
}
|
||||
|
||||
func FailJson(isExit bool, r *ghttp.Request, msg string, data ...interface{}) {
|
||||
response.FailJson(isExit, r, msg, data...)
|
||||
}
|
||||
|
||||
func WriteTpl(r *ghttp.Request, tpl string, view *gview.View, params ...gview.Params) error {
|
||||
return response.WriteTpl(r, tpl, view, params...)
|
||||
}
|
||||
|
||||
// 返回JSON数据并退出当前HTTP执行函数。
|
||||
func (res *Response) JsonExit(r *ghttp.Request, code int, msg string, data ...interface{}) {
|
||||
res.RJson(r, code, msg, data...)
|
||||
r.ExitAll()
|
||||
}
|
||||
|
||||
// 标准返回结果数据结构封装。
|
||||
// 返回固定数据结构的JSON:
|
||||
// code: 状态码(200:成功,302跳转,和http请求状态码一至);
|
||||
// msg: 请求结果信息;
|
||||
// data: 请求结果,根据不同接口返回结果的数据结构不同;
|
||||
func (res *Response) RJson(r *ghttp.Request, code int, msg string, data ...interface{}) {
|
||||
responseData := interface{}(nil)
|
||||
if len(data) > 0 {
|
||||
responseData = data[0]
|
||||
}
|
||||
r.Response.WriteJson(&Response{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
Data: responseData,
|
||||
})
|
||||
}
|
||||
|
||||
// 成功返回JSON
|
||||
func (res *Response) SusJson(isExit bool, r *ghttp.Request, msg string, data ...interface{}) {
|
||||
if isExit {
|
||||
res.JsonExit(r, SuccessCode, msg, data...)
|
||||
}
|
||||
res.RJson(r, SuccessCode, msg, data...)
|
||||
}
|
||||
|
||||
// 失败返回JSON
|
||||
func (res *Response) FailJson(isExit bool, r *ghttp.Request, msg string, data ...interface{}) {
|
||||
if isExit {
|
||||
res.JsonExit(r, ErrorCode, msg, data...)
|
||||
}
|
||||
res.RJson(r, ErrorCode, msg, data...)
|
||||
}
|
||||
|
||||
// WriteTpl 模板输出
|
||||
func (res *Response) WriteTpl(r *ghttp.Request, tpl string, view *gview.View, params ...gview.Params) error {
|
||||
//绑定模板中需要用到的方法
|
||||
view.BindFuncMap(g.Map{
|
||||
// 根据长度i来切割字符串
|
||||
"subStr": func(str interface{}, i int) (s string) {
|
||||
s1 := gconv.String(str)
|
||||
if gstr.LenRune(s1) > i {
|
||||
s = gstr.SubStrRune(s1, 0, i) + "..."
|
||||
return s
|
||||
}
|
||||
return s1
|
||||
},
|
||||
})
|
||||
r.Response.Write(view.Parse(r.GetCtx(), tpl, params...))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (res *Response) Redirect(r *ghttp.Request, location string, code ...int) {
|
||||
r.Response.RedirectTo(location, code...)
|
||||
}
|
36
library/libRouter/router.go
Normal file
36
library/libRouter/router.go
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* @desc:路由处理
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu<yxh669@qq.com>
|
||||
* @Date: 2022/11/16 11:09
|
||||
*/
|
||||
|
||||
package libRouter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// RouterAutoBind 收集需要被绑定的控制器,自动绑定
|
||||
// 路由的方法命名规则必须为:BindXXXController
|
||||
func RouterAutoBind(ctx context.Context, R interface{}, group *ghttp.RouterGroup) (err error) {
|
||||
//TypeOf会返回目标数据的类型,比如int/float/struct/指针等
|
||||
typ := reflect.TypeOf(R)
|
||||
//ValueOf返回目标数据的的值
|
||||
val := reflect.ValueOf(R)
|
||||
if val.Elem().Kind() != reflect.Struct {
|
||||
err = gerror.New("expect struct but a " + val.Elem().Kind().String())
|
||||
return
|
||||
}
|
||||
for i := 0; i < typ.NumMethod(); i++ {
|
||||
if match := gregex.IsMatchString(`^Bind(.+)Controller$`, typ.Method(i).Name); match {
|
||||
//调用绑定方法
|
||||
val.Method(i).Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(group)})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
249
library/libUtils/slice_tree.go
Normal file
249
library/libUtils/slice_tree.go
Normal file
@ -0,0 +1,249 @@
|
||||
package libUtils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// ParentSonSort 有层级关系的数组,父级-》子级 排序
|
||||
func ParentSonSort(list g.List, params ...interface{}) g.List {
|
||||
args := make([]interface{}, 8)
|
||||
for k, v := range params {
|
||||
if k == 8 {
|
||||
break
|
||||
}
|
||||
args[k] = v
|
||||
}
|
||||
var (
|
||||
pid int //父级id
|
||||
level int //层级数
|
||||
fieldName string //父级id键名
|
||||
id string //id键名
|
||||
levelName string //层级名称
|
||||
title string //标题名称
|
||||
breaks int //中断层级
|
||||
prefixStr string //字符串前缀
|
||||
)
|
||||
pid = gconv.Int(GetSliceByKey(args, 0, 0))
|
||||
level = gconv.Int(GetSliceByKey(args, 1, 0))
|
||||
fieldName = gconv.String(GetSliceByKey(args, 2, "pid"))
|
||||
id = gconv.String(GetSliceByKey(args, 3, "id"))
|
||||
levelName = gconv.String(GetSliceByKey(args, 4, "flg"))
|
||||
title = gconv.String(GetSliceByKey(args, 5, "title"))
|
||||
breaks = gconv.Int(GetSliceByKey(args, 6, -1))
|
||||
prefixStr = gconv.String(GetSliceByKey(args, 7, "─"))
|
||||
//定义一个新slice用于返回
|
||||
var returnSlice g.List
|
||||
for _, v := range list {
|
||||
if pid == gconv.Int(v[fieldName]) {
|
||||
v[levelName] = level
|
||||
levelClone := level
|
||||
titlePrefix := ""
|
||||
for {
|
||||
if levelClone < 0 {
|
||||
break
|
||||
}
|
||||
titlePrefix += prefixStr
|
||||
levelClone--
|
||||
}
|
||||
titlePrefix = "├" + titlePrefix
|
||||
if level == 0 {
|
||||
v["title_prefix"] = ""
|
||||
} else {
|
||||
v["title_prefix"] = titlePrefix
|
||||
}
|
||||
v["title_show"] = fmt.Sprintf("%s%s", v["title_prefix"], v[title])
|
||||
returnSlice = append(returnSlice, v)
|
||||
if breaks != -1 && breaks == level {
|
||||
continue
|
||||
}
|
||||
args[0] = v[id]
|
||||
args[1] = level + 1
|
||||
newSlice2 := ParentSonSort(list, args...)
|
||||
if len(newSlice2) > 0 {
|
||||
returnSlice = append(returnSlice, newSlice2...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnSlice
|
||||
}
|
||||
|
||||
// PushSonToParent 有层级关系的数组 ,将子级压入到父级(树形结构)
|
||||
func PushSonToParent(list g.List, params ...interface{}) g.List {
|
||||
args := make([]interface{}, 7)
|
||||
for k, v := range params {
|
||||
if k == 7 {
|
||||
break
|
||||
}
|
||||
args[k] = v
|
||||
}
|
||||
var (
|
||||
pid string //父级id
|
||||
fieldName string //父级id键名
|
||||
id string //id键名
|
||||
key string //子级数组键名
|
||||
filter string //过滤键名
|
||||
filterVal interface{} //过滤的值
|
||||
showNoChild bool //是否显示不存在的子级健
|
||||
)
|
||||
pid = gconv.String(GetSliceByKey(args, 0, 0))
|
||||
fieldName = gconv.String(GetSliceByKey(args, 1, "pid"))
|
||||
id = gconv.String(GetSliceByKey(args, 2, "id"))
|
||||
key = gconv.String(GetSliceByKey(args, 3, "children"))
|
||||
filter = gconv.String(GetSliceByKey(args, 4, ""))
|
||||
filterVal = GetSliceByKey(args, 5, nil)
|
||||
showNoChild = gconv.Bool(GetSliceByKey(args, 6, true))
|
||||
var returnList g.List
|
||||
for _, v := range list {
|
||||
if gconv.String(v[fieldName]) == pid {
|
||||
if filter != "" {
|
||||
if reflect.DeepEqual(v[filter], filterVal) {
|
||||
args[0] = v[id]
|
||||
child := PushSonToParent(list, args...)
|
||||
if child != nil || showNoChild {
|
||||
v[key] = child
|
||||
}
|
||||
returnList = append(returnList, v)
|
||||
}
|
||||
} else {
|
||||
args[0] = v[id]
|
||||
child := PushSonToParent(list, args...)
|
||||
if child != nil || showNoChild {
|
||||
v[key] = child
|
||||
}
|
||||
returnList = append(returnList, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnList
|
||||
}
|
||||
|
||||
// GetSliceByKey 获取切片里的值 若为nil 可设置默认值val
|
||||
func GetSliceByKey(args []interface{}, key int, val interface{}) interface{} {
|
||||
var value interface{}
|
||||
if args[key] != nil {
|
||||
value = args[key]
|
||||
} else {
|
||||
value = val
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// FindSonByParentId 有层级关系的切片,通过父级id查找所有子级id数组
|
||||
// parentId 父级id
|
||||
// parentIndex 父级索引名称
|
||||
// idIndex id索引名称
|
||||
func FindSonByParentId(list g.List, parentId interface{}, parentIndex, idIndex string) g.List {
|
||||
newList := make(g.List, 0, len(list))
|
||||
for _, v := range list {
|
||||
if reflect.DeepEqual(v[parentIndex], parentId) {
|
||||
newList = append(newList, v)
|
||||
fList := FindSonByParentId(list, v[idIndex], parentIndex, idIndex)
|
||||
newList = append(newList, fList...)
|
||||
}
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
// GetTopPidList 获取最顶层 parent Id
|
||||
func GetTopPidList(list g.List, parentIndex, idIndex string) *garray.Array {
|
||||
arr := garray.NewArray()
|
||||
for _, v1 := range list {
|
||||
tag := true
|
||||
for _, v2 := range list {
|
||||
if v1[parentIndex] == v2[idIndex] {
|
||||
tag = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if tag {
|
||||
arr.PushRight(v1[parentIndex])
|
||||
}
|
||||
}
|
||||
return arr.Unique()
|
||||
}
|
||||
|
||||
// FindParentBySonPid 有层级关系的数组,通过子级fid查找所有父级数组
|
||||
func FindParentBySonPid(list g.List, id int, params ...interface{}) g.List {
|
||||
args := make([]interface{}, 4)
|
||||
for k, v := range params {
|
||||
if k == 4 {
|
||||
break
|
||||
}
|
||||
args[k] = v
|
||||
}
|
||||
var (
|
||||
filter = gconv.String(GetSliceByKey(args, 0, "filter")) //过滤键名
|
||||
fPid = gconv.String(GetSliceByKey(args, 1, "pid")) //父级id字段键名
|
||||
filterValue = GetSliceByKey(args, 2, nil) //过滤键值
|
||||
fid = gconv.String(GetSliceByKey(args, 3, "id")) //id字段键名
|
||||
)
|
||||
rList := make(g.List, 0, len(list))
|
||||
for _, v := range list {
|
||||
if gconv.Int(v[fid]) == id {
|
||||
if fv, ok := v[filter]; ok {
|
||||
if reflect.DeepEqual(fv, filterValue) {
|
||||
rList = append(rList, v)
|
||||
}
|
||||
} else {
|
||||
rList = append(rList, v)
|
||||
}
|
||||
r := FindParentBySonPid(list, gconv.Int(v[fPid]), filter, fPid, filterValue, fid)
|
||||
rList = append(rList, r...)
|
||||
}
|
||||
}
|
||||
return rList
|
||||
}
|
||||
|
||||
// FindTopParent
|
||||
/**
|
||||
* 根据id查询最顶层父级信息
|
||||
* @param list 有层级关系的数组
|
||||
* @param id 查找的id
|
||||
* @param string fpid 父级id键名
|
||||
* @param string fid 当前id键名
|
||||
* @return g.Map
|
||||
*/
|
||||
func FindTopParent(list g.List, id int64, params ...interface{}) g.Map {
|
||||
if len(list) == 0 {
|
||||
return g.Map{}
|
||||
}
|
||||
args := make([]interface{}, 2)
|
||||
for k, v := range params {
|
||||
if k == 2 {
|
||||
break
|
||||
}
|
||||
args[k] = v
|
||||
}
|
||||
var (
|
||||
fPid = gconv.String(GetSliceByKey(args, 0, "pid")) //父级id字段键名
|
||||
fid = gconv.String(GetSliceByKey(args, 1, "id")) //id字段键名
|
||||
)
|
||||
hasParent := true
|
||||
top := g.Map{}
|
||||
//找到要查找id值的数组
|
||||
for _, v := range list {
|
||||
if gconv.Int64(v[fid]) == gconv.Int64(id) {
|
||||
top = v
|
||||
break
|
||||
}
|
||||
}
|
||||
for {
|
||||
if !hasParent {
|
||||
break
|
||||
}
|
||||
//查询最顶层
|
||||
for _, v := range list {
|
||||
if gconv.Int64(top[fPid]) == gconv.Int64(v[fid]) {
|
||||
top = v
|
||||
hasParent = true
|
||||
break
|
||||
}
|
||||
hasParent = false
|
||||
}
|
||||
}
|
||||
return top
|
||||
}
|
232
library/libUtils/utils.go
Normal file
232
library/libUtils/utils.go
Normal file
@ -0,0 +1,232 @@
|
||||
/*
|
||||
* @desc:工具
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu
|
||||
* @Date: 2022/3/4 22:16
|
||||
*/
|
||||
|
||||
package libUtils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bwmarrin/snowflake"
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/encoding/gcharset"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/encoding/gurl"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
"github.com/samber/lo"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/common/consts"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/system/dao"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncryptPassword 密码加密
|
||||
func EncryptPassword(password, salt string) string {
|
||||
return gmd5.MustEncryptString(gmd5.MustEncryptString(password) + gmd5.MustEncryptString(salt))
|
||||
}
|
||||
|
||||
// GetDomain 获取当前请求接口域名
|
||||
func GetDomain(ctx context.Context) string {
|
||||
r := g.RequestFromCtx(ctx)
|
||||
pathInfo, err := gurl.ParseURL(r.GetUrl(), -1)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s://%s:%s/", pathInfo["scheme"], pathInfo["host"], pathInfo["port"])
|
||||
}
|
||||
|
||||
// GetClientIp 获取客户端IP
|
||||
func GetClientIp(ctx context.Context) string {
|
||||
return g.RequestFromCtx(ctx).GetClientIp()
|
||||
}
|
||||
|
||||
// GetUserAgent 获取user-agent
|
||||
func GetUserAgent(ctx context.Context) string {
|
||||
return ghttp.RequestFromCtx(ctx).Header.Get("User-Agent")
|
||||
}
|
||||
|
||||
// GetLocalIP 服务端ip
|
||||
func GetLocalIP() (ip string, err error) {
|
||||
var addrs []net.Addr
|
||||
addrs, err = net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
ipAddr, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipAddr.IP.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
if !ipAddr.IP.IsGlobalUnicast() {
|
||||
continue
|
||||
}
|
||||
return ipAddr.IP.String(), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetCityByIp 获取ip所属城市
|
||||
func GetCityByIp(ip string) string {
|
||||
if ip == "" {
|
||||
return ""
|
||||
}
|
||||
if ip == "::1" || ip == "127.0.0.1" {
|
||||
return "内网IP"
|
||||
}
|
||||
url := "http://whois.pconline.com.cn/ipJson.jsp?json=true&ip=" + ip
|
||||
bytes := g.Client().GetBytes(context.TODO(), url)
|
||||
src := string(bytes)
|
||||
srcCharset := "GBK"
|
||||
tmp, _ := gcharset.ToUTF8(srcCharset, src)
|
||||
json, err := gjson.DecodeToJson(tmp)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if json.Get("code").Int() == 0 {
|
||||
city := fmt.Sprintf("%s %s", json.Get("pro").String(), json.Get("city").String())
|
||||
return city
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
func WriteToFile(fileName string, content string) error {
|
||||
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, _ := f.Seek(0, os.SEEK_END)
|
||||
_, err = f.WriteAt([]byte(content), n)
|
||||
defer f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// 文件或文件夹是否存在
|
||||
func FileIsExisted(filename string) bool {
|
||||
existed := true
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
existed = false
|
||||
}
|
||||
return existed
|
||||
}
|
||||
|
||||
// 解析路径获取文件名称及后缀
|
||||
func ParseFilePath(pathStr string) (fileName string, fileType string) {
|
||||
fileNameWithSuffix := path.Base(pathStr)
|
||||
fileType = path.Ext(fileNameWithSuffix)
|
||||
fileName = strings.TrimSuffix(fileNameWithSuffix, fileType)
|
||||
return
|
||||
}
|
||||
|
||||
// IsNotExistMkDir 检查文件夹是否存在
|
||||
// 如果不存在则新建文件夹
|
||||
func IsNotExistMkDir(src string) error {
|
||||
if exist := !FileIsExisted(src); exist == false {
|
||||
if err := MkDir(src); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MkDir 新建文件夹
|
||||
func MkDir(src string) error {
|
||||
err := os.MkdirAll(src, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取文件后缀
|
||||
func GetExt(fileName string) string {
|
||||
return path.Ext(fileName)
|
||||
}
|
||||
|
||||
// GetType 获取文件类型
|
||||
func GetType(p string) (result string, err error) {
|
||||
file, err := os.Open(p)
|
||||
if err != nil {
|
||||
g.Log().Error(context.TODO(), err)
|
||||
return
|
||||
}
|
||||
buff := make([]byte, 512)
|
||||
|
||||
_, err = file.Read(buff)
|
||||
|
||||
if err != nil {
|
||||
g.Log().Error(context.TODO(), err)
|
||||
return
|
||||
}
|
||||
filetype := http.DetectContentType(buff)
|
||||
return filetype, nil
|
||||
}
|
||||
|
||||
// GetFilesPath 获取附件相对路径
|
||||
func GetFilesPath(ctx context.Context, fileUrl string) (path string, err error) {
|
||||
upType := g.Cfg().MustGet(ctx, "upload.default").Int()
|
||||
if upType != 0 || (upType == 0 && !gstr.ContainsI(fileUrl, consts.UploadPath)) {
|
||||
path = fileUrl
|
||||
return
|
||||
}
|
||||
pathInfo, err := gurl.ParseURL(fileUrl, 32)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, err)
|
||||
err = gerror.New("解析附件路径失败")
|
||||
return
|
||||
}
|
||||
pos := gstr.PosI(pathInfo["path"], consts.UploadPath)
|
||||
if pos >= 0 {
|
||||
path = gstr.SubStr(pathInfo["path"], pos)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetFakeOpenID 生成 Fake OpenID
|
||||
func GetFakeOpenID() (string, error) {
|
||||
node, err := snowflake.NewNode(1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 生成一个 Fake OpenID
|
||||
openID := node.Generate().Base64()
|
||||
|
||||
// 保证生成的 OpenID 在 bus_construction_user 表中唯一
|
||||
_, err = lo.Attempt(3, func(_ int) error {
|
||||
exist, err := dao.BusConstructionUser.Ctx(context.Background()).Where(dao.BusConstructionUser.Columns().Openid, openID).Count()
|
||||
if err != nil {
|
||||
openID = node.Generate().Base64()
|
||||
return err
|
||||
}
|
||||
|
||||
if exist > 0 {
|
||||
openID = node.Generate().Base64()
|
||||
return errors.New("OpenID 已存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return openID, nil
|
||||
}
|
108
library/libValidate/custom_validations.go
Normal file
108
library/libValidate/custom_validations.go
Normal file
@ -0,0 +1,108 @@
|
||||
package libValidate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/text/gregex"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gogf/gf/v2/util/gvalid"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Register() {
|
||||
gvalid.RegisterRule("integer-array", IntegerArray)
|
||||
gvalid.RegisterRule("float-array", FloatArray)
|
||||
gvalid.RegisterRule("date-array", DateArray)
|
||||
gvalid.RegisterRule("datetime-array", DatetimeArray)
|
||||
}
|
||||
|
||||
func IntegerArray(ctx context.Context, RI gvalid.RuleFuncInput) error {
|
||||
if RI.Value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if RI.Value.IsSlice() {
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
for _, v := range RI.Value.Array() {
|
||||
valueStr := strings.TrimSpace(gconv.String(v))
|
||||
if valueStr == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := strconv.Atoi(valueStr); err != nil {
|
||||
continue
|
||||
}
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FloatArray(ctx context.Context, RI gvalid.RuleFuncInput) error {
|
||||
if RI.Value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
if !RI.Value.IsSlice() {
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
for _, v := range RI.Value.Array() {
|
||||
valueStr := strings.TrimSpace(gconv.String(v))
|
||||
if valueStr == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := strconv.ParseFloat(valueStr, 10); err == nil {
|
||||
continue
|
||||
}
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DateArray(ctx context.Context, RI gvalid.RuleFuncInput) error {
|
||||
if RI.Value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
if !RI.Value.IsSlice() {
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
for _, v := range RI.Value.Array() {
|
||||
// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
|
||||
if _, ok := v.(gtime.Time); ok {
|
||||
continue
|
||||
}
|
||||
valueStr := strings.TrimSpace(gconv.String(v))
|
||||
if valueStr == "" {
|
||||
continue
|
||||
}
|
||||
if gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr) {
|
||||
continue
|
||||
}
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DatetimeArray(ctx context.Context, RI gvalid.RuleFuncInput) error {
|
||||
if RI.Value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
if !RI.Value.IsSlice() {
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
for _, v := range RI.Value.Array() {
|
||||
// support for time value, eg: gtime.Time/*gtime.Time, time.Time/*time.Time.
|
||||
if _, ok := v.(gtime.Time); ok {
|
||||
continue
|
||||
}
|
||||
valueStr := strings.TrimSpace(gconv.String(v))
|
||||
if valueStr == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := gtime.StrToTimeFormat(valueStr, "Y-m-d H:i:s"); err == nil {
|
||||
continue
|
||||
}
|
||||
return gerror.New(RI.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
30
library/liberr/err.go
Normal file
30
library/liberr/err.go
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* @desc:错误处理
|
||||
* @company:云南奇讯科技有限公司
|
||||
* @Author: yixiaohu
|
||||
* @Date: 2022/3/2 14:53
|
||||
*/
|
||||
|
||||
package liberr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
func ErrIsNil(ctx context.Context, err error, msg ...string) {
|
||||
if !g.IsNil(err) {
|
||||
if len(msg) > 0 {
|
||||
g.Log().Error(ctx, err.Error())
|
||||
panic(msg[0])
|
||||
} else {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ValueIsNil(value interface{}, msg string) {
|
||||
if g.IsNil(value) {
|
||||
panic(msg)
|
||||
}
|
||||
}
|
47
library/upload/init.go
Normal file
47
library/upload/init.go
Normal file
@ -0,0 +1,47 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/system"
|
||||
)
|
||||
|
||||
const (
|
||||
SourceLocal UploaderType = iota // 上传到本地
|
||||
SourceTencent // 上传至腾讯云
|
||||
SourceQiniu // 上传到七牛云
|
||||
)
|
||||
|
||||
type UploaderType int
|
||||
|
||||
type IUpload interface {
|
||||
Upload(ctx context.Context, file *ghttp.UploadFile) (result system.UploadResponse, err error)
|
||||
}
|
||||
|
||||
var uploadCollection map[UploaderType]IUpload
|
||||
|
||||
// 注册上传接口
|
||||
func RegisterUploader(key UploaderType, value IUpload) {
|
||||
if uploadCollection == nil {
|
||||
uploadCollection = make(map[UploaderType]IUpload)
|
||||
}
|
||||
uploadCollection[key] = value
|
||||
}
|
||||
|
||||
// 获取上传接口
|
||||
func GetUploader(key UploaderType) IUpload {
|
||||
if uploadCollection == nil {
|
||||
return nil
|
||||
}
|
||||
if v, ok := uploadCollection[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Register() {
|
||||
|
||||
RegisterUploader(SourceLocal, &Local{})
|
||||
RegisterUploader(SourceTencent, &Tencent{})
|
||||
RegisterUploader(SourceQiniu, &Qiniou{})
|
||||
}
|
57
library/upload/local.go
Normal file
57
library/upload/local.go
Normal file
@ -0,0 +1,57 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/system"
|
||||
"github.com/tiger1103/gfast/v3/internal/app/common/consts"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Local struct {
|
||||
}
|
||||
|
||||
func (s *Local) Upload(ctx context.Context, file *ghttp.UploadFile) (result system.UploadResponse, err error) {
|
||||
if file == nil {
|
||||
err = errors.New("文件必须!")
|
||||
return
|
||||
}
|
||||
r := g.RequestFromCtx(ctx)
|
||||
urlPerfix := fmt.Sprintf("http://%s/", r.Host)
|
||||
p := strings.Trim(consts.UploadPath, "/")
|
||||
sp := s.getStaticPath(ctx)
|
||||
if sp != "" {
|
||||
sp = strings.Trim(sp, "/")
|
||||
}
|
||||
nowData := time.Now().Format("2006-01-02")
|
||||
// 包含静态文件夹的路径
|
||||
fullDirPath := sp + "/" + p + "/" + nowData
|
||||
fileName, err := file.Save(fullDirPath, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 不含静态文件夹的路径
|
||||
fullPath := p + "/" + nowData + "/" + fileName
|
||||
|
||||
result = system.UploadResponse{
|
||||
Size: file.Size,
|
||||
Path: fullPath,
|
||||
FullPath: urlPerfix + fullPath,
|
||||
Name: file.Filename,
|
||||
Type: file.Header.Get("Content-type"),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 静态文件夹目录
|
||||
func (s *Local) getStaticPath(ctx context.Context) string {
|
||||
value, _ := g.Cfg().Get(ctx, "server.serverRoot")
|
||||
if !value.IsEmpty() {
|
||||
return value.String()
|
||||
}
|
||||
return ""
|
||||
}
|
75
library/upload/qiniou.go
Normal file
75
library/upload/qiniou.go
Normal file
@ -0,0 +1,75 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||
"github.com/qiniu/go-sdk/v7/storage"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/system"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Qiniou struct{}
|
||||
|
||||
func (s *Qiniou) Upload(ctx context.Context, file *ghttp.UploadFile) (result system.UploadResponse, err error) {
|
||||
|
||||
url, err := s.toQiniou(ctx, file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = system.UploadResponse{
|
||||
Size: file.Size,
|
||||
Path: url,
|
||||
FullPath: url,
|
||||
Name: file.Filename,
|
||||
Type: file.Header.Get("Content-type"),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Qiniou) toQiniou(ctx context.Context, f *ghttp.UploadFile) (url string, err error) {
|
||||
file, err := f.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
v, err := g.Cfg().Get(ctx, "upload.qiniou")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m := v.MapStrVar()
|
||||
var AccessKey = m["accessKey"].String()
|
||||
var SerectKey = m["sercetKey"].String()
|
||||
var Bucket = m["bucket"].String()
|
||||
var ImgUrl = m["qiniuServer"].String()
|
||||
|
||||
putPlicy := storage.PutPolicy{
|
||||
Scope: Bucket,
|
||||
}
|
||||
mac := qbox.NewMac(AccessKey, SerectKey)
|
||||
upToken := putPlicy.UploadToken(mac)
|
||||
cfg := storage.Config{
|
||||
Zone: &storage.ZoneHuanan,
|
||||
UseCdnDomains: false,
|
||||
UseHTTPS: false,
|
||||
}
|
||||
putExtra := storage.PutExtra{
|
||||
MimeType: f.Header.Get("Content-type"),
|
||||
}
|
||||
formUploader := storage.NewFormUploader(&cfg)
|
||||
ret := storage.PutRet{}
|
||||
filename := guid.S() + path.Ext(f.Filename)
|
||||
fileSize := f.Size
|
||||
//err = formUploader.PutWithoutKey(context.Background(), &ret, upToken, file, fileSize, &putExtra)
|
||||
err = formUploader.Put(context.Background(), &ret, upToken, filename, file, fileSize, &putExtra)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
url = ImgUrl + "/" + filename
|
||||
return url, nil
|
||||
|
||||
}
|
75
library/upload/tencent.go
Normal file
75
library/upload/tencent.go
Normal file
@ -0,0 +1,75 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
"github.com/tencentyun/cos-go-sdk-v5"
|
||||
"github.com/tencentyun/cos-go-sdk-v5/debug"
|
||||
"github.com/tiger1103/gfast/v3/api/v1/system"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tencent struct {
|
||||
}
|
||||
|
||||
func (s *Tencent) Upload(ctx context.Context, file *ghttp.UploadFile) (result system.UploadResponse, err error) {
|
||||
v, err := g.Cfg().Get(ctx, "upload.tencentCOS")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m := v.MapStrVar()
|
||||
var (
|
||||
upPath = m["upPath"].String()
|
||||
rawUrl = m["rawUrl"].String()
|
||||
secretID = m["secretID"].String()
|
||||
secretKey = m["secretKey"].String()
|
||||
)
|
||||
name := gfile.Basename(file.Filename)
|
||||
name = strings.ToLower(strconv.FormatInt(gtime.TimestampNano(), 36) + grand.S(6))
|
||||
name = name + gfile.Ext(file.Filename)
|
||||
|
||||
path := upPath + name
|
||||
|
||||
url, _ := url.Parse(rawUrl)
|
||||
b := &cos.BaseURL{BucketURL: url}
|
||||
client := cos.NewClient(b, &http.Client{
|
||||
Transport: &cos.AuthorizationTransport{
|
||||
SecretID: secretID,
|
||||
SecretKey: secretKey,
|
||||
Transport: &debug.DebugRequestTransport{
|
||||
RequestHeader: false,
|
||||
RequestBody: false,
|
||||
ResponseHeader: false,
|
||||
ResponseBody: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
opt := &cos.ObjectPutOptions{
|
||||
ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{
|
||||
ContentLength: int64(file.Size),
|
||||
},
|
||||
}
|
||||
var f io.ReadCloser
|
||||
f, err = file.Open()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = client.Object.Put(context.Background(), path, f, opt)
|
||||
result = system.UploadResponse{
|
||||
Size: file.Size,
|
||||
Path: rawUrl + path,
|
||||
FullPath: rawUrl + path,
|
||||
Name: file.Filename,
|
||||
Type: file.Header.Get("Content-type"),
|
||||
}
|
||||
return
|
||||
}
|
7
library/upload_chunk/consts.go
Normal file
7
library/upload_chunk/consts.go
Normal file
@ -0,0 +1,7 @@
|
||||
package upload_chunk
|
||||
|
||||
const (
|
||||
Tmp = "./resource/public/big_file/" // 文件保存目录
|
||||
RelativePath = "big_file/" // 返回前端的相对路径
|
||||
FileName = "upfile" // 上传文件的文件名
|
||||
)
|
81
library/upload_chunk/helper.go
Normal file
81
library/upload_chunk/helper.go
Normal file
@ -0,0 +1,81 @@
|
||||
package upload_chunk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 解析文件目录,文件前缀及文件后缀
|
||||
func parseFilePath(filename string) (dir, prefix, suffix string) {
|
||||
filenameall := path.Base(filename) //不含目录的文件名
|
||||
filesuffix := path.Ext(filename) // 后缀
|
||||
fileprefix := strings.TrimSuffix(filenameall, filesuffix) // 前缀
|
||||
|
||||
dir = strings.TrimRight(filename, filenameall) // 文件目录
|
||||
prefix = fileprefix // 文件前缀
|
||||
suffix = filesuffix // 文件后缀
|
||||
return
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
func createFile(fullFilePath string) (ok bool, err error) {
|
||||
ok = fileExists(fullFilePath)
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
dir, _, _ := parseFilePath(fullFilePath)
|
||||
if dir != "" {
|
||||
err = mkDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
newFile, err := os.Create(fullFilePath)
|
||||
defer newFile.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建文件夹
|
||||
func mkDir(path string) (err error) {
|
||||
if !fileExists(path) {
|
||||
if err = os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return errors.New(fmt.Sprintf(`os.MkdirAll failed for path "%s" with perm "%d" , err : %v`, path, os.ModePerm, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 判断所给路径是否为文件夹
|
||||
func IsDir(path string) bool {
|
||||
s, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return s.IsDir()
|
||||
}
|
||||
|
||||
// 是否文件
|
||||
func IsFile(path string) bool {
|
||||
return !IsDir(path)
|
||||
}
|
||||
|
||||
// 判断文件或文件夹是否存在
|
||||
func fileExists(path string) bool {
|
||||
if stat, err := os.Stat(path); stat != nil && !os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 生成分片文件名
|
||||
func getChunkFilename(identifier, chunkNumber string) string {
|
||||
return fmt.Sprintf("uploader-%s.%s", identifier, chunkNumber)
|
||||
}
|
241
library/upload_chunk/option.go
Normal file
241
library/upload_chunk/option.go
Normal file
@ -0,0 +1,241 @@
|
||||
package upload_chunk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type checkErr func() error
|
||||
|
||||
type UploadReq struct {
|
||||
ChunkNumber int `json:"chunkNumber"` // 分片序列号
|
||||
ChunkSize int `json:"chunkSize"` // 分片大小
|
||||
CurrentChunkSize int `json:"currentChunkSize"` // 当前分片大小
|
||||
TotalSize int64 `json:"totalSize"` // 文件总大小
|
||||
Identifier string `json:"identifier"` // 标识
|
||||
Filename string `json:"filename"` // 文件名
|
||||
RelativePath string `json:"relativePath"` //文件夹上传的时候文件的相对路径属性
|
||||
TotalChunks int `json:"totalChunks"` // 分片总数
|
||||
File multipart.File
|
||||
FileHeader *multipart.FileHeader
|
||||
|
||||
verification map[interface{}][]checkErr
|
||||
}
|
||||
|
||||
func (u *UploadReq) verificationInit() {
|
||||
if u.verification == nil {
|
||||
u.verification = make(map[interface{}][]checkErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustChunkNumber() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["ChunkNumber"] = append(u.verification["ChunkNumber"], func() error {
|
||||
if u.ChunkNumber == 0 {
|
||||
return errors.New("ChunkNumber不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustChunkSize() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["ChunkSize"] = append(u.verification["ChunkSize"], func() error {
|
||||
if u.ChunkSize == 0 {
|
||||
return errors.New("ChunkSize不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustCurrentChunkSize() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["CurrentChunkSize"] = append(u.verification["CurrentChunkSize"], func() error {
|
||||
if u.CurrentChunkSize == 0 {
|
||||
return errors.New("CurrentChunkSize不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustTotalSize() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["TotalSize"] = append(u.verification["TotalSize"], func() error {
|
||||
if u.TotalSize == 0 {
|
||||
return errors.New("TotalSize不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustIdentifier() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["Identifier"] = append(u.verification["Identifier"], func() error {
|
||||
if u.Identifier == "" {
|
||||
return errors.New("Identifier不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustFilename() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["Filename"] = append(u.verification["Filename"], func() error {
|
||||
if u.Filename == "" {
|
||||
return errors.New("Filename不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustTotalChunks() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["TotalChunks"] = append(u.verification["TotalChunks"], func() error {
|
||||
if u.TotalChunks == 0 {
|
||||
return errors.New("TotalChunks不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) MustFile() *UploadReq {
|
||||
u.verificationInit()
|
||||
u.verification["File"] = append(u.verification["File"], func() error {
|
||||
if u.File == nil || u.FileHeader == nil {
|
||||
return errors.New("File不能为空")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return u
|
||||
}
|
||||
|
||||
func (u *UploadReq) Check() (err error) {
|
||||
for _, item := range u.verification {
|
||||
for _, fn := range item {
|
||||
err = fn()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (u *UploadReq) Bind(r *http.Request) (err error) {
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
l := r.ContentLength
|
||||
if l == 0 {
|
||||
return
|
||||
}
|
||||
buf := make([]byte, l)
|
||||
_, err = r.Body.Read(buf)
|
||||
|
||||
if err != nil && err != io.EOF {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(buf, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
chunkNumber := r.FormValue("chunkNumber")
|
||||
if chunkNumber != "" {
|
||||
u.ChunkNumber, _ = strconv.Atoi(chunkNumber)
|
||||
}
|
||||
|
||||
chunkSize := r.FormValue("chunkSize")
|
||||
if chunkSize != "" {
|
||||
u.ChunkSize, _ = strconv.Atoi(chunkSize)
|
||||
}
|
||||
|
||||
currentChunkSize := r.FormValue("currentChunkSize")
|
||||
if currentChunkSize != "" {
|
||||
u.CurrentChunkSize, _ = strconv.Atoi(currentChunkSize)
|
||||
}
|
||||
|
||||
totalSize := r.FormValue("totalSize")
|
||||
if totalSize != "" {
|
||||
u.TotalSize, _ = strconv.ParseInt(totalSize, 10, 64)
|
||||
}
|
||||
|
||||
identifier := r.FormValue("identifier")
|
||||
if identifier != "" {
|
||||
u.Identifier = identifier
|
||||
}
|
||||
|
||||
filename := r.FormValue("filename")
|
||||
if filename != "" {
|
||||
u.Filename = filename
|
||||
}
|
||||
|
||||
relativePath := r.FormValue("relativePath")
|
||||
if relativePath != "" {
|
||||
u.RelativePath = relativePath
|
||||
}
|
||||
|
||||
totalChunks := r.FormValue("totalChunks")
|
||||
if totalChunks != "" {
|
||||
u.TotalChunks, _ = strconv.Atoi(totalChunks)
|
||||
}
|
||||
}
|
||||
|
||||
u.File, u.FileHeader, _ = r.FormFile(FileName)
|
||||
return
|
||||
}
|
||||
|
||||
type BaseRes struct {
|
||||
Filename string `json:"filename"` // 文件名
|
||||
TotalSize int64 `json:"totalSize"` // 文件总大小
|
||||
Url string `json:"url"` // 上传文件路径
|
||||
}
|
||||
|
||||
// 返回前端的相对路径
|
||||
func (b *BaseRes) RelativePath() {
|
||||
_, prefix, suffix := parseFilePath(b.Url)
|
||||
b.Url = RelativePath + prefix + suffix
|
||||
}
|
||||
|
||||
type UpLoadRes struct {
|
||||
BaseRes
|
||||
NeedMerge bool `json:"needMerge"` // 是否合并文件
|
||||
Identifier string `json:"identifier"` // 标识
|
||||
TotalChunks int `json:"totalChunks"` // 分片总数
|
||||
}
|
||||
|
||||
type CheckRes struct {
|
||||
SkipUpload bool `json:"skipUpload"` // 秒传
|
||||
Uploaded []int `json:"uploaded"` // 已上传过的分片
|
||||
BaseRes
|
||||
}
|
||||
|
||||
func (r *CheckRes) HasUploaded(i int) (ok bool) {
|
||||
for _, item := range r.Uploaded {
|
||||
if item == i {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *CheckRes) HasFirst() (ok bool) {
|
||||
return r.HasUploaded(1)
|
||||
}
|
||||
|
||||
type MergeRes struct {
|
||||
BaseRes
|
||||
}
|
309
library/upload_chunk/upload_chunk.go
Normal file
309
library/upload_chunk/upload_chunk.go
Normal file
@ -0,0 +1,309 @@
|
||||
package upload_chunk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type UploadChunk struct{}
|
||||
|
||||
// 检查分片
|
||||
func (u *UploadChunk) CheckChunk(uploadReq UploadReq) (result *CheckRes, err error) {
|
||||
|
||||
err = uploadReq.MustIdentifier().MustTotalChunks().MustFilename().MustTotalSize().Check()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identifier, totalChunks, filename, totalSize := uploadReq.Identifier, uploadReq.TotalChunks, uploadReq.Filename, uploadReq.TotalSize
|
||||
|
||||
dir, prefix, suffix := parseFilePath(filename)
|
||||
_, _, _ = dir, prefix, suffix
|
||||
|
||||
if !strings.Contains(suffix, ".") {
|
||||
err = errors.New("文件名解析错误")
|
||||
return
|
||||
}
|
||||
|
||||
result = &CheckRes{}
|
||||
|
||||
// 秒传
|
||||
resultFilePath := u.Tmp() + identifier + suffix
|
||||
if fileExists(resultFilePath) {
|
||||
result.SkipUpload = true
|
||||
result.Url = resultFilePath
|
||||
result.Filename = filename
|
||||
result.TotalSize = totalSize
|
||||
result.RelativePath()
|
||||
return
|
||||
}
|
||||
|
||||
// 断点续传
|
||||
for i := 1; i <= totalChunks; i++ {
|
||||
chunkFilePath := u.chunkPath(identifier, strconv.FormatInt(int64(i), 10))
|
||||
if fileExists(chunkFilePath) {
|
||||
result.Uploaded = append(result.Uploaded, i)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 合并文件
|
||||
func (u *UploadChunk) MergeChunk(uploadReq UploadReq) (result *MergeRes, err error) {
|
||||
err = uploadReq.MustIdentifier().MustTotalChunks().MustTotalSize().MustFilename().Check()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
identifier, totalChunks, totalSize, filename := uploadReq.Identifier, uploadReq.TotalChunks, uploadReq.TotalSize, uploadReq.Filename
|
||||
|
||||
_, _, suffix := parseFilePath(filename)
|
||||
if !strings.Contains(suffix, ".") {
|
||||
return nil, errors.New("文件名解析错误")
|
||||
}
|
||||
|
||||
// 合并后的文件
|
||||
resultFilePath := u.Tmp() + identifier + suffix
|
||||
ok, err := createFile(resultFilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = new(MergeRes)
|
||||
// 文件已存在
|
||||
if ok {
|
||||
result.Url = resultFilePath
|
||||
result.Filename = filename
|
||||
result.TotalSize = totalSize
|
||||
result.RelativePath()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查分片文件是否完整
|
||||
ok, err = u.checkChunkAll(identifier, totalChunks, totalSize)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("分片文件检查错误:%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err = errors.New("分片文件不完整")
|
||||
return
|
||||
}
|
||||
|
||||
var chunkSize int64
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan struct{}, 10)
|
||||
for i := 1; i <= totalChunks; i++ {
|
||||
// 分片文件
|
||||
filePath := u.chunkPath(identifier, fmt.Sprintf("%d", i))
|
||||
if chunkSize == 0 {
|
||||
fi, _ := os.Stat(filePath)
|
||||
if chunkSize = fi.Size(); chunkSize == 0 {
|
||||
err = errors.New("分片文件大小为0")
|
||||
return
|
||||
}
|
||||
}
|
||||
ch <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer func() {
|
||||
<-ch
|
||||
wg.Done()
|
||||
}()
|
||||
uploadChunk := &UploadChunk{}
|
||||
err = uploadChunk.mergeFile(filePath, resultFilePath, chunkSize*int64(i-1))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
result.Url = resultFilePath
|
||||
result.Filename = filename
|
||||
result.TotalSize = totalSize
|
||||
result.RelativePath()
|
||||
return
|
||||
}
|
||||
|
||||
// 上传分片文件
|
||||
func (u *UploadChunk) Upload(uploadReq UploadReq) (result *UpLoadRes, err error) {
|
||||
err = uploadReq.MustTotalChunks().MustChunkNumber().MustIdentifier().MustFile().MustTotalSize().MustFilename().Check()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
totalChunks, chunkNumber, identifier, upFile, filename := uploadReq.TotalChunks, uploadReq.ChunkNumber, uploadReq.Identifier, uploadReq.File, uploadReq.Filename
|
||||
var fullFilePath string
|
||||
if totalChunks > 1 {
|
||||
// 分片文件路径
|
||||
fullFilePath = u.chunkPath(identifier, strconv.Itoa(chunkNumber))
|
||||
} else {
|
||||
_, _, suffix := parseFilePath(filename)
|
||||
if !strings.Contains(suffix, ".") {
|
||||
return nil, errors.New("文件名解析错误")
|
||||
}
|
||||
fullFilePath = u.Tmp() + identifier + suffix
|
||||
}
|
||||
|
||||
// 创建文件
|
||||
ok, err := createFile(fullFilePath)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
// 文件已经存在
|
||||
result = &UpLoadRes{
|
||||
BaseRes: BaseRes{
|
||||
Filename: uploadReq.Filename,
|
||||
TotalSize: uploadReq.TotalSize,
|
||||
Url: fullFilePath,
|
||||
},
|
||||
NeedMerge: totalChunks > 1,
|
||||
Identifier: uploadReq.Identifier,
|
||||
TotalChunks: uploadReq.TotalChunks,
|
||||
}
|
||||
result.RelativePath()
|
||||
return
|
||||
}
|
||||
|
||||
// 打开分片文件
|
||||
file, err := os.OpenFile(fullFilePath, os.O_CREATE|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
// 文件偏移量
|
||||
var fi os.FileInfo
|
||||
fi, err = os.Stat(fullFilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
start := fi.Size()
|
||||
// 写入分片文件
|
||||
_, err = u.writeFile(upFile, start, file, start)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result = &UpLoadRes{
|
||||
BaseRes: BaseRes{
|
||||
Filename: uploadReq.Filename,
|
||||
TotalSize: uploadReq.TotalSize,
|
||||
Url: fullFilePath,
|
||||
},
|
||||
NeedMerge: totalChunks > 1,
|
||||
Identifier: uploadReq.Identifier,
|
||||
TotalChunks: uploadReq.TotalChunks,
|
||||
}
|
||||
result.RelativePath()
|
||||
return
|
||||
}
|
||||
|
||||
func (u *UploadChunk) Tmp() string {
|
||||
return Tmp
|
||||
}
|
||||
|
||||
// 合并文件
|
||||
func (u *UploadChunk) mergeFile(chunkFile, mergeFile string, offset int64) (err error) {
|
||||
|
||||
// 合并后的文件
|
||||
file, err := os.OpenFile(mergeFile, os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Seek(offset, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 分片文件
|
||||
chunkFileObj, err := os.Open(chunkFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer chunkFileObj.Close()
|
||||
// 写入数据
|
||||
data := make([]byte, 1024, 1024)
|
||||
|
||||
for {
|
||||
tal, e := chunkFileObj.Read(data)
|
||||
if e == io.EOF {
|
||||
chunkFileObj.Close()
|
||||
os.Remove(chunkFile)
|
||||
break
|
||||
}
|
||||
_, e = file.Write(data[:tal])
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查分片文件是否完整
|
||||
func (u *UploadChunk) checkChunkAll(identifier string, totalChunks int, totalSize int64) (ok bool, err error) {
|
||||
if identifier == "" || totalChunks == 0 {
|
||||
return false, errors.New("checkChunkAll 参数错误")
|
||||
}
|
||||
var _totalSize int64
|
||||
for i := 1; i <= totalChunks; i++ {
|
||||
filePath := u.chunkPath(identifier, fmt.Sprintf("%d", i))
|
||||
fi, e := os.Stat(filePath)
|
||||
if e != nil {
|
||||
return false, e
|
||||
}
|
||||
_totalSize += fi.Size()
|
||||
}
|
||||
|
||||
return _totalSize == totalSize, nil
|
||||
}
|
||||
|
||||
// 获取分片文件路径
|
||||
func (u *UploadChunk) chunkPath(identifier string, chunkNumber string) string {
|
||||
return fmt.Sprintf("%s%s", u.Tmp(), getChunkFilename(identifier, chunkNumber))
|
||||
}
|
||||
|
||||
// 检查文件完整性
|
||||
func (u *UploadChunk) verifyFileSize(fullFilePath string, size int64) (ok bool, err error) {
|
||||
fi, err := os.Stat(fullFilePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if fi.Size() == size {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
func (u *UploadChunk) writeFile(upfile multipart.File, upSeek int64, file *os.File, fSeek int64) (result int, err error) {
|
||||
// 上传文件大小记录
|
||||
fileSzie := 0
|
||||
// 设置上传偏移量
|
||||
upfile.Seek(upSeek, 0)
|
||||
// 设置文件偏移量
|
||||
file.Seek(fSeek, 0)
|
||||
data := make([]byte, 1024, 1024)
|
||||
for {
|
||||
total, e := upfile.Read(data)
|
||||
if e == io.EOF {
|
||||
// 文件复制完毕
|
||||
break
|
||||
}
|
||||
l, e := file.Write(data[:total])
|
||||
if e != nil {
|
||||
return 0, errors.New("文件上传失败")
|
||||
}
|
||||
// 记录上传长度
|
||||
fileSzie += l
|
||||
}
|
||||
return fileSzie, nil
|
||||
}
|
Reference in New Issue
Block a user