This commit is contained in:
2023-11-02 04:34:46 +08:00
commit c4548fe498
369 changed files with 40208 additions and 0 deletions

47
utils/ast/ast.go Normal file
View File

@@ -0,0 +1,47 @@
package ast
import (
"fmt"
"go/ast"
"go/token"
)
// 增加 import 方法
func AddImport(astNode ast.Node, imp string) {
impStr := fmt.Sprintf("\"%s\"", imp)
ast.Inspect(astNode, func(node ast.Node) bool {
if genDecl, ok := node.(*ast.GenDecl); ok {
if genDecl.Tok == token.IMPORT {
for i := range genDecl.Specs {
if impNode, ok := genDecl.Specs[i].(*ast.ImportSpec); ok {
if impNode.Path.Value == impStr {
return false
}
}
}
genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: impStr,
},
})
}
}
return true
})
}
// 查询特定function方法
func FindFunction(astNode ast.Node, FunctionName string) *ast.FuncDecl {
var funcDeclP *ast.FuncDecl
ast.Inspect(astNode, func(node ast.Node) bool {
if funcDecl, ok := node.(*ast.FuncDecl); ok {
if funcDecl.Name.String() == FunctionName {
funcDeclP = funcDecl
return false
}
}
return true
})
return funcDeclP
}

View File

@@ -0,0 +1,47 @@
package ast
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
)
func ImportForAutoEnter(path string, funcName string, code string) {
src, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
ast.Inspect(astFile, func(node ast.Node) bool {
if typeSpec, ok := node.(*ast.TypeSpec); ok {
if typeSpec.Name.Name == funcName {
if st, ok := typeSpec.Type.(*ast.StructType); ok {
for i := range st.Fields.List {
if t, ok := st.Fields.List[i].Type.(*ast.Ident); ok {
if t.Name == code {
return false
}
}
}
sn := &ast.Field{
Type: &ast.Ident{Name: code},
}
st.Fields.List = append(st.Fields.List, sn)
}
}
}
return true
})
var out []byte
bf := bytes.NewBuffer(out)
err = printer.Fprint(bf, fileSet, astFile)
if err != nil {
return
}
_ = os.WriteFile(path, bf.Bytes(), 0666)
}

View File

@@ -0,0 +1,7 @@
package ast
import "testing"
func TestImportForAutoEnter(t *testing.T) {
ImportForAutoEnter("D:\\gin-vue-admin\\server.exe.exe\\api\\v1\\test\\enter.go", "ApiGroup", "test")
}

181
utils/ast/ast_enter.go Normal file
View File

@@ -0,0 +1,181 @@
package ast
import (
"bytes"
"go/ast"
"go/format"
"go/parser"
"go/token"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"log"
"os"
"strconv"
"strings"
)
type Visitor struct {
ImportCode string
StructName string
PackageName string
GroupName string
}
func (vi *Visitor) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.GenDecl:
// 查找有没有import context包
// Notice没有考虑没有import任何包的情况
if n.Tok == token.IMPORT && vi.ImportCode != "" {
vi.addImport(n)
// 不需要再遍历子树
return nil
}
if n.Tok == token.TYPE && vi.StructName != "" && vi.PackageName != "" && vi.GroupName != "" {
vi.addStruct(n)
return nil
}
case *ast.FuncDecl:
if n.Name.Name == "Routers" {
vi.addFuncBodyVar(n)
return nil
}
}
return vi
}
func (vi *Visitor) addStruct(genDecl *ast.GenDecl) ast.Visitor {
for i := range genDecl.Specs {
switch n := genDecl.Specs[i].(type) {
case *ast.TypeSpec:
if strings.Index(n.Name.Name, "Group") > -1 {
switch t := n.Type.(type) {
case *ast.StructType:
f := &ast.Field{
Names: []*ast.Ident{
{
Name: vi.StructName,
Obj: &ast.Object{
Kind: ast.Var,
Name: vi.StructName,
},
},
},
Type: &ast.SelectorExpr{
X: &ast.Ident{
Name: vi.PackageName,
},
Sel: &ast.Ident{
Name: vi.GroupName,
},
},
}
t.Fields.List = append(t.Fields.List, f)
}
}
}
}
return vi
}
func (vi *Visitor) addImport(genDecl *ast.GenDecl) ast.Visitor {
// 是否已经import
hasImported := false
for _, v := range genDecl.Specs {
importSpec := v.(*ast.ImportSpec)
// 如果已经包含
if importSpec.Path.Value == strconv.Quote(vi.ImportCode) {
hasImported = true
}
}
if !hasImported {
genDecl.Specs = append(genDecl.Specs, &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
Value: strconv.Quote(vi.ImportCode),
},
})
}
return vi
}
func (vi *Visitor) addFuncBodyVar(funDecl *ast.FuncDecl) ast.Visitor {
hasVar := false
for _, v := range funDecl.Body.List {
switch varSpec := v.(type) {
case *ast.AssignStmt:
for i := range varSpec.Lhs {
switch nn := varSpec.Lhs[i].(type) {
case *ast.Ident:
if nn.Name == vi.PackageName+"Router" {
hasVar = true
}
}
}
}
}
if !hasVar {
assignStmt := &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{
Name: vi.PackageName + "Router",
Obj: &ast.Object{
Kind: ast.Var,
Name: vi.PackageName + "Router",
},
},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{
&ast.SelectorExpr{
X: &ast.SelectorExpr{
X: &ast.Ident{
Name: "router",
},
Sel: &ast.Ident{
Name: "RouterGroupApp",
},
},
Sel: &ast.Ident{
Name: cases.Title(language.English).String(vi.PackageName),
},
},
},
}
funDecl.Body.List = append(funDecl.Body.List, funDecl.Body.List[1])
index := 1
copy(funDecl.Body.List[index+1:], funDecl.Body.List[index:])
funDecl.Body.List[index] = assignStmt
}
return vi
}
func ImportReference(filepath, importCode, structName, packageName, groupName string) error {
fSet := token.NewFileSet()
fParser, err := parser.ParseFile(fSet, filepath, nil, parser.ParseComments)
if err != nil {
return err
}
importCode = strings.TrimSpace(importCode)
v := &Visitor{
ImportCode: importCode,
StructName: structName,
PackageName: packageName,
GroupName: groupName,
}
if importCode == "" {
ast.Print(fSet, fParser)
}
ast.Walk(v, fParser)
var output []byte
buffer := bytes.NewBuffer(output)
err = format.Node(buffer, fSet, fParser)
if err != nil {
log.Fatal(err)
}
// 写回数据
return os.WriteFile(filepath, buffer.Bytes(), 0o600)
}

166
utils/ast/ast_gorm.go Normal file
View File

@@ -0,0 +1,166 @@
package ast
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
)
// 自动为 gorm.go 注册一个自动迁移
func AddRegisterTablesAst(path, funcName, pk, varName, dbName, model string) {
modelPk := fmt.Sprintf("miniapp/model/%s", pk)
src, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
fmt.Println(err)
}
AddImport(astFile, modelPk)
FuncNode := FindFunction(astFile, funcName)
if FuncNode != nil {
ast.Print(fileSet, FuncNode)
}
addDBVar(FuncNode.Body, varName, dbName)
addAutoMigrate(FuncNode.Body, varName, pk, model)
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(path, bf.Bytes(), 0666)
}
// 增加一个 db库变量
func addDBVar(astBody *ast.BlockStmt, varName, dbName string) {
if dbName == "" {
return
}
dbStr := fmt.Sprintf("\"%s\"", dbName)
for i := range astBody.List {
if assignStmt, ok := astBody.List[i].(*ast.AssignStmt); ok {
if ident, ok := assignStmt.Lhs[0].(*ast.Ident); ok {
if ident.Name == varName {
return
}
}
}
}
assignNode := &ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{
Name: varName,
},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{
&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{
Name: "global",
},
Sel: &ast.Ident{
Name: "GetGlobalDBByDBName",
},
},
Args: []ast.Expr{
&ast.BasicLit{
Kind: token.STRING,
Value: dbStr,
},
},
},
},
}
astBody.List = append([]ast.Stmt{assignNode}, astBody.List...)
}
// 为db库变量增加 AutoMigrate 方法
func addAutoMigrate(astBody *ast.BlockStmt, dbname string, pk string, model string) {
if dbname == "" {
dbname = "db"
}
flag := true
ast.Inspect(astBody, func(node ast.Node) bool {
// 首先判断需要加入的方法调用语句是否存在 不存在则直接走到下方逻辑
switch n := node.(type) {
case *ast.CallExpr:
// 判断是否找到了AutoMigrate语句
if s, ok := n.Fun.(*ast.SelectorExpr); ok {
if x, ok := s.X.(*ast.Ident); ok {
if s.Sel.Name == "AutoMigrate" && x.Name == dbname {
flag = false
if !NeedAppendModel(n, pk, model) {
return false
}
// 判断已经找到了AutoMigrate语句
n.Args = append(n.Args, &ast.CompositeLit{
Type: &ast.SelectorExpr{
X: &ast.Ident{
Name: pk,
},
Sel: &ast.Ident{
Name: model,
},
},
})
return false
}
}
}
}
return true
//然后判断 pk.model是否存在 如果存在直接跳出 如果不存在 则向已经找到的方法调用语句的node里面push一条
})
if flag {
exprStmt := &ast.ExprStmt{
X: &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{
Name: dbname,
},
Sel: &ast.Ident{
Name: "AutoMigrate",
},
},
Args: []ast.Expr{
&ast.CompositeLit{
Type: &ast.SelectorExpr{
X: &ast.Ident{
Name: pk,
},
Sel: &ast.Ident{
Name: model,
},
},
},
},
}}
astBody.List = append(astBody.List, exprStmt)
}
}
// 为automigrate增加实参
func NeedAppendModel(callNode ast.Node, pk string, model string) bool {
flag := true
ast.Inspect(callNode, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.SelectorExpr:
if x, ok := n.X.(*ast.Ident); ok {
if n.Sel.Name == model && x.Name == pk {
flag = false
return false
}
}
}
return true
})
return flag
}

