This commit is contained in:
2022-03-01 13:50:13 +08:00
parent 8eab9943e9
commit d2cabd7d58
113 changed files with 7925 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
package base
import "time"
// GlobalModel 全局映射实体
type GlobalModel struct {
CreateTime time.Time `json:"createTime"` // 创建时间
CreateBy string `json:"createBy"` // 创建人
UpdateTime time.Time `json:"updateTime"` // 更新时间
UpdateBy string `json:"updateBy"` // 更新人
}
// GlobalQuery 全局Query通用条件
type GlobalQuery struct {
BeginTime string `form:"beginTime"` // 开始时间
EndTime string `form:"endTime"` // 结束时间
PageNum int `form:"pageNum"` // 当前页码
PageSize int `form:"pageSize"` // 显示条数
}
+58
View File
@@ -0,0 +1,58 @@
package cache
import (
"cutego/core/dao"
"cutego/pkg/common"
"cutego/pkg/constant"
)
// RemoveList 批量根据Key删除数据
// @Param list []string 键合集
func RemoveList(list []string) {
dao.RedisDB.DELALL(list)
}
// RemoveKey 根据key删除
// @Param key 键
// @Return int 删除的数量
func RemoveCache(key string) int {
del, err := dao.RedisDB.DEL(key)
if err != nil {
common.ErrorLog(err)
}
return del
}
// GetCache 获取缓存数据
// @Param key 键
// @Return string 值
func GetCache(key string) string {
val, err := dao.RedisDB.GET(key)
if err != nil {
common.ErrorLog(constant.RedisConst{}.GetRedisError(), err.Error())
return ""
}
return val
}
// SetCache 设置缓存数据
// @Param key 键
// @Param value 值
// @Return 新增的行数
func SetCache(key string, value interface{}) int {
n, err := dao.RedisDB.SET(key, common.StructToJson(value))
if err != nil {
common.ErrorLog(constant.RedisConst{}.GetRedisError(), err.Error())
return 0
}
return int(n)
}
// SetCache 设置缓存数据, 并指定过期时间
// @Param key 键
// @Param value 值
// @Param sec 过期时间(单位: 秒)
// @Return 新增的行数
func SetCacheTTL(key string, value interface{}, sec int) {
dao.RedisDB.SETEX(key, sec, common.StructToJson(value))
}
+51
View File
@@ -0,0 +1,51 @@
package common
// RedisData 存储数据结构
type RedisData struct {
Key string
Field string
Value string
Expire int64
}
// RedisDataArray RedisData of array
type RedisDataArray []*RedisData
// IRedis redis client interface
type IRedis interface {
// KEYS get patten key array
KEYS(patten string) ([]string, error)
// SCAN get patten key array
SCAN(patten string) ([]string, error)
// DEL delete k-v
DEL(key string) (int, error)
// DELALL delete key array
DELALL(key []string) (int, error)
// GET get k-v
GET(key string) (string, error)
// SET set k-v
//SET(key string, value string) (int64, error)
// SETEX set k-v expire seconds
SETEX(key string, sec int, value string) (int64, error)
// EXPIRE set key expire seconds
EXPIRE(key string, sec int64) (int64, error)
// HGETALL get map of key
HGETALL(key string) (map[string]string, error)
// HGET get value of key-field
HGET(key string, field string) (string, error)
// HSET set value of key-field
//HSET(key string, field string, value string) (int64, error)
// Write 向redis中写入多组数据
Write(data RedisDataArray)
}
+23
View File
@@ -0,0 +1,23 @@
package common
import "github.com/druidcaesa/gotool"
func ErrorLog(v ...interface{}) {
gotool.Logs.ErrorLog().Println(v)
}
func FatalfLog(format string, v ...interface{}) {
gotool.Logs.ErrorLog().Fatalf(format, v)
}
func InfoLog(v ...interface{}) {
gotool.Logs.InfoLog().Println(v)
}
func InfoLogf(format string, v ...interface{}) {
gotool.Logs.InfoLog().Printf(format+"\n", v)
}
func DebugLogf(format string, v ...interface{}) {
gotool.Logs.DebugLog().Printf(format+"\n", v)
}
+142
View File
@@ -0,0 +1,142 @@
package common
import (
"cutego/pkg/config"
"encoding/json"
"fmt"
"os"
"runtime"
"strconv"
)
// IntToString int转string
func IntToString(n int) string {
return strconv.Itoa(n)
}
func mapToBytes(data map[string]interface{}) []byte {
bytes, _ := json.Marshal(data)
return bytes
}
// MapToStruct map转struct
func MapToStruct(data map[string]interface{}, v interface{}) {
_ = json.Unmarshal(mapToBytes(data), v)
}
// GetDirPath 获取目录路径
func GetDirPath(resType string) string {
sysType := runtime.GOOS
switch sysType {
case "linux":
if resType == "log" {
return config.AppCoreConfig.CuteGoConfig.File.Linux.Logs
} else if resType == "avatar" {
return config.AppCoreConfig.CuteGoConfig.File.Linux.Avatar
} else if resType == "file" {
return config.AppCoreConfig.CuteGoConfig.File.Linux.Path
}
break
case "windows":
if resType == "log" {
return config.AppCoreConfig.CuteGoConfig.File.Windows.Logs
} else if resType == "avatar" {
return config.AppCoreConfig.CuteGoConfig.File.Windows.Avatar
} else if resType == "file" {
return config.AppCoreConfig.CuteGoConfig.File.Windows.Path
}
break
case "mac":
if resType == "log" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Logs
} else if resType == "avatar" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Avatar
} else if resType == "file" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Path
}
break
case "darwin":
if resType == "log" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Logs
} else if resType == "avatar" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Avatar
} else if resType == "file" {
return config.AppCoreConfig.CuteGoConfig.File.Mac.Path
}
}
return config.AppCoreConfig.CuteGoConfig.File.Linux.Logs
}
// CreateAllDir 递归创建文件夹
func CreateAllDir(filePath string) error {
if !IsFileOrDirExist(filePath) {
err := os.MkdirAll(filePath, os.ModePerm)
if err != nil {
fmt.Println("创建文件夹失败, error info:", err)
return err
}
return err
}
return nil
}
// IsFileOrDirExist 判断所给路径文件/文件夹是否存在(返回true是存在)
func IsFileOrDirExist(path string) bool {
// os.Stat获取文件信息
_, err := os.Stat(path)
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
// 类三元表达式
// condition 成立条件
// trueVal 当条件为true时返回
// false 当条件为false时返回
func If(condition bool, trueVal, falseVal interface{}) interface{} {
if condition {
return trueVal
}
return falseVal
}
// 结构体、Map等转Json字符串
// @Param v interface{}
// @Return Json字符串
func StructToJson(v interface{}) string {
jsonBytes, err := json.Marshal(&v)
if err != nil {
ErrorLog(err)
return ""
}
s := string(jsonBytes)
DebugLogf("StructToJson, json=%s", s)
return s
}
// Json字符串转结构体、Map等
//
// 单个对象
// s := new(models2.SysConfig)
// return common.JsonToStruct(get, s).(*models2.SysConfig)
//
// 切片(interface{}.(期望类型))
// s := make([]interface {}, 0)
// target := common.JsonToStruct(get, s)
// target.([]entity.SysDictData)
//
// @Param data Json字符串
// @Param s 容器(结构体、Map等)
// @Return interface{}
func JsonToStruct(data string, s interface{}) interface{} {
err := json.Unmarshal([]byte(data), &s)
if err != nil {
ErrorLog(err)
return nil
}
DebugLogf("JsonToStruct, obj=%v", s)
return s
}
+96
View File
@@ -0,0 +1,96 @@
package config
import (
config "cutego/pkg/config/models"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
)
var (
// AppCoreConfig 核心配置
AppCoreConfig *config.ApplicationCoreStruct
// AppEnvConfig 环境配置
AppEnvConfig *config.ApplicationEnvStruct
)
// GetRootPath 获取项目根路径
func GetRootPath() string {
rootPath, _ := os.Getwd()
return rootPath
}
// GetPathSeparator 获取路径分隔符
func GetPathSeparator() string {
return string(os.PathSeparator)
}
// PathExists 判断文件或文件夹是否存在
// 如果返回的错误为nil,说明文件或文件夹存在
// 如果返回的错误类型使用os.IsNotExist()判断为true,说明文件或文件夹不存在
// 如果返回的错误为其它类型,则不确定是否在存在
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// LoadYamlFile 加载yaml文件
func LoadYamlFile(filename string, v interface{}) {
data, err := ioutil.ReadFile(filename)
if err != nil {
panic(err.Error())
}
err = yaml.Unmarshal(data, v)
if err != nil {
panic(err.Error())
}
}
// 配置文件所在路径
const BaseConfigDirPath = "configs"
func readAppYmlFile(resourcePath string) {
// 读取主配置文件
applicationCoreFileName := BaseConfigDirPath + "/application.yml"
applicationCoreFilePath := resourcePath + GetPathSeparator() + applicationCoreFileName
exists, _ := PathExists(applicationCoreFilePath)
if !exists {
panic(applicationCoreFileName + "配置文件不存在!")
}
AppCoreConfig = &config.ApplicationCoreStruct{}
// 由于要改变appConfig内部的值, 所以这里要取址
LoadYamlFile(applicationCoreFilePath, AppCoreConfig)
// 读取环境文件
applicationEnvFileName := fmt.Sprintf(BaseConfigDirPath+"/application-%s.yml", AppCoreConfig.CuteGoConfig.Active)
applicationEnvFilePath := resourcePath + GetPathSeparator() + applicationEnvFileName
exists, _ = PathExists(applicationEnvFilePath)
if !exists {
panic(applicationEnvFileName + "配置文件不存在!")
}
AppEnvConfig = &config.ApplicationEnvStruct{}
// 由于要改变appConfig内部的值, 所以这里要取址
LoadYamlFile(applicationEnvFilePath, AppEnvConfig)
}
// ExecuteTip 执行日志
func ExecuteTip(message string) {
fmt.Println(fmt.Sprintf("-------------------- %s --------------------", message))
}
func init() {
// 资源文件所在的路径
resourcePath := GetRootPath()
ExecuteTip("初始化应用配置 start")
readAppYmlFile(resourcePath)
ExecuteTip("初始化应用配置 end")
return
}
+67
View File
@@ -0,0 +1,67 @@
package config
type ApplicationCoreStruct struct {
CuteGoConfig CuteGoConfig `yaml:"cutego"`
}
// CuteGoConfig 总配置
type CuteGoConfig struct {
// 默认激活dev配置
Active string `yaml:"active" default:"dev"`
// 开启演示模式
DemoMode bool `yaml:"demo-mode"`
Mail MailConfig `yaml:"mail"`
TaskPool TaskPoolConfig `yaml:"task-pool"`
Captcha CaptchaConfig `yaml:"captcha"`
File FileConfig `yaml:"file"`
}
// MailConfig 邮件
type MailConfig struct {
// 服务地址
Host string `yaml:"host"`
// 服务端口
Port int `yaml:"port"`
// 用户名
Username string `yaml:"username"`
// 密码
Password string `yaml:"password"`
// 默认编码
DefaultEncoding string `yaml:"default-encoding"`
}
// TaskPoolConfig 线程池
type TaskPoolConfig struct {
// 核心线程池大小
CorePoolSize int `yaml:"core-pool-size"`
// 最大线程数(尽可能的大)
MaxPoolSize int `yaml:"max-pool-size"`
// 活跃时间(单位: s)
KeepAliveSeconds int `yaml:"keep-alive-seconds"`
// 队列容量
QueueCapacity int `yaml:"queue-capacity"`
}
// CaptchaConfig 验证码有效时间(单位: s)
type CaptchaConfig struct {
// 邮箱
Email int `yaml:"email"`
// 手机短信
Sms int `yaml:"sms"`
}
// FileConfig 文件上传
type FileConfig struct {
// 文件大小(单位: mb)
FileMaxSize int `yaml:"file-max-size"`
// 头像大小(单位: mb)
AvatarMaxSize int `yaml:"avatar-max-size"`
Mac FilePath `yaml:"mac"`
Linux FilePath `yaml:"linux"`
Windows FilePath `yaml:"windows"`
}
type FilePath struct {
Path string `yaml:"path"`
Avatar string `yaml:"avatar"`
Logs string `yaml:"logs"`
}
+111
View File
@@ -0,0 +1,111 @@
package config
type ApplicationEnvStruct struct {
Server ServerConfig `yaml:"server"`
DataSource DataSourceConfig `yaml:"datasource"`
Redis RedisConfig `yaml:"redis"`
MongoDb MongoDbConfig `yaml:"mongodb"`
Login LoginConfig `yaml:"login"`
Jwt JwtConfig `yaml:"jwt"`
Logger LoggerConfig `yaml:"logger"`
}
// ServerConfig web服务
type ServerConfig struct {
// Running in "debug" mode. Switch to "release" mode in production
RunMode string `yaml:"run-mode"`
// web服务监听端口(生产的端口有可能不一样,所以根据环境隔离开)
Port int `yaml:"port"`
}
// DataSourceConfig 数据源
type DataSourceConfig struct {
// 数据库类型
DbType string `yaml:"db-type" default:"mysql"`
// 服务地址
Host string `yaml:"host"`
// 服务端口
Port int `yaml:"port"`
// 用户名称
Username string `yaml:"username"`
// 用户密码
Password string `yaml:"password"`
// 数据库名称
Database string `yaml:"database"`
// 编码
Charset string `yaml:"charset"`
// 空闲时的最大连接数
MaxIdleSize int `yaml:"max-idle-size"`
// 数据库的最大打开连接数
MaxOpenSize int `yaml:"max-open-size"`
}
// RedisConfig 缓存
type RedisConfig struct {
// 数据库索引
Database int `yaml:"database"`
// 服务地址
Host string `yaml:"host"`
// 服务端口
Port int `yaml:"port"`
// 服务密码
Password string `yaml:"password"`
// 连接超时时间
Timeout int `yaml:"timeout"`
Pool RedisPoolConfig `yaml:"pool"`
}
// RedisPoolConfig redis连接池配置
type RedisPoolConfig struct {
// 连接池最大连接数(使用负值表示没有限制, 最佳配置为cpu核数+1)
MaxActive int `yaml:"max-active"`
// 连接池中的最大空闲连接
MaxIdle int `yaml:"max-idle"`
// 连接池最大阻塞等待时间(使用负值表示没有限制)
MaxWait int `yaml:"max-wait"`
}
// MongoDbConfig MongoDB
type MongoDbConfig struct {
Url string `yaml:"url"`
Port string `yaml:"port"`
DB string `yaml:"db"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
// LoginConfig 登录相关
type LoginConfig struct {
// 是否限制单用户登录
Single bool `yaml:"single"`
}
// JwtConfig jwt参数
type JwtConfig struct {
// 请求头前缀
Header string `yaml:"header" json:"header,omitempty"`
// 令牌前缀
TokenStartWith string `yaml:"token-start-with" json:"token_start_with,omitempty"`
// 加密密钥
TokenSecret string `yaml:"token-secret" json:"token_secret,omitempty"`
// 令牌过期时间 此处单位: h , 默认1小时
TokenExpired int `yaml:"token-expired" json:"token_expired,omitempty"`
// token在cookie中的别称
CookieKey string `yaml:"cookie-key"`
// 在线用户key
OnlineKey string `yaml:"online-key" json:"online_key,omitempty"`
// token 续期检查时间范围(默认30分钟, 单位毫秒), 在token即将过期的一段时间内用户操作了, 则给用户的token续期
Detect int `yaml:"detect" json:"detect,omitempty"`
// 续期时间范围, 默认1小时, 单位毫秒
Renew int `yaml:"renew" json:"renew,omitempty"`
}
// LoggerConfig 日志文件
type LoggerConfig struct {
// 最大保存时间(单位: d)
MaxSaveAge int `yaml:"max-save-age"`
// 日志切割时间间隔(单位: d)
RotationTime int `yaml:"rotation-time"`
// 日志级别
Level string `yaml:"level"`
}
+5
View File
@@ -0,0 +1,5 @@
package constant
const (
RedisOnlineUserKey = "online:"
)
+12
View File
@@ -0,0 +1,12 @@
package constant
const (
mysqlErrorMsg = "调用MySQL发生异常, %s"
)
type MysqlConst struct{}
// GetMysqlError Mysql异常拼接常量
func (c MysqlConst) GetMysqlError() string {
return mysqlErrorMsg
}
+25
View File
@@ -0,0 +1,25 @@
package constant
const (
redisErrorMsg = "调用Redis发生异常, %s"
redisDictKey = "dict:"
redisConfigKey = "config:"
)
// RedisConst Redis相关操作常量
type RedisConst struct{}
// GetRedisError Redis异常拼接常量
func (c RedisConst) GetRedisError() string {
return redisErrorMsg
}
// GetRedisDictKey 获取Redis的Dict的key
func (c RedisConst) GetRedisDictKey() string {
return redisDictKey
}
// GetRedisConfigKey 获取redis的config的key
func (c RedisConst) GetRedisConfigKey() string {
return redisConfigKey
}
+70
View File
@@ -0,0 +1,70 @@
package cronjob
import (
"cutego/core/api/v1/request"
"cutego/core/job"
"cutego/core/service"
"cutego/pkg/common"
"github.com/robfig/cron"
"time"
)
// Cron表达式参考
// 每隔5秒执行一次:*/5 * * * * ?
// 每隔1分钟执行一次:0 */1 * * * ?
// 每天23点执行一次:0 0 23 * * ?
// 每天凌晨1点执行一次:0 0 1 * * ?
// 每月1号凌晨1点执行一次:0 0 1 1 * ?
// 每月最后一天23点执行一次:0 0 23 L * ?
// 每周星期天凌晨1点实行一次:0 0 1 ? * L
// 在26分、29分、33分执行一次:0 26,29,33 * * * ?
// 每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
// 定时任务: 别名与调度器的映射
var AliasCronMap = make(map[string]*cron.Cron)
// 停止任务, 不会停止已开始的任务
func StopCronFunc(aliasName string) {
common.InfoLogf("停止任务 %s ---> Start", aliasName)
AliasCronMap[aliasName].Stop()
common.InfoLogf("停止任务 %s ---> Finish", aliasName)
}
// 开始任务
func StartCronFunc(aliasName string) {
common.InfoLogf("唤起任务 %s ---> Start", aliasName)
AliasCronMap[aliasName].Start()
common.InfoLogf("唤起任务 %s ---> Finish", aliasName)
}
func init() {
if len(job.AliasFuncMap) > 0 {
//go test()
index := 1
for true {
q := request.CronJobQuery{}
q.PageNum = index
data, _ := service.CronJobService{}.FindPage(q)
if len(data) == 0 {
break
}
for _, datum := range data {
c := cron.New()
c.AddFunc(datum.JobCron, job.AliasFuncMap[datum.FuncAlias])
c.Start()
AliasCronMap[datum.FuncAlias] = c
common.InfoLogf("调度定时任务 --- %s ---> Success", datum.JobName)
}
index += 1
}
}
}
// 测试通过
func test() {
time.Sleep(time.Second * 10)
StopCronFunc("test1")
time.Sleep(time.Second * 10)
StartCronFunc("test1")
}
+143
View File
@@ -0,0 +1,143 @@
package excels
import (
"github.com/druidcaesa/gotool"
"github.com/xuri/excelize/v2"
"reflect"
"strconv"
)
// 获取title头list
func getTitle(list []map[string]string) []string {
titleList := make([]string, 0)
for _, item := range list {
for _, s := range item {
titleList = append(titleList, s)
}
}
return titleList
}
// ExportExcel 导出excel
func ExportExcel(list []interface{}, title string) (error, *excelize.File) {
// 获取标题
headerList, expList := ExcelCreate(list[0])
headers := getTitle(headerList)
// 默认存在第一个工作簿是 Sheet1 首字母要大写, 否则会报错。
// 如果想额外的创建工作簿, 可以使用, sheet2 := file.NewSheet("Sheet2"), 工作簿的名称不区分大小写。
// 如果有多个工作簿, 可以使用 file.SetActiveSheet(index) 来指定打开文件时focus到哪个工作簿
sheet1 := "Sheet1"
files := excelize.NewFile()
character := string(65 + len(headers) - 1)
/* -------------------- 第一行大标题 -------------------- */
// 设置行高
err := files.SetRowHeight(sheet1, 1, 25)
if err != nil {
return err, nil
}
// 合并单元格
err = files.MergeCell(sheet1, "A1", character+"1")
if err != nil {
return err, nil
}
// 设置单元格样式:对齐;字体, 大小;单元格边框
styleTitle, _ := files.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},"font":{"bold":true,"italic":false,"family":"Calibri","size":16,"color":"#000000"},"border":[{"type":"left","color":"#3FAD08","style":0},{"type":"top","color":"#3FAD08","style":0},{"type":"bottom","color":"#3FAD08","style":2},{"type":"right","color":"#3FAD08","style":0}]}`)
err = files.SetCellStyle(sheet1, "A1", character+"1", styleTitle)
if err != nil {
return err, nil
}
err = files.SetCellValue(sheet1, "A1", title)
if err != nil {
return err, nil
}
/* -------------------- 字段标题 -------------------- */
styleHeader, _ := files.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},"font":{"bold":false,"italic":false,"family":"Calibri","size":10,"color":"#000000"}}`)
err = files.SetCellStyle(sheet1, "A2", character+"2", styleHeader)
if err != nil {
return err, nil
}
for k, v := range headers {
err = files.SetCellValue(sheet1, string(65+k)+"2", v)
if err != nil {
return err, nil
}
}
// 设置最后一列宽度
err = files.SetColWidth(sheet1, "C", character, 20)
if err != nil {
return err, nil
}
// 冻结窗口:冻结第一行和第二行
err = files.SetPanes(sheet1, `{"freeze":true,"split":false,"x_split":0,"y_split":2}`)
if err != nil {
return err, nil
}
///* -------------------- 填充行数据 -------------------- */
line := 3
for _, v := range list {
var num = 0
lineChr := strconv.Itoa(line)
// 设置样式
err = files.SetCellStyle(sheet1, "A"+lineChr, character+lineChr, styleHeader)
if err != nil {
return err, nil
}
// 反射获取数据和类型
getValue := reflect.ValueOf(v)
getType := reflect.TypeOf(v)
n := getValue.NumField()
for i := 0; i < n; i++ {
val := getValue.Field(i)
name := getType.Field(i).Name
if !getIsTitle(name, headerList) {
continue
}
err = files.SetCellValue(sheet1, string(65+num)+lineChr, getExp(name, expList, val.Interface()))
if err != nil {
return err, nil
}
num++
}
line++
}
return nil, files
}
func getIsTitle(name string, headerList []map[string]string) bool {
flag := false
for _, m := range headerList {
if gotool.StrUtils.HasNotEmpty(m[name]) {
flag = true
}
}
return flag
}
func getExp(name string, expList []map[string][]map[string]string, value interface{}) interface{} {
for _, m := range expList {
if len(m[name]) > 0 {
maps := m[name]
for _, m2 := range maps {
if gotool.StrUtils.HasNotEmpty(m2[interfaceToString(value)]) {
value = m2[interfaceToString(value)]
}
}
}
}
return value
}
func interfaceToString(inter interface{}) string {
switch inter.(type) {
case string:
return inter.(string)
case int:
return strconv.Itoa(inter.(int))
case int64:
return strconv.FormatInt(inter.(int64), 10)
default:
return inter.(string)
}
}
+84
View File
@@ -0,0 +1,84 @@
package excels
import (
"reflect"
"strings"
)
// 定义tagName名称
const tagName = "excel"
// Excels 定义Excel接口
type Excels interface {
Excels(interface{}) (bool, string)
}
// DefaultExcels 默认Excel配置
type DefaultExcels struct {
}
// Excels 实现接口
func (e DefaultExcels) Excels(val interface{}) (bool, string) {
return true, ""
}
// `excel:"name='',readConverterExp=''"`
// 获取标题头
func getExcelFromTagTitle(proName string, tag string) map[string]string {
args := strings.SplitN(tag, ",", 2)
m := make(map[string]string)
for i := 0; i < len(args); i++ {
contains := strings.Contains(args[i], "name")
if contains {
m[proName] = strings.Split(args[i], "=")[1]
break
}
}
return m
}
// `excel:"name='',readConverterExp='Y=1,N=2'"`
func getReadConverterExp(proName string, tag string) map[string][]map[string]string {
list := make(map[string][]map[string]string, 0)
args := strings.SplitN(tag, ",", 2)
for i := 0; i < len(args); i++ {
if strings.Contains(args[i], "format") {
maps := make([]map[string]string, 0)
converter := strings.SplitN(args[i], "=", 2)[1]
values := strings.Split(converter, ",")
for j := 0; j < len(values); j++ {
split := strings.Split(values[j], "=")
m := make(map[string]string)
m[split[0]] = split[1]
maps = append(maps, m)
}
list[proName] = maps
break
}
}
return list
}
// ExcelCreate 获取name名称和readConverterExp集合
func ExcelCreate(s interface{}) ([]map[string]string, []map[string][]map[string]string) {
titleList := make([]map[string]string, 0)
expList := make([]map[string][]map[string]string, 0)
// 获取值域
v := reflect.ValueOf(s)
t := reflect.TypeOf(s)
var title map[string]string
var exp map[string][]map[string]string
// 遍历反射树形
for i := 0; i < t.NumField(); i++ {
tag := v.Type().Field(i).Tag.Get(tagName)
if tag == "" || tag == "-" {
continue
}
name := t.Field(i).Name
title = getExcelFromTagTitle(name, tag)
exp = getReadConverterExp(name, tag)
titleList = append(titleList, title)
expList = append(expList, exp)
}
return titleList, expList
}
+16
View File
@@ -0,0 +1,16 @@
package file
import (
"github.com/druidcaesa/gotool"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
)
// DownloadExcel 公共下载execl方法
func DownloadExcel(c *gin.Context, file *excelize.File) {
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename="+gotool.IdUtils.IdUUIDToRan(false)+".xlsx")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("FileName", gotool.IdUtils.IdUUIDToRan(false)+".xlsx")
_ = file.Write(c.Writer)
}
+43
View File
@@ -0,0 +1,43 @@
package filter
import (
"cutego/pkg/config"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func DemoHandler() gin.HandlerFunc {
return func(c *gin.Context) {
if config.AppCoreConfig.CuteGoConfig.DemoMode {
request := inDisRequest()
for i := 0; i < len(request); i++ {
if c.Request.Method == http.MethodDelete || c.Request.Method == http.MethodPut || strings.Contains(c.Request.RequestURI, request[i]) {
c.JSON(http.StatusOK, gin.H{
"status": http.StatusInternalServerError,
"msg": "演示模式, 不允许操作",
})
c.Abort()
return
}
}
}
}
}
// 禁用请求
func inDisRequest() []string {
return []string{
"/remove",
"/profile/avatar",
"/resetPwd",
"/edit",
"/insert",
"/add",
"/delete",
"/export",
"/import",
}
}
+28
View File
@@ -0,0 +1,28 @@
package jwt
import (
"github.com/gin-gonic/gin"
"strings"
)
// 是否在放行范围内
func doSquare(c *gin.Context) bool {
request := inSquareRequest()
for i := 0; i < len(request); i++ {
replace := strings.Contains(c.Request.RequestURI, request[i])
if replace {
return true
}
}
return false
}
// 放行的请求
func inSquareRequest() []string {
var req []string
// 放行登录请求
req = append(req, "/api/v1/login")
// 放行websocket请求
req = append(req, "/websocket")
return req
}
+173
View File
@@ -0,0 +1,173 @@
package jwt
import (
"cutego/core/api/v1/response"
"cutego/core/dao"
"cutego/pkg/cache"
"cutego/pkg/config"
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"strings"
"time"
)
// JWTAuth 中间件, 检查token
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 放行的请求先放行
if doSquare(c) {
return
}
authHeader := c.Request.Header.Get(config.AppEnvConfig.Jwt.Header)
if authHeader == "" {
c.JSON(http.StatusOK, gin.H{
"status": http.StatusUnauthorized,
"msg": "请求未携带token, 无权限访问",
})
c.Abort()
return
}
// 按空格分割
authHeaderSplit := strings.SplitN(authHeader, " ", 2)
if !(len(authHeaderSplit) == 2 && authHeaderSplit[0] == config.AppEnvConfig.Jwt.TokenStartWith) {
c.JSON(http.StatusOK, gin.H{
"status": http.StatusUnauthorized,
"msg": "请求头中Token格式有误",
})
c.Abort()
return
}
// authHeaderSplit[1]是获取到的tokenString, 我们使用之前定义好的解析JWT的函数来解析它
currentTokenStr := authHeaderSplit[1]
claims, err := ParseToken(currentTokenStr)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": http.StatusUnauthorized,
"msg": err.Error(),
})
c.Abort()
return
}
singleTag := config.AppEnvConfig.Login.Single
if singleTag {
token, err := dao.RedisDB.GET(claims.UserInfo.UserName)
if err == nil {
if !(token == currentTokenStr) {
c.JSON(http.StatusOK, gin.H{
"status": http.StatusUnauthorized,
"msg": "您的账号已在其他终端登录, 请重新登录",
})
c.Abort()
return
}
}
}
// 继续交由下一个路由处理,并将解析出的信息传递下去
c.Set("claims", claims)
}
}
// 一些常量
var (
TokenExpired error = errors.New("授权已过期")
TokenNotValidYet error = errors.New("Token not active yet")
TokenMalformed error = errors.New("令牌非法")
TokenInvalid error = errors.New("Couldn't handle this token:")
)
// CuteClaims 自定义声明结构体并内嵌jwt.StandardClaims
// jwt包自带的jwt.StandardClaims只包含了官方字段
// 我们这里需要额外记录一些字段,所以要自定义结构体
// 如果想要保存更多信息,都可以添加到这个结构体中
type CuteClaims struct {
UserInfo response.UserResponse `json:"userInfo"`
jwt.StandardClaims
}
// CreateToken 生成一个token
func CreateToken(claims CuteClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 定义Secret
var tokenSecret = []byte(config.AppEnvConfig.Jwt.TokenSecret)
return token.SignedString(tokenSecret)
}
// CreateUserToken 生成含有用户信息的token
func CreateUserToken(u *response.UserResponse) (string, error) {
if config.AppEnvConfig.Jwt.TokenExpired == 0 {
config.AppEnvConfig.Jwt.TokenExpired = 1
}
// 定义JWT的过期时间
tokenExpired := time.Hour * time.Duration(config.AppEnvConfig.Jwt.TokenExpired)
// 定义Secret
var tokenSecret = []byte(config.AppEnvConfig.Jwt.TokenSecret)
// 创建我们自己的声明
c := CuteClaims{
UserInfo: *u, // 自定义字段
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(tokenExpired).Unix(), // 过期时间
Issuer: "tianjun@odboy.cn", // 签发人
},
}
// 使用指定的签名方法创建签名对象
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 使用指定的secret签名并获得完整的编码后的字符串token
return token.SignedString(tokenSecret)
}
// ParseToken 解析Token
func ParseToken(tokenString string) (*CuteClaims, error) {
// 定义Secret
var tokenSecret = []byte(config.AppEnvConfig.Jwt.TokenSecret)
token, err := jwt.ParseWithClaims(tokenString, &CuteClaims{}, func(token *jwt.Token) (interface{}, error) {
return tokenSecret, 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 过期(授权已过期)
return nil, TokenExpired
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, TokenNotValidYet
} else {
return nil, TokenInvalid
}
}
}
if claims, ok := token.Claims.(*CuteClaims); ok && token.Valid {
if config.AppEnvConfig.Login.Single {
tokenData := cache.GetCache(claims.UserInfo.UserName)
if tokenData == "" {
return nil, TokenExpired
}
}
return claims, nil
}
return nil, TokenInvalid
}
// RefreshToken 更新token
func RefreshToken(tokenString string) (string, error) {
// 定义Secret
var tokenSecret = []byte(config.AppEnvConfig.Jwt.TokenSecret)
jwt.TimeFunc = func() time.Time {
return time.Unix(0, 0)
}
token, err := jwt.ParseWithClaims(tokenString, &CuteClaims{}, func(token *jwt.Token) (interface{}, error) {
return tokenSecret, nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(*CuteClaims); ok && token.Valid {
jwt.TimeFunc = time.Now
claims.StandardClaims.ExpiresAt = time.Now().Add(1 * time.Hour).Unix()
return CreateToken(*claims)
}
return "", TokenInvalid
}
+141
View File
@@ -0,0 +1,141 @@
package logger
import (
"cutego/pkg/common"
"cutego/pkg/config"
"fmt"
"github.com/druidcaesa/gotool"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"os"
"path"
"time"
)
// LoggerToFile 日志记录到文件
func LoggerToFile() gin.HandlerFunc {
dirPath := common.GetDirPath("log")
fileName := path.Join(dirPath, "application.log")
if !common.IsFileOrDirExist(dirPath) {
err := common.CreateAllDir(dirPath)
if err != nil {
common.ErrorLog(err)
}
}
if !gotool.FileUtils.Exists(fileName) {
create, err := os.Create(fileName)
if err != nil {
common.ErrorLog(err)
}
defer create.Close()
}
// 写入文件
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
if err != nil {
fmt.Println("err", err)
}
logger := logrus.New()
// 输出源
logger.Out = src
switch config.AppEnvConfig.Logger.Level {
case "debug":
logger.SetLevel(logrus.DebugLevel)
break
case "warn":
logger.SetLevel(logrus.WarnLevel)
break
case "info":
logger.SetLevel(logrus.InfoLevel)
break
case "error":
logger.SetLevel(logrus.ErrorLevel)
break
case "fatal":
logger.SetLevel(logrus.FatalLevel)
case "panic":
logger.SetLevel(logrus.PanicLevel)
break
case "trace":
logger.SetLevel(logrus.TraceLevel)
default:
logger.SetLevel(logrus.DebugLevel)
}
logger.SetLevel(logrus.DebugLevel)
// 设置 rotatelogs
if config.AppEnvConfig.Logger.MaxSaveAge == 0 {
config.AppEnvConfig.Logger.MaxSaveAge = 7
}
if config.AppEnvConfig.Logger.RotationTime == 0 {
config.AppEnvConfig.Logger.RotationTime = 1
}
logWriter, err := rotatelogs.New(
// 分割后的文件名称
fileName+".%Y%m%d.logs",
// 生成软链, 指向最新日志文件
rotatelogs.WithLinkName(fileName),
rotatelogs.WithMaxAge(time.Duration(config.AppEnvConfig.Logger.MaxSaveAge)*24*time.Hour),
rotatelogs.WithRotationTime(time.Duration(config.AppEnvConfig.Logger.RotationTime)*24*time.Hour),
)
writeMap := lfshook.WriterMap{
logrus.InfoLevel: logWriter,
logrus.FatalLevel: logWriter,
logrus.DebugLevel: logWriter,
logrus.WarnLevel: logWriter,
logrus.ErrorLevel: logWriter,
logrus.PanicLevel: logWriter,
}
lfHook := lfshook.NewHook(writeMap, &logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
// 新增 Hook
logger.AddHook(lfHook)
return func(c *gin.Context) {
// 开始时间
startTime := time.Now()
// 处理请求
c.Next()
// 结束时间
endTime := time.Now()
// 执行时间
latencyTime := endTime.Sub(startTime)
// 请求方式
reqMethod := c.Request.Method
// 请求路由
reqUri := c.Request.RequestURI
// 状态码
statusCode := c.Writer.Status()
// 请求IP
clientIP := c.ClientIP()
// 日志格式
logger.WithFields(logrus.Fields{
"status_code": statusCode,
"latency_time": latencyTime,
"client_ip": clientIP,
"req_method": reqMethod,
"req_uri": reqUri,
}).Info()
}
}
// 日志记录到 MongoDB
func LoggerToMongo() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 ES
func LoggerToES() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
// 日志记录到 MQ
func LoggerToMQ() gin.HandlerFunc {
return func(c *gin.Context) {
}
}
+36
View File
@@ -0,0 +1,36 @@
package middleware
import (
"cutego/pkg/common"
"github.com/gin-gonic/gin"
"net/http"
"runtime/debug"
)
func Recover(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
common.ErrorLog("panic: %v\n", r)
debug.PrintStack()
c.JSON(http.StatusOK, gin.H{
"code": http.StatusInternalServerError,
"msg": errorToString(r),
"data": nil,
})
// 终止后续接口调用, 不加的话recover捕捉到异常后, 还会继续执行接口后续的代码
c.Abort()
}
}()
// 加载完 defer recover, 继续后续接口调用
c.Next()
}
// recover错误, 转string
func errorToString(r interface{}) string {
switch v := r.(type) {
case error:
return v.Error()
default:
return r.(string)
}
}
+41
View File
@@ -0,0 +1,41 @@
package page
import (
"cutego/pkg/common"
"github.com/go-xorm/xorm"
)
// Page 分页结构体
type Page struct {
Size int `json:"size"` // 显示条数
Total int64 `json:"total"` // 总条数
List interface{} `json:"list"` // 数据
}
type Start struct {
}
// StartSize 获取分页偏移量
func StartSize(pageNum int, size int) int {
if pageNum == 0 {
pageNum = 1
}
if size == 0 {
size = 10
}
num := (pageNum - 1) * size
return num
}
// GetTotal 获取总条数
func GetTotal(engine *xorm.Session, args ...interface{}) (int64, error) {
if args != nil {
engine.Table(args)
}
count, err := engine.Count()
if err != nil {
common.ErrorLog(err.Error())
return 0, err
}
return count, nil
}
+220
View File
@@ -0,0 +1,220 @@
package redispool
import (
"cutego/pkg/common"
"cutego/pkg/config"
"fmt"
"github.com/druidcaesa/gotool"
"github.com/gomodule/redigo/redis"
"time"
)
// https://godoc.org/github.com/gomodule/redigo/redis#pkg-examples
// https://github.com/gomodule/redigo
// RedisClient redis client instance
type RedisClient struct {
pool *redis.Pool
// 数据接收
chanRx chan common.RedisDataArray
// 是否退出
isExit bool
}
// NewRedis new redis client
func NewRedis() *RedisClient {
return &RedisClient{
pool: newPool(),
chanRx: make(chan common.RedisDataArray, 100),
}
}
// newPool 线程池
func newPool() *redis.Pool {
if config.AppEnvConfig.Redis.Pool.MaxIdle == 0 {
config.AppEnvConfig.Redis.Pool.MaxIdle = 3
}
return &redis.Pool{
MaxIdle: config.AppEnvConfig.Redis.Pool.MaxIdle,
IdleTimeout: time.Duration(config.AppEnvConfig.Redis.Pool.MaxWait) * time.Second,
MaxActive: config.AppEnvConfig.Redis.Pool.MaxActive,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", config.AppEnvConfig.Redis.Host, config.AppEnvConfig.Redis.Port))
if err != nil {
common.FatalfLog("Redis.Dial: %v", err)
return nil, err
}
if gotool.StrUtils.HasNotEmpty(config.AppEnvConfig.Redis.Password) {
if _, err := c.Do("AUTH", config.AppEnvConfig.Redis.Password); err != nil {
c.Close()
common.FatalfLog("Redis.AUTH: %v", err)
return nil, err
}
}
if _, err := c.Do("SELECT", config.AppEnvConfig.Redis.Database); err != nil {
c.Close()
common.FatalfLog("Redis.SELECT: %v", err)
return nil, err
}
return c, nil
},
}
}
// Start 启动接收任务协程
func (r *RedisClient) Start() {
r.isExit = false
// 开启协程用于循环接收数据
go r.loopRead()
}
// Stop 停止接收任务
func (r *RedisClient) Stop() {
r.isExit = true
// 关闭数据接收通道
close(r.chanRx)
// 关闭redis线程池
r.pool.Close()
}
// Write 向redis中写入多组数据
func (r *RedisClient) Write(data common.RedisDataArray) {
r.chanRx <- data
}
// loopRead 循环接收数据
func (r *RedisClient) loopRead() {
for !r.isExit {
select {
case rx := <-r.chanRx:
for _, it := range rx {
if len(it.Key) > 0 {
if len(it.Field) > 0 {
if _, err := r.HSET(it.Key, it.Field, it.Value); err != nil {
common.DebugLogf("[%s, %s, %s]: %s\n", it.Key, it.Field, it.Value, err.Error())
}
} else {
if _, err := r.SET(it.Key, it.Value); err != nil {
common.DebugLogf("[%s, %s, %s]: %s\n", it.Key, it.Field, it.Value, err.Error())
}
}
if it.Expire > 0 {
r.EXPIRE(it.Key, it.Expire)
}
}
}
}
}
}
// Error get redis connect error
func (r *RedisClient) Error() error {
conn := r.pool.Get()
defer conn.Close()
return conn.Err()
}
// 常用Redis操作命令的封装
// http://redis.io/commands
// KEYS get patten key array
func (r *RedisClient) KEYS(patten string) ([]string, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Strings(conn.Do("KEYS", patten))
}
// SCAN 获取大量key
func (r *RedisClient) SCAN(patten string) ([]string, error) {
conn := r.pool.Get()
defer conn.Close()
var out []string
var cursor uint64 = 0xffffff
isFirst := true
for cursor != 0 {
if isFirst {
cursor = 0
isFirst = false
}
arr, err := conn.Do("SCAN", cursor, "MATCH", patten, "COUNT", 100)
if err != nil {
return out, err
}
switch arr := arr.(type) {
case []interface{}:
cursor, _ = redis.Uint64(arr[0], nil)
it, _ := redis.Strings(arr[1], nil)
out = append(out, it...)
}
}
out = gotool.StrArrayUtils.ArrayDuplication(out)
return out, nil
}
// DEL delete k-v
func (r *RedisClient) DEL(key string) (int, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Int(conn.Do("DEL", key))
}
// DELALL delete key array
func (r *RedisClient) DELALL(key []string) (int, error) {
conn := r.pool.Get()
defer conn.Close()
arr := make([]interface{}, len(key))
for i, v := range key {
arr[i] = v
}
return redis.Int(conn.Do("DEL", arr...))
}
// GET get k-v
func (r *RedisClient) GET(key string) (string, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.String(conn.Do("GET", key))
}
// SET set k-v
func (r *RedisClient) SET(key string, value string) (int64, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Int64(conn.Do("SET", key, value))
}
// SETEX set k-v expire seconds
func (r *RedisClient) SETEX(key string, sec int, value string) (int64, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Int64(conn.Do("SETEX", key, sec, value))
}
// EXPIRE set key expire seconds
func (r *RedisClient) EXPIRE(key string, sec int64) (int64, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Int64(conn.Do("EXPIRE", key, sec))
}
// HGETALL get map of key
func (r *RedisClient) HGETALL(key string) (map[string]string, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.StringMap(conn.Do("HGETALL", key))
}
// HGET get value of key-field
func (r *RedisClient) HGET(key string, field string) (string, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.String(conn.Do("HGET", key, field))
}
// HSET set value of key-field
func (r *RedisClient) HSET(key string, field string, value string) (int64, error) {
conn := r.pool.Get()
defer conn.Close()
return redis.Int64(conn.Do("HSET", key, field, value))
}
+102
View File
@@ -0,0 +1,102 @@
package resp
import (
"github.com/gin-gonic/gin"
"net/http"
)
// Response 数据返回结构体
type Response struct {
Status int `json:"status"` // 返回状态值
Msg string `json:"msg"` // 返回的提示语
Data interface{} `json:"data"` // 返回数据
}
// Success 正确返回
func Success(data interface{}, msg ...string) *Response {
response := Response{
Status: http.StatusOK,
Data: data,
Msg: "操作成功",
}
if len(msg) > 0 {
response.Msg = msg[0]
}
return &response
}
// ErrorResp 错误返回
func ErrorResp(data ...interface{}) *Response {
response := Response{
Status: http.StatusInternalServerError,
Msg: "操作失败",
Data: nil,
}
for _, value := range data {
switch value.(type) {
case string:
response.Msg = value.(string)
case int:
response.Status = value.(int)
case interface{}:
response.Data = value.(interface{})
}
}
return &response
}
func Error(c *gin.Context, data ...interface{}) {
response := Response{
Status: http.StatusInternalServerError,
Msg: "操作失败",
Data: nil,
}
for _, value := range data {
switch value.(type) {
case string:
response.Msg = value.(string)
case int:
response.Status = value.(int)
case interface{}:
response.Data = value.(interface{})
}
}
c.JSON(http.StatusOK, response)
return
}
func ParamError(c *gin.Context, data ...interface{}) {
response := Response{
Status: http.StatusInternalServerError,
Msg: "参数绑定异常",
Data: nil,
}
for _, value := range data {
switch value.(type) {
case string:
response.Msg = value.(string)
case int:
response.Status = value.(int)
case interface{}:
response.Data = value.(interface{})
}
}
c.JSON(http.StatusBadRequest, response)
return
}
func OK(c *gin.Context, data ...interface{}) {
response := Response{
Status: http.StatusOK,
Msg: "操作成功",
Data: nil,
}
for _, datum := range data {
switch datum.(type) {
case string:
response.Msg = datum.(string)
case interface{}:
response.Data = datum.(interface{})
}
}
c.JSON(http.StatusOK, response)
return
}
+219
View File
@@ -0,0 +1,219 @@
package tree_dept
import (
"cutego/core/entity"
"sort"
)
// Tree 统一定义菜单树的数据结构, 也可以自定义添加其他字段
type Tree struct {
Id int `json:"id"`
Data interface{} `json:"-"` // 自定义对象
Label string `json:"label"`
Leaf bool `json:"-"` // 叶子节点
Selected bool `json:"-"` // 选中
PartialSelected bool `json:"-"` // 部分选中
Children []Tree `json:"children"` // 子节点
}
// INode 其他的结构体想要生成菜单树, 直接实现这个接口
type INode interface {
GetId() int
GetLabel() string
GetParentId() int
GetData() interface{}
// IsRoot 判断当前节点是否是顶层根节点
IsRoot() bool
}
type INodes []INode
func (nodes INodes) Len() int {
return len(nodes)
}
func (nodes INodes) Swap(i, j int) {
nodes[i], nodes[j] = nodes[j], nodes[i]
}
func (nodes INodes) Less(i, j int) bool {
return nodes[i].GetId() < nodes[j].GetId()
}
// GenerateTree 自定义的结构体实现 INode 接口后调用此方法生成树结构
// nodes 需要生成树的节点
// selectedNode 生成树后选中的节点
// menuTrees 生成成功后的树结构对象
func GenerateTree(nodes, selectedNodes []INode) (trees []Tree) {
trees = []Tree{}
// 定义顶层根和子节点
var roots, childList []INode
for _, v := range nodes {
if v.IsRoot() {
// 判断顶层根节点
roots = append(roots, v)
}
childList = append(childList, v)
}
for _, v := range roots {
childTree := &Tree{
Id: v.GetId(),
Label: v.GetLabel(),
Data: v.GetData(),
}
// 递归之前, 根据父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
// 递归
recursiveTree(childTree, childList, selectedNodes)
// 递归之后, 根据子节点确认 childTree 的选中状态
if !childTree.Selected {
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后, 根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后, 根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
trees = append(trees, *childTree)
}
return
}
// recursiveTree 递归生成树结构
// tree 递归的树对象
// nodes 递归的节点
// selectedNodes 选中的节点
func recursiveTree(tree *Tree, nodes, selectedNodes []INode) {
data := tree.Data.(INode)
for _, v := range nodes {
if v.IsRoot() {
// 如果当前节点是顶层根节点就跳过
continue
}
if data.GetId() == v.GetParentId() {
childTree := &Tree{
Id: v.GetId(),
Label: v.GetLabel(),
Data: v.GetData(),
}
// 递归之前, 根据子节点和父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) || tree.Selected
recursiveTree(childTree, nodes, selectedNodes)
if !childTree.Selected {
// 递归之后, 根据子节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后, 根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后, 根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
tree.Children = append(tree.Children, *childTree)
}
}
}
// FindRelationNode 在 allTree 中查询 nodes 中节点的所有父节点
// nodes 要查询父节点的子节点数组
// allTree 所有节点数组
func FindRelationNode(nodes, allNodes []INode) (respNodes []INode) {
nodeMap := make(map[int]INode)
for _, v := range nodes {
recursiveFindRelationNode(nodeMap, allNodes, v, 0)
}
for _, v := range nodeMap {
respNodes = append(respNodes, v)
}
sort.Sort(INodes(respNodes))
return
}
// recursiveFindRelationNode 递归查询关联父子节点
// nodeMap 查询结果搜集到map中
// allNodes 所有节点
// node 递归节点
// t 递归查找类型:0 查找父、子节点;1 只查找父节点;2 只查找子节点
func recursiveFindRelationNode(nodeMap map[int]INode, allNodes []INode, node INode, t int) {
nodeMap[node.GetId()] = node
for _, v := range allNodes {
if _, ok := nodeMap[v.GetId()]; ok {
continue
}
// 查找父节点
if t == 0 || t == 1 {
if node.GetParentId() == v.GetId() {
nodeMap[v.GetId()] = v
if v.IsRoot() {
// 是顶层根节点时, 不再进行递归
continue
}
recursiveFindRelationNode(nodeMap, allNodes, v, 1)
}
}
// 查找子节点
if t == 0 || t == 2 {
if node.GetId() == v.GetParentId() {
nodeMap[v.GetId()] = v
recursiveFindRelationNode(nodeMap, allNodes, v, 2)
}
}
}
}
// nodeSelected 判断节点的选中状态
// node 进行判断节点
func nodeSelected(node INode, selectedNodes []INode, children []Tree) bool {
for _, v := range selectedNodes {
if node.GetId() == v.GetId() {
// 1. 如果选择节点数组中存在当前节点
return true
}
}
if len(children) == 0 {
// 2. 不满足前置条件1, 且没有子节点
return false
}
selectedNum := 0
for _, v := range children {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(children) {
// 不满足前置条件1, 2 , 且子节点全部是选中状态
return true
}
return false
}
// nodePartialSelected 判断节点的半选中状态
func nodePartialSelected(trees []Tree) bool {
selectedNum := 0
for _, v := range trees {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(trees) || selectedNum == 0 {
// 子节点全选中, 或一个也没有选中
return false
}
return true
}
type DeptList []entity.SysDept
// ConvertToINodeArray 将当前数组转换成父类 INode 接口 数组
func (s DeptList) ConvertToINodeArray(*[]entity.SysDept) (nodes []INode) {
for _, v := range s {
nodes = append(nodes, v)
}
return
}
// GetTree 获取树结构
func (s DeptList) GetTree(treeSelect *[]entity.SysDept) []Tree {
s = *treeSelect
array := s.ConvertToINodeArray(treeSelect)
return GenerateTree(array, nil)
}
+283
View File
@@ -0,0 +1,283 @@
package tree_menu
import (
"cutego/core/entity"
"github.com/druidcaesa/gotool"
"sort"
)
// Tree 统一定义菜单树的数据结构, 也可以自定义添加其他字段
type Tree struct {
Name string `json:"name,omitempty"` // 节点名字
Path string `json:"path,omitempty"`
Hidden bool `json:"hidden"`
Redirect string `json:"redirect,omitempty"`
Component string `json:"component,omitempty"`
AlwaysShow bool `json:"alwaysShow"`
Data interface{} `json:"-"` // 自定义对象
Meta interface{} `json:"meta"`
Leaf bool `json:"-"` // 叶子节点
Selected bool `json:"-"` // 选中
PartialSelected bool `json:"-"` // 部分选中
Children []Tree `json:"children"` // 子节点
Id int `json:"id"`
Label string `json:"label"`
}
// INode 其他的结构体想要生成菜单树, 直接实现这个接口
type INode interface {
// GetName 获取显示名字
GetName() string
// GetMenuId 获取id
GetMenuId() int
// GetParentId 获取父id
GetParentId() int
// GetData 获取附加数据
GetData() interface{}
// IsRoot 判断当前节点是否是顶层根节点
IsRoot() bool
// GetPath 路径
GetPath() string
GetId() int
GetLabel() string
}
type INodes []INode
func (nodes INodes) Len() int {
return len(nodes)
}
func (nodes INodes) Swap(i, j int) {
nodes[i], nodes[j] = nodes[j], nodes[i]
}
func (nodes INodes) Less(i, j int) bool {
return nodes[i].GetMenuId() < nodes[j].GetMenuId()
}
// GenerateTree 自定义的结构体实现 INode 接口后调用此方法生成树结构
// nodes 需要生成树的节点
// selectedNode 生成树后选中的节点
// menuTrees 生成成功后的树结构对象
func GenerateTree(nodes, selectedNodes []INode) (trees []Tree) {
trees = []Tree{}
// 定义顶层根和子节点
var roots, childs []INode
for _, v := range nodes {
if v.IsRoot() {
// 判断顶层根节点
roots = append(roots, v)
}
childs = append(childs, v)
}
for _, v := range roots {
var flag = false
var component = "Layout"
m := make(map[string]interface{})
if v.GetData() != nil {
menu := v.GetData().(entity.SysMenu)
m["title"] = menu.MenuName
m["icon"] = menu.Icon
m["noCache"] = menu.IsCache == 1
if menu.IsFrame == 0 {
m["link"] = menu.Path
} else {
m["link"] = nil
}
flag = menu.Visible == "1"
if !gotool.StrUtils.HasEmpty(menu.Component) {
component = menu.Component
}
}
childTree := &Tree{
Name: v.GetName(),
Data: v.GetData(),
Path: "/" + v.GetPath(),
Hidden: flag,
AlwaysShow: true,
Redirect: "noRedirect",
Meta: m,
Component: component,
Id: v.GetId(),
Label: v.GetLabel(),
}
// 递归之前, 根据父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
// 递归
recursiveTree(childTree, childs, selectedNodes)
// 递归之后, 根据子节点确认 childTree 的选中状态
if !childTree.Selected {
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后, 根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后, 根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
trees = append(trees, *childTree)
}
return
}
// recursiveTree 递归生成树结构
// tree 递归的树对象
// nodes 递归的节点
// selectedNodes 选中的节点
func recursiveTree(tree *Tree, nodes, selectedNodes []INode) {
data := tree.Data.(INode)
for _, v := range nodes {
if v.IsRoot() {
// 如果当前节点是顶层根节点就跳过
continue
}
var flag = false
var component = "Layout"
m := make(map[string]interface{})
if v.GetData() != nil {
menu := v.GetData().(entity.SysMenu)
flag = menu.Visible == "1"
m["title"] = menu.MenuName
m["icon"] = menu.Icon
m["noCache"] = menu.IsCache == 1
if menu.IsFrame == 0 {
m["link"] = menu.Path
} else {
m["link"] = nil
}
if !gotool.StrUtils.HasEmpty(menu.Component) {
component = menu.Component
}
}
if data.GetMenuId() == v.GetParentId() {
childTree := &Tree{
Name: v.GetName(),
Data: v.GetData(),
Path: v.GetPath(),
Hidden: flag,
Meta: m,
Component: component,
Id: v.GetId(),
Label: v.GetLabel(),
}
// 递归之前, 根据子节点和父节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children) || tree.Selected
recursiveTree(childTree, nodes, selectedNodes)
if !childTree.Selected {
// 递归之后, 根据子节点确认 childTree 的选中状态
childTree.Selected = nodeSelected(v, selectedNodes, childTree.Children)
}
// 递归之后, 根据子节点确认 childTree 的半选中状态
childTree.PartialSelected = nodePartialSelected(childTree.Children)
// 递归之后, 根据子节确认是否是叶子节点
childTree.Leaf = len(childTree.Children) == 0
tree.Children = append(tree.Children, *childTree)
}
}
}
// FindRelationNode 在 allTree 中查询 nodes 中节点的所有父节点
// nodes 要查询父节点的子节点数组
// allTree 所有节点数组
func FindRelationNode(nodes, allNodes []INode) (respNodes []INode) {
nodeMap := make(map[int]INode)
for _, v := range nodes {
recursiveFindRelationNode(nodeMap, allNodes, v, 0)
}
for _, v := range nodeMap {
respNodes = append(respNodes, v)
}
sort.Sort(INodes(respNodes))
return
}
// recursiveFindRelationNode 递归查询关联父子节点
// nodeMap 查询结果搜集到map中
// allNodes 所有节点
// node 递归节点
// t 递归查找类型:0 查找父、子节点;1 只查找父节点;2 只查找子节点
func recursiveFindRelationNode(nodeMap map[int]INode, allNodes []INode, node INode, t int) {
nodeMap[node.GetMenuId()] = node
for _, v := range allNodes {
if _, ok := nodeMap[v.GetMenuId()]; ok {
continue
}
// 查找父节点
if t == 0 || t == 1 {
if node.GetParentId() == v.GetMenuId() {
nodeMap[v.GetMenuId()] = v
if v.IsRoot() {
// 是顶层根节点时, 不再进行递归
continue
}
recursiveFindRelationNode(nodeMap, allNodes, v, 1)
}
}
// 查找子节点
if t == 0 || t == 2 {
if node.GetMenuId() == v.GetParentId() {
nodeMap[v.GetMenuId()] = v
recursiveFindRelationNode(nodeMap, allNodes, v, 2)
}
}
}
}
// nodeSelected 判断节点的选中状态
// node 进行判断节点
func nodeSelected(node INode, selectedNodes []INode, children []Tree) bool {
for _, v := range selectedNodes {
if node.GetMenuId() == v.GetMenuId() {
// 1. 如果选择节点数组中存在当前节点
return true
}
}
if len(children) == 0 {
// 2. 不满足前置条件1, 且没有子节点
return false
}
selectedNum := 0
for _, v := range children {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(children) {
// 不满足前置条件1, 2 , 且子节点全部是选中状态
return true
}
return false
}
// nodePartialSelected 判断节点的半选中状态
func nodePartialSelected(trees []Tree) bool {
selectedNum := 0
for _, v := range trees {
if v.Selected {
selectedNum++
}
}
if selectedNum == len(trees) || selectedNum == 0 {
// 子节点全选中, 或一个也没有选中
return false
}
return true
}
type SystemMenus []entity.SysMenu
// ConvertToINodeArray 将当前数组转换成父类 INode 接口 数组
func (s SystemMenus) ConvertToINodeArray(*[]entity.SysMenu) (nodes []INode) {
for _, v := range s {
nodes = append(nodes, v)
}
return
}
// GetTree 获取树结构数据
func (s SystemMenus) GetTree(m *[]entity.SysMenu) []Tree {
s = *m
array := s.ConvertToINodeArray(m)
return GenerateTree(array, nil)
}
+58
View File
@@ -0,0 +1,58 @@
package util
import (
"cutego/core/api/v1/response"
"cutego/core/dao"
"cutego/core/entity"
"cutego/pkg/common"
"cutego/pkg/config"
"cutego/pkg/jwt"
"github.com/gin-gonic/gin"
"strings"
)
type UserUtils struct {
}
// GetUserInfo 通过jwt获取当前登录用户
func GetUserInfo(c *gin.Context) *response.UserResponse {
token := c.Request.Header.Get("Authorization")
s := strings.Split(token, " ")
// parseToken 解析token包含的信息
claims, err := jwt.ParseToken(s[1])
if err != nil {
common.ErrorLog(err)
}
info := claims.UserInfo
return &info
}
// CheckLockToken 校验多终端登录锁
func CheckLockToken(c *gin.Context) bool {
if config.AppEnvConfig.Login.Single {
// 获取redis中的token数据
info := GetUserInfo(c)
get, err := dao.RedisDB.GET(info.UserName)
if err != nil {
common.ErrorLog(err)
return false
}
token := c.Request.Header.Get(config.AppEnvConfig.Jwt.Header)
s := strings.Split(token, " ")
if get == s[1] {
return true
} else {
return false
}
}
return true
}
// CheckIsAdmin 判断是否是超级管理员
func CheckIsAdmin(user *entity.SysUser) bool {
if user.UserId == 1 {
return true
} else {
return false
}
}
+106
View File
@@ -0,0 +1,106 @@
package websocket
import (
"cutego/pkg/common"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
)
// 连接例子
// <script>
// var ws = new WebSocket("ws://127.0.0.1:21366/websocket?user=admin&code=notice");
// // 连接打开时触发
// ws.onopen = function(evt) {
// console.log("Connection open ...");
// ws.send("Hello WebSockets!");
// };
// // 接收到消息时触发
// ws.onmessage = function(evt) {
// console.log("Received Message: " + evt.data);
// };
// // 连接关闭时触发
// ws.onclose = function(evt) {
// console.log("Connection closed.");
// };
// </script>
// 定义回调接口(消息类型、内容)
type OnReceiveMessage func(messageType int, content []byte) error
// 分割符号
const SignalSplitSymbol = "=_="
// 用户名 <--> websocket通道
var OnlineUserMap = make(map[string]*websocket.Conn)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// webSocket请求ping 返回pong
func HandleWebSocketMessage(c *gin.Context) {
userValue, userExist := c.GetQuery("user")
codeValue, codeExist := c.GetQuery("code")
if !userExist && !codeExist {
return
}
// 升级get请求为webSocket协议
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
// defer ws.Close()
cacheKey := userValue + SignalSplitSymbol + codeValue
// 如果存在则踢掉之前的通道
if OnlineUserMap[cacheKey] != nil {
// 函数结束后关闭
tmpConn := OnlineUserMap[cacheKey]
defer tmpConn.Close()
}
OnlineUserMap[cacheKey] = ws
// 回收监听消息
go ListenWebSocketMessage(userValue, codeValue, HandleAdminNotice)
}
// 回调函数的具体实现
func HandleAdminNotice(messageType int, content []byte) error {
common.InfoLog("messageType=%d\n", messageType)
common.InfoLog("content=%s\n", string(content))
return nil
}
// 监听到消息
// onReceiveMessage为函数提供的回调接口, 让外部去实现
func ListenWebSocketMessage(user string, code string, onReceiveMessage OnReceiveMessage) {
cacheKey := user + SignalSplitSymbol + code
ws := OnlineUserMap[cacheKey]
if ws != nil {
for {
// 读取ws中的数据
mt, message, err := ws.ReadMessage()
if err != nil {
break
}
// 处理心跳
if string(message) == "ping" {
message = []byte("pong")
// 写入ws数据
err = ws.WriteMessage(mt, message)
if err != nil {
break
}
continue
}
// 处理接收到的数据
err = onReceiveMessage(mt, message)
if err != nil {
break
}
}
}
}