Roc 8 months ago
parent
commit
5eef6df83a
82 changed files with 10126 additions and 0 deletions
  1. 12 0
      .gitignore
  2. 128 0
      config/config.go
  3. 40 0
      controller/auth.go
  4. 155 0
      controller/resp/base.go
  5. 107 0
      controller/xiangyu/auth.go
  6. 63 0
      controller/xiangyu/crm.go
  7. 126 0
      controller/xiangyu/index.go
  8. 56 0
      controller/xiangyu/user.go
  9. 161 0
      core/log.go
  10. 32 0
      core/run_server.go
  11. 89 0
      docs/docs.go
  12. 21 0
      docs/swagger.json
  13. 16 0
      docs/swagger.yaml
  14. 82 0
      global/global.go
  15. 47 0
      global/validator.go
  16. 115 0
      go.mod
  17. 904 0
      go.sum
  18. 75 0
      init_serve/mysql.go
  19. 57 0
      init_serve/redis.go
  20. 23 0
      init_serve/router.go
  21. 29 0
      init_serve/task.go
  22. 0 0
      logic/logic.txt
  23. 102 0
      logic/xiangyu/auth.go
  24. 105 0
      logic/xiangyu/crm.go
  25. 73 0
      logic/xiangyu/index.go
  26. 439 0
      logic/xiangyu/user.go
  27. 10 0
      main.go
  28. 159 0
      middleware/auth.go
  29. 27 0
      middleware/common.go
  30. 27 0
      middleware/cors.go
  31. 57 0
      middleware/recover.go
  32. 55 0
      middleware/token.go
  33. 171 0
      models/base/base.go
  34. 165 0
      models/base/page.go
  35. 13 0
      models/base/time_base.go
  36. 25 0
      models/crm/open_api_user.go
  37. 99 0
      models/eta/admin.go
  38. 36 0
      models/eta/crm_config.go
  39. 49 0
      models/eta/sys_department.go
  40. 57 0
      models/eta/sys_group.go
  41. 46 0
      models/eta/sys_role.go
  42. 39 0
      models/eta/sys_session.go
  43. 36 0
      models/eta/sys_third_session.go
  44. 25 0
      models/eta/sys_user_login_record.go
  45. 36 0
      models/index/base.go
  46. 59 0
      models/index/business_sys_interaction_log.go
  47. 44 0
      models/index/edb_data_business.go
  48. 35 0
      models/index/edb_update_log.go
  49. 41 0
      models/rddp/report_attachment.go
  50. 39 0
      models/rddp/sys_session.go
  51. 25 0
      models/rddp/sys_user_login_record.go
  52. 11 0
      models/request/auth.go
  53. 16 0
      models/request/xiangyu/auth.go
  54. 52 0
      models/request/xiangyu/index.go
  55. 13 0
      models/response/auth.go
  56. 12 0
      routers/auth.go
  57. 52 0
      routers/xiangyu.go
  58. 31 0
      services/alarm_msg/alarm_msg.go
  59. 22 0
      services/company.go
  60. 199 0
      services/oss/ali_oss.go
  61. 109 0
      services/oss/aws_s3.go
  62. 76 0
      services/oss/minio.go
  63. 50 0
      services/oss/oss.go
  64. 104 0
      services/pcsg/mail.go
  65. 102 0
      services/session.go
  66. 372 0
      services/xiangyu/auth.go
  67. 228 0
      services/xiangyu/crm.go
  68. 402 0
      services/xiangyu/index.go
  69. 578 0
      services/xiangyu/user.go
  70. 72 0
      task/task.go
  71. 955 0
      utils/common.go
  72. 102 0
      utils/constants.go
  73. 187 0
      utils/des3.go
  74. 48 0
      utils/directory.go
  75. 357 0
      utils/drawtext.go
  76. 206 0
      utils/file.go
  77. 415 0
      utils/mail/imap.go
  78. 41 0
      utils/redis.go
  79. 294 0
      utils/redis/cluster_redis.go
  80. 286 0
      utils/redis/standalone_redis.go
  81. 214 0
      utils/validator.go
  82. 88 0
      utils/whereBuild.go

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+/.idea/
+/log/*
+latest_log
+latest_binlog
+latest_filelog
+/config/*.yaml
+.DS_Store
+/binlog
+/rdlucklog
+/*.exe
+eta_email_analysis
+static

+ 128 - 0
config/config.go

@@ -0,0 +1,128 @@
+package config
+
+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"`
+	Smm     Smm     `mapstructure:"smm" json:"smm" yaml:"smm"`
+	Xiangyu Xiangyu `mapstructure:"xiangyu" json:"xiangyu" yaml:"xiangyu"`
+	Email   Email   `mapstructure:"email" json:"email" yaml:"email"`
+	Oss     Oss     `mapstructure:"oss" json:"oss" yaml:"oss"`
+}
+
+// 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:"上传的文件存储目录地址"`
+
+	AppNameEn  string `mapstructure:"app-name-en" json:"app-name-en" yaml:"app-name-en" description:"项目名称英文"`
+	Md5Key     string `mapstructure:"md5-key" json:"md5-key" yaml:"md5-key" description:"Md5密钥"`
+	DesKey     string `mapstructure:"des-key" json:"des-key" yaml:"des-key" description:"Des密钥"`
+	IsStopTask bool   `mapstructure:"is-stop-task" json:"is-stop-task" yaml:"is-stop-task" description:"是否停止任务,此功能用于多服务器部署时,定时任务只需一台服务器执行,其他服务器不执行的情况下设置为true"`
+}
+
+// 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日志软链接"`
+	FilelogDirPath  string `mapstructure:"filelog-dir-path" json:"filelog-dir-path" yaml:"filelog-dir-path" description:"用户自主记录的日志目录"`
+	FilelogSoftLink string `mapstructure:"filelog-soft-link" json:"filelog-soft-link" yaml:"filelog-soft-link" description:"用户自主记录的日志软链接"`
+}
+
+// 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库"`
+	ServeType string `mapstructure:"serve-type" json:"serve-type" yaml:"serve-type" description:"redis服务类型,单机或者cluster;默认单机"`
+}
+
+// Smm smm配置
+type Smm struct {
+	Username string `mapstructure:"username" json:"username" yaml:"username" description:"smm账号"`
+	Password string `mapstructure:"password" json:"password" yaml:"password" description:"smm密码"`
+}
+
+// Xiangyu 象屿的配置
+type Xiangyu struct {
+	SystemCode            string `mapstructure:"system-code" json:"system-code" yaml:"system-code" description:"系统编码"`
+	UserSyncTarget        string `mapstructure:"user-sync-target" json:"user-sync-target" yaml:"user-sync-target" description:"用户同步平台编码"`
+	UserSyncHost          string `mapstructure:"user-sync-host" json:"user-sync-host" yaml:"user-sync-host" description:"用户同步平台地址"`
+	UserAuthHost          string `mapstructure:"user-auth-host" json:"user-auth-host" yaml:"user-auth-host" description:"用户认证平台地址"`
+	UserSyncAuthUserName  string `mapstructure:"user-sync-auth-user-name" json:"user-sync-auth-user-name" yaml:"user-sync-auth-user-name" description:"用户统一身份的鉴权username"`
+	UserSyncAuthPwd       string `mapstructure:"user-sync-auth-pwd" json:"user-sync-auth-pwd" yaml:"user-sync-auth-pwd" description:"用户统一身份的鉴权password"`
+	UserKey               string `mapstructure:"user-key" json:"user-key" yaml:"user-key" description:"统一平台秘钥"`
+	DefaultRoleId         int    `mapstructure:"default-role-id" json:"default-role-id" yaml:"default-role-id" description:"默认的角色id"`
+	IndexSyncHost         string `mapstructure:"index-sync-host" json:"index-sync-host" yaml:"index-sync-host" description:"指标同步平台地址"`
+	IndexSyncAuthUserName string `mapstructure:"index-sync-auth-user-name" json:"index-sync-auth-user-name" yaml:"index-sync-auth-user-name" description:"用户统一身份的鉴权username"`
+	IndexSyncAuthPwd      string `mapstructure:"index-sync-auth-pwd" json:"index-sync-auth-pwd" yaml:"index-sync-auth-pwd" description:"用户统一身份的鉴权password"`
+	IndexKey              string `mapstructure:"index-key" json:"index-key" yaml:"index-key" description:"统一平台秘钥"`
+	IndexSyncTarget       string `mapstructure:"index-sync-target" json:"index-sync-target" yaml:"index-sync-target" description:"指标同步平台编码"`
+	IndexCrmHost          string `mapstructure:"index-crm-host" json:"index-crm-host" yaml:"index-crm-host" description:"crm指标平台地址"`
+	IndexCrmTarget        string `mapstructure:"index-crm-target" json:"index-crm-target" yaml:"index-crm-target" description:"crm指标平台编码"`
+}
+
+type Email struct {
+	Host          string `mapstructure:"host" json:"host" yaml:"host" description:"邮件服务器"`
+	Port          int    `mapstructure:"port" json:"port" yaml:"port" description:"邮件服务器端口"`
+	UserName      string `mapstructure:"username" json:"username" yaml:"username" description:"邮件服务器用户名"`
+	Password      string `mapstructure:"password" json:"password" yaml:"password" description:"邮件服务器密码"`
+	Folder        string `mapstructure:"folder" json:"folder" yaml:"folder" description:"邮件服务器文件夹"`
+	ReadBatchSize int    `mapstructure:"read-batch-size" json:"read-batch-size" yaml:"read-batch-size" description:"邮件读取批次"`
+}
+
+type Oss struct {
+	ObjectStorageClient string `mapstructure:"object-storage-client" json:"object-storage-client" yaml:"object-storage-client" description:"oss类型,取值:oss minio,s3,默认oss"`
+	EndPoint            string `mapstructure:"end-point" json:"end-point" yaml:"end-point" description:"oss服务地址"`
+	BackEndPoint        string `mapstructure:"back-end-point" json:"back-end-point" yaml:"back-end-point" description:"oss服务地址(用于内网)"`
+	AccessKeyId         string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id" description:"oss服务access-key-id"`
+	AccessKeySecret     string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret" description:"oss服务access-key-secret"`
+	BucketName          string `mapstructure:"bucket-name" json:"bucket-name" yaml:"bucket-name" description:"oss服务bucketname"`
+	RegionId            string `mapstructure:"region-id" json:"region-id" yaml:"region-id" description:"oss服务region-id"`
+	ImgHost             string `mapstructure:"img-host" json:"img-host" yaml:"img-host" description:"oss服务imghost"`
+	UploadDir           string `mapstructure:"upload-dir" json:"upload-dir" yaml:"upload-dir" description:"oss服务上传路径"`
+	Port                string `mapstructure:"port" json:"port" yaml:"port" description:"oss服务端口"`
+	ForceStyle          bool   `mapstructure:"force-style" json:"force-style" yaml:"force-style" description:"oss服务是否强制使用style,默认是True"`
+	Protocol            string `mapstructure:"protocol" json:"protocol" yaml:"protocol" description:"oss服务协议,默认是http"`
+	DisableSSL          bool   `mapstructure:"disable-ssl" json:"disable-ssl" yaml:"disable-ssl" description:"是否禁用ssl"`
+	OpenAcl             bool   `mapstructure:"open-acl" json:"open-acl" yaml:"open-acl" description:"是否开启acl"`
+	ResourceProxyUrl    string `mapstructure:"resource-proxy-url" json:"resource-proxy-url" yaml:"resource-proxy-url" description:"资源代理地址"`
+	Ram                 OssRam `mapstructure:"ram" json:"ram" yaml:"ram"`
+}
+
+type OssRam struct {
+	AccessKeyId      string `mapstructure:"access-key-id" json:"access-key-id" yaml:"access-key-id" description:"oss服务access-key-id"`
+	AccessKeySecret  string `mapstructure:"access-key-secret" json:"access-key-secret" yaml:"access-key-secret" description:"oss服务access-key-secret"`
+	StsScheme        string `mapstructure:"sts-scheme" json:"sts-scheme" yaml:"sts-scheme" description:"oss服务sts-scheme"`
+	RoleArn          string `mapstructure:"role-arn" json:"role-arn" yaml:"role-arn" description:"oss服务role-arn"`
+	RoleSessionName  string `mapstructure:"role-session-name" json:"role-session-name" yaml:"role-session-name" description:"oss服务role-session-name"`
+	STSTokenCacheKey string `mapstructure:"sts-token-cache-key" json:"sts-token-cache-key" yaml:"sts-token-cache-key" description:"oss服务sts-token-cache-key(缓存key,用于获取后短期内不再重复获取)"`
+}

+ 40 - 0
controller/auth.go

@@ -0,0 +1,40 @@
+package controller
+
+type AuthController struct{}
+
+// CreateAuthCode
+// @Description 生成编码
+// @Success 200 {string} string "获取成功"
+// @Router /auth/auth_code [post]
+//func (a *AuthController) CreateAuthCode(c *gin.Context) {
+//	var req request.CreateAuthCodeReq
+//	err := c.Bind(&req)
+//	if err != nil {
+//		errs, ok := err.(validator.ValidationErrors)
+//		if !ok {
+//			resp.FailData("参数解析失败", "Err:"+err.Error(), c)
+//			return
+//		}
+//		resp.FailData("参数解析失败", errs.Translate(global.Trans), c)
+//		return
+//	}
+//
+//	prefix := map[int]string{
+//		utils.SOURCE_CRM_FLAG: utils.CACHE_ETA_AUTH_CODE_PREFIX,
+//		utils.SOURCE_ETA_FLAG: utils.CACHE_CRM_AUTH_CODE_PREFIX,
+//	}
+//	str := base64.URLEncoding.EncodeToString([]byte(req.AdminName))
+//	key := fmt.Sprint(prefix[req.Source], str)
+//
+//	// 是否已生成但未消费
+//	exist, _ := global.Rc.RedisString(key)
+//	if exist != "" {
+//		resp.OkData("获取成功", str, c)
+//		return
+//	}
+//	if global.Rc.SetNX(key, req.AdminName, utils.GetTodayLastSecond()) {
+//		resp.OkData("获取成功", str, c)
+//		return
+//	}
+//	return
+//}

+ 155 - 0
controller/resp/base.go

@@ -0,0 +1,155 @@
+package resp
+
+import (
+	"encoding/json"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"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" {
+		global.LOG.Info(strings.Join(logSlice, ";"))
+		c.JSON(code, resultData)
+	} else {
+		global.LOG.Info(strings.Join(logSlice, ";"))
+		encryptResult := utils.DesBase64Encrypt(jsonByte, global.CONFIG.Serve.DesKey)
+		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)
+}

+ 107 - 0
controller/xiangyu/auth.go

@@ -0,0 +1,107 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/logic/xiangyu"
+	xyReq "eta/eta_email_analysis/models/request/xiangyu"
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+)
+
+// GetToken
+// @Description: 获取token
+// @author: Roc
+// @receiver xc
+// @datetime 2024-01-23 17:06:34
+// @param c *gin.Context
+func (xc *XiangyuController) GetToken(c *gin.Context) {
+	var req xyReq.GetTokenReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+	if req.Code == "" {
+		resp.FailMsg("请传入code码", "请输入指标code码", c)
+		return
+	}
+
+	result, err, errMsg := xiangyu.LoginEta(req.Code)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("获取成功", result, c)
+
+	return
+}
+
+// RefreshToken
+// @Description: 刷新token
+// @author: Roc
+// @receiver xc
+// @datetime 2024-01-23 17:06:25
+// @param c *gin.Context
+func (xc *XiangyuController) RefreshToken(c *gin.Context) {
+	var req xyReq.RefreshTokenReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+	if req.RefreshToken == "" {
+		resp.FailMsg("请传入refresh_token", "请传入refresh_token", c)
+		return
+	}
+
+	err := xiangyu.RefreshToken(req.RefreshToken)
+
+	if err != nil {
+		resp.FailData("参数解析失败", err.Error(), c)
+		return
+	}
+	resp.Ok("刷新成功", c)
+	return
+}
+
+// RevokeToken
+// @Description: 销毁token
+// @author: Roc
+// @receiver xc
+// @datetime 2024-01-23 17:06:12
+// @param c *gin.Context
+func (xc *XiangyuController) RevokeToken(c *gin.Context) {
+	var req xyReq.RevokeTokenReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+	if req.Token == "" {
+		resp.FailMsg("请传入token", "请传入token", c)
+		return
+	}
+
+	err := xiangyu.RevokeToken(req.Token)
+
+	if err != nil {
+		resp.FailData("参数解析失败", err.Error(), c)
+		return
+	}
+	resp.Ok("销毁成功", c)
+	return
+}

+ 63 - 0
controller/xiangyu/crm.go

@@ -0,0 +1,63 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/logic/xiangyu"
+	"eta/eta_email_analysis/models/index"
+	xiangyuSrc "eta/eta_email_analysis/services/xiangyu"
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+)
+
+// GetCrmData
+// @Description: 获取crm数据
+// @author: Roc
+// @receiver xc
+// @datetime 2024-5-14 13:09:30
+// @param c *gin.Context
+func (xc *XiangyuController) GetCrmData(c *gin.Context) {
+	var req index.GetBusinessDataReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+
+	if req.IndexCode == `` && req.IndexPkgCode == `` {
+		resp.Fail("指标编码或者指标包编码不允许为空", c)
+	}
+	//if req.Partition == `` {
+	//	resp.Fail("数据分区参数不允许为空", c)
+	//}
+
+	xyReq := xiangyuSrc.PostGetIndexDataParamReq{
+		PageNum:                   req.CurrentIndex,
+		PageSize:                  req.PageSize,
+		AssetCd:                   req.IndexCode,
+		AssetPkgCd:                req.IndexPkgCode,
+		DataDt:                    "",
+		StartDt:                   "",
+		EndDt:                     "",
+		Sort:                      "",
+		DataSourceType:            req.DataSourceType,
+		InfoLastUpdateStartTime:   req.InfoLastUpdateStartTime,
+		InfoLastUpdateEndTime:     req.InfoLastUpdateEndTime,
+		DetailLastUpdateStartTime: req.DetailLastUpdateStartTime,
+		DetailLastUpdateEndTime:   req.DetailLastUpdateEndTime,
+	}
+
+	dataResp, err, errMsg := xiangyu.PostGetIndexData(xyReq)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("同步成功", dataResp, c)
+
+	return
+}

+ 126 - 0
controller/xiangyu/index.go

@@ -0,0 +1,126 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/logic/xiangyu"
+	xiangyuSrc "eta/eta_email_analysis/services/xiangyu"
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+)
+
+// PushIndexDataResp
+// @Description: 指标信息推送
+// @author: Roc
+// @receiver xc
+// @datetime 2024-02-27 17:53:24
+// @param c *gin.Context
+func (xc *XiangyuController) PushIndexDataResp(c *gin.Context) {
+	var req xiangyuSrc.PushBaseParamReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+
+	err, errMsg := xiangyu.PushIndexDataResp(req)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("同步成功", "", c)
+
+	return
+}
+
+// PushIndexValueDataResp
+// @Description: 指标日期值信息推送
+// @author: Roc
+// @receiver xc
+// @datetime 2024-02-27 17:53:24
+// @param c *gin.Context
+func (xc *XiangyuController) PushIndexValueDataResp(c *gin.Context) {
+	var req xiangyuSrc.PushBaseParamReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+
+	err, errMsg := xiangyu.PushEdbValueDataResp(req)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("同步成功", "", c)
+
+	return
+}
+
+// PushClassifyDataResp
+// @Description: 指标分类信息推送
+// @author: Roc
+// @receiver xc
+// @datetime 2024-02-27 17:53:24
+// @param c *gin.Context
+func (xc *XiangyuController) PushClassifyDataResp(c *gin.Context) {
+	var req xiangyuSrc.PushBaseParamReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+
+	err, errMsg := xiangyu.PushClassifyDataResp(req)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("同步成功", "", c)
+
+	return
+}
+
+// PushEdbClassifyDataResp
+// @Description: 指标与分类的关系信息推送
+// @author: Roc
+// @receiver xc
+// @datetime 2024-02-27 17:53:24
+// @param c *gin.Context
+func (xc *XiangyuController) PushEdbClassifyDataResp(c *gin.Context) {
+	var req xiangyuSrc.PushBaseParamReq
+	if e := c.Bind(&req); e != nil {
+		err, ok := e.(validator.ValidationErrors)
+		if !ok {
+			resp.FailData("参数解析失败", "Err:"+e.Error(), c)
+			return
+		}
+		resp.FailData("参数解析失败", err.Translate(global.Trans), c)
+		return
+	}
+
+	err, errMsg := xiangyu.PushEdbClassifyDataResp(req)
+
+	if err != nil {
+		resp.FailData(errMsg, err.Error(), c)
+		return
+	}
+	resp.OkData("同步成功", "", c)
+
+	return
+}

+ 56 - 0
controller/xiangyu/user.go

@@ -0,0 +1,56 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/logic/xiangyu"
+	"github.com/gin-gonic/gin"
+	"sync"
+)
+
+// XiangyuController 象屿指标
+type XiangyuController struct{}
+
+// 同步用户锁,防止重复同步,不管是全量还是增量,都是同一时间只能一个同步
+var lockSyncUser sync.Mutex
+
+// SyncUser
+// @Description: 全量同步用户
+// @author: Roc
+// @receiver xc
+// @datetime 2024-01-22 15:51:25
+// @param c *gin.Context
+func (xc *XiangyuController) SyncUser(c *gin.Context) {
+	lockSyncUser.Lock()
+	defer func() {
+		lockSyncUser.Unlock()
+	}()
+	err := xiangyu.SyncUser()
+	if err != nil {
+		resp.FailData("同步失败", err.Error(), c)
+		return
+	}
+	resp.Ok("同步成功", c)
+
+	return
+}
+
+// PullUser
+// @Description: 增量同步用户
+// @author: Roc
+// @receiver xc
+// @datetime 2024-01-23 17:13:07
+// @param c *gin.Context
+func (xc *XiangyuController) PullUser(c *gin.Context) {
+	lockSyncUser.Lock()
+	defer func() {
+		lockSyncUser.Unlock()
+	}()
+	err := xiangyu.PullUser()
+	if err != nil {
+		resp.FailData("同步失败", err.Error(), c)
+		return
+	}
+	resp.Ok("同步成功", c)
+
+	return
+}

+ 161 - 0
core/log.go

@@ -0,0 +1,161 @@
+package core
+
+import (
+	"eta/eta_email_analysis/config"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
+	oplogging "github.com/op/go-logging"
+	"io"
+	"os"
+	"strings"
+	"time"
+)
+
+const (
+	Module = "eta_email_analysis"
+)
+
+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()
+
+	// 初始化自定义输出的日志
+	initFileLog()
+}
+
+// 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
+}
+
+// initFileLog 初始化自定义日志
+func initFileLog() {
+	logConfig := global.CONFIG.Log
+
+	// 没有单独配置的话,那就走默认的日志输出
+	if logConfig.FilelogDirPath == "" || logConfig.FilelogSoftLink == `` {
+		global.FILE_LOG = global.LOG
+		return
+	}
+
+	logger := oplogging.MustGetLogger(Module)
+	var backends []oplogging.Backend
+	//注册控制台输出
+	//registerStdout(logConfig, &backends)
+
+	// 注册文件输出(日志文件)
+	_ = registerFile(logConfig, &backends, logConfig.FilelogDirPath, logConfig.FilelogSoftLink)
+	defaultBackend := oplogging.AddModuleLevel(backends[0])
+	logger.SetBackend(defaultBackend)
+	global.FILE_LOG = logger
+}
+
+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)
+}

+ 32 - 0
core/run_server.go

@@ -0,0 +1,32 @@
+package core
+
+import (
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/init_serve"
+	"fmt"
+)
+
+func RunServe() {
+	// 初始化路由
+	r := init_serve.InitRouter()
+
+	// 如果配置了redis,那么链接redis
+	if global.CONFIG.Serve.UseRedis {
+		//初始化redis
+		init_serve.Redis()
+	}
+
+	// 初始化mysql数据库
+	init_serve.Mysql()
+
+	// 启动任务
+	init_serve.InitTask()
+
+	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(fmt.Errorf("服务启动失败,Err:%s", err))
+	} else {
+		fmt.Println("Run:", fmt.Sprint("0.0.0.0:", global.CONFIG.Serve.Port))
+	}
+}

+ 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{})
+}

+ 21 - 0
docs/swagger.json

@@ -0,0 +1,21 @@
+{
+    "swagger": "2.0",
+    "info": {
+        "description": "弘则人力资源管理系统API接口文档",
+        "title": "弘则人力资源管理系统API接口文档",
+        "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": "1.0"
+    },
+    "host": "127.0.0.1:8607",
+    "basePath": "/",
+    "paths": {}
+}

+ 16 - 0
docs/swagger.yaml

@@ -0,0 +1,16 @@
+basePath: /
+host: 127.0.0.1:8607
+info:
+  contact:
+    email: pyan@hzinsights.com
+    name: www.hzinsights.com/
+    url: https://www.hzinsights.com/
+  description: 弘则人力资源管理系统API接口文档
+  license:
+    name: Apache 2.0
+    url: http://www.apache.org/licenses/LICENSE-2.0.html
+  termsOfService: https://www.hzinsights.com/
+  title: 弘则人力资源管理系统API接口文档
+  version: "1.0"
+paths: {}
+swagger: "2.0"

+ 82 - 0
global/global.go

@@ -0,0 +1,82 @@
+package global
+
+import (
+	"database/sql"
+	"eta/eta_email_analysis/config"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/fsnotify/fsnotify"
+	oplogging "github.com/op/go-logging"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+	"io"
+)
+
+var (
+	CONFIG        config.Config //配置文件
+	LOG           *oplogging.Logger
+	FILE_LOG      *oplogging.Logger   // 自定义的输出日志
+	MYSQL         map[string]*gorm.DB //数据库连接配置
+	MYSQL_LOG     io.Writer
+	DEFAULT_MYSQL *gorm.DB //默认数据库连接配置
+	//Redis         *redis.Client //redis链接
+	OracleJy *sql.DB //嘉悦物产数据库连接
+
+	//Rc *cache.Cache //redis缓存
+	Rc utils.RedisClient //redis缓存
+	Re error             //redis错误
+)
+
+const ConfigFile = "config/config_debug.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(fmt.Errorf("读取配置失败,Err: %s \n", err))
+	}
+
+	//持续监听文件
+	v.WatchConfig()
+
+	v.OnConfigChange(func(e fsnotify.Event) {
+		fmt.Println("配置文件变更:", e.Name)
+		handleConfig(v, "配置重赋值")
+	})
+	handleConfig(v, "配置初始化")
+
+	return
+}
+
+// handleConfig
+// @Description: 配置文件处理
+// @author: Roc
+// @datetime 2024-03-13 10:27:40
+// @param v *viper.Viper
+// @param handleType string
+func handleConfig(v *viper.Viper, handleType string) {
+	if err := v.Unmarshal(&CONFIG); err != nil {
+		fmt.Println(handleType+"赋值失败,Err:", err)
+	}
+
+	//if CONFIG.Mysql.Binlog.LogSaveDay == 0 {
+	//	CONFIG.Mysql.Binlog.LogSaveDay = 31
+	//}
+
+	//fmt.Println(CONFIG)
+	//fmt.Println(CONFIG.Mysql.UpdateLogSaveDay)
+}

+ 47 - 0
global/validator.go

@@ -0,0 +1,47 @@
+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
+}

+ 115 - 0
go.mod

@@ -0,0 +1,115 @@
+module eta/eta_email_analysis
+
+go 1.21.7
+
+require (
+	github.com/astaxie/beego v1.12.3
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/emersion/go-imap v1.2.1
+	github.com/emersion/go-message v0.15.0
+	github.com/fsnotify/fsnotify v1.6.0
+	github.com/gin-gonic/gin v1.9.1
+	github.com/go-playground/locales v0.14.1
+	github.com/go-playground/universal-translator v0.18.1
+	github.com/go-playground/validator/v10 v10.14.0
+	github.com/go-redis/redis/v8 v8.11.5
+	github.com/godror/godror v0.40.3
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+	github.com/h2non/filetype v1.1.3
+	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
+	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
+	github.com/rdlucklib/rdluck_tools v1.0.3
+	github.com/spf13/viper v1.16.0
+	github.com/swaggo/swag v1.16.1
+	github.com/tealeg/xlsx v1.0.5
+	golang.org/x/image v0.9.0
+	golang.org/x/text v0.17.0
+	gorm.io/driver/mysql v1.5.1
+	gorm.io/gorm v1.25.2
+)
+
+require (
+	github.com/BurntSushi/toml v0.3.1 // indirect
+	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/aliyun/alibaba-cloud-sdk-go v1.63.13 // indirect
+	github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible // indirect
+	github.com/aws/aws-sdk-go v1.55.5 // indirect
+	github.com/benbjohnson/clock v1.1.0 // indirect
+	github.com/bytedance/sonic v1.9.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
+	github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+	github.com/garyburd/redigo v1.6.3 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-ini/ini v1.67.0 // indirect
+	github.com/go-logfmt/logfmt v0.6.0 // indirect
+	github.com/go-mysql-org/go-mysql v1.7.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.3 // indirect
+	github.com/godror/knownpb v0.1.1 // indirect
+	github.com/google/uuid v1.6.0 // 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/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/jonboulle/clockwork v0.4.0 // indirect
+	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/compress v1.17.9 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
+	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/lestrrat-go/strftime v1.0.6 // indirect
+	github.com/magiconair/properties v1.8.7 // indirect
+	github.com/mailru/easyjson v0.7.6 // indirect
+	github.com/mattn/go-isatty v0.0.19 // indirect
+	github.com/minio/md5-simd v1.1.2 // indirect
+	github.com/minio/minio-go/v7 v7.0.76 // 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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
+	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+	github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
+	github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
+	github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/robfig/cron/v3 v3.0.1 // indirect
+	github.com/rs/xid v1.6.0 // indirect
+	github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
+	github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 // indirect
+	github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 // indirect
+	github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 // indirect
+	github.com/spf13/afero v1.9.5 // indirect
+	github.com/spf13/cast v1.5.1 // 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.11 // indirect
+	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/multierr v1.8.0 // indirect
+	go.uber.org/zap v1.21.0 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.26.0 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/sys v0.24.0 // indirect
+	golang.org/x/time v0.1.0 // indirect
+	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 904 - 0
go.sum

@@ -0,0 +1,904 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
+github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.13 h1:eqUG3EF9o1oxYxQbLq7yOyPRqNZSyLzzdZIuXZX1Weg=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.13/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
+github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
+github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
+github.com/astaxie/beego v1.12.3 h1:SAQkdD2ePye+v8Gn1r4X6IKZM1wd28EyUOVQ3PDSOOQ=
+github.com/astaxie/beego v1.12.3/go.mod h1:p3qIm0Ryx7zeBHLljmd7omloyca1s4yu1a8kM1FkpIA=
+github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
+github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ=
+github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU=
+github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/couchbase/go-couchbase v0.0.0-20200519150804-63f3cdb75e0d/go.mod h1:TWI8EKQMs5u5jLKW/tsb9VwauIrMIxQG1r5fMsswK5U=
+github.com/couchbase/gomemcached v0.0.0-20200526233749-ec430f949808/go.mod h1:srVSlQLB8iXBVXHgnqemxUXqN6FCvClgCMPCsjBDR7c=
+github.com/couchbase/goutils v0.0.0-20180530154633-e865a1461c8a/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY=
+github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
+github.com/cznic/sortutil v0.0.0-20181122101858-f5f958428db8/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ=
+github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI=
+github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
+github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
+github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
+github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY=
+github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/garyburd/redigo v1.6.3 h1:HCeeRluvAgMusMomi1+6Y5dmFOdYV/JzoRrrbFlkGIc=
+github.com/garyburd/redigo v1.6.3/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-mysql-org/go-mysql v1.7.0 h1:qE5FTRb3ZeTQmlk3pjE+/m2ravGxxRDrVDTyDe9tvqI=
+github.com/go-mysql-org/go-mysql v1.7.0/go.mod h1:9cRWLtuXNKhamUPMkrDVzBhaomGvqLRLtBiyjvjc4pk=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/godror/godror v0.40.3 h1:PrelvDGjhMHEARnWAHgafv9534SJfn+wkmJ7fVHaCZo=
+github.com/godror/godror v0.40.3/go.mod h1:/ITLUvKT2lTItLwW/okueC5j0Ykn8LbPmVBy5gXpTBc=
+github.com/godror/knownpb v0.1.1 h1:A4J7jdx7jWBhJm18NntafzSC//iZDHkDi1+juwQ5pTI=
+github.com/godror/knownpb v0.1.1/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
+github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
+github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
+github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
+github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.76 h1:9nxHH2XDai61cT/EFhyIw/wW4vJfpPNvl7lSFpRt+Ng=
+github.com/minio/minio-go/v7 v7.0.76/go.mod h1:AVM3IUN6WwKzmwBxVdjzhH8xq+f57JSbbvzqvUzR6eg=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
+github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
+github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
+github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ=
+github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
+github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 h1:+FZIDR/D97YOPik4N4lPDaUcLDF/EQPogxtlHB2ZZRM=
+github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=
+github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 h1:k2BbABz9+TNpYRwsCCFS8pEEnFVOdbgEjL/kTlLuzZQ=
+github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7/go.mod h1:8AanEdAHATuRurdGxZXBz0At+9avep+ub7U1AGYLIMM=
+github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d h1:1DyyRrgYeNjqPkgjrdEsaIbX+kHpuTTk5ZOCtrcRFcQ=
+github.com/pingcap/tidb/parser v0.0.0-20221126021158-6b02a5d8ba7d/go.mod h1:ElJiub4lRy6UZDb+0JHDkGEdr6aOli+ykhyej7VCLoI=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/rdlucklib/rdluck_tools v1.0.3 h1:iOtK2QPlPQ6CL6c1htCk5VnFCHzyG6DCfJtunrMswK0=
+github.com/rdlucklib/rdluck_tools v1.0.3/go.mod h1:9Onw9o4w19C8KE5lxb8GyxgRBbZweRVkQSc79v38EaA=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
+github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
+github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik=
+github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
+github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM=
+github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw=
+github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 h1:oI+RNwuC9jF2g2lP0u0cVEEZrc/AYBCuFdvwrLWM/6Q=
+github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07/go.mod h1:yFdBgwXP24JziuRl2NMUahT7nGLNOKi1SIiFxMttVD4=
+github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s=
+github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
+github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
+github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
+github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
+github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
+github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
+github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
+github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
+github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
+github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/wendal/errors v0.0.0-20130201093226-f66c77a7882b/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc=
+github.com/ylywyn/jpush-api-go-client v0.0.0-20190906031852-8c4466c6e369/go.mod h1:Nv7wKD2/bCdKUFNKcJRa99a+1+aSLlCRJFriFYdjz/I=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
+go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20181106170214-d68db9428509/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
+golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA=
+golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
+gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
+gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
+gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
+modernc.org/golex v1.0.1/go.mod h1:QCA53QtsT1NdGkaZZkF5ezFwk4IXh4BGNafAARTC254=
+modernc.org/lex v1.0.0/go.mod h1:G6rxMTy3cH2iA0iXL/HRRv4Znu8MK4higxph/lE7ypk=
+modernc.org/lexer v1.0.0/go.mod h1:F/Dld0YKYdZCLQ7bD0USbWL4YKCyTDRDHiDTOs0q0vk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/parser v1.0.0/go.mod h1:H20AntYJ2cHHL6MHthJ8LZzXCdDCHMWt1KZXtIMjejA=
+modernc.org/parser v1.0.2/go.mod h1:TXNq3HABP3HMaqLK7brD1fLA/LfN0KS6JxZn71QdDqs=
+modernc.org/scanner v1.0.1/go.mod h1:OIzD2ZtjYk6yTuyqZr57FmifbM9fIH74SumloSsajuE=
+modernc.org/sortutil v1.0.0/go.mod h1:1QO0q8IlIlmjBIwm6t/7sof874+xCfZouyqZMLIAtxM=
+modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/y v1.0.1/go.mod h1:Ho86I+LVHEI+LYXoUKlmOMAM1JTXOCfj8qi1T8PsClE=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

+ 75 - 0
init_serve/mysql.go

@@ -0,0 +1,75 @@
+package init_serve
+
+import (
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"io"
+	"log"
+	"os"
+	"time"
+)
+
+// Mysql 数据库初始化
+func Mysql() {
+	mysqlConf := global.CONFIG.Mysql
+	if len(mysqlConf.List) <= 0 {
+		global.LOG.Error("mysql链接未配置")
+		panic(fmt.Errorf("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(fmt.Errorf("mysql 启动异常,数据库:", conf.AliasName, "Err:%s", err))
+		}
+
+		//创建连接池
+		sqlDB, err := db.DB()
+		if err != nil {
+			global.LOG.Errorf("mysql 创建连接池失败,数据库:", conf.AliasName, ";Err:", err)
+			panic(fmt.Errorf("mysql 创建连接池失败,数据库:", conf.AliasName, "Err:%s", err))
+		}
+
+		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
+
+	fmt.Println("mysql init end")
+}

+ 57 - 0
init_serve/redis.go

@@ -0,0 +1,57 @@
+package init_serve
+
+import (
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+)
+
+//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("redis 链接失败:" + err.Error())
+//	}
+//
+//	//全局赋值redis链接
+//	global.Redis = client
+//}
+//
+//func RedisTool() {
+//	fmt.Println("init RedisTool start")
+//	redisConf := global.CONFIG.Redis
+//	fmt.Println(redisConf)
+//	REDIS_CACHE := fmt.Sprintf(`{"key":"redis","conn":"%s","password":"%s"}`, redisConf.Address, redisConf.Password)
+//	fmt.Println("REDIS_CACHE:" + REDIS_CACHE)
+//	global.Rc, global.Re = cache.NewCache(REDIS_CACHE) //初始化缓存
+//	if global.Re != nil {
+//		fmt.Println(global.Re)
+//		panic(global.Re)
+//	}
+//	fmt.Println("init RedisTool end")
+//}
+
+func Redis() {
+	var conf string
+	//switch global.CONFIG.Redis.ServeType {
+	//case "cluster": // 集群
+	//	conf = fmt.Sprintf(`{"key":"redis","conn":"%s","password":"%s"}`,global.CONFIG.Redis.Address, global.CONFIG.Redis.Password)
+	//default: // 默认走单机
+	//	redisClient, err = redis.InitStandaloneRedis(conf)
+	//}
+	conf = fmt.Sprintf(`{"key":"redis","conn":"%s","password":"%s"}`, global.CONFIG.Redis.Address, global.CONFIG.Redis.Password)
+	redisClient, err := utils.InitRedis(global.CONFIG.Redis.ServeType, conf)
+
+	if err != nil {
+		fmt.Println("redis链接异常:", err)
+		panic(any(err))
+	}
+	global.Rc = redisClient
+}

+ 23 - 0
init_serve/router.go

@@ -0,0 +1,23 @@
+package init_serve
+
+import (
+	_ "eta/eta_email_analysis/docs"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/middleware"
+	"eta/eta_email_analysis/routers"
+	"github.com/gin-gonic/gin"
+)
+
+// InitRouter 初始化路由
+func InitRouter() (r *gin.Engine) {
+	gin.SetMode(global.CONFIG.Serve.RunMode)
+
+	r = gin.Default()
+	r.Use(middleware.Cors())
+	r.Use(middleware.Recover())
+
+	rBase := r.Group("api/")
+	routers.InitAuth(rBase)
+	routers.InitXiangyu(rBase)
+	return
+}

+ 29 - 0
init_serve/task.go

@@ -0,0 +1,29 @@
+package init_serve
+
+import (
+	"eta/eta_email_analysis/global"
+	"github.com/robfig/cron/v3"
+)
+
+func InitTask() {
+	// 如果配置文件中设置了停止任务,则不执行定时任务
+	// 此功能用于多服务器部署时,定时任务只需一台服务器执行,其他服务器不执行;所以只需要一台服务器设置为false,其他服务器设置为true
+	if global.CONFIG.Serve.IsStopTask {
+		return
+	}
+
+	// 开始定时任务
+	c := cron.New(cron.WithSeconds())
+
+	//// 定时清除指标更新日志
+	//if global.CONFIG.Mysql.Binlog.IsListen {
+	//	_, err := c.AddFunc("0 0 0 * * *", index_data.DeleteBeforeTenDayLog)
+	//	if err != nil {
+	//		global.LOG.Error("DeleteBeforeTenDayLog err" + err.Error())
+	//	}
+	//}
+
+	//pcsg.ListenMail()
+
+	c.Start()
+}

+ 0 - 0
logic/logic.txt


+ 102 - 0
logic/xiangyu/auth.go

@@ -0,0 +1,102 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/models/eta"
+	"eta/eta_email_analysis/models/response"
+	"eta/eta_email_analysis/services"
+	"eta/eta_email_analysis/services/xiangyu"
+	"fmt"
+	"time"
+)
+
+// LoginEta
+// @Description: 获取eta的session
+// @author: Roc
+// @datetime 2024-01-23 17:44:15
+// @param code string
+// @return resp response.LoginResp
+// @return err error
+// @return errMsg string
+func LoginEta(code string) (resp response.LoginResp, err error, errMsg string) {
+	//  获取xiangyu token
+	tokenResp, err := xiangyu.GetToken(code)
+	if err != nil {
+		return
+	}
+
+	//// 获取eta用户信息
+	//adminInfo, err := eta.GetSysUserByOutId(tokenResp.Uid)
+	//if err != nil {
+	//	return
+	//}
+
+	// 获取用户信息
+	userInfoResp, err := xiangyu.GetUserInfo(tokenResp.AccessToken)
+	if err != nil {
+		return
+	}
+
+	// 获取eta用户信息
+	adminInfo, err := eta.GetSysUserByAdminName(userInfoResp.LoginName)
+	if err != nil {
+		return
+	}
+
+	// 生成登录session
+	resp, session, err, errMsg := services.CreateEtaSession(adminInfo)
+	if err != nil {
+		return
+	}
+
+	// 记录两方session的绑定信息
+	thirdSession := &eta.SysThirdSession{
+		ID:               0,
+		SysUserID:        uint64(session.SysUserId),
+		UserName:         session.UserName,
+		AccessToken:      session.AccessToken,
+		ThirdAccessToken: tokenResp.AccessToken,
+		//ThirdRefreshToken: tokenResp.RefreshToken,
+		ThirdExpiredTime: time.Now().Add(time.Duration(tokenResp.ExpiresIn-100) * time.Second),
+		CreatedTime:      time.Now(),
+		LastUpdatedTime:  time.Now(),
+	}
+	eta.AddSysThirdSession(thirdSession)
+
+	return
+}
+
+func GetToken(code string) (err error) {
+	fmt.Println(xiangyu.GetToken(code))
+
+	return
+}
+
+func GetUserInfo(token string) (err error) {
+	fmt.Println(xiangyu.GetUserInfo(token))
+
+	return
+}
+
+func CheckToken(token string) (err error) {
+	fmt.Println(xiangyu.CheckToken(token))
+
+	return
+}
+
+func GetTokenInfo(token string) (err error) {
+	fmt.Println(xiangyu.GetTokenInfo(token))
+
+	return
+}
+
+func RefreshToken(token string) (err error) {
+	fmt.Println(xiangyu.RefreshToken(token))
+
+	return
+}
+
+func RevokeToken(token string) (err error) {
+	fmt.Println(xiangyu.RevokeToken(token))
+
+	return
+}

+ 105 - 0
logic/xiangyu/crm.go

@@ -0,0 +1,105 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/models/index"
+	"eta/eta_email_analysis/services/xiangyu"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strings"
+	"time"
+)
+
+// PostGetIndexData
+// @Description: 获取数据
+// @author: Roc
+// @datetime 2024-05-14 17:23:39
+// @param data xiangyu.PostGetIndexDataParamReq
+// @return dataResp xiangyu.CrmDataResp
+// @return err error
+// @return errMsg string
+func PostGetIndexData(data xiangyu.PostGetIndexDataParamReq) (dataResp index.GetBusinessDataResp, err error, errMsg string) {
+	errMsgList := make([]string, 0)
+	defer func() {
+		if len(errMsgList) > 0 {
+			tips := "PostGetIndexData-获取CRM数据失败, ErrMsg:\n" + strings.Join(errMsgList, "\n")
+			global.FILE_LOG.Error(tips)
+		}
+	}()
+	dataResp = index.GetBusinessDataResp{
+		List: make([]index.PushBusinessIndex, 0),
+	}
+	errMsg = `获取失败`
+	resp, err := xiangyu.PostGetCrmData(data)
+	if err != nil {
+		return
+	}
+
+	dataResp.Paging = paging.GetPaging(resp.PageNum, resp.PageSize, resp.TotalNum)
+
+	// 列表数据
+
+	businessIndexMap := make(map[string]index.PushBusinessIndex)
+	//index.PushBusinessIndex
+	for _, v := range resp.Rows {
+		tmpDate, tmpErr := time.ParseInLocation(utils.FormatDateUnSpace, v.DataDt, time.Local)
+		if tmpErr != nil {
+			errMsgList = append(errMsgList, fmt.Sprintf("指标日期转换失败,指标编码:%s;日期:%s", v.AssetCd, v.DataDt))
+			continue
+		}
+
+		frequency := handleXyFrequency(v.FrequencyName)
+		if frequency == `` {
+			errMsgList = append(errMsgList, fmt.Sprintf("指标频度异常,指标编码:%s;日期:%s", v.AssetCd, v.FrequencyName))
+			continue
+		}
+
+		businessIndex, ok := businessIndexMap[v.AssetCd]
+
+		//
+		if !ok {
+			dataList := make([]index.AddBusinessData, 0)
+			businessIndex = index.PushBusinessIndex{
+				IndexCode:            v.AssetCd,
+				IndexName:            v.AssetName,
+				Unit:                 v.UnitName,
+				Frequency:            frequency,
+				SourceName:           v.DataSourceType,
+				Remark:               "",
+				DetailDataCreateTime: v.DetailDataStockInTime,
+				DataList:             dataList,
+			}
+		}
+
+		businessIndex.DataList = append(businessIndex.DataList, index.AddBusinessData{
+			Value: v.Price,
+			Date:  tmpDate.Format(utils.FormatDate),
+		})
+		businessIndexMap[v.AssetCd] = businessIndex
+	}
+
+	// 将处理后的指标返回
+	for _, v := range businessIndexMap {
+		dataResp.List = append(dataResp.List, v)
+	}
+
+	return
+}
+
+// handleXyFrequency
+// @Description: 处理象屿频度问题
+// @author: Roc
+// @datetime 2024-05-14 18:15:28
+// @param frequencyName string
+// @return frequency string
+func handleXyFrequency(frequencyName string) (frequency string) {
+	switch frequencyName {
+	case "季度", "半年度", "年度", "日度", "周度", "旬度", "月度 ":
+		frequency = frequencyName
+	case "每天", "每交易日":
+		frequency = "日度"
+	}
+
+	return
+}

+ 73 - 0
logic/xiangyu/index.go

@@ -0,0 +1,73 @@
+package xiangyu
+
+import (
+	"eta/eta_email_analysis/services/xiangyu"
+)
+
+// PushIndexDataResp
+// @Description: 推送指标信息数据
+// @author: Roc
+// @datetime 2024-02-28 17:48:01
+// @param data xiangyu.PushBaseParamReq
+// @return err error
+// @return errMsg string
+func PushIndexDataResp(data xiangyu.PushBaseParamReq) (err error, errMsg string) {
+	//  获取xiangyu token
+
+	errMsg = `同步失败`
+	data.TableCode = "CY_DATA_ASSETS_INFO" // 固定字段
+	_, err = xiangyu.PushIndexData(data)
+	//fmt.Println(resp)
+
+	return
+}
+
+// PushEdbValueDataResp
+// @Description: 推送指标对应的日期值数据
+// @author: Roc
+// @datetime 2024-02-29 09:39:07
+// @param data xiangyu.PushBaseParamReq
+// @return err error
+// @return errMsg string
+func PushEdbValueDataResp(data xiangyu.PushBaseParamReq) (err error, errMsg string) {
+	//  获取xiangyu token
+
+	errMsg = `同步失败`
+	data.TableCode = "CY_DATA_ASSETS_DETAIL" // 固定字段
+	_, err = xiangyu.PushEdbValue(data)
+	//fmt.Println(resp)
+
+	return
+}
+
+// PushClassifyDataResp
+// @Description: 推送分类数据
+// @author: Roc
+// @datetime 2024-02-29 09:39:07
+// @param data xiangyu.PushBaseParamReq
+// @return err error
+// @return errMsg string
+func PushClassifyDataResp(data xiangyu.PushBaseParamReq) (err error, errMsg string) {
+	errMsg = `同步失败`
+	data.TableCode = "CY_CATALOG_CLASSIFY" // 固定字段
+	_, err = xiangyu.PushClassify(data)
+	//fmt.Println(resp)
+
+	return
+}
+
+// PushEdbClassifyDataResp
+// @Description: 推送指标分类数据
+// @author: Roc
+// @datetime 2024-02-29 09:39:07
+// @param data xiangyu.PushBaseParamReq
+// @return err error
+// @return errMsg string
+func PushEdbClassifyDataResp(data xiangyu.PushBaseParamReq) (err error, errMsg string) {
+	errMsg = `同步失败`
+	data.TableCode = "CY_CATALOG_INDEX_ASSOCIATE" // 固定字段
+	_, err = xiangyu.PushBase(data)
+	//fmt.Println(resp)
+
+	return
+}

+ 439 - 0
logic/xiangyu/user.go

@@ -0,0 +1,439 @@
+package xiangyu
+
+import (
+	"errors"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/models/eta"
+	"eta/eta_email_analysis/services/xiangyu"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// SyncUser
+// @Description: 全量同步用户
+// @author: Roc
+// @datetime 2024-01-22 10:28:39
+// @return err error
+func SyncUser() (err error) {
+	if global.CONFIG.Xiangyu.DefaultRoleId <= 0 {
+		err = errors.New("默认角色ID不能为空")
+		return
+	}
+	// 指定的角色
+	roleInfo, err := eta.GetSysRoleById(global.CONFIG.Xiangyu.DefaultRoleId)
+	if err != nil {
+		return
+	}
+
+	// 获取所有部门和分组
+	departmentMap, departmentNameMap, groupMap, groupNameMap, userMap, err := getAllDepartmentAndGroup()
+	if err != nil {
+		return
+	}
+
+	// 获取token
+	tokenId, err := xiangyu.Login()
+	if err != nil {
+		return
+	}
+	//tokenId := `228f7024-1966-47bf-86b5-9ec61ac149ed`
+
+	defer func() {
+		// 获取用户列表
+		tmpErr := xiangyu.Logout(tokenId)
+		if tmpErr != nil {
+			fmt.Println("退出失败:", tmpErr.Error())
+		}
+	}()
+
+	for {
+		// 获取用户列表
+		userResp, tmpErr := xiangyu.SyncTask(tokenId)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		// 没有数据了,那么结束同步了
+		if userResp == nil || userResp.Out.ObjectCode == `` {
+			return
+		}
+
+		// 是否同步成功
+		var isSync bool
+
+		var etaUserId, message string
+
+		//  处理用户信息,将象屿用户与eta用户做关联
+		tmpErr = handleUser(userResp.Out.Data, userResp.Out.Id, departmentMap, departmentNameMap, groupMap, groupNameMap, userMap, roleInfo)
+		if tmpErr == nil {
+			isSync = true
+			if etUserInfo, ok := userMap[userResp.Out.Data.Username]; ok {
+				etaUserId = fmt.Sprint(etUserInfo.AdminId)
+			}
+		} else {
+			message = tmpErr.Error()
+		}
+
+		// 结束单次同步
+		err = xiangyu.SyncFinish(tokenId, userResp.Out.ObjectCode, userResp.Out.ObjectType, userResp.Out.Id, etaUserId, message, isSync)
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+// PullUser
+// @Description: 增量同步用户
+// @author: Roc
+// @datetime 2024-01-23 18:01:27
+// @return err error
+func PullUser() (err error) {
+	if global.CONFIG.Xiangyu.DefaultRoleId <= 0 {
+		err = errors.New("默认角色ID不能为空")
+		return
+	}
+	defer func() {
+		if err != nil {
+			global.FILE_LOG.Info("同步增量用户失败:" + err.Error())
+		}
+	}()
+
+	// 指定的角色
+	roleInfo, err := eta.GetSysRoleById(global.CONFIG.Xiangyu.DefaultRoleId)
+	if err != nil {
+		return
+	}
+
+	// 获取所有部门和分组
+	departmentMap, departmentNameMap, groupMap, groupNameMap, userMap, err := getAllDepartmentAndGroup()
+	if err != nil {
+		return
+	}
+
+	// 获取token
+	tokenId, err := xiangyu.Login()
+	if err != nil {
+		return
+	}
+
+	defer func() {
+		// 获取用户列表
+		tmpErr := xiangyu.Logout(tokenId)
+		if tmpErr != nil {
+			fmt.Println("退出失败:", tmpErr.Error())
+		}
+	}()
+
+	for {
+		// 获取增量用户列表
+		userResp, tmpErr := xiangyu.PullTask(tokenId)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+
+		// 没有数据了,那么结束同步了
+		if userResp == nil || userResp.Out.ObjectCode == `` {
+			return
+		}
+
+		// 是否同步成功
+		var isSync bool
+
+		var etaUserId, message string
+
+		//  处理用户信息,将象屿用户与eta用户做关联
+		tmpErr = handleUser(userResp.Out.Data, userResp.Out.Id, departmentMap, departmentNameMap, groupMap, groupNameMap, userMap, roleInfo)
+		if tmpErr == nil {
+			isSync = true
+			if etUserInfo, ok := userMap[userResp.Out.Data.Username]; ok {
+				etaUserId = fmt.Sprint(etUserInfo.AdminId)
+			}
+		} else {
+			message = tmpErr.Error()
+		}
+
+		// 结束单次增量同步
+		err = xiangyu.PullFinish(tokenId, userResp.Out.TaskId, etaUserId, message, isSync)
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+// TaskPullUser
+// @Description: 定时增量同步用户
+// @author: Roc
+// @datetime 2024-01-23 18:03:04
+func TaskPullUser() {
+	PullUser()
+	return
+}
+
+// getAllDepartmentAndGroup
+// @Description: 获取所有的部门和分组
+// @author: Roc
+// @datetime 2024-01-18 17:39:14
+// @return departmentMap map[string]*eta.SysDepartment
+// @return groupMap map[string]*eta.SysGroup
+// @return err error
+func getAllDepartmentAndGroup() (departmentMap, departmentNameMap map[string]*eta.SysDepartment, groupMap, groupNameMap map[string]*eta.SysGroup, userMap map[string]*eta.Admin, err error) {
+	// 获取所有的部门
+	departmentList, err := eta.GetAllDepartment()
+	if err != nil {
+		return
+	}
+
+	// 部门信息
+	departmentMap = make(map[string]*eta.SysDepartment)
+	departmentNameMap = make(map[string]*eta.SysDepartment)
+	for _, department := range departmentList {
+		if department.OutID == `` {
+			departmentNameMap[department.DepartmentName] = department
+		} else {
+			departmentMap[department.OutID] = department
+		}
+	}
+
+	// 获取所有的分组
+	groupList, err := eta.GetAllSysGroup()
+	if err != nil {
+		return
+	}
+
+	// 部门信息
+	groupMap = make(map[string]*eta.SysGroup)
+	groupNameMap = make(map[string]*eta.SysGroup)
+	for _, group := range groupList {
+		if group.OutID == `` {
+			groupNameMap[fmt.Sprint(group.DepartmentId, "_", group.GroupName)] = group
+		} else {
+			groupMap[group.OutID] = group
+		}
+	}
+
+	// 获取所有的用户
+	adminList, err := eta.GetAllSysUser()
+	if err != nil {
+		return
+	}
+
+	// 部门信息
+	userMap = make(map[string]*eta.Admin)
+	for _, admin := range adminList {
+		userMap[admin.AdminName] = admin
+	}
+
+	return
+}
+
+// handleUser
+// @Description: 用户信息处理
+// @author: Roc
+// @datetime 2024-01-18 18:00:04
+// @param xyUserInfo xiangyu.UserData
+// @param departmentMap map[string]*eta.SysDepartment
+// @param groupMap map[string]*eta.SysGroup
+// @param userMap map[string]*eta.Admin
+// @return err error
+func handleUser(xyUserInfo xiangyu.UserData, xyUserId string, departmentMap, departmentNameMap map[string]*eta.SysDepartment, groupMap, groupNameMap map[string]*eta.SysGroup, userMap map[string]*eta.Admin, roleInfo *eta.SysRole) (err error) {
+	// 判断公司id是否存在,如果不存在,那么就去创建
+	departmentInfo, ok := departmentMap[xyUserInfo.CompanyId]
+	if !ok {
+		// 没有外部id的部门,那么用名称去匹配
+		departmentInfo, ok = departmentNameMap[xyUserInfo.CompanyName]
+		if !ok {
+			// 如果没有的话,那么就创建该部门
+			departmentInfo = &eta.SysDepartment{
+				DepartmentId:   0,
+				DepartmentName: xyUserInfo.CompanyName,
+				OutID:          xyUserInfo.CompanyId,
+				CreateTime:     time.Now(),
+				Sort:           0,
+			}
+			err = departmentInfo.Create()
+
+			if err != nil {
+				return
+			}
+		} else {
+			// 如果通过名称找到了,那么就更新一下外部id
+			departmentInfo.OutID = xyUserInfo.CompanyId
+			err = departmentInfo.Update([]string{"OutID"})
+			if err != nil {
+				return
+			}
+			delete(departmentNameMap, xyUserInfo.CompanyName)
+		}
+
+		departmentMap[xyUserInfo.CompanyId] = departmentInfo
+	}
+
+	// 判断部门id是否存在,如果不存在,那么就去创建
+	groupInfo, ok := groupMap[xyUserInfo.DepartId]
+	if !ok {
+		groupInfo, ok = groupNameMap[fmt.Sprint(departmentInfo.DepartmentId, "_", xyUserInfo.DepartmentName)]
+		if !ok {
+			// 如果通过名称还是没有找到,那么就创建一个分组
+			groupInfo = &eta.SysGroup{
+				GroupId:      0,
+				DepartmentId: departmentInfo.DepartmentId,
+				GroupName:    xyUserInfo.DepartmentName,
+				CreateTime:   time.Now(),
+				ParentId:     0,
+				Sort:         0,
+				OutID:        xyUserInfo.DepartId,
+			}
+
+			// 如果没有的话,那么就创建该分组
+			err = groupInfo.Create()
+			if err != nil {
+				return
+			}
+		} else {
+			// 如果通过名称找到了,那么就更新一下外部id
+			groupInfo.OutID = xyUserInfo.DepartId
+			err = groupInfo.Update([]string{"OutID"})
+			if err != nil {
+				return
+			}
+			delete(groupNameMap, xyUserInfo.DepartmentName)
+		}
+		groupMap[xyUserInfo.DepartId] = groupInfo
+	}
+
+	enabled := 1
+	if xyUserInfo.IsDisabled {
+		enabled = 0
+	}
+	// 判断用户是否存在,如果不存在,那么就去创建
+	userInfo, ok := userMap[xyUserInfo.Username]
+	if !ok {
+
+		userInfo = &eta.Admin{
+			AdminId:     0,
+			AdminName:   xyUserInfo.Username,
+			AdminAvatar: "",
+			RealName:    xyUserInfo.FullName,
+			Password:    xyUserInfo.Password,
+			//LastUpdatedPasswordTime:   time.Time{},
+			Enabled: enabled,
+			Email:   "",
+			//LastLoginTime:             time.Time{},
+			CreatedTime:     time.Now(),
+			LastUpdatedTime: time.Now(),
+			//Role:            roleInfo.,
+			Mobile:         xyUserInfo.Mobile,
+			RoleType:       0,
+			RoleId:         roleInfo.RoleId,
+			RoleName:       roleInfo.RoleName,
+			RoleTypeCode:   roleInfo.RoleTypeCode,
+			DepartmentId:   departmentInfo.DepartmentId,
+			DepartmentName: departmentInfo.DepartmentName,
+			GroupId:        groupInfo.GroupId,
+			GroupName:      groupInfo.GroupName,
+			Authority:      0,
+			Position:       xyUserInfo.PositionName,
+			//DisableTime:               time.Time{},
+			ChartPermission:           0,
+			EdbPermission:             0,
+			MysteelChemicalPermission: 0,
+			OpenId:                    "",
+			UnionId:                   "",
+			PredictEdbPermission:      0,
+			Province:                  "",
+			ProvinceCode:              "",
+			City:                      "",
+			CityCode:                  "",
+			EmployeeId:                xyUserInfo.EmployeeNo,
+			TelAreaCode:               "",
+			OutID:                     xyUserId,
+		}
+
+		if strings.Contains(roleInfo.RoleTypeCode, "researcher") {
+			userInfo.Role = "researcher"
+		} else if strings.Contains(userInfo.RoleTypeCode, "seller") {
+			userInfo.Role = "sales"
+		} else {
+			userInfo.Role = "admin"
+		}
+
+		// 如果没有的话,那么就创建该用户
+		err = userInfo.Create()
+		if err != nil {
+			return
+		}
+
+	} else {
+		updateColList := make([]string, 0)
+
+		//状态
+		if userInfo.Enabled != enabled {
+			updateColList = append(updateColList, "Enabled")
+			userInfo.Enabled = enabled
+		}
+
+		// 部门
+		if userInfo.DepartmentId != departmentInfo.DepartmentId {
+			updateColList = append(updateColList, "DepartmentId", "DepartmentName")
+			userInfo.DepartmentId = departmentInfo.DepartmentId
+			userInfo.DepartmentName = departmentInfo.DepartmentName
+		}
+
+		// 分组
+		if userInfo.GroupId != groupInfo.GroupId {
+			updateColList = append(updateColList, "GroupId", "GroupName")
+			userInfo.GroupId = groupInfo.GroupId
+			userInfo.GroupName = groupInfo.GroupName
+		}
+
+		// 职位
+		if userInfo.Position != xyUserInfo.PositionName {
+			updateColList = append(updateColList, "Position")
+			userInfo.Position = xyUserInfo.PositionName
+		}
+
+		// 姓名
+		if userInfo.RealName != xyUserInfo.FullName {
+			updateColList = append(updateColList, "RealName")
+			userInfo.RealName = xyUserInfo.FullName
+		}
+
+		// 手机号
+		if userInfo.Mobile != xyUserInfo.Mobile {
+			updateColList = append(updateColList, "Mobile")
+			userInfo.Mobile = xyUserInfo.Mobile
+		}
+
+		// 外部id
+		if userInfo.OutID == `` {
+			updateColList = append(updateColList, "OutID")
+			userInfo.OutID = xyUserId
+		}
+
+		// 工号
+		if userInfo.EmployeeId != xyUserInfo.EmployeeNo {
+			updateColList = append(updateColList, "EmployeeId")
+			userInfo.OutID = xyUserInfo.EmployeeNo
+		}
+
+		if len(updateColList) > 0 {
+			updateColList = append(updateColList, "LastUpdatedTime")
+			userInfo.LastUpdatedTime = time.Now()
+			err = userInfo.Update(updateColList)
+			if err != nil {
+				return
+			}
+		}
+	}
+
+	userMap[xyUserInfo.Username] = userInfo
+
+	return
+}

+ 10 - 0
main.go

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

+ 159 - 0
middleware/auth.go

@@ -0,0 +1,159 @@
+package middleware
+
+import (
+	"crypto/md5"
+	"errors"
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/models/crm"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"math"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func BaseAuthCheck() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		method := c.Request.Method
+		if method != "POST" {
+			resp.TokenError(nil, "请求异常", "不支持非POST请求", c)
+			c.Abort()
+			return
+		}
+		pass, e := signCheck(c)
+		if e != nil {
+			resp.TokenError(nil, "签名错误", "签名校验失败, Err: "+e.Error(), c)
+			c.Abort()
+			return
+		}
+		if !pass {
+			resp.TokenError(nil, "签名错误", "签名错误", c)
+			c.Abort()
+			return
+		}
+	}
+}
+
+func signCheck(c *gin.Context) (ok bool, err error) {
+	params := make(map[string][]string)
+	err = c.ShouldBind(params)
+	if err != nil {
+		return
+	}
+	signData := convertParam(params)
+
+	// 签名校验
+	ip := c.ClientIP()
+	err = checkSignData(signData, ip)
+	if err != nil {
+		return
+	}
+	ok = true
+	return
+}
+
+// convertParam 将请求传入的数据格式转换成签名需要的格式
+func convertParam(params map[string][]string) (signData map[string]string) {
+	signData = make(map[string]string)
+	for key := range params {
+		signData[key] = params[key][0]
+	}
+	return signData
+}
+
+// checkSignData 请求参数签名校验
+func checkSignData(postData map[string]string, ip string) (err error) {
+	isSandbox := postData["is_sandbox"]
+	// 如果是测试环境, 且是沙箱环境的话, 那么绕过测试
+	if global.CONFIG.Serve.RunMode == "debug" && isSandbox != "" {
+		return
+	}
+
+	appid := postData["appid"]
+	if appid == "" {
+		err = errors.New("参数异常,缺少appid")
+		return
+	}
+
+	openApiOB := new(crm.OpenApiUser)
+	apiUser, e := openApiOB.GetItemByAppid(appid)
+	if e != nil {
+		if e != utils.ErrNoRow {
+			err = errors.New("系统异常,请联系管理员")
+			return
+		}
+		err = errors.New("appid异常,请联系管理员")
+		return
+	}
+	if apiUser == nil {
+		err = errors.New("系统异常,请联系管理员")
+		return
+	}
+
+	// 如果有ip限制, 则校验IP
+	if apiUser.Ip != "" {
+		if !strings.Contains(apiUser.Ip, ip) {
+			err = errors.New(fmt.Sprintf("无权限访问该接口,ip:%v,请联系管理员", ip))
+			return
+		}
+	}
+
+	// 接口提交的签名字符串
+	ownSign := postData["sign"]
+	if ownSign == "" {
+		err = errors.New("参数异常,缺少签名字符串")
+		return
+	}
+	if postData["nonce_str"] == "" {
+		err = errors.New("参数异常,缺少随机字符串")
+		return
+	}
+	if postData["timestamp"] == "" {
+		err = errors.New("参数异常,缺少时间戳")
+		return
+	} else {
+		timeUnix := time.Now().Unix() // 当前格林威治时间,int64类型
+		// 将接口传入的时间做转换
+		timestamp, timeErr := strconv.ParseInt(postData["timestamp"], 10, 64)
+		if timeErr != nil {
+			err = errors.New("参数异常,时间戳格式异常")
+			return
+		}
+		if math.Abs(float64(timeUnix-timestamp)) > 300 {
+			err = errors.New("当前时间异常,请调整设备时间与北京时间一致")
+			return
+		}
+	}
+
+	// 先取出除sign外的所有的提交的参数key
+	var keys []string
+	for k := range postData {
+		if k != "sign" {
+			keys = append(keys, k)
+		}
+	}
+
+	//1,根据参数名称的ASCII码表的顺序排序
+	sort.Strings(keys)
+
+	//2 根据排序后的参数名称,取出对应的值,并拼接字符串
+	var signStr string
+	for _, v := range keys {
+		signStr += v + "=" + postData[v] + "&"
+	}
+	//3,全转小写(md5(拼装的字符串后+分配给你的app_secret))
+	//sign := strings.ToLower(fmt.Sprintf("%x", md5.Sum([]byte(strings.Trim(signStr, "&")+key))))
+
+	//md5.Sum([]byte(signStr+"key="+key))  这是md5加密出来后的每个字符的AscII码,需要再转换成对应的字符
+	//3,全转大写(md5(拼装的字符串后+分配给你的app_secret))
+	sign := strings.ToUpper(fmt.Sprintf("%x", md5.Sum([]byte(signStr+"secret="+apiUser.Secret))))
+	if sign != ownSign {
+		global.LOG.Info(fmt.Sprintf("签名校验异常,签名字符串:%v;服务端签名值:%v", signStr, sign))
+		return errors.New("签名校验异常,请核实签名")
+	}
+	return nil
+}

+ 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()
+	}
+}

+ 57 - 0
middleware/recover.go

@@ -0,0 +1,57 @@
+package middleware
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/services/alarm_msg"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"runtime"
+	"time"
+)
+
+// 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) {
+				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()
+	}
+}

+ 55 - 0
middleware/token.go

@@ -0,0 +1,55 @@
+package middleware
+
+import (
+	"eta/eta_email_analysis/controller/resp"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"github.com/gin-gonic/gin"
+)
+
+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
+		}
+		if token != utils.CrmEtaAuthorization {
+			resp.TokenError(nil, "非法访问", "非法访问", c)
+			c.Abort()
+			return
+		}
+		c.Next()
+	}
+}
+
+func InternalToken() 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
+		}
+		checkToken := utils.MD5(global.CONFIG.Serve.AppNameEn + global.CONFIG.Serve.Md5Key)
+		if token != checkToken {
+			resp.TokenError(nil, "非法访问", "非法访问", c)
+			c.Abort()
+			return
+		}
+		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()
+}

+ 25 - 0
models/crm/open_api_user.go

@@ -0,0 +1,25 @@
+package crm
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type OpenApiUser struct {
+	Appid      string    `gorm:"primaryKey;column:appid;type:varchar(32);not null;default:''" json:"appid"`      // 开放平台appid
+	Secret     string    `gorm:"column:secret;type:varchar(64);default:''" json:"secret"`                        // 开放平台秘钥
+	Ip         string    `gorm:"column:ip;type:text" json:"ip"`                                                  // 限制请求来源ip,多个ip用英文,隔开
+	Remark     string    `gorm:"column:remark;type:varchar(255);default:''" json:"remark"`                       // 备注,记录来源等信息
+	CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP" json:"create_time"` // 创建时间
+	ModifyTime time.Time `gorm:"column:modify_time;type:datetime" json:"modify_time"`                            // 最近一次更新时间
+}
+
+func (m *OpenApiUser) TableName() string {
+	return "open_api_user"
+}
+
+// GetItemByAppid AppID获取信息
+func (m *OpenApiUser) GetItemByAppid(appid string) (item *OpenApiUser, err error) {
+	err = global.MYSQL["hz_crm"].Model(m).Where("appid = ?", appid).First(&item).Error
+	return
+}

+ 99 - 0
models/eta/admin.go

@@ -0,0 +1,99 @@
+package eta
+
+import (
+	"errors"
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+// Admin 管理员表
+type Admin struct {
+	AdminId                   int       `gorm:"primaryKey;column:admin_id;type:bigint(20);not null" json:"admin_id"`
+	AdminName                 string    `gorm:"uniqueIndex:un;index:name;index:admin_pass;column:admin_name;type:varchar(60);not null" json:"admin_name"`
+	AdminAvatar               string    `gorm:"column:admin_avatar;type:varchar(255);not null;default:''" json:"admin_avatar"` // 用户头像
+	RealName                  string    `gorm:"column:real_name;type:varchar(60)" json:"real_name"`
+	Password                  string    `gorm:"index:password;index:admin_pass;column:password;type:varchar(60);not null" json:"password"`
+	LastUpdatedPasswordTime   time.Time `gorm:"column:last_updated_password_time;type:datetime" json:"last_updated_password_time"`
+	Enabled                   int       `gorm:"uniqueIndex:un;column:enabled;type:tinyint(1);not null" json:"enabled"` // 1:有效,0:禁用
+	Email                     string    `gorm:"column:email;type:varchar(60)" json:"email"`
+	LastLoginTime             time.Time `gorm:"column:last_login_time;type:datetime" json:"last_login_time"`                                        // 最近登陆时间
+	CreatedTime               time.Time `gorm:"index:created_time;column:created_time;type:datetime;default:CURRENT_TIMESTAMP" json:"created_time"` // 创建时间
+	LastUpdatedTime           time.Time `gorm:"index:last_updated_time;column:last_updated_time;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"last_updated_time"`
+	Role                      string    `gorm:"column:role;type:varchar(30);default:saller" json:"role"`                                                  // 用户角色
+	Mobile                    string    `gorm:"column:mobile;type:varchar(20)" json:"mobile"`                                                             // 手机号
+	RoleType                  int       `gorm:"column:role_type;type:tinyint(4);default:0" json:"role_type"`                                              // 角色类型:1需要录入指标,0:不需要
+	RoleId                    int       `gorm:"column:role_id;type:int(11);default:0" json:"role_id"`                                                     // 角色id
+	RoleName                  string    `gorm:"column:role_name;type:varchar(100)" json:"role_name"`                                                      // 角色名称
+	RoleTypeCode              string    `gorm:"column:role_type_code;type:varchar(20);default:''" json:"role_type_code"`                                  // 角色编码
+	DepartmentId              int       `gorm:"column:department_id;type:int(11);default:0" json:"department_id"`                                         // 部门id
+	DepartmentName            string    `gorm:"column:department_name;type:varchar(100)" json:"department_name"`                                          // 部门名称
+	GroupId                   int       `gorm:"column:group_id;type:int(11);default:0" json:"group_id"`                                                   // 分组id
+	GroupName                 string    `gorm:"column:group_name;type:varchar(100)" json:"group_name"`                                                    // 分组名称
+	Authority                 int       `gorm:"column:authority;type:tinyint(4);default:0" json:"authority"`                                              // 管理权限,0:无,1:部门负责人,2:小组负责人,或者ficc销售主管,4:ficc销售组长
+	Position                  string    `gorm:"column:position;type:varchar(100)" json:"position"`                                                        // 职位
+	DisableTime               time.Time `gorm:"column:disable_time;type:datetime" json:"disable_time"`                                                    // 禁用时间
+	ChartPermission           uint      `gorm:"column:chart_permission;type:tinyint(9) unsigned;default:0" json:"chart_permission"`                       // 图表指标操作权限,0:只能操作 自己的,1:所有图表可操作
+	EdbPermission             uint      `gorm:"column:edb_permission;type:tinyint(9) unsigned;default:0" json:"edb_permission"`                           // 指标库操作权限,0:只能操作 自己的,1:所有指标可操作
+	MysteelChemicalPermission uint      `gorm:"column:mysteel_chemical_permission;type:tinyint(9) unsigned;default:0" json:"mysteel_chemical_permission"` // 钢联化工指标操作权限,0:只能操作 自己的,1:所有指标可操作
+	OpenId                    string    `gorm:"column:open_id;type:varchar(100);default:''" json:"open_id"`                                               // 弘则部门公众号的openid
+	UnionId                   string    `gorm:"column:union_id;type:varchar(100);default:''" json:"union_id"`                                             // 微信公众平台唯一标识
+	PredictEdbPermission      int       `gorm:"column:predict_edb_permission;type:tinyint(9)" json:"predict_edb_permission"`                              // 预测指标库操作权限,0:只能操作 自己的,1:所有预测指标可操作
+	Province                  string    `gorm:"column:province;type:varchar(255);default:''" json:"province"`                                             // 省
+	ProvinceCode              string    `gorm:"column:province_code;type:varchar(100);default:''" json:"province_code"`                                   // 省编码
+	City                      string    `gorm:"column:city;type:varchar(255);default:''" json:"city"`                                                     // 市
+	CityCode                  string    `gorm:"column:city_code;type:varchar(100);default:''" json:"city_code"`                                           // 市编码
+	EmployeeId                string    `gorm:"column:employee_id;type:varchar(64);not null;default:''" json:"employee_id"`                               // 员工工号(钉钉/每刻报销)
+	TelAreaCode               string    `gorm:"column:tel_area_code;type:varchar(32);not null;default:'86'" json:"tel_area_code"`                         // 手机号区号
+	OutID                     string    `gorm:"column:out_id" json:"out_id"`                                                                              // 外部id
+}
+
+func (m *Admin) TableName() string {
+	return "admin"
+}
+
+// Create 新增用户
+func (m *Admin) Create() (err error) {
+	err = global.MYSQL["hz_eta"].Create(m).Error
+	return
+}
+
+// Update 更新用户
+func (m *Admin) Update(cols []string) (err error) {
+	err = global.MYSQL["hz_eta"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+// GetSysUserById 主键获取系统用户
+func GetSysUserById(sysUserId int) (item *Admin, err error) {
+	err = global.MYSQL["hz_eta"].Where("admin_id = ?", sysUserId).First(&item).Error
+	return
+}
+
+// GetSysUserByAdminName 用户名获取系统用户
+func GetSysUserByAdminName(adminName string) (item *Admin, err error) {
+	err = global.MYSQL["hz_eta"].Where("admin_name = ?", adminName).First(&item).Error
+	return
+}
+
+// DeleteSysUserByName 根据用户名删除用户
+func DeleteSysUserByName(adminName string) (err error) {
+	sql := `DELETE FROM admin WHERE admin_name = ? LIMIT 1`
+	err = global.MYSQL["hz_eta"].Exec(sql, adminName).Error
+	return
+}
+
+// GetAllSysUser 获取所有用户
+func GetAllSysUser() (items []*Admin, err error) {
+	err = global.MYSQL["hz_eta"].Find(&items).Error
+	return
+}
+
+// GetSysUserByOutId 通过外部id获取系统用户
+func GetSysUserByOutId(outId string) (item *Admin, err error) {
+	if outId == `` {
+		err = errors.New("outId不能为空")
+		return
+	}
+	err = global.MYSQL["hz_eta"].Where("out_id = ?", outId).First(&item).Error
+	return
+}

+ 36 - 0
models/eta/crm_config.go

@@ -0,0 +1,36 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+const (
+	ConfEnAuthRoleKey = "en_auth_role" // 英文权限角色配置Key
+)
+
+// CrmConfig CRM配置表
+type CrmConfig struct {
+	ConfigId    int       `gorm:"primaryKey;column:config_id;type:int(11);not null" json:"config_id"`
+	ConfigCode  string    `gorm:"column:config_code;type:varchar(50);default:''" json:"config_code"` // 配置编码
+	ConfigValue string    `gorm:"column:config_value;type:text" json:"config_value"`                 // 配置值
+	Remark      string    `gorm:"column:remark;type:varchar(255);default:''" json:"remark"`          // 备注信息
+	CreateTime  time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`               // 创建时间
+}
+
+func (m *CrmConfig) TableName() string {
+	return "crm_config"
+}
+
+// GetCrmConfByCode code获取配置
+func GetCrmConfByCode(code string) (item *CrmConfig, err error) {
+	err = global.MYSQL["hz_eta"].Where("config_code = ?", code).First(&item).Error
+	return
+}
+
+// ConfEnAuthRole 配置-英文权限角色
+type ConfEnAuthRole struct {
+	RoleCode string `description:"角色编码"`
+	RoleName string `description:"角色名称"`
+	SyncCrm  bool   `description:"是否同步CRM"`
+}

+ 49 - 0
models/eta/sys_department.go

@@ -0,0 +1,49 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysDepartment struct {
+	DepartmentId   int       `gorm:"primaryKey;column:department_id;type:int(11);not null" json:"department_id"`
+	DepartmentName string    `gorm:"unique;column:department_name;type:varchar(255);default:''" json:"department_name"` // 部门名称
+	OutID          string    `gorm:"column:out_id" json:"out_id"`                                                       // 外部id
+	CreateTime     time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`                               // 创建时间
+	Sort           int       `gorm:"column:sort;type:int(10);not null;default:0" json:"sort"`                           // 排序
+}
+
+func (m *SysDepartment) TableName() string {
+	return "sys_department"
+}
+
+// Create 新增部门
+func (m *SysDepartment) Create() (err error) {
+	err = global.MYSQL["hz_eta"].Create(m).Error
+	return
+}
+
+// Update 更新部门
+func (m *SysDepartment) Update(cols []string) (err error) {
+	err = global.MYSQL["hz_eta"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+// GetDepartmentById 主键获取部门
+func GetDepartmentById(departmentId int) (item *SysDepartment, err error) {
+	err = global.MYSQL["hz_eta"].Where("department_id = ?", departmentId).First(&item).Error
+	return
+}
+
+// GetAllDepartment 获取所有部门
+func GetAllDepartment() (items []*SysDepartment, err error) {
+	err = global.MYSQL["hz_eta"].Find(&items).Error
+	return
+}
+
+// DeleteDepartmentById 主键删除部门
+func DeleteDepartmentById(departmentId int) (err error) {
+	sql := `DELETE FROM sys_department WHERE department_id = ? LIMIT 1`
+	err = global.MYSQL["hz_eta"].Exec(sql, departmentId).Error
+	return
+}

+ 57 - 0
models/eta/sys_group.go

@@ -0,0 +1,57 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysGroup struct {
+	GroupId      int       `gorm:"primaryKey;column:group_id;type:int(11);not null" json:"group_id"`
+	DepartmentId int       `gorm:"index:idx_department_id;column:department_id;type:int(11)" json:"department_id"` // 部门id
+	GroupName    string    `gorm:"column:group_name;type:varchar(255);default:''" json:"group_name"`               // 分组名称
+	CreateTime   time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`
+	ParentId     int       `gorm:"column:parent_id;type:int(11);default:0" json:"parent_id"`
+	Sort         int       `gorm:"column:sort;type:int(10);not null;default:0" json:"sort"` // 排序
+	OutID        string    `gorm:"column:out_id" json:"out_id"`                             // 外部id
+}
+
+func (m *SysGroup) TableName() string {
+	return "sys_group"
+}
+
+// Create 新增分组
+func (m *SysGroup) Create() (err error) {
+	err = global.MYSQL["hz_eta"].Create(m).Error
+	return
+}
+
+// Update 更新分组
+func (m *SysGroup) Update(cols []string) (err error) {
+	err = global.MYSQL["hz_eta"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+// GetSysGroupByGroupId 获取分组-根据主键
+func GetSysGroupByGroupId(groupId int) (item *SysGroup, err error) {
+	err = global.MYSQL["hz_eta"].Where("group_id = ?", groupId).First(&item).Error
+	return
+}
+
+// GetAllSysGroup 获取所有的分组
+func GetAllSysGroup() (items []*SysGroup, err error) {
+	err = global.MYSQL["hz_eta"].Find(&items).Error
+	return
+}
+
+// DeleteGroupByGroupId 删除分组-根据主键
+func DeleteGroupByGroupId(groupId int) (err error) {
+	sql := `DELETE FROM sys_group WHERE group_id = ? LIMIT 1`
+	err = global.MYSQL["hz_eta"].Exec(sql, groupId).Error
+	if err != nil {
+		return
+	}
+	// 清除关联
+	sql = `UPDATE admin SET group_id = 0, group_name = '' WHERE group_id = ? `
+	err = global.MYSQL["hz_eta"].Exec(sql, groupId).Error
+	return
+}

+ 46 - 0
models/eta/sys_role.go

@@ -0,0 +1,46 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+// SysRole 角色表
+type SysRole struct {
+	RoleId       int       `gorm:"primaryKey;column:role_id;type:int(11);not null" json:"role_id""`
+	RoleName     string    `gorm:"column:role_name;type:varchar(100)" json:"role_name"`
+	RoleType     string    `gorm:"column:role_type;type:varchar(50)" json:"role_type"`            // 角色类型
+	RoleTypeCode string    `gorm:"column:role_type_code;type:varchar(100)" json:"role_type_code"` // 角色类型编码
+	CreateTime   time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`
+	ModifyTime   time.Time `gorm:"column:modify_time;type:datetime" json:"modify_time"`
+	RoleLevel    int       `gorm:"column:role_level;type:tinyint(4);not null;default:0" json:"role_level"` // 角色等级:0-表示一级角色,每个账号只能绑定一个一级角色 ,1-表示二级角色,每个账号可以绑定多个二级 角色
+}
+
+func (m *SysRole) TableName() string {
+	return "sys_role"
+}
+
+// Create 新增角色
+func (m *SysRole) Create() (err error) {
+	err = global.MYSQL["hz_eta"].Create(m).Error
+	return
+}
+
+// Update 更新角色
+func (m *SysRole) Update(cols []string) (err error) {
+	err = global.MYSQL["hz_eta"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+// GetSysRoleById 主键获取角色
+func GetSysRoleById(roleId int) (item *SysRole, err error) {
+	err = global.MYSQL["hz_eta"].Where("role_id = ?", roleId).First(&item).Error
+	return
+}
+
+// DeleteRoleById 主键删除角色
+func DeleteRoleById(roleId int) (err error) {
+	sql := `DELETE FROM sys_role WHERE role_id = ? LIMIT 1`
+	err = global.MYSQL["hz_eta"].Exec(sql, roleId).Error
+	return
+}

+ 39 - 0
models/eta/sys_session.go

@@ -0,0 +1,39 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysSession struct {
+	Id              int       `gorm:"primaryKey;column:id;type:bigint(20);not null" json:"id"`
+	SysUserId       int       `gorm:"column:sys_user_id;type:bigint(20)" json:"sys_user_id"`
+	UserName        string    `gorm:"column:user_name;type:varchar(32)" json:"user_name"`
+	AccessToken     string    `gorm:"index:access_token;column:access_token;type:varchar(255)" json:"access_token"`
+	IsRemember      int       `gorm:"column:is_remember;type:tinyint(9) unsigned;default:0" json:"is_remember"` // 是否记住密码,属于受信设备
+	ExpiredTime     time.Time `gorm:"column:expired_time;type:datetime" json:"expired_time"`
+	CreatedTime     time.Time `gorm:"index:created_time;column:created_time;type:datetime;default:CURRENT_TIMESTAMP" json:"created_time"` // 创建时间
+	LastUpdatedTime time.Time `gorm:"index:last_updated_time;column:last_updated_time;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"last_updated_time"`
+}
+
+func (m *SysSession) TableName() string {
+	return "sys_session"
+}
+
+// AddSysSession 新增session
+func AddSysSession(item *SysSession) (err error) {
+	err = global.MYSQL["hz_eta"].Create(item).Error
+	return
+}
+
+// GetSysSessionByToken 根据Token获取session
+func GetSysSessionByToken(token string) (item *SysSession, err error) {
+	err = global.MYSQL["hz_eta"].Where("access_token = ? AND expired_time > NOW()", token).Order("expired_time DESC").First(&item).Error
+	return
+}
+
+// GetUserUnexpiredSysSession 获取用户未过期的Token
+func GetUserUnexpiredSysSession(userName, expiredTime string) (item *SysSession, err error) {
+	err = global.MYSQL["hz_eta"].Where("user_name = ? AND expired_time > ?", userName, expiredTime).Order("expired_time DESC").First(&item).Error
+	return
+}

+ 36 - 0
models/eta/sys_third_session.go

@@ -0,0 +1,36 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+// SysThirdSession
+// @Description: 第三方登录会话session
+type SysThirdSession struct {
+	ID               uint64 `gorm:"primaryKey;column:id"`
+	SysUserID        uint64 `gorm:"column:sys_user_id"`
+	UserName         string `gorm:"column:user_name"`
+	AccessToken      string `gorm:"index:access_token;column:access_token"`
+	ThirdAccessToken string `gorm:"index:third_access_token;column:third_access_token"`
+	//ThirdRefreshToken string    `gorm:"column:third_refresh_token"`
+	ThirdExpiredTime time.Time `gorm:"column:third_expired_time"`
+	CreatedTime      time.Time `gorm:"index:created_time;column:created_time"`
+	LastUpdatedTime  time.Time `gorm:"index:last_updated_time;column:last_updated_time"`
+}
+
+func (m *SysThirdSession) TableName() string {
+	return "sys_third_session"
+}
+
+// AddSysThirdSession 新增第三方的session
+func AddSysThirdSession(item *SysThirdSession) (err error) {
+	err = global.MYSQL["hz_eta"].Create(item).Error
+	return
+}
+
+// GetSysThirdSessionByToken 根据Token获取第三方session
+func GetSysThirdSessionByToken(token string) (item *SysThirdSession, err error) {
+	err = global.MYSQL["hz_eta"].Where("access_token = ? AND expired_time > NOW()", token).Order("expired_time DESC").First(&item).Error
+	return
+}

+ 25 - 0
models/eta/sys_user_login_record.go

@@ -0,0 +1,25 @@
+package eta
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysUserLoginRecord struct {
+	Id         int       `gorm:"primaryKey;column:id;type:int(11);not null" json:"id"`
+	Uid        int       `gorm:"column:uid;type:int(11)" json:"uid"`
+	UserName   string    `gorm:"column:user_name;type:varchar(30)" json:"user_name"`
+	Ip         string    `gorm:"column:ip;type:varchar(30)" json:"ip"`
+	Stage      string    `gorm:"column:stage;type:varchar(30)" json:"stage"`
+	CreateTime time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`
+}
+
+func (m *SysUserLoginRecord) TableName() string {
+	return "sys_user_login_record"
+}
+
+// AddSysUserLoginRecord 新增登录记录
+func AddSysUserLoginRecord(item *SysUserLoginRecord) (err error) {
+	err = global.MYSQL["hz_eta"].Create(item).Error
+	return
+}

+ 36 - 0
models/index/base.go

@@ -0,0 +1,36 @@
+package index
+
+import "eta/eta_email_analysis/global"
+
+// BinlogFormatStruct
+// @Description: 数据库的binlog格式
+type BinlogFormatStruct struct {
+	VariableName string `json:"Variable_name"`
+	Value        string `json:"Value"`
+}
+
+// GetBinlogFormat
+// @Description: 获取数据库的binlog格式
+// @return item
+// @return err
+func GetBinlogFormat() (item *BinlogFormatStruct, err error) {
+	sql := `SHOW VARIABLES LIKE 'binlog_format';`
+	err = global.MYSQL["index"].Raw(sql).First(&item).Error
+	return
+}
+
+type BinlogFileStruct struct {
+	File     string `json:"File"`
+	Position uint32 `json:"Position"`
+}
+
+// GetShowMaster
+// @Description: 获取master的状态
+// @return item
+// @return err
+func GetShowMaster() (item *BinlogFileStruct, err error) {
+	sql := `show master status;`
+	err = global.MYSQL["index"].Raw(sql).First(&item).Error
+
+	return
+}

+ 59 - 0
models/index/business_sys_interaction_log.go

@@ -0,0 +1,59 @@
+package index
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+// BusinessSysInteractionLog 商家系统交互记录表
+type BusinessSysInteractionLog struct {
+	ID             uint32    `gorm:"primaryKey;column:id;type:int(10) unsigned;not null" json:"-"`
+	InteractionKey string    `gorm:"unique;column:interaction_key;type:varchar(128);not null;default:''" json:"interactionKey"` // 记录Key
+	InteractionVal string    `gorm:"column:interaction_val;type:text;default:null" json:"interactionVal"`                       // 记录值
+	Remark         string    `gorm:"column:remark;type:varchar(128);not null;default:''" json:"remark"`                         // 备注
+	ModifyTime     time.Time `gorm:"column:modify_time;type:datetime;default:null" json:"modifyTime"`                           // 修改日期
+	CreateTime     time.Time `gorm:"column:create_time;type:datetime;default:null" json:"createTime"`                           // 创建时间
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *BusinessSysInteractionLog) TableName() string {
+	return "business_sys_interaction_log"
+}
+
+// BusinessSysInteractionLogColumns get sql column name.获取数据库列名
+var BusinessSysInteractionLogColumns = struct {
+	ID             string
+	InteractionKey string
+	InteractionVal string
+	Remark         string
+	ModifyTime     string
+	CreateTime     string
+}{
+	ID:             "id",
+	InteractionKey: "interaction_key",
+	InteractionVal: "interaction_val",
+	Remark:         "remark",
+	ModifyTime:     "modify_time",
+	CreateTime:     "create_time",
+}
+
+// Create 添加数据
+func (m *BusinessSysInteractionLog) Create() (err error) {
+	err = global.MYSQL["index"].Create(m).Error
+	return
+}
+
+// Update 更新数据
+func (m *BusinessSysInteractionLog) Update(cols []string) (err error) {
+	err = global.MYSQL["index"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+var BinlogFileNameKey = "binlog_filename"        // binlog文件名
+var BinlogPositionKey = "binlog_binlog_position" // binlog位置
+
+// GetBusinessSysInteractionLogByKey 根据记录key获取数据
+func GetBusinessSysInteractionLogByKey(key string) (item *BusinessSysInteractionLog, err error) {
+	err = global.MYSQL["index"].Where("interaction_key = ?", key).First(&item).Error
+	return
+}

+ 44 - 0
models/index/edb_data_business.go

@@ -0,0 +1,44 @@
+package index
+
+import "github.com/rdlucklib/rdluck_tools/paging"
+
+type GetBusinessDataReq struct {
+	CurrentIndex int    `json:"current_index" form:"current_index" binding:"required"  description:"当前页码"`
+	PageSize     int    `json:"page_size" form:"page_size" binding:"required"  description:"单页条数,自己填,一页要多少条,最大2000条"`
+	IndexCode    string `json:"index_code" form:"index_code" description:"指标编码"`
+	IndexPkgCode string `json:"index_pkg_code" form:"index_pkg_code" description:"指标包编码,与指标编码二选一填写"`
+	DataDate     string `json:"data_date" form:"data_date" description:"指定数据日期,数据日期格式为2024-05-15"`
+	StartDate    string `json:"start_date" form:"start_date" description:"启始时间,格式 YYYY-MM-DD 如:2024-05-15;不为空时,将过滤出数据日期>=startDate 的数据行"`
+	EndDate      string `json:"end_date" form:"end_date" description:"结束时间,格式 YYYY-MM-DD 如:2024-05-15;不为空时,将过滤出数据日期<=endDate 的数据行"`
+	//Sort                      string `json:"sort" description:"排序字段,默认为0正序;按数据日期字段排序,0为正序 1为倒序"`
+	DataSourceType            string `json:"data_source_type" form:"data_source_type"  description:"内部来源系统参数,可只获取对应数据源数据,不传则默认获取所有数据源信息;参数含义:① CY产研平台;② RPA;③ KSF 金仕达;④CRM参数例子: CY,RPA,KSF,CRM  (参数传递字符串列表,通过逗号分隔)"`
+	InfoLastUpdateStartTime   string `json:"info_last_update_start_time" form:"info_last_update_start_time" description:"资产信息数据落到数仓时间,参数:YYYY-MM-DD HH24:MI:SS 如:2024-05-15 10:16:52不为空时,将过滤出 资产信息入库时间>=infoLastUpdateStartTime 的数据行"`
+	InfoLastUpdateEndTime     string `json:"info_last_update_end_time" form:"info_last_update_end_time" description:"资产信息数据落到数仓时间,参数:YYYY-MM-DD HH24:MI:SS 如:2024-05-15 10:16:52不为空时,将过滤出 资产信息入库时间<=infoLastUpdateStartTime 的数据行"`
+	DetailLastUpdateStartTime string `json:"detail_last_update_start_time" form:"detail_last_update_start_time" description:"明细数据落到数仓启始时间,参数:YYYY-MM-DD HH24:MI:SS 如:2024-05-15 10:16:52不为空时,将过滤出 资产详细信息入库时间>=detailLastUpdateStartTime 的数据行,建议延迟15分钟抽取"`
+	DetailLastUpdateEndTime   string `json:"detail_last_update_end_time" form:"detail_last_update_end_time" description:"明细数据落到数仓结束时间,参数:YYYY-MM-DD HH24:MI:SS 如:2024-05-15 10:16:52不为空时,将过滤出 资产详细信息入库时间<=detailLastUpdateStartTime 的数据行,建议延迟15分钟抽取"`
+}
+
+type GetBusinessDataResp struct {
+	List   []PushBusinessIndex
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+// PushBusinessIndex
+// @Description:  添加外部指标(商家)请求
+type PushBusinessIndex struct {
+	IndexCode            string            `description:"指标编码"`
+	IndexName            string            `description:"指标名称"`
+	Unit                 string            `description:"单位"`
+	Frequency            string            `description:"频度"`
+	SourceName           string            `description:"数据来源名称"`
+	Remark               string            `description:"备注字段"`
+	DetailDataCreateTime string            `description:"明细数据入库时间"`
+	DataList             []AddBusinessData `description:"指标数据"`
+}
+
+// AddBusinessData
+// @Description: 外部指标(商家系统)数据
+type AddBusinessData struct {
+	Value float64 `description:"值"`
+	Date  string  `description:"日期"`
+}

+ 35 - 0
models/index/edb_update_log.go

@@ -0,0 +1,35 @@
+package index
+
+import (
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"time"
+)
+
+type EdbUpdateLog struct {
+	Id          int64     `gorm:"column:id;type:bigint(20) UNSIGNED;primaryKey;not null;" json:"id"`
+	OpDbName    string    `gorm:"column:op_db_name;type:varchar(255);comment:库名;default:NULL;" json:"op_db_name"` // 库名
+	OpTableName string    `gorm:"column:op_table_name;type:varchar(255);comment:表名;" json:"op_table_name"`        // 表名
+	OpType      string    `gorm:"column:op_type;type:varchar(16);comment:变更类型;default:NULL;" json:"op_type"`      // 变更类型
+	OldData     string    `gorm:"column:old_data;type:text;comment:历史数据;" json:"old_data"`                        // 历史数据
+	NewData     string    `gorm:"column:new_data;type:text;comment:新数据;" json:"new_data"`                         // 新数据
+	CreateTime  time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP;" json:"create_time"`
+}
+
+func (m *EdbUpdateLog) TableName() string {
+	return "edb_update_log"
+}
+
+// Create 添加数据
+func (m *EdbUpdateLog) Create() (err error) {
+	err = global.MYSQL["index"].Create(m).Error
+	return
+}
+
+// DeleteBeforeTenDayLog 清除n前的日志数据
+func (m *EdbUpdateLog) DeleteBeforeTenDayLog(beforeDate string) (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE create_time < ?`, m.TableName())
+	err = global.MYSQL["index"].Exec(sql, beforeDate).Error
+
+	return
+}

+ 41 - 0
models/rddp/report_attachment.go

@@ -0,0 +1,41 @@
+package rddp
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+// ReportAttachment 报告附件
+type ReportAttachment struct {
+	ReportAttachmentID int       `gorm:"primaryKey;column:report_attachment_id" json:"-"`
+	ReportID           int       `gorm:"column:report_id" json:"reportId"`     // 报告id
+	Title              string    `gorm:"column:title" json:"title"`            // 附件名称
+	URL                string    `gorm:"column:url" json:"url"`                // 附件地址
+	CreateTime         time.Time `gorm:"column:create_time" json:"createTime"` // 附件新增时间
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *ReportAttachment) TableName() string {
+	return "report_attachment"
+}
+
+// ReportAttachmentColumns get sql column name.获取数据库列名
+var ReportAttachmentColumns = struct {
+	ReportAttachmentID string
+	ReportID           string
+	Title              string
+	URL                string
+	CreateTime         string
+}{
+	ReportAttachmentID: "report_attachment_id",
+	ReportID:           "report_id",
+	Title:              "title",
+	URL:                "url",
+	CreateTime:         "create_time",
+}
+
+// AddReportAttachment 新增附件
+func AddReportAttachment(item *ReportAttachment) (err error) {
+	err = global.MYSQL["rddp"].Create(item).Error
+	return
+}

+ 39 - 0
models/rddp/sys_session.go

@@ -0,0 +1,39 @@
+package rddp
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysSession struct {
+	Id              int       `gorm:"primaryKey;column:id;type:bigint(20);not null" json:"id"`
+	SysUserId       int       `gorm:"column:sys_user_id;type:bigint(20)" json:"sys_user_id"`
+	UserName        string    `gorm:"column:user_name;type:varchar(32)" json:"user_name"`
+	AccessToken     string    `gorm:"index:access_token;column:access_token;type:varchar(255)" json:"access_token"`
+	IsRemember      int       `gorm:"column:is_remember;type:tinyint(9) unsigned;default:0" json:"is_remember"` // 是否记住密码,属于受信设备
+	ExpiredTime     time.Time `gorm:"column:expired_time;type:datetime" json:"expired_time"`
+	CreatedTime     time.Time `gorm:"index:created_time;column:created_time;type:datetime;default:CURRENT_TIMESTAMP" json:"created_time"` // 创建时间
+	LastUpdatedTime time.Time `gorm:"index:last_updated_time;column:last_updated_time;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"last_updated_time"`
+}
+
+func (m *SysSession) TableName() string {
+	return "sys_session"
+}
+
+// AddSysSession 新增session
+func AddSysSession(item *SysSession) (err error) {
+	err = global.MYSQL["rddp"].Create(item).Error
+	return
+}
+
+// GetSysSessionByToken 根据Token获取session
+func GetSysSessionByToken(token string) (item *SysSession, err error) {
+	err = global.MYSQL["rddp"].Where("access_token = ? AND expired_time > NOW()", token).Order("expired_time DESC").First(&item).Error
+	return
+}
+
+// GetUserUnexpiredSysSession 获取用户未过期的Token
+func GetUserUnexpiredSysSession(userName, expiredTime string) (item *SysSession, err error) {
+	err = global.MYSQL["rddp"].Where("user_name = ? AND expired_time > ?", userName, expiredTime).Order("expired_time DESC").First(&item).Error
+	return
+}

+ 25 - 0
models/rddp/sys_user_login_record.go

@@ -0,0 +1,25 @@
+package rddp
+
+import (
+	"eta/eta_email_analysis/global"
+	"time"
+)
+
+type SysUserLoginRecord struct {
+	Id         int       `gorm:"primaryKey;column:id;type:int(11);not null" json:"id"`
+	Uid        int       `gorm:"column:uid;type:int(11)" json:"uid"`
+	UserName   string    `gorm:"column:user_name;type:varchar(30)" json:"user_name"`
+	Ip         string    `gorm:"column:ip;type:varchar(30)" json:"ip"`
+	Stage      string    `gorm:"column:stage;type:varchar(30)" json:"stage"`
+	CreateTime time.Time `gorm:"column:create_time;type:datetime" json:"create_time"`
+}
+
+func (m *SysUserLoginRecord) TableName() string {
+	return "sys_user_login_record"
+}
+
+// AddSysUserLoginRecord 新增登录记录
+func AddSysUserLoginRecord(item *SysUserLoginRecord) (err error) {
+	err = global.MYSQL["rddp"].Create(item).Error
+	return
+}

+ 11 - 0
models/request/auth.go

@@ -0,0 +1,11 @@
+package request
+
+type CreateAuthCodeReq struct {
+	Source    int    `json:"source" form:"source" binding:"oneof=1 2" description:"来源: 1-CRM; 2-ETA"`
+	AdminName string `json:"admin_name" form:"admin_name" binding:"required"`
+}
+
+type TokenLoginReq struct {
+	AuthCode string `json:"auth_code" form:"auth_code"`
+	Code     string `json:"code" form:"code"`
+}

+ 16 - 0
models/request/xiangyu/auth.go

@@ -0,0 +1,16 @@
+package xiangyu
+
+// GetTokenReq 获取token请求体
+type GetTokenReq struct {
+	Code string `json:"code" form:"code" description:"code编码"`
+}
+
+// RefreshTokenReq 刷新token请求体
+type RefreshTokenReq struct {
+	RefreshToken string `json:"refresh_token" form:"refresh_token" description:"刷新token"`
+}
+
+// RevokeTokenReq 销毁token请求体
+type RevokeTokenReq struct {
+	Token string `json:"token" form:"token" description:"token"`
+}

+ 52 - 0
models/request/xiangyu/index.go

@@ -0,0 +1,52 @@
+package xiangyu
+
+// PushIndexParamReq
+// @Description: 业务报文
+type PushIndexParamReq struct {
+	SerialID    string                 `json:"serialID" description:"流水号"`
+	TableCode   string                 `json:"tableCode" description:"数据表编码"`
+	Total       int                    `json:"total" description:"本次落表数据总数"`
+	IsEmailWarn int                    `json:"isEmailWarn" description:"是否发送预警邮件,(1-是 0-否)"`
+	Data        []PushDataParamDataReq `json:"data" description:"报文体,指标数据列表"`
+}
+
+// PushDataParamDataReq
+// @Description: 指标数据结构
+type PushDataParamDataReq struct {
+	SourceIndexCode      string `json:"source_index_code" description:"上游来源指标ID"`
+	IndexCode            string `json:"Index_code" description:""`
+	IndexName            string `json:"index_name" description:""`
+	IndexShortName       string `json:"index_short_name" description:""`
+	FrequenceName        string `json:"frequence_name" description:""`
+	UnitName             string `json:"unit_name" description:""`
+	CountryName          string `json:"country_name" description:""`
+	ProvinceName         string `json:"province_name" description:""`
+	AreaName             string `json:"area_name" description:""`
+	CityName             string `json:"city_name" description:""`
+	CountyName           string `json:"county_name" description:""`
+	RegionName           string `json:"region_name" description:""`
+	CompanyName          string `json:"company_name" description:""`
+	BreedName            string `json:"breed_name" description:""`
+	MaterialName         string `json:"material_name" description:""`
+	SpecName             string `json:"spec_name" description:""`
+	MarketName           string `json:"market_name" description:""`
+	DerivativeType       string `json:"derivative_type" description:""`
+	ContractName         string `json:"contract_name" description:""`
+	AuthKindName         string `json:"auth_kind_name" description:""`
+	CustomSmallClassName string `json:"custom_small_class_name" description:""`
+	AssetBeginDate       string `json:"asset_begin_date" description:""`
+	AssetEndDate         string `json:"asset_end_date" description:""`
+	CreateUser           string `json:"create_user" description:""`
+	IndexCreateTime      string `json:"index_create_time" description:""`
+	UpdateUser           string `json:"update_user" description:""`
+	DetailUpdateTime     string `json:"detail_update_time" description:""`
+	IndexUpdateTime      string `json:"index_update_time" description:""`
+	DutyDept             string `json:"duty_dept" description:""`
+	BusinessDept         string `json:"business_dept" description:""`
+	OrginSource          string `json:"orgin_source" description:""`
+	OrginSysSource       string `json:"orgin_sys_source" description:""`
+	SysSource            string `json:"sys_source" description:""`
+	SourceType           string `json:"source_type" description:""`
+	EtlTime              string `json:"etl_time" description:""`
+	Status               int    `json:"status" description:""`
+}

+ 13 - 0
models/response/auth.go

@@ -0,0 +1,13 @@
+package response
+
+type LoginResp struct {
+	Authorization   string
+	AdminName       string `description:"系统用户名称"`
+	RealName        string `description:"系统用户姓名"`
+	RoleName        string `description:"角色名称"`
+	RoleTypeCode    string `description:"角色类型编码"`
+	SysRoleTypeCode string `description:"角色类型编码"`
+	AdminId         int    `description:"系统用户id"`
+	ProductName     string `description:"产品名称:admin,ficc,权益"`
+	Authority       int    `description:"管理权限,0:无,1:部门负责人,2:小组负责人,或者ficc销售主管,4:ficc销售组长"`
+}

+ 12 - 0
routers/auth.go

@@ -0,0 +1,12 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+func InitAuth(r *gin.RouterGroup) {
+	//登录
+	//authController := new(controller.AuthController)
+	//authGroup := r.Group("auth/").Use(middleware.Token())
+	//authGroup.POST("auth_code", authController.CreateAuthCode)
+}

+ 52 - 0
routers/xiangyu.go

@@ -0,0 +1,52 @@
+package routers
+
+import (
+	"eta/eta_email_analysis/controller/xiangyu"
+	"eta/eta_email_analysis/middleware"
+	"github.com/gin-gonic/gin"
+)
+
+// InitXiangyu 获取象屿
+func InitXiangyu(r *gin.RouterGroup) {
+	control := new(xiangyu.XiangyuController)
+	group := r.Group("xy/")
+	group.GET("user/sync", control.SyncUser)
+	group.GET("user/pull", control.PullUser)
+
+	// 需要内部鉴权的接口
+	initAuthXiangyu(r)
+
+	// 指标接口
+	initIndexXiangyu(r)
+
+	// crm数据接口
+	initCrmXiangyu(r)
+}
+
+func initAuthXiangyu(r *gin.RouterGroup) {
+	control := new(xiangyu.XiangyuController)
+	group := r.Group("xy/").Use(middleware.InternalToken())
+	group.POST("auth/getToken", control.GetToken)
+	group.GET("auth/refreshToken", control.RefreshToken)
+	group.POST("auth/revokeToken", control.RevokeToken)
+}
+
+func initIndexXiangyu(r *gin.RouterGroup) {
+	control := new(xiangyu.XiangyuController)
+	group := r.Group("xy/").Use(middleware.InternalToken())
+	group.POST("index/pushIndexData", control.PushIndexDataResp)
+	group.POST("index/pushIndexValue", control.PushIndexValueDataResp)
+	group.POST("index/pushClassify", control.PushClassifyDataResp)
+	group.POST("index/pushEdbClassify", control.PushEdbClassifyDataResp)
+}
+
+// initCrmXiangyu
+// @Description: Crm数据接口
+// @author: Roc
+// @datetime 2024-05-14 17:21:22
+// @param r *gin.RouterGroup
+func initCrmXiangyu(r *gin.RouterGroup) {
+	control := new(xiangyu.XiangyuController)
+	group := r.Group("xy/index/").Use(middleware.InternalToken())
+	group.GET("crm/getCrmData", control.GetCrmData)
+}

+ 31 - 0
services/alarm_msg/alarm_msg.go

@@ -0,0 +1,31 @@
+package alarm_msg
+
+import (
+	"encoding/json"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"github.com/rdlucklib/rdluck_tools/http"
+)
+
+var (
+	AlarmMsgUrl = "http://127.0.0.1:8606/api/alarm/send"
+)
+
+// SendAlarmMsg
+// projectName-项目名称
+// runMode-运行模式
+// msgBody-消息内容
+// level:消息基本,1:提示消息,2:警告消息,3:严重错误信息,默认为1 提示消息
+func SendAlarmMsg(msgBody string, level int) {
+	params := make(map[string]interface{})
+	params["ProjectName"] = utils.APPNAME
+	params["RunMode"] = global.CONFIG.Serve.RunMode
+	params["MsgBody"] = msgBody
+	params["Level"] = level
+	param, err := json.Marshal(params)
+	if err != nil {
+		global.LOG.Critical("SendAlarmMsg json.Marshal Err:" + err.Error())
+		return
+	}
+	_, _ = http.Post(AlarmMsgUrl, string(param))
+}

+ 22 - 0
services/company.go

@@ -0,0 +1,22 @@
+package services
+
+import "eta/eta_email_analysis/utils"
+
+// GetProductId 根据角色类型获取对应的产品ID
+func GetProductId(roleTypeCode string) (productId int) {
+	if roleTypeCode == utils.ROLE_TYPE_CODE_FICC_SELLER ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_FICC_ADMIN ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_FICC_TEAM ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_FICC_GROUP ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_FICC_DEPARTMENT {
+		productId = 1
+	} else if roleTypeCode == utils.ROLE_TYPE_CODE_RAI_SELLER ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_RAI_ADMIN ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_RAI_GROUP ||
+		roleTypeCode == utils.ROLE_TYPE_CODE_RAI_DEPARTMENT {
+		productId = 2
+	} else {
+		productId = 0
+	}
+	return
+}

+ 199 - 0
services/oss/ali_oss.go

@@ -0,0 +1,199 @@
+package oss
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/services/alarm_msg"
+	"fmt"
+	"github.com/aliyun/aliyun-oss-go-sdk/oss"
+	"time"
+
+	"eta/eta_email_analysis/utils"
+	"github.com/aliyun/alibaba-cloud-sdk-go/services/sts"
+)
+
+type STSToken struct {
+	AccessKeyId     string
+	AccessKeySecret string
+	SecurityToken   string
+	ExpiredTime     string
+	RegionId        string
+	Bucketname      string
+	Endpoint        string
+	Imghost         string
+}
+
+// GetOssSTSToken 获取STSToken
+func GetOssSTSToken() (item *STSToken, err error) {
+	defer func() {
+		if err != nil {
+			global.FILE_LOG.Info(err.Error())
+			go alarm_msg.SendAlarmMsg("获取STSToken失败, ErrMsg: "+err.Error(), 3)
+		}
+	}()
+	item = new(STSToken)
+	// 获取缓存中的Token
+	recent, _ := global.Rc.RedisString(global.CONFIG.Oss.Ram.STSTokenCacheKey)
+	if recent != "" {
+		lastToken := new(STSToken)
+		if e := json.Unmarshal([]byte(recent), &lastToken); e != nil {
+			err = errors.New("GetOssSTSToken lastToken Unmarshal Err: " + e.Error())
+			return
+		}
+		// 未防止正在上传大文件时Token过期, 将判定的过期时间提前10分钟
+		afterTime := time.Now().Local().Add(10 * time.Minute)
+		expired, e := time.ParseInLocation(utils.FormatDateTime, lastToken.ExpiredTime, time.Local)
+		if e != nil {
+			err = errors.New("GetOssSTSToken expiredTime Parse Err: " + e.Error())
+			return
+		}
+		if expired.After(afterTime) {
+			item.AccessKeyId = lastToken.AccessKeyId
+			item.AccessKeySecret = lastToken.AccessKeySecret
+			item.SecurityToken = lastToken.SecurityToken
+			item.ExpiredTime = lastToken.ExpiredTime
+			item.RegionId = global.CONFIG.Oss.RegionId
+			item.Bucketname = global.CONFIG.Oss.BucketName
+			item.Endpoint = global.CONFIG.Oss.EndPoint
+			item.Imghost = global.CONFIG.Oss.ImgHost
+			return
+		}
+	}
+	// 已过期则获取新的token
+	newToken, e := NewSTSToken()
+	if e != nil {
+		err = errors.New("GetOssSTSToken NewSTSToken Err: " + e.Error())
+		return
+	}
+	newTokenJson, e := json.Marshal(newToken)
+	if e != nil {
+		err = errors.New("GetOssSTSToken NewToken JSON Err: " + e.Error())
+		return
+	}
+	// 覆盖缓存
+	if e := global.Rc.Put(global.CONFIG.Oss.Ram.STSTokenCacheKey, newTokenJson, time.Hour); e != nil {
+		err = errors.New("GetOssSTSToken SetRedis Err: " + e.Error())
+		return
+	}
+	item = newToken
+	return
+}
+
+// NewSTSToken 获取一个新的STSToken
+func NewSTSToken() (item *STSToken, err error) {
+	defer func() {
+		if err != nil {
+			global.FILE_LOG.Info("获取阿里云NewSTSToken失败", err.Error())
+		}
+	}()
+	item = new(STSToken)
+
+	client, e := sts.NewClientWithAccessKey(global.CONFIG.Oss.RegionId, global.CONFIG.Oss.Ram.AccessKeyId, global.CONFIG.Oss.Ram.AccessKeySecret)
+	if e != nil {
+		err = errors.New("NewSTSToken NewClient Err: " + e.Error())
+		return
+	}
+	request := sts.CreateAssumeRoleRequest()
+	request.Scheme = global.CONFIG.Oss.Ram.StsScheme
+	request.RegionId = global.CONFIG.Oss.RegionId
+	request.RoleArn = global.CONFIG.Oss.Ram.RoleArn
+	now := time.Now().Format(utils.FormatDateTimeUnSpace)
+	request.RoleSessionName = global.CONFIG.Oss.Ram.RoleSessionName + now
+	request.DurationSeconds = "3600"
+	request.ConnectTimeout = 300 * time.Second
+	request.ReadTimeout = 300 * time.Second
+
+	response, e := client.AssumeRole(request)
+	if e != nil {
+		err = errors.New("NewSTSToken AssumeRole Err: " + e.Error())
+		return
+	}
+	if response != nil {
+		item.AccessKeyId = response.Credentials.AccessKeyId
+		item.AccessKeySecret = response.Credentials.AccessKeySecret
+		item.SecurityToken = response.Credentials.SecurityToken
+		t, _ := time.Parse(time.RFC3339, response.Credentials.Expiration)
+		expiration := t.In(time.Local)
+		item.ExpiredTime = expiration.Format(utils.FormatDateTime)
+		item.RegionId = global.CONFIG.Oss.RegionId
+		item.Bucketname = global.CONFIG.Oss.BucketName
+		item.Endpoint = global.CONFIG.Oss.EndPoint
+		item.Imghost = global.CONFIG.Oss.ImgHost
+	}
+	return
+}
+
+// UploadAliyunToDir 上传至hzchart
+//func UploadAliyunToDir(filename, filepath, uploadDir, fileDir string) (string, error) {
+//	if utils.AccessKeyId == `` {
+//		return "0", errors.New("阿里云信息未配置")
+//	}
+//	client, err := oss.New(utils.Endpoint, utils.AccessKeyId, utils.AccessKeySecret)
+//	if err != nil {
+//		return "1", err
+//	}
+//	bucket, err := client.Bucket(utils.Bucketname)
+//	if err != nil {
+//		return "2", err
+//	}
+//	if uploadDir == "" {
+//		uploadDir = utils.UploadDir
+//	}
+//	if fileDir == "" {
+//		fileDir = time.Now().Format("200601/20060102/")
+//	}
+//	path := uploadDir + fileDir
+//	path += filename
+//	err = bucket.PutObjectFromFile(path, filepath)
+//	if err != nil {
+//		return "3", err
+//	}
+//	path = utils.Imghost + path
+//	return path, err
+//}
+
+type AliOss struct{}
+
+// UploadFile 上传文件
+func (m *AliOss) UploadFile(fileName, filePath, savePath string) (string, error) {
+	if global.CONFIG.Oss.AccessKeyId == `` {
+		return "0", errors.New("阿里云信息未配置")
+	}
+	client, err := oss.New(global.CONFIG.Oss.EndPoint, global.CONFIG.Oss.AccessKeyId, global.CONFIG.Oss.AccessKeySecret)
+	if err != nil {
+		return "1", err
+	}
+	bucket, err := client.Bucket(global.CONFIG.Oss.BucketName)
+	if err != nil {
+		return "2", err
+	}
+
+	path := savePath
+	if savePath == "" {
+		path = global.CONFIG.Oss.UploadDir + time.Now().Format("200601/20060102/") + fileName
+	}
+	err = bucket.PutObjectFromFile(path, filePath)
+	if err != nil {
+		return "3", err
+	}
+	resourceUrl := global.CONFIG.Oss.ImgHost + path
+	return resourceUrl, err
+}
+
+func (m *AliOss) GetUploadToken() (token OssToken, err error) {
+	stsToken, e := GetOssSTSToken()
+	if e != nil {
+		err = fmt.Errorf("GetOssSTSToken err: %s", e.Error())
+		return
+	}
+	token.AccessKeyId = stsToken.AccessKeyId
+	token.AccessKeySecret = stsToken.AccessKeySecret
+	token.SecurityToken = stsToken.SecurityToken
+	token.ExpiredTime = stsToken.ExpiredTime
+	token.RegionId = stsToken.RegionId
+	token.Bucketname = stsToken.Bucketname
+	token.Endpoint = stsToken.Endpoint
+	token.Imghost = stsToken.Imghost
+	return
+}

+ 109 - 0
services/oss/aws_s3.go

@@ -0,0 +1,109 @@
+package oss
+
+import (
+	"bytes"
+	"crypto/tls"
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/s3"
+	"io/ioutil"
+	"net/http"
+	"time"
+)
+
+type S3Oss struct{}
+
+func (m *S3Oss) UploadFile(fileName, localFile, savePath string) (resourceUrl string, err error) {
+	defer func() {
+		if err != nil {
+			fmt.Println(err.Error())
+		}
+	}()
+
+	// 默认使用后端这个, 这里有两个配置的原因是
+	// 前端上传跨域问题可能会使用反向代理来解决, 这样的话同一个endpoint就会导致一端正常另一端不正常
+	endpoint := global.CONFIG.Oss.BackEndPoint
+	if endpoint == "" {
+		endpoint = global.CONFIG.Oss.EndPoint
+	}
+	accessKey := global.CONFIG.Oss.AccessKeyId
+	secretKey := global.CONFIG.Oss.AccessKeySecret
+	region := global.CONFIG.Oss.RegionId
+	bucketName := global.CONFIG.Oss.BucketName
+	uploadDir := global.CONFIG.Oss.UploadDir
+
+	config := &aws.Config{
+		Region:           aws.String(region),
+		Credentials:      credentials.NewStaticCredentials(accessKey, secretKey, ""),
+		Endpoint:         aws.String(endpoint),
+		S3ForcePathStyle: aws.Bool(global.CONFIG.Oss.ForceStyle),
+		DisableSSL:       aws.Bool(global.CONFIG.Oss.DisableSSL),
+	}
+	if global.CONFIG.Oss.DisableSSL {
+		config.HTTPClient = &http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+			},
+		}
+	}
+	//b, _ := json.Marshal(config)
+	//fmt.Println(string(b))
+
+	// 创建AWS会话
+	sess, e := session.NewSession(config)
+	if e != nil {
+		err = fmt.Errorf("new session err: %s", e.Error())
+		return
+	}
+
+	// 创建S3服务客户端
+	client := s3.New(sess)
+
+	// 读取文件内容
+	fileContent, e := ioutil.ReadFile(localFile)
+	if e != nil {
+		err = fmt.Errorf("read file err: %s", e.Error())
+		return
+	}
+
+	path := savePath
+	if savePath == "" {
+		path = uploadDir + time.Now().Format("200601/20060102/") + fileName
+	}
+	putObjectInput := &s3.PutObjectInput{
+		Bucket: aws.String(bucketName),
+		Key:    aws.String(path),
+		Body:   bytes.NewReader(fileContent),
+	}
+	if global.CONFIG.Oss.OpenAcl {
+		putObjectInput.ACL = aws.String(s3.ObjectCannedACLPublicRead)
+	}
+	fmt.Printf("put object input: %+v\n", putObjectInput)
+	_, e = client.PutObject(putObjectInput)
+	if e != nil {
+		err = fmt.Errorf("put object err: %s", e.Error())
+		return
+	}
+	resourceUrl = global.CONFIG.Oss.ImgHost + path
+	if global.CONFIG.Oss.ResourceProxyUrl != "" {
+		resourceUrl = global.CONFIG.Oss.ResourceProxyUrl + path
+	}
+	return
+}
+
+func (m *S3Oss) GetUploadToken() (token OssToken, err error) {
+	token.Endpoint = global.CONFIG.Oss.EndPoint
+	token.AccessKeyId = global.CONFIG.Oss.AccessKeyId
+	token.AccessKeySecret = global.CONFIG.Oss.AccessKeySecret
+	token.RegionId = global.CONFIG.Oss.RegionId
+	token.Bucketname = global.CONFIG.Oss.BucketName
+	token.ImgHost = global.CONFIG.Oss.ImgHost
+	token.Port = global.CONFIG.Oss.Port
+
+	token.S3ForceStyle = global.CONFIG.Oss.ForceStyle
+	token.S3Protocol = global.CONFIG.Oss.Protocol
+	return
+}

+ 76 - 0
services/oss/minio.go

@@ -0,0 +1,76 @@
+package oss
+
+import (
+	"context"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"github.com/minio/minio-go/v7"
+	"github.com/minio/minio-go/v7/pkg/credentials"
+	"log"
+	"time"
+)
+
+type MinioOss struct{}
+
+// UploadFile 上传文件
+func (m *MinioOss) UploadFile(fileName, filePath, savePath string) (string, error) {
+	if global.CONFIG.Oss.AccessKeyId == `` || global.CONFIG.Oss.AccessKeySecret == `` {
+		return "0", errors.New("MinIo信息未配置")
+	}
+
+	ctx := context.Background()
+	// 此处兼容一下前后端endpoint不一致的情况, 前端用minio_endpoint后端用minio_back_endpoint, minio_back_endpoint为空则都取前者
+	endpoint := global.CONFIG.Oss.EndPoint
+	if global.CONFIG.Oss.BackEndPoint != "" {
+		endpoint = global.CONFIG.Oss.BackEndPoint
+	}
+	accessKeyID := global.CONFIG.Oss.AccessKeyId
+	secretAccessKey := global.CONFIG.Oss.AccessKeySecret
+	useSSL := false
+	if !global.CONFIG.Oss.DisableSSL {
+		useSSL = true
+	}
+	minioClient, err := minio.New(endpoint, &minio.Options{
+		Creds:  credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
+		Secure: useSSL,
+	})
+	if err != nil {
+		log.Fatalln(err)
+		return "1", err
+	}
+	bucketName := global.CONFIG.Oss.BucketName
+	exists, errBucketExists := minioClient.BucketExists(ctx, bucketName)
+	if errBucketExists != nil || !exists {
+		err = fmt.Errorf("BucketExists: %v; err: %v", exists, errBucketExists)
+		return "2", err
+	}
+
+	path := savePath
+	if savePath == "" {
+		path = global.CONFIG.Oss.UploadDir + time.Now().Format("200601/20060102/") + fileName
+	}
+	_, err = minioClient.FPutObject(ctx, bucketName, path, filePath, minio.PutObjectOptions{})
+	if err != nil {
+		log.Fatalln(err)
+		return "3", err
+	}
+	resourceUrl := global.CONFIG.Oss.ImgHost + path
+	return resourceUrl, err
+}
+
+func (m *MinioOss) GetUploadToken() (token OssToken, err error) {
+	useSsl := "1"
+	if !global.CONFIG.Oss.DisableSSL {
+		useSsl = `0`
+	}
+	token.AccessKeyId = global.CONFIG.Oss.AccessKeyId
+	token.SecretKeyId = global.CONFIG.Oss.AccessKeySecret
+	token.Endpoint = global.CONFIG.Oss.EndPoint
+	token.ImgHost = global.CONFIG.Oss.ImgHost
+	token.Bucketname = global.CONFIG.Oss.BucketName
+	token.UseSSL = useSsl
+	token.RegionId = global.CONFIG.Oss.RegionId
+	token.Port = global.CONFIG.Oss.Port
+	return
+}

+ 50 - 0
services/oss/oss.go

@@ -0,0 +1,50 @@
+package oss
+
+import (
+	"eta/eta_email_analysis/global"
+)
+
+const (
+	STORAGESOURCE_OSS_NAME   = "oss"
+	STORAGESOURCE_MINIO_NAME = "minio"
+	STORAGESOURCE_S3_NAME    = "s3"
+)
+
+type OssClient interface {
+	UploadFile(string, string, string) (string, error)
+	GetUploadToken() (OssToken, error)
+}
+
+func NewOssClient() OssClient {
+	switch global.CONFIG.Oss.ObjectStorageClient {
+	case STORAGESOURCE_MINIO_NAME:
+		return new(MinioOss)
+	case STORAGESOURCE_S3_NAME:
+		return new(S3Oss)
+	default:
+		// 默认使用阿里云OSS
+		return new(AliOss)
+	}
+}
+
+// OssToken 此处为了兼容前端那边所以有重复的
+type OssToken struct {
+	AccessKeyId string
+	SecretKeyId string
+	RegionId    string
+	Bucketname  string
+	Endpoint    string
+	ImgHost     string
+	UseSSL      string
+	Port        string
+	//AccessKeyId     string
+	AccessKeySecret string
+	SecurityToken   string
+	ExpiredTime     string
+	//RegionId        string
+	//Bucketname      string
+	//Endpoint        string
+	Imghost      string
+	S3ForceStyle bool
+	S3Protocol   string
+}

+ 104 - 0
services/pcsg/mail.go

@@ -0,0 +1,104 @@
+package pcsg
+
+import (
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/models/rddp"
+	"eta/eta_email_analysis/services/oss"
+	"eta/eta_email_analysis/utils"
+	"eta/eta_email_analysis/utils/mail"
+	"fmt"
+	"html"
+	"os"
+	"path"
+	"strings"
+	"time"
+)
+
+func ListenMail() (err error) {
+	mailMessageChan := make(chan mail.MailMessage, 5) // 创建一个通道,用于接收邮件消息
+	mailMessageDoneChan := make(chan bool, 1)         // 创建一个通道,用于接收邮件消息
+
+	fmt.Println(len(mailMessageChan))
+
+	go afterByListen(mailMessageChan, mailMessageDoneChan)
+	mailAddress := fmt.Sprintf("%s:%d", global.CONFIG.Email.Host, global.CONFIG.Email.Port)
+	mail.ListenMail(mailAddress, global.CONFIG.Email.Folder, global.CONFIG.Email.UserName, global.CONFIG.Email.Password, global.CONFIG.Email.ReadBatchSize, mailMessageChan, mailMessageDoneChan)
+
+	return
+}
+
+func afterByListen(mailMessageChan chan mail.MailMessage, mailMessageDoneChan chan bool) {
+
+	for {
+		select {
+		case mailMessage := <-mailMessageChan:
+			fmt.Println("读取成功")
+			fmt.Println(mailMessage.Title)
+			handleMailMessage(mailMessage)
+		case <-time.After(10 * time.Second):
+			fmt.Println("超时了")
+			break
+		case <-mailMessageDoneChan:
+			fmt.Println("读取完成了")
+			for len(mailMessageChan) > 0 {
+				mailMessage := <-mailMessageChan
+				handleMailMessage(mailMessage)
+			}
+			fmt.Println("结束了")
+			return
+		}
+	}
+}
+
+func handleMailMessage(mailMessage mail.MailMessage) {
+	rootPath := `C:\Users\123\go\src\eta\eta_email_analysis\static\`
+	ossClient := oss.NewOssClient()
+	fmt.Println(mailMessage.Title)
+	for k, v := range mailMessage.Resources {
+
+		randStr := utils.GetRandStringNoSpecialChar(28)
+		ext := path.Ext(v)
+		fileName := randStr + ext
+
+		resourceUrl, err := ossClient.UploadFile(fileName, v, "")
+		os.Remove(v)
+		if err != nil {
+			fmt.Println("上传文件失败:", err)
+			continue
+		}
+		tmpK := strings.Replace(k, "<", "cid:", -1)
+		tmpK = strings.Replace(tmpK, ">", "", -1)
+
+		mailMessage.Content = strings.Replace(mailMessage.Content, fmt.Sprint(tmpK, `"`), fmt.Sprint(resourceUrl, `"`), -1)
+	}
+
+	for name, v := range mailMessage.Attachment {
+		randStr := utils.GetRandStringNoSpecialChar(28)
+		ext := path.Ext(v)
+		fileName := randStr + ext
+
+		resourceUrl, err := ossClient.UploadFile(fileName, v, "")
+		os.Remove(v)
+		if err != nil {
+			fmt.Println("上传文件失败:", err)
+			continue
+		}
+		item := &rddp.ReportAttachment{
+			ReportAttachmentID: 0,
+			ReportID:           5494,
+			Title:              name,
+			URL:                resourceUrl,
+			CreateTime:         time.Now(),
+		}
+		rddp.AddReportAttachment(item)
+	}
+
+	fileName := fmt.Sprintf("%s%s.%s", rootPath, mailMessage.Title, "html")
+
+	htmlEscapeBody := html.EscapeString(mailMessage.Content)
+	fmt.Println(htmlEscapeBody)
+	err := utils.SaveToFile([]byte(mailMessage.Content), fileName)
+	if err != nil {
+		fmt.Println(fileName, "生成失败;err:", err)
+	}
+}

+ 102 - 0
services/session.go

@@ -0,0 +1,102 @@
+package services
+
+import (
+	"errors"
+	"eta/eta_email_analysis/models/eta"
+	"eta/eta_email_analysis/models/response"
+	"eta/eta_email_analysis/utils"
+	"time"
+)
+
+// CreateEtaSession
+// @Description: 创建ETA会话session
+// @author: Roc
+// @datetime 2024-01-23 17:42:54
+// @param sysUser *eta.Admin
+// @return login *response.LoginResp
+// @return err error
+// @return errMsg string
+func CreateEtaSession(sysUser *eta.Admin) (login response.LoginResp, session *eta.SysSession, err error, errMsg string) {
+	errMsg = `登录失败`
+	login = response.LoginResp{}
+
+	var token string
+	account := utils.MD5(sysUser.AdminName)
+	//// 获取用户未过期的session, 避免过于频繁生成token
+	//expired := time.Now().AddDate(0, 0, 1).Format(utils.FormatDateTime)
+	//session, _ = eta.GetUserUnexpiredSysSession(sysUser.AdminName, expired)
+	//if session != nil && session.AccessToken != "" {
+	//	token = session.AccessToken
+	//} else {
+	//	token = utils.GenToken(account)
+	//	session = new(eta.SysSession)
+	//	session.UserName = sysUser.AdminName
+	//	session.SysUserId = sysUser.AdminId
+	//	session.ExpiredTime = time.Now().AddDate(0, 0, 90)
+	//	session.IsRemember = 1
+	//	session.CreatedTime = time.Now()
+	//	session.LastUpdatedTime = time.Now()
+	//	session.AccessToken = token
+	//	if e := eta.AddSysSession(session); e != nil {
+	//		errMsg = "新增session失败, err: "
+	//		err = errors.New("新增session失败, err: " + e.Error())
+	//		return
+	//	}
+	//}
+
+	token = utils.GenToken(account)
+	session = new(eta.SysSession)
+	session.UserName = sysUser.AdminName
+	session.SysUserId = sysUser.AdminId
+	session.ExpiredTime = time.Now().AddDate(0, 0, 90)
+	session.IsRemember = 1
+	session.CreatedTime = time.Now()
+	session.LastUpdatedTime = time.Now()
+	session.AccessToken = token
+	if e := eta.AddSysSession(session); e != nil {
+		errMsg = "新增session失败, err: "
+		err = errors.New("新增session失败, err: " + e.Error())
+		return
+	}
+
+	login.Authorization = token
+	login.Authorization = "authorization=" + token
+	login.RealName = sysUser.RealName
+	login.AdminId = sysUser.AdminId
+	login.AdminName = sysUser.AdminName
+	login.RoleName = sysUser.RoleName
+	login.SysRoleTypeCode = sysUser.RoleTypeCode //系统角色编码
+	login.RoleTypeCode = sysUser.RoleTypeCode
+	login.Authority = sysUser.Authority
+
+	// 判断实际的角色类型
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_GROUP {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_TEAM {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_DEPARTMENT {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_RAI_GROUP {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_RAI_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_RAI_DEPARTMENT {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_RAI_SELLER
+	}
+	if sysUser.RoleName == utils.ROLE_NAME_FICC_DIRECTOR {
+		login.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+
+	// 角色产品ID
+	productId := GetProductId(sysUser.RoleTypeCode)
+	productIdName := map[int]string{
+		0:                             "admin",
+		utils.COMPANY_PRODUCT_FICC_ID: utils.COMPANY_PRODUCT_FICC_NAME,
+		utils.COMPANY_PRODUCT_RAI_ID:  utils.COMPANY_PRODUCT_RAI_NAME,
+	}
+	login.ProductName = productIdName[productId]
+
+	return
+}

+ 372 - 0
services/xiangyu/auth.go

@@ -0,0 +1,372 @@
+package xiangyu
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+)
+
+// ErrResp
+// @Description: 错误信息返回
+type ErrResp struct {
+	ErrCode string `json:"errcode" description:"失败编码 1001:缺少参数client_id, 2001:缺少参数access_token, 2006:缺少参数uid, 2002:参数access _token,不正确或过期, 1005:参数client_id非法"`
+	Msg     string `json:"msg" description:"失败信息"`
+}
+
+// GetTokenResp
+// @Description: 获取token返回
+type GetTokenResp struct {
+	ErrResp
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+	Uid          string `json:"uid"`
+	ExpiresIn    int    `json:"expires_in"`
+}
+
+// GetToken
+// @Description: 获取token信息
+// @author: Roc
+// @datetime 2024-01-23 15:40:56
+// @param code string
+// @return resp *GetTokenResp
+// @return err error
+func GetToken(code string) (resp *GetTokenResp, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/getToken?client_id=%s&grant_type=authorization_code&code=%s&client_secret=%s`, global.CONFIG.Xiangyu.SystemCode, code, global.CONFIG.Xiangyu.UserKey)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpPostAuth(urlPath, ``, "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.AccessToken == "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	return
+}
+
+// UserInfoResp
+// @Description: 用户信息
+type UserInfoResp struct {
+	ErrResp
+
+	Country     string `json:"country"`
+	SorgId      string `json:"sorgId"`
+	UpdateDate  string `json:"updateDate"`
+	Mail        string `json:"mail"`
+	Gender      string `json:"gender"`
+	LoginType   string `json:"loginType"`
+	Nation      string `json:"nation"`
+	DisplayName string `json:"displayName" description:"姓名"`
+	Uid         string `json:"uid" description:"唯一标识"`
+	LoginName   string `json:"loginName" description:"登录账号"`
+	//PositionNumber          string   `json:"positionNumber"`
+	//Title                   string   `json:"title"`
+	//LoginInterceptFlagTwo   int           `json:"loginInterceptFlagTwo"`
+	//LoginInterceptFlagFour  int           `json:"loginInterceptFlagFour"`
+	//EmployeeNumber          string   `json:"employeeNumber"`
+	//ChangePwdAt             string        `json:"changePwdAt"`
+	////SpRoleList              []interface{} `json:"spRoleList"`
+	//PwdPolicy               string        `json:"pwdPolicy"`
+	//LoginInterceptFlagFive  int           `json:"loginInterceptFlagFive"`
+	//LoginInterceptFlagThree int           `json:"loginInterceptFlagThree"`
+	//IdentityNumber          string  `json:"identityNumber"`
+	//IdentityType            string   `json:"identityType"`
+	//SecAccValid             int           `json:"secAccValid"`
+	//PinyinShortName         string   `json:"pinyinShortName"`
+	//OrgNumber               string   `json:"orgNumber"`
+	//WechatNo                string   `json:"wechatNo"`
+	//OrgNamePath             string        `json:"orgNamePath"`
+	//PasswordModifyRequired  int           `json:"passwordModifyRequired"`
+	//BirthDay                string        `json:"birthDay"`
+	//GivenName               string   `json:"givenName"`
+	//Mobile                  string        `json:"mobile"`
+	//LoginInterceptFlagOne   int           `json:"loginInterceptFlagOne"`
+	//CertSn                  interface{}   `json:"certSn"`
+	//EmployeeType            interface{}   `json:"employeeType"`
+	//OrgCodePath             interface{}   `json:"orgCodePath"`
+	//OtpKey                  interface{}   `json:"otpKey"`
+	//PositionStatus          interface{}   `json:"positionStatus"`
+	//DepartmentNumber        interface{}   `json:"departmentNumber"`
+	//CertDn                  interface{}   `json:"certDn"`
+	//SpNameList              []string      `json:"spNameList"`
+	//IsPassRemind            int           `json:"isPassRemind"`
+}
+
+// GetUserInfo
+// @Description: 获取用户信息
+// @author: Roc
+// @datetime 2024-01-23 15:49:38
+// @param token string
+// @return resp *UserInfoResp
+// @return err error
+func GetUserInfo(token string) (resp *UserInfoResp, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/getUserInfo?client_id=%s&access_token=%s`, global.CONFIG.Xiangyu.SystemCode, token)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpGetAuth(urlPath)
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	return
+}
+
+// CheckTokenResp
+// @Description: 校验token是否有效的返回
+type CheckTokenResp struct {
+	ErrResp
+	Result bool `json:"result" description:"校验结果,true 有效"`
+}
+
+// CheckToken
+// @Description: 校验token是否有效
+// @author: Roc
+// @datetime 2024-01-23 15:54:16
+// @param token string
+// @return isOk bool
+// @return err error
+func CheckToken(token string) (isOk bool, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/checkTokenValid?&access_token=%s`, token)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpGetAuth(urlPath)
+	if err != nil {
+		return
+	}
+
+	var resp *CheckTokenResp
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	if resp.Result {
+		isOk = true
+	}
+
+	return
+}
+
+// GetTokenInfo
+// @Description: 查询授权信息
+// @author: Roc
+// @datetime 2024-01-23 15:57:04
+// @param token string
+// @return resp *GetTokenResp
+// @return err error
+func GetTokenInfo(token string) (resp *GetTokenResp, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/getTokenInfo?&access_token=%s`, token)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpGetAuth(urlPath)
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	return
+}
+
+// RefreshToken
+// @Description: 刷新token
+// @author: Roc
+// @datetime 2024-01-23 16:02:03
+// @param refreshToken string
+// @return resp *GetTokenResp
+// @return err error
+func RefreshToken(refreshToken string) (resp *GetTokenResp, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/refreshToken?client_id=%s&grant_type=refresh_token&client_secret=%s&refresh_token=%s`, global.CONFIG.Xiangyu.SystemCode, global.CONFIG.Xiangyu.UserKey, refreshToken)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpPostAuth(urlPath, ``, "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	return
+}
+
+// RevokeToken
+// @Description: token销毁
+// @author: Roc
+// @datetime 2024-01-23 16:06:52
+// @param token string
+// @return isOk bool
+// @return err error
+func RevokeToken(token string) (isOk bool, err error) {
+	urlPath := fmt.Sprintf(`/idp/oauth2/revokeToken?&access_token=%s`, token)
+
+	if err != nil {
+		return
+	}
+	result, err := HttpGetAuth(urlPath)
+	if err != nil {
+		return
+	}
+
+	var resp *CheckTokenResp
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.ErrCode, resp.Msg))
+		return
+	}
+
+	if resp.Result {
+		isOk = true
+	}
+
+	return
+}
+
+// HttpPostAuth
+// @Description: post请求
+// @author: Roc
+// @datetime 2024-01-23 16:20:10
+// @param urlPath string
+// @param postData string
+// @param params ...string
+// @return []byte
+// @return error
+func HttpPostAuth(urlPath, postData string, params ...string) ([]byte, error) {
+	if global.CONFIG.Xiangyu.UserAuthHost == `` {
+		return nil, errors.New("统一用户同步接口地址为空")
+	}
+	// 请求地址
+	postUrl := global.CONFIG.Xiangyu.UserAuthHost + urlPath
+
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", postUrl, 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)
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// 日志记录
+	global.FILE_LOG.Debug("统一认证:地址:" + postUrl + ";\n请求参数:" + postData + ";\n返回参数:" + string(result))
+
+	//  解析返回参数,判断是否是json
+	if !json.Valid(result) {
+		err = errors.New("返回参数不是json格式")
+	}
+
+	return result, err
+}
+
+// HttpGetAuth
+// @Description: get请求
+// @author: Roc
+// @datetime 2024-01-23 16:20:16
+// @param urlPath string
+// @return []byte
+// @return error
+func HttpGetAuth(urlPath string) ([]byte, error) {
+	if global.CONFIG.Xiangyu.UserAuthHost == `` {
+		return nil, errors.New("统一用户同步接口地址为空")
+	}
+	// 请求地址
+	getUrl := global.CONFIG.Xiangyu.UserAuthHost + urlPath
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", getUrl, nil)
+	if err != nil {
+		return nil, err
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// 日志记录
+	global.FILE_LOG.Debug("统一认证:地址:" + getUrl + ";\n返回参数:" + string(result))
+
+	//  解析返回参数,判断是否是json
+	if !json.Valid(result) {
+		err = errors.New("返回参数不是json格式")
+	}
+
+	return result, err
+}

+ 228 - 0
services/xiangyu/crm.go

@@ -0,0 +1,228 @@
+package xiangyu
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+type PostGetIndexDataParamReq struct {
+	PageNum                   int    `json:"pageNum" description:"页码,先取第一页,看一下总条数,然后根据总条数给返回"`
+	PageSize                  int    `json:"pageSize" description:"单页条数,自己填,一页要多少条,最大2000条"`
+	AssetCd                   string `json:"assetCd" description:"资产编码,与资产包编码二选一填写,该类业务数据在数仓中的唯一编码,该编码由数仓提供给项目组"`
+	AssetPkgCd                string `json:"assetPkgCd" description:"资产包编码,与资产编码二选一填写,传入该参数时,将返回该资产包内的所有资产,该编码由数仓提供给项目组"`
+	DataDt                    string `json:"dataDt" description:"数据日期,如2022年5月25日的铝的市价,数据日期则为20220525"`
+	StartDt                   string `json:"startDt" description:"启始时间,格式 YYYYMMDD 如:20211010;不为空时,将过滤出数据日期>=startDate 的数据行"`
+	EndDt                     string `json:"endDt" description:"结束时间,格式 YYYYMMDD 如:20211010;不为空时,将过滤出数据日期<=endDate 的数据行"`
+	Sort                      string `json:"sort" description:"排序字段,默认为0正序;按数据日期字段排序,0为正序 1为倒序"`
+	DataSourceType            string `json:"dataSourceType" description:"内部来源系统参数,可只获取对应数据源数据,不传则默认获取所有数据源信息;参数含义:① CY产研平台;② RPA;③ KSF 金仕达;④CRM参数例子: CY,RPA,KSF,CRM  (参数传递字符串列表,通过逗号分隔)"`
+	InfoLastUpdateStartTime   string `json:"infoLastUpdateStartTime" description:"资产信息数据落到数仓时间,参数:YYYYMMDD HH24:MI:SS 如:20240229 18:00:00 不为空时,将过滤出 资产信息入库时间>=infoLastUpdateStartTime 的数据行"`
+	InfoLastUpdateEndTime     string `json:"infoLastUpdateEndTime" description:"资产信息数据落到数仓时间,参数:YYYYMMDD HH24:MI:SS 如:20240229 18:00:00 不为空时,将过滤出 资产信息入库时间<=infoLastUpdateStartTime 的数据行"`
+	DetailLastUpdateStartTime string `json:"detailLastUpdateStartTime" description:"明细数据落到数仓启始时间,参数:YYYYMMDD HH24:MI:SS 如:20240229 18:00:00 不为空时,将过滤出 资产详细信息入库时间>=detailLastUpdateStartTime 的数据行,建议延迟15分钟抽取"`
+	DetailLastUpdateEndTime   string `json:"detailLastUpdateEndTime" description:"明细数据落到数仓结束时间,参数:YYYYMMDD HH24:MI:SS 如:20240229 18:00:00 不为空时,将过滤出 资产详细信息入库时间<=detailLastUpdateStartTime 的数据行,建议延迟15分钟抽取"`
+
+	//CommonParameters In0 `json:"commonParameters" description:"公共参数"`
+}
+
+func structToURLParams(req interface{}) (string, error) {
+	// 首先,将结构体转换为map[string]interface{}
+	marshaledBytes, err := json.Marshal(req)
+	if err != nil {
+		return "", err
+	}
+
+	var params map[string]interface{}
+	err = json.Unmarshal(marshaledBytes, &params)
+	if err != nil {
+		return "", err
+	}
+
+	// 创建url.Values,它是一个键值对的集合,用于构建URL查询字符串
+	values := url.Values{}
+	for key, value := range params {
+		values.Set(key, fmt.Sprintf("%v", value))
+	}
+
+	// 返回查询字符串
+	return values.Encode(), nil
+}
+
+// CrmBaseResp
+// @Description: Crm基础信息返回
+type CrmBaseResp struct {
+	ErrCode   int         `json:"errCode"`
+	RequestId string      `json:"requestId"`
+	ErrMsg    string      `json:"errMsg"`
+	ApiLog    interface{} `json:"apiLog"`
+}
+
+// GetCrmDataResp
+// @Description: CRM数据接口返回
+type GetCrmDataResp struct {
+	CrmBaseResp
+	Data CrmDataResp `json:"data"`
+}
+
+// CrmDataResp
+// @Description: 实际数据返回
+type CrmDataResp struct {
+	TotalNum int               `json:"totalNum"`
+	PageSize int               `json:"pageSize"`
+	Rows     []CrmDataItemResp `json:"rows"`
+	PageNum  int               `json:"pageNum"`
+}
+
+// CrmDataItemResp
+// @Description: 指标数据返回
+type CrmDataItemResp struct {
+	Price                 float64     `json:"price"`
+	DataDt                string      `json:"datadt"`
+	UpdateTime            string      `json:"updatetime"`
+	AssetCd               string      `json:"assetcd"`
+	MarketName            string      `json:"marketname"`
+	AreaName              string      `json:"areaname"`
+	AssetName             string      `json:"assetname"`
+	Currency              interface{} `json:"currency"`
+	MaterialName          string      `json:"materialname"`
+	SpecName              string      `json:"specname"`
+	BreedName             string      `json:"breedname"`
+	UnitName              string      `json:"unitname"`
+	SourceName            string      `json:"sourcename"`
+	FrequencyName         string      `json:"frequencyname"`
+	CountryName           string      `json:"countryname"`
+	ProvinceName          string      `json:"provincename"`
+	CityName              string      `json:"cityname"`
+	CountyName            string      `json:"countyname"`
+	CompanyName           string      `json:"companyname"`
+	Description           string      `json:"description"`
+	BeginDate             string      `json:"begindate"`
+	EndDate               string      `json:"enddate"`
+	CreateTime            string      `json:"createtime"`
+	Status                interface{} `json:"status"`
+	PublishRuleName       string      `json:"publishrulename"`
+	AuthKind              *string     `json:"authkind"`
+	SmallClassName        interface{} `json:"smallclassname"`
+	DataSource            string      `json:"datasource"`
+	DataSourceType        string      `json:"datasourcetype"`
+	OrginSysSource        interface{} `json:"orginsyssource"`
+	SourceType            interface{} `json:"sourcetype"`
+	DeptCd                string      `json:"deptcd"`
+	DutyDept              string      `json:"dutydept"`
+	DerivativeType        string      `json:"derivativetype"`
+	StkCode               string      `json:"stkcode"`
+	StkName               string      `json:"stkname"`
+	MetricName            string      `json:"metricname"`
+	AssetInfoStockInTime  string      `json:"asset_info_stock_in_time" description:"资产信息数据落到数仓时间,参数:YYYYMMDD HH24:MI:SS;如:20240229 18:00:00"`
+	DetailDataStockInTime string      `json:"detail_data_stock_in_time" description:"明细数据落到数仓时间,参数:YYYYMMDD HH24:MI:SS;如:20240229 18:00:00"`
+}
+
+// PostGetCrmData
+// @Description: 获取CRM数据
+// @author: Roc
+// @datetime 2024-05-14 16:45:40
+// @param req PostGetIndexDataParamReq
+// @return dataResp *CrmDataResp
+// @return err error
+func PostGetCrmData(req PostGetIndexDataParamReq) (dataResp CrmDataResp, err error) {
+	urlPath := `/mpdata/index/data`
+	//req.CommonParameters = In0{
+	//	PageTotal: "",
+	//	PageNo:    "",
+	//	DocType:   "数仓市价服务",
+	//	Property:  "",
+	//	//DocCode:   getDocCode(),
+	//	Source: global.CONFIG.Xiangyu.SystemCode,
+	//	Target: global.CONFIG.Xiangyu.IndexCrmTarget,
+	//}
+
+	queryString, err := structToURLParams(req)
+	if err != nil {
+		fmt.Println("Error:", err)
+		return
+	}
+	urlPath += "?" + queryString
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPostXmxyg(urlPath, string(postData))
+	if err != nil {
+		return
+	}
+
+	var resp *GetCrmDataResp
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.ErrCode != 0 {
+		err = errors.New(fmt.Sprintf("响应代码:%d,错误信息:%s", resp.ErrCode, resp.ErrMsg))
+		return
+	}
+
+	dataResp = resp.Data
+
+	return
+}
+
+// HttpPostXmxyg
+// @Description: post请求
+// @author: Roc
+// @datetime 2024-02-27 18:45:30
+// @param urlPath string
+// @param postData string
+// @return []byte
+// @return error
+func HttpPostXmxyg(urlPath, postData string) ([]byte, error) {
+	if global.CONFIG.Xiangyu.IndexCrmHost == `` {
+		return nil, errors.New("crm数据平台地址为空")
+	}
+	// 请求地址
+	postUrl := global.CONFIG.Xiangyu.IndexCrmHost + urlPath
+
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", postUrl, body)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept-Encoding", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+
+	// 鉴权
+	req.SetBasicAuth(global.CONFIG.Xiangyu.IndexSyncAuthUserName, global.CONFIG.Xiangyu.IndexSyncAuthPwd)
+
+	// 秘钥
+	if global.CONFIG.Xiangyu.IndexKey != `` {
+		req.Header.Set("deipaaskeyauth", global.CONFIG.Xiangyu.IndexKey)
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// 日志记录
+	global.FILE_LOG.Debug("crm数据平台:地址:" + postUrl + ";\n请求参数:" + postData + ";\n返回参数:" + string(result))
+
+	//  解析返回参数,判断是否是json
+	if !json.Valid(result) {
+		err = errors.New("返回参数不是json格式")
+	}
+
+	return result, err
+}

+ 402 - 0
services/xiangyu/index.go

@@ -0,0 +1,402 @@
+package xiangyu
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+)
+
+// IndexErrResp
+// @Description: 错误信息返回
+type IndexErrResp struct {
+	ErrCode string `json:"errcode" description:"失败编码 1001:缺少参数client_id, 2001:缺少参数access_token, 2006:缺少参数uid, 2002:参数access _token,不正确或过期, 1005:参数client_id非法"`
+	Msg     string `json:"msg" description:"失败信息"`
+}
+
+// PushResp
+// @Description: push数据返回
+type PushResp struct {
+	IndexErrResp
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+	Uid          string `json:"uid"`
+	ExpiresIn    int    `json:"expires_in"`
+}
+
+// PushIndexReq
+// @Description: 指标推送请求
+type PushIndexReq struct {
+	In0        In0               `json:"in0"`
+	Parameters PushIndexParamReq `json:"parameters" description:"业务报文"`
+}
+
+// PushIndexParamReq
+// @Description: 业务报文
+type PushIndexParamReq struct {
+	SerialID    string             `json:"serialID" description:"流水号"`
+	TableCode   string             `json:"tableCode" description:"数据表编码"`
+	Total       int                `json:"total" description:"本次落表数据总数"`
+	IsEmailWarn int                `json:"isEmailWarn" description:"是否发送预警邮件,(1-是 0-否)"`
+	Data        []PushIndexItemReq `json:"data" description:"报文体,指标数据列表"`
+}
+
+// PushIndexItemReq
+// @Description: 指标数据结构
+type PushIndexItemReq struct {
+	SourceIndexCode string `json:"source_index_code" description:"上游来源指标ID"`
+	IndexCode       string `json:"index_code" description:""`
+	IndexName       string `json:"index_name" description:""`
+	IndexShortName  string `json:"index_short_name" description:""`
+	FrequenceName   string `json:"frequence_name" description:""`
+	UnitName        string `json:"unit_name" description:""`
+	//CountryName          string `json:"country_name" description:""`
+	//ProvinceName         string `json:"province_name" description:""`
+	//AreaName             string `json:"area_name" description:""`
+	//CityName             string `json:"city_name" description:""`
+	//CountyName           string `json:"county_name" description:""`
+	//RegionName           string `json:"region_name" description:""`
+	//CompanyName          string `json:"company_name" description:""`
+	//BreedName            string `json:"breed_name" description:""`
+	//MaterialName         string `json:"material_name" description:""`
+	//SpecName             string `json:"spec_name" description:""`
+	//MarketName           string `json:"market_name" description:""`
+	//DerivativeType       string `json:"derivative_type" description:""`
+	//ContractName         string `json:"contract_name" description:""`
+	//AuthKindName         string `json:"auth_kind_name" description:""`
+	//CustomSmallClassName string `json:"custom_small_class_name" description:""`
+	AssetBeginDate   string `json:"asset_begin_date" description:""`
+	AssetEndDate     string `json:"asset_end_date" description:""`
+	CreateUser       string `json:"create_user" description:""`
+	IndexCreateTime  string `json:"index_create_time" description:""`
+	UpdateUser       string `json:"update_user" description:""`
+	DetailUpdateTime string `json:"detail_update_time" description:""`
+	IndexUpdateTime  string `json:"index_update_time" description:""`
+	//DutyDept         string `json:"duty_dept" description:""`
+	//BusinessDept     string `json:"business_dept" description:""`
+	OrginSource    string `json:"orgin_source" description:""`
+	OrginSysSource string `json:"orgin_sys_source" description:""`
+	SysSource      string `json:"sys_source" description:""`
+	SourceType     string `json:"source_type" description:""`
+	Status         int    `json:"status" description:""`
+}
+
+type PushIndexDataResp struct {
+	Out struct {
+		ReturnCode string `json:"returnCode"`
+		ReturnMsg  string `json:"returnMsg"`
+		Data       int    `json:"data"`
+	} `json:"out"`
+}
+
+// PushIndexData
+// @Description: 指标信息推送
+// @author: Roc
+// @datetime 2024-02-28 17:45:03
+// @param data PushIndexParamReq
+// @return resp *PushDataResp
+// @return err error
+func PushIndexData(data PushBaseParamReq) (resp *PushIndexDataResp, err error) {
+	urlPath := `/DAQ/CY/ProxyServices/pushMarketPricePS`
+	req := PushBaseReq{
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pushMarketPricePS",
+			Property:  "",
+			//DocCode:   getDocCode(),
+			Source: global.CONFIG.Xiangyu.SystemCode,
+			Target: global.CONFIG.Xiangyu.IndexSyncTarget,
+		},
+		Parameters: data,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPostIndex(urlPath, string(postData))
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.ReturnCode != "S" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg))
+		return
+	}
+
+	return
+}
+
+// PushIndexValueReq
+// @Description: 指标日期值推送请求
+type PushIndexValueReq struct {
+	In0        In0                    `json:"in0"`
+	Parameters PushIndexValueParamReq `json:"parameters" description:"业务报文"`
+}
+
+// PushIndexValueParamReq
+// @Description: 业务报文
+type PushIndexValueParamReq struct {
+	SerialID    string                  `json:"serialID" description:"流水号"`
+	TableCode   string                  `json:"tableCode" description:"数据表编码"`
+	Total       int                     `json:"total" description:"本次落表数据总数"`
+	IsEmailWarn int                     `json:"isEmailWarn" description:"是否发送预警邮件,(1-是 0-否)"`
+	Data        []PushIndexValueItemReq `json:"data" description:"报文体,指标日期值数据列表"`
+}
+
+// PushIndexValueItemReq
+// @Description: 指标日期值数据结构
+type PushIndexValueItemReq struct {
+	Id           string `json:"id"`
+	IndexCode    string `json:"index_code" description:"指标代码"`
+	Value        string `json:"value" description:"数值"`
+	BusinessDate string `json:"business_date" description:"业务日期(数据日期)"`
+	CreateTime   string `json:"create_time" description:"数据进入ETA的时间"`
+	UpdateTime   string `json:"update_time" description:"eta库中修改数据的时间"`
+	Status       string `json:"status" description:"逻辑删除使用,0-禁用,1-启用"`
+}
+
+// PushEdbValue
+// @Description: 获取token信息
+// @author: Roc
+// @datetime 2024-01-23 15:40:56
+// @param code string
+// @return resp *GetTokenResp
+// @return err error
+func PushEdbValue(data PushBaseParamReq) (resp *PushIndexDataResp, err error) {
+	urlPath := `/DAQ/CY/ProxyServices/pushMarketPricePS`
+	req := PushBaseReq{
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pushMarketPricePS",
+			Property:  "",
+			//DocCode:   getDocCode(),
+			Source: global.CONFIG.Xiangyu.SystemCode,
+			Target: global.CONFIG.Xiangyu.IndexSyncTarget,
+		},
+		Parameters: data,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPostIndex(urlPath, string(postData))
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.ReturnCode != "S" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg))
+		return
+	}
+
+	return
+}
+
+// PushBaseReq
+// @Description: 基础请求
+type PushBaseReq struct {
+	In0        In0              `json:"in0"`
+	Parameters PushBaseParamReq `json:"parameters" description:"业务报文"`
+}
+
+// PushBaseParamReq
+// @Description: 基础业务报文
+type PushBaseParamReq struct {
+	SerialID    string      `json:"serialID" description:"流水号"`
+	TableCode   string      `json:"tableCode" description:"数据表编码"`
+	Total       int         `json:"total" description:"本次落表数据总数"`
+	IsEmailWarn int         `json:"isEmailWarn" description:"是否发送预警邮件,(1-是 0-否)"`
+	Data        interface{} `json:"data" description:"报文体,指标日期值数据列表"`
+}
+
+// PushClassifyItemReq
+// @Description: 指标分类数据结构
+type PushClassifyItemReq struct {
+	ClassifyId      int    `json:"classify_id" description:"自增id"`
+	ClassifyType    int    `json:"classify_type" description:"分类类型,0:普通指标分类,1:预测指标分类"`
+	ClassifyName    string `json:"classify_name" description:"分类名称"`
+	ParentId        int    `json:"parent_id" description:"父级id"`
+	HasData         int    `json:"has_data" description:"是否存在指标数据,1:有,2:无"`
+	CreateTime      string `json:"create_time" description:"创建时间"`
+	UpdateTime      string `json:"update_time" description:"修改时间"`
+	SysUserId       int    `json:"sys_user_id" description:"创建人id"`
+	SysUserRealName string `json:"sys_user_real_name" description:"创建人姓名"`
+	Level           int    `json:"level" description:"层级"`
+	UniqueCode      string `json:"unique_code" description:"唯一编码"`
+	SortColumn      int    `json:"sort_column" description:"排序字段,越小越靠前,默认值:10"`
+}
+
+// PushClassify
+// @Description: 推送指标分类
+// @author: Roc
+// @datetime 2024-01-23 15:40:56
+// @param code string
+// @return resp *GetTokenResp
+// @return err error
+func PushClassify(data PushBaseParamReq) (resp *PushIndexDataResp, err error) {
+	urlPath := `/DAQ/CY/ProxyServices/pushMarketPricePS`
+	req := PushBaseReq{
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pushMarketPricePS",
+			Property:  "",
+			//DocCode:   getDocCode(),
+			Source: global.CONFIG.Xiangyu.SystemCode,
+			Target: global.CONFIG.Xiangyu.IndexSyncTarget,
+		},
+		Parameters: data,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPostIndex(urlPath, string(postData))
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.ReturnCode != "S" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg))
+		return
+	}
+
+	return
+}
+
+// PushBase
+// @Description: 基础推送接口
+// @author: Roc
+// @datetime 2024-01-23 15:40:56
+// @param code string
+// @return resp *GetTokenResp
+// @return err error
+func PushBase(data PushBaseParamReq) (resp *PushIndexDataResp, err error) {
+	urlPath := `/DAQ/CY/ProxyServices/pushMarketPricePS`
+	req := PushBaseReq{
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pushMarketPricePS",
+			Property:  "",
+			//DocCode:   getDocCode(),
+			Source: global.CONFIG.Xiangyu.SystemCode,
+			Target: global.CONFIG.Xiangyu.IndexSyncTarget,
+		},
+		Parameters: data,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPostIndex(urlPath, string(postData))
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.ReturnCode != "S" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg))
+		return
+	}
+
+	return
+}
+
+// PushEdbClassifyItemReq
+// @Description: 指标与目录的关系请求结构
+type PushEdbClassifyItemReq struct {
+	Id         string `json:"id" description:"唯一主键"`
+	ClassifyId int    `json:"classify_id" description:"目录分类ID"`
+	IndexCode  string `json:"index_code" description:"指标ID"`
+	CreateTime string `json:"create_time" description:"创建时间"`
+	CreateUser string `json:"create_user" description:"创建人"`
+	UpdateTime string `json:"update_time" description:"修改时间"`
+	UpdateUser string `json:"update_user" description:"修改人"`
+}
+
+// HttpPostIndex
+// @Description: post请求
+// @author: Roc
+// @datetime 2024-02-27 18:45:30
+// @param urlPath string
+// @param postData string
+// @return []byte
+// @return error
+func HttpPostIndex(urlPath, postData string) ([]byte, error) {
+	if global.CONFIG.Xiangyu.IndexSyncHost == `` {
+		return nil, errors.New("数仓同步接口地址为空")
+	}
+	// 请求地址
+	postUrl := global.CONFIG.Xiangyu.IndexSyncHost + urlPath
+
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", postUrl, body)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("content-Type", "application/json; charset=utf-8")
+	req.Header.Set("Accept-Encoding", "application/json; charset=utf-8")
+	req.Header.Set("Accept", "application/json; charset=utf-8")
+
+	// 鉴权
+	req.SetBasicAuth(global.CONFIG.Xiangyu.IndexSyncAuthUserName, global.CONFIG.Xiangyu.IndexSyncAuthPwd)
+
+	// 秘钥
+	if global.CONFIG.Xiangyu.IndexKey != `` {
+		req.Header.Set("deipaaskeyauth", global.CONFIG.Xiangyu.IndexKey)
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// 日志记录
+	global.FILE_LOG.Debug("指标同步服务:地址:" + postUrl + ";\n请求参数:" + postData + ";\n返回参数:" + string(result))
+
+	//  解析返回参数,判断是否是json
+	if !json.Valid(result) {
+		err = errors.New("返回参数不是json格式")
+	}
+
+	return result, err
+}

+ 578 - 0
services/xiangyu/user.go

@@ -0,0 +1,578 @@
+package xiangyu
+
+import (
+	"encoding/json"
+	"errors"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+	"time"
+)
+
+// LoginReq
+// @Description: 登录请求参数
+type LoginReq struct {
+	SystemCode     string `json:"systemCode" description:"来源系统"`
+	In0            In0    `json:"in0"`
+	IntegrationKey string `json:"integrationKey" description:"集成密钥"`
+}
+
+// In0
+// @Description: 通用参数
+type In0 struct {
+	PageTotal string `json:"pageTotal" description:"总页数"`
+	PageNo    string `json:"pageNo" description:"当前请求页"`
+	DocType   string `json:"docType" description:"单据类型"`
+	Property  string `json:"property" description:"备用字段"`
+	DocCode   string `json:"docCode" description:"单据号码"`
+	Source    string `json:"source" description:"来源系统"`
+	Target    string `json:"target" description:"目标系统"`
+}
+
+// BaseResponse
+// @Description: 基础返回数据返回
+type BaseResponse struct {
+	ReturnCode string `json:"returnCode" description:"响应代码,成功--S,失败--F"`
+	ReturnMsg  string `json:"returnMsg" description:"响应消息"`
+	Success    bool   `description:"true 执行成功,false 执行失败"`
+	Message    string `description:"消息"`
+}
+
+// LoginResp
+// @Description:  登录响应参数
+type LoginResp struct {
+	Out struct {
+		ReturnCode string `json:"returnCode" description:"响应代码,成功--S,失败--F"`
+		ReturnMsg  string `json:"returnMsg" description:"响应消息"`
+		TokenId    string `json:"tokenId" description:"令牌id,login接口生成"`
+		Success    bool   `description:"true 执行成功,false 执行失败"`
+		Message    string `description:"消息"`
+	} `json:"out"`
+}
+
+// Login
+// @Description: 第三方应用系统集成统一身份管理平台,通过login接口进行身份验证,获取token,用于后续调用身份信息同步接口
+// @author: Roc
+// @datetime 2024-01-16 16:20:10
+// @return tokenId string
+// @return err error
+func Login() (tokenId string, err error) {
+	urlPath := `/IAM/XYGIntegrationService/login/ProxyServices/loginPS`
+	req := LoginReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "login",
+			Property:  "",
+			//DocCode:   getDocCode(),
+			Source: global.CONFIG.Xiangyu.SystemCode,
+			Target: global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		IntegrationKey: global.CONFIG.Xiangyu.UserKey,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	var resp *LoginResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg, resp.Out.Message))
+		return
+	}
+
+	if resp.Out.TokenId == "" {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg, resp.Out.Message))
+		return
+	}
+
+	tokenId = resp.Out.TokenId
+
+	return
+}
+
+// LogoutReq
+// @Description: 清除tokenId的请求参数
+type LogoutReq struct {
+	SystemCode string `json:"systemCode" description:"来源系统"`
+	In0        In0    `json:"in0"`
+	TokenId    string `json:"tokenId" description:"令牌id,login接口生成"`
+}
+
+// Logout
+// @Description: 第三方应用系统集成统一身份管理平台,身份信息同步完成后,调用logout接口清除tokenId,结束整个身份信息同步流程
+// @author: Roc
+// @datetime 2024-01-16 13:53:12
+// @param tokenId string
+// @return err error
+func Logout(tokenId string) (err error) {
+	urlPath := `/IAM/XYGIntegrationService/logout/ProxyServices/logoutPS`
+	req := LogoutReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "logout",
+			Property:  "",
+			DocCode:   getDocCode(),
+			Source:    global.CONFIG.Xiangyu.SystemCode,
+			Target:    global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		TokenId: tokenId,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	var resp *LoginResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg, resp.Out.Message))
+		return
+	}
+
+	return
+}
+
+// PullTaskResp
+// @Description: 开始同步用户的接口返回数据
+type PullTaskResp struct {
+	BaseResponse
+	ObjectCode string `json:"objectCode" description:"对象代码,BIM中注册对象代码:账号:T_ACC;机构:T_ORC;岗位:T_POS"`
+	Id         string `json:"id" description:"身份管理平台内部对象ID"`
+	TaskId     string `json:"taskId" description:"身份管理平台下拉任务ID"`
+	EffectOn   string `json:"effectOn" description:"任务操作类型;创建:CREATED、更新:UPDATED、禁用:DISABLED、启用:ENABLED"`
+	Timestamp  int64  `json:"timestamp" description:"BIM当前时间戳"`
+	ObjectType string `json:"objectType" description:"对象类型:账号:TARGET_ACCOUNT、机构;TARGET_ORGANIZATION、资源:TARGET_RESOURCE、角色:TARGET_ROLE"`
+	Data       string `json:"data" description:"任务数据,JSON格式,具体数据结构根据任务类型不同而不同"`
+}
+
+// AccountData
+// @Description: 账号对象信息
+type AccountData struct {
+	Username     string      `json:"username"`
+	Password     interface{} `json:"password"`
+	Fullname     string      `json:"fullname"`
+	IsDisabled   bool        `json:"isDisabled"`
+	StartDate    interface{} `json:"startDate"`
+	EndDate      interface{} `json:"endDate"`
+	IsAdmin      bool        `json:"isAdmin"`
+	CompanyEmail interface{} `json:"companyEmail"`
+	CompanyPlate string      `json:"companyPlate"`
+	DepartName   string      `json:"departName"`
+	Departld     string      `json:"departld"`
+	CompanyName  string      `json:"companyName"`
+	Companyld    string      `json:"companyld"`
+	Sex          string      `json:"sex"`
+	PyName       string      `json:"pyName"`
+	Personld     string      `json:"personld"`
+	IdCard       interface{} `json:"idCard"`
+	EmployeeNo   string      `json:"employeeNo"`
+	Mobile       interface{} `json:"mobile"`
+	ScEmail      interface{} `json:"ScEmail"`
+	Email        interface{} `json:"email"`
+}
+
+// SyncTaskResp
+// @Description:  全量同步的响应数据
+// 1.objectType 为TARGET_ACCOUNT   时,表示下拉数据为账号信息。
+// 2.objectType 为TARGET_ORGANIZATION  时,表示下拉数据为机构(公司和部门)信息。
+// 3.objectType 为TARGET_RESOURCE并且objectCode为T_USR时,表示下拉数据为HR人员信息。
+// 4.objectType为TARGET_RESOURCE  并且objectCode为T_POS 时,表示下拉数据为岗位信息。
+// 5.objectType 为TARGET_RESOURCE并且objectCode为T_SEQ时,表示下拉数据为岗位序列信息
+type SyncTaskResp struct {
+	Out struct {
+		ReturnCode string `json:"returnCode" description:"响应代码,成功--S,失败--F"`
+		ReturnMsg  string `json:"returnMsg" description:"响应消息"`
+		ObjectCode string `json:"objectCode" description:"BIM中注册对象代码:账号:T_ACC;机构:T_ORC;岗位:T_POS"`
+		Success    bool   `description:"true 执行成功,false 执行失败"`
+		Message    string `description:"消息"`
+		Id         string `json:"id"`
+		Timestamp  int64  `json:"timestamp"`
+		ObjectType string `json:"objectType" description:"对象类型:账号:TARGET_ACCOUNT、机构;TARGET_ORGANIZATION、资源:TARGET_RESOURCE、角色:TARGET_ROLE"`
+	} `json:"out"`
+}
+
+// SyncTaskUserResp
+// @Description:  全量同步的账号响应数据
+type SyncTaskUserResp struct {
+	Out struct {
+		ReturnCode string   `json:"returnCode" description:"响应代码,成功--S,失败--F"`
+		ReturnMsg  string   `json:"returnMsg" description:"响应消息"`
+		ObjectCode string   `json:"objectCode" description:"BIM中注册对象代码:账号:T_ACC;机构:T_ORC;岗位:T_POS"`
+		Success    bool     `description:"true 执行成功,false 执行失败"`
+		Message    string   `description:"消息"`
+		Id         string   `json:"id"`
+		Timestamp  int64    `json:"timestamp"`
+		ObjectType string   `json:"objectType" description:"对象类型:账号:TARGET_ACCOUNT、机构;TARGET_ORGANIZATION、资源:TARGET_RESOURCE、角色:TARGET_ROLE"`
+		Data       UserData `json:"data"`
+	} `json:"out"`
+}
+
+// UserData
+// @Description: 用户账户信息
+type UserData struct {
+	Username       string      `json:"username" description:"统一登录名,和AD账号一致"`
+	Password       string      `json:"password" description:"密码"`
+	FullName       string      `json:"fullname" description:"姓名"`
+	IsDisabled     bool        `json:"isDisabled" description:"状态(true禁用,false启用)"`
+	StartDate      interface{} `json:"startDate" description:""`
+	EndDate        interface{} `json:"endDate" description:""`
+	PositionId     string      `json:"positionId" description:"HR人员中所属岗位ID(主岗职位记录中的)"`
+	DepartId       string      `json:"departId" description:"HR人员中所属部门ID(主岗职位记录中的)"`
+	CompanyId      string      `json:"companyId" description:"HR人员中所属公司ID(主岗职位记录中的)"`
+	RefOrgid       interface{} `json:"ref_orgid" description:""`
+	PositionName   string      `json:"positionName" description:"HR人员中所属岗位名称(主岗职位记录中的)"`
+	CompanyName    string      `json:"companyName" description:"HR人员中所属公司名称(主岗职位记录中的)"`
+	DepartmentName string      `json:"departmentName" description:"HR人员中所属部门名称(主岗职位记录中的)"`
+	Mobile         string      `json:"mobile" description:"手机号"`
+	OfficePhone    string      `json:"officePhone" description:""`
+	Sex            string      `json:"sex" description:"性别(1男,2女)"`
+	EmployeeNo     string      `json:"employeeNo" description:"员工号。"`
+}
+
+// SyncTask
+// @Description: 第三方应用系统集成统一身份管理平台,通过调用syncTask同步接口,从统一身份管理平台全量同步数据(账号、机构、岗位
+// @author: Roc
+// @datetime 2024-01-18 13:30:38
+// @param tokenId string
+// @return resp *BaseResponse
+// @return err error
+func SyncTask(tokenId string) (userResp *SyncTaskUserResp, err error) {
+	urlPath := `/IAM/XYGIntegrationService/syncTask/ProxyServices/syncTaskPS`
+	req := LogoutReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "syncTask",
+			Property:  "",
+			DocCode:   getDocCode(),
+			Source:    global.CONFIG.Xiangyu.SystemCode,
+			Target:    global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		TokenId: tokenId,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	var baseResp *SyncTaskResp
+	err = json.Unmarshal(result, &baseResp)
+	if err != nil {
+		return
+	}
+
+	if baseResp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", baseResp.Out.ReturnCode, baseResp.Out.ReturnMsg, baseResp.Out.Message))
+		return
+	}
+
+	// 如果不是账号信息的话,那么就过滤,不做处理
+	if baseResp.Out.ObjectType != "TARGET_ACCOUNT" {
+		return
+	}
+
+	//  解析数据 获取账号信息
+	err = json.Unmarshal(result, &userResp)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// SyncFinishReq
+// @Description: 结束同步的接口请求
+type SyncFinishReq struct {
+	SystemCode string      `json:"systemCode" description:"来源系统"`
+	In0        In0         `json:"in0"`
+	TokenId    string      `json:"tokenId" description:"令牌id,login接口生成"`
+	ObjectCode string      `json:"objectCode" description:"对象代码,BIM中注册对象代码"`
+	ObjectType string      `json:"objectType" description:"对象类型:账号:TARGET__ACCOUNT、机构:TARGET_ORGANIZATION、资源:TARGET__RESOURCE、角色:TARGET_ROLE"`
+	Id         string      `json:"id" description:"BIM中对象ID,从syncTask响应中获取"`
+	Success    bool        `json:"success" description:"成功--true,失败--false"`
+	Data       interface{} `json:"data" description:"默认给空Map对象{)不能为null"`
+	Guid       string      `json:"guid" description:"下游系统中对象主键字段success=true时必须"`
+	Message    string      `json:"message" description:"success=false时必须"`
+}
+
+// SyncFinish
+// @Description: syncFinish 接口需要配合syncTask 接 口 同 时 使 用 ,syncFinish 接口作用是:应用系统调用syncTask  接口同步数据,并对数据做处理后,将结果通过syncFinish接口回写到统一身份管理平台
+// @author: Roc
+// @datetime 2024-01-18 13:30:38
+// @param tokenId string
+// @return resp *BaseResponse
+// @return err error
+func SyncFinish(tokenId, objectCode, objectType, id, guid, message string, success bool) (err error) {
+	urlPath := `/IAM/XYGIntegrationService/syncFinish/ProxyServices/syncFinishPS`
+
+	req := SyncFinishReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "syncFinish",
+			Property:  "",
+			DocCode:   getDocCode(),
+			Source:    global.CONFIG.Xiangyu.SystemCode,
+			Target:    global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		TokenId:    tokenId,
+		ObjectCode: objectCode,
+		ObjectType: objectType,
+		Id:         id,
+		Success:    success,
+		Data:       struct{}{},
+		Guid:       guid,
+		Message:    message,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	var resp *LoginResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg, resp.Out.Message))
+		return
+	}
+
+	return
+}
+
+// PullTaskUserResp
+// @Description:  增量同步的账号响应数据
+type PullTaskUserResp struct {
+	Out struct {
+		ReturnCode string   `json:"returnCode" description:"响应代码,成功--S,失败--F"`
+		ReturnMsg  string   `json:"returnMsg" description:"响应消息"`
+		ObjectCode string   `json:"objectCode" description:"BIM中注册对象代码:账号:T_ACC;机构:T_ORC;岗位:T_POS"`
+		Success    bool     `description:"true 执行成功,false 执行失败"`
+		Message    string   `description:"消息"`
+		Id         string   `json:"id" description:"身份管理平台内部对象ID"`
+		Timestamp  int64    `json:"timestamp"`
+		ObjectType string   `json:"objectType" description:"对象类型:账号:TARGET_ACCOUNT、机构;TARGET_ORGANIZATION、资源:TARGET_RESOURCE、角色:TARGET_ROLE"`
+		Data       UserData `json:"data"`
+		GUid       string   `json:"guid" description:"下游数据主键,下游系统下拉到的数据后,若要做更新、启 用、禁用操作,可根据此字段查询到对应记录。"`
+		EffectOn   string   `json:"effectOn" description:"作用点,任务操作类型;创建:CREATED、更新:UPDATED、禁用:DISABLED、启用:ENABLED。"`
+		TaskId     string   `json:"Taskid" description:"任务id,身份管理平台下拉任务ID"`
+	} `json:"out"`
+}
+
+// PullTask
+// @Description: 第三方应用系统集成统一身份管理平台,通过调用pulITask 下拉接口,从统一身份管理平台获取身份信息(机构、账号、岗位)。  注:下拉接口只会从统一身份管理平台下拉变更(新增、更新、启用、禁用)的记录,并且每一条变更记录在统一身份管理平台,都是 以待处理任务形式存在。 pullTask 接口下拉逻辑为:每调用一次下拉接口,获取一条待处理任务(任务中有详细身份数据)。当接口响应中 "objectCode"  为N U L L 时 , 表 示 统 一 身 份 管 理 平 台 待 处 理 任 务 全 部 下 拉 完 成 。
+// @Description: 这个接口只会返回一条数据,所以同步账号的时候,需要循环调用
+// @author: Roc
+// @datetime 2024-01-16 14:04:47
+// @param tokenId string
+// @return resp *BaseResponse
+// @return err error
+func PullTask(tokenId string) (userResp *PullTaskUserResp, err error) {
+	urlPath := `/IAM/XYGIntegrationService/pullTask/ProxyServices/pullTaskPS`
+	req := LogoutReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pullTask",
+			Property:  "",
+			DocCode:   getDocCode(),
+			Source:    global.CONFIG.Xiangyu.SystemCode,
+			Target:    global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		TokenId: tokenId,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	var baseResp *SyncTaskResp
+	err = json.Unmarshal(result, &baseResp)
+	if err != nil {
+		return
+	}
+
+	if baseResp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", baseResp.Out.ReturnCode, baseResp.Out.ReturnMsg, baseResp.Out.Message))
+		return
+	}
+
+	// 如果不是账号信息的话,那么就过滤,不做处理
+	if baseResp.Out.ObjectType != "TARGET_ACCOUNT" {
+		return
+	}
+
+	//  解析数据 获取账号信息
+	err = json.Unmarshal(result, &userResp)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+type PullFinishReq struct {
+	SystemCode string `json:"systemCode" description:"来源系统"`
+	In0        In0    `json:"in0"`
+	TokenId    string `json:"tokenId" description:"令牌id,login接口生成"`
+	TaskId     string `json:"taskId" description:"任务ID,pullTask获取的下拉任务ID"`
+	Guid       string `json:"guid" description:"唯一标识,第三方系统中对象唯一标识success=true时必须"`
+	Success    bool   `json:"success" description:"true 成功,false 失败"`
+	Message    string `json:"message" description:"错误消息,success=false时必须"`
+}
+
+// PullFinish
+// @Description: pullFinish接口作用是:应用系统调用pullTask 接口下拉数据,并对数据做处理后,将结果通过pullFinish接口回写到统一身份管理平台,统一身份管理平台根据结果标记下拉任务状态。
+// @Description: pullFinish接口需要配合pullTask 接口同时使用。
+// @author: Roc
+// @datetime 2024-01-16 14:17:27
+// @param tokenId string
+// @param taskId string
+// @param guid string
+// @param success bool
+// @param message string
+// @return resp *BaseResponse
+// @return err error
+func PullFinish(tokenId, taskId, guid, message string, success bool) (err error) {
+	urlPath := `/IAM/XYGIntegrationService/pullFinish/ProxyServices/pullFinishPS`
+	req := PullFinishReq{
+		SystemCode: global.CONFIG.Xiangyu.SystemCode,
+		In0: In0{
+			PageTotal: "",
+			PageNo:    "",
+			DocType:   "pullFinish",
+			Property:  "",
+			DocCode:   getDocCode(),
+			Source:    global.CONFIG.Xiangyu.SystemCode,
+			Target:    global.CONFIG.Xiangyu.UserSyncTarget,
+		},
+		TokenId: tokenId,
+		TaskId:  taskId,
+		Guid:    guid,
+		Success: success,
+		Message: message,
+	}
+
+	postData, err := json.Marshal(req)
+	if err != nil {
+		return
+	}
+	result, err := HttpPost(urlPath, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+
+	//  解析响应结果
+	var resp *LoginResp
+	err = json.Unmarshal(result, &resp)
+	if err != nil {
+		return
+	}
+
+	if resp.Out.Success == false {
+		err = errors.New(fmt.Sprintf("响应代码:%s,响应消息:%s,错误信息:%s", resp.Out.ReturnCode, resp.Out.ReturnMsg, resp.Out.Message))
+		return
+	}
+
+	return
+}
+
+// getDocCode
+// @Description: 获取单据号码
+// @author: Roc
+// @datetime 2024-01-16 10:29:02
+// @return string
+func getDocCode() string {
+	return fmt.Sprintf("%s-%s", time.Now().Format(utils.FormatDateTimeUnSpace), utils.GetRandStringNoSpecialChar(32))
+}
+
+func HttpPost(urlPath, postData string, params ...string) ([]byte, error) {
+	if global.CONFIG.Xiangyu.UserSyncHost == `` {
+		return nil, errors.New("统一用户同步接口地址为空")
+	}
+	// 请求地址
+	postUrl := global.CONFIG.Xiangyu.UserSyncHost + urlPath
+
+	//body := strings.NewReader(postData)
+	body := io.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", postUrl, 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.SetBasicAuth(global.CONFIG.Xiangyu.UserSyncAuthUserName, global.CONFIG.Xiangyu.UserSyncAuthPwd)
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	result, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	// 日志记录
+	global.FILE_LOG.Debug("统一身份:地址:" + postUrl + ";\n请求参数:" + postData + ";\n返回参数:" + string(result))
+
+	//  解析返回参数,判断是否是json
+	if !json.Valid(result) {
+		err = errors.New("返回参数不是json格式")
+	}
+
+	return result, err
+}

+ 72 - 0
task/task.go

@@ -0,0 +1,72 @@
+package task
+
+import (
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/services/alarm_msg"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"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)
+}

+ 955 - 0
utils/common.go

@@ -0,0 +1,955 @@
+package utils
+
+import (
+	"crypto/md5"
+	"crypto/sha1"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/astaxie/beego/logs"
+	"github.com/dgrijalva/jwt-go"
+	"gorm.io/gorm"
+	"math"
+	"math/rand"
+	"os/exec"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var ErrNoRow = gorm.ErrRecordNotFound
+
+var (
+	KEY = []byte("5Mb5Gdmb5y")
+)
+
+// GenToken 发放token
+func GenToken(account string) string {
+	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_admin",
+		Subject:   account,
+	}
+	ss, err := token.SignedString(KEY)
+	if err != nil {
+		logs.Error(err)
+		return ""
+	}
+	return ss
+}
+
+// 随机数种子
+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)
+}
+
+// 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"
+}
+
+// GetOracleInReplace 获取oracle sql的in查询替换:1的方法
+func GetOracleInReplace(num int) string {
+	template := make([]string, num)
+	for i := 0; i < num; i++ {
+		template[i] = ":1"
+	}
+	return strings.Join(template, ",")
+}
+
+// InArrayByInt php中的in_array(判断Int类型的切片中是否存在该Int值)
+func InArrayByInt(idStrList []int, searchId int) (has bool) {
+	for _, id := range idStrList {
+		if id == searchId {
+			has = true
+			return
+		}
+	}
+	return
+}

+ 102 - 0
utils/constants.go

@@ -0,0 +1,102 @@
+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"          //完整时间格式
+	FormatDateDefault     = "2006-01-02T15:04:05+08:00"
+	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 = "ETA桥接服务"
+)
+
+const (
+	CACHE_CRM_AUTH_CODE_PREFIX = "hz_crm_eta:crm_auth_code_" // 免密登录Code-CRM
+	CACHE_ETA_AUTH_CODE_PREFIX = "hz_crm_eta:eta_auth_code_" // 免密登录Code-ETA
+	CACHE_EDB_SMM_SERVER_TOKEN = "edb:smm_server_token"      //smm调用凭证
+)
+
+// 管理员,ficc管理员,ficc销售,权益管理员,权益销售。
+// 角色类型/类型编码
+const (
+	ROLE_TYPE_ADMIN       = "管理员"
+	ROLE_TYPE_FICC_ADMIN  = "ficc管理员"
+	ROLE_TYPE_FICC_SELLER = "ficc销售"
+	ROLE_TYPE_RAI_ADMIN   = "权益管理员"
+	ROLE_TYPE_RAI_SELLER  = "权益销售"
+
+	ROLE_TYPE_FICC_GROUP      = "ficc销售组长"
+	ROLE_TYPE_FICC_MANAGER    = "ficc销售主管"
+	ROLE_TYPE_RAI_GROUP       = "权益组长"
+	ROLE_TYPE_FICC_DEPARTMENT = "ficc部门经理"
+	ROLE_TYPE_RAI_DEPARTMENT  = "权益部门经理"
+	ROLE_TYPE_FICC_RESEARCHR  = "ficc研究员"
+	ROLE_TYPE_RAI_RESEARCHR   = "权益研究员"
+	ROLE_NAME_FICC_DIRECTOR   = "ficc销售经理" // 实际角色类型为ficc销售主管
+
+	ROLE_TYPE_CODE_ADMIN           = "admin"           //管理员
+	ROLE_TYPE_CODE_FICC_ADMIN      = "ficc_admin"      //ficc管理员
+	ROLE_TYPE_CODE_FICC_SELLER     = "ficc_seller"     //ficc销售
+	ROLE_TYPE_CODE_RAI_ADMIN       = "rai_admin"       //权益管理员
+	ROLE_TYPE_CODE_RAI_SELLER      = "rai_seller"      //权益销售
+	ROLE_TYPE_CODE_FICC_GROUP      = "ficc_group"      //ficc销售主管
+	ROLE_TYPE_CODE_RAI_GROUP       = "rai_group"       //ficc组长
+	ROLE_TYPE_CODE_FICC_DEPARTMENT = "ficc_department" //ficc部门经理
+	ROLE_TYPE_CODE_RAI_DEPARTMENT  = "rai_department"  //权益部门经理
+	ROLE_TYPE_CODE_FICC_RESEARCHR  = "ficc_researcher" //ficc研究员
+	ROLE_TYPE_CODE_RESEARCHR       = "researcher"      //ficc研究员(最早定义的)
+	ROLE_TYPE_CODE_RAI_RESEARCHR   = "rai_researcher"  //权益研究员
+	ROLE_TYPE_CODE_COMPLIANCE      = "compliance"      //合规角色
+	ROLE_TYPE_CODE_FINANCE         = "finance"         //财务角色
+	ROLE_TYPE_CODE_FICC_TEAM       = "ficc_team"       //ficc销售组长
+)
+
+const (
+	COMPANY_PRODUCT_FICC_ID   = 1
+	COMPANY_PRODUCT_FICC_NAME = "ficc"
+	COMPANY_PRODUCT_RAI_ID    = 2
+	COMPANY_PRODUCT_RAI_NAME  = "权益"
+)
+
+// 系统来源
+const (
+	SOURCE_CRM_FLAG = 1
+	SOURCE_ETA_FLAG = 2
+)
+
+// 同步角色和用户的缓存队列key
+const (
+	CACHE_SYNC_ADMIN        = "hz_crm_eta:sync_admin"
+	CACHE_SYNC_ROLE         = "hz_crm_eta:sync_role"
+	CACHE_SYNC_DEPARTMENT   = "hz_crm_eta:sync_department"
+	CACHE_SYNC_GROUP        = "hz_crm_eta:sync_group"
+	CACHE_SYNC_USER_EN_ROLE = "hz_crm_eta:sync_user_en_role" // 同步用户英文权限角色缓存队列key
+)
+
+const CrmEtaAuthorization = "NIi1RbEmH0C2rksXtPGDPBBgRgTZY87Q"
+
+// 自用商户号
+const (
+	BusinessCodeSandbox = "E2023080700" // ETA体验版
+	BusinessCodeRelease = "E2023080900" // 弘则ETA
+)
+
+const CACHE_MYSQL_MASTER_FILENAME = "eta:mysql:binlog:filename"
+const CACHE_MYSQL_MASTER_POSITION = "eta:mysql:binlog:position"

+ 187 - 0
utils/des3.go

@@ -0,0 +1,187 @@
+// 加密工具类,用了3des和base64
+package utils
+
+import (
+	"bytes"
+	"crypto/cipher"
+	"crypto/des"
+	"encoding/base64"
+	"encoding/hex"
+	"errors"
+	"strings"
+)
+
+// des3 + base64 encrypt
+func DesBase64Encrypt(origData []byte, desKey string) []byte {
+	result, err := TripleDesEncrypt(origData, []byte(desKey))
+	if err != nil {
+		panic(err)
+	}
+	return []byte(base64.StdEncoding.EncodeToString(result))
+}
+
+func DesBase64Decrypt(crypted []byte, desKey string) []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(desKey))
+	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
+}

+ 206 - 0
utils/file.go

@@ -0,0 +1,206 @@
+package utils
+
+import (
+	"bufio"
+	"encoding/base64"
+	"fmt"
+	"image"
+	"image/png"
+	"io"
+	"net/http"
+	"os"
+	"path"
+)
+
+// SaveToFile
+// @Description: 将字节内容保存到本地文件
+// @author: Roc
+// @datetime 2024-09-05 18:12:55
+// @param content []byte
+// @param fileName string
+// @return error
+func SaveToFile(content []byte, fileName string) error {
+	file, err := os.Create(fileName)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = file.Close()
+	}()
+
+	_, err = file.Write(content)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// FileIsExist
+// @Description: 判断文件是否存在
+// @author: Roc
+// @datetime 2024-09-05 18:12:49
+// @param filePath string
+// @return bool
+func FileIsExist(filePath string) bool {
+	_, err := os.Stat(filePath)
+	return err == nil || os.IsExist(err)
+}
+
+// GetImgExt
+// @Description: 获取图片扩展名
+// @author: Roc
+// @datetime 2024-09-05 18:12:44
+// @param file string
+// @return ext string
+// @return err error
+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 保存图片
+
+// SaveImage
+// @Description: 保存图片
+// @author: Roc
+// @datetime 2024-09-05 18:09:02
+// @param img image.Image
+// @param path string
+// @return err error
+func SaveImage(img image.Image, path string) (err error) {
+	//需要保持的文件
+	imgFile, err := os.Create(path)
+	defer func() {
+		_ = imgFile.Close()
+	}()
+	// 以PNG格式保存文件
+	err = png.Encode(imgFile, img)
+
+	return err
+}
+
+// DownloadImage
+// @Description: 下载图片
+// @author: Roc
+// @datetime 2024-09-05 18:10:37
+// @param imgUrl string
+// @return filePath string
+// @return err error
+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)
+
+	_, err = io.Copy(writer, reader)
+	//fmt.Printf("Total length: %d \n", written)
+
+	return
+}
+
+// SaveBase64ToFile
+// @Description: 保存base64数据为文件
+// @author: Roc
+// @datetime 2024-09-05 18:11:47
+// @param content string
+// @param path string
+// @return error
+func SaveBase64ToFile(content, path string) error {
+	data, err := base64.StdEncoding.DecodeString(content)
+	if err != nil {
+		return err
+	}
+
+	f, err := os.Create(path)
+	defer func() {
+		_ = f.Close()
+	}()
+	if err != nil {
+		return err
+	}
+	_, 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
+}

+ 415 - 0
utils/mail/imap.go

@@ -0,0 +1,415 @@
+package mail
+
+import (
+	"errors"
+	"eta/eta_email_analysis/global"
+	"eta/eta_email_analysis/utils"
+	"fmt"
+	"github.com/emersion/go-imap"
+	"github.com/emersion/go-imap/client"
+	"github.com/emersion/go-message"
+	"github.com/emersion/go-message/mail"
+	"github.com/h2non/filetype"
+	"golang.org/x/text/encoding/simplifiedchinese"
+	"golang.org/x/text/transform"
+	"io"
+	"log"
+	"path"
+	"strings"
+	"time"
+)
+
+type MailMessage struct {
+	Title      string            `description:"邮件标题"`
+	Content    string            `description:"邮件主体正文"`
+	Resources  map[string]string `description:"正文内嵌资源"`
+	Attachment map[string]string `description:"附件资源"`
+}
+
+func ListenMail(mailAddress, folder, userName, password string, readBatchSize int, mailMessageChan chan MailMessage, mailMessageDoneChan chan bool) (err error) { // 收件箱
+	defer func() {
+		if err != nil {
+			fmt.Println("err:", err.Error())
+		}
+	}()
+	// 建立与 IMAP 服务器的连接
+	c, err := client.DialTLS(mailAddress, nil)
+	if err != nil {
+		fmt.Printf("连接 IMAP 服务器失败: %+v \n", err)
+		return
+	}
+	// 最后一定不要忘记退出登录
+	defer func() {
+		_ = c.Logout()
+	}()
+
+	// 登录
+	if err = c.Login(userName, password); err != nil {
+		fmt.Printf("邮箱[%s] 登录失败: %v \n", fmt.Sprintf("%s:%s", userName, mailAddress), err)
+		return
+	}
+	// 列出当前邮箱中的文件夹
+	mailboxes := make(chan *imap.MailboxInfo, 10)
+	done := make(chan error, 1) // 记录错误的 chan
+	go func() {
+		done <- c.List("", "*", mailboxes)
+	}()
+	log.Println("-->当前邮箱的文件夹 Mailboxes:")
+
+	var folderExists bool
+	for m := range mailboxes {
+		log.Println("* ", m.Name)
+		if m.Name == folder {
+			folderExists = true
+		}
+	}
+
+	if err := <-done; err != nil {
+		log.Fatalf("列出邮箱列表时,出现错误:%v \n", err)
+	}
+	log.Println("-->列出邮箱列表完毕!")
+	if !folderExists {
+		err = errors.New(fmt.Sprintf("文件夹[%s] 不存在 \n", folder))
+		return
+	}
+
+	// 选择指定的文件夹
+	mbox, err := c.Select(folder, false)
+	if err != nil {
+		err = errors.New(fmt.Sprintf("选择邮件箱失败: %+v", err))
+		return
+	}
+	//log.Printf("mbox %+v \n", mbox)
+	log.Printf("当前文件夹[%s]中,总共有 %d 封邮件 \n", folder, mbox.Messages)
+	if mbox.Messages == 0 {
+		//log.Fatalf("当前文件夹[%s]中没有邮件", folder)
+		return
+	}
+
+	// 创建一个序列集,用于批量读取邮件
+	seqSet := new(imap.SeqSet)
+
+	// 假设需要获取最后4封邮件时
+	from := uint32(1)
+	to := mbox.Messages // 此文件下的邮件总数
+
+	if to > 13 {
+		from = to - 13
+	}
+	step := uint32(2)
+	for i := from; i <= to; {
+		end := i + step - 1
+		if end > to {
+			end = to
+		}
+
+		seqSet.Clear()
+		seqSet.AddRange(i, end) // 添加指定范围内的邮件编号
+
+		// 获取整个消息正文
+		// imap.FetchEnvelope:请求获取邮件的信封数据(例如发件人、收件人、主题等元数据)。
+		// imap.FetchRFC822:请求获取完整的邮件内容,包括所有头部和正文。
+		items := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchRFC822}
+
+		// 获取邮件内容 Start
+		messages := make(chan *imap.Message, readBatchSize) // 创建一个通道,用于接收邮件消息
+		fetchDone := make(chan error, 1)                    // 创建一个通道,用于接收错误消息
+		go func() {
+			// Fetch方法用于从服务器获取邮件数据,这里请求了邮件的信封和完整内容
+			fetchDone <- c.Fetch(seqSet, items, messages)
+		}()
+
+		if err := <-fetchDone; err != nil {
+			log.Fatalf("获取邮件信息出现错误:%v \n", err)
+		}
+		// 获取邮件内容 End
+
+		log.Println("开始读取邮件内容")
+		for msg := range messages {
+			mailMessage, tmpErr := readEveryMsg(msg)
+			if tmpErr != nil {
+				global.FILE_LOG.Fatalf("读取邮件内容时出现错误:%v \n", tmpErr)
+				continue
+			}
+			// 写入邮件处理chan
+			mailMessageChan <- mailMessage
+		}
+
+		//time.Sleep(time.Second * 5) // 休眠10秒
+
+		i = i + step
+	}
+
+	log.Println("读取了所有邮件,完毕!")
+	mailMessageDoneChan <- true
+
+	return
+}
+
+//func ListenMail(mailAddress, folder, userName, password string, readBatchSize int, mailMessageChan chan MailMessage, mailMessageDoneChan chan bool) (err error) { // 收件箱
+//	defer func() {
+//		if err != nil {
+//			fmt.Println("err:", err.Error())
+//		}
+//	}()
+//	// 建立与 IMAP 服务器的连接
+//	c, err := client.DialTLS(mailAddress, nil)
+//	if err != nil {
+//		fmt.Printf("连接 IMAP 服务器失败: %+v \n", err)
+//		return
+//	}
+//	// 最后一定不要忘记退出登录
+//	defer func() {
+//		_ = c.Logout()
+//	}()
+//
+//	// 登录
+//	if err = c.Login(userName, password); err != nil {
+//		fmt.Printf("邮箱[%s] 登录失败: %v \n", fmt.Sprintf("%s:%s", userName, mailAddress), err)
+//		return
+//	}
+//	// 列出当前邮箱中的文件夹
+//	mailboxes := make(chan *imap.MailboxInfo, 10)
+//	done := make(chan error, 1) // 记录错误的 chan
+//	go func() {
+//		done <- c.List("", "*", mailboxes)
+//	}()
+//	log.Println("-->当前邮箱的文件夹 Mailboxes:")
+//
+//	var folderExists bool
+//	for m := range mailboxes {
+//		log.Println("* ", m.Name)
+//		if m.Name == folder {
+//			folderExists = true
+//		}
+//	}
+//
+//	if err := <-done; err != nil {
+//		log.Fatalf("列出邮箱列表时,出现错误:%v \n", err)
+//	}
+//	log.Println("-->列出邮箱列表完毕!")
+//	if !folderExists {
+//		err = errors.New(fmt.Sprintf("文件夹[%s] 不存在 \n", folder))
+//		return
+//	}
+//
+//	// 选择指定的文件夹
+//	mbox, err := c.Select(folder, false)
+//	if err != nil {
+//		err = errors.New(fmt.Sprintf("选择邮件箱失败: %+v", err))
+//		return
+//	}
+//	//log.Printf("mbox %+v \n", mbox)
+//	log.Printf("当前文件夹[%s]中,总共有 %d 封邮件 \n", folder, mbox.Messages)
+//	if mbox.Messages == 0 {
+//		//log.Fatalf("当前文件夹[%s]中没有邮件", folder)
+//		return
+//	}
+//
+//	// 创建一个序列集,用于批量读取邮件
+//	seqSet := new(imap.SeqSet)
+//
+//	// 假设需要获取最后4封邮件时
+//	from := uint32(1)
+//	to := mbox.Messages // 此文件下的邮件总数
+//	if mbox.Messages > 2 {
+//		from = mbox.Messages - 1
+//	}
+//	//from = mbox.Messages - 9
+//	//to = mbox.Messages - 9
+//	from = mbox.Messages
+//	to = mbox.Messages
+//	seqSet.AddRange(from, to) // 添加指定范围内的邮件编号
+//
+//
+//
+//	// 获取整个消息正文
+//	// imap.FetchEnvelope:请求获取邮件的信封数据(例如发件人、收件人、主题等元数据)。
+//	// imap.FetchRFC822:请求获取完整的邮件内容,包括所有头部和正文。
+//	items := []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchRFC822}
+//
+//	// 获取邮件内容 Start
+//	messages := make(chan *imap.Message, readBatchSize) // 创建一个通道,用于接收邮件消息
+//	fetchDone := make(chan error, 1)                    // 创建一个通道,用于接收错误消息
+//	go func() {
+//		// Fetch方法用于从服务器获取邮件数据,这里请求了邮件的信封和完整内容
+//		fetchDone <- c.Fetch(seqSet, items, messages)
+//	}()
+//
+//	if err := <-fetchDone; err != nil {
+//		log.Fatalf("获取邮件信息出现错误:%v \n", err)
+//	}
+//	// 获取邮件内容 End
+//
+//	log.Println("开始读取邮件内容")
+//	for msg := range messages {
+//		mailMessage, tmpErr := readEveryMsg(msg)
+//		if tmpErr != nil {
+//			global.FILE_LOG.Fatalf("读取邮件内容时出现错误:%v \n", tmpErr)
+//			continue
+//		}
+//		// 写入邮件处理chan
+//		mailMessageChan <- mailMessage
+//	}
+//
+//	time.Sleep(time.Second * 5) // 休眠10秒
+//
+//	log.Println("读取了所有邮件,完毕!")
+//
+//	return
+//}
+
+// 定义一个自定义的 CharsetReader 函数,它能够处理 gb2312 和 gbk 字符集
+func myCharsetReader(charset string, input io.Reader) (io.Reader, error) {
+
+	switch strings.ToLower(charset) {
+	case "gb2312", "gbk":
+		reader := transform.NewReader(input, simplifiedchinese.GBK.NewDecoder())
+		return reader, nil
+	case "utf-8":
+		return input, nil
+	default:
+		return input, fmt.Errorf("unsupported charset: %s", charset)
+	}
+}
+
+// document link: https://github.com/emersion/go-imap/wiki/Fetching-messages
+func readEveryMsg(msg *imap.Message) (mailMessage MailMessage, err error) {
+	message.CharsetReader = myCharsetReader
+	mailMessage.Resources = make(map[string]string)  // 内嵌资源
+	mailMessage.Attachment = make(map[string]string) // 附件
+
+	htmlStr := ``
+	textStr := ``
+	rootPath := `C:\Users\123\go\src\eta\eta_email_analysis\static\`
+
+	//log.Printf("当前邮件的消息序列号 %+v \n", msg.SeqNum)
+	//log.Println("-------------------------")
+	// 获取邮件正文
+	r := msg.GetBody(&imap.BodySectionName{})
+	if r == nil {
+		log.Fatal("服务器没有返回消息内容")
+	}
+
+	mr, err := mail.CreateReader(r)
+	if err != nil {
+		//log.Fatalf("邮件读取时出现错误: %v \n", err)
+		err = errors.New(fmt.Sprintf("邮件读取时出现错误:%v \n", err))
+		return
+	}
+	//if date, err := mr.Header.Date(); err == nil {
+	//	log.Println("收件时间 Date:", date)
+	//}
+	//if from, err := mr.Header.AddressList("From"); err == nil {
+	//	log.Println("发件人 From:", from)
+	//}
+	//if to, err := mr.Header.AddressList("To"); err == nil {
+	//	log.Println("收件人 To:", to)
+	//}
+	//log.Printf("抄送 Cc: %+v \n", msg.Envelope.Cc)
+
+	// 邮件标题
+	subject, err := mr.Header.Subject()
+	if err != nil {
+		log.Println("邮件主题 Subject ERR:", err)
+	}
+	log.Println("邮件主题 Subject:", subject)
+	mailMessage.Title = subject
+
+	for {
+		p, tmpErr := mr.NextPart()
+		if tmpErr == io.EOF {
+			break
+		} else if tmpErr != nil {
+			//log.Fatalf("读取邮件内容时出现错误:%v \n", tmpErr)
+			err = tmpErr
+			return
+		}
+
+		bodyBytes, _ := io.ReadAll(p.Body)
+		if err != nil {
+			//log.Fatalf("读取邮件部分时出现错误:%v \n", err)
+			err = errors.New(fmt.Sprintf("读取邮件部分时出现错误:%v \n", err))
+			return
+		}
+
+		switch h := p.Header.(type) {
+		case *mail.InlineHeader:
+			// 这是消息的文本(可以是纯文本或 HTML)
+			contentType := h.Get("Content-Type")
+			//log.Println("消息内容content-type:", contentType)
+			if strings.HasPrefix(contentType, "text/plain") {
+				//log.Printf("得到正文 -> TEXT: %v \n", string(bodyBytes))
+				textStr += string(bodyBytes)
+			} else if strings.HasPrefix(contentType, "text/html") {
+				//log.Printf("得到正文 -> HTML: %v \n", len(b))
+				//log.Printf("得到正文 -> HTML: %v \n", string(bodyBytes))
+				htmlStr += string(bodyBytes)
+			}
+			// 这是内嵌资源
+			if cid := p.Header.Get("Content-ID"); cid != "" {
+
+				// 确定文件后缀
+				fileSuffix := determineFileSuffix(bodyBytes)
+				fileName := fmt.Sprintf("%s%s.%s", rootPath, cid[1:len(cid)-1], fileSuffix)
+
+				err = utils.SaveToFile(bodyBytes, fileName)
+				if err != nil {
+					//log.Fatalf("保存文件时出现错误:%v \n", err)
+					err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
+					return
+				}
+				mailMessage.Resources[cid] = fileName
+			}
+			break
+		case *mail.AttachmentHeader:
+			// 这是一个附件
+			filename, _ := h.Filename()
+			//log.Printf("得到附件: %v,content-type:%s \n", filename, p.Header.Get("Content-Type"))
+			saveName := fmt.Sprint(msg.SeqNum, utils.MD5(filename), time.Now().Format(utils.FormatDateTimeUnSpace), time.Now().Nanosecond(), path.Ext(filename))
+			filePath := fmt.Sprintf("%s%s%s", rootPath, `file\`, saveName)
+			err = utils.SaveToFile(bodyBytes, filePath)
+			if err != nil {
+				//log.Fatalf("保存文件时出现错误:%v \n", err)
+				err = errors.New(fmt.Sprintf("保存文件时出现错误:%v \n", err))
+				return
+			}
+
+			// 这是附件资源
+			if contentDisposition := p.Header.Get("Content-Disposition"); contentDisposition != "" {
+				if strings.HasPrefix(contentDisposition, "attachment") {
+					mailMessage.Attachment[filename] = filePath
+				}
+			} else if cid := p.Header.Get("Content-ID"); cid != "" {
+				// 这是内嵌资源
+				mailMessage.Resources[cid] = filePath
+			}
+			//else {
+			//	mailMessage.Attachment[filename] = filePath
+			//}
+
+			break
+		default:
+			log.Println(h)
+		}
+	}
+	mailMessage.Content = htmlStr
+	if mailMessage.Content == `` {
+		mailMessage.Content = textStr
+	}
+
+	//log.Println("一封邮件读取完毕")
+	//log.Printf("------------------------- \n\n")
+
+	return
+}
+
+// 根据文件内容确定文件后缀
+func determineFileSuffix(content []byte) string {
+	kind, err := filetype.Match(content)
+	if err != nil {
+		log.Printf("无法确定文件类型:%v \n", err)
+		return ".bin"
+	}
+	return kind.Extension
+}

+ 41 - 0
utils/redis.go

@@ -0,0 +1,41 @@
+package utils
+
+import (
+	"eta/eta_email_analysis/utils/redis"
+	"time"
+)
+
+type RedisClient interface {
+	Get(key string) interface{}
+	GetStr(key string) string
+	GetInt64(key string) (int64, error)
+	GetUInt64(key string) (uint64, error)
+	RedisBytes(key string) (data []byte, err error)
+	RedisString(key string) (data string, err error)
+	RedisInt(key string) (data int, err error)
+	Put(key string, val interface{}, timeout time.Duration) error
+	SetNX(key string, val interface{}, timeout time.Duration) bool
+	Delete(key string) error
+	IsExist(key string) bool
+	LPush(key string, val interface{}) error
+	Brpop(key string, callback func([]byte))
+	GetRedisTTL(key string) time.Duration
+	Incrby(key string, num int) (interface{}, error)
+	Do(commandName string, args ...interface{}) (reply interface{}, err error)
+}
+
+func InitRedis(redisType string, conf string) (redisClient RedisClient, err error) {
+	switch redisType {
+	case "cluster": // 集群
+		redisClient, err = redis.InitClusterRedis(conf)
+	default: // 默认走单机
+		redisClient, err = redis.InitStandaloneRedis(conf)
+	}
+
+	return
+}
+
+// redis没有key的错误
+const RedisNoKeyErr = "redis: nil"
+
+//const redisNoKeyErr = redis2.Error(redis2.Nil)

+ 294 - 0
utils/redis/cluster_redis.go

@@ -0,0 +1,294 @@
+package redis
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/go-redis/redis/v8"
+	"strings"
+	"time"
+)
+
+// ClusterRedisClient
+// @Description: 集群的redis客户端
+type ClusterRedisClient struct {
+	redisClient *redis.ClusterClient
+}
+
+var DefaultKey = "zcmRedis"
+
+// InitClusterRedis
+// @Description: 初始化集群redis客户端
+// @param config
+// @return clusterRedisClient
+// @return err
+func InitClusterRedis(config string) (clusterRedisClient *ClusterRedisClient, err error) {
+	var cf map[string]string
+	err = json.Unmarshal([]byte(config), &cf)
+	if err != nil {
+		return
+	}
+	//if _, ok := cf["key"]; !ok {
+	//	cf["key"] = DefaultKey
+	//}
+
+	// 集群地址
+	connList := make([]string, 0)
+	if _, ok := cf["conn"]; !ok {
+		err = errors.New("config has no conn key")
+		return
+	} else {
+		connList = strings.Split(cf["conn"], ",")
+		if len(connList) <= 1 {
+			err = errors.New("conn address less than or equal to 1")
+			return
+		}
+	}
+
+	// 密码
+	if _, ok := cf["password"]; !ok {
+		cf["password"] = ""
+	}
+
+	// 创建 Redis 客户端配置对象
+	clusterOptions := &redis.ClusterOptions{
+		Addrs:    connList, // 设置 Redis 节点的 IP 地址和端口号
+		Password: cf["password"],
+	}
+
+	// 创建 Redis 集群客户端
+	client := redis.NewClusterClient(clusterOptions)
+
+	// 测试连接并获取信息
+	_, err = client.Ping(context.TODO()).Result()
+	if err != nil {
+		err = errors.New("redis 链接失败:" + err.Error())
+		return
+	}
+
+	clusterRedisClient = &ClusterRedisClient{redisClient: client}
+
+	return
+}
+
+// Get
+// @Description: 根据key获取数据(其实是返回的字节编码)
+// @receiver rc
+// @param key
+// @return interface{}
+func (rc *ClusterRedisClient) Get(key string) interface{} {
+	data, err := rc.redisClient.Get(context.TODO(), key).Bytes()
+	if err != nil {
+		return nil
+	}
+
+	return data
+}
+
+// GetStr
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return string
+func (rc *ClusterRedisClient) GetStr(key string) string {
+	return rc.redisClient.Get(context.TODO(), key).Val()
+}
+
+// GetInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *ClusterRedisClient) GetInt64(key string) (int64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Int64()
+}
+
+// GetInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *ClusterRedisClient) GetUInt64(key string) (uint64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Uint64()
+}
+
+// RedisBytes
+// @Description: 根据key获取字节编码数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *ClusterRedisClient) RedisBytes(key string) (data []byte, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Bytes()
+
+	return
+}
+
+// RedisString
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *ClusterRedisClient) RedisString(key string) (data string, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Result()
+
+	return
+}
+
+// RedisInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *ClusterRedisClient) RedisInt(key string) (data int, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Int()
+
+	return
+}
+
+// Put
+// @Description: put一个数据到redis
+// @receiver rc
+// @param key
+// @param val
+// @param timeout
+// @return error
+func (rc *ClusterRedisClient) Put(key string, val interface{}, timeout time.Duration) error {
+	var err error
+	err = rc.redisClient.SetEX(context.TODO(), key, val, timeout).Err()
+	if err != nil {
+		return err
+	}
+
+	err = rc.redisClient.HSet(context.TODO(), DefaultKey, key, true).Err()
+
+	return err
+}
+
+// SetNX
+// @Description: 设置一个会过期时间的值
+// @receiver rc
+// @param key
+// @param val
+// @param timeout
+// @return bool
+func (rc *ClusterRedisClient) SetNX(key string, val interface{}, timeout time.Duration) bool {
+	result, err := rc.redisClient.SetEX(context.TODO(), key, val, timeout).Result()
+	if err != nil || result != "OK" {
+		return false
+	}
+
+	return true
+}
+
+// Delete
+// @Description: 删除redis中的键值对
+// @receiver rc
+// @param key
+// @return error
+func (rc *ClusterRedisClient) Delete(key string) error {
+	var err error
+
+	err = rc.redisClient.Del(context.TODO(), key).Err()
+	if err != nil {
+		return err
+	}
+
+	err = rc.redisClient.HDel(context.TODO(), DefaultKey, key).Err()
+
+	return err
+}
+
+// IsExist
+// @Description: 根据key判断是否写入缓存中
+// @receiver rc
+// @param key
+// @return bool
+func (rc *ClusterRedisClient) IsExist(key string) bool {
+	result, err := rc.redisClient.Exists(context.TODO(), key).Result()
+	if err != nil {
+		return false
+	}
+	if result == 0 {
+		_ = rc.redisClient.HDel(context.TODO(), DefaultKey, key).Err()
+		return false
+	}
+
+	return true
+}
+
+// LPush
+// @Description: 写入list
+// @receiver rc
+// @param key
+// @param val
+// @return error
+func (rc *ClusterRedisClient) LPush(key string, val interface{}) error {
+	data, _ := json.Marshal(val)
+	err := rc.redisClient.LPush(context.TODO(), key, data).Err()
+
+	return err
+}
+
+// Brpop
+// @Description: 从list中读取
+// @receiver rc
+// @param key
+// @param callback
+func (rc *ClusterRedisClient) Brpop(key string, callback func([]byte)) {
+	values, err := rc.redisClient.BRPop(context.TODO(), 1*time.Second, key).Result()
+	if err != nil {
+		return
+	}
+	if len(values) < 2 {
+		fmt.Println("assert is wrong")
+		return
+	}
+
+	callback([]byte(values[1]))
+
+}
+
+// GetRedisTTL
+// @Description: 获取key的过期时间
+// @receiver rc
+// @param key
+// @return time.Duration
+func (rc *ClusterRedisClient) GetRedisTTL(key string) time.Duration {
+	value, err := rc.redisClient.TTL(context.TODO(), key).Result()
+	if err != nil {
+		return 0
+	}
+
+	return value
+
+}
+
+// Incrby
+// @Description: 设置自增值
+// @receiver rc
+// @param key
+// @param num
+// @return interface{}
+// @return error
+func (rc *ClusterRedisClient) Incrby(key string, num int) (interface{}, error) {
+	return rc.redisClient.IncrBy(context.TODO(), key, int64(num)).Result()
+}
+
+// Do
+// @Description: cmd执行redis命令
+// @receiver rc
+// @param commandName
+// @param args
+// @return reply
+// @return err
+func (rc *ClusterRedisClient) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	newArgs := []interface{}{commandName}
+	newArgs = append(newArgs, args...)
+	return rc.redisClient.Do(context.TODO(), newArgs...).Result()
+}

+ 286 - 0
utils/redis/standalone_redis.go

@@ -0,0 +1,286 @@
+package redis
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/go-redis/redis/v8"
+	"strconv"
+	"time"
+)
+
+// StandaloneRedisClient
+// @Description: 单机redis客户端
+type StandaloneRedisClient struct {
+	redisClient *redis.Client
+}
+
+func InitStandaloneRedis(config string) (standaloneRedis *StandaloneRedisClient, err error) {
+	var cf map[string]string
+	err = json.Unmarshal([]byte(config), &cf)
+	if err != nil {
+		return
+	}
+	//if _, ok := cf["key"]; !ok {
+	//	cf["key"] = DefaultKey
+	//}
+
+	if _, ok := cf["conn"]; !ok {
+		err = errors.New("config has no conn key")
+		return
+	}
+
+	// db库
+	dbNum := 0
+	// 如果指定了db库
+	if _, ok := cf["dbNum"]; ok {
+		dbNum, err = strconv.Atoi(cf["dbNum"])
+		if err != nil {
+			return
+		}
+	}
+
+	// 密码
+	if _, ok := cf["password"]; !ok {
+		cf["password"] = ""
+	}
+
+	client := redis.NewClient(&redis.Options{
+		Addr:     cf["conn"],
+		Password: cf["password"],
+		DB:       dbNum,
+		//PoolSize: 10, //连接池最大socket连接数,默认为10倍CPU数, 10 * runtime.NumCPU(暂不配置)
+	})
+
+	_, err = client.Ping(context.TODO()).Result()
+	if err != nil {
+		err = errors.New("redis 链接失败:" + err.Error())
+		return
+	}
+
+	standaloneRedis = &StandaloneRedisClient{redisClient: client}
+
+	return
+}
+
+// Get
+// @Description: 根据key获取数据(其实是返回的字节编码)
+// @receiver rc
+// @param key
+// @return interface{}
+func (rc *StandaloneRedisClient) Get(key string) interface{} {
+	data, err := rc.redisClient.Get(context.TODO(), key).Bytes()
+	if err != nil {
+		return nil
+	}
+
+	return data
+}
+
+// GetStr
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return string
+func (rc *StandaloneRedisClient) GetStr(key string) string {
+	return rc.redisClient.Get(context.TODO(), key).Val()
+}
+
+// GetInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *StandaloneRedisClient) GetInt64(key string) (int64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Int64()
+}
+
+// GetInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return int
+// @return error
+func (rc *StandaloneRedisClient) GetUInt64(key string) (uint64, error) {
+	return rc.redisClient.Get(context.TODO(), key).Uint64()
+}
+
+// RedisBytes
+// @Description: 根据key获取字节编码数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *StandaloneRedisClient) RedisBytes(key string) (data []byte, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Bytes()
+
+	return
+}
+
+// RedisString
+// @Description: 根据key获取字符串数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *StandaloneRedisClient) RedisString(key string) (data string, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Result()
+
+	return
+}
+
+// RedisInt
+// @Description: 根据key获取int数据
+// @receiver rc
+// @param key
+// @return data
+// @return err
+func (rc *StandaloneRedisClient) RedisInt(key string) (data int, err error) {
+	data, err = rc.redisClient.Get(context.TODO(), key).Int()
+
+	return
+}
+
+// Put
+// @Description: put一个数据到redis
+// @receiver rc
+// @param key
+// @param val
+// @param timeout
+// @return error
+func (rc *StandaloneRedisClient) Put(key string, val interface{}, timeout time.Duration) error {
+	var err error
+	err = rc.redisClient.SetEX(context.TODO(), key, val, timeout).Err()
+	if err != nil {
+		return err
+	}
+
+	err = rc.redisClient.HSet(context.TODO(), DefaultKey, key, true).Err()
+
+	return err
+}
+
+// SetNX
+// @Description: 设置一个会过期时间的值
+// @receiver rc
+// @param key
+// @param val
+// @param timeout
+// @return bool
+func (rc *StandaloneRedisClient) SetNX(key string, val interface{}, timeout time.Duration) bool {
+	result, err := rc.redisClient.SetEX(context.TODO(), key, val, timeout).Result()
+	if err != nil || result != "OK" {
+		return false
+	}
+
+	return true
+}
+
+// Delete
+// @Description: 删除redis中的键值对
+// @receiver rc
+// @param key
+// @return error
+func (rc *StandaloneRedisClient) Delete(key string) error {
+	var err error
+
+	err = rc.redisClient.Del(context.TODO(), key).Err()
+	if err != nil {
+		return err
+	}
+
+	err = rc.redisClient.HDel(context.TODO(), DefaultKey, key).Err()
+
+	return err
+}
+
+// IsExist
+// @Description: 根据key判断是否写入缓存中
+// @receiver rc
+// @param key
+// @return bool
+func (rc *StandaloneRedisClient) IsExist(key string) bool {
+	result, err := rc.redisClient.Exists(context.TODO(), key).Result()
+	if err != nil {
+		return false
+	}
+	if result == 0 {
+		_ = rc.redisClient.HDel(context.TODO(), DefaultKey, key).Err()
+		return false
+	}
+
+	return true
+}
+
+// LPush
+// @Description: 写入list
+// @receiver rc
+// @param key
+// @param val
+// @return error
+func (rc *StandaloneRedisClient) LPush(key string, val interface{}) error {
+	data, _ := json.Marshal(val)
+	err := rc.redisClient.LPush(context.TODO(), key, data).Err()
+
+	return err
+}
+
+// Brpop
+// @Description: 从list中读取
+// @receiver rc
+// @param key
+// @param callback
+func (rc *StandaloneRedisClient) Brpop(key string, callback func([]byte)) {
+	values, err := rc.redisClient.BRPop(context.TODO(), 1*time.Second, key).Result()
+	if err != nil {
+		return
+	}
+	if len(values) < 2 {
+		fmt.Println("assert is wrong")
+		return
+	}
+
+	callback([]byte(values[1]))
+
+}
+
+// GetRedisTTL
+// @Description: 获取key的过期时间
+// @receiver rc
+// @param key
+// @return time.Duration
+func (rc *StandaloneRedisClient) GetRedisTTL(key string) time.Duration {
+	value, err := rc.redisClient.TTL(context.TODO(), key).Result()
+	if err != nil {
+		return 0
+	}
+
+	return value
+
+}
+
+// Incrby
+// @Description: 设置自增值
+// @receiver rc
+// @param key
+// @param num
+// @return interface{}
+// @return error
+func (rc *StandaloneRedisClient) Incrby(key string, num int) (interface{}, error) {
+	return rc.redisClient.IncrBy(context.TODO(), key, int64(num)).Result()
+}
+
+// Do
+// @Description: cmd执行redis命令
+// @receiver rc
+// @param commandName
+// @param args
+// @return reply
+// @return err
+func (rc *StandaloneRedisClient) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
+	newArgs := []interface{}{commandName}
+	newArgs = append(newArgs, args...)
+	return rc.redisClient.Do(context.TODO(), newArgs...).Result()
+}

+ 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
+}