hsun 1 年之前
當前提交
6dfa2938b3

+ 11 - 0
.gitignore

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

+ 54 - 0
config/config.go

@@ -0,0 +1,54 @@
+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"`
+}
+
+// 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:"上传的文件存储目录地址"`
+}
+
+// Log 日志配置
+type Log struct {
+	Prefix         string `mapstructure:"prefix" json:"prefix" yaml:"prefix" description:"日志输出前缀"`
+	LogFile        bool   `mapstructure:"log-file" json:"logFile" yaml:"log-file" description:""`
+	Stdout         string `mapstructure:"stdout" json:"stdout" yaml:"stdout" description:""`
+	FileStdout     string `mapstructure:"file-stdout" json:"file-stdout" yaml:"file-stdout" description:""`
+	SaveMaxDay     int    `mapstructure:"save-max-day" json:"save-max-day" yaml:"save-max-day" description:"最多保留多少天的日志"`
+	CuttingDay     int    `mapstructure:"cutting-day" json:"cutting-day" yaml:"cutting-day" description:"相隔几天切割文件"`
+	LogDirPath     string `mapstructure:"log-dir-path" json:"log-dir-path" yaml:"log-dir-path" description:"日志目录"`
+	LogSoftLink    string `mapstructure:"log-soft-link" json:"log-soft-link" yaml:"log-soft-link" description:"日志软链接"`
+	BinlogDirPath  string `mapstructure:"binlog-dir-path" json:"binlog-dir-path" yaml:"binlog-dir-path" description:"binlog日志目录"`
+	BinlogSoftLink string `mapstructure:"binlog-soft-link" json:"binlog-soft-link" yaml:"binlog-soft-link" description:"binlog日志软链接"`
+}
+
+// Mysql 数据库配置
+type Mysql struct {
+	//LogMode             bool        `mapstructure:"log-mode" json:"log-mode" yaml:"log-mode" description:"是否开启日志"`
+	Stdout              bool        `mapstructure:"stdout" json:"stdout" yaml:"stdout" description:"日志是否输出在控制台"`
+	DefaultDsnAliasName string      `mapstructure:"default-dsn-alias-name" json:"default-dsn-alias-name" yaml:"default-dsn-alias-name" description:"默认的数据库连接别名"`
+	List                []MysqlConn `mapstructure:"list" json:"list" yaml:"list" description:"数据库链接配置列表"`
+}
+
+// MysqlConn mysql数据链接配置
+type MysqlConn struct {
+	Dsn          string `mapstructure:"dsn" json:"dsc" yaml:"dsn" description:"数据库连接配置"`
+	AliasName    string `mapstructure:"alias-name" json:"alias-name" yaml:"alias-name" description:"数据库别名"`
+	MaxIdleConns int    `mapstructure:"max-idle-conns" json:"max-idle-conns" yaml:"max-idle-conns" description:"最大空闲连接"`
+	MaxOpenConns int    `mapstructure:"max-open-conns" json:"max-open-conns" yaml:"max-open-conns" description:"最大连接数"`
+}
+
+// Redis redis配置
+type Redis struct {
+	Address  string `mapstructure:"address" json:"address" yaml:"address" description:"redis服务链接地址"`
+	Password string `mapstructure:"password" json:"password" yaml:"password" description:"redis服务密码"`
+	Db       int    `mapstructure:"db" json:"db" yaml:"db" description:"默认使用的redis库"`
+}

+ 280 - 0
controller/auth.go

@@ -0,0 +1,280 @@
+package controller
+
+import (
+	"encoding/base64"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+	"hongze/hz_crm_eta/controller/resp"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/models/crm"
+	"hongze/hz_crm_eta/models/eta"
+	"hongze/hz_crm_eta/models/rddp"
+	"hongze/hz_crm_eta/models/request"
+	"hongze/hz_crm_eta/models/response"
+	"hongze/hz_crm_eta/services"
+	"hongze/hz_crm_eta/utils"
+	"time"
+)
+
+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_CRM_AUTH_CODE_PREFIX, utils.SOURCE_ETA_FLAG: utils.CACHE_ETA_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
+}
+
+// GetEtaToken
+// @Description 换取ETA系统Token
+// @Success 200 {string} string "操作成功"
+// @Router /auth/eta_token [post]
+func (a *AuthController) GetEtaToken(c *gin.Context) {
+	var req request.TokenLoginReq
+	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
+	}
+
+	key := fmt.Sprint(utils.CACHE_CRM_AUTH_CODE_PREFIX, req.AuthCode)
+	adminName, e := global.Rc.RedisString(key)
+	if e != nil {
+		resp.FailMsg("获取失败", "获取失败, Redis Err: "+e.Error(), c)
+		return
+	}
+	if adminName == "" {
+		resp.Fail("获取失败, 无效编码", c)
+		return
+	}
+	// 清除AuthCode
+	defer func() {
+		_ = global.Rc.Delete(key)
+	}()
+
+	// 获取用户信息
+	sysUser, e := eta.GetSysUserByAdminName(adminName)
+	if e != nil {
+		if e == utils.ErrNoRow {
+			resp.Fail("用户不存在", c)
+			return
+		}
+		resp.FailMsg("获取失败", "获取用户信息失败, err: "+e.Error(), c)
+		return
+	}
+
+	account := utils.MD5(adminName)
+	token := utils.GenToken(account)
+	sysSession := new(eta.SysSession)
+	sysSession.UserName = adminName
+	sysSession.SysUserId = sysUser.AdminId
+	sysSession.ExpiredTime = time.Now().AddDate(0, 0, 90)
+	sysSession.IsRemember = 1
+	sysSession.CreatedTime = time.Now()
+	sysSession.LastUpdatedTime = time.Now()
+	sysSession.AccessToken = token
+	if e := eta.AddSysSession(sysSession); e != nil {
+		resp.FailMsg("获取失败", "新增session失败, err: "+e.Error(), c)
+		return
+	}
+
+	login := new(response.LoginResp)
+	login.Authorization = token
+	login.Authorization = "authorization=" + token + "$account=" + account
+	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 := services.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]
+
+	// 新增登录记录
+	{
+		record := new(eta.SysUserLoginRecord)
+		record.Uid = sysUser.AdminId
+		record.UserName = adminName
+		record.Ip = c.RemoteIP()
+		record.Stage = "login"
+		record.CreateTime = time.Now()
+		go eta.AddSysUserLoginRecord(record)
+	}
+
+	resp.OkData("获取成功", login, c)
+}
+
+// GetCrmToken
+// @Description 换取CRM系统Token
+// @Success 200 {string} string "操作成功"
+// @Router /auth/crm_token [post]
+func (a *AuthController) GetCrmToken(c *gin.Context) {
+	var req request.TokenLoginReq
+	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
+	}
+
+	key := fmt.Sprint(utils.CACHE_ETA_AUTH_CODE_PREFIX, req.AuthCode)
+	adminName, e := global.Rc.RedisString(key)
+	if e != nil {
+		resp.FailMsg("获取失败", "获取失败, Redis Err: "+e.Error(), c)
+		return
+	}
+	if adminName == "" {
+		resp.Fail("获取失败, 无效编码", c)
+		return
+	}
+	// 清除AuthCode
+	defer func() {
+		_ = global.Rc.Delete(key)
+	}()
+
+	// 获取用户信息
+	sysUser, e := crm.GetSysUserByAdminName(adminName)
+	if e != nil {
+		if e == utils.ErrNoRow {
+			resp.Fail("用户不存在", c)
+			return
+		}
+		resp.FailMsg("获取失败", "获取用户信息失败, err: "+e.Error(), c)
+		return
+	}
+
+	account := utils.MD5(adminName)
+	token := utils.GenToken(account)
+	sysSession := new(rddp.SysSession)
+	sysSession.UserName = adminName
+	sysSession.SysUserId = sysUser.AdminId
+	sysSession.ExpiredTime = time.Now().AddDate(0, 0, 90)
+	sysSession.IsRemember = 1
+	sysSession.CreatedTime = time.Now()
+	sysSession.LastUpdatedTime = time.Now()
+	sysSession.AccessToken = token
+	if e := rddp.AddSysSession(sysSession); e != nil {
+		resp.FailMsg("获取失败", "新增session失败, err: "+e.Error(), c)
+		return
+	}
+
+	login := new(response.LoginResp)
+	login.Authorization = token
+	login.Authorization = "authorization=" + token + "$account=" + account
+	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 := services.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]
+
+	// 新增登录记录
+	{
+		record := new(rddp.SysUserLoginRecord)
+		record.Uid = sysUser.AdminId
+		record.UserName = adminName
+		record.Ip = c.RemoteIP()
+		record.Stage = "login"
+		record.CreateTime = time.Now()
+		go rddp.AddSysUserLoginRecord(record)
+	}
+
+	resp.OkData("获取成功", login, c)
+}

+ 155 - 0
controller/resp/base.go

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

+ 136 - 0
core/log.go

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

+ 30 - 0
core/run_server.go

@@ -0,0 +1,30 @@
+package core
+
+import (
+	"fmt"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/init_serve"
+)
+
+func RunServe() {
+	// 初始化路由
+	r := init_serve.InitRouter()
+
+	// 初始化mysql数据库
+	init_serve.Mysql()
+
+	// 如果配置了redis,那么链接redis
+	if global.CONFIG.Serve.UseRedis {
+		//初始化redis
+		init_serve.RedisTool()
+	}
+
+	// 启动任务
+	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))
+	}
+}

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

+ 64 - 0
global/global.go

@@ -0,0 +1,64 @@
+package global
+
+import (
+	"fmt"
+	"github.com/fsnotify/fsnotify"
+	"github.com/go-redis/redis/v8"
+	oplogging "github.com/op/go-logging"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+	"hongze/hz_crm_eta/config"
+	"hongze/hz_crm_eta/utils"
+	"io"
+
+	"github.com/rdlucklib/rdluck_tools/cache"
+)
+
+var (
+	CONFIG        config.Config //配置文件
+	LOG           *oplogging.Logger
+	MYSQL         map[string]*gorm.DB //数据库连接配置
+	MYSQL_LOG     io.Writer
+	DEFAULT_MYSQL *gorm.DB      //默认数据库连接配置
+	Redis         *redis.Client //redis链接
+
+	Rc *cache.Cache //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)
+		if err := v.Unmarshal(&CONFIG); err != nil {
+			fmt.Println("配置重赋值失败,Err:", err)
+		}
+		fmt.Println(CONFIG)
+	})
+	if err := v.Unmarshal(&CONFIG); err != nil {
+		fmt.Println("配置初始化赋值失败,Err:", err)
+	}
+}

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

+ 76 - 0
go.mod

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

+ 73 - 0
init_serve/mysql.go

@@ -0,0 +1,73 @@
+package init_serve
+
+import (
+	"fmt"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+	"hongze/hz_crm_eta/global"
+	"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
+}

+ 41 - 0
init_serve/redis.go

@@ -0,0 +1,41 @@
+package init_serve
+
+import (
+	"context"
+	"fmt"
+	"github.com/go-redis/redis/v8"
+	"hongze/hz_crm_eta/global"
+
+	"github.com/rdlucklib/rdluck_tools/cache"
+)
+
+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")
+	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)
+	}
+}

+ 22 - 0
init_serve/router.go

@@ -0,0 +1,22 @@
+package init_serve
+
+import (
+	"github.com/gin-gonic/gin"
+	_ "hongze/hz_crm_eta/docs"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/middleware"
+	"hongze/hz_crm_eta/routers"
+)
+
+// 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)
+	return
+}

+ 13 - 0
init_serve/task.go

@@ -0,0 +1,13 @@
+package init_serve
+
+import "hongze/hz_crm_eta/services"
+
+func InitTask() {
+	// 监听角色同步
+	services.ListenSyncRoleFromCrm()
+	services.ListenSyncRoleFromEta()
+
+	// 监听用户同步
+	services.ListenSyncAdminFromCrm()
+	services.ListenSyncAdminFromEta()
+}

+ 0 - 0
logic/logic.txt


+ 10 - 0
main.go

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

+ 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 (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"hongze/hz_crm_eta/controller/resp"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/services/alarm_msg"
+	"hongze/hz_crm_eta/utils"
+	"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()
+	}
+}

+ 27 - 0
middleware/token.go

@@ -0,0 +1,27 @@
+package middleware
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"hongze/hz_crm_eta/controller/resp"
+)
+
+func Token() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.Request.Header.Get("Authorization")
+		if token == "" {
+			token = c.DefaultQuery("authorization", "")
+			if token == "" {
+				token = c.DefaultQuery("Authorization", "")
+			}
+		}
+		if token == "" {
+			resp.TokenError(nil, "未登录或非法访问", "未登录或非法访问", c)
+			c.Abort()
+			return
+		}
+		fmt.Println("token:" + token)
+		//c.Set("adminInfo", admin)
+		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()
+}

+ 62 - 0
models/crm/admin.go

@@ -0,0 +1,62 @@
+package crm
+
+import (
+	"hongze/hz_crm_eta/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;default:1" 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"`                       // 职位
+	DirectorId                int       `gorm:"column:director_id;type:int(11)" json:"director_id"`
+	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"`                               // 员工工号(钉钉/每刻报销)
+}
+
+func (m *Admin) TableName() string {
+	return "admin"
+}
+
+// GetSysUserById 主键获取系统用户
+func GetSysUserById(sysUserId int) (item *Admin, err error) {
+	err = global.MYSQL["hz_crm"].Where("admin_id = ?", sysUserId).First(&item).Error
+	return
+}
+
+// GetSysUserByAdminName 用户名获取系统用户
+func GetSysUserByAdminName(adminName string) (item *Admin, err error) {
+	err = global.MYSQL["hz_crm"].Where("admin_name = ?", adminName).First(&item).Error
+	return
+}

+ 46 - 0
models/crm/sys_role.go

@@ -0,0 +1,46 @@
+package crm
+
+import (
+	"hongze/hz_crm_eta/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_crm"].Create(m).Error
+	return
+}
+
+// Update 更新角色
+func (m *SysRole) Update(cols []string) (err error) {
+	err = global.MYSQL["hz_crm"].Model(m).Select(cols).Updates(m).Error
+	return
+}
+
+// GetSysRoleByName 根据角色名称获取角色
+func GetSysRoleByName(roleName string) (item *SysRole, err error) {
+	err = global.MYSQL["hz_crm"].Where("role_name = ?", roleName).First(&item).Error
+	return
+}
+
+// DeleteRoleByName 根据角色名称删除角色
+func DeleteRoleByName(roleName string) (err error) {
+	sql := `DELETE FROM sys_role WHERE role_name = ? LIMIT 1`
+	err = global.MYSQL["hz_crm"].Exec(sql, roleName).Error
+	return
+}

+ 62 - 0
models/eta/admin.go

@@ -0,0 +1,62 @@
+package eta
+
+import (
+	"hongze/hz_crm_eta/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;default:1" 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"`                       // 职位
+	DirectorId                int       `gorm:"column:director_id;type:int(11)" json:"director_id"`
+	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"`                               // 员工工号(钉钉/每刻报销)
+}
+
+func (m *Admin) TableName() string {
+	return "admin"
+}
+
+// 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
+}

+ 46 - 0
models/eta/sys_role.go

@@ -0,0 +1,46 @@
+package eta
+
+import (
+	"hongze/hz_crm_eta/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
+}
+
+// GetSysRoleByName 根据角色名称获取角色
+func GetSysRoleByName(roleName string) (item *SysRole, err error) {
+	err = global.MYSQL["hz_eta"].Where("role_name = ?", roleName).First(&item).Error
+	return
+}
+
+// DeleteRoleByName 根据角色名称删除角色
+func DeleteRoleByName(roleName string) (err error) {
+	sql := `DELETE FROM sys_role WHERE role_name = ? LIMIT 1`
+	err = global.MYSQL["hz_eta"].Exec(sql, roleName).Error
+	return
+}

+ 33 - 0
models/eta/sys_session.go

@@ -0,0 +1,33 @@
+package eta
+
+import (
+	"hongze/hz_crm_eta/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
+}

+ 25 - 0
models/eta/sys_user_login_record.go

@@ -0,0 +1,25 @@
+package eta
+
+import (
+	"hongze/hz_crm_eta/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
+}

+ 33 - 0
models/rddp/sys_session.go

@@ -0,0 +1,33 @@
+package rddp
+
+import (
+	"hongze/hz_crm_eta/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
+}

+ 25 - 0
models/rddp/sys_user_login_record.go

@@ -0,0 +1,25 @@
+package rddp
+
+import (
+	"hongze/hz_crm_eta/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
+}

+ 10 - 0
models/request/auth.go

@@ -0,0 +1,10 @@
+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" binding:"required"`
+}

+ 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销售组长"`
+}

+ 15 - 0
routers/auth.go

@@ -0,0 +1,15 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"hongze/hz_crm_eta/controller"
+)
+
+func InitAuth(r *gin.RouterGroup) {
+	//登录
+	authController := new(controller.AuthController)
+	authGroup := r.Group("auth/")
+	authGroup.POST("auth_code", authController.CreateAuthCode)
+	authGroup.POST("eta_token", authController.GetEtaToken)
+	authGroup.POST("crm_token", authController.GetCrmToken)
+}

+ 31 - 0
services/alarm_msg/alarm_msg.go

@@ -0,0 +1,31 @@
+package alarm_msg
+
+import (
+	"encoding/json"
+	"github.com/rdlucklib/rdluck_tools/http"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/utils"
+)
+
+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))
+}

+ 67 - 0
services/cache_queue.go

@@ -0,0 +1,67 @@
+package services
+
+import (
+	"fmt"
+	"hongze/hz_crm_eta/global"
+	"hongze/hz_crm_eta/utils"
+)
+
+// ListenSyncAdminFromCrm 从CRM系统中同步用户
+func ListenSyncAdminFromCrm() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[ListenSyncAdminFromCrm]", err)
+		}
+	}()
+	for {
+		global.Rc.Brpop(utils.CACHE_SYNC_SYS_USER_FROM_CRM, func(b []byte) {
+			adminName := string(b)
+			_ = HandleSyncAdminFromCrm(utils.SOURCE_CRM_FLAG, adminName)
+		})
+	}
+}
+
+// ListenSyncAdminFromEta 从ETA系统中同步用户
+func ListenSyncAdminFromEta() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[ListenSyncAdminFromEta]", err)
+		}
+	}()
+	for {
+		global.Rc.Brpop(utils.CACHE_SYNC_SYS_USER_FROM_ETA, func(b []byte) {
+			adminName := string(b)
+			_ = HandleSyncAdminFromCrm(utils.SOURCE_ETA_FLAG, adminName)
+		})
+	}
+}
+
+// ListenSyncRoleFromCrm 从CRM系统中同步角色
+func ListenSyncRoleFromCrm() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[ListenSyncRoleFromCrm]", err)
+		}
+	}()
+	for {
+		global.Rc.Brpop(utils.CACHE_SYNC_SYS_ROLE_FROM_CRM, func(b []byte) {
+			roleName := string(b)
+			_ = HandleSyncSysRole(utils.SOURCE_CRM_FLAG, roleName)
+		})
+	}
+}
+
+// ListenSyncRoleFromEta 从ETA系统中同步角色
+func ListenSyncRoleFromEta() {
+	defer func() {
+		if err := recover(); err != nil {
+			fmt.Println("[ListenSyncRoleFromEta]", err)
+		}
+	}()
+	for {
+		global.Rc.Brpop(utils.CACHE_SYNC_SYS_ROLE_FROM_ETA, func(b []byte) {
+			roleName := string(b)
+			_ = HandleSyncSysRole(utils.SOURCE_ETA_FLAG, roleName)
+		})
+	}
+}

+ 22 - 0
services/company.go

@@ -0,0 +1,22 @@
+package services
+
+import "hongze/hz_crm_eta/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
+}

+ 115 - 0
services/sys_user_sync.go

@@ -0,0 +1,115 @@
+package services
+
+import (
+	"fmt"
+	"hongze/hz_crm_eta/models/crm"
+	"hongze/hz_crm_eta/models/eta"
+	"hongze/hz_crm_eta/utils"
+	"time"
+)
+
+// HandleSyncSysRole 同步系统角色
+func HandleSyncSysRole(source int, roleName string) (err error) {
+	// 角色名称唯一, 所以用角色名称做同步
+	if roleName == "" {
+		return
+	}
+	if source != utils.SOURCE_CRM_FLAG && source != utils.SOURCE_ETA_FLAG {
+		return
+	}
+
+	// CRM
+	if source == utils.SOURCE_CRM_FLAG {
+		// 获取角色不存在, 则表示CRM删除了该角色, 需删除对应ETA角色
+		crmRole, e := crm.GetSysRoleByName(roleName)
+		if e != nil {
+			if e != utils.ErrNoRow {
+				err = fmt.Errorf("获取CRM角色信息失败, Err: " + e.Error())
+				return
+			}
+			_ = eta.DeleteRoleByName(roleName)
+			return
+		}
+
+		// 获取ETA对应角色, 存在则更新, 否则新增
+		etaRole, e := eta.GetSysRoleByName(roleName)
+		if e != nil {
+			if e != utils.ErrNoRow {
+				err = fmt.Errorf("获取ETA对应角色信息失败, Err: " + e.Error())
+				return
+			}
+			// 新增
+			newRole := new(eta.SysRole)
+			newRole.RoleName = crmRole.RoleName
+			newRole.RoleType = crmRole.RoleType
+			newRole.RoleTypeCode = crmRole.RoleTypeCode
+			newRole.RoleLevel = crmRole.RoleLevel
+			newRole.CreateTime = time.Now().Local()
+			newRole.ModifyTime = time.Now().Local()
+			if e = newRole.Create(); e != nil {
+				err = fmt.Errorf("新增ETA对应角色失败, Err: " + e.Error())
+				return
+			}
+		}
+		// 更新
+		etaRole.RoleName = crmRole.RoleName
+		etaRole.RoleType = crmRole.RoleType
+		etaRole.RoleTypeCode = crmRole.RoleTypeCode
+		etaRole.RoleLevel = crmRole.RoleLevel
+		etaRole.ModifyTime = time.Now().Local()
+		cols := []string{"RoleName", "RoleType", "RoleTypeCode", "RoleLevel", "ModifyTime"}
+		e = etaRole.Update(cols)
+		if e != nil {
+			err = fmt.Errorf("更新ETA对应角色失败, Err: " + e.Error())
+		}
+		return
+	}
+
+	// ETA
+	etaRole, e := eta.GetSysRoleByName(roleName)
+	if e != nil {
+		if e != utils.ErrNoRow {
+			err = fmt.Errorf("获取ETA角色信息失败, Err: " + e.Error())
+			return
+		}
+		_ = crm.DeleteRoleByName(roleName)
+		return
+	}
+
+	crmRole, e := crm.GetSysRoleByName(roleName)
+	if e != nil {
+		if e != utils.ErrNoRow {
+			err = fmt.Errorf("获取CRM对应角色信息失败, Err: " + e.Error())
+			return
+		}
+		// 新增
+		newRole := new(crm.SysRole)
+		newRole.RoleName = etaRole.RoleName
+		newRole.RoleType = etaRole.RoleType
+		newRole.RoleTypeCode = etaRole.RoleTypeCode
+		newRole.RoleLevel = etaRole.RoleLevel
+		newRole.CreateTime = time.Now().Local()
+		newRole.ModifyTime = time.Now().Local()
+		if e = newRole.Create(); e != nil {
+			err = fmt.Errorf("新增CRM对应角色失败, Err: " + e.Error())
+			return
+		}
+	}
+	// 更新
+	crmRole.RoleName = etaRole.RoleName
+	crmRole.RoleType = etaRole.RoleType
+	crmRole.RoleTypeCode = etaRole.RoleTypeCode
+	crmRole.RoleLevel = etaRole.RoleLevel
+	crmRole.ModifyTime = time.Now().Local()
+	cols := []string{"RoleName", "RoleType", "RoleTypeCode", "RoleLevel", "ModifyTime"}
+	e = crmRole.Update(cols)
+	if e != nil {
+		err = fmt.Errorf("更新CRM对应角色失败, Err: " + e.Error())
+	}
+	return
+}
+
+// HandleSyncAdminFromCrm 同步系统用户
+func HandleSyncAdminFromCrm(source int, adminName string) (err error) {
+	return
+}

+ 0 - 0
static/static.txt


+ 72 - 0
task/task.go

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

+ 1073 - 0
utils/common.go

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

+ 87 - 0
utils/constants.go

@@ -0,0 +1,87 @@
+package utils
+
+// 常量定义
+const (
+	FormatTime            = "15:04:05"                //时间格式
+	FormatDate            = "2006-01-02"              //日期格式
+	FormatDateCN          = "2006年01月02日"             //日期格式(中文)
+	FormatDateUnSpace     = "20060102"                //日期格式
+	FormatDateTime        = "2006-01-02 15:04:05"     //完整时间格式
+	HlbFormatDateTime     = "2006-01-02_15:04:05.999" //完整时间格式
+	FormatDateTimeUnSpace = "20060102150405"          //完整时间格式
+	PageSize15            = 15                        //列表页每页数据量
+	PageSize5             = 5
+	PageSize10            = 10
+	PageSize20            = 20
+	PageSize30            = 30
+)
+
+// 手机号,电子邮箱正则
+const (
+	RegularMobile = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0-9])|(17[0-9])|(16[0-9])|(19[0-9]))\\d{8}$" //手机号码
+	RegularEmail  = `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`                                             //匹配电子邮箱
+)
+
+const (
+	APPNAME = "弘则CRM-ETA"
+)
+
+const (
+	CACHE_CRM_AUTH_CODE_PREFIX = "hz_crm_eta:crm:auth_code:"
+	CACHE_ETA_AUTH_CODE_PREFIX = "hz_crm_eta:eta:auth_code:"
+)
+
+// 管理员,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
+)
+
+const (
+	CACHE_SYNC_SYS_USER_FROM_CRM = "hz_crm_eta:crm_admin_sync:list"
+	CACHE_SYNC_SYS_USER_FROM_ETA = "hz_crm_eta:eta_admin_sync:list"
+	CACHE_SYNC_SYS_ROLE_FROM_CRM = "hz_crm_eta:crm_role_sync:list"
+	CACHE_SYNC_SYS_ROLE_FROM_ETA = "hz_crm_eta:eta_role_sync:list"
+)

+ 191 - 0
utils/des3.go

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

+ 48 - 0
utils/directory.go

@@ -0,0 +1,48 @@
+package utils
+
+import (
+	"fmt"
+	"os"
+)
+
+// @title    PathExists
+// @description   文件目录是否存在
+// @auth                     (2020/04/05  20:22)
+// @param     path            string
+// @return    err             error
+
+func PathExists(path string) (bool, error) {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true, nil
+	}
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return false, err
+}
+
+// @title    createDir
+// @description   批量创建文件夹
+// @auth                     (2020/04/05  20:22)
+// @param     dirs            string
+// @return    err             error
+
+func CreateDir(dirs ...string) (err error) {
+	for _, v := range dirs {
+		exist, err := PathExists(v)
+		if err != nil {
+			return err
+		}
+		if !exist {
+			fmt.Println("create directory ", v)
+			//global.LOG.Debug("create directory ", v)
+			err = os.MkdirAll(v, os.ModePerm)
+			if err != nil {
+				fmt.Println("create directory", v, " error:", err)
+				//global.LOG.Error("create directory", v, " error:", err)
+			}
+		}
+	}
+	return err
+}

+ 357 - 0
utils/drawtext.go

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

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