View File

@@ -0,0 +1,18 @@
package ast
import (
"miniapp/global"
"miniapp/model/example"
"testing"
)
const A = 123
func TestAddRegisterTablesAst(t *testing.T) {
AddRegisterTablesAst("D:\\gin-vue-admin\\server.exe.exe\\utils\\ast_test.go", "Register", "test", "testDB", "testModel")
}
func Register() {
test := global.GetGlobalDBByDBName("test")
test.AutoMigrate(example.ExaFile{})
}

157
utils/ast/ast_rollback.go Normal file
View File

@@ -0,0 +1,157 @@
package ast
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"miniapp/global"
"os"
"path/filepath"
)
func RollBackAst(pk, model string) {
RollGormBack(pk, model)
RollRouterBack(pk, model)
}
func RollGormBack(pk, model string) {
// 首先分析存在多少个ttt作为调用方的node块
// 如果多个 仅仅删除对应块即可
// 如果单个 那么还需要剔除import
path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "gorm.go")
src, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
fmt.Println(err)
}
var n *ast.CallExpr
var k int = -1
var pkNum = 0
ast.Inspect(astFile, func(node ast.Node) bool {
if node, ok := node.(*ast.CallExpr); ok {
for i := range node.Args {
pkOK := false
modelOK := false
ast.Inspect(node.Args[i], func(item ast.Node) bool {
if ii, ok := item.(*ast.Ident); ok {
if ii.Name == pk {
pkOK = true
pkNum++
}
if ii.Name == model {
modelOK = true
}
}
if pkOK && modelOK {
n = node
k = i
}
return true
})
}
}
return true
})
if k > 0 {
n.Args = append(append([]ast.Expr{}, n.Args[:k]...), n.Args[k+1:]...)
}
if pkNum == 1 {
var imI int = -1
var gp *ast.GenDecl
ast.Inspect(astFile, func(node ast.Node) bool {
if gen, ok := node.(*ast.GenDecl); ok {
for i := range gen.Specs {
if imspec, ok := gen.Specs[i].(*ast.ImportSpec); ok {
if imspec.Path.Value == "\"miniapp/model/"+pk+"\"" {
gp = gen
imI = i
return false
}
}
}
}
return true
})
if imI > -1 {
gp.Specs = append(append([]ast.Spec{}, gp.Specs[:imI]...), gp.Specs[imI+1:]...)
}
}
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.Remove(path)
os.WriteFile(path, bf.Bytes(), 0666)
}
func RollRouterBack(pk, model string) {
// 首先抓到所有的代码块结构 {}
// 分析结构中是否存在一个变量叫做 pk+Router
// 然后获取到代码块指针 对内部需要回滚的代码进行剔除
path := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "initialize", "router.go")
src, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, 0)
if err != nil {
fmt.Println(err)
}
var block *ast.BlockStmt
ast.Inspect(astFile, func(node ast.Node) bool {
if n, ok := node.(*ast.BlockStmt); ok {
ast.Inspect(n, func(bNode ast.Node) bool {
if in, ok := bNode.(*ast.Ident); ok {
if in.Name == pk+"Router" {
block = n
return false
}
}
return true
})
return true
}
return true
})
var k int
for i := range block.List {
if stmtNode, ok := block.List[i].(*ast.ExprStmt); ok {
ast.Inspect(stmtNode, func(node ast.Node) bool {
if n, ok := node.(*ast.Ident); ok {
if n.Name == "Init"+model+"Router" {
k = i
return false
}
}
return true
})
}
}
block.List = append(append([]ast.Stmt{}, block.List[:k]...), block.List[k+1:]...)
if len(block.List) == 1 {
// 说明这个块就没任何意义了
block.List = nil
// TODO 删除空的{}
}
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.Remove(path)
os.WriteFile(path, bf.Bytes(), 0666)
}

View File

@@ -0,0 +1,11 @@
package ast
import "testing"
func TestRollRouterBack(t *testing.T) {
RollRouterBack("ttt", "Testttt")
}
func TestRollGormBack(t *testing.T) {
RollGormBack("ttt", "Testttt")
}

132
utils/ast/ast_router.go Normal file
View File

@@ -0,0 +1,132 @@
package ast
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"os"
"strings"
)
func AppendNodeToList(stmts []ast.Stmt, stmt ast.Stmt, index int) []ast.Stmt {
return append(stmts[:index], append([]ast.Stmt{stmt}, stmts[index:]...)...)
}
func AddRouterCode(path, funcName, pk, model string) {
src, err := os.ReadFile(path)
if err != nil {
fmt.Println(err)
}
fileSet := token.NewFileSet()
astFile, err := parser.ParseFile(fileSet, "", src, parser.ParseComments)
if err != nil {
fmt.Println(err)
}
FuncNode := FindFunction(astFile, funcName)
pkName := strings.ToUpper(pk[:1]) + pk[1:]
routerName := fmt.Sprintf("%sRouter", pk)
modelName := fmt.Sprintf("Init%sRouter", model)
var bloctPre *ast.BlockStmt
for i := len(FuncNode.Body.List) - 1; i >= 0; i-- {
if block, ok := FuncNode.Body.List[i].(*ast.BlockStmt); ok {
bloctPre = block
}
}
ast.Print(fileSet, FuncNode)
if ok, b := needAppendRouter(FuncNode, pk); ok {
routerNode :=
&ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{
&ast.Ident{Name: routerName},
},
Tok: token.DEFINE,
Rhs: []ast.Expr{
&ast.SelectorExpr{
X: &ast.SelectorExpr{
X: &ast.Ident{Name: "router"},
Sel: &ast.Ident{Name: "RouterGroupApp"},
},
Sel: &ast.Ident{Name: pkName},
},
},
},
},
}
FuncNode.Body.List = AppendNodeToList(FuncNode.Body.List, routerNode, len(FuncNode.Body.List)-2)
bloctPre = routerNode
} else {
bloctPre = b
}
if needAppendInit(FuncNode, routerName, modelName) {
bloctPre.List = append(bloctPre.List,
&ast.ExprStmt{
X: &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: routerName},
Sel: &ast.Ident{Name: modelName},
},
Args: []ast.Expr{
&ast.Ident{
Name: "PrivateGroup",
},
},
},
})
}
var out []byte
bf := bytes.NewBuffer(out)
printer.Fprint(bf, fileSet, astFile)
os.WriteFile(path, bf.Bytes(), 0666)
}
func needAppendRouter(funcNode ast.Node, pk string) (bool, *ast.BlockStmt) {
flag := true
var block *ast.BlockStmt
ast.Inspect(funcNode, func(node ast.Node) bool {
switch n := node.(type) {
case *ast.BlockStmt:
for i := range n.List {
if assignNode, ok := n.List[i].(*ast.AssignStmt); ok {
if identNode, ok := assignNode.Lhs[0].(*ast.Ident); ok {
if identNode.Name == fmt.Sprintf("%sRouter", pk) {
flag = false
block = n
return false
}
}
}
}
}
return true
})
return flag, block
}
func needAppendInit(funcNode ast.Node, routerName string, modelName string) bool {
flag := true
ast.Inspect(funcNode, func(node ast.Node) bool {
switch n := funcNode.(type) {
case *ast.CallExpr:
if selectNode, ok := n.Fun.(*ast.SelectorExpr); ok {
x, xok := selectNode.X.(*ast.Ident)
if xok && x.Name == routerName && selectNode.Sel.Name == modelName {
flag = false
return false
}
}
}
return true
})
return flag
}

View File

@@ -0,0 +1,9 @@
package ast
import (
"testing"
)
func TestAddRouterCode(t *testing.T) {
AddRouterCode("D:\\gin-vue-admin\\server.exe.exe\\utils\\ast\\ast_router_test.go", "Routers", "testRouter", "GVAStruct")
}

View File

@@ -0,0 +1,112 @@
package utils
import (
"errors"
"os"
"strconv"
"strings"
)
// 前端传来文件片与当前片为什么文件的第几片
// 后端拿到以后比较次分片是否上传 或者是否为不完全片
// 前端发送每片多大
// 前端告知是否为最后一片且是否完成
const (
breakpointDir = "./breakpointDir/"
finishDir = "./fileDir/"
)
//@author: [piexlmax](https://github.com/piexlmax)
//@function: BreakPointContinue
//@description: 断点续传
//@param: content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string
//@return: error, string
func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) {
path := breakpointDir + fileMd5 + "/"
err := os.MkdirAll(path, os.ModePerm)
if err != nil {
return path, err
}
pathC, err := makeFileContent(content, fileName, path, contentNumber)
return pathC, err
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: CheckMd5
//@description: 检查Md5
//@param: content []byte, chunkMd5 string
//@return: CanUpload bool
func CheckMd5(content []byte, chunkMd5 string) (CanUpload bool) {
fileMd5 := MD5V(content)
if fileMd5 == chunkMd5 {
return true // 可以继续上传
} else {
return false // 切片不完整,废弃
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: makeFileContent
//@description: 创建切片内容
//@param: content []byte, fileName string, FileDir string, contentNumber int
//@return: string, error
func makeFileContent(content []byte, fileName string, FileDir string, contentNumber int) (string, error) {
if strings.Index(fileName, "..") > -1 || strings.Index(FileDir, "..") > -1 {
return "", errors.New("文件名或路径不合法")
}
path := FileDir + fileName + "_" + strconv.Itoa(contentNumber)
f, err := os.Create(path)
if err != nil {
return path, err
} else {
_, err = f.Write(content)
if err != nil {
return path, err
}
}
defer f.Close()
return path, nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: makeFileContent
//@description: 创建切片文件
//@param: fileName string, FileMd5 string
//@return: error, string
func MakeFile(fileName string, FileMd5 string) (string, error) {
rd, err := os.ReadDir(breakpointDir + FileMd5)
if err != nil {
return finishDir + fileName, err
}
_ = os.MkdirAll(finishDir, os.ModePerm)
fd, err := os.OpenFile(finishDir+fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644)
if err != nil {
return finishDir + fileName, err
}
defer fd.Close()
for k := range rd {
content, _ := os.ReadFile(breakpointDir + FileMd5 + "/" + fileName + "_" + strconv.Itoa(k))
_, err = fd.Write(content)
if err != nil {
_ = os.Remove(finishDir + fileName)
return finishDir + fileName, err
}
}
return finishDir + fileName, nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: RemoveChunk
//@description: 移除切片
//@param: FileMd5 string
//@return: error
func RemoveChunk(FileMd5 string) error {
err := os.RemoveAll(breakpointDir + FileMd5)
return err
}

60
utils/captcha/redis.go Normal file
View File

@@ -0,0 +1,60 @@
package captcha
import (
"context"
"time"
"github.com/mojocn/base64Captcha"
"go.uber.org/zap"
"miniapp/global"
)
func NewDefaultRedisStore() *RedisStore {
return &RedisStore{
Expiration: time.Second * 180,
PreKey: "CAPTCHA_",
Context: context.TODO(),
}
}
type RedisStore struct {
Expiration time.Duration
PreKey string
Context context.Context
}
func (rs *RedisStore) UseWithCtx(ctx context.Context) base64Captcha.Store {
rs.Context = ctx
return rs
}
func (rs *RedisStore) Set(id string, value string) error {
err := global.GVA_REDIS.Set(rs.Context, rs.PreKey+id, value, rs.Expiration).Err()
if err != nil {
global.GVA_LOG.Error("RedisStoreSetError!", zap.Error(err))
return err
}
return nil
}
func (rs *RedisStore) Get(key string, clear bool) string {
val, err := global.GVA_REDIS.Get(rs.Context, key).Result()
if err != nil {
global.GVA_LOG.Error("RedisStoreGetError!", zap.Error(err))
return ""
}
if clear {
err := global.GVA_REDIS.Del(rs.Context, key).Err()
if err != nil {
global.GVA_LOG.Error("RedisStoreClearError!", zap.Error(err))
return ""
}
}
return val
}
func (rs *RedisStore) Verify(id, answer string, clear bool) bool {
key := rs.PreKey + id
v := rs.Get(key, clear)
return v == answer
}

88
utils/clamis.go Normal file
View File

@@ -0,0 +1,88 @@
package utils
import (
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid/v5"
"miniapp/global"
systemReq "miniapp/model/system/request"
)
func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) {
token := c.Request.Header.Get("x-token")
j := NewJWT()
claims, err := j.ParseToken(token)
if err != nil {
global.GVA_LOG.Error("从Gin的Context中获取从jwt解析信息失败, 请检查请求头是否存在x-token且claims是否为规定结构")
}
return claims, err
}
// GetUserID 从Gin的Context中获取从jwt解析出来的用户ID
func GetUserID(c *gin.Context) uint {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return 0
} else {
return cl.BaseClaims.ID
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse.BaseClaims.ID
}
}
// GetUserUuid 从Gin的Context中获取从jwt解析出来的用户UUID
func GetUserUuid(c *gin.Context) uuid.UUID {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return uuid.UUID{}
} else {
return cl.UUID
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse.UUID
}
}
// GetUserAuthorityId 从Gin的Context中获取从jwt解析出来的用户角色id
func GetUserAuthorityId(c *gin.Context) uint {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return 0
} else {
return cl.AuthorityId
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse.AuthorityId
}
}
// GetUserInfo 从Gin的Context中获取从jwt解析出来的用户角色id
func GetUserInfo(c *gin.Context) *systemReq.CustomClaims {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return nil
} else {
return cl
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse
}
}
// GetUserName 从Gin的Context中获取从jwt解析出来的用户名
func GetUserName(c *gin.Context) string {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return ""
} else {
return cl.Username
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse.Username
}
}

29
utils/db_automation.go Normal file
View File

@@ -0,0 +1,29 @@
package utils
import (
"errors"
"fmt"
"time"
"gorm.io/gorm"
)
//@author: [songzhibin97](https://github.com/songzhibin97)
//@function: ClearTable
//@description: 清理数据库表数据
//@param: db(数据库对象) *gorm.DB, tableName(表名) string, compareField(比较字段) string, interval(间隔) string
//@return: error
func ClearTable(db *gorm.DB, tableName string, compareField string, interval string) error {
if db == nil {
return errors.New("db Cannot be empty")
}
duration, err := time.ParseDuration(interval)
if err != nil {
return err
}
if duration < 0 {
return errors.New("parse duration < 0")
}
return db.Debug().Exec(fmt.Sprintf("DELETE FROM %s WHERE %s < ?", tableName, compareField), time.Now().Add(-duration)).Error
}

16
utils/desensitization.go Normal file
View File

@@ -0,0 +1,16 @@
package utils
type desensitization struct{}
// Desensitization 暴露接口 - 脱敏相关
func Desensitization() *desensitization {
return &desensitization{}
}
// Phone 脱敏手机号
func (desensitization) Phone(phone string) string {
if len(phone) == 11 {
return phone[:3] + "****" + phone[7:]
}
return phone
}

124
utils/directory.go Normal file
View File

@@ -0,0 +1,124 @@
package utils
import (
"errors"
"os"
"path/filepath"
"reflect"
"strings"
"go.uber.org/zap"
"miniapp/global"
)
//@author: [piexlmax](https://github.com/piexlmax)
//@function: PathExists
//@description: 文件目录是否存在
//@param: path string
//@return: bool, error
func PathExists(path string) (bool, error) {
fi, err := os.Stat(path)
if err == nil {
if fi.IsDir() {
return true, nil
}
return false, errors.New("存在同名文件")
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: CreateDir
//@description: 批量创建文件夹
//@param: dirs ...string
//@return: err error
func CreateDir(dirs ...string) (err error) {
for _, v := range dirs {
exist, err := PathExists(v)
if err != nil {
return err
}
if !exist {
global.GVA_LOG.Debug("create directory" + v)
if err := os.MkdirAll(v, os.ModePerm); err != nil {
global.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err))
return err
}
}
}
return err
}
//@author: [songzhibin97](https://github.com/songzhibin97)
//@function: FileMove
//@description: 文件移动供外部调用
//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹)
//@return: err error
func FileMove(src string, dst string) (err error) {
if dst == "" {
return nil
}
src, err = filepath.Abs(src)
if err != nil {
return err
}
dst, err = filepath.Abs(dst)
if err != nil {
return err
}
revoke := false
dir := filepath.Dir(dst)
Redirect:
_, err = os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0o755)
if err != nil {
return err
}
if !revoke {
revoke = true
goto Redirect
}
}
return os.Rename(src, dst)
}
func DeLFile(filePath string) error {
return os.RemoveAll(filePath)
}
//@author: [songzhibin97](https://github.com/songzhibin97)
//@function: TrimSpace
//@description: 去除结构体空格
//@param: target interface (target: 目标结构体,传入必须是指针类型)
//@return: null
func TrimSpace(target interface{}) {
t := reflect.TypeOf(target)
if t.Kind() != reflect.Ptr {
return
}
t = t.Elem()
v := reflect.ValueOf(target).Elem()
for i := 0; i < t.NumField(); i++ {
switch v.Field(i).Kind() {
case reflect.String:
v.Field(i).SetString(strings.TrimSpace(v.Field(i).String()))
}
}
}
// FileExist 判断文件是否存在
func FileExist(path string) bool {
fi, err := os.Lstat(path)
if err == nil {
return !fi.IsDir()
}
return !os.IsNotExist(err)
}

67
utils/fmt_plus.go Normal file
View File

@@ -0,0 +1,67 @@
package utils
import (
"fmt"
"reflect"
"strings"
)
//@author: [piexlmax](https://github.com/piexlmax)
//@function: StructToMap
//@description: 利用反射将结构体转化为map
//@param: obj interface{}
//@return: map[string]interface{}
func StructToMap(obj interface{}) map[string]interface{} {
obj1 := reflect.TypeOf(obj)
obj2 := reflect.ValueOf(obj)
data := make(map[string]interface{})
for i := 0; i < obj1.NumField(); i++ {
if obj1.Field(i).Tag.Get("mapstructure") != "" {
data[obj1.Field(i).Tag.Get("mapstructure")] = obj2.Field(i).Interface()
} else {
data[obj1.Field(i).Name] = obj2.Field(i).Interface()
}
}
return data
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: ArrayToString
//@description: 将数组格式化为字符串
//@param: array []interface{}
//@return: string
func ArrayToString(array []interface{}) string {
return strings.Replace(strings.Trim(fmt.Sprint(array), "[]"), " ", ",", -1)
}
func Pointer[T any](in T) (out *T) {
return &in
}
func FirstUpper(s string) string {
if s == "" {
return ""
}
return strings.ToUpper(s[:1]) + s[1:]
}
func FirstLower(s string) string {
if s == "" {
return ""
}
return strings.ToLower(s[:1]) + s[1:]
}
// MaheHump 将字符串转换为驼峰命名
func MaheHump(s string) string {
words := strings.Split(s, "-")
for i := 1; i < len(words); i++ {
words[i] = strings.Title(words[i])
}
return strings.Join(words, "")
}

31
utils/hash.go Normal file
View File

@@ -0,0 +1,31 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// BcryptHash 使用 bcrypt 对密码进行加密
func BcryptHash(password string) string {
bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes)
}
// BcryptCheck 对比明文密码和数据库的哈希值
func BcryptCheck(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: MD5V
//@description: md5加密
//@param: str []byte
//@return: string
func MD5V(str []byte, b ...byte) string {
h := md5.New()
h.Write(str)
return hex.EncodeToString(h.Sum(b))
}

29
utils/human_duration.go Normal file
View File

@@ -0,0 +1,29 @@
package utils
import (
"strconv"
"strings"
"time"
)
func ParseDuration(d string) (time.Duration, error) {
d = strings.TrimSpace(d)
dr, err := time.ParseDuration(d)
if err == nil {
return dr, nil
}
if strings.Contains(d, "d") {
index := strings.Index(d, "d")
hour, _ := strconv.Atoi(d[:index])
dr = time.Hour * 24 * time.Duration(hour)
ndr, err := time.ParseDuration(d[index+1:])
if err != nil {
return dr, nil
}
return dr + ndr, nil
}
dv, err := strconv.ParseInt(d, 10, 64)
return time.Duration(dv), err
}

View File

@@ -0,0 +1,49 @@
package utils
import (
"testing"
"time"
)
func TestParseDuration(t *testing.T) {
type args struct {
d string
}
tests := []struct {
name string
args args
want time.Duration
wantErr bool
}{
{
name: "5h20m",
args: args{"5h20m"},
want: time.Hour*5 + 20*time.Minute,
wantErr: false,
},
{
name: "1d5h20m",
args: args{"1d5h20m"},
want: 24*time.Hour + time.Hour*5 + 20*time.Minute,
wantErr: false,
},
{
name: "1d",
args: args{"1d"},
want: 24 * time.Hour,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseDuration(tt.args.d)
if (err != nil) != tt.wantErr {
t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParseDuration() got = %v, want %v", got, tt.want)
}
})
}
}

180
utils/injection_code.go Normal file
View File

@@ -0,0 +1,180 @@
package utils
import (
"errors"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"strings"
)
//@author: [LeonardWang](https://github.com/WangLeonard)
//@function: AutoInjectionCode
//@description: 向文件中固定注释位置写入代码
//@param: filepath string, funcName string, codeData string
//@return: error
const (
startComment = "Code generated by miniapp Begin; DO NOT EDIT."
endComment = "Code generated by miniapp End; DO NOT EDIT."
)
//@author: [LeonardWang](https://github.com/WangLeonard)
//@function: AutoInjectionCode
//@description: 向文件中固定注释位置写入代码
//@param: filepath string, funcName string, codeData string
//@return: error
func AutoInjectionCode(filepath string, funcName string, codeData string) error {
srcData, err := os.ReadFile(filepath)
if err != nil {
return err
}
srcDataLen := len(srcData)
fset := token.NewFileSet()
fparser, err := parser.ParseFile(fset, filepath, srcData, parser.ParseComments)
if err != nil {
return err
}
codeData = strings.TrimSpace(codeData)
codeStartPos := -1
codeEndPos := srcDataLen
var expectedFunction *ast.FuncDecl
startCommentPos := -1
endCommentPos := srcDataLen
// 如果指定了函数名,先寻找对应函数
if funcName != "" {
for _, decl := range fparser.Decls {
if funDecl, ok := decl.(*ast.FuncDecl); ok && funDecl.Name.Name == funcName {
expectedFunction = funDecl
codeStartPos = int(funDecl.Body.Lbrace)
codeEndPos = int(funDecl.Body.Rbrace)
break
}
}
}
// 遍历所有注释
for _, comment := range fparser.Comments {
if int(comment.Pos()) > codeStartPos && int(comment.End()) <= codeEndPos {
if startComment != "" && strings.Contains(comment.Text(), startComment) {
startCommentPos = int(comment.Pos()) // Note: Pos is the second '/'
}
if endComment != "" && strings.Contains(comment.Text(), endComment) {
endCommentPos = int(comment.Pos()) // Note: Pos is the second '/'
}
}
}
if endCommentPos == srcDataLen {
return fmt.Errorf("comment:%s not found", endComment)
}
// 在指定函数名且函数中startComment和endComment都存在时进行区间查重
if (codeStartPos != -1 && codeEndPos <= srcDataLen) && (startCommentPos != -1 && endCommentPos != srcDataLen) && expectedFunction != nil {
if exist := checkExist(&srcData, startCommentPos, endCommentPos, expectedFunction.Body, codeData); exist {
fmt.Printf("文件 %s 待插入数据 %s 已存在\n", filepath, codeData)
return nil // 这里不需要返回错误?
}
}
// 两行注释中间没有换行时会被认为是一条Comment
if startCommentPos == endCommentPos {
endCommentPos = startCommentPos + strings.Index(string(srcData[startCommentPos:]), endComment)
for srcData[endCommentPos] != '/' {
endCommentPos--
}
}
// 记录"//"之前的空字符,保持写入后的格式一致
tmpSpace := make([]byte, 0, 10)
for tmp := endCommentPos - 2; tmp >= 0; tmp-- {
if srcData[tmp] != '\n' {
tmpSpace = append(tmpSpace, srcData[tmp])
} else {
break
}
}
reverseSpace := make([]byte, 0, len(tmpSpace))
for index := len(tmpSpace) - 1; index >= 0; index-- {
reverseSpace = append(reverseSpace, tmpSpace[index])
}
// 插入数据
indexPos := endCommentPos - 1
insertData := []byte(append([]byte(codeData+"\n"), reverseSpace...))
remainData := append([]byte{}, srcData[indexPos:]...)
srcData = append(append(srcData[:indexPos], insertData...), remainData...)
// 写回数据
return os.WriteFile(filepath, srcData, 0o600)
}
func checkExist(srcData *[]byte, startPos int, endPos int, blockStmt *ast.BlockStmt, target string) bool {
for _, list := range blockStmt.List {
switch stmt := list.(type) {
case *ast.ExprStmt:
if callExpr, ok := stmt.X.(*ast.CallExpr); ok &&
int(callExpr.Pos()) > startPos && int(callExpr.End()) < endPos {
text := string((*srcData)[int(callExpr.Pos()-1):int(callExpr.End())])
key := strings.TrimSpace(text)
if key == target {
return true
}
}
case *ast.BlockStmt:
if checkExist(srcData, startPos, endPos, stmt, target) {
return true
}
case *ast.AssignStmt:
// 为 model 中的代码进行检查
if len(stmt.Rhs) > 0 {
if callExpr, ok := stmt.Rhs[0].(*ast.CallExpr); ok {
for _, arg := range callExpr.Args {
if int(arg.Pos()) > startPos && int(arg.End()) < endPos {
text := string((*srcData)[int(arg.Pos()-1):int(arg.End())])
key := strings.TrimSpace(text)
if key == target {
return true
}
}
}
}
}
}
}
return false
}
func AutoClearCode(filepath string, codeData string) error {
srcData, err := os.ReadFile(filepath)
if err != nil {
return err
}
srcData, err = cleanCode(codeData, string(srcData))
if err != nil {
return err
}
return os.WriteFile(filepath, srcData, 0o600)
}
func cleanCode(clearCode string, srcData string) ([]byte, error) {
bf := make([]rune, 0, 1024)
for i, v := range srcData {
if v == '\n' {
if strings.TrimSpace(string(bf)) == clearCode {
return append([]byte(srcData[:i-len(bf)]), []byte(srcData[i+1:])...), nil
}
bf = (bf)[:0]
continue
}
bf = append(bf, v)
}
return []byte(srcData), errors.New("未找到内容")
}

88
utils/jwt.go Normal file
View File

@@ -0,0 +1,88 @@
package utils
import (
"errors"
"time"
jwt "github.com/golang-jwt/jwt/v4"
"miniapp/global"
"miniapp/model/system/request"
)
type JWT struct {
SigningKey []byte
}
var (
TokenExpired = errors.New("Token is expired")
TokenNotValidYet = errors.New("Token not active yet")
TokenMalformed = errors.New("That's not even a token")
TokenInvalid = errors.New("Couldn't handle this token:")
)
func NewJWT() *JWT {
return &JWT{
[]byte(global.GVA_CONFIG.JWT.SigningKey),
}
}
func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims {
bf, _ := ParseDuration(global.GVA_CONFIG.JWT.BufferTime)
ep, _ := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
claims := request.CustomClaims{
BaseClaims: baseClaims,
BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"GVA"}, // 受众
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件
Issuer: global.GVA_CONFIG.JWT.Issuer, // 签名的发行者
},
}
return claims
}
// 创建一个token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// CreateTokenByOldToken 旧token 换新token 使用归并回源避免并发问题
func (j *JWT) CreateTokenByOldToken(oldToken string, claims request.CustomClaims) (string, error) {
v, err, _ := global.GVA_Concurrency_Control.Do("JWT:"+oldToken, func() (interface{}, error) {
return j.CreateToken(claims)
})
return v.(string), err
}
// 解析 token
func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
return j.SigningKey, nil
})
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, TokenMalformed
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// Token is expired
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if token != nil {
if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, TokenInvalid
} else {
return nil, TokenInvalid
}
}

18
utils/plugin/plugin.go Normal file
View File

@@ -0,0 +1,18 @@
package plugin
import (
"github.com/gin-gonic/gin"
)
const (
OnlyFuncName = "Plugin"
)
// Plugin 插件模式接口化
type Plugin interface {
// Register 注册路由
Register(group *gin.RouterGroup)
// RouterPath 用户返回注册路由
RouterPath() string
}

18
utils/reload.go Normal file
View File

@@ -0,0 +1,18 @@
package utils
import (
"errors"
"os"
"os/exec"
"runtime"
"strconv"
)
func Reload() error {
if runtime.GOOS == "windows" {
return errors.New("系统不支持")
}
pid := os.Getpid()
cmd := exec.Command("kill", "-1", strconv.Itoa(pid))
return cmd.Run()
}

118
utils/server.go Normal file
View File

@@ -0,0 +1,118 @@
package utils
import (
"runtime"
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
)
const (
B = 1
KB = 1024 * B
MB = 1024 * KB
GB = 1024 * MB
)
type Server struct {
Os Os `json:"os"`
Cpu Cpu `json:"cpu"`
Ram Ram `json:"ram"`
Disk Disk `json:"disk"`
}
type Os struct {
GOOS string `json:"goos"`
NumCPU int `json:"numCpu"`
Compiler string `json:"compiler"`
GoVersion string `json:"goVersion"`
NumGoroutine int `json:"numGoroutine"`
}
type Cpu struct {
Cpus []float64 `json:"cpus"`
Cores int `json:"cores"`
}
type Ram struct {
UsedMB int `json:"usedMb"`
TotalMB int `json:"totalMb"`
UsedPercent int `json:"usedPercent"`
}
type Disk struct {
UsedMB int `json:"usedMb"`
UsedGB int `json:"usedGb"`
TotalMB int `json:"totalMb"`
TotalGB int `json:"totalGb"`
UsedPercent int `json:"usedPercent"`
}
//@author: [SliverHorn](https://github.com/SliverHorn)
//@function: InitCPU
//@description: OS信息
//@return: o Os, err error
func InitOS() (o Os) {
o.GOOS = runtime.GOOS
o.NumCPU = runtime.NumCPU()
o.Compiler = runtime.Compiler
o.GoVersion = runtime.Version()
o.NumGoroutine = runtime.NumGoroutine()
return o
}
//@author: [SliverHorn](https://github.com/SliverHorn)
//@function: InitCPU
//@description: CPU信息
//@return: c Cpu, err error
func InitCPU() (c Cpu, err error) {
if cores, err := cpu.Counts(false); err != nil {
return c, err
} else {
c.Cores = cores
}
if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil {
return c, err
} else {
c.Cpus = cpus
}
return c, nil
}
//@author: [SliverHorn](https://github.com/SliverHorn)
//@function: InitRAM
//@description: RAM信息
//@return: r Ram, err error
func InitRAM() (r Ram, err error) {
if u, err := mem.VirtualMemory(); err != nil {
return r, err
} else {
r.UsedMB = int(u.Used) / MB
r.TotalMB = int(u.Total) / MB
r.UsedPercent = int(u.UsedPercent)
}
return r, nil
}
//@author: [SliverHorn](https://github.com/SliverHorn)
//@function: InitDisk
//@description: 硬盘信息
//@return: d Disk, err error
func InitDisk() (d Disk, err error) {
if u, err := disk.Usage("/"); err != nil {
return d, err
} else {
d.UsedMB = int(u.Used) / MB
d.UsedGB = int(u.Used) / GB
d.TotalMB = int(u.Total) / MB
d.TotalGB = int(u.Total) / GB
d.UsedPercent = int(u.UsedPercent)
}
return d, nil
}

132
utils/timer/timed_task.go Normal file
View File

@@ -0,0 +1,132 @@
package timer
import (
"sync"
"github.com/robfig/cron/v3"
)
type Timer interface {
AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error)
AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error)
FindCron(taskName string) (*cron.Cron, bool)
StartTask(taskName string)
StopTask(taskName string)
Remove(taskName string, id int)
Clear(taskName string)
Close()
}
// timer 定时任务管理
type timer struct {
taskList map[string]*cron.Cron
sync.Mutex
}
// AddTaskByFunc 通过函数的方法添加任务
func (t *timer) AddTaskByFunc(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddFunc(spec, task)
t.taskList[taskName].Start()
return id, err
}
// AddTaskByFuncWithSeconds 通过函数的方法使用WithSeconds添加任务
func (t *timer) AddTaskByFuncWhithSecond(taskName string, spec string, task func(), option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
option = append(option, cron.WithSeconds())
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddFunc(spec, task)
t.taskList[taskName].Start()
return id, err
}
// AddTaskByJob 通过接口的方法添加任务
func (t *timer) AddTaskByJob(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddJob(spec, job)
t.taskList[taskName].Start()
return id, err
}
// AddTaskByJobWithSeconds 通过接口的方法添加任务
func (t *timer) AddTaskByJobWithSeconds(taskName string, spec string, job interface{ Run() }, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
option = append(option, cron.WithSeconds())
if _, ok := t.taskList[taskName]; !ok {
t.taskList[taskName] = cron.New(option...)
}
id, err := t.taskList[taskName].AddJob(spec, job)
t.taskList[taskName].Start()
return id, err
}
// FindCron 获取对应taskName的cron 可能会为空
func (t *timer) FindCron(taskName string) (*cron.Cron, bool) {
t.Lock()
defer t.Unlock()
v, ok := t.taskList[taskName]
return v, ok
}
// StartTask 开始任务
func (t *timer) StartTask(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Start()
}
}
// StopTask 停止任务
func (t *timer) StopTask(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Stop()
}
}
// Remove 从taskName 删除指定任务
func (t *timer) Remove(taskName string, id int) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Remove(cron.EntryID(id))
}
}
// Clear 清除任务
func (t *timer) Clear(taskName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.taskList[taskName]; ok {
v.Stop()
delete(t.taskList, taskName)
}
}
// Close 释放资源
func (t *timer) Close() {
t.Lock()
defer t.Unlock()
for _, v := range t.taskList {
v.Stop()
}
}
func NewTimerTask() Timer {
return &timer{taskList: make(map[string]*cron.Cron)}
}

View File

@@ -0,0 +1,67 @@
package timer
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var job = mockJob{}
type mockJob struct{}
func (job mockJob) Run() {
mockFunc()
}
func mockFunc() {
time.Sleep(time.Second)
fmt.Println("1s...")
}
func TestNewTimerTask(t *testing.T) {
tm := NewTimerTask()
_tm := tm.(*timer)
{
_, err := tm.AddTaskByFunc("func", "@every 1s", mockFunc)
assert.Nil(t, err)
_, ok := _tm.taskList["func"]
if !ok {
t.Error("no find func")
}
}
{
_, err := tm.AddTaskByJob("job", "@every 1s", job)
assert.Nil(t, err)
_, ok := _tm.taskList["job"]
if !ok {
t.Error("no find job")
}
}
{
_, ok := tm.FindCron("func")
if !ok {
t.Error("no find func")
}
_, ok = tm.FindCron("job")
if !ok {
t.Error("no find job")
}
_, ok = tm.FindCron("none")
if ok {
t.Error("find none")
}
}
{
tm.Clear("func")
_, ok := tm.FindCron("func")
if ok {
t.Error("find func")
}
}
}

View File

@@ -0,0 +1,75 @@
package upload
import (
"errors"
"mime/multipart"
"time"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"go.uber.org/zap"
"miniapp/global"
)
type AliyunOSS struct{}
func (*AliyunOSS) UploadFile(file *multipart.FileHeader) (string, string, error) {
bucket, err := NewBucket()
if err != nil {
global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error()))
return "", "", errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error())
}
// 读取本地文件。
f, openError := file.Open()
if openError != nil {
global.GVA_LOG.Error("function file.Open() Failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() Failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
// 上传阿里云路径 文件名格式 自己可以改 建议保证唯一性
// yunFileTmpPath := filepath.Join("uploads", time.Now().Format("2006-01-02")) + "/" + file.Filename
yunFileTmpPath := global.GVA_CONFIG.AliyunOSS.BasePath + "/" + "uploads" + "/" + time.Now().Format("2006-01-02") + "/" + file.Filename
// 上传文件流。
err = bucket.PutObject(yunFileTmpPath, f)
if err != nil {
global.GVA_LOG.Error("function formUploader.Put() Failed", zap.Any("err", err.Error()))
return "", "", errors.New("function formUploader.Put() Failed, err:" + err.Error())
}
return global.GVA_CONFIG.AliyunOSS.BucketUrl + "/" + yunFileTmpPath, yunFileTmpPath, nil
}
func (*AliyunOSS) DeleteFile(key string) error {
bucket, err := NewBucket()
if err != nil {
global.GVA_LOG.Error("function AliyunOSS.NewBucket() Failed", zap.Any("err", err.Error()))
return errors.New("function AliyunOSS.NewBucket() Failed, err:" + err.Error())
}
// 删除单个文件。objectName表示删除OSS文件时需要指定包含文件后缀在内的完整路径例如abc/efg/123.jpg。
// 如需删除文件夹请将objectName设置为对应的文件夹名称。如果文件夹非空则需要将文件夹下的所有object删除后才能删除该文件夹。
err = bucket.DeleteObject(key)
if err != nil {
global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
}
return nil
}
func NewBucket() (*oss.Bucket, error) {
// 创建OSSClient实例。
client, err := oss.New(global.GVA_CONFIG.AliyunOSS.Endpoint, global.GVA_CONFIG.AliyunOSS.AccessKeyId, global.GVA_CONFIG.AliyunOSS.AccessKeySecret)
if err != nil {
return nil, err
}
// 获取存储空间。
bucket, err := client.Bucket(global.GVA_CONFIG.AliyunOSS.BucketName)
if err != nil {
return nil, err
}
return bucket, nil
}

97
utils/upload/aws_s3.go Normal file
View File

@@ -0,0 +1,97 @@
package upload
import (
"errors"
"fmt"
"mime/multipart"
"time"
"miniapp/global"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"go.uber.org/zap"
)
type AwsS3 struct{}
//@author: [WqyJh](https://github.com/WqyJh)
//@object: *AwsS3
//@function: UploadFile
//@description: Upload file to Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-upload-file-to-bucket
//@param: file *multipart.FileHeader
//@return: string, string, error
func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) {
session := newSession()
uploader := s3manager.NewUploader(session)
fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + fileKey
f, openError := file.Open()
if openError != nil {
global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
_, err := uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket),
Key: aws.String(filename),
Body: f,
})
if err != nil {
global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error()))
return "", "", err
}
return global.GVA_CONFIG.AwsS3.BaseURL + "/" + filename, fileKey, nil
}
//@author: [WqyJh](https://github.com/WqyJh)
//@object: *AwsS3
//@function: DeleteFile
//@description: Delete file from Aws S3 using aws-sdk-go. See https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/s3-example-basic-bucket-operations.html#s3-examples-bucket-ops-delete-bucket-item
//@param: file *multipart.FileHeader
//@return: string, string, error
func (*AwsS3) DeleteFile(key string) error {
session := newSession()
svc := s3.New(session)
filename := global.GVA_CONFIG.AwsS3.PathPrefix + "/" + key
bucket := global.GVA_CONFIG.AwsS3.Bucket
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(filename),
})
if err != nil {
global.GVA_LOG.Error("function svc.DeleteObject() failed", zap.Any("err", err.Error()))
return errors.New("function svc.DeleteObject() failed, err:" + err.Error())
}
_ = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(filename),
})
return nil
}
// newSession Create S3 session
func newSession() *session.Session {
sess, _ := session.NewSession(&aws.Config{
Region: aws.String(global.GVA_CONFIG.AwsS3.Region),
Endpoint: aws.String(global.GVA_CONFIG.AwsS3.Endpoint), //minio在这里设置地址,可以兼容
S3ForcePathStyle: aws.Bool(global.GVA_CONFIG.AwsS3.S3ForcePathStyle),
DisableSSL: aws.Bool(global.GVA_CONFIG.AwsS3.DisableSSL),
Credentials: credentials.NewStaticCredentials(
global.GVA_CONFIG.AwsS3.SecretID,
global.GVA_CONFIG.AwsS3.SecretKey,
"",
),
})
return sess
}

86
utils/upload/local.go Normal file
View File

@@ -0,0 +1,86 @@
package upload
import (
"errors"
"io"
"mime/multipart"
"os"
"path"
"strings"
"time"
"go.uber.org/zap"
"miniapp/global"
"miniapp/utils"
)
type Local struct{}
//@author: [piexlmax](https://github.com/piexlmax)
//@author: [ccfish86](https://github.com/ccfish86)
//@author: [SliverHorn](https://github.com/SliverHorn)
//@object: *Local
//@function: UploadFile
//@description: 上传文件
//@param: file *multipart.FileHeader
//@return: string, string, error
func (*Local) UploadFile(file *multipart.FileHeader) (string, string, error) {
// 读取文件后缀
ext := path.Ext(file.Filename)
// 读取文件名并加密
name := strings.TrimSuffix(file.Filename, ext)
name = utils.MD5V([]byte(name))
// 拼接新文件名
filename := name + "_" + time.Now().Format("20060102150405") + ext
// 尝试创建此路径
mkdirErr := os.MkdirAll(global.GVA_CONFIG.Local.StorePath, os.ModePerm)
if mkdirErr != nil {
global.GVA_LOG.Error("function os.MkdirAll() failed", zap.Any("err", mkdirErr.Error()))
return "", "", errors.New("function os.MkdirAll() failed, err:" + mkdirErr.Error())
}
// 拼接路径和文件名
p := global.GVA_CONFIG.Local.StorePath + "/" + filename
filepath := global.GVA_CONFIG.Local.Path + "/" + filename
f, openError := file.Open() // 读取文件
if openError != nil {
global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
out, createErr := os.Create(p)
if createErr != nil {
global.GVA_LOG.Error("function os.Create() failed", zap.Any("err", createErr.Error()))
return "", "", errors.New("function os.Create() failed, err:" + createErr.Error())
}
defer out.Close() // 创建文件 defer 关闭
_, copyErr := io.Copy(out, f) // 传输(拷贝)文件
if copyErr != nil {
global.GVA_LOG.Error("function io.Copy() failed", zap.Any("err", copyErr.Error()))
return "", "", errors.New("function io.Copy() failed, err:" + copyErr.Error())
}
return filepath, filename, nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@author: [ccfish86](https://github.com/ccfish86)
//@author: [SliverHorn](https://github.com/SliverHorn)
//@object: *Local
//@function: DeleteFile
//@description: 删除文件
//@param: key string
//@return: error
func (*Local) DeleteFile(key string) error {
p := global.GVA_CONFIG.Local.StorePath + "/" + key
if strings.Contains(p, global.GVA_CONFIG.Local.StorePath) {
if err := os.Remove(p); err != nil {
return errors.New("本地文件删除失败, err:" + err.Error())
}
}
return nil
}

67
utils/upload/obs.go Normal file
View File

@@ -0,0 +1,67 @@
package upload
import (
"mime/multipart"
"github.com/huaweicloud/huaweicloud-sdk-go-obs/obs"
"github.com/pkg/errors"
"miniapp/global"
)
var HuaWeiObs = new(Obs)
type Obs struct{}
func NewHuaWeiObsClient() (client *obs.ObsClient, err error) {
return obs.New(global.GVA_CONFIG.HuaWeiObs.AccessKey, global.GVA_CONFIG.HuaWeiObs.SecretKey, global.GVA_CONFIG.HuaWeiObs.Endpoint)
}
func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) {
// var open multipart.File
open, err := file.Open()
if err != nil {
return "", "", err
}
defer open.Close()
filename := file.Filename
input := &obs.PutObjectInput{
PutObjectBasicInput: obs.PutObjectBasicInput{
ObjectOperationInput: obs.ObjectOperationInput{
Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket,
Key: filename,
},
ContentType: file.Header.Get("content-type"),
},
Body: open,
}
var client *obs.ObsClient
client, err = NewHuaWeiObsClient()
if err != nil {
return "", "", errors.Wrap(err, "获取华为对象存储对象失败!")
}
_, err = client.PutObject(input)
if err != nil {
return "", "", errors.Wrap(err, "文件上传失败!")
}
filepath := global.GVA_CONFIG.HuaWeiObs.Path + "/" + filename
return filepath, filename, err
}
func (o *Obs) DeleteFile(key string) error {
client, err := NewHuaWeiObsClient()
if err != nil {
return errors.Wrap(err, "获取华为对象存储对象失败!")
}
input := &obs.DeleteObjectInput{
Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket,
Key: key,
}
var output *obs.DeleteObjectOutput
output, err = client.DeleteObject(input)
if err != nil {
return errors.Wrapf(err, "删除对象(%s)失败!, output: %v", key, output)
}
return nil
}

96
utils/upload/qiniu.go Normal file
View File

@@ -0,0 +1,96 @@
package upload
import (
"context"
"errors"
"fmt"
"mime/multipart"
"time"
"github.com/qiniu/api.v7/v7/auth/qbox"
"github.com/qiniu/api.v7/v7/storage"
"go.uber.org/zap"
"miniapp/global"
)
type Qiniu struct{}
//@author: [piexlmax](https://github.com/piexlmax)
//@author: [ccfish86](https://github.com/ccfish86)
//@author: [SliverHorn](https://github.com/SliverHorn)
//@object: *Qiniu
//@function: UploadFile
//@description: 上传文件
//@param: file *multipart.FileHeader
//@return: string, string, error
func (*Qiniu) UploadFile(file *multipart.FileHeader) (string, string, error) {
putPolicy := storage.PutPolicy{Scope: global.GVA_CONFIG.Qiniu.Bucket}
mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey)
upToken := putPolicy.UploadToken(mac)
cfg := qiniuConfig()
formUploader := storage.NewFormUploader(cfg)
ret := storage.PutRet{}
putExtra := storage.PutExtra{Params: map[string]string{"x:name": "github logo"}}
f, openError := file.Open()
if openError != nil {
global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename) // 文件名格式 自己可以改 建议保证唯一性
putErr := formUploader.Put(context.Background(), &ret, upToken, fileKey, f, file.Size, &putExtra)
if putErr != nil {
global.GVA_LOG.Error("function formUploader.Put() failed", zap.Any("err", putErr.Error()))
return "", "", errors.New("function formUploader.Put() failed, err:" + putErr.Error())
}
return global.GVA_CONFIG.Qiniu.ImgPath + "/" + ret.Key, ret.Key, nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@author: [ccfish86](https://github.com/ccfish86)
//@author: [SliverHorn](https://github.com/SliverHorn)
//@object: *Qiniu
//@function: DeleteFile
//@description: 删除文件
//@param: key string
//@return: error
func (*Qiniu) DeleteFile(key string) error {
mac := qbox.NewMac(global.GVA_CONFIG.Qiniu.AccessKey, global.GVA_CONFIG.Qiniu.SecretKey)
cfg := qiniuConfig()
bucketManager := storage.NewBucketManager(mac, cfg)
if err := bucketManager.Delete(global.GVA_CONFIG.Qiniu.Bucket, key); err != nil {
global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
}
return nil
}
//@author: [SliverHorn](https://github.com/SliverHorn)
//@object: *Qiniu
//@function: qiniuConfig
//@description: 根据配置文件进行返回七牛云的配置
//@return: *storage.Config
func qiniuConfig() *storage.Config {
cfg := storage.Config{
UseHTTPS: global.GVA_CONFIG.Qiniu.UseHTTPS,
UseCdnDomains: global.GVA_CONFIG.Qiniu.UseCdnDomains,
}
switch global.GVA_CONFIG.Qiniu.Zone { // 根据配置文件进行初始化空间对应的机房
case "ZoneHuadong":
cfg.Zone = &storage.ZoneHuadong
case "ZoneHuabei":
cfg.Zone = &storage.ZoneHuabei
case "ZoneHuanan":
cfg.Zone = &storage.ZoneHuanan
case "ZoneBeimei":
cfg.Zone = &storage.ZoneBeimei
case "ZoneXinjiapo":
cfg.Zone = &storage.ZoneXinjiapo
}
return &cfg
}

View File

@@ -0,0 +1,61 @@
package upload
import (
"context"
"errors"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"time"
"miniapp/global"
"github.com/tencentyun/cos-go-sdk-v5"
"go.uber.org/zap"
)
type TencentCOS struct{}
// UploadFile upload file to COS
func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) {
client := NewClient()
f, openError := file.Open()
if openError != nil {
global.GVA_LOG.Error("function file.Open() failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
_, err := client.Object.Put(context.Background(), global.GVA_CONFIG.TencentCOS.PathPrefix+"/"+fileKey, f, nil)
if err != nil {
panic(err)
}
return global.GVA_CONFIG.TencentCOS.BaseURL + "/" + global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil
}
// DeleteFile delete file form COS
func (*TencentCOS) DeleteFile(key string) error {
client := NewClient()
name := global.GVA_CONFIG.TencentCOS.PathPrefix + "/" + key
_, err := client.Object.Delete(context.Background(), name)
if err != nil {
global.GVA_LOG.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
}
return nil
}
// NewClient init COS client
func NewClient() *cos.Client {
urlStr, _ := url.Parse("https://" + global.GVA_CONFIG.TencentCOS.Bucket + ".cos." + global.GVA_CONFIG.TencentCOS.Region + ".myqcloud.com")
baseURL := &cos.BaseURL{BucketURL: urlStr}
client := cos.NewClient(baseURL, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: global.GVA_CONFIG.TencentCOS.SecretID,
SecretKey: global.GVA_CONFIG.TencentCOS.SecretKey,
},
})
return client
}

37
utils/upload/upload.go Normal file
View File

@@ -0,0 +1,37 @@
package upload
import (
"mime/multipart"
"miniapp/global"
)
// OSS 对象存储接口
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [ccfish86](https://github.com/ccfish86)
type OSS interface {
UploadFile(file *multipart.FileHeader) (string, string, error)
DeleteFile(key string) error
}
// NewOss OSS的实例化方法
// Author [SliverHorn](https://github.com/SliverHorn)
// Author [ccfish86](https://github.com/ccfish86)
func NewOss() OSS {
switch global.GVA_CONFIG.System.OssType {
case "local":
return &Local{}
case "qiniu":
return &Qiniu{}
case "tencent-cos":
return &TencentCOS{}
case "aliyun-oss":
return &AliyunOSS{}
case "huawei-obs":
return HuaWeiObs
case "aws-s3":
return &AwsS3{}
default:
return &Local{}
}
}

294
utils/validator.go Normal file
View File

@@ -0,0 +1,294 @@
package utils
import (
"errors"
"reflect"
"regexp"
"strconv"
"strings"
)
type Rules map[string][]string
type RulesMap map[string]Rules
var CustomizeMap = make(map[string]Rules)
//@author: [piexlmax](https://github.com/piexlmax)
//@function: RegisterRule
//@description: 注册自定义规则方案建议在路由初始化层即注册
//@param: key string, rule Rules
//@return: err error
func RegisterRule(key string, rule Rules) (err error) {
if CustomizeMap[key] != nil {
return errors.New(key + "已注册,无法重复注册")
} else {
CustomizeMap[key] = rule
return nil
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: NotEmpty
//@description: 非空 不能为其对应类型的0值
//@return: string
func NotEmpty() string {
return "notEmpty"
}
// @author: [zooqkl](https://github.com/zooqkl)
// @function: RegexpMatch
// @description: 正则校验 校验输入项是否满足正则表达式
// @param: rule string
// @return: string
func RegexpMatch(rule string) string {
return "regexp=" + rule
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Lt
//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Lt(mark string) string {
return "lt=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Le
//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Le(mark string) string {
return "le=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Eq
//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Eq(mark string) string {
return "eq=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Ne
//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Ne(mark string) string {
return "ne=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Ge
//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Ge(mark string) string {
return "ge=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Gt
//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Gt(mark string) string {
return "gt=" + mark
}
//
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Verify
//@description: 校验方法
//@param: st interface{}, roleMap Rules(入参实例规则map)
//@return: err error
func Verify(st interface{}, roleMap Rules) (err error) {
compareMap := map[string]bool{
"lt": true,
"le": true,
"eq": true,
"ne": true,
"ge": true,
"gt": true,
}
typ := reflect.TypeOf(st)
val := reflect.ValueOf(st) // 获取reflect.Type类型
kd := val.Kind() // 获取到st对应的类别
if kd != reflect.Struct {
return errors.New("expect struct")
}
num := val.NumField()
// 遍历结构体的所有字段
for i := 0; i < num; i++ {
tagVal := typ.Field(i)
val := val.Field(i)
if tagVal.Type.Kind() == reflect.Struct {
if err = Verify(val.Interface(), roleMap); err != nil {
return err
}
}
if len(roleMap[tagVal.Name]) > 0 {
for _, v := range roleMap[tagVal.Name] {
switch {
case v == "notEmpty":
if isBlank(val) {
return errors.New(tagVal.Name + "值不能为空")
}
case strings.Split(v, "=")[0] == "regexp":
if !regexpMatch(strings.Split(v, "=")[1], val.String()) {
return errors.New(tagVal.Name + "格式校验不通过")
}
case compareMap[strings.Split(v, "=")[0]]:
if !compareVerify(val, v) {
return errors.New(tagVal.Name + "长度或值不在合法范围," + v)
}
}
}
}
}
return nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: compareVerify
//@description: 长度和数字的校验方法 根据类型自动校验
//@param: value reflect.Value, VerifyStr string
//@return: bool
func compareVerify(value reflect.Value, VerifyStr string) bool {
switch value.Kind() {
case reflect.String:
return compare(len([]rune(value.String())), VerifyStr)
case reflect.Slice, reflect.Array:
return compare(value.Len(), VerifyStr)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return compare(value.Uint(), VerifyStr)
case reflect.Float32, reflect.Float64:
return compare(value.Float(), VerifyStr)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return compare(value.Int(), VerifyStr)
default:
return false
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: isBlank
//@description: 非空校验
//@param: value reflect.Value
//@return: bool
func isBlank(value reflect.Value) bool {
switch value.Kind() {
case reflect.String, reflect.Slice:
return value.Len() == 0
case reflect.Bool:
return !value.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return value.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return value.Uint() == 0
case reflect.Float32, reflect.Float64:
return value.Float() == 0
case reflect.Interface, reflect.Ptr:
return value.IsNil()
}
return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: compare
//@description: 比较函数
//@param: value interface{}, VerifyStr string
//@return: bool
func compare(value interface{}, VerifyStr string) bool {
VerifyStrArr := strings.Split(VerifyStr, "=")
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64)
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Int() < VInt
case VerifyStrArr[0] == "le":
return val.Int() <= VInt
case VerifyStrArr[0] == "eq":
return val.Int() == VInt
case VerifyStrArr[0] == "ne":
return val.Int() != VInt
case VerifyStrArr[0] == "ge":
return val.Int() >= VInt
case VerifyStrArr[0] == "gt":
return val.Int() > VInt
default:
return false
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
VInt, VErr := strconv.Atoi(VerifyStrArr[1])
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Uint() < uint64(VInt)
case VerifyStrArr[0] == "le":
return val.Uint() <= uint64(VInt)
case VerifyStrArr[0] == "eq":
return val.Uint() == uint64(VInt)
case VerifyStrArr[0] == "ne":
return val.Uint() != uint64(VInt)
case VerifyStrArr[0] == "ge":
return val.Uint() >= uint64(VInt)
case VerifyStrArr[0] == "gt":
return val.Uint() > uint64(VInt)
default:
return false
}
case reflect.Float32, reflect.Float64:
VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64)
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Float() < VFloat
case VerifyStrArr[0] == "le":
return val.Float() <= VFloat
case VerifyStrArr[0] == "eq":
return val.Float() == VFloat
case VerifyStrArr[0] == "ne":
return val.Float() != VFloat
case VerifyStrArr[0] == "ge":
return val.Float() >= VFloat
case VerifyStrArr[0] == "gt":
return val.Float() > VFloat
default:
return false
}
default:
return false
}
}
func regexpMatch(rule, matchStr string) bool {
return regexp.MustCompile(rule).MatchString(matchStr)
}

37
utils/validator_test.go Normal file
View File

@@ -0,0 +1,37 @@
package utils
import (
"miniapp/model/common/request"
"testing"
)
type PageInfoTest struct {
PageInfo request.PageInfo
Name string
}
func TestVerify(t *testing.T) {
PageInfoVerify := Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}, "Name": {NotEmpty()}}
var testInfo PageInfoTest
testInfo.Name = "test"
testInfo.PageInfo.Page = 0
testInfo.PageInfo.PageSize = 0
err := Verify(testInfo, PageInfoVerify)
if err == nil {
t.Error("校验失败未能捕捉0值")
}
testInfo.Name = ""
testInfo.PageInfo.Page = 1
testInfo.PageInfo.PageSize = 10
err = Verify(testInfo, PageInfoVerify)
if err == nil {
t.Error("校验失败未能正常检测name为空")
}
testInfo.Name = "test"
testInfo.PageInfo.Page = 1
testInfo.PageInfo.PageSize = 10
err = Verify(testInfo, PageInfoVerify)
if err != nil {
t.Error("校验失败,未能正常通过检测")
}
}

19
utils/verify.go Normal file
View File

@@ -0,0 +1,19 @@
package utils
var (
IdVerify = Rules{"ID": []string{NotEmpty()}}
ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}}
MenuVerify = Rules{"Path": {NotEmpty()}, "ParentId": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}}
MenuMetaVerify = Rules{"Title": {NotEmpty()}}
LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Username": {NotEmpty()}, "Password": {NotEmpty()}}
RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}}
PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}}
CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}}
AutoCodeVerify = Rules{"Abbreviation": {NotEmpty()}, "StructName": {NotEmpty()}, "PackageName": {NotEmpty()}, "Fields": {NotEmpty()}}
AutoPackageVerify = Rules{"PackageName": {NotEmpty()}}
AuthorityVerify = Rules{"AuthorityId": {NotEmpty()}, "AuthorityName": {NotEmpty()}}
AuthorityIdVerify = Rules{"AuthorityId": {NotEmpty()}}
OldAuthorityVerify = Rules{"OldAuthorityId": {NotEmpty()}}
ChangePasswordVerify = Rules{"Password": {NotEmpty()}, "NewPassword": {NotEmpty()}}
SetUserAuthorityVerify = Rules{"AuthorityId": {NotEmpty()}}
)

70
utils/wechat.go Normal file
View File

@@ -0,0 +1,70 @@
package utils
import (
"github.com/medivhzhan/weapp/v3"
"github.com/medivhzhan/weapp/v3/auth"
"github.com/medivhzhan/weapp/v3/phonenumber"
"go.uber.org/zap"
"miniapp/global"
"miniapp/model/app/request"
)
type wechat struct{}
func WeChatUtils() *wechat {
return &wechat{}
}
// GetWechatUnionId 获取微信用户基础信息
func (w wechat) GetWechatUnionId(code string) (unionId, openId, sessionKey string, err error) {
sdk := weapp.NewClient(global.GVA_CONFIG.MiniApp.AppId, global.GVA_CONFIG.MiniApp.AppSecret, weapp.WithLogger(nil))
cli := sdk.NewAuth()
p := auth.Code2SessionRequest{
Appid: global.GVA_CONFIG.MiniApp.AppId,
Secret: global.GVA_CONFIG.MiniApp.AppSecret,
JsCode: code,
GrantType: "authorization_code",
}
session, err := cli.Code2Session(&p)
if err != nil {
return
}
if session.GetResponseError() != nil {
global.GVA_LOG.Error("Code解析失败: %v", zap.Error(session.GetResponseError()))
err = session.GetResponseError()
return
}
// 设置UnionId值
unionId = session.Unionid
openId = session.Openid
sessionKey = session.SessionKey
return
}
// GetWechatPhone 根据Code获取小程序用户手机号
// return 不带区号的手机号
func (w wechat) GetWechatPhone(param request.DecryptMobile) (string, error) {
sdk := weapp.NewClient(global.GVA_CONFIG.MiniApp.AppId, global.GVA_CONFIG.MiniApp.AppSecret)
mobile, err := sdk.DecryptMobile(param.SessionKey, param.EncryptedData, param.Iv)
if err != nil {
global.GVA_LOG.Error("解密手机号失败: %v", zap.Error(err))
return "", err
}
global.GVA_LOG.Debug("解密后的手机号: %+v", zap.String("mobile", mobile.PurePhoneNumber))
return mobile.PurePhoneNumber, nil
}
// GetPhoneNumber 获取手机号
func (w wechat) GetPhoneNumber(code string) (phone string, err error) {
sdk := weapp.NewClient(global.GVA_CONFIG.MiniApp.AppId, global.GVA_CONFIG.MiniApp.AppSecret)
resp, err := sdk.NewPhonenumber().GetPhoneNumber(&phonenumber.GetPhoneNumberRequest{Code: code})
if err != nil {
global.GVA_LOG.Error("获取手机号失败: %v", zap.Error(err))
return
}
// 获取手机号
phone = resp.Data.PurePhoneNumber
return
}

110
utils/zip.go Normal file
View File

@@ -0,0 +1,110 @@
package utils
import (
"archive/zip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// 解压
func Unzip(zipFile string, destDir string) ([]string, error) {
zipReader, err := zip.OpenReader(zipFile)
var paths []string
if err != nil {
return []string{}, err
}
defer zipReader.Close()
for _, f := range zipReader.File {
if strings.Index(f.Name, "..") > -1 {
return []string{}, fmt.Errorf("%s 文件名不合法", f.Name)
}
fpath := filepath.Join(destDir, f.Name)
paths = append(paths, fpath)
if f.FileInfo().IsDir() {
os.MkdirAll(fpath, os.ModePerm)
} else {
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return []string{}, err
}
inFile, err := f.Open()
if err != nil {
return []string{}, err
}
defer inFile.Close()
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return []string{}, err
}
defer outFile.Close()
_, err = io.Copy(outFile, inFile)
if err != nil {
return []string{}, err
}
}
}
return paths, nil
}
func ZipFiles(filename string, files []string, oldForm, newForm string) error {
newZipFile, err := os.Create(filename)
if err != nil {
return err
}
defer func() {
_ = newZipFile.Close()
}()
zipWriter := zip.NewWriter(newZipFile)
defer func() {
_ = zipWriter.Close()
}()
// 把files添加到zip中
for _, file := range files {
err = func(file string) error {
zipFile, err := os.Open(file)
if err != nil {
return err
}
defer zipFile.Close()
// 获取file的基础信息
info, err := zipFile.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// 使用上面的FileInforHeader() 就可以把文件保存的路径替换成我们自己想要的了,如下面
header.Name = strings.Replace(file, oldForm, newForm, -1)
// 优化压缩
// 更多参考see http://golang.org/pkg/archive/zip/#pkg-constants
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if _, err = io.Copy(writer, zipFile); err != nil {
return err
}
return nil
}(file)
if err != nil {
return err
}
}
return nil
}