Roc 8 months ago
commit
5a60a16070
59 changed files with 6454 additions and 0 deletions
  1. 13 0
      .gitignore
  2. 87 0
      config/config.go
  3. 14 0
      controller/auth.go
  4. 325 0
      controller/index/index.go
  5. 63 0
      controller/index/wechat.go
  6. 154 0
      controller/resp/base.go
  7. 38 0
      core/config.go
  8. 136 0
      core/log.go
  9. 38 0
      core/run_server.go
  10. 89 0
      docs/docs.go
  11. 69 0
      global/global.go
  12. 45 0
      global/validator.go
  13. 81 0
      go.mod
  14. 73 0
      init_serve/mysql.go
  15. 25 0
      init_serve/redis.go
  16. 40 0
      init_serve/router.go
  17. 87 0
      init_serve/task.go
  18. 32 0
      init_serve/ws.go
  19. 0 0
      logic/logic.txt
  20. 102 0
      logic/wechat_group.go
  21. 10 0
      main.go
  22. 28 0
      middleware/check_base_auth.go
  23. 27 0
      middleware/common.go
  24. 27 0
      middleware/cors.go
  25. 56 0
      middleware/recover.go
  26. 33 0
      middleware/token.go
  27. 80 0
      middleware/token_no_login.go
  28. 171 0
      models/base/base.go
  29. 165 0
      models/base/page.go
  30. 13 0
      models/base/time_base.go
  31. 99 0
      models/edb_info.go
  32. 28 0
      models/eta_business.go
  33. 41 0
      models/eta_business_menu.go
  34. 45 0
      models/report_send_ths_detail.go
  35. 6 0
      models/request/wechat.go
  36. 74 0
      models/wechat_group.go
  37. 38 0
      models/wechat_group_helper_relation.go
  38. 34 0
      models/wechat_group_tag.go
  39. 58 0
      models/wechat_helper.go
  40. 41 0
      models/wechat_listen_msg.go
  41. 111 0
      models/wechat_msg_push_record.go
  42. 13 0
      routers/auth.go
  43. 13 0
      routers/index.go
  44. 15 0
      routers/wechat.go
  45. 13 0
      routers/ws.go
  46. 35 0
      services/alarm_msg/alarm_msg.go
  47. 24 0
      services/api_tool.go
  48. 315 0
      services/send_to_wx.go
  49. 1043 0
      task/add_push_record.go
  50. 196 0
      task/send_wechat_msg.go
  51. 72 0
      task/task.go
  52. 1070 0
      utils/common.go
  53. 41 0
      utils/constants.go
  54. 191 0
      utils/des3.go
  55. 48 0
      utils/directory.go
  56. 357 0
      utils/drawtext.go
  57. 10 0
      utils/index_files.go
  58. 214 0
      utils/validator.go
  59. 88 0
      utils/whereBuild.go

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+/.idea/
+/log
+latest_log
+/config/*.yaml
+.DS_Store
+go.sum
+/binlog
+latest_binlog
+/rdlucklog
+/*.exe
+eta_menu_sync.exe
+eta_menu_sync
+static/

+ 87 - 0
config/config.go

@@ -0,0 +1,87 @@
+package config
+
+import "time"
+
+type Config struct {
+	Log      Log      `mapstructure:"log" json:"log" yaml:"log"`
+	Serve    Serve    `mapstructure:"serve" json:"serve" yaml:"serve"`
+	Mysql    Mysql    `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
+	Redis    Redis    `mapstructure:"redis" json:"redis" yaml:"redis"`
+	AliOss   AliOss   `mapstructure:"ali-oss" json:"ali-oss" yaml:"ali-oss"`
+	EsClient EsClient `mapstructure:"es_client" json:"es_client" yaml:"es_client"`
+}
+
+// Serve gin服务配置
+type Serve struct {
+	Port       int    `mapstructure:"port" json:"port" yaml:"port" description:"gin开启监听http服务的端口"`
+	RunMode    string `mapstructure:"run-mode" json:"run-mode" yaml:"run-mode" description:"gin运行模式的默认,枚举值:debug,release"`
+	UseRedis   bool   `mapstructure:"use-redis" json:"use-redis" yaml:"use-redis" description:"是否使用redis"`
+	AppName    string `mapstructure:"app-name" json:"app-name" yaml:"app-name" description:"项目名称"`
+	StaticDir  string `mapstructure:"static-dir" json:"static-dir" yaml:"static-dir" description:"上传的文件存储目录地址"`
+	ListenWxId string `mapstructure:"listen-wx-id" json:"listen-wx-id" yaml:"listen-wx-id" description:"监听的微信群id"`
+	Token      string `mapstructure:"token" json:"token" yaml:"token" description:"鉴权token"`
+}
+
+// Log 日志配置
+type Log struct {
+	Prefix         string `mapstructure:"prefix" json:"prefix" yaml:"prefix" description:"日志输出前缀"`
+	LogFile        bool   `mapstructure:"log-file" json:"logFile" yaml:"log-file" description:""`
+	Stdout         string `mapstructure:"stdout" json:"stdout" yaml:"stdout" description:""`
+	FileStdout     string `mapstructure:"file-stdout" json:"file-stdout" yaml:"file-stdout" description:""`
+	SaveMaxDay     int    `mapstructure:"save-max-day" json:"save-max-day" yaml:"save-max-day" description:"最多保留多少天的日志"`
+	CuttingDay     int    `mapstructure:"cutting-day" json:"cutting-day" yaml:"cutting-day" description:"相隔几天切割文件"`
+	LogDirPath     string `mapstructure:"log-dir-path" json:"log-dir-path" yaml:"log-dir-path" description:"日志目录"`
+	LogSoftLink    string `mapstructure:"log-soft-link" json:"log-soft-link" yaml:"log-soft-link" description:"日志软链接"`
+	BinlogDirPath  string `mapstructure:"binlog-dir-path" json:"binlog-dir-path" yaml:"binlog-dir-path" description:"binlog日志目录"`
+	BinlogSoftLink string `mapstructure:"binlog-soft-link" json:"binlog-soft-link" yaml:"binlog-soft-link" description:"binlog日志软链接"`
+}
+
+// Mysql 数据库配置
+type Mysql struct {
+	//LogMode             bool        `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode" description:"是否开启日志"`
+	Stdout              bool        `mapstructure:"stdout" json:"stdout" yaml:"stdout" description:"日志是否输出在控制台"`
+	DefaultDsnAliasName string      `mapstructure:"default-dsn-alias-name" json:"default-dsn-alias-name" yaml:"default-dsn-alias-name" description:"默认的数据库连接别名"`
+	List                []MysqlConn `mapstructure:"list" json:"list" yaml:"list" description:"数据库链接配置列表"`
+}
+
+// MysqlConn mysql数据链接配置
+type MysqlConn struct {
+	Dsn          string `mapstructure:"dsn" json:"dsc" yaml:"dsn" description:"数据库连接配置"`
+	AliasName    string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name" description:"数据库别名"`
+	MaxIdleConns int    `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns" description:"最大空闲连接"`
+	MaxOpenConns int    `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns" description:"最大连接数"`
+}
+
+// Redis redis配置
+type Redis struct {
+	Address  string `mapstructure:"address" json:"address" yaml:"address" description:"redis服务链接地址"`
+	Password string `mapstructure:"password" json:"password" yaml:"password" description:"redis服务密码"`
+	Db       int    `mapstructure:"db" json:"db" yaml:"db" description:"默认使用的redis库"`
+}
+
+// AliOss aliOss配置
+type AliOss struct {
+	BucketName      string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name"`
+	EndPoint        string `mapstructure:"end-point" json:"end-point" yaml:"end-point"`
+	ImgHost         string `mapstructure:"img-host" json:"img-host" yaml:"img-host" description:"阿里oss主机地址"`
+	UploadDir       string `mapstructure:"upload-dir" json:"upload-dir" yaml:"upload-dir" description:"图片上传的文件目录"`
+	UploadAudioDir  string `mapstructure:"upload-audio-dir" json:"upload-audio-dir" yaml:"upload-audio-dir" description:"视频上传的文件目录"`
+	AccessKeyId     string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id" description:"access-key-id"`
+	AccessKeySecret string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret" description:"access-key-secret"`
+}
+
+// EsClient es客户端配置
+type EsClient struct {
+	// 终端地址
+	Endpoints string `json:"endpoints" yaml:"endpoints"`
+	// 用户
+	Username string `json:"username" yaml:"username"`
+	// 密码
+	Password string `json:"password" yaml:"password"`
+	// 超时时间,单位ms
+	Timeout time.Duration `json:"timeout" yaml:"timeout"`
+	// es日志目录
+	Log string `json:"log" yaml:"log"`
+	// 索引前缀
+	Prefix string `json:"prefix" yaml:"prefix"`
+}

+ 14 - 0
controller/auth.go

@@ -0,0 +1,14 @@
+package controller
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/resp"
+)
+
+type AuthController struct {
+}
+
+func (a *AuthController) Login(c *gin.Context) {
+	resp.OkData("登录成功", nil, c)
+	return
+}

File diff suppressed because it is too large
+ 325 - 0
controller/index/index.go


+ 63 - 0
controller/index/wechat.go

@@ -0,0 +1,63 @@
+package index
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin/binding"
+	"eta/eta_menu_sync/controller/resp"
+	"eta/eta_menu_sync/logic"
+	"eta/eta_menu_sync/models"
+	"eta/eta_menu_sync/models/request"
+	"eta/eta_menu_sync/services"
+	"eta/eta_menu_sync/utils"
+)
+
+// WechatController 接口
+type WechatController struct {
+}
+
+// SyncHelper 同步小助手
+func (s *WechatController) SyncHelper(c *gin.Context) {
+	fmt.Println(c.Request.Body)
+
+	var req request.SyncHelperReq
+	if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
+		fmt.Println("bind err:", err)
+		resp.Fail("参数有误", c)
+		return
+	}
+	if req.WechatId == `` {
+		resp.Fail("微信小助手标识未提交", c)
+		return
+	}
+	wechatHelperObj := models.WechatHelper{}
+	wechatHelperInfo, err := wechatHelperObj.GetByWechatId(req.WechatId)
+	if err != nil {
+		if err == utils.ErrNoRow {
+			resp.Fail("找不到该小助手", c)
+		} else {
+			resp.FailMsg("获取小助手信息失败", "获取小助手信息失败"+err.Error(), c)
+		}
+		return
+	}
+	result, _, err := services.GetAllGroup(wechatHelperInfo.ServerUrl, wechatHelperInfo.WechatId)
+	if err != nil {
+		resp.FailMsg("获取小助手的群信息失败", "获取小助手的群信息失败:"+err.Error(), c)
+		return
+	}
+	if result.Code != 200 {
+		resp.Fail("获取小助手的群信息失败:"+result.Msg, c)
+		return
+	}
+
+	// 群同步
+	err = logic.SyncWxGroup(result.Result, wechatHelperInfo)
+	if err != nil {
+		resp.FailMsg("同步微信群失败", "同步微信群失败:"+err.Error(), c)
+		return
+	}
+
+	resp.OkData("同步成功", "ok", c)
+
+	return
+}

+ 154 - 0
controller/resp/base.go

@@ -0,0 +1,154 @@
+package resp
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/utils"
+	"strings"
+)
+
+var (
+	OK_CODE            = 200  //业务成功
+	FAIL_CODE          = 400  //业务错误
+	TOKEN_ERROR_CODE   = 401  //toke异常
+	NO_AUTH            = 403  //没有权限
+	SPECIFIC_FAIL_CODE = 4001 // 业务指定错误
+)
+
+type ResultData struct {
+	Code   int         `json:"code" description:"状态码"`
+	Msg    string      `json:"msg" description:"提示信息"`
+	Data   interface{} `json:"data" description:"返回数据"`
+	ErrMsg string      `json:"-" description:"错误信息,不用返回给前端,只是做日志记录"`
+}
+
+func result(code int, resultData ResultData, c *gin.Context) {
+	jsonByte, _ := json.Marshal(resultData)
+	token := c.Request.Header.Get("Authorization")
+	if token == "" {
+		token = c.DefaultQuery("authorization", "")
+		if token == "" {
+			token = c.DefaultQuery("Authorization", "")
+		}
+	}
+	logSlice := make([]string, 0)
+	logSlice = append(logSlice, fmt.Sprint("Url:", c.Request.RequestURI))
+	logSlice = append(logSlice, fmt.Sprint("Token:", token))
+	logSlice = append(logSlice, fmt.Sprint("resultData:", string(jsonByte)))
+
+	//记录错误日志
+	if resultData.ErrMsg != "" {
+		logSlice = append(logSlice, fmt.Sprint("ErrMsg:", resultData.ErrMsg))
+		//global.LOG.Info(strings.Join(logSlice, ";"))
+	}
+
+	// 测试环境不加密
+	if global.CONFIG.Serve.RunMode == "debug" {
+		c.JSON(code, resultData)
+	} else {
+		global.LOG.Info(strings.Join(logSlice, ";"))
+		encryptResult := utils.DesBase64Encrypt(jsonByte)
+		c.JSON(code, string(encryptResult))
+	}
+	c.Abort()
+}
+
+// OK 操作成功
+func Ok(msg string, c *gin.Context) {
+	resultData := ResultData{
+		Code: OK_CODE,
+		Msg:  msg,
+	}
+	result(200, resultData, c)
+}
+
+// OkData 成功返回数据
+func OkData(msg string, data interface{}, c *gin.Context) {
+	resultData := ResultData{
+		Code: OK_CODE,
+		Msg:  msg,
+		Data: data,
+	}
+	result(200, resultData, c)
+}
+
+// Fail 操作失败
+func Fail(msg string, c *gin.Context) {
+	resultData := ResultData{
+		Code: FAIL_CODE,
+		Msg:  msg,
+	}
+	result(200, resultData, c)
+}
+
+// FailData 成功返回数据
+func FailData(msg string, data interface{}, c *gin.Context) {
+	resultData := ResultData{
+		Code: FAIL_CODE,
+		Msg:  msg,
+		Data: data,
+	}
+	result(200, resultData, c)
+}
+
+// Custom 自定义状态码+操作成功
+func Custom(code int, msg string, c *gin.Context) {
+	resultData := ResultData{
+		Code: code,
+		Msg:  msg,
+	}
+	result(200, resultData, c)
+}
+
+// CustomData 自定义状态码+返回数据
+func CustomData(code int, msg string, data interface{}, c *gin.Context) {
+	resultData := ResultData{
+		Code: code,
+		Msg:  msg,
+		Data: data,
+	}
+	result(200, resultData, c)
+}
+
+// TokenError token异常
+func TokenError(data interface{}, message, errMsg string, c *gin.Context) {
+	resultData := ResultData{
+		Code:   TOKEN_ERROR_CODE,
+		Msg:    message,
+		Data:   data,
+		ErrMsg: errMsg,
+	}
+	result(200, resultData, c)
+}
+
+// AuthError 没有权限
+func AuthError(data interface{}, message string, c *gin.Context) {
+	resultData := ResultData{
+		Code: NO_AUTH,
+		Msg:  message,
+		Data: data,
+	}
+	result(200, resultData, c)
+}
+
+// SpecificFail 业务指定错误
+func SpecificFail(data interface{}, message string, c *gin.Context) {
+	resultData := ResultData{
+		Code: SPECIFIC_FAIL_CODE,
+		Msg:  message,
+		Data: data,
+	}
+	result(200, resultData, c)
+}
+
+// FailMsg 操作失败
+func FailMsg(msg, errMsg string, c *gin.Context) {
+	resultData := ResultData{
+		Code:   FAIL_CODE,
+		Msg:    msg,
+		ErrMsg: errMsg,
+	}
+	result(200, resultData, c)
+}

+ 38 - 0
core/config.go

@@ -0,0 +1,38 @@
+package core
+
+//
+//const ConfigFile = "config/config.yaml"                                //本地(测试)环境下的配置文件地址
+//const ProConfigFile = "/home/code/config/hongze_yb/config/config.yaml" //生产环境下的配置文件地址
+//
+//func init() {
+//	v := viper.New()
+//
+//	configFilePath := ConfigFile
+//
+//	//如果不存在该配置文件,那么应该是线上环境,那么去寻找线上配置文件的路径
+//	if !utils.FileIsExist(configFilePath) {
+//		configFilePath = ProConfigFile
+//	}
+//
+//	//设置配置文件
+//	v.SetConfigFile(configFilePath)
+//
+//	err := v.ReadInConfig()
+//	if err != nil {
+//		panic(fmt.Errorf("读取配置失败,Err: %s \n", err))
+//	}
+//
+//	//持续监听文件
+//	v.WatchConfig()
+//
+//	v.OnConfigChange(func(e fsnotify.Event) {
+//		fmt.Println("配置文件变更:", e.Name)
+//		if err := v.Unmarshal(&global.CONFIG); err != nil {
+//			fmt.Println("配置重赋值失败,Err:", err)
+//		}
+//		fmt.Println(global.CONFIG)
+//	})
+//	if err := v.Unmarshal(&global.CONFIG); err != nil {
+//		fmt.Println("配置初始化赋值失败,Err:", err)
+//	}
+//}

+ 136 - 0
core/log.go

@@ -0,0 +1,136 @@
+package core
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
+	oplogging "github.com/op/go-logging"
+	"eta/eta_menu_sync/config"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/utils"
+	"io"
+	"os"
+	"strings"
+	"time"
+)
+
+const (
+	Module = "hongze_wx_listen"
+)
+
+var (
+	defaultFormatter = ` %{time:2006/01/02 - 15:04:05.000} %{longfile} %{color:bold}▶ [%{level:.6s}] %{message}%{color:reset}`
+)
+
+// 初始化日志
+func init() {
+	logConfig := global.CONFIG.Log
+	logger := oplogging.MustGetLogger(Module)
+	var backends []oplogging.Backend
+	//注册输出
+	registerStdout(logConfig, &backends)
+
+	//注册框架输出(日志文件)
+	fileWriter := registerFile(logConfig, &backends, logConfig.LogDirPath, logConfig.LogSoftLink)
+	if fileWriter != nil {
+		if global.CONFIG.Serve.RunMode == "debug" {
+			gin.DefaultWriter = io.MultiWriter(fileWriter, os.Stdout)
+		} else {
+			gin.DefaultWriter = io.MultiWriter(fileWriter)
+		}
+	}
+	oplogging.SetBackend(backends...)
+	global.LOG = logger
+
+	//初始化mysql数据库日志
+	initMysqlLog()
+}
+
+// initMysqlLog 初始化mysql数据库日志
+func initMysqlLog() {
+	logConfig := global.CONFIG.Log
+	var backends []oplogging.Backend
+	//注册输出
+	registerStdout(logConfig, &backends)
+
+	//注册框架输出(日志文件)
+	fileWriter := registerFile(logConfig, &backends, logConfig.BinlogDirPath, logConfig.BinlogSoftLink)
+	global.MYSQL_LOG = fileWriter
+}
+
+func registerStdout(c config.Log, backends *[]oplogging.Backend) {
+	if c.Stdout != "" {
+		level, err := oplogging.LogLevel(c.Stdout)
+		if err != nil {
+			fmt.Println(err)
+		}
+		*backends = append(*backends, createBackend(os.Stdout, c, level))
+	}
+}
+
+// registerFile 注册文件日志
+func registerFile(c config.Log, backends *[]oplogging.Backend, logDir, logSoftLink string) io.Writer {
+	if c.FileStdout != "" {
+		if ok, _ := utils.PathExists(logDir); !ok {
+			// directory not exist
+			fmt.Println("create log directory")
+			_ = os.Mkdir(logDir, os.ModePerm)
+		}
+
+		//日志保留时间
+		saveMaxTime := time.Duration(global.CONFIG.Log.SaveMaxDay) * 24 * time.Hour
+		//日志切割时间(每隔多久切割一次)
+		cuttingTime := time.Duration(global.CONFIG.Log.CuttingDay) * 24 * time.Hour
+
+		//使用rotatelogs
+		fileWriter, err := rotatelogs.New(
+			logDir+string(os.PathSeparator)+"%Y-%m-%d-%H-%M.log",
+			// 生成软链,指向最新日志文件
+			rotatelogs.WithLinkName(logSoftLink),
+			// 以下两个配置不能同时设置,一个是保留天数,一个是保留分拣份数
+			rotatelogs.WithMaxAge(saveMaxTime), //日志保留多久,最小分钟为单位
+			//rotatelogs.WithRotationCount(5),        //number 默认7份 大于7份 或到了清理时间 开始清理
+			// time period of log file switching
+			rotatelogs.WithRotationTime(cuttingTime), //rotate 最小为1分钟轮询。默认60s  低于1分钟就按1分钟来
+		)
+		if err != nil {
+			fmt.Println(err)
+		}
+		level, err := oplogging.LogLevel(c.FileStdout)
+		if err != nil {
+			fmt.Println(err)
+		}
+		*backends = append(*backends, createBackend(fileWriter, c, level))
+
+		return fileWriter
+	}
+	return nil
+}
+
+// createBackend 创建一个新的日志记录器
+func createBackend(w io.Writer, c config.Log, level oplogging.Level) oplogging.Backend {
+	backend := oplogging.NewLogBackend(w, c.Prefix, 0)
+	stdoutWriter := false
+	if w == os.Stdout {
+		stdoutWriter = true
+	}
+	format := getLogFormatter(c, stdoutWriter)
+	backendLeveled := oplogging.AddModuleLevel(oplogging.NewBackendFormatter(backend, format))
+	backendLeveled.SetLevel(level, Module)
+	return backendLeveled
+}
+
+func getLogFormatter(c config.Log, stdoutWriter bool) oplogging.Formatter {
+	pattern := defaultFormatter
+	if !stdoutWriter {
+		// Color is only required for console output
+		// Other writers don't need %{color} tag
+		pattern = strings.Replace(pattern, "%{color:bold}", "", -1)
+		pattern = strings.Replace(pattern, "%{color:reset}", "", -1)
+	}
+	if !c.LogFile {
+		// Remove %{logfile} tag
+		pattern = strings.Replace(pattern, "%{longfile}", "", -1)
+	}
+	return oplogging.MustStringFormatter(pattern)
+}

+ 38 - 0
core/run_server.go

@@ -0,0 +1,38 @@
+package core
+
+import (
+	"fmt"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/init_serve"
+)
+
+func RunServe() {
+	//初始化路由
+	r := init_serve.InitRouter()
+	//初始化mysql数据库
+	init_serve.Mysql()
+	// 初始化websocket
+	init_serve.InitWs()
+
+	//如果配置了redis,那么链接redis
+	fmt.Println("redis")
+	fmt.Println(global.CONFIG.Serve.UseRedis)
+	if global.CONFIG.Serve.UseRedis {
+		//初始化redis
+		init_serve.Redis()
+	}
+	//初始化验证器
+	//if err := global.InitTrans("zh"); err != nil {
+	//	fmt.Printf("init trans failed, err:%v\n", err)
+	//	return
+	//}
+	//启动任务
+	init_serve.InitTask()
+	// 3.监听端口,默认在8080
+	// Run("里面不指定端口号默认为8080")
+	fmt.Println("port:", global.CONFIG.Serve.Port)
+	err := r.Run(fmt.Sprint("0.0.0.0:", global.CONFIG.Serve.Port)) // 监听并在 0.0.0.0:8080 上启动服务
+	if err != nil {
+		panic(any(fmt.Sprintf("服务启动失败,Err:%s", err.Error())))
+	}
+}

+ 89 - 0
docs/docs.go

@@ -0,0 +1,89 @@
+// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
+// This file was generated by swaggo/swag
+package docs
+
+import (
+	"bytes"
+	"encoding/json"
+	"strings"
+	"text/template"
+
+	"github.com/swaggo/swag"
+)
+
+var doc = `{
+    "schemes": {{ marshal .Schemes }},
+    "swagger": "2.0",
+    "info": {
+        "description": "{{escape .Description}}",
+        "title": "{{.Title}}",
+        "termsOfService": "https://www.hzinsights.com/",
+        "contact": {
+            "name": "www.hzinsights.com/",
+            "url": "https://www.hzinsights.com/",
+            "email": "pyan@hzinsights.com"
+        },
+        "license": {
+            "name": "Apache 2.0",
+            "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+        },
+        "version": "{{.Version}}"
+    },
+    "host": "{{.Host}}",
+    "basePath": "{{.BasePath}}",
+    "paths": {}
+}`
+
+type swaggerInfo struct {
+	Version     string
+	Host        string
+	BasePath    string
+	Schemes     []string
+	Title       string
+	Description string
+}
+
+// SwaggerInfo holds exported Swagger Info so clients can modify it
+var SwaggerInfo = swaggerInfo{
+	Version:     "1.0",
+	Host:        "127.0.0.1:8607",
+	BasePath:    "/",
+	Schemes:     []string{},
+	Title:       "弘则人力资源管理系统API接口文档",
+	Description: "弘则人力资源管理系统API接口文档",
+}
+
+type s struct{}
+
+func (s *s) ReadDoc() string {
+	sInfo := SwaggerInfo
+	sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1)
+
+	t, err := template.New("swagger_info").Funcs(template.FuncMap{
+		"marshal": func(v interface{}) string {
+			a, _ := json.Marshal(v)
+			return string(a)
+		},
+		"escape": func(v interface{}) string {
+			// escape tabs
+			str := strings.Replace(v.(string), "\t", "\\t", -1)
+			// replace " with \", and if that results in \\", replace that with \\\"
+			str = strings.Replace(str, "\"", "\\\"", -1)
+			return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1)
+		},
+	}).Parse(doc)
+	if err != nil {
+		return doc
+	}
+
+	var tpl bytes.Buffer
+	if err := t.Execute(&tpl, sInfo); err != nil {
+		return doc
+	}
+
+	return tpl.String()
+}
+
+func init() {
+	swag.Register("swagger", &s{})
+}

+ 69 - 0
global/global.go

@@ -0,0 +1,69 @@
+package global
+
+import (
+	"eta/eta_menu_sync/config"
+	"eta/eta_menu_sync/utils"
+	"fmt"
+	"github.com/fsnotify/fsnotify"
+	"github.com/go-redis/redis/v8"
+	socketio "github.com/googollee/go-socket.io"
+	"github.com/olivere/elastic/v7"
+	oplogging "github.com/op/go-logging"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+	"io"
+
+	"github.com/rdlucklib/rdluck_tools/cache"
+)
+
+var (
+	CONFIG        config.Config //配置文件
+	LOG           *oplogging.Logger
+	MYSQL         map[string]*gorm.DB //数据库连接配置
+	MYSQL_LOG     io.Writer
+	DEFAULT_MYSQL *gorm.DB      //默认数据库连接配置
+	Redis         *redis.Client //redis链接
+	EsClient      *elastic.Client
+
+	Rc *cache.Cache     //redis缓存
+	Re error            //redis错误
+	Ws *socketio.Server //websocket的链接
+)
+
+// const ConfigFile = "config/config_debug.yaml" //本地(测试)环境下的配置文件地址
+const ConfigFile = "config/config.yaml"    //本地(测试)环境下的配置文件地址
+const ProConfigFile = "config/config.yaml" //生产环境下的配置文件地址
+
+func init() {
+	v := viper.New()
+
+	configFilePath := ConfigFile
+
+	//如果不存在该配置文件,那么应该是线上环境,那么去寻找线上配置文件的路径
+	if !utils.FileIsExist(configFilePath) {
+		configFilePath = ProConfigFile
+	}
+
+	fmt.Println("configFilePath->", configFilePath)
+	//设置配置文件
+	v.SetConfigFile(configFilePath)
+
+	err := v.ReadInConfig()
+	if err != nil {
+		panic(any(fmt.Errorf("读取配置失败,Err: %s \n", err)))
+	}
+
+	//持续监听文件
+	v.WatchConfig()
+
+	v.OnConfigChange(func(e fsnotify.Event) {
+		fmt.Println("配置文件变更:", e.Name)
+		if err := v.Unmarshal(&CONFIG); err != nil {
+			fmt.Println("配置重赋值失败,Err:", err)
+		}
+		fmt.Println(CONFIG)
+	})
+	if err := v.Unmarshal(&CONFIG); err != nil {
+		fmt.Println("配置初始化赋值失败,Err:", err)
+	}
+}

+ 45 - 0
global/validator.go

@@ -0,0 +1,45 @@
+package global
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin/binding"
+	"github.com/go-playground/locales/en"
+	"github.com/go-playground/locales/zh"
+	ut "github.com/go-playground/universal-translator"
+	"github.com/go-playground/validator/v10"
+	enTranslations "github.com/go-playground/validator/v10/translations/en"
+	zhTranslations "github.com/go-playground/validator/v10/translations/zh"
+)
+
+// 定义一个全局翻译器T
+var Trans ut.Translator
+// InitTrans 初始化翻译器
+func InitTrans(locale string) (err error) {
+	// 修改gin框架中的Validator引擎属性,实现自定制
+	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+		zhT := zh.New() // 中文翻译器
+		enT := en.New() // 英文翻译器
+		// 第一个参数是备用(fallback)的语言环境
+		// 后面的参数是应该支持的语言环境(支持多个)
+		// uni := ut.New(zhT, zhT) 也是可以的
+		uni := ut.New(enT, zhT, enT)
+		// locale 通常取决于 http 请求头的 'Accept-Language'
+		var ok bool
+		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
+		Trans, ok = uni.GetTranslator(locale)
+		if !ok {
+			return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
+		}
+		// 注册翻译器
+		switch locale {
+		case "en":
+			err = enTranslations.RegisterDefaultTranslations(v, Trans)
+		case "zh":
+			err = zhTranslations.RegisterDefaultTranslations(v, Trans)
+		default:
+			err = enTranslations.RegisterDefaultTranslations(v, Trans)
+		}
+		return
+	}
+	return
+}

+ 81 - 0
go.mod

@@ -0,0 +1,81 @@
+module eta/eta_menu_sync
+
+go 1.19
+
+require (
+	github.com/fsnotify/fsnotify v1.6.0
+	github.com/gin-gonic/gin v1.9.0
+	github.com/go-playground/locales v0.14.1
+	github.com/go-playground/universal-translator v0.18.1
+	github.com/go-playground/validator/v10 v10.12.0
+	github.com/go-redis/redis/v8 v8.11.5
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
+	github.com/olivere/elastic/v7 v7.0.32
+	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
+	github.com/rdlucklib/rdluck_tools v1.0.3
+	github.com/robfig/cron/v3 v3.0.1
+	github.com/sbabiv/xml2map v1.2.1
+	github.com/spf13/viper v1.15.0
+	github.com/swaggo/swag v1.8.12
+	github.com/tealeg/xlsx v1.0.5
+	golang.org/x/image v0.6.0
+	gorm.io/driver/mysql v1.4.7
+	gorm.io/gorm v1.24.6
+)
+
+require (
+	github.com/KyleBanks/depth v1.2.1 // indirect
+	github.com/PuerkitoBio/purell v1.1.1 // indirect
+	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
+	github.com/bytedance/sonic v1.8.0 // indirect
+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/garyburd/redigo v1.6.3 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-openapi/jsonpointer v0.19.5 // indirect
+	github.com/go-openapi/jsonreference v0.19.6 // indirect
+	github.com/go-openapi/spec v0.20.4 // indirect
+	github.com/go-openapi/swag v0.19.15 // indirect
+	github.com/go-sql-driver/mysql v1.7.0 // indirect
+	github.com/goccy/go-json v0.10.0 // indirect
+	github.com/gofrs/uuid v4.0.0+incompatible // indirect
+	github.com/gomodule/redigo v2.0.0+incompatible // indirect
+	github.com/googollee/go-socket.io v1.7.0 // indirect
+	github.com/gorilla/websocket v1.4.2 // indirect
+	github.com/hashicorp/hcl v1.0.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jonboulle/clockwork v0.3.0 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/leodido/go-urn v1.2.2 // indirect
+	github.com/lestrrat-go/strftime v1.0.6 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/spf13/afero v1.9.3 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
+	github.com/spf13/jwalterweatherman v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/subosito/gotenv v1.4.2 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.9 // indirect
+	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+	golang.org/x/crypto v0.7.0 // indirect
+	golang.org/x/net v0.8.0 // indirect
+	golang.org/x/sys v0.6.0 // indirect
+	golang.org/x/text v0.8.0 // indirect
+	golang.org/x/tools v0.7.0 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 73 - 0
init_serve/mysql.go

@@ -0,0 +1,73 @@
+package init_serve
+
+import (
+	"fmt"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"eta/eta_menu_sync/global"
+	"io"
+	"log"
+	"os"
+	"time"
+)
+
+// Mysql 数据库初始化
+func Mysql() {
+	mysqlConf := global.CONFIG.Mysql
+	if len(mysqlConf.List) <= 0 {
+		global.LOG.Error("mysql链接未配置")
+		panic(any("mysql链接未配置"))
+	}
+
+	//开启日志
+	logWriter := io.MultiWriter(global.MYSQL_LOG) //binlog日志,记录到文件中去
+	if global.CONFIG.Mysql.Stdout {
+		logWriter = io.MultiWriter(global.MYSQL_LOG, os.Stdout)
+	}
+	newLogger := logger.New(log.New(logWriter, "\r\n", log.LstdFlags), logger.Config{
+		SlowThreshold:             200 * time.Millisecond, //慢sql :200ms
+		LogLevel:                  logger.Info,            //记录的日志类型,info代表所有信息都记录
+		IgnoreRecordNotFoundError: true,                   //是否忽略找不到数据错误信息(只是日志记录记录成err还是普通的输出的区别,并不影响业务代码中的:找不到数据行error)
+		Colorful:                  true,                   //是否颜色输出
+	})
+
+	mysqlMap := make(map[string]*gorm.DB)
+	for _, conf := range mysqlConf.List {
+		dsn := conf.Dsn
+
+		db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
+			Logger: newLogger,
+		})
+		if err != nil {
+			global.LOG.Errorf("mysql 启动异常,数据库:", conf.AliasName, ";Err:", err)
+			panic(any(fmt.Sprintf("mysql 启动异常,数据库:", conf.AliasName, "Err:%s", err.Error())))
+		}
+
+		//创建连接池
+		sqlDB, err := db.DB()
+		if err != nil {
+			global.LOG.Errorf("mysql 创建连接池失败,数据库:", conf.AliasName, ";Err:", err)
+			panic(any(fmt.Sprintf("mysql 创建连接池失败,数据库:", conf.AliasName, "Err:%s", err.Error())))
+		}
+
+		mysqlMap[conf.AliasName] = db
+
+		//默认数据库连接
+		if mysqlConf.DefaultDsnAliasName == conf.AliasName {
+			global.DEFAULT_MYSQL = db
+		}
+
+		// SetMaxIdleConns 设置空闲连接池中连接的最大数量
+		sqlDB.SetMaxIdleConns(conf.MaxIdleConns)
+
+		// SetMaxOpenConns 设置打开数据库连接的最大数量。
+		sqlDB.SetMaxOpenConns(conf.MaxOpenConns)
+
+		// SetConnMaxLifetime 设置了连接可复用的最大时间。
+		//sqlDB.SetConnMaxLifetime(time.Hour)
+	}
+
+	//全局赋值数据库链接
+	global.MYSQL = mysqlMap
+}

+ 25 - 0
init_serve/redis.go

@@ -0,0 +1,25 @@
+package init_serve
+
+import (
+	"context"
+	"github.com/go-redis/redis/v8"
+	"eta/eta_menu_sync/global"
+)
+
+func Redis() {
+	redisConf := global.CONFIG.Redis
+	client := redis.NewClient(&redis.Options{
+		Addr:     redisConf.Address,
+		Password: redisConf.Password,
+		DB:       redisConf.Db,
+		//PoolSize: 10, //连接池最大socket连接数,默认为10倍CPU数, 10 * runtime.NumCPU(暂不配置)
+	})
+	_, err := client.Ping(context.TODO()).Result()
+	if err != nil {
+		global.LOG.Error("redis 链接失败:", err)
+		panic(any("redis 链接失败:" + err.Error()))
+	}
+
+	//全局赋值redis链接
+	global.Redis = client
+}

+ 40 - 0
init_serve/router.go

@@ -0,0 +1,40 @@
+package init_serve
+
+import (
+	"github.com/gin-gonic/gin"
+	_ "eta/eta_menu_sync/docs"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/middleware"
+	"eta/eta_menu_sync/routers"
+)
+
+// InitRouter 初始化路由
+func InitRouter() (r *gin.Engine) {
+	//设置
+	gin.SetMode(global.CONFIG.Serve.RunMode)
+	// 1.创建路由
+	r = gin.Default()
+	r.Use(middleware.Cors())
+	//r.Use(gin.Recovery())
+	r.Use(middleware.Recover())
+
+	// 公共的中间件
+	r.Use(middleware.Common())
+
+	//swagger界面访问地址 http://localhost:8390/swagger/index.html
+	//r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
+	rBase := r.Group("api/")
+	//系统相关路由
+	indexGroup := rBase.Group("index/")
+	routers.InitIndex(indexGroup)
+	// 微信相关
+	wechatGroup := rBase.Group("wechat/")
+	routers.InitWechat(wechatGroup)
+	// webSocket相关
+	wsGroup := rBase.Group("ws/")
+	routers.InitWs(wsGroup)
+
+	//fmt.Println("routes:")
+	//fmt.Println(r.Routes()[0].Path)
+	return
+}

+ 87 - 0
init_serve/task.go

@@ -0,0 +1,87 @@
+package init_serve
+
+import (
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/models"
+	"eta/eta_menu_sync/utils"
+	"fmt"
+	"os"
+	"strings"
+	"time"
+)
+
+func InitTask() {
+	fmt.Println("开始执行生成sql语句")
+	etaBusinessObj := models.EtaBusiness{}
+	list, err := etaBusinessObj.GetAllEtaBusiness()
+	if err != nil {
+		fmt.Println("err:", err)
+		return
+	}
+
+	dateDir := time.Now().Format("20060102")
+	baseDir := global.CONFIG.Serve.StaticDir + "menu" + dateDir + ""
+	err = os.MkdirAll(baseDir, 0766)
+	if err != nil {
+		fmt.Println("存储目录创建失败,Err:" + err.Error())
+		return
+	}
+
+	etaBusinessMenuObj := models.EtaBusinessMenu{}
+	for _, v := range list {
+		menuList, tmpErr := etaBusinessMenuObj.GetMenuList(v.BusinessCode)
+		if tmpErr != nil {
+			fmt.Println(v.BusinessName, "生成失败,err:", tmpErr)
+			continue
+		}
+		if len(menuList) <= 0 {
+			fmt.Println(v.BusinessName, "生成失败,没有配置菜单")
+			continue
+		}
+		tableName := fmt.Sprintf("sys_menu_%s_bak", time.Now().Format(utils.FormatDateUnSpace))
+		sqlStr := "# 备份表\n# create table " + tableName + " like sys_menu;\n# insert into " + tableName + " select * from sys_menu;\n\n"
+		sqlStr += "delete from sys_menu where 1=1;\nINSERT INTO sys_menu (`menu_id`, `parent_id`, `name`, `sort`, `path`, `icon_path`, `component`, `hidden`, `is_level`, `level_path`, `menu_type`, `button_code`, `create_time`, `modify_time`,`api`,`name_en`) VALUES "
+
+		sqlStr += getMenuStr(menuList)
+
+		sqlStr += "SELECT role_id INTO @sys_id FROM sys_role WHERE role_name = 'admin';\nDELETE FROM sys_role_menu WHERE role_id = @sys_id;\nINSERT INTO sys_role_menu(`role_id`, `menu_id`, `type`)\nSELECT @sys_id, menu_id, 0 FROM sys_menu;"
+
+		err = writeFile(baseDir, sqlStr, v)
+		if err != nil {
+			fmt.Println(v.BusinessName, "生成失败,文件写入失败,Err:", err)
+			continue
+		}
+	}
+
+	fmt.Println("生成完成")
+}
+
+func getMenuStr(menuList []*models.EtaBusinessMenu) (str string) {
+	menuStrList := []string{}
+	for _, v := range menuList {
+		tmpStr := fmt.Sprintf(`(%d, %d, "%s", %d, "%s","%s", "%s",%d, %d, "%s", %d, "%s", "%s", "%s", "%s", "%s")`,
+			v.MenuId, v.ParentId, v.Name, v.Sort, v.Path, v.IconPath, v.Component, v.Hidden, v.IsLevel, v.LevelPath, v.MenuType, v.ButtonCode, v.CreateTime.Format(utils.FormatDateTime), v.ModifyTime.Format(utils.FormatDateTime), v.Api, v.NameEn)
+		menuStrList = append(menuStrList, tmpStr)
+
+	}
+	str = strings.Join(menuStrList, ",\n")
+
+	str += ";\n"
+	return
+}
+
+// writeFile 文件写入
+func writeFile(baseDir, str string, etaBusiness *models.EtaBusiness) (err error) {
+	file, err := os.OpenFile(baseDir+"/"+etaBusiness.BusinessName+"_"+etaBusiness.BusinessCode+".txt", os.O_WRONLY|os.O_CREATE, 0666)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	defer file.Close()
+	_, err = file.Write([]byte(str))
+	if err != nil {
+		return
+	}
+
+	return
+}

+ 32 - 0
init_serve/ws.go

@@ -0,0 +1,32 @@
+package init_serve
+
+import (
+	"fmt"
+	socketio "github.com/googollee/go-socket.io"
+	"eta/eta_menu_sync/global"
+)
+
+func InitWs() {
+	// 创建一个 Socket.IO 服务器实例
+	server := socketio.NewServer(nil)
+
+	// 处理 Socket.IO 连接
+	server.OnConnect("/", func(s socketio.Conn) error {
+		s.SetContext("")
+		fmt.Println("connected:", s.ID())
+		return nil
+	})
+
+	// 处理 Socket.IO 断开连接
+	server.OnDisconnect("/", func(s socketio.Conn, reason string) {
+		fmt.Println("disconnected:", s.ID(), reason)
+	})
+
+	// 处理 Socket.IO 消息
+	server.OnEvent("/", "message", func(s socketio.Conn, msg string) {
+		fmt.Println("message:", msg)
+		s.Emit("response", "hello")
+	})
+
+	global.Ws = server
+}

+ 0 - 0
logic/logic.txt


+ 102 - 0
logic/wechat_group.go

@@ -0,0 +1,102 @@
+package logic
+
+import (
+	"eta/eta_menu_sync/models"
+	"eta/eta_menu_sync/services"
+	"time"
+)
+
+// SyncWxGroup 同步微信群
+func SyncWxGroup(wxGroupList []services.GroupResult, wechatHelper *models.WechatHelper) (err error) {
+	wxIdList := make([]string, 0)
+	wxIdMap := make(map[string]services.GroupResult)
+	for _, v := range wxGroupList {
+		wxIdList = append(wxIdList, v.Wxid)
+		wxIdMap[v.Wxid] = v
+	}
+
+	wechatGroupObj := models.WechatGroup{}
+	// 将未添加入库的群入库
+	{
+		existsGroupList, tmpErr := wechatGroupObj.GetListByWxIdList(wxIdList)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		// 已经入库的微信群需要移除
+		for _, v := range existsGroupList {
+			if tmpWechatInfo, ok := wxIdMap[v.WechatId]; ok {
+				if v.WechatGroupName != tmpWechatInfo.Nick { // 可能存在群昵称不一致的情况,那么就修改吧
+					v.WechatGroupName = tmpWechatInfo.Nick
+					v.Update([]string{"WechatGroupName"})
+				}
+			}
+			delete(wxIdMap, v.WechatId)
+		}
+
+		addList := make([]models.WechatGroup, 0)
+		for _, v := range wxIdMap {
+			nick := v.Nick
+			if v.Nick == `` { // 可能存在群昵称不一致的情况,那就用微信群标识吧
+				nick = v.Wxid
+			}
+			addList = append(addList, models.WechatGroup{
+				WechatGroupId:   0,
+				WechatGroupName: nick, //群昵称
+				WechatId:        v.Wxid,
+				Enabled:         1,
+				ModifyTime:      time.Now(),
+				CreateTime:      time.Now(),
+			})
+		}
+		if len(addList) > 0 {
+			err = wechatGroupObj.CreateList(addList)
+			if err != nil {
+				return
+			}
+		}
+	}
+
+	{
+		existsGroupList, tmpErr := wechatGroupObj.GetListByWxIdList(wxIdList)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		wechatGroupIdMap := make(map[int]int, 0)
+		// 已经入库的微信标识
+		for _, v := range existsGroupList {
+			wechatGroupIdMap[v.WechatGroupId] = v.WechatGroupId
+		}
+
+		wechatHelperGroupRelationObj := models.WechatGroupHelperRelation{}
+		wechatHelperGroupList, tmpErr := wechatHelperGroupRelationObj.GetListByHelperId(wechatHelper.WechatHelperId)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		// 移除已经关联的群
+		for _, v := range wechatHelperGroupList {
+			delete(wechatGroupIdMap, v.WechatGroupId)
+		}
+
+		// 添加群与小助手的关系
+		addList := make([]models.WechatGroupHelperRelation, 0)
+		for _, v := range wechatGroupIdMap {
+			addList = append(addList, models.WechatGroupHelperRelation{
+				WechatGroupId:  v,
+				WechatHelperId: wechatHelper.WechatHelperId,
+			})
+		}
+		if len(addList) > 0 {
+			err = wechatHelperGroupRelationObj.CreateList(addList)
+			if err != nil {
+				return
+			}
+		}
+	}
+
+	return
+}

+ 10 - 0
main.go

@@ -0,0 +1,10 @@
+package main
+
+import (
+	"eta/eta_menu_sync/core"
+)
+
+// @BasePath /
+func main() {
+	core.RunServe()
+}

+ 28 - 0
middleware/check_base_auth.go

@@ -0,0 +1,28 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+// CheckBaseAuth 基本权限校验
+func CheckBaseAuth() gin.HandlerFunc {
+
+	return func(c *gin.Context) {
+
+		//userInfo := user.GetInfoByClaims(c)
+		//
+		//ok, checkInfo, _, err := company.CheckBaseFiccPermission(userInfo.CompanyID, int(userInfo.UserID))
+		//if err != nil {
+		//	resp.FailMsg("用户权限验证失败", "CheckBaseAuth-用户权限验证失败" + err.Error(), c)
+		//	c.Abort()
+		//	return
+		//}
+		//if !ok {
+		//	resp.AuthError(checkInfo, "暂无权限", c)
+		//	c.Abort()
+		//	return
+		//}
+
+		c.Next()
+	}
+}

+ 27 - 0
middleware/common.go

@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"strconv"
+)
+
+// Common 公共中间件
+func Common() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		var currPage, pageSize int
+		reqPage := c.DefaultQuery("curr_page", "0")
+		currPage, _ = strconv.Atoi(reqPage)
+		if currPage <= 0 {
+			currPage = 1
+		}
+
+		reqPageSize := c.DefaultQuery("page_size", "0")
+		pageSize, _ = strconv.Atoi(reqPageSize)
+		if pageSize <= 0 {
+			pageSize = 20
+		}
+		c.Set("curr_page", currPage)
+		c.Set("page_size", pageSize)
+		c.Next()
+	}
+}

+ 27 - 0
middleware/cors.go

@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+// Cors 处理跨域请求,支持options访问
+func Cors() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		method := c.Request.Method
+
+		c.Header("Access-Control-Allow-Origin", "*")
+		//c.Header("Access-Control-Allow-Origin", c.Request.Referer())
+		c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id")
+		c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
+		c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+		c.Header("Access-Control-Allow-Credentials", "true")
+
+		// 放行所有OPTIONS方法
+		if method == "OPTIONS" {
+			c.AbortWithStatus(http.StatusNoContent)
+		}
+		// 处理请求
+		c.Next()
+	}
+}

+ 56 - 0
middleware/recover.go

@@ -0,0 +1,56 @@
+package middleware
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/resp"
+	"eta/eta_menu_sync/global"
+	"net/http"
+	"runtime"
+)
+
+// Recover 异常处理
+func Recover() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		contentType := c.ContentType()
+		// 为 multipart forms 设置较低的内存限制(50M) (默认是 32 MiB)
+		if contentType == "multipart/form-data" {
+			err := c.Request.ParseMultipartForm(10 << 20)
+			if err != nil {
+				resp.Custom(http.StatusRequestEntityTooLarge, "上传文件太大,err:"+err.Error(), c)
+				c.Abort()
+				return
+			}
+		}
+		defer func() {
+			if err := recover(); err != any(nil) {
+				fmt.Println("recover err:", err)
+				return
+				stack := ""
+
+				msg := fmt.Sprintf("The request url is  %v", c.Request.RequestURI)
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				msg = fmt.Sprintf("The request data is %v", c.Params)
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				msg = fmt.Sprintf("Handler crashed with error %v", err)
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				for i := 1; ; i++ {
+					_, file, line, ok := runtime.Caller(i)
+					if !ok {
+						break
+					}
+					global.LOG.Critical(fmt.Sprintf("%s:%d", file, line))
+					stack = stack + fmt.Sprintln(fmt.Sprintf("%s:%d</br>", file, line))
+				}
+				fmt.Println("stack:", stack)
+				resp.Custom(http.StatusInternalServerError, "系统异常", c)
+				//go alarm_msg.SendAlarmMsg(utils.APPNAME+"崩了"+time.Now().Format("2006-01-02 15:04:05")+";Err:"+stack, 3, ``)
+				return
+			}
+		}()
+		c.Next()
+	}
+}

+ 33 - 0
middleware/token.go

@@ -0,0 +1,33 @@
+package middleware
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/resp"
+	"eta/eta_menu_sync/global"
+)
+
+func Token() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("Authorization")
+		if token == "" {
+			token = c.DefaultQuery("authorization", "")
+			if token == "" {
+				token = c.DefaultQuery("Authorization", "")
+			}
+		}
+		if token == "" {
+			resp.TokenError(nil, "未登录或非法访问", "未登录或非法访问", c)
+			c.Abort()
+			return
+		}
+		fmt.Println("token:" + token)
+		if token != global.CONFIG.Serve.Token {
+			resp.TokenError(nil, "非法访问", "非法访问", c)
+			c.Abort()
+			return
+		}
+		//c.Set("adminInfo", admin)
+		c.Next()
+	}
+}

+ 80 - 0
middleware/token_no_login.go

@@ -0,0 +1,80 @@
+package middleware
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/resp"
+)
+
+func TokenNoLogin() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("Authorization")
+		if token == "" {
+			token = c.GetString("authorization")
+			if token == "" {
+				token = c.GetString("Authorization")
+			}
+		}
+		if token == "" {
+			resp.TokenError(nil, "未登录或非法访问", "token为空", c)
+			c.Abort()
+			return
+		}
+		//sessionInfo, err := session.GetTokenByToken(token)
+		//if err != nil {
+		//	if err == utils.ErrNoRow {
+		//		resp.TokenError(nil, "信息已变更,请重新登陆!", "找不到session", c)
+		//		c.Abort()
+		//		return
+		//	}
+		//	resp.TokenError(nil, "网络异常,请稍后重试!", err.Error(), c)
+		//	c.Abort()
+		//	return
+		//}
+		//
+		//if sessionInfo == nil {
+		//	resp.TokenError(nil, "网络异常,请稍后重试!", "session为空", c)
+		//	c.Abort()
+		//	return
+		//}
+		//
+		//var userInfo services.UserInfo
+		//
+		//if sessionInfo.OpenID != "" {
+		//	tmpUserInfo, tmpErr := services.GetWxUserItemByOpenId(sessionInfo.OpenID)
+		//	userInfo = tmpUserInfo
+		//	err = tmpErr
+		//	if err != nil && err != services.ERR_NO_USER_RECORD && err != services.ERR_USER_NOT_BIND{
+		//		resp.TokenError(nil, "数据异常!", "openid查询用户信息错误", c)
+		//		c.Abort()
+		//		return
+		//	}
+		//} else {
+		//	//判断pc端登录的情况
+		//	tmpUserInfo, tmpErr := services.GetWxUserItemByUserId(int(sessionInfo.UserID), 3)
+		//	userInfo = tmpUserInfo
+		//	err = tmpErr
+		//	if err != nil {
+		//		resp.TokenError(nil, "数据异常!", "userID查询用户信息错误", c)
+		//		c.Abort()
+		//		return
+		//	}
+		//}
+		//
+		////如果查询异常,且异常信息不是:用户openid查询出来发现没有绑定用户
+		//if err != nil && err != services.ERR_USER_NOT_BIND {
+		//	//没有找到记录
+		//	if err == utils.ErrNoRow {
+		//		resp.TokenError(nil, "信息已变更,请重新登陆!", err.Error(), c)
+		//		c.Abort()
+		//		return
+		//	}
+		//
+		//	resp.TokenError(nil, "网络异常,请稍后重试!", err.Error(), c)
+		//	c.Abort()
+		//	return
+		//}
+		//
+		//c.Set("userInfo", userInfo)
+		c.Next()
+	}
+}

+ 171 - 0
models/base/base.go

@@ -0,0 +1,171 @@
+package base
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"gorm.io/gorm"
+)
+
+var globalIsRelated bool = true // 全局预加载
+
+// prepare for other
+type BaseMgr struct {
+	*gorm.DB
+	Ctx       context.Context
+	cancel    context.CancelFunc
+	timeout   time.Duration
+	isRelated bool
+}
+
+// SetTimeOut set timeout
+func (obj *BaseMgr) SetTimeOut(timeout time.Duration) {
+	obj.Ctx, obj.cancel = context.WithTimeout(context.Background(), timeout)
+	obj.timeout = timeout
+}
+
+// SetCtx set context
+func (obj *BaseMgr) SetCtx(c context.Context) {
+	if c != nil {
+		obj.Ctx = c
+	}
+}
+
+// GetCtx get context
+func (obj *BaseMgr) GetCtx() context.Context {
+	return obj.Ctx
+}
+
+// Cancel cancel context
+func (obj *BaseMgr) Cancel(c context.Context) {
+	obj.cancel()
+}
+
+// GetDB get gorm.DB info
+func (obj *BaseMgr) GetDB() *gorm.DB {
+	return obj.DB
+}
+
+// UpdateDB update gorm.DB info
+func (obj *BaseMgr) UpdateDB(db *gorm.DB) {
+	obj.DB = db
+}
+
+// GetIsRelated Query foreign key Association.获取是否查询外键关联(gorm.Related)
+func (obj *BaseMgr) GetIsRelated() bool {
+	return obj.isRelated
+}
+
+// SetIsRelated Query foreign key Association.设置是否查询外键关联(gorm.Related)
+func (obj *BaseMgr) SetIsRelated(b bool) {
+	obj.isRelated = b
+}
+
+// New new gorm.新gorm,重置条件
+func (obj *BaseMgr) New() {
+	obj.DB = obj.NewDB()
+}
+
+// NewDB new gorm.新gorm
+func (obj *BaseMgr) NewDB() *gorm.DB {
+	return obj.DB.Session(&gorm.Session{NewDB: true, Context: obj.Ctx})
+}
+
+type Options struct {
+	Query map[string]interface{}
+}
+
+// Option overrides behavior of Connect.
+type Option interface {
+	Apply(*Options)
+}
+
+type OptionFunc func(*Options)
+
+func (f OptionFunc) apply(o *Options) {
+	f(o)
+}
+
+// OpenRelated 打开全局预加载
+func OpenRelated() {
+	globalIsRelated = true
+}
+
+// CloseRelated 关闭全局预加载
+func CloseRelated() {
+	globalIsRelated = true
+}
+
+// 自定义sql查询
+type Condition struct {
+	list []*conditionInfo
+}
+
+func (c *Condition) AndWithCondition(condition bool, column string, cases string, value interface{}) *Condition {
+	if condition {
+		c.list = append(c.list, &conditionInfo{
+			andor:  "and",
+			column: column, // 列名
+			case_:  cases,  // 条件(and,or,in,>=,<=)
+			value:  value,
+		})
+	}
+	return c
+}
+
+// And a Condition by and .and 一个条件
+func (c *Condition) And(column string, cases string, value interface{}) *Condition {
+	return c.AndWithCondition(true, column, cases, value)
+}
+
+func (c *Condition) OrWithCondition(condition bool, column string, cases string, value interface{}) *Condition {
+	if condition {
+		c.list = append(c.list, &conditionInfo{
+			andor:  "or",
+			column: column, // 列名
+			case_:  cases,  // 条件(and,or,in,>=,<=)
+			value:  value,
+		})
+	}
+	return c
+}
+
+// Or a Condition by or .or 一个条件
+func (c *Condition) Or(column string, cases string, value interface{}) *Condition {
+	return c.OrWithCondition(true, column, cases, value)
+}
+
+func (c *Condition) Get() (where string, out []interface{}) {
+	firstAnd := -1
+	for i := 0; i < len(c.list); i++ { // 查找第一个and
+		if c.list[i].andor == "and" {
+			where = fmt.Sprintf("`%v` %v ?", c.list[i].column, c.list[i].case_)
+			out = append(out, c.list[i].value)
+			firstAnd = i
+			break
+		}
+	}
+
+	if firstAnd < 0 && len(c.list) > 0 { // 补刀
+		where = fmt.Sprintf("`%v` %v ?", c.list[0].column, c.list[0].case_)
+		out = append(out, c.list[0].value)
+		firstAnd = 0
+	}
+
+	for i := 0; i < len(c.list); i++ { // 添加剩余的
+		if firstAnd != i {
+			where += fmt.Sprintf(" %v `%v` %v ?", c.list[i].andor, c.list[i].column, c.list[i].case_)
+			out = append(out, c.list[i].value)
+		}
+	}
+
+	return
+}
+
+type conditionInfo struct {
+	andor  string
+	column string // 列名
+	case_  string // 条件(in,>=,<=)
+	value  interface{}
+}

+ 165 - 0
models/base/page.go

@@ -0,0 +1,165 @@
+package base
+
+import (
+	"fmt"
+	"strings"
+)
+
+type IPage interface {
+	GetTotal() int64             //获取总记录数
+	SetTotal(int64)              //设置总记录数
+	GetCurrent() int64           //获取当前页
+	SetCurrent(int64)            //设置当前页
+	GetPageSize() int64          //获取每页显示大小
+	SetPageSize(int64)           //设置每页显示大小
+	AddOrderItem(OrderItem)      // 设置排序条件
+	AddOrderItems([]OrderItem)   // 批量设置排序条件
+	GetOrderItemsString() string // 将排序条件拼接成字符串
+	Offset() int64               // 获取偏移量
+	GetPages() int64             // 获取总的分页数
+}
+
+type OrderItem struct {
+	column string // 需要排序的字段
+	asc    bool   // 是否正序排列,默认true
+}
+
+type BaseData struct {
+	Page *Page        `json:"page"`
+	List interface{} `json:"list"` // 查询数据列表
+}
+
+type Page struct {
+	Total    int64       `json:"total"`     // 总的记录数
+	Pages    int64       `json:"pages"`     //总页数
+	PageSize int64       `json:"page_size"` // 每页显示的大小
+	Current  int64       `json:"current"`   // 当前页
+	orders   []OrderItem `json:"orders"`    // 排序条件
+}
+
+type PageReq struct {
+	PageSize int64 `json:"page_size" form:"page_size,default=20"`
+	Current  int64 `json:"current" form:"current,default=1"`
+}
+
+func (b *BaseData) GetList() interface{} {
+	return b.List
+}
+
+func (b *BaseData) SetList(list interface{}) {
+	b.List = list
+}
+
+func (b *BaseData) GetPage() *Page {
+	return b.Page
+}
+func (b *BaseData) SetPage(page *Page) {
+	b.Page = page
+}
+
+func (page *Page) GetTotal() int64 {
+	return page.Total
+}
+
+func (page *Page) SetTotal(total int64) {
+	page.Total = total
+	page.SetPages()
+}
+
+func (page *Page) GetCurrent() int64 {
+	return page.Current
+}
+
+func (page *Page) SetCurrent(current int64) {
+	page.Current = current
+}
+
+func (page *Page) GetPageSize() int64 {
+	return page.PageSize
+}
+func (page *Page) SetPageSize(size int64) {
+	page.PageSize = size
+}
+
+func (page *Page) AddOrderItem(orderItem OrderItem) {
+	page.orders = append(page.orders, orderItem)
+}
+
+func (page *Page) AddOrderItems(orderItems []OrderItem) {
+	page.orders = append(page.orders, orderItems...)
+}
+
+func (page *Page) GetOrderItemsString() string {
+	arr := make([]string, 0)
+	var order string
+
+	for _, val := range page.orders {
+		if val.asc {
+			order = ""
+		} else {
+			order = "desc"
+		}
+		arr = append(arr, fmt.Sprintf("%s %s", val.column, order))
+	}
+	return strings.Join(arr, ",")
+}
+
+func (page *Page) Offset() int64 {
+	if page.GetCurrent() > 0 {
+		return (page.GetCurrent() - 1) * page.GetPageSize()
+	} else {
+		return 0
+	}
+}
+
+func (page *Page) GetPages() int64 {
+	if page.GetPageSize() == 0 {
+		return 0
+	}
+	pages := page.GetTotal() / page.GetPageSize()
+	if page.GetTotal()%page.PageSize != 0 {
+		pages++
+	}
+
+	return pages
+}
+
+func (page *Page) SetPages() {
+	if page.GetPageSize() == 0 {
+		page.Pages = 0
+	} else {
+		pages := page.GetTotal() / page.GetPageSize()
+		if page.GetTotal()%page.PageSize != 0 {
+			pages++
+		}
+		page.Pages = pages
+	}
+}
+
+func BuildAsc(column string) OrderItem {
+	return OrderItem{column: column, asc: true}
+}
+
+func BuildDesc(column string) OrderItem {
+	return OrderItem{column: column, asc: false}
+}
+
+func BuildAscs(columns ...string) []OrderItem {
+	items := make([]OrderItem, 0)
+	for _, val := range columns {
+		items = append(items, BuildAsc(val))
+	}
+	return items
+}
+
+func BuildDescs(columns ...string) []OrderItem {
+	items := make([]OrderItem, 0)
+	for _, val := range columns {
+		items = append(items, BuildDesc(val))
+	}
+	return items
+}
+
+func NewPage(pageSize, current int64, orderItems ...OrderItem) *Page {
+	return &Page{PageSize: pageSize, Current: current, orders: orderItems}
+}

+ 13 - 0
models/base/time_base.go

@@ -0,0 +1,13 @@
+package base
+
+import "time"
+
+type TimeBase struct {
+	CreateTime time.Time `gorm:"autoCreateTime;column:create_time" json:"create_time"`       //创建时间
+	ModifyTime time.Time `gorm:"autoUpdateTime:milli;column:modify_time" json:"modify_time"` //最后更新时间
+}
+
+func (item *TimeBase) Set() {
+	item.CreateTime = time.Now()
+	item.ModifyTime = time.Now()
+}

+ 99 - 0
models/edb_info.go

@@ -0,0 +1,99 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+type EdbInfo struct {
+	EdbInfoId        int    `orm:"column(edb_info_id);pk"`
+	SourceName       string `description:"来源名称"`
+	Source           int    `description:"来源id"`
+	EdbCode          string `description:"指标编码"`
+	EdbName          string `description:"指标名称"`
+	EdbNameSource    string `description:"指标名称来源"`
+	Frequency        string `description:"频率"`
+	Unit             string `description:"单位"`
+	StartDate        string `description:"起始日期"`
+	EndDate          string `description:"终止日期"`
+	ClassifyId       int    `description:"分类id"`
+	SysUserId        int
+	SysUserRealName  string
+	UniqueCode       string `description:"指标唯一编码"`
+	CreateTime       time.Time
+	ModifyTime       time.Time
+	MinValue         float64 `description:"指标最小值"`
+	MaxValue         float64 `description:"指标最大值"`
+	CalculateFormula string  `description:"计算公式"`
+	EdbType          int     `description:"指标类型:1:基础指标,2:计算指标"`
+	Sort             int     `description:"排序字段"`
+	MoveType         int     `description:"移动方式:1:领先(默认),2:滞后"`
+	MoveFrequency    string  `description:"移动频度"`
+	NoUpdate         int8    `description:"是否停止更新,0:继续更新;1:停止更新"`
+	ServerUrl        string  `description:"服务器地址"`
+
+	EdbInfoType int     `description:"指标类型,0:普通指标,1:预测指标"`
+	EdbNameEn   string  `description:"英文指标名称"`
+	UnitEn      string  `description:"英文单位"`
+	LatestDate  string  `description:"数据最新日期"`
+	LatestValue float64 `description:"数据最新值"`
+	ChartImage  string  `description:"图表图片"`
+}
+
+func (d *EdbInfo) GetEdbInfoItem(runMod, indexCode string) (item *EdbInfo, err error) {
+	if runMod == "release" {
+		err = global.MYSQL["hzdata"].WithContext(context.TODO()).Table("edb_info").
+			Where("edb_code = ?", indexCode).First(&item).Error
+		return
+	} else {
+		err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Table("edb_info").
+			Where("edb_code = ?", indexCode).First(&item).Error
+		return
+	}
+}
+
+type EdbDataMysteelChemical struct {
+	EdbDataId     int `orm:"column(edb_data_id);pk"`
+	EdbInfoId     int
+	EdbCode       string
+	DataTime      string
+	Value         string
+	CreateTime    time.Time
+	ModifyTime    time.Time
+	DataTimestamp int64
+}
+
+func (d *EdbDataMysteelChemical) GetEdbInfoDataList(runMod string, indexCode string) (item []*EdbDataMysteelChemical, err error) {
+	if runMod == "release" {
+		err = global.MYSQL["hzdata"].WithContext(context.TODO()).Model(d).
+			Where("index_code = ?", indexCode).Find(&item).Error
+		return
+	} else {
+		err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(d).
+			Where("index_code = ?", indexCode).Find(&item).Error
+		return
+	}
+}
+
+// 新增
+func (r *EdbDataMysteelChemical) Add(runMod string, list []EdbDataMysteelChemical) (err error) {
+	if runMod == "release" {
+		err = global.MYSQL["hzdata"].Create(list).Error
+		return
+	} else {
+		err = global.DEFAULT_MYSQL.Create(list).Error
+		return
+	}
+}
+
+// 修改
+func (r *EdbDataMysteelChemical) Update(runMod string, updateCols []string) (err error) {
+	if runMod == "release" {
+		err = global.MYSQL["hzdata"].Model(r).Where("edb_data_id=?", r.EdbDataId).Select(updateCols).Updates(r).Error
+		return
+	} else {
+		err = global.DEFAULT_MYSQL.Model(r).Where("edb_data_id=?", r.EdbDataId).Select(updateCols).Updates(r).Error
+		return
+	}
+}

+ 28 - 0
models/eta_business.go

@@ -0,0 +1,28 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+type EtaBusiness struct {
+	EtaBusinessId int    `orm:"column(eta_business_id);pk"`
+	BusinessName  string `description:"商家名称"`
+	BusinessCode  string `description:"商家编码"`
+	Enable        int    `description:"状态:0-禁用;1-启用"`
+	CreateTime    time.Time
+	ModifyTime    time.Time
+}
+
+func (m *EtaBusiness) TableName() string {
+	return "eta_business"
+}
+
+func (m *EtaBusiness) GetAllEtaBusiness() (items []*EtaBusiness, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("enable = ?  ", 1).Find(&items).Error
+
+	//Where("index_name = '' AND create_time<=? ", endTime).Find(&item).Error
+	return
+}

+ 41 - 0
models/eta_business_menu.go

@@ -0,0 +1,41 @@
+package models
+
+import (
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+type EtaBusinessMenu struct {
+	MenuId     int    `orm:"column(menu_id);pk"`
+	ParentId   int    `description:"商家名称"`
+	Name       string `description:"商家编码"`
+	Sort       int    `description:"排序序号"`
+	Path       string `description:"路由地址"`
+	IconPath   string `description:"菜单图标地址"`
+	Component  string `description:"组件路径"`
+	Hidden     int    `description:"是否隐藏:1-隐藏 0-显示"`
+	IsLevel    int    `description:"是否为多级菜单:1,只有一级;2,有多级"`
+	LevelPath  string `description:"兼容以前menu表的字段"`
+	MenuType   int    `description:"菜单类型: 0-菜单; 1-按钮; 2-字段(需要特殊处理)"`
+	ButtonCode string `description:"按钮唯一标识"`
+	CreateTime time.Time
+	ModifyTime time.Time
+	Api        string `description:"api"`
+	NameEn     string `description:"name_en"`
+}
+
+func (m *EtaBusinessMenu) TableName() string {
+	return "eta_business_menu"
+}
+
+// GetMenuList 获取菜单列表
+func (m *EtaBusinessMenu) GetMenuList(businessCode string) (items []*EtaBusinessMenu, err error) {
+	sql := `SELECT a.* FROM eta_business_menu AS a
+JOIN eta_business_menu_relate AS b ON a.menu_id = b.menu_id
+WHERE b.eta_business_id = (
+    SELECT eta_business_id FROM eta_business WHERE business_code = ?
+) AND b.type = 0;`
+
+	err = global.DEFAULT_MYSQL.Raw(sql, businessCode).Scan(&items).Error
+	return
+}

+ 45 - 0
models/report_send_ths_detail.go

@@ -0,0 +1,45 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// ReportSendThsDetail 报告推送给同花顺的表结构体
+type ReportSendThsDetail struct {
+	SendId       int       `gorm:"primaryKey;column:send_id;" description:"发送给同花顺的Id"`
+	ReportId     int       `description:"报告id"`
+	ReportType   string    `description:"报告类型"`
+	Status       int8      `description:"发送结果,0:待发送,-1发送失败,1发送成功"`
+	Remark       string    `description:"失败原因"`
+	PushTime     time.Time `description:"实际开始推送时间/预推送时间"`
+	CreateTime   time.Time `description:"发送时间"`
+	Title        string    `description:"推送标题"`
+	LabelStr     string    `description:"推送标签"`
+	RecordStatus int8      `description:"记录生成状态,0:未生成,1:已生成;默认:0"`
+	MsgType      int       `description:"消息类型,1:h5链接;2:小程序,3:文字;4:图片"`
+	Content      string    `description:"推送内容"`
+	JumpUrl      string    `description:"跳转地址"`
+	Pic          string    `description:"推送图片"`
+	Level        int       `description:"等级,1:紧急,2:优先,3:普通"`
+}
+
+func (m *ReportSendThsDetail) TableName() string {
+	return "report_send_ths_detail"
+}
+
+// GetNoRecordList 找出未生成记录的消息
+func (m *ReportSendThsDetail) GetNoRecordList() (items []*ReportSendThsDetail, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("status = ? AND record_status = ?  ", 1, 0).Find(&items).Error
+
+	return
+}
+
+// Update 更新对应字段数据
+func (m *ReportSendThsDetail) Update(updateCols []string) (err error) {
+	err = global.DEFAULT_MYSQL.Model(m).Select(updateCols).Updates(*m).Error
+
+	return
+}

+ 6 - 0
models/request/wechat.go

@@ -0,0 +1,6 @@
+package request
+
+// SyncHelperReq 同步小助手的请求
+type SyncHelperReq struct {
+	WechatId string `description:"微信小助手标识"`
+}

+ 74 - 0
models/wechat_group.go

@@ -0,0 +1,74 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// WechatGroup 微信群表
+type WechatGroup struct {
+	WechatGroupId   int       `gorm:"primaryKey;column:wechat_group_id;" description:"微信群id"`
+	WechatGroupName string    `description:"微信群名"`
+	WechatId        string    `description:"微信群标识"`
+	Enabled         int8      `description:"1:有效,0:禁用"`
+	ModifyTime      time.Time `description:"修改时间"`
+	CreateTime      time.Time `description:"发送时间"`
+}
+
+func (m *WechatGroup) TableName() string {
+	return "wechat_group"
+}
+
+// GetListByIdList 通过群id列表获取所有关联的群
+func (m *WechatGroup) GetListByIdList(wechatGroupIdList []int) (items []*WechatGroup, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_group_id in (?) ", wechatGroupIdList).Find(&items).Error
+
+	return
+}
+
+// GetEnabledListByIdList 通过群id列表获取所有关联的激活的群
+func (m *WechatGroup) GetEnabledListByIdList(wechatGroupIdList []int) (items []*WechatGroup, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_group_id in (?) AND enabled = 1 ", wechatGroupIdList).Find(&items).Error
+
+	return
+}
+
+// GetByWxId 通过群id获取的群信息
+func (m *WechatGroup) GetByWxId(wxId string) (item *WechatGroup, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_id = ? ", wxId).First(&item).Error
+
+	return
+}
+
+// GetListByWxIdList 通过群标识列表获取所有关联的群
+func (m *WechatGroup) GetListByWxIdList(wechatIdList []string) (items []*WechatGroup, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_id in (?) ", wechatIdList).Find(&items).Error
+
+	return
+}
+
+// Create 添加记录
+func (m *WechatGroup) Create() (err error) {
+	err = global.DEFAULT_MYSQL.Create(m).Error
+
+	return
+}
+
+// CreateList 批量添加入库
+func (m *WechatGroup) CreateList(list []WechatGroup) (err error) {
+	err = global.DEFAULT_MYSQL.Create(list).Error
+
+	return
+}
+
+// Update 更新对应字段数据
+func (m *WechatGroup) Update(updateCols []string) (err error) {
+	err = global.DEFAULT_MYSQL.Model(m).Select(updateCols).Updates(*m).Error
+
+	return
+}

+ 38 - 0
models/wechat_group_helper_relation.go

@@ -0,0 +1,38 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+)
+
+// WechatGroupHelperRelation 微信群与小助手关系表
+type WechatGroupHelperRelation struct {
+	WechatGroupId  int `gorm:"primaryKey;column:wechat_group_id;autoIncrement:false;" description:"微信群id"`
+	WechatHelperId int `gorm:"primaryKey;column:wechat_helper_id;autoIncrement:false;"  description:"微信小助手id"`
+}
+
+func (m *WechatGroupHelperRelation) TableName() string {
+	return "wechat_group_helper_relation"
+}
+
+// Create 添加记录
+func (m *WechatGroupHelperRelation) Create() (err error) {
+	err = global.DEFAULT_MYSQL.Create(m).Error
+
+	return
+}
+
+// GetListByHelperId 通过小助手id获取所有关联的群信息
+func (m *WechatGroupHelperRelation) GetListByHelperId(helperId int) (items []*WechatGroupHelperRelation, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_helper_id = ? ", helperId).Find(&items).Error
+
+	return
+}
+
+// CreateList 批量添加入库
+func (m *WechatGroupHelperRelation) CreateList(list []WechatGroupHelperRelation) (err error) {
+	err = global.DEFAULT_MYSQL.Create(list).Error
+
+	return
+}

+ 34 - 0
models/wechat_group_tag.go

@@ -0,0 +1,34 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// WechatGroupTag 微信群标签表
+type WechatGroupTag struct {
+	WechatGroupId int       `gorm:"primaryKey;autoIncrement:false;column:wechat_group_id;" description:"微信群id"`
+	TagName       string    `gorm:"primaryKey;autoIncrement:false;column:tag_name;" description:"报告标签"`
+	ModifyTime    time.Time `description:"修改时间"`
+	CreateTime    time.Time `description:"发送时间"`
+}
+
+func (r *WechatGroupTag) TableName() string {
+	return "wechat_group_tag"
+}
+
+// GetListByTagNameList 通过标签名称获取所有关联的群
+func (m *WechatGroupTag) GetListByTagNameList(tagNameList []string) (items []*WechatGroupTag, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("tag_name in (?) ", tagNameList).Group("wechat_group_id").Find(&items).Error
+
+	return
+}
+
+// Create 添加记录
+func (m *WechatGroupTag) Create() (err error) {
+	err = global.DEFAULT_MYSQL.Create(m).Error
+
+	return
+}

+ 58 - 0
models/wechat_helper.go

@@ -0,0 +1,58 @@
+package models
+
+import (
+	"context"
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// WechatHelper 微信小助手表
+type WechatHelper struct {
+	WechatHelperId   int       `gorm:"primaryKey;column:wechat_helper_id;" description:"微信小助手id"`
+	WechatHelperName string    `description:"微信小助手名称"`
+	WechatId         string    `description:"微信小助手标识"`
+	Num              int       `description:"一分钟推送几条"`
+	ServerUrl        string    `description:"服务器地址"`
+	Enabled          int8      `description:"1:有效,0:禁用"`
+	ModifyTime       time.Time `description:"修改时间"`
+	CreateTime       time.Time `description:"发送时间"`
+}
+
+func (m *WechatHelper) TableName() string {
+	return "wechat_helper"
+}
+
+func (m *WechatHelper) GetAllHelper() (items []*WechatHelper, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("enabled = ?  ", 1).Find(&items).Error
+
+	//Where("index_name = '' AND create_time<=? ", endTime).Find(&item).Error
+	return
+}
+
+// GetByWechatId 根据微信小助手标识 获取信息
+func (m *WechatHelper) GetByWechatId(wechatId string) (item *WechatHelper, err error) {
+	err = global.DEFAULT_MYSQL.WithContext(context.TODO()).Model(m).
+		Where("wechat_id = ?  ", wechatId).First(&item).Error
+
+	//Where("index_name = '' AND create_time<=? ", endTime).Find(&item).Error
+	return
+}
+
+// WechatHelperGroup 微信小助手表
+type WechatHelperGroup struct {
+	WechatHelperId   int       ` description:"微信小助手id"`
+	WechatHelperName string    `description:"微信小助手名称"`
+	WechatId         string    `description:"微信群标识"`
+	Enabled          int8      `description:"1:有效,0:禁用"`
+	WechatGroupId    int       ` description:"微信群id"`
+	ModifyTime       time.Time `description:"修改时间"`
+	CreateTime       time.Time `description:"发送时间"`
+}
+
+func GetAllWechatGroup() (items []*WechatHelperGroup, err error) {
+	sql := `select a.*,b.wechat_group_id FROM wechat_helper a  join wechat_group_helper_relation b on a.wechat_helper_id = b.wechat_helper_id order by a.wechat_helper_id asc `
+	err = global.DEFAULT_MYSQL.Raw(sql).Scan(&items).Error
+
+	return
+}

+ 41 - 0
models/wechat_listen_msg.go

@@ -0,0 +1,41 @@
+package models
+
+import (
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// ListenWxAppType 小程序的消息类型
+var ListenWxAppType = 49
+
+// WechatListenMsgRecord 微信消息监听记录表
+type WechatListenMsgRecord struct {
+	WechatMsgId       int       `gorm:"primaryKey;column:wechat_listen_record_id;" description:"自增id"`
+	Content           string    `description:"监听的微信消息内容"`
+	FromWechatId      string    `description:"来源于哪个用户/群的微信标识"`
+	FinalFromWechatId string    `description:"群内发言人微信标识"`
+	WechatId          string    `description:"消息来源的微信标识;就是当前登录的微信用户"`
+	MsgType           int       `description:"消息类型,消息类型;\r\n1 文本消息\r\n3 图片消息\r\n34 语音消息\r\n37 好友确认消息\r\n40 好友推荐消息\r\n42 共享名片\r\n43 视频消息\r\n44 主动撤回\r\n47 动画表情\r\n48 位置消息\r\n49 APP分享链接/文件\r\n50 VOIP消息\r\n51 微信初始化消息 52 VOIP结束消息\r\n53 VOIP邀请\r\n62 小视频\r\n9999 SYSNOTICE\r\n10000 系统消息\r\n10002 撤回消息"`
+	Msg               string    `description:"xml消息内容"`
+	JumpPath          string    `description:"微信跳转地址"`
+	CreateTime        time.Time `description:"发送时间"`
+}
+
+func (m *WechatListenMsgRecord) TableName() string {
+	return "wechat_listen_msg_record"
+}
+
+// Create 添加记录
+func (m *WechatListenMsgRecord) Create() (err error) {
+	err = global.DEFAULT_MYSQL.Create(m).Error
+
+	return
+}
+
+// GetByJumpPath 根据消息类型和跳转地址获取数据
+func (m *WechatListenMsgRecord) GetByJumpPath(msgType int, jumpPath string) (item *WechatListenMsgRecord, err error) {
+	err = global.DEFAULT_MYSQL.Where(" msg_type = ? AND jump_path = ? ", msgType, jumpPath).
+		Order("wechat_listen_record_id DESC").First(&item).Error
+
+	return
+}

+ 111 - 0
models/wechat_msg_push_record.go

@@ -0,0 +1,111 @@
+package models
+
+import (
+	"eta/eta_menu_sync/global"
+	"time"
+)
+
+// WechatMsgPushRecord 推送给微信的消息
+type WechatMsgPushRecord struct {
+	WechatMsgId         int       `gorm:"primaryKey;column:wechat_msg_id;" description:"消息id"`
+	MsgType             int       `description:"消息类型,1:h5链接;2:小程序,3:文字;4:图片"`
+	WechatGroupId       int       `description:"微信群id"`
+	WechatHelperId      int       `description:"微信小助手id"`
+	WechatGroupWechatId string    `description:"微信群/用户所属的微信标识"`
+	Title               string    `description:"标题"`
+	Content             string    `description:"内容"`
+	Pic                 string    `description:"图片地址"`
+	JumpUrl             string    `description:"跳转地址"`
+	Status              int8      `description:"推送状态,0:未发送,-1:发送失败,1:发送中,2:发送成功,-2:暂停发送"`
+	Remark              string    `description:"备注信息"`
+	ServerResult        string    `description:"推送结果"`
+	ModifyTime          time.Time `description:"修改时间"`
+	CreateTime          time.Time `description:"发送时间"`
+	SendId              int       `description:"同花顺id"`
+	Level               int       `description:"等级,1:紧急,2:优先,3:普通"`
+}
+
+func (m *WechatMsgPushRecord) TableName() string {
+	return "wechat_msg_push_record"
+}
+
+// GetNoPushListByHelperId 根据小助手id获取未发送的列表
+func (m *WechatMsgPushRecord) GetNoPushListByHelperId(helperId, size int) (items []*WechatMsgPushRecord, err error) {
+	err = global.DEFAULT_MYSQL.Where(" status = 0 AND wechat_helper_id = ? ", helperId).Limit(size).
+		Order("wechat_msg_id asc").Find(&items).Error
+
+	return
+}
+
+// WechatMsgPushRecordItem 推送给微信的消息
+type WechatMsgPushRecordItem struct {
+	WechatMsgId         int       `gorm:"primaryKey;column:wechat_msg_id;" description:"消息id"`
+	MsgType             int       `description:"消息类型,1:h5链接;2:小程序,3:文字;4:图片"`
+	WechatGroupId       int       `description:"微信群id"`
+	WechatHelperId      int       `description:"微信小助手id"`
+	WechatGroupWechatId string    `description:"微信群/用户所属的微信标识"`
+	Title               string    `description:"标题"`
+	Content             string    `description:"内容"`
+	Pic                 string    `description:"图片地址"`
+	JumpUrl             string    `description:"跳转地址"`
+	Status              int8      `description:"推送状态,0:未发送,-1:发送失败,1:发送中,2:发送成功"`
+	Remark              string    `description:"备注信息"`
+	ServerResult        string    `description:"推送结果"`
+	ModifyTime          time.Time `description:"修改时间"`
+	CreateTime          time.Time `description:"发送时间"`
+	SendId              int       `description:"同花顺id"`
+}
+
+// GetNoPushListByHelperIdV2 获取研究报告章节详情
+func (m *WechatMsgPushRecord) GetNoPushListByHelperIdV2(helperId, size int) (items []*WechatMsgPushRecordItem, err error) {
+	//	sql := `select a.*,b.wechat_helper_id
+	//FROM wechat_msg_push_record AS a
+	//inner join wechat_group_helper_relation AS b  on a.wechat_group_id = b.wechat_group_id
+	//WHERE status = 0 AND b.wechat_helper_id = ? limit ?`
+
+	sql := `select a.wechat_msg_id,a.msg_type,a.wechat_group_id,a.wechat_group_wechat_id,a.title,a.content,a.pic,a.jump_url,a.status,a.remark,a.server_result,a.modify_time,a.create_time,a.send_id,b.wechat_helper_id
+FROM wechat_msg_push_record AS a
+inner join wechat_group_helper_relation AS b  on a.wechat_group_id = b.wechat_group_id
+WHERE A.status = 0 AND b.wechat_helper_id = ? order by a.level ASC,a.wechat_msg_id ASC limit ?;`
+
+	err = global.DEFAULT_MYSQL.Raw(sql, helperId, size).Scan(&items).Error
+	return
+}
+
+// Update 更新对应字段数据
+func (m *WechatMsgPushRecord) Update(updateCols []string) (err error) {
+	err = global.DEFAULT_MYSQL.Model(m).Select(updateCols).Updates(*m).Error
+
+	return
+}
+
+// UpdateV2 更新对应字段数据
+func (m *WechatMsgPushRecord) UpdateV2(id, wechatHelperId int, status int8, remark, serverResult string) (err error) {
+	sql := `UPDATE wechat_msg_push_record SET wechat_helper_id =? ,status = ?,remark = ?,server_result=?,modify_time=? WHERE wechat_msg_id  = ? `
+	err = global.DEFAULT_MYSQL.Exec(sql, wechatHelperId, status, remark, serverResult, time.Now(), id).Error
+
+	return
+}
+
+// MultiUpdateStatus 批量修改
+func (m *WechatMsgPushRecord) MultiUpdateStatus(idList []int) (err error) {
+	sql := `UPDATE wechat_msg_push_record SET status = 1 WHERE wechat_msg_id in (?) `
+	err = global.DEFAULT_MYSQL.Exec(sql, idList).Error
+
+	return
+}
+
+// CreateList 批量添加入库
+func (m *WechatMsgPushRecord) CreateList(list []WechatMsgPushRecord) (err error) {
+	err = global.DEFAULT_MYSQL.Create(list).Error
+
+	return
+}
+
+// StopTimeOutRecord 暂停超时未发送的记录
+func (m *WechatMsgPushRecord) StopTimeOutRecord(day int) (err error) {
+	sql := `UPDATE wechat_msg_push_record SET status = -2,remark = ? ,modify_time=? WHERE status  = 0 and create_time <=?`
+	err = global.DEFAULT_MYSQL.Exec(sql, `超时未发送`, time.Now(), time.Now().Add(time.Duration(-24*day)*time.Hour)).Error
+
+	return
+}

+ 13 - 0
routers/auth.go

@@ -0,0 +1,13 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller"
+)
+
+func InitAuth(baseGroup *gin.RouterGroup) {
+	//登录
+	authController := new(controller.AuthController)
+	authGroup := baseGroup.Group("auth/")
+	authGroup.POST("login", authController.Login)
+}

+ 13 - 0
routers/index.go

@@ -0,0 +1,13 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/index"
+)
+
+func InitIndex(group *gin.RouterGroup) {
+	// 监听服务
+	indexController := new(index.IndexController)
+	group.GET("server_check", indexController.ServerCheck)
+	group.POST("listen", indexController.Listen)
+}

+ 15 - 0
routers/wechat.go

@@ -0,0 +1,15 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/controller/index"
+	"eta/eta_menu_sync/middleware"
+)
+
+func InitWechat(group *gin.RouterGroup) {
+	group.Use(middleware.Token()) // 这些接口需要鉴权才行
+
+	// 微信助手
+	wechatController := new(index.WechatController)
+	group.POST("sync_helper", wechatController.SyncHelper)
+}

+ 13 - 0
routers/ws.go

@@ -0,0 +1,13 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"eta/eta_menu_sync/global"
+)
+
+func InitWs(group *gin.RouterGroup) {
+	// 将 Socket.IO 服务器附加到 Gin 的 HTTP 路由器上
+	//group.Any("/socket.io/*any", gin.WrapH(global.Ws))
+	group.GET("/socket.io/*any", gin.WrapH(global.Ws))
+	group.POST("/socket.io/*any", gin.WrapH(global.Ws))
+}

+ 35 - 0
services/alarm_msg/alarm_msg.go

@@ -0,0 +1,35 @@
+package alarm_msg
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/http"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/utils"
+)
+
+var (
+	AlarmMsgUrl = "http://47.102.213.75:8606/api/alarm/send"
+)
+
+// SendAlarmMsg
+// msgBody-消息内容
+// level:消息基本,1:提示消息,2:警告消息,3:严重错误信息,默认为1 提示消息
+// email 指定邮箱
+func SendAlarmMsg(msgBody string, level int, email string) {
+	params := make(map[string]interface{})
+	params["ProjectName"] = utils.APPNAME
+	params["RunMode"] = "release" //global.CONFIG.Serve.RunMode
+	params["MsgBody"] = msgBody
+	params["Level"] = level
+	if email != `` {
+		params["Email"] = email
+	}
+	param, err := json.Marshal(params)
+	if err != nil {
+		global.LOG.Critical("SendAlarmMsg json.Marshal Err:" + err.Error())
+		return
+	}
+	r, e := http.Post(AlarmMsgUrl, string(param))
+	fmt.Println("SendAlarmMsg", string(r), e)
+}

+ 24 - 0
services/api_tool.go

@@ -0,0 +1,24 @@
+package services
+
+import (
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/http"
+	"net/url"
+)
+
+const (
+	RefreshUrl = "http://127.0.0.1:7007/mysteel_chemical/refresh"
+)
+
+func MysteelChemicalRefresh(filePath string) {
+	var refreshUrl string
+	//filePathStr:=url.PathEscape(filePath)
+	refreshUrl = RefreshUrl + "?FilePath=" + url.QueryEscape(filePath)
+	fmt.Println("MysteelChemicalRefresh URL:"+refreshUrl)
+	body, err := http.Get(refreshUrl)
+	if err != nil {
+		fmt.Println("MysteelChemicalRefresh Err:" + err.Error())
+		return
+	}
+	fmt.Println("MysteelChemicalRefresh Result:" + string(body))
+}

+ 315 - 0
services/send_to_wx.go

@@ -0,0 +1,315 @@
+package services
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/utils"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+// const BaseUrl = `http://127.0.0.1:7777/DaenWxHook/httpapi/?`
+const BaseUrl = `http://192.168.77.14:7777/DaenWxHook/httpapi/?`
+const BasePath = `/DaenWxHook/httpapi/?`
+
+type WxPushResp struct {
+	Code      int         `json:"code"`
+	Msg       string      `json:"msg"`
+	Result    interface{} `json:"result"`
+	Wxid      string      `json:"wxid"`
+	Port      int         `json:"port"`
+	Pid       int         `json:"pid"`
+	Flag      string      `json:"flag"`
+	Timestamp string      `json:"timestamp"`
+}
+
+type SendReq struct {
+	Type string      `json:"type"`
+	Data interface{} `json:"data"`
+}
+
+// "wxid": "wxid_6895448954512",    //鹏
+//"wxid": "wxid_vgnigr1qaizm22",    //培鼎
+
+// SendH5 发送链接
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+// @param toWxId string 需要发送的微信用户/群的id
+// @param title string h5链接的标题
+// @param picPath string 图片路径,可以是网络图片,也可以是服务器上的本地路径
+// @param content string h5链接上面的分享文案(简介)
+// @param jumpUrl string h5跳转链接
+func SendH5(serverUrl, wxHelperId, toWxId, title, picPath, content, jumpUrl string) (resp WxPushResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	//	"app": "wx64f9cf5b17af074d",
+	requestDataMap["wxid"] = toWxId
+	requestDataMap["path"] = picPath
+	requestDataMap["title"] = title
+	requestDataMap["content"] = content
+	requestDataMap["jumpUrl"] = jumpUrl
+	//requestDataMap["app"] = jumpUrl
+	requestData := SendReq{
+		Type: "Q0012",
+		Data: requestDataMap,
+	}
+	postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+
+	body, err := HttpPost(urlStr, string(postData), true, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal(body, &resp)
+	return
+}
+
+// SendPic 发送图片
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+// @param toWxId string 需要发送的微信用户/群的id
+// @param picPath string 图片路径,可以是网络图片,也可以是服务器上的本地路径
+func SendPic(serverUrl, wxHelperId, toWxId, picPath string) (resp WxPushResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	//	"app": "wx64f9cf5b17af074d",
+	requestDataMap["wxid"] = toWxId
+	requestDataMap["path"] = picPath
+	requestData := SendReq{
+		Type: "Q0010",
+		Data: requestDataMap,
+	}
+	postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+
+	body, err := HttpPost(urlStr, string(postData), true, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal(body, &resp)
+	return
+}
+
+// SendText 发送文字
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+// @param toWxId string 需要发送的微信用户/群的id
+// @param content string 文案内容
+func SendText(serverUrl, wxHelperId, toWxId, content string) (resp WxPushResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	requestDataMap["wxid"] = toWxId
+	requestDataMap["msg"] = content
+	requestData := SendReq{
+		Type: "Q0001",
+		Data: requestDataMap,
+	}
+	postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+
+	body, err := HttpPost(urlStr, string(postData), true, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal(body, &resp)
+	return
+}
+
+// SendXml 发送xml(目前用来发送小程序推送)
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+// @param toWxId string 需要发送的微信用户/群的id
+// @param xml string xml内容
+func SendXml(serverUrl, wxHelperId, toWxId, xml string) (resp WxPushResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	requestDataMap["wxid"] = toWxId
+	requestDataMap["xml"] = xml
+	requestData := SendReq{
+		Type: "Q0015",
+		Data: requestDataMap,
+	}
+	//postData, err := json.Marshal(requestData)
+	//if err != nil {
+	//	return
+	//}
+
+	bf := bytes.NewBuffer([]byte{})
+	jsonEncoder := json.NewEncoder(bf)
+	jsonEncoder.SetEscapeHTML(false)
+	err = jsonEncoder.Encode(requestData)
+	//jsonStr := bf.String()
+	//postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+	//postStr := string(postData)
+	postStr := bf.String()
+
+	body, err := HttpPost(urlStr, postStr, true, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal(body, &resp)
+	return
+}
+
+// Listen 微信状态检测
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+func Listen(serverUrl, wxHelperId string) (resp WxPushResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	requestData := SendReq{
+		Type: "Q0000",
+		Data: requestDataMap,
+	}
+	postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+
+	body, err := HttpPost(urlStr, string(postData), false, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+
+	// 调用函数处理特殊隐藏字符
+	jsonStr := TrimHiddenCharacter(resultStr)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal([]byte(jsonStr), &resp)
+	return
+}
+
+// WxGroupResp 微信群列表返回
+type WxGroupResp struct {
+	Code      int           `json:"code"`
+	Msg       string        `json:"msg"`
+	Result    []GroupResult `json:"result"`
+	Wxid      string        `json:"wxid"`
+	Port      int           `json:"port"`
+	Pid       int           `json:"pid"`
+	Flag      string        `json:"flag"`
+	Timestamp string        `json:"timestamp"`
+}
+
+// GroupResult 群信息
+type GroupResult struct {
+	AvatarMaxUrl           string `json:"avatarMaxUrl"`
+	AvatarMinUrl           string `json:"avatarMinUrl"`
+	City                   string `json:"city"`
+	Country                string `json:"country"`
+	EnBrief                string `json:"enBrief"`
+	EnWhole                string `json:"enWhole"`
+	MemberNum              int    `json:"memberNum"`
+	MomentsBackgroudImgUrl string `json:"momentsBackgroudImgUrl"`
+	Nick                   string `json:"nick"`
+	NickBrief              string `json:"nickBrief"`
+	NickWhole              string `json:"nickWhole"`
+	Province               string `json:"province"`
+	Remark                 string `json:"remark"`
+	RemarkBrief            string `json:"remarkBrief"`
+	RemarkWhole            string `json:"remarkWhole"`
+	Sex                    string `json:"sex"`
+	Sign                   string `json:"sign"`
+	V3                     string `json:"v3"`
+	WxNum                  string `json:"wxNum"`
+	Wxid                   string `json:"wxid"`
+}
+
+// GetAllGroup 获取所有的微信群
+// @param serverUrl string 小助手所在的服务器地址
+// @param wxHelperId string 小助手的微信id
+func GetAllGroup(serverUrl, wxHelperId string) (resp WxGroupResp, resultStr string, err error) {
+	urlStr := "http://" + serverUrl + BasePath + "wxid=" + wxHelperId
+
+	requestDataMap := make(map[string]interface{})
+	requestDataMap["type"] = 2 //1:缓存获取,2:实时获取
+	requestData := SendReq{
+		Type: "Q0006",
+		Data: requestDataMap,
+	}
+	postData, err := json.Marshal(requestData)
+	if err != nil {
+		return
+	}
+
+	body, err := HttpPost(urlStr, string(postData), true, "application/json")
+	if err != nil {
+		return
+	}
+	resultStr = string(body)
+
+	// 调用函数处理特殊隐藏字符
+	jsonStr := TrimHiddenCharacter(resultStr)
+	//fmt.Println(resultStr)
+	err = json.Unmarshal([]byte(jsonStr), &resp)
+	return
+}
+
+func HttpPost(url, postData string, isLog bool, params ...string) ([]byte, error) {
+	body := ioutil.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	contentType := "application/x-www-form-urlencoded;charset=utf-8"
+	if len(params) > 0 && params[0] != "" {
+		contentType = params[0]
+	}
+	req.Header.Set("Content-Type", contentType)
+	req.Header.Set("authorization", utils.MD5(utils.APP_EDB_LIB_NAME_EN+utils.EDB_LIB_Md5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		global.LOG.Info(fmt.Sprint("发送消息失败:request params:", postData, ";err:", err.Error()))
+		return []byte{}, err
+	}
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+
+	if isLog {
+		global.LOG.Info(fmt.Sprint("request params:", postData, ";response:", string(b)))
+	}
+	//fmt.Println("HttpPost:" + string(b))
+	return b, err
+}
+
+// TrimHiddenCharacter 处理特殊隐藏字符
+func TrimHiddenCharacter(originStr string) string {
+	srcRunes := []rune(originStr)
+	dstRunes := make([]rune, 0, len(srcRunes))
+	for _, c := range srcRunes {
+		if c >= 0 && c <= 31 {
+			continue
+		}
+		if c == 127 {
+			continue
+		}
+		dstRunes = append(dstRunes, c)
+	}
+	return string(dstRunes)
+}

+ 1043 - 0
task/add_push_record.go

@@ -0,0 +1,1043 @@
+package task
+
+import (
+	"fmt"
+	"github.com/tealeg/xlsx"
+	"eta/eta_menu_sync/models"
+	"eta/eta_menu_sync/utils"
+	"strings"
+	"time"
+)
+
+// AddMsgPushRecord 定时生成待发送记录
+func AddMsgPushRecord() {
+	//fmt.Println("start task")
+	errMsgList := make([]string, 0)
+	defer func() {
+		if len(errMsgList) > 0 {
+			fmt.Println("定时发送消息失败:")
+			for _, errMsg := range errMsgList {
+				fmt.Println(errMsg)
+			}
+		}
+	}()
+	wechatGroupTagObj := models.WechatGroupTag{}
+	wechatGroupObj := models.WechatGroup{}
+	wechatMsgPushRecordObj := models.WechatMsgPushRecord{}
+
+	reportSendThsDetailObj := models.ReportSendThsDetail{}
+	list, err := reportSendThsDetailObj.GetNoRecordList()
+	if err != nil {
+		fmt.Println("获取同花顺数据失败,err:", err)
+		return
+	}
+
+	wechatListenMsgRecordObj := models.WechatListenMsgRecord{}
+	//fmt.Println("开始处理了")
+	for _, thsRecord := range list {
+		if thsRecord.MsgType == 2 {
+			// 如果类型是:小程序
+			_, tmpErr := wechatListenMsgRecordObj.GetByJumpPath(models.ListenWxAppType, thsRecord.JumpUrl)
+			if tmpErr != nil {
+				if tmpErr != utils.ErrNoRow {
+					fmt.Println("查询小程序推送首发群记录失败,err:", err.Error())
+				}
+				continue
+			}
+		}
+		thsRecord.RecordStatus = 1
+		err = thsRecord.Update([]string{"RecordStatus"})
+
+		if err != nil {
+			fmt.Println("更改同花顺记录失败:", err.Error())
+			continue
+		}
+		labelList := make([]string, 0)
+		if thsRecord.LabelStr != `` {
+			labelList = strings.Split(thsRecord.LabelStr, ",")
+		}
+		if len(labelList) <= 0 {
+			continue
+		}
+		wechatGroupTagList, tmpErr := wechatGroupTagObj.GetListByTagNameList(labelList)
+		if tmpErr != nil {
+			fmt.Println("寻找客户标签失败:", tmpErr.Error())
+			continue
+		}
+
+		wechatGroupIdList := make([]int, 0)
+		for _, wechatGroupTag := range wechatGroupTagList {
+			wechatGroupIdList = append(wechatGroupIdList, wechatGroupTag.WechatGroupId)
+		}
+		if len(wechatGroupIdList) <= 0 {
+			continue
+		}
+
+		wechatGroupList, tmpErr := wechatGroupObj.GetEnabledListByIdList(wechatGroupIdList)
+		if tmpErr != nil {
+			fmt.Println("寻找客户失败:", tmpErr.Error())
+			continue
+		}
+
+		wechatGroupMap := make(map[int]*models.WechatGroup)
+		for _, wechatGroup := range wechatGroupList {
+			wechatGroupMap[wechatGroup.WechatGroupId] = wechatGroup
+		}
+
+		addList := make([]models.WechatMsgPushRecord, 0)
+		for _, wechatGroupTag := range wechatGroupTagList {
+			wechatGroup, ok := wechatGroupMap[wechatGroupTag.WechatGroupId]
+			if !ok {
+				continue
+			}
+			addList = append(addList, models.WechatMsgPushRecord{
+				//WechatMsgId:         0,
+				MsgType:             thsRecord.MsgType,
+				WechatGroupId:       wechatGroupTag.WechatGroupId,
+				WechatHelperId:      0,
+				WechatGroupWechatId: wechatGroup.WechatId,
+				Title:               thsRecord.Title,
+				Content:             thsRecord.Content,
+				Pic:                 thsRecord.Pic,
+				JumpUrl:             thsRecord.JumpUrl,
+				Status:              0,
+				Remark:              "",
+				ServerResult:        "",
+				ModifyTime:          time.Now(),
+				CreateTime:          time.Now(),
+				SendId:              thsRecord.SendId,
+				Level:               thsRecord.Level,
+			})
+
+		}
+		if len(addList) > 0 {
+			err = wechatMsgPushRecordObj.CreateList(addList)
+			if err != nil {
+				fmt.Println("添加入库失败")
+				continue
+			}
+		}
+	}
+
+}
+
+// StopMsgPushRecord 超时未发送的消息需要停止
+func StopMsgPushRecord() {
+	wechatMsgPushRecordObj := models.WechatMsgPushRecord{}
+
+	day := 2
+	err := wechatMsgPushRecordObj.StopTimeOutRecord(day)
+	if err != nil {
+		fmt.Println("获取同花顺数据失败,err:", err)
+		return
+	}
+
+	return
+}
+
+func A() {
+	path := `C:\Users\123\Desktop\标签.xlsx`
+	xlFile, err := xlsx.OpenFile(path)
+	if err != nil {
+		fmt.Println(err.Error())
+		return
+	}
+
+	type tmp struct {
+		Name   string
+		WxId   string
+		Label  string
+		Helper []string
+	}
+	wxGroup := make(map[string]tmp)
+	helperMap := make(map[string]string)
+	// 遍历sheet页读取
+	for _, sheet := range xlFile.Sheets {
+		fmt.Println("sheet name: ", sheet.Name)
+		//遍历行读取
+		maxRow := sheet.MaxRow
+		fmt.Println("maxRow:", maxRow)
+		fmt.Println("maxRow")
+		for i := 0; i < maxRow; i++ {
+
+			if i == 0 {
+				continue
+			}
+			row := sheet.Row(i)
+			//fmt.Println(row)
+			cells := row.Cells
+
+			name := cells[1].Value
+			wxId := cells[2].Value
+			label := cells[3].Value
+			helper := cells[4].Value
+
+			if _, ok := helperMap[helper]; !ok {
+				helperMap[helper] = helper
+			}
+
+			if wxId == `` {
+				continue
+			}
+			tmpData, ok := wxGroup[wxId]
+			var tmpHelper []string
+			if !ok {
+				tmpHelper = make([]string, 0)
+				tmpData = tmp{
+					Name:   name,
+					WxId:   wxId,
+					Label:  label,
+					Helper: tmpHelper,
+				}
+			} else {
+				tmpHelper = tmpData.Helper
+			}
+			tmpHelper = append(tmpHelper, helper)
+			tmpData.Helper = tmpHelper
+			wxGroup[wxId] = tmpData
+			//fmt.Println(name)
+			//fmt.Println(wxId)
+			//fmt.Println(label)
+			//fmt.Println(helper)
+			//return
+
+			//if i == 1 {
+			//	row := sheet.Row(i)
+			//	cells := row.Cells
+			//
+			//	templateFail := false
+			//	if cells[0].Value != "品种分类" {
+			//		templateFail = true
+			//	}
+			//	if cells[1].Value != "录入日期" {
+			//		templateFail = true
+			//	}
+			//	if cells[2].Value != "指标名称" {
+			//		templateFail = true
+			//	}
+			//	if cells[3].Value != "值" {
+			//		templateFail = true
+			//	}
+			//	if cells[4].Value != "频度" {
+			//		templateFail = true
+			//	}
+			//	if cells[5].Value != "单位" {
+			//		templateFail = true
+			//	}
+			//	if templateFail {
+			//		br.ErrMsg = "导入文件异常,请下载最新导入模板文件"
+			//		br.Msg = "导入文件异常,请下载最新导入模板文件"
+			//		return
+			//	}
+			//}
+			//
+			//if i > 1 {
+			//	row := sheet.Row(i)
+			//	cells := row.Cells
+			//	if len(cells) >= 6 {
+			//		classifyName := cells[0].Value //分类
+			//		if classifyName == "" {        //过滤空白行
+			//			continue
+			//		}
+			//		cell1 := cells[1].Value
+			//		//createDate := utils.ConvertToFormatDay(cell1) //录入日期
+			//		createDate := cell1
+			//		cell2 := cells[2].Value //指标名称
+			//		secName := strings.Trim(cell2, " ")
+			//		frequency := cells[4].Value //频度
+			//		unit := cells[5].Value      //单位
+			//
+			//		closeVal := cells[3].Value //值
+			//		if strings.Contains(closeVal, "#N/A") {
+			//			continue
+			//		}
+			//		closeValFloat, err := cells[3].Float() //值
+			//
+			//		if err != nil {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = cells[3].Value
+			//			failItem.Remark = "值类型异常"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//		newDecimal := decimal.NewFromFloat(closeValFloat)
+			//		newDecimal.Round(4)
+			//		closeVal = newDecimal.String()
+			//		if strings.Contains(closeVal, "#N/A") {
+			//			continue
+			//		}
+			//		//closeVal := closeValFloat.
+			//
+			//		//判断指标,类型,等数据是否正常
+			//		classifyName = strings.Trim(classifyName, " ")
+			//		frequency = utils.TrimStr(frequency)
+			//		unit = utils.TrimStr(unit)
+			//
+			//		// 成功数量超过20个指标,那就不导入了
+			//		if len(targetMap) >= 150 {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "导入指标数量过多"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			continue
+			//		}
+			//
+			//		if !strings.Contains(strings.Join(classifyNameStrList, ","), classifyName) {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "没有该品种分类权限"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//		//fmt.Println(classifyName, createDate, secName, closeVal)
+			//
+			//		//校验表格中的日期格式
+			//		if strings.Contains(createDate, "-") {
+			//			//如果是带有 - 的普通日期格式文本
+			//			_, timeErr := time.Parse("2006-1-2", createDate)
+			//			if timeErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//		} else if strings.Contains(createDate, "/") {
+			//			//如果是带有 / 的普通日期格式文本
+			//			createDateTime, timeErr := time.Parse("2006/1/2", createDate)
+			//			if timeErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//			createDate = createDateTime.Format("2006-01-02")
+			//		} else {
+			//			//可能是excel的日期格式
+			//			_, tmpErr := strconv.Atoi(createDate)
+			//			if tmpErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//			createDate = utils.ConvertToFormatDay(createDate) //录入日期
+			//		}
+			//
+			//		//获取指标分类信息
+			//		classify, ok := edbDataClassifyMap[classifyName]
+			//		if !ok {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "指标分类不存在"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			continue
+			//		}
+			//
+			//		//判断指标是否存在
+			//		target, ok := targetMap[secName]
+			//		if !ok {
+			//			tmpTarget, err := models.GetTargetBySecName(secName)
+			//			if err != nil {
+			//				//如果是找不到该指标,那么新增指标
+			//				if err.Error() == utils.ErrNoRow() {
+			//					if frequency == "" {
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败,频度字段为空"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					if unit == "" {
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败,单位字段为空"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					tmpErr := data.AddEdbInfo(secName, unit, frequency, "", sysUser.Mobile, classify.ClassifyId, sysUser.AdminId)
+			//					if tmpErr != nil {
+			//						fmt.Println("line 158")
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					tmpTarget, tmpErr := models.GetTargetBySecName(secName)
+			//					target = tmpTarget
+			//					targetMap[secName] = target
+			//				} else {
+			//					fmt.Println("导入数据 获取指标:Err:" + err.Error())
+			//				}
+			//			} else {
+			//				target = tmpTarget
+			//				targetMap[secName] = target
+			//			}
+			//
+			//			//设置10分钟缓存,不允许其他地方删除
+			//			key := "import:edbinfo:data:" + target.TradeCode
+			//			utils.Rc.SetNX(key, 1, time.Second*600)
+			//		}
+			//
+			//		if target == nil {
+			//			fmt.Println("指标不存在")
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "指标不存在"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//
+			//		//更新指标信息
+			//		updateCols := make([]string, 0)
+			//		//更新指标分类
+			//		if target.ClassifyId <= 0 && classify.ClassifyId > 0 {
+			//			target.ClassifyId = classify.ClassifyId
+			//			updateCols = append(updateCols, "ClassifyId")
+			//		}
+			//		if target.Frequency != frequency {
+			//			target.Frequency = frequency
+			//			target.NoticeTime = ""
+			//			updateCols = append(updateCols, "Frequency", "NoticeTime")
+			//		}
+			//		if target.Unit != unit {
+			//			target.Unit = unit
+			//			updateCols = append(updateCols, "Unit")
+			//		}
+			//		if len(updateCols) > 0 {
+			//			_ = target.Update(updateCols)
+			//		}
+			//
+			//		//判断数据是否已经存在
+			//		count, err := models.GetTargetsDataCount(target.TradeCode, createDate)
+			//		if err != nil {
+			//			go alarm_msg.SendAlarmMsg("导入数据 判断数据是否存在失败,Err:"+err.Error(), 3)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 判断数据是否存在失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//		}
+			//		//数据已存在,进行更新操作
+			//		if count > 0 {
+			//			edbData, err := models.GetTargetsData(target.TradeCode, createDate)
+			//			if err != nil {
+			//				go alarm_msg.SendAlarmMsg("导入数据 获取指标数据失败,Err:"+err.Error(), 3)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取指标数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			}
+			//			if edbData != nil {
+			//				err = models.ModifyTargetsDataByImport(target.TradeCode, createDate, closeVal)
+			//				if err != nil {
+			//					go alarm_msg.SendAlarmMsg("导入数据 修改数据失败,Err:"+err.Error(), 3)
+			//					//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 修改数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				}
+			//			}
+			//		} else { //数据不存在,进行新增操作
+			//			if target.TradeCode != "" && createDate != "" && closeVal != "" {
+			//				models.AddTargetsDataByImport(target.TradeCode, createDate, closeVal)
+			//				if err != nil {
+			//					go alarm_msg.SendAlarmMsg("导入数据 新增数据失败,Err:"+err.Error(), 3)
+			//					//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 新增数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				}
+			//			}
+			//		}
+			//		successCount++
+			//	} else {
+			//		//br.ErrMsg = "请填写导入内容"
+			//		//br.Msg = "请填写导入内容"
+			//		//return
+			//	}
+			//}
+		}
+	}
+
+	//wechatGroupList := make([]models.WechatGroup,0)
+	wechatGroupObj := models.WechatGroup{}
+	wxIdList := make([]string, 0)
+	wxIdMap := make(map[string]string)
+	for _, v := range wxGroup {
+		if _, ok := wxIdMap[v.WxId]; !ok {
+			wxIdList = append(wxIdList, v.WxId)
+			wxIdMap[v.WxId] = ``
+		}
+	}
+
+	if len(wxIdList) <= 0 {
+		fmt.Println("没有群")
+		return
+	}
+
+	groupList, err := wechatGroupObj.GetListByWxIdList(wxIdList)
+	if err != nil {
+		fmt.Println("查找群列表失败,err:", err)
+		return
+	}
+
+	groupMap := make(map[string]*models.WechatGroup)
+	for _, v := range groupList {
+		groupMap[v.WechatId] = v
+	}
+
+	for _, v := range wxGroup {
+		item, ok := groupMap[v.WxId]
+		if !ok {
+			fmt.Println("找不到该群:", v.Name)
+			continue
+		}
+		if len(v.Helper) > 0 {
+			for _, helper := range v.Helper {
+				if helper != `弘则FICC小助手17号` {
+					continue
+				}
+				if helperId, ok := helperIdMap[helper]; ok {
+					tmpWechatGroupHelperRelation := &models.WechatGroupHelperRelation{
+						WechatGroupId:  item.WechatGroupId,
+						WechatHelperId: helperId,
+					}
+
+					fmt.Println(tmpWechatGroupHelperRelation)
+					err = tmpWechatGroupHelperRelation.Create()
+					if err != nil {
+						fmt.Println(v.WxId, ";", v.Name, ";助手:", helper, "添加关系失败,ERR:", err)
+					}
+				}
+			}
+		}
+	}
+	fmt.Println(wxGroup)
+
+}
+
+func B() {
+	path := `C:\Users\123\Desktop\标签.xlsx`
+	xlFile, err := xlsx.OpenFile(path)
+	if err != nil {
+		fmt.Println(err.Error())
+		return
+	}
+
+	type tmp struct {
+		Name   string
+		WxId   string
+		Label  string
+		Helper []string
+	}
+	wxGroup := make(map[string]tmp)
+	helperMap := make(map[string]string)
+	// 遍历sheet页读取
+	for _, sheet := range xlFile.Sheets {
+		fmt.Println("sheet name: ", sheet.Name)
+		//遍历行读取
+		maxRow := sheet.MaxRow
+		fmt.Println("maxRow:", maxRow)
+		fmt.Println("maxRow")
+		for i := 0; i < maxRow; i++ {
+
+			if i == 0 {
+				continue
+			}
+			row := sheet.Row(i)
+			//fmt.Println(row)
+			cells := row.Cells
+
+			name := cells[1].Value
+			wxId := cells[2].Value
+			label := cells[3].Value
+			helper := cells[4].Value
+
+			if _, ok := helperMap[helper]; !ok {
+				helperMap[helper] = helper
+			}
+
+			if wxId == `` {
+				continue
+			}
+			tmpData, ok := wxGroup[wxId]
+			var tmpHelper []string
+			if !ok {
+				tmpHelper = make([]string, 0)
+				tmpData = tmp{
+					Name:   name,
+					WxId:   wxId,
+					Label:  label,
+					Helper: tmpHelper,
+				}
+			} else {
+				tmpHelper = tmpData.Helper
+			}
+			tmpHelper = append(tmpHelper, helper)
+			tmpData.Helper = tmpHelper
+			wxGroup[wxId] = tmpData
+			//fmt.Println(name)
+			//fmt.Println(wxId)
+			//fmt.Println(label)
+			//fmt.Println(helper)
+			//return
+
+			//if i == 1 {
+			//	row := sheet.Row(i)
+			//	cells := row.Cells
+			//
+			//	templateFail := false
+			//	if cells[0].Value != "品种分类" {
+			//		templateFail = true
+			//	}
+			//	if cells[1].Value != "录入日期" {
+			//		templateFail = true
+			//	}
+			//	if cells[2].Value != "指标名称" {
+			//		templateFail = true
+			//	}
+			//	if cells[3].Value != "值" {
+			//		templateFail = true
+			//	}
+			//	if cells[4].Value != "频度" {
+			//		templateFail = true
+			//	}
+			//	if cells[5].Value != "单位" {
+			//		templateFail = true
+			//	}
+			//	if templateFail {
+			//		br.ErrMsg = "导入文件异常,请下载最新导入模板文件"
+			//		br.Msg = "导入文件异常,请下载最新导入模板文件"
+			//		return
+			//	}
+			//}
+			//
+			//if i > 1 {
+			//	row := sheet.Row(i)
+			//	cells := row.Cells
+			//	if len(cells) >= 6 {
+			//		classifyName := cells[0].Value //分类
+			//		if classifyName == "" {        //过滤空白行
+			//			continue
+			//		}
+			//		cell1 := cells[1].Value
+			//		//createDate := utils.ConvertToFormatDay(cell1) //录入日期
+			//		createDate := cell1
+			//		cell2 := cells[2].Value //指标名称
+			//		secName := strings.Trim(cell2, " ")
+			//		frequency := cells[4].Value //频度
+			//		unit := cells[5].Value      //单位
+			//
+			//		closeVal := cells[3].Value //值
+			//		if strings.Contains(closeVal, "#N/A") {
+			//			continue
+			//		}
+			//		closeValFloat, err := cells[3].Float() //值
+			//
+			//		if err != nil {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = cells[3].Value
+			//			failItem.Remark = "值类型异常"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//		newDecimal := decimal.NewFromFloat(closeValFloat)
+			//		newDecimal.Round(4)
+			//		closeVal = newDecimal.String()
+			//		if strings.Contains(closeVal, "#N/A") {
+			//			continue
+			//		}
+			//		//closeVal := closeValFloat.
+			//
+			//		//判断指标,类型,等数据是否正常
+			//		classifyName = strings.Trim(classifyName, " ")
+			//		frequency = utils.TrimStr(frequency)
+			//		unit = utils.TrimStr(unit)
+			//
+			//		// 成功数量超过20个指标,那就不导入了
+			//		if len(targetMap) >= 150 {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "导入指标数量过多"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			continue
+			//		}
+			//
+			//		if !strings.Contains(strings.Join(classifyNameStrList, ","), classifyName) {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "没有该品种分类权限"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//		//fmt.Println(classifyName, createDate, secName, closeVal)
+			//
+			//		//校验表格中的日期格式
+			//		if strings.Contains(createDate, "-") {
+			//			//如果是带有 - 的普通日期格式文本
+			//			_, timeErr := time.Parse("2006-1-2", createDate)
+			//			if timeErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//		} else if strings.Contains(createDate, "/") {
+			//			//如果是带有 / 的普通日期格式文本
+			//			createDateTime, timeErr := time.Parse("2006/1/2", createDate)
+			//			if timeErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//			createDate = createDateTime.Format("2006-01-02")
+			//		} else {
+			//			//可能是excel的日期格式
+			//			_, tmpErr := strconv.Atoi(createDate)
+			//			if tmpErr != nil {
+			//				failItem := new(models.EdbdataImportFail)
+			//				failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//				failItem.ClassifyName = classifyName
+			//				failItem.CreateDate = createDate
+			//				failItem.SecName = secName
+			//				failItem.Close = closeVal
+			//				failItem.Remark = "日期格式异常"
+			//				failItem.Frequency = frequency
+			//				failItem.Unit = unit
+			//				failDatas = append(failDatas, failItem)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				continue
+			//			}
+			//			createDate = utils.ConvertToFormatDay(createDate) //录入日期
+			//		}
+			//
+			//		//获取指标分类信息
+			//		classify, ok := edbDataClassifyMap[classifyName]
+			//		if !ok {
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "指标分类不存在"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取分类:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			continue
+			//		}
+			//
+			//		//判断指标是否存在
+			//		target, ok := targetMap[secName]
+			//		if !ok {
+			//			tmpTarget, err := models.GetTargetBySecName(secName)
+			//			if err != nil {
+			//				//如果是找不到该指标,那么新增指标
+			//				if err.Error() == utils.ErrNoRow() {
+			//					if frequency == "" {
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败,频度字段为空"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					if unit == "" {
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败,单位字段为空"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					tmpErr := data.AddEdbInfo(secName, unit, frequency, "", sysUser.Mobile, classify.ClassifyId, sysUser.AdminId)
+			//					if tmpErr != nil {
+			//						fmt.Println("line 158")
+			//						failItem := new(models.EdbdataImportFail)
+			//						failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//						failItem.ClassifyName = classifyName
+			//						failItem.CreateDate = createDate
+			//						failItem.SecName = secName
+			//						failItem.Close = closeVal
+			//						failItem.Remark = "新增指标失败"
+			//						failItem.Frequency = frequency
+			//						failItem.Unit = unit
+			//						failDatas = append(failDatas, failItem)
+			//						continue
+			//					}
+			//					tmpTarget, tmpErr := models.GetTargetBySecName(secName)
+			//					target = tmpTarget
+			//					targetMap[secName] = target
+			//				} else {
+			//					fmt.Println("导入数据 获取指标:Err:" + err.Error())
+			//				}
+			//			} else {
+			//				target = tmpTarget
+			//				targetMap[secName] = target
+			//			}
+			//
+			//			//设置10分钟缓存,不允许其他地方删除
+			//			key := "import:edbinfo:data:" + target.TradeCode
+			//			utils.Rc.SetNX(key, 1, time.Second*600)
+			//		}
+			//
+			//		if target == nil {
+			//			fmt.Println("指标不存在")
+			//			failItem := new(models.EdbdataImportFail)
+			//			failItem.SysUserId = strconv.Itoa(sysUser.AdminId)
+			//			failItem.ClassifyName = classifyName
+			//			failItem.CreateDate = createDate
+			//			failItem.SecName = secName
+			//			failItem.Close = closeVal
+			//			failItem.Remark = "指标不存在"
+			//			failItem.Frequency = frequency
+			//			failItem.Unit = unit
+			//			failDatas = append(failDatas, failItem)
+			//			continue
+			//		}
+			//
+			//		//更新指标信息
+			//		updateCols := make([]string, 0)
+			//		//更新指标分类
+			//		if target.ClassifyId <= 0 && classify.ClassifyId > 0 {
+			//			target.ClassifyId = classify.ClassifyId
+			//			updateCols = append(updateCols, "ClassifyId")
+			//		}
+			//		if target.Frequency != frequency {
+			//			target.Frequency = frequency
+			//			target.NoticeTime = ""
+			//			updateCols = append(updateCols, "Frequency", "NoticeTime")
+			//		}
+			//		if target.Unit != unit {
+			//			target.Unit = unit
+			//			updateCols = append(updateCols, "Unit")
+			//		}
+			//		if len(updateCols) > 0 {
+			//			_ = target.Update(updateCols)
+			//		}
+			//
+			//		//判断数据是否已经存在
+			//		count, err := models.GetTargetsDataCount(target.TradeCode, createDate)
+			//		if err != nil {
+			//			go alarm_msg.SendAlarmMsg("导入数据 判断数据是否存在失败,Err:"+err.Error(), 3)
+			//			//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 判断数据是否存在失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//		}
+			//		//数据已存在,进行更新操作
+			//		if count > 0 {
+			//			edbData, err := models.GetTargetsData(target.TradeCode, createDate)
+			//			if err != nil {
+			//				go alarm_msg.SendAlarmMsg("导入数据 获取指标数据失败,Err:"+err.Error(), 3)
+			//				//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 获取指标数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//			}
+			//			if edbData != nil {
+			//				err = models.ModifyTargetsDataByImport(target.TradeCode, createDate, closeVal)
+			//				if err != nil {
+			//					go alarm_msg.SendAlarmMsg("导入数据 修改数据失败,Err:"+err.Error(), 3)
+			//					//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 修改数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				}
+			//			}
+			//		} else { //数据不存在,进行新增操作
+			//			if target.TradeCode != "" && createDate != "" && closeVal != "" {
+			//				models.AddTargetsDataByImport(target.TradeCode, createDate, closeVal)
+			//				if err != nil {
+			//					go alarm_msg.SendAlarmMsg("导入数据 新增数据失败,Err:"+err.Error(), 3)
+			//					//go utils.SendEmail(utils.APPNAME+"失败提醒", "导入数据 新增数据失败:Err:"+err.Error(), utils.EmailSendToUsers)
+			//				}
+			//			}
+			//		}
+			//		successCount++
+			//	} else {
+			//		//br.ErrMsg = "请填写导入内容"
+			//		//br.Msg = "请填写导入内容"
+			//		//return
+			//	}
+			//}
+		}
+	}
+
+	//wechatGroupList := make([]models.WechatGroup,0)
+	//wechatGroupObj := models.WechatGroup{}
+	//for _, v := range wxGroup {
+	//	item, tmpErr := wechatGroupObj.GetByWxId(v.WxId)
+	//	if tmpErr != nil {
+	//		fmt.Println(v.WxId, ";", v.Name, "查找失败,ERR:", err)
+	//		continue
+	//	}
+	//	if len(v.Helper) > 0 {
+	//		for _, helper := range v.Helper {
+	//			if helper != `弘则FICC小助手17号` {
+	//				continue
+	//			}
+	//			if helperId, ok := helperIdMap[helper]; ok {
+	//				tmpWechatGroupHelperRelation := &models.WechatGroupHelperRelation{
+	//					WechatGroupId:  item.WechatGroupId,
+	//					WechatHelperId: helperId,
+	//				}
+	//
+	//				fmt.Println(tmpWechatGroupHelperRelation)
+	//				//err = tmpWechatGroupHelperRelation.Create()
+	//				//if err != nil {
+	//				//	fmt.Println(v.WxId, ";", v.Name, ";助手:", helper, "添加关系失败,ERR:", err)
+	//				//}
+	//			}
+	//		}
+	//	}
+	//
+	//	//tmpWechatGroup := &models.WechatGroup{
+	//	//	WechatGroupId:   0,
+	//	//	WechatGroupName: v.Name,
+	//	//	WechatId:        v.WxId,
+	//	//	ModifyTime:      time.Now(),
+	//	//	CreateTime:      time.Now(),
+	//	//}
+	//	//
+	//	//err = tmpWechatGroup.Create()
+	//	//if err != nil {
+	//	//	global.LOG.Info("添加监听消息失败")
+	//	//}
+	//	//
+	//	//if v.Label == `` {
+	//	//	continue
+	//	//}
+	//	//labelList := strings.Split(v.Label, ",")
+	//	//
+	//	//for _, label := range labelList {
+	//	//	tmpTag := models.WechatGroupTag{
+	//	//		WechatGroupId: tmpWechatGroup.WechatGroupId,
+	//	//		TagName:       label,
+	//	//		ModifyTime:    time.Now(),
+	//	//		CreateTime:    time.Now(),
+	//	//	}
+	//	//	tmpTag.Create()
+	//	//}
+	//}
+	fmt.Println(wxGroup)
+
+	fmt.Println("a")
+}
+
+var helperIdMap = map[string]int{
+	"弘则FICC小助手16号": 9,
+	"弘则FICC小助手18号": 11,
+	"弘则FICC小助手1号":  2,
+	//"弘则FICC小助手6号":  9,
+	//"弘则FICC小助手14号": 9,
+	"弘则FICC小助手5号": 8,
+	//"弘则FICC小助手9号": 2,
+	"弘则FICC小助手7号":  5,
+	"弘则FICC小助手3号":  7,
+	"弘则FICC小助手15号": 6,
+	"弘则FICC小助手2号":  3,
+	"弘则FICC小助手8号":  1,
+	"弘则FICC小助手17号": 12,
+	"弘则FICC小助手4号":  10,
+}

+ 196 - 0
task/send_wechat_msg.go

@@ -0,0 +1,196 @@
+package task
+
+import (
+	"context"
+	"fmt"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/models"
+	"eta/eta_menu_sync/services"
+	"eta/eta_menu_sync/services/alarm_msg"
+	"eta/eta_menu_sync/utils"
+	"strings"
+	"time"
+)
+
+func SendMsg() {
+	//fmt.Println("start task")
+	errMsgList := make([]string, 0)
+	defer func() {
+		if len(errMsgList) > 0 {
+			fmt.Println("定时发送消息失败:")
+			for _, errMsg := range errMsgList {
+				fmt.Println(errMsg)
+			}
+		}
+	}()
+	wechatHelperObj := models.WechatHelper{}
+	list, err := wechatHelperObj.GetAllHelper()
+	if err != nil {
+		fmt.Println("err:", err)
+		return
+	}
+
+	wechatMsgPushRecordObj := models.WechatMsgPushRecord{}
+	for _, helper := range list {
+		//fmt.Println(helper)
+		recordList, tmpErr := wechatMsgPushRecordObj.GetNoPushListByHelperIdV2(helper.WechatHelperId, helper.Num)
+		if tmpErr != nil {
+			errMsgList = append(errMsgList, fmt.Sprint(helper.WechatHelperName, "发送微信消息失败,获取未发送记录失败,Err:", tmpErr.Error()))
+			continue
+		}
+
+		// 如果没有待推送数据,那么就退出当前循环,进入下一循环
+		if len(recordList) <= 0 {
+			continue
+		}
+
+		{
+			recordIdList := make([]int, 0)
+			for _, record := range recordList {
+				recordIdList = append(recordIdList, record.WechatMsgId)
+			}
+			tmpErr = wechatMsgPushRecordObj.MultiUpdateStatus(recordIdList)
+			if tmpErr != nil {
+				errMsgList = append(errMsgList, fmt.Sprint(helper.WechatHelperName, "发送微信消息失败,变更消息发送中状态失败,Err:", tmpErr.Error()))
+				continue
+			}
+		}
+
+		go PushWxMsgList(recordList, helper)
+	}
+}
+
+// PushWxMsgList 处理发送消息列表
+func PushWxMsgList(list []*models.WechatMsgPushRecordItem, helper *models.WechatHelper) {
+	num := len(list)
+	// 平均延迟时间
+	avgDelayTime := 60 / num
+	// 最小延迟时间
+	minDelayTime := 5
+	if avgDelayTime < 2 {
+		minDelayTime = 0
+	} else if avgDelayTime <= 6 {
+		minDelayTime = 3
+	}
+
+	//fmt.Println("minDelayTime:", minDelayTime)
+	//fmt.Println("avgDelayTime:", avgDelayTime)
+	for _, record := range list {
+		// 发送消息
+		go PushWxMsg(record, helper)
+		// 本次延迟时间
+		currDelayTime := utils.GetRandInt(minDelayTime, avgDelayTime)
+		fmt.Println("下一条消息时间是 ", currDelayTime, " 秒后")
+		time.Sleep(time.Duration(currDelayTime) * time.Second)
+	}
+}
+
+// PushWxMsg 真正开始处理推送消息业务
+func PushWxMsg(record *models.WechatMsgPushRecordItem, wxHelper *models.WechatHelper) {
+	fmt.Println(time.Now().Format(utils.FormatDateTime), " :", record.WechatMsgId, "开始推送了")
+	var errMsg, resultStr string
+	defer func() {
+		record.ModifyTime = time.Now()
+		record.Status = 2
+		record.Remark = errMsg
+		record.ServerResult = resultStr
+		record.WechatHelperId = wxHelper.WechatHelperId
+		if errMsg != `` { // 发送失败了
+			record.Status = -1
+			// TODO 发送邮件提醒?
+		} else {
+			record.Remark = `发送成功`
+		}
+		obj := models.WechatMsgPushRecord{}
+		err := obj.UpdateV2(record.WechatMsgId, record.WechatHelperId, record.Status, record.Remark, record.ServerResult)
+		if err != nil {
+			fmt.Println("err:", err)
+		}
+		//record.Update([]string{"Status", "Remark", "ServerResult", "ModifyTime"})
+	}()
+	var resp services.WxPushResp
+	var err error
+	switch record.MsgType { // 消息类型,1:h5链接;2:小程序,3:文字;4:图片
+	case 1: // h5链接
+		if record.Content == `` {
+			errMsg = `发送数据内容为空`
+			return
+		}
+		resp, resultStr, err = services.SendH5(wxHelper.ServerUrl, wxHelper.WechatId, record.WechatGroupWechatId, record.Title, record.Pic, record.Content, record.JumpUrl)
+	case 2: // 小程序
+		//GetByJumpPath
+		wechatListenMsgRecordObj := models.WechatListenMsgRecord{}
+		listenMsg, tmpErr := wechatListenMsgRecordObj.GetByJumpPath(models.ListenWxAppType, record.JumpUrl)
+		if tmpErr != nil {
+			if tmpErr != utils.ErrNoRow {
+				err = tmpErr
+			}
+			break
+		}
+
+		// 替换消息内容,将原来内容里面的 发送人微信标识 替换为 小助手微信标识
+		xml := strings.Replace(listenMsg.Msg, listenMsg.FinalFromWechatId, wxHelper.WechatId, -1)
+
+		resp, resultStr, err = services.SendXml(wxHelper.ServerUrl, wxHelper.WechatId, record.WechatGroupWechatId, xml)
+	case 3: // 文字
+		if record.Content == `` {
+			errMsg = `发送数据内容为空`
+			return
+		}
+		resp, resultStr, err = services.SendText(wxHelper.ServerUrl, wxHelper.WechatId, record.WechatGroupWechatId, record.Content)
+	case 4: // 图片
+		resp, resultStr, err = services.SendPic(wxHelper.ServerUrl, wxHelper.WechatId, record.WechatGroupWechatId, record.Pic)
+	default:
+		errMsg = `错误的推送类型`
+		return
+	}
+	if err != nil {
+		errMsg = "发送消息给微信失败,ERR:" + err.Error()
+		return
+	}
+	if resp.Code != 200 {
+		errMsg = "发送消息给微信,服务返回失败信息,ERR:" + resp.Msg
+		return
+	}
+
+	return
+}
+
+func ListenWechatOnline() {
+	offlineHelperList := make([]*models.WechatHelper, 0)
+	baseKey := `listen:wechat_helper:`
+	defer func() {
+		if len(offlineHelperList) > 0 {
+			errMsgList := make([]string, 0)
+			for _, helper := range offlineHelperList {
+				result := global.Redis.SetNX(context.TODO(), fmt.Sprint(baseKey, helper.WechatId), 1, 30*time.Minute)
+				if result.Val() {
+					errMsgList = append(errMsgList, fmt.Sprint("机器人:", helper.WechatHelperName, ";所属服务器:", helper.ServerUrl))
+				}
+			}
+			if len(errMsgList) > 0 {
+				email := `984198890@qq.com;317699326@qq.com;pdzhao@hzinsights.com`
+				go alarm_msg.SendAlarmMsg(time.Now().Format(utils.FormatDateTime)+":微信小助手掉线检测:<br/>"+strings.Join(errMsgList, "<br/>"), 3, email)
+			}
+		}
+	}()
+	wechatHelperObj := models.WechatHelper{}
+	list, err := wechatHelperObj.GetAllHelper()
+	if err != nil {
+		fmt.Println("err:", err)
+		return
+	}
+
+	for _, helper := range list {
+		resp, _, tmpErr := services.Listen(helper.ServerUrl, helper.WechatId)
+		if tmpErr != nil {
+			offlineHelperList = append(offlineHelperList, helper)
+			continue
+		}
+		if resp.Code != 200 {
+			offlineHelperList = append(offlineHelperList, helper)
+			continue
+		}
+
+	}
+}

+ 72 - 0
task/task.go

@@ -0,0 +1,72 @@
+package task
+
+import (
+	"fmt"
+	"eta/eta_menu_sync/global"
+	"eta/eta_menu_sync/services/alarm_msg"
+	"eta/eta_menu_sync/utils"
+	"os"
+	"runtime"
+	"sync"
+	"time"
+)
+
+type TaskFunc func(params ...interface{})
+
+var taskList chan *Executor //任务列表
+
+var once sync.Once
+
+func GetTaskList() chan *Executor {
+	once.Do(func() {
+		taskList = make(chan *Executor, 1000)
+	})
+	return taskList
+}
+
+type Executor struct {
+	f      TaskFunc
+	params []interface{}
+}
+
+func (e *Executor) Exec() { //执行任务
+	go func() {
+		defer func() {
+			if err := recover(); err != any(nil) {
+				stack := ""
+				msg := fmt.Sprintf("当前进程pid:%d; 父进程ppid:%d", os.Getpid(), os.Getppid())
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				msg = fmt.Sprintf("The params data is %v", e.params)
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				msg = fmt.Sprintf("Handler crashed with error %v", err)
+				stack += msg + "</br>"
+				global.LOG.Critical(msg)
+				for i := 1; ; i++ {
+					_, file, line, ok := runtime.Caller(i)
+					if !ok {
+						break
+					}
+					global.LOG.Critical(fmt.Sprintf("%s:%d", file, line))
+					stack = stack + fmt.Sprintln(fmt.Sprintf("%s:%d</br>", file, line))
+				}
+				fmt.Println("stack:", stack)
+				//go services.SendEmail(utils.APPNAME+"崩了"+time.Now().Format("2006-01-02 15:04:05"), stack, utils.EmailSendToUsers)
+				go alarm_msg.SendAlarmMsg(utils.APPNAME+"崩了"+time.Now().Format("2006-01-02 15:04:05")+";Msg:"+stack, 2, ``)
+			}
+		}()
+		//time.Sleep(60*time.Second)
+		//fmt.Println("i am here new")
+		e.f(e.params...)
+	}()
+
+}
+
+func NewExecutor(f TaskFunc, params []interface{}) *Executor {
+	return &Executor{f: f, params: params}
+}
+
+func Task(f TaskFunc, params ...interface{}) {
+	taskList <- NewExecutor(f, params)
+}

+ 1070 - 0
utils/common.go

@@ -0,0 +1,1070 @@
+package utils
+
+import (
+	"bufio"
+	"crypto/md5"
+	"crypto/sha1"
+	"encoding/base64"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"gorm.io/gorm"
+	"image"
+	"image/png"
+	"io"
+	"math"
+	"math/rand"
+	"net/http"
+	"os"
+	"os/exec"
+	"path"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var ErrNoRow = gorm.ErrRecordNotFound
+
+/*var (
+	KEY = []byte("OsL5s35Xv6")
+)*/
+
+// 发放token
+func GenToken(account string) (accessToken string, err error) {
+	/*token := jwt.New(jwt.SigningMethodHS256)
+	token.Claims = &jwt.StandardClaims{
+		NotBefore: int64(time.Now().Unix()),
+		ExpiresAt: int64(time.Now().Unix() + 90*24*60*60),
+		Issuer:    "hongze_wx_listen",
+		Subject:   account,
+	}
+	accessToken, err = token.SignedString(KEY)*/
+	timeUnix := time.Now().Unix()
+	timeUnixStr := strconv.FormatInt(timeUnix, 10)
+	accessToken = MD5(account) + MD5(timeUnixStr)
+	return
+}
+
+//随机数种子
+var rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
+
+// GetRandString 获取随机字符串
+func GetRandString(size int) string {
+	allLetterDigit := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "!", "@", "#", "$", "%", "^", "&", "*"}
+	randomSb := ""
+	digitSize := len(allLetterDigit)
+	for i := 0; i < size; i++ {
+		randomSb += allLetterDigit[rnd.Intn(digitSize)]
+	}
+	return randomSb
+}
+
+// GetRandStringNoSpecialChar
+func GetRandStringNoSpecialChar(size int) string {
+	allLetterDigit := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
+	randomSb := ""
+	digitSize := len(allLetterDigit)
+	for i := 0; i < size; i++ {
+		randomSb += allLetterDigit[rnd.Intn(digitSize)]
+	}
+	return randomSb
+}
+
+// StringsToJSON
+func StringsToJSON(str string) string {
+	rs := []rune(str)
+	jsons := ""
+	for _, r := range rs {
+		rint := int(r)
+		if rint < 128 {
+			jsons += string(r)
+		} else {
+			jsons += "\\u" + strconv.FormatInt(int64(rint), 16) // json
+		}
+	}
+	return jsons
+}
+
+// ToString 序列化
+func ToString(v interface{}) string {
+	data, _ := json.Marshal(v)
+	return string(data)
+}
+
+// MD5 md5加密
+func MD5(data string) string {
+	m := md5.Sum([]byte(data))
+	return hex.EncodeToString(m[:])
+}
+
+// GetRandDigit 获取数字随机字符
+func GetRandDigit(n int) string {
+	return fmt.Sprintf("%0"+strconv.Itoa(n)+"d", rnd.Intn(int(math.Pow10(n))))
+}
+
+// GetRandNumber 获取随机数
+func GetRandNumber(n int) int {
+	return rnd.Intn(n)
+}
+
+// GetRandInt
+func GetRandInt(min, max int) int {
+	if min >= max || min == 0 || max == 0 {
+		return max
+	}
+	return rand.Intn(max-min) + min
+}
+
+// GetToday 获取今天的随机字符
+func GetToday(format string) string {
+	today := time.Now().Format(format)
+	return today
+}
+
+// GetTodayLastSecond 获取今天剩余秒数
+func GetTodayLastSecond() time.Duration {
+	today := GetToday(FormatDate) + " 23:59:59"
+	end, _ := time.ParseInLocation(FormatDateTime, today, time.Local)
+	return time.Duration(end.Unix()-time.Now().Local().Unix()) * time.Second
+}
+
+// GetBrithDate 处理出生日期函数
+func GetBrithDate(idcard string) string {
+	l := len(idcard)
+	var s string
+	if l == 15 {
+		s = "19" + idcard[6:8] + "-" + idcard[8:10] + "-" + idcard[10:12]
+		return s
+	}
+	if l == 18 {
+		s = idcard[6:10] + "-" + idcard[10:12] + "-" + idcard[12:14]
+		return s
+	}
+	return GetToday(FormatDate)
+}
+
+// WhichSexByIdcard 处理性别
+func WhichSexByIdcard(idcard string) string {
+	var sexs = [2]string{"女", "男"}
+	length := len(idcard)
+	if length == 18 {
+		sex, _ := strconv.Atoi(string(idcard[16]))
+		return sexs[sex%2]
+	} else if length == 15 {
+		sex, _ := strconv.Atoi(string(idcard[14]))
+		return sexs[sex%2]
+	}
+	return "男"
+}
+
+// SubFloatToString 截取小数点后几位
+func SubFloatToString(f float64, m int) string {
+	n := strconv.FormatFloat(f, 'f', -1, 64)
+	if n == "" {
+		return ""
+	}
+	if m >= len(n) {
+		return n
+	}
+	newn := strings.Split(n, ".")
+	if m == 0 {
+		return newn[0]
+	}
+	if len(newn) < 2 || m >= len(newn[1]) {
+		return n
+	}
+	return newn[0] + "." + newn[1][:m]
+}
+
+// SubFloatToFloat 截取小数点后几位
+func SubFloatToFloat(f float64, m int) float64 {
+	newn := SubFloatToString(f, m)
+	newf, _ := strconv.ParseFloat(newn, 64)
+	return newf
+}
+
+// SubFloatToFloatStr 截取小数点后几位
+func SubFloatToFloatStr(f float64, m int) string {
+	newn := SubFloatToString(f, m)
+	return newn
+}
+
+// GetYearDiffer 获取相差时间-年
+func GetYearDiffer(start_time, end_time string) int {
+	t1, _ := time.ParseInLocation("2006-01-02", start_time, time.Local)
+	t2, _ := time.ParseInLocation("2006-01-02", end_time, time.Local)
+	age := t2.Year() - t1.Year()
+	if t2.Month() < t1.Month() || (t2.Month() == t1.Month() && t2.Day() < t1.Day()) {
+		age--
+	}
+	return age
+}
+
+// GetSecondDifferByTime 获取相差时间-秒
+func GetSecondDifferByTime(start_time, end_time time.Time) int64 {
+	diff := end_time.Unix() - start_time.Unix()
+	return diff
+}
+
+// FixFloat
+func FixFloat(f float64, m int) float64 {
+	newn := SubFloatToString(f+0.00000001, m)
+	newf, _ := strconv.ParseFloat(newn, 64)
+	return newf
+}
+
+// StrListToString 将字符串数组转化为逗号分割的字符串形式  ["str1","str2","str3"] >>> "str1,str2,str3"
+func StrListToString(strList []string) (str string) {
+	if len(strList) > 0 {
+		for k, v := range strList {
+			if k == 0 {
+				str = v
+			} else {
+				str = str + "," + v
+			}
+		}
+		return
+	}
+	return ""
+}
+
+// ValidateEmailFormatat 校验邮箱格式
+func ValidateEmailFormatat(email string) bool {
+	reg := regexp.MustCompile(RegularEmail)
+	return reg.MatchString(email)
+}
+
+// ValidateMobileFormatat 验证是否是手机号
+func ValidateMobileFormatat(mobileNum string) bool {
+	reg := regexp.MustCompile(RegularMobile)
+	return reg.MatchString(mobileNum)
+}
+
+// FileIsExist 判断文件是否存在
+func FileIsExist(filePath string) bool {
+	_, err := os.Stat(filePath)
+	return err == nil || os.IsExist(err)
+}
+
+// GetImgExt 获取图片扩展名
+func GetImgExt(file string) (ext string, err error) {
+	var headerByte []byte
+	headerByte = make([]byte, 8)
+	fd, err := os.Open(file)
+	if err != nil {
+		return "", err
+	}
+	defer fd.Close()
+	_, err = fd.Read(headerByte)
+	if err != nil {
+		return "", err
+	}
+	xStr := fmt.Sprintf("%x", headerByte)
+	switch {
+	case xStr == "89504e470d0a1a0a":
+		ext = ".png"
+	case xStr == "0000010001002020":
+		ext = ".ico"
+	case xStr == "0000020001002020":
+		ext = ".cur"
+	case xStr[:12] == "474946383961" || xStr[:12] == "474946383761":
+		ext = ".gif"
+	case xStr[:10] == "0000020000" || xStr[:10] == "0000100000":
+		ext = ".tga"
+	case xStr[:8] == "464f524d":
+		ext = ".iff"
+	case xStr[:8] == "52494646":
+		ext = ".ani"
+	case xStr[:4] == "4d4d" || xStr[:4] == "4949":
+		ext = ".tiff"
+	case xStr[:4] == "424d":
+		ext = ".bmp"
+	case xStr[:4] == "ffd8":
+		ext = ".jpg"
+	case xStr[:2] == "0a":
+		ext = ".pcx"
+	default:
+		ext = ""
+	}
+	return ext, nil
+}
+
+// SaveImage 保存图片
+func SaveImage(path string, img image.Image) (err error) {
+	//需要保持的文件
+	imgfile, err := os.Create(path)
+	defer imgfile.Close()
+	// 以PNG格式保存文件
+	err = png.Encode(imgfile, img)
+	return err
+}
+
+// DownloadImage 下载图片
+func DownloadImage(imgUrl string) (filePath string, err error) {
+	imgPath := "./static/imgs/"
+	fileName := path.Base(imgUrl)
+	res, err := http.Get(imgUrl)
+	if err != nil {
+		fmt.Println("A error occurred!")
+		return
+	}
+	defer res.Body.Close()
+	// 获得get请求响应的reader对象
+	reader := bufio.NewReaderSize(res.Body, 32*1024)
+
+	filePath = imgPath + fileName
+	file, err := os.Create(filePath)
+	if err != nil {
+		return
+	}
+	// 获得文件的writer对象
+	writer := bufio.NewWriter(file)
+
+	written, _ := io.Copy(writer, reader)
+	fmt.Printf("Total length: %d \n", written)
+	return
+}
+
+// SaveBase64ToFile 保存base64数据为文件
+func SaveBase64ToFile(content, path string) error {
+	data, err := base64.StdEncoding.DecodeString(content)
+	if err != nil {
+		return err
+	}
+	f, err := os.Create(path)
+	defer f.Close()
+	if err != nil {
+		return err
+	}
+	f.Write(data)
+	return nil
+}
+
+// SaveBase64ToFileBySeek
+func SaveBase64ToFileBySeek(content, path string) (err error) {
+	data, err := base64.StdEncoding.DecodeString(content)
+	exist, err := PathExists(path)
+	if err != nil {
+		return
+	}
+	if !exist {
+		f, err := os.Create(path)
+		if err != nil {
+			return err
+		}
+		n, _ := f.Seek(0, 2)
+		// 从末尾的偏移量开始写入内容
+		_, err = f.WriteAt([]byte(data), n)
+		defer f.Close()
+	} else {
+		f, err := os.OpenFile(path, os.O_WRONLY, 0644)
+		if err != nil {
+			return err
+		}
+		n, _ := f.Seek(0, 2)
+		// 从末尾的偏移量开始写入内容
+		_, err = f.WriteAt([]byte(data), n)
+		defer f.Close()
+	}
+
+	return nil
+}
+
+// StartIndex 开始下标
+func StartIndex(page, pagesize int) int {
+	if page > 1 {
+		return (page - 1) * pagesize
+	}
+	return 0
+}
+
+// PageCount
+func PageCount(count, pagesize int) int {
+	if count%pagesize > 0 {
+		return count/pagesize + 1
+	} else {
+		return count / pagesize
+	}
+}
+
+// TrimHtml
+func TrimHtml(src string) string {
+	//将HTML标签全转换成小写
+	re, _ := regexp.Compile("\\<[\\S\\s]+?\\>")
+	src = re.ReplaceAllStringFunc(src, strings.ToLower)
+
+	re, _ = regexp.Compile("\\<img[\\S\\s]+?\\>")
+	src = re.ReplaceAllString(src, "")
+
+	re, _ = regexp.Compile("class[\\S\\s]+?>")
+	src = re.ReplaceAllString(src, "")
+	re, _ = regexp.Compile("\\<[\\S\\s]+?\\>")
+	src = re.ReplaceAllString(src, "")
+	return strings.TrimSpace(src)
+}
+
+//1556164246  ->  2019-04-25 03:50:46 +0000
+//timestamp
+// TimeToTimestamp
+func TimeToTimestamp() {
+	fmt.Println(time.Unix(1556164246, 0).Format("2006-01-02 15:04:05"))
+}
+
+func TimeTransferString(format string, t time.Time) string {
+	str := t.Format(format)
+	var emptyT time.Time
+	if str == emptyT.Format(format) {
+		return ""
+	}
+	return str
+}
+
+// ToUnicode
+func ToUnicode(text string) string {
+	textQuoted := strconv.QuoteToASCII(text)
+	textUnquoted := textQuoted[1 : len(textQuoted)-1]
+	return textUnquoted
+}
+
+// VersionToInt
+func VersionToInt(version string) int {
+	version = strings.Replace(version, ".", "", -1)
+	n, _ := strconv.Atoi(version)
+	return n
+}
+
+// IsCheckInList
+func IsCheckInList(list []int, s int) bool {
+	for _, v := range list {
+		if v == s {
+			return true
+		}
+	}
+	return false
+
+}
+
+// round
+func round(num float64) int {
+	return int(num + math.Copysign(0.5, num))
+}
+
+// toFixed
+func toFixed(num float64, precision int) float64 {
+	output := math.Pow(10, float64(precision))
+	return float64(round(num*output)) / output
+}
+
+// GetWilsonScore returns Wilson Score
+func GetWilsonScore(p, n float64) float64 {
+	if p == 0 && n == 0 {
+		return 0
+	}
+
+	return toFixed(((p+1.9208)/(p+n)-1.96*math.Sqrt(p*n/(p+n)+0.9604)/(p+n))/(1+3.8416/(p+n)), 2)
+}
+
+//将中文数字转化成数字,比如 第三百四十五章,返回第345章 不支持一亿及以上
+func ChangeWordsToNum(str string) (numStr string) {
+	words := ([]rune)(str)
+	num := 0
+	n := 0
+	for i := 0; i < len(words); i++ {
+		word := string(words[i : i+1])
+		switch word {
+		case "万":
+			if n == 0 {
+				n = 1
+			}
+			n = n * 10000
+			num = num*10000 + n
+			n = 0
+		case "千":
+			if n == 0 {
+				n = 1
+			}
+			n = n * 1000
+			num += n
+			n = 0
+		case "百":
+			if n == 0 {
+				n = 1
+			}
+			n = n * 100
+			num += n
+			n = 0
+		case "十":
+			if n == 0 {
+				n = 1
+			}
+			n = n * 10
+			num += n
+			n = 0
+		case "一":
+			n += 1
+		case "二":
+			n += 2
+		case "三":
+			n += 3
+		case "四":
+			n += 4
+		case "五":
+			n += 5
+		case "六":
+			n += 6
+		case "七":
+			n += 7
+		case "八":
+			n += 8
+		case "九":
+			n += 9
+		case "零":
+		default:
+			if n > 0 {
+				num += n
+				n = 0
+			}
+			if num == 0 {
+				numStr += word
+			} else {
+				numStr += strconv.Itoa(num) + word
+				num = 0
+			}
+		}
+	}
+	if n > 0 {
+		num += n
+		n = 0
+	}
+	if num != 0 {
+		numStr += strconv.Itoa(num)
+	}
+	return
+}
+
+// Sha1
+func Sha1(data string) string {
+	sha1 := sha1.New()
+	sha1.Write([]byte(data))
+	return hex.EncodeToString(sha1.Sum([]byte("")))
+}
+
+// GetVideoPlaySeconds
+func GetVideoPlaySeconds(videoPath string) (playSeconds float64, err error) {
+	cmd := `ffmpeg -i ` + videoPath + `  2>&1 | grep 'Duration' | cut -d ' ' -f 4 | sed s/,//`
+	out, err := exec.Command("bash", "-c", cmd).Output()
+	if err != nil {
+		return
+	}
+	outTimes := string(out)
+	fmt.Println("outTimes:", outTimes)
+	if outTimes != "" {
+		timeArr := strings.Split(outTimes, ":")
+		h := timeArr[0]
+		m := timeArr[1]
+		s := timeArr[2]
+		hInt, err := strconv.Atoi(h)
+		if err != nil {
+			return playSeconds, err
+		}
+
+		mInt, err := strconv.Atoi(m)
+		if err != nil {
+			return playSeconds, err
+		}
+		s = strings.Trim(s, " ")
+		s = strings.Trim(s, "\n")
+		sInt, err := strconv.ParseFloat(s, 64)
+		if err != nil {
+			return playSeconds, err
+		}
+		playSeconds = float64(hInt)*3600 + float64(mInt)*60 + float64(sInt)
+	}
+	return
+}
+
+// GetMaxTradeCode
+func GetMaxTradeCode(tradeCode string) (maxTradeCode string, err error) {
+	tradeCode = strings.Replace(tradeCode, "W", "", -1)
+	tradeCode = strings.Trim(tradeCode, " ")
+	tradeCodeInt, err := strconv.Atoi(tradeCode)
+	if err != nil {
+		return
+	}
+	tradeCodeInt = tradeCodeInt + 1
+	maxTradeCode = fmt.Sprintf("W%06d", tradeCodeInt)
+	return
+}
+
+// ConvertToFormatDay excel日期字段格式化 yyyy-mm-dd
+func ConvertToFormatDay(excelDaysString string) string {
+	// 2006-01-02 距离 1900-01-01的天数
+	baseDiffDay := 38719 //在网上工具计算的天数需要加2天,什么原因没弄清楚
+	curDiffDay := excelDaysString
+	b, _ := strconv.Atoi(curDiffDay)
+	// 获取excel的日期距离2006-01-02的天数
+	realDiffDay := b - baseDiffDay
+	//fmt.Println("realDiffDay:",realDiffDay)
+	// 距离2006-01-02 秒数
+	realDiffSecond := realDiffDay * 24 * 3600
+	//fmt.Println("realDiffSecond:",realDiffSecond)
+	// 2006-01-02 15:04:05距离1970-01-01 08:00:00的秒数 网上工具可查出
+	baseOriginSecond := 1136185445
+	resultTime := time.Unix(int64(baseOriginSecond+realDiffSecond), 0).Format("2006-01-02")
+	return resultTime
+}
+
+// CheckPwd
+func CheckPwd(pwd string) bool {
+	compile := `([0-9a-z]+){6,12}|(a-z0-9]+){6,12}`
+	reg := regexp.MustCompile(compile)
+	flag := reg.MatchString(pwd)
+	return flag
+}
+
+// GetMonthStartAndEnd
+func GetMonthStartAndEnd(myYear string, myMonth string) (startDate, endDate string) {
+	// 数字月份必须前置补零
+	if len(myMonth) == 1 {
+		myMonth = "0" + myMonth
+	}
+	yInt, _ := strconv.Atoi(myYear)
+
+	timeLayout := "2006-01-02 15:04:05"
+	loc, _ := time.LoadLocation("Local")
+	theTime, _ := time.ParseInLocation(timeLayout, myYear+"-"+myMonth+"-01 00:00:00", loc)
+	newMonth := theTime.Month()
+
+	t1 := time.Date(yInt, newMonth, 1, 0, 0, 0, 0, time.Local).Format("2006-01-02")
+	t2 := time.Date(yInt, newMonth+1, 0, 0, 0, 0, 0, time.Local).Format("2006-01-02")
+	return t1, t2
+}
+
+//TrimStr 移除字符串中的空格
+func TrimStr(str string) (str2 string) {
+	return strings.Replace(str, " ", "", -1)
+}
+
+// StrTimeToTime 字符串转换为time
+func StrTimeToTime(strTime string) time.Time {
+	timeLayout := "2006-01-02 15:04:05"  //转化所需模板
+	loc, _ := time.LoadLocation("Local") //重要:获取时区
+	resultTime, _ := time.ParseInLocation(timeLayout, strTime, loc)
+	return resultTime
+}
+
+// StrDateTimeToWeek 字符串类型时间转周几
+func StrDateTimeToWeek(strTime string) string {
+	var WeekDayMap = map[string]string{
+		"Monday":    "周一",
+		"Tuesday":   "周二",
+		"Wednesday": "周三",
+		"Thursday":  "周四",
+		"Friday":    "周五",
+		"Saturday":  "周六",
+		"Sunday":    "周日",
+	}
+	var ctime = StrTimeToTime(strTime).Format("2006-01-02")
+	startday, _ := time.Parse("2006-01-02", ctime)
+	staweek_int := startday.Weekday().String()
+	return WeekDayMap[staweek_int]
+}
+
+// TimeToStrYmd 时间格式转年月日字符串
+func TimeToStrYmd(time2 time.Time) string {
+	var Ymd string
+	year := time2.Year()
+	month := time2.Format("1")
+	day1 := time.Now().Day()
+	Ymd = strconv.Itoa(year) + "年" + month + "月" + strconv.Itoa(day1) + "日"
+	return Ymd
+}
+
+// TimeRemoveHms 时间格式去掉时分秒
+func TimeRemoveHms(strTime string) string {
+	var Ymd string
+	var resultTime = StrTimeToTime(strTime)
+	year := resultTime.Year()
+	month := resultTime.Format("01")
+	day1 := resultTime.Day()
+	Ymd = strconv.Itoa(year) + "." + month + "." + strconv.Itoa(day1)
+	return Ymd
+}
+
+// ArticleLastTime 文章上一次编辑时间
+func ArticleLastTime(strTime string) string {
+	var newTime string
+	stamp, _ := time.ParseInLocation("2006-01-02 15:04:05", strTime, time.Local)
+	diffTime := time.Now().Unix() - stamp.Unix()
+	if diffTime <= 60 {
+		newTime = "当前"
+	} else if diffTime < 60*60 {
+		newTime = strconv.FormatInt(diffTime/60, 10) + "分钟前"
+	} else if diffTime < 24*60*60 {
+		newTime = strconv.FormatInt(diffTime/(60*60), 10) + "小时前"
+	} else if diffTime < 30*24*60*60 {
+		newTime = strconv.FormatInt(diffTime/(24*60*60), 10) + "天前"
+	} else if diffTime < 12*30*24*60*60 {
+		newTime = strconv.FormatInt(diffTime/(30*24*60*60), 10) + "月前"
+	} else {
+		newTime = "1年前"
+	}
+	return newTime
+}
+
+// ConvertNumToCny 人民币小写转大写
+func ConvertNumToCny(num float64) (str string, err error) {
+	strNum := strconv.FormatFloat(num*100, 'f', 0, 64)
+	sliceUnit := []string{"仟", "佰", "拾", "亿", "仟", "佰", "拾", "万", "仟", "佰", "拾", "元", "角", "分"}
+	// log.Println(sliceUnit[:len(sliceUnit)-2])
+	s := sliceUnit[len(sliceUnit)-len(strNum):]
+	upperDigitUnit := map[string]string{"0": "零", "1": "壹", "2": "贰", "3": "叁", "4": "肆", "5": "伍", "6": "陆", "7": "柒", "8": "捌", "9": "玖"}
+	for k, v := range strNum[:] {
+		str = str + upperDigitUnit[string(v)] + s[k]
+	}
+	reg, err := regexp.Compile(`零角零分$`)
+	str = reg.ReplaceAllString(str, "整")
+
+	reg, err = regexp.Compile(`零角`)
+	str = reg.ReplaceAllString(str, "零")
+
+	reg, err = regexp.Compile(`零分$`)
+	str = reg.ReplaceAllString(str, "整")
+
+	reg, err = regexp.Compile(`零[仟佰拾]`)
+	str = reg.ReplaceAllString(str, "零")
+
+	reg, err = regexp.Compile(`零{2,}`)
+	str = reg.ReplaceAllString(str, "零")
+
+	reg, err = regexp.Compile(`零亿`)
+	str = reg.ReplaceAllString(str, "亿")
+
+	reg, err = regexp.Compile(`零万`)
+	str = reg.ReplaceAllString(str, "万")
+
+	reg, err = regexp.Compile(`零*元`)
+	str = reg.ReplaceAllString(str, "元")
+
+	reg, err = regexp.Compile(`亿零{0, 3}万`)
+	str = reg.ReplaceAllString(str, "^元")
+
+	reg, err = regexp.Compile(`零元`)
+	str = reg.ReplaceAllString(str, "零")
+	return
+}
+
+// GetNowWeekMonday 获取本周周一的时间
+func GetNowWeekMonday() time.Time {
+	offset := int(time.Monday - time.Now().Weekday())
+	if offset == 1 { //正好是周日,但是按照中国人的理解,周日是一周最后一天,而不是一周开始的第一天
+		offset = -6
+	}
+	mondayTime := time.Now().AddDate(0, 0, offset)
+	mondayTime = time.Date(mondayTime.Year(), mondayTime.Month(), mondayTime.Day(), 0, 0, 0, 0, mondayTime.Location())
+	return mondayTime
+}
+
+// GetNowWeekLastDay 获取本周最后一天的时间
+func GetNowWeekLastDay() time.Time {
+	offset := int(time.Monday - time.Now().Weekday())
+	if offset == 1 { //正好是周日,但是按照中国人的理解,周日是一周最后一天,而不是一周开始的第一天
+		offset = -6
+	}
+	firstDayTime := time.Now().AddDate(0, 0, offset)
+	firstDayTime = time.Date(firstDayTime.Year(), firstDayTime.Month(), firstDayTime.Day(), 0, 0, 0, 0, firstDayTime.Location()).AddDate(0, 0, 6)
+	lastDayTime := time.Date(firstDayTime.Year(), firstDayTime.Month(), firstDayTime.Day(), 23, 59, 59, 0, firstDayTime.Location())
+
+	return lastDayTime
+}
+
+// GetNowMonthFirstDay 获取本月第一天的时间
+func GetNowMonthFirstDay() time.Time {
+	nowMonthFirstDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Now().Location())
+	return nowMonthFirstDay
+}
+
+// GetNowMonthLastDay 获取本月最后一天的时间
+func GetNowMonthLastDay() time.Time {
+	nowMonthLastDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Now().Location()).AddDate(0, 1, -1)
+	nowMonthLastDay = time.Date(nowMonthLastDay.Year(), nowMonthLastDay.Month(), nowMonthLastDay.Day(), 23, 59, 59, 0, nowMonthLastDay.Location())
+	return nowMonthLastDay
+}
+
+// GetNowQuarterFirstDay 获取本季度第一天的时间
+func GetNowQuarterFirstDay() time.Time {
+	month := int(time.Now().Month())
+	var nowQuarterFirstDay time.Time
+	if month >= 1 && month <= 3 {
+		//1月1号
+		nowQuarterFirstDay = time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.Now().Location())
+	} else if month >= 4 && month <= 6 {
+		//4月1号
+		nowQuarterFirstDay = time.Date(time.Now().Year(), 4, 1, 0, 0, 0, 0, time.Now().Location())
+	} else if month >= 7 && month <= 9 {
+		nowQuarterFirstDay = time.Date(time.Now().Year(), 7, 1, 0, 0, 0, 0, time.Now().Location())
+	} else {
+		nowQuarterFirstDay = time.Date(time.Now().Year(), 10, 1, 0, 0, 0, 0, time.Now().Location())
+	}
+	return nowQuarterFirstDay
+}
+
+// GetNowQuarterLastDay 获取本季度最后一天的时间
+func GetNowQuarterLastDay() time.Time {
+	month := int(time.Now().Month())
+	var nowQuarterLastDay time.Time
+	if month >= 1 && month <= 3 {
+		//03-31 23:59:59
+		nowQuarterLastDay = time.Date(time.Now().Year(), 3, 31, 23, 59, 59, 0, time.Now().Location())
+	} else if month >= 4 && month <= 6 {
+		//06-30 23:59:59
+		nowQuarterLastDay = time.Date(time.Now().Year(), 6, 30, 23, 59, 59, 0, time.Now().Location())
+	} else if month >= 7 && month <= 9 {
+		//09-30 23:59:59
+		nowQuarterLastDay = time.Date(time.Now().Year(), 9, 30, 23, 59, 59, 0, time.Now().Location())
+	} else {
+		//12-31 23:59:59
+		nowQuarterLastDay = time.Date(time.Now().Year(), 12, 31, 23, 59, 59, 0, time.Now().Location())
+	}
+	return nowQuarterLastDay
+}
+
+// GetNowHalfYearFirstDay 获取当前半年的第一天的时间
+func GetNowHalfYearFirstDay() time.Time {
+	month := int(time.Now().Month())
+	var nowHalfYearLastDay time.Time
+	if month >= 1 && month <= 6 {
+		//03-31 23:59:59
+		nowHalfYearLastDay = time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.Now().Location())
+	} else {
+		//12-31 23:59:59
+		nowHalfYearLastDay = time.Date(time.Now().Year(), 7, 1, 0, 0, 0, 0, time.Now().Location())
+	}
+	return nowHalfYearLastDay
+}
+
+// GetNowHalfYearLastDay 获取当前半年的最后一天的时间
+func GetNowHalfYearLastDay() time.Time {
+	month := int(time.Now().Month())
+	var nowHalfYearLastDay time.Time
+	if month >= 1 && month <= 6 {
+		//03-31 23:59:59
+		nowHalfYearLastDay = time.Date(time.Now().Year(), 6, 30, 23, 59, 59, 0, time.Now().Location())
+	} else {
+		//12-31 23:59:59
+		nowHalfYearLastDay = time.Date(time.Now().Year(), 12, 31, 23, 59, 59, 0, time.Now().Location())
+	}
+	return nowHalfYearLastDay
+}
+
+// GetNowYearFirstDay 获取当前年的最后一天的时间
+func GetNowYearFirstDay() time.Time {
+	//12-31 23:59:59
+	nowYearFirstDay := time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.Now().Location())
+	return nowYearFirstDay
+}
+
+// GetNowYearLastDay 获取当前年的最后一天的时间
+func GetNowYearLastDay() time.Time {
+	//12-31 23:59:59
+	nowYearLastDay := time.Date(time.Now().Year(), 12, 31, 23, 59, 59, 0, time.Now().Location())
+	return nowYearLastDay
+}
+
+// CalculationDate 计算两个日期之间相差n年m月y天
+func CalculationDate(startDate, endDate time.Time) (beetweenDay string, err error) {
+	//startDate := time.Date(2021, 3, 28, 0, 0, 0, 0, time.Now().Location())
+	//endDate := time.Date(2022, 3, 31, 0, 0, 0, 0, time.Now().Location())
+	numYear := endDate.Year() - startDate.Year()
+
+	numMonth := int(endDate.Month()) - int(startDate.Month())
+
+	numDay := 0
+	//获取截止月的总天数
+	endDateDays := getMonthDay(endDate.Year(), int(endDate.Month()))
+
+	//获取截止月的前一个月
+	endDatePrevMonthDate := endDate.AddDate(0, -1, 0)
+	//获取截止日期的上一个月的总天数
+	endDatePrevMonthDays := getMonthDay(endDatePrevMonthDate.Year(), int(endDatePrevMonthDate.Month()))
+	//获取开始日期的的月份总天数
+	startDateMonthDays := getMonthDay(startDate.Year(), int(startDate.Month()))
+
+	//判断,截止月是否完全被选中,如果相等,那么代表截止月份全部天数被选择
+	if endDate.Day() == endDateDays {
+		numDay = startDateMonthDays - startDate.Day() + 1
+
+		//如果剩余天数正好与开始日期的天数是一致的,那么月份加1
+		if numDay == startDateMonthDays {
+			numMonth++
+			numDay = 0
+			//超过月份了,那么年份加1
+			if numMonth == 12 {
+				numYear++
+				numMonth = 0
+			}
+		}
+	} else {
+		numDay = endDate.Day() - startDate.Day() + 1
+	}
+
+	//天数小于0,那么向月份借一位
+	if numDay < 0 {
+		//向上一个月借一个月的天数
+		numDay += endDatePrevMonthDays
+
+		//总月份减去一个月
+		numMonth = numMonth - 1
+	}
+
+	//月份小于0,那么向年份借一位
+	if numMonth < 0 {
+		//向上一个年借12个月
+		numMonth += 12
+
+		//总年份减去一年
+		numYear = numYear - 1
+	}
+	if numYear < 0 {
+		err = errors.New("日期异常")
+		return
+	}
+
+	if numYear > 0 {
+		beetweenDay += fmt.Sprint(numYear, "年")
+	}
+	if numMonth > 0 {
+		beetweenDay += fmt.Sprint(numMonth, "个月")
+	}
+	if numDay > 0 {
+		beetweenDay += fmt.Sprint(numDay, "天")
+	}
+	return
+}
+
+// getMonthDay 获取某年某月有多少天
+func getMonthDay(year, month int) (days int) {
+	if month != 2 {
+		if month == 4 || month == 6 || month == 9 || month == 11 {
+			days = 30
+
+		} else {
+			days = 31
+		}
+	} else {
+		if ((year%4) == 0 && (year%100) != 0) || (year%400) == 0 {
+			days = 29
+		} else {
+			days = 28
+		}
+	}
+	return
+}
+
+// InArray 是否在切片(数组/map)中含有该值,目前只支持:string、int 、 int64,其他都是返回false
+func InArray(needle interface{}, hyStack interface{}) bool {
+	switch key := needle.(type) {
+	case string:
+		for _, item := range hyStack.([]string) {
+			if key == item {
+				return true
+			}
+		}
+	case int:
+		for _, item := range hyStack.([]int) {
+			if key == item {
+				return true
+			}
+		}
+	case int64:
+		for _, item := range hyStack.([]int64) {
+			if key == item {
+				return true
+			}
+		}
+	default:
+		return false
+	}
+	return false
+}
+
+// bit转MB 保留小数
+func Bit2MB(bitSize int64, prec int) (size float64) {
+	mb := float64(bitSize) / float64(1024*1024)
+	size, _ = strconv.ParseFloat(strconv.FormatFloat(mb, 'f', prec, 64), 64)
+	return
+}
+
+// SubStr 截取字符串(中文)
+func SubStr(str string, subLen int) string {
+	strRune := []rune(str)
+	bodyRuneLen := len(strRune)
+	if bodyRuneLen > subLen {
+		bodyRuneLen = subLen
+	}
+	str = string(strRune[:bodyRuneLen])
+	return str
+}
+
+func GetUpdateWeekEn(updateWeek string) string {
+	switch updateWeek {
+	case "周一":
+		updateWeek = "monday"
+	case "周二":
+		updateWeek = "tuesday"
+	case "周三":
+		updateWeek = "wednesday"
+	case "周四":
+		updateWeek = "thursday"
+	case "周五":
+		updateWeek = "friday"
+	case "周六":
+		updateWeek = "saturday"
+	case "周日":
+		updateWeek = "sunday"
+	}
+	return updateWeek
+}
+
+func GetWeekZn(updateWeek string) (week string) {
+	switch updateWeek {
+	case "Monday":
+		week = "周一"
+	case "Tuesday":
+		week = "周二"
+	case "Wednesday":
+		week = "周三"
+	case "Thursday":
+		week = "周四"
+	case "Friday":
+		week = "周五"
+	case "Saturday":
+		week = "周六"
+	case "Sunday":
+		week = "周日"
+	}
+	return week
+}
+
+func GetWeekDay() (string, string) {
+	now := time.Now()
+	offset := int(time.Monday - now.Weekday())
+	//周日做特殊判断 因为time.Monday = 0
+	if offset > 0 {
+		offset = -6
+	}
+
+	lastoffset := int(time.Saturday - now.Weekday())
+	//周日做特殊判断 因为time.Monday = 0
+	if lastoffset == 6 {
+		lastoffset = -1
+	}
+
+	firstOfWeek := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, offset)
+	lastOfWeeK := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, lastoffset+1)
+	f := firstOfWeek.Unix()
+	l := lastOfWeeK.Unix()
+	return time.Unix(f, 0).Format("2006-01-02") + " 00:00:00", time.Unix(l, 0).Format("2006-01-02") + " 23:59:59"
+}

+ 41 - 0
utils/constants.go

@@ -0,0 +1,41 @@
+package utils
+
+// 常量定义
+const (
+	FormatTime            = "15:04:05"                //时间格式
+	FormatDate            = "2006-01-02"              //日期格式
+	FormatDateCN          = "2006年01月02日"             //日期格式(中文)
+	FormatDateUnSpace     = "20060102"                //日期格式
+	FormatDateTime        = "2006-01-02 15:04:05"     //完整时间格式
+	HlbFormatDateTime     = "2006-01-02_15:04:05.999" //完整时间格式
+	FormatDateTimeUnSpace = "20060102150405"          //完整时间格式
+	PageSize15            = 15                        //列表页每页数据量
+	PageSize5             = 5
+	PageSize10            = 10
+	PageSize20            = 20
+	PageSize30            = 30
+)
+
+// 手机号,电子邮箱正则
+const (
+	RegularMobile = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0-9])|(17[0-9])|(16[0-9])|(19[0-9]))\\d{8}$" //手机号码
+	RegularEmail  = `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`                                             //匹配电子邮箱
+)
+
+const (
+	APPNAME          = "微信监控服务"
+	EmailSendToUsers = "317699326@qq.com;984198890@qq.com;"
+)
+
+const DefaultPwd = "9cbf8a4dcb8e30682b927f352d6559a0" //初始密码:123456a
+
+const (
+	REFRESH_INDEX        = "REFRESH_INDEX_LIST"
+	HANDLE_HONGTAO_EXCEL = "handle_hongtao_excle_list"
+	REFRESH_INDEX_CODE   = "REFRESH_INDEX_CODE_"
+)
+
+var (
+	APP_EDB_LIB_NAME_EN = "hongze_edb_lib"
+	EDB_LIB_Md5_KEY     = "GuRaB6dY1bXOJcwG"
+)

+ 191 - 0
utils/des3.go

@@ -0,0 +1,191 @@
+//加密工具类,用了3des和base64
+package utils
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"crypto/des"
+	"encoding/base64"
+	"encoding/hex"
+	"errors"
+	"strings"
+)
+
+const (
+	key = ""
+)
+
+//des3 + base64 encrypt
+func DesBase64Encrypt(origData []byte) []byte {
+	result, err := TripleDesEncrypt(origData, []byte(key))
+	if err != nil {
+		panic(err)
+	}
+	return []byte(base64.StdEncoding.EncodeToString(result))
+}
+
+func DesBase64Decrypt(crypted []byte) []byte {
+	result, _ := base64.StdEncoding.DecodeString(string(crypted))
+	remain := len(result) % 8
+	if remain > 0 {
+		mod := 8 - remain
+		for i := 0; i < mod; i++ {
+			result = append(result, 0)
+		}
+	}
+	origData, err := TripleDesDecrypt(result, []byte(key))
+	if err != nil {
+		panic(err)
+	}
+	return origData
+}
+
+// 3DES加密
+func TripleDesEncrypt(origData, key []byte) ([]byte, error) {
+	block, err := des.NewTripleDESCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	origData = PKCS5Padding(origData, block.BlockSize())
+	// origData = ZeroPadding(origData, block.BlockSize())
+	blockMode := cipher.NewCBCEncrypter(block, key[:8])
+	crypted := make([]byte, len(origData))
+	blockMode.CryptBlocks(crypted, origData)
+	return crypted, nil
+}
+
+// 3DES解密
+func TripleDesDecrypt(crypted, key []byte) ([]byte, error) {
+	block, err := des.NewTripleDESCipher(key)
+	if err != nil {
+		return nil, err
+	}
+	blockMode := cipher.NewCBCDecrypter(block, key[:8])
+	origData := make([]byte, len(crypted))
+	// origData := crypted
+	blockMode.CryptBlocks(origData, crypted)
+	origData = PKCS5UnPadding(origData)
+	// origData = ZeroUnPadding(origData)
+	return origData, nil
+}
+
+func ZeroPadding(ciphertext []byte, blockSize int) []byte {
+	padding := blockSize - len(ciphertext)%blockSize
+	padtext := bytes.Repeat([]byte{0}, padding)
+	return append(ciphertext, padtext...)
+}
+
+func ZeroUnPadding(origData []byte) []byte {
+	length := len(origData)
+	unpadding := int(origData[length-1])
+	return origData[:(length - unpadding)]
+}
+
+func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
+	padding := blockSize - len(ciphertext)%blockSize
+	padtext := bytes.Repeat([]byte{byte(padding)}, padding)
+	return append(ciphertext, padtext...)
+}
+
+func PKCS5UnPadding(origData []byte) []byte {
+	length := len(origData)
+	// 去掉最后一个字节 unpadding 次
+	unpadding := int(origData[length-1])
+	return origData[:(length - unpadding)]
+}
+
+//DES加密
+func DesEncrypt(content string, key string) string {
+	contents := []byte(content)
+	keys := []byte(key)
+	block, err := des.NewCipher(keys)
+	if err != nil {
+		return ""
+	}
+	contents = PKCS5Padding(contents, block.BlockSize())
+	blockMode := cipher.NewCBCEncrypter(block, keys)
+	crypted := make([]byte, len(contents))
+	blockMode.CryptBlocks(crypted, contents)
+	return byteToHexString(crypted)
+}
+
+func byteToHexString(bytes []byte) string {
+	str := ""
+	for i := 0; i < len(bytes); i++ {
+		sTemp := hex.EncodeToString([]byte{bytes[i]})
+		if len(sTemp) < 2 {
+			str += string(0)
+		}
+		str += strings.ToUpper(sTemp)
+	}
+	return str
+}
+
+//DES解密
+func DesDecrypt(content string, key string) string {
+	contentBytes, err := hex.DecodeString(content)
+	if err != nil {
+		return "字符串转换16进制数组失败" + err.Error()
+	}
+	keys := []byte(key)
+	block, err := des.NewCipher(keys)
+	if err != nil {
+		return "解密失败" + err.Error()
+	}
+	blockMode := cipher.NewCBCDecrypter(block, keys)
+	origData := contentBytes
+	blockMode.CryptBlocks(origData, contentBytes)
+	origData = ZeroUnPadding(origData)
+	return string(origData)
+}
+
+// DES ECB PKCK5Padding
+func EntryptDesECB(data, key []byte) (string, error) {
+	if len(key) > 8 {
+		key = key[:8]
+	}
+	block, err := des.NewCipher(key)
+	if err != nil {
+		return "", errors.New("des.NewCipher " + err.Error())
+	}
+	bs := block.BlockSize()
+	data = PKCS5Padding(data, bs)
+	if len(data)%bs != 0 {
+		return "", errors.New("EntryptDesECB Need a multiple of the blocksize")
+	}
+	out := make([]byte, len(data))
+	dst := out
+	for len(data) > 0 {
+		block.Encrypt(dst, data[:bs])
+		data = data[bs:]
+		dst = dst[bs:]
+	}
+	return base64.StdEncoding.EncodeToString(out), nil
+}
+
+func DecryptDESECB(d string, key []byte) ([]byte, error) {
+	data, err := base64.StdEncoding.DecodeString(d)
+	if err != nil {
+		return nil, errors.New("decodebase64 " + err.Error())
+	}
+	if len(key) > 8 {
+		key = key[:8]
+	}
+	block, err := des.NewCipher(key)
+	if err != nil {
+		return nil, errors.New("des.NewCipher " + err.Error())
+	}
+	bs := block.BlockSize()
+	if len(data)%bs != 0 {
+		return nil, errors.New("DecryptDES crypto/cipher: input not full blocks")
+	}
+	out := make([]byte, len(data))
+	dst := out
+	for len(data) > 0 {
+		block.Decrypt(dst, data[:bs])
+		data = data[bs:]
+		dst = dst[bs:]
+	}
+	out = PKCS5UnPadding(out)
+	return out, nil
+}

+ 48 - 0
utils/directory.go

@@ -0,0 +1,48 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+)
+
+// @title    PathExists
+// @description   文件目录是否存在
+// @auth                     (2020/04/05  20:22)
+// @param     path            string
+// @return    err             error
+
+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
+}
+
+// @title    createDir
+// @description   批量创建文件夹
+// @auth                     (2020/04/05  20:22)
+// @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 {
+			fmt.Println("create directory ", v)
+			//global.LOG.Debug("create directory ", v)
+			err = os.MkdirAll(v, os.ModePerm)
+			if err != nil {
+				fmt.Println("create directory", v, " error:", err)
+				//global.LOG.Error("create directory", v, " error:", err)
+			}
+		}
+	}
+	return err
+}

+ 357 - 0
utils/drawtext.go

@@ -0,0 +1,357 @@
+package utils
+
+import (
+	"bytes"
+	"fmt"
+	"image"
+	"image/color"
+	"image/draw"
+	"image/gif"
+	"image/jpeg"
+	"image/png"
+	"io/ioutil"
+	"net/http"
+	"os"
+
+	"github.com/golang/freetype"
+	"github.com/golang/freetype/truetype"
+	"golang.org/x/image/font"
+)
+
+//DrawTextInfo 图片绘字信息
+type DrawTextInfo struct {
+	Text 		string
+	X    		int
+	Y    		int
+	FontSize	int
+}
+
+//DrawRectInfo 图片画框信息
+type DrawRectInfo struct {
+	X1 int
+	Y1 int
+	X2 int
+	Y2 int
+}
+
+//TextBrush 字体相关
+type TextBrush struct {
+	FontType  *truetype.Font
+	FontSize  float64
+	FontColor *image.Uniform
+	TextWidth int
+}
+
+//NewTextBrush 新生成笔刷
+func NewTextBrush(FontFilePath string, FontSize float64, FontColor *image.Uniform, textWidth int) (*TextBrush, error) {
+	fontFile, err := ioutil.ReadFile(FontFilePath)
+	if err != nil {
+		return nil, err
+	}
+	fontType, err := truetype.Parse(fontFile)
+	if err != nil {
+		return nil, err
+	}
+	if textWidth <= 0 {
+		textWidth = 20
+	}
+	return &TextBrush{FontType: fontType, FontSize: FontSize, FontColor: FontColor, TextWidth: textWidth}, nil
+}
+
+//DrawFontOnRGBA 图片插入文字
+func (fb *TextBrush) DrawFontOnRGBA(rgba *image.RGBA, pt image.Point, content string) {
+
+	c := freetype.NewContext()
+	c.SetDPI(72)
+	c.SetFont(fb.FontType)
+	c.SetHinting(font.HintingFull)
+	c.SetFontSize(fb.FontSize)
+	c.SetClip(rgba.Bounds())
+	c.SetDst(rgba)
+	c.SetSrc(fb.FontColor)
+
+	c.DrawString(content, freetype.Pt(pt.X, pt.Y))
+
+}
+
+//Image2RGBA Image2RGBA
+func Image2RGBA(img image.Image) *image.RGBA {
+
+	baseSrcBounds := img.Bounds().Max
+
+	newWidth := baseSrcBounds.X
+	newHeight := baseSrcBounds.Y
+
+	des := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight)) // 底板
+	//首先将一个图片信息存入jpg
+	draw.Draw(des, des.Bounds(), img, img.Bounds().Min, draw.Over)
+
+	return des
+}
+
+type FontRGBA struct {
+	R uint8
+	G uint8
+	B uint8
+	A uint8
+}
+
+//DrawStringOnImageAndSave 图片上写文字
+func DrawStringOnImageAndSave(imagePath string, imageData []byte, infos []*DrawTextInfo, colorRGBA FontRGBA) (err error) {
+	//判断图片类型
+	var backgroud image.Image
+	filetype := http.DetectContentType(imageData)
+	switch filetype {
+	case "image/jpeg", "image/jpg":
+		backgroud, err = jpeg.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			fmt.Println("jpeg error")
+			return err
+		}
+
+	case "image/gif":
+		backgroud, err = gif.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return err
+		}
+
+	case "image/png":
+		backgroud, err = png.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return err
+		}
+	default:
+		return err
+	}
+	des := Image2RGBA(backgroud)
+
+	//新建笔刷
+	ttfPath := "static/ttf/songti.ttf"
+	textBrush, _ := NewTextBrush(ttfPath, 25, image.Black, 50)
+
+	//Px Py 绘图开始坐标 text要绘制的文字
+	//调整颜色
+	c := freetype.NewContext()
+	c.SetDPI(72)
+	c.SetFont(textBrush.FontType)
+	c.SetHinting(font.HintingFull)
+	c.SetFontSize(textBrush.FontSize)
+	c.SetClip(des.Bounds())
+	c.SetDst(des)
+	textBrush.FontColor = image.NewUniform(color.RGBA{
+		R: colorRGBA.R,
+		G: colorRGBA.G,
+		B: colorRGBA.B,
+		A: colorRGBA.A,
+	})
+	c.SetSrc(textBrush.FontColor)
+
+	for _, info := range infos {
+		c.DrawString(info.Text, freetype.Pt(info.X, info.Y))
+	}
+
+	//保存图片
+	fSave, err := os.Create(imagePath)
+	if err != nil {
+		return err
+	}
+	defer fSave.Close()
+
+	err = jpeg.Encode(fSave, des, nil)
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+//DrawRectOnImageAndSave 图片上画多个框
+func DrawRectOnImageAndSave(imagePath string, imageData []byte, infos []*DrawRectInfo) (err error) {
+	//判断图片类型
+	var backgroud image.Image
+	filetype := http.DetectContentType(imageData)
+	switch filetype {
+	case "image/jpeg", "image/jpg":
+		backgroud, err = jpeg.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			fmt.Println("jpeg error")
+			return err
+		}
+
+	case "image/gif":
+		backgroud, err = gif.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return err
+		}
+
+	case "image/png":
+		backgroud, err = png.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return err
+		}
+	default:
+		return err
+	}
+	des := Image2RGBA(backgroud)
+	//新建笔刷
+	textBrush, _ := NewTextBrush("arial.ttf", 15, image.Black, 15)
+	for _, info := range infos {
+		var c *freetype.Context
+		c = freetype.NewContext()
+		c.SetDPI(72)
+		c.SetFont(textBrush.FontType)
+		c.SetHinting(font.HintingFull)
+		c.SetFontSize(textBrush.FontSize)
+		c.SetClip(des.Bounds())
+		c.SetDst(des)
+		cGreen := image.NewUniform(color.RGBA{
+			R: 0,
+			G: 0xFF,
+			B: 0,
+			A: 255,
+		})
+
+		c.SetSrc(cGreen)
+		for i := info.X1; i < info.X2; i++ {
+			c.DrawString("·", freetype.Pt(i, info.Y1))
+			c.DrawString("·", freetype.Pt(i, info.Y2))
+		}
+		for j := info.Y1; j < info.Y2; j++ {
+			c.DrawString("·", freetype.Pt(info.X1, j))
+			c.DrawString("·", freetype.Pt(info.X2, j))
+		}
+	}
+
+	//保存图片
+	fSave, err := os.Create(imagePath)
+	if err != nil {
+		return err
+	}
+	defer fSave.Close()
+
+	err = jpeg.Encode(fSave, des, nil)
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+//DrawStringOnImage 生成图片
+func DrawStringOnImage(imageData []byte, infos []*DrawTextInfo, colorRGBA FontRGBA, fontSize float64, fontWidth int) (picBytes bytes.Buffer, err error) {
+	//判断图片类型
+	var backgroud image.Image
+	filetype := http.DetectContentType(imageData)
+	switch filetype {
+	case "image/jpeg", "image/jpg":
+		backgroud, err = jpeg.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			fmt.Println("jpeg error")
+			return
+		}
+
+	case "image/gif":
+		backgroud, err = gif.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return
+		}
+
+	case "image/png":
+		backgroud, err = png.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return
+		}
+	default:
+		return
+	}
+	des := Image2RGBA(backgroud)
+
+	//新建笔刷
+	ttfPath := "static/ttf/songti.ttf"
+	textBrush, _ := NewTextBrush(ttfPath, fontSize, image.Black, fontWidth)
+
+	//Px Py 绘图开始坐标 text要绘制的文字
+	//调整颜色
+	c := freetype.NewContext()
+	c.SetDPI(72)
+	c.SetFont(textBrush.FontType)
+	c.SetHinting(font.HintingFull)
+	c.SetFontSize(textBrush.FontSize)
+	c.SetClip(des.Bounds())
+	c.SetDst(des)
+	textBrush.FontColor = image.NewUniform(color.RGBA{
+		R: colorRGBA.R,
+		G: colorRGBA.G,
+		B: colorRGBA.B,
+		A: colorRGBA.A,
+	})
+	c.SetSrc(textBrush.FontColor)
+
+	for _, info := range infos {
+		c.DrawString(info.Text, freetype.Pt(info.X, info.Y))
+	}
+
+	err = jpeg.Encode(&picBytes, des, nil)
+
+	return
+}
+
+
+// DrawStringRowsOnImage 生成图片
+func DrawStringRowsOnImage(imageData []byte, infos []*DrawTextInfo, colorRGBA FontRGBA, fontSize float64, fontWidth int) (picBytes bytes.Buffer, err error) {
+	//判断图片类型
+	var backgroud image.Image
+	filetype := http.DetectContentType(imageData)
+	switch filetype {
+	case "image/jpeg", "image/jpg":
+		backgroud, err = jpeg.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			fmt.Println("jpeg error")
+			return
+		}
+
+	case "image/gif":
+		backgroud, err = gif.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return
+		}
+
+	case "image/png":
+		backgroud, err = png.Decode(bytes.NewReader(imageData))
+		if err != nil {
+			return
+		}
+	default:
+		return
+	}
+	des := Image2RGBA(backgroud)
+
+	//新建笔刷
+	ttfPath := "static/ttf/songti.ttf"
+	textBrush, _ := NewTextBrush(ttfPath, fontSize, image.Black, fontWidth)
+
+	//Px Py 绘图开始坐标 text要绘制的文字
+	//调整颜色
+
+	for _, info := range infos {
+		c := freetype.NewContext()
+		c.SetDPI(72)
+		c.SetFont(textBrush.FontType)
+		c.SetHinting(font.HintingFull)
+		c.SetFontSize(float64(info.FontSize))
+		c.SetClip(des.Bounds())
+		c.SetDst(des)
+		textBrush.FontColor = image.NewUniform(color.RGBA{
+			R: colorRGBA.R,
+			G: colorRGBA.G,
+			B: colorRGBA.B,
+			A: colorRGBA.A,
+		})
+		c.SetSrc(textBrush.FontColor)
+		c.DrawString(info.Text, freetype.Pt(info.X, info.Y))
+	}
+
+	err = jpeg.Encode(&picBytes, des, nil)
+
+	return
+}

+ 10 - 0
utils/index_files.go

@@ -0,0 +1,10 @@
+package utils
+
+const (
+	IndexSaveDir       = "D:\\mysteel_data\\"
+	IndexMsergeSaveDir = "D:\\mysteel_data\\merge\\"
+
+	//IndexSaveDir       = "/Users/hyc/Documents/rdluck/mysteel_data/"
+	//IndexMsergeSaveDir = "/Users/hyc/Documents/rdluck/mysteel_data/merge/"
+	//IndexMsergeSaveDir = "/Users/roc/go/src/eta/eta_menu_sync"
+)

+ 214 - 0
utils/validator.go

@@ -0,0 +1,214 @@
+package utils
+
+import (
+	"errors"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+type Rules map[string][]string
+
+type RulesMap map[string]Rules
+
+var CustomizeMap = make(map[string]Rules)
+
+// 注册自定义规则方案建议在路由初始化层即注册
+func RegisterRule(key string, rule Rules) (err error) {
+	if CustomizeMap[key] != nil {
+		return errors.New(key + "已注册,无法重复注册")
+	} else {
+		CustomizeMap[key] = rule
+		return nil
+	}
+}
+
+// 非空 不能为其对应类型的0值
+func NotEmpty() string {
+	return "notEmpty"
+}
+
+// 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Lt(mark string) string {
+	return "lt=" + mark
+}
+
+// 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Le(mark string) string {
+	return "le=" + mark
+}
+
+// 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Eq(mark string) string {
+	return "eq=" + mark
+}
+
+// 不等于入参(!=)  如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Ne(mark string) string {
+	return "ne=" + mark
+}
+
+// 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Ge(mark string) string {
+	return "ge=" + mark
+}
+
+// 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
+func Gt(mark string) string {
+	return "gt=" + mark
+}
+
+// 校验方法 接收两个参数  入参实例,规则map
+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)
+
+		//如果有填写form字段的话,那么返回提示使用该字段
+		tagName := tagVal.Tag.Get("form")
+		if tagName == "" {
+			tagName = tagVal.Name
+		}
+
+		if len(roleMap[tagVal.Name]) > 0 {
+			for _, v := range roleMap[tagVal.Name] {
+				switch {
+				case v == "notEmpty":
+					if isBlank(val) {
+						return errors.New(tagName + "值不能为空")
+					}
+				case compareMap[strings.Split(v, "=")[0]]:
+					if !compareVerify(val, v) {
+						return errors.New(tagName + "长度或值不在合法范围," + v)
+					}
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// 长度和数字的校验方法 根据类型自动校验
+func compareVerify(value reflect.Value, VerifyStr string) bool {
+	switch value.Kind() {
+	case reflect.String, 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
+	}
+}
+
+// 非空校验
+func isBlank(value reflect.Value) bool {
+	switch value.Kind() {
+	case reflect.String:
+		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())
+}
+
+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
+	}
+}

+ 88 - 0
utils/whereBuild.go

@@ -0,0 +1,88 @@
+package utils
+
+import (
+	"fmt"
+	"strings"
+)
+
+type NullType byte
+
+const (
+	_ NullType = iota
+	// IsNull the same as `is null`
+	IsNull
+	// IsNotNull the same as `is not null`
+	IsNotNull
+)
+
+// sql生成工具
+func WhereBuild(where map[string]interface{}) (whereSQL string, vals []interface{}, err error) {
+	for k, v := range where {
+		ks := strings.Split(k, " ")
+		if len(ks) > 2 {
+			return "", nil, fmt.Errorf("Error in query condition: %s. ", k)
+		}
+
+		if whereSQL != "" {
+			whereSQL += " AND "
+		}
+		strings.Join(ks, ",")
+		switch len(ks) {
+		case 1:
+			//fmt.Println(reflect.TypeOf(v))
+			switch v := v.(type) {
+			case NullType:
+				if v == IsNotNull {
+					whereSQL += fmt.Sprint(k, " IS NOT NULL")
+				} else {
+					whereSQL += fmt.Sprint(k, " IS NULL")
+				}
+			default:
+				whereSQL += fmt.Sprint(k, "=?")
+				vals = append(vals, v)
+			}
+			break
+		case 2:
+			k = ks[0]
+			switch ks[1] {
+			case "=":
+				whereSQL += fmt.Sprint(k, "=?")
+				vals = append(vals, v)
+				break
+			case ">":
+				whereSQL += fmt.Sprint(k, ">?")
+				vals = append(vals, v)
+				break
+			case ">=":
+				whereSQL += fmt.Sprint(k, ">=?")
+				vals = append(vals, v)
+				break
+			case "<":
+				whereSQL += fmt.Sprint(k, "<?")
+				vals = append(vals, v)
+				break
+			case "<=":
+				whereSQL += fmt.Sprint(k, "<=?")
+				vals = append(vals, v)
+				break
+			case "!=":
+				whereSQL += fmt.Sprint(k, "!=?")
+				vals = append(vals, v)
+				break
+			case "<>":
+				whereSQL += fmt.Sprint(k, "!=?")
+				vals = append(vals, v)
+				break
+			case "in":
+				whereSQL += fmt.Sprint(k, " in (?)")
+				vals = append(vals, v)
+				break
+			case "like":
+				whereSQL += fmt.Sprint(k, " like ?")
+				vals = append(vals, v)
+			}
+			break
+		}
+	}
+	return
+}

Some files were not shown because too many files changed in this diff