Browse Source

商用eta1.4相关接口

hsun 1 year ago
parent
commit
42bd5b0b26

+ 888 - 0
controllers/user_login.go

@@ -0,0 +1,888 @@
+package controllers
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"github.com/mojocn/base64Captcha"
+	"hongze/hongze_ETA_mobile_api/models"
+	"hongze/hongze_ETA_mobile_api/models/company"
+	"hongze/hongze_ETA_mobile_api/models/system"
+	"hongze/hongze_ETA_mobile_api/services"
+	"hongze/hongze_ETA_mobile_api/utils"
+	"image/color"
+	"strings"
+	"time"
+)
+
+// UserLoginController 登录-无需Token
+type UserLoginController struct {
+	BaseCommonController
+}
+
+// UserLoginAuthController 登录-需Token
+type UserLoginAuthController struct {
+	BaseAuthController
+}
+
+// GenerateCaptcha
+// @Title 生成图形验证码
+// @Description 生成图形验证码
+// @Success 200 Ret=200 获取成功
+// @router /get_captcha [get]
+func (this *UserLoginController) GenerateCaptcha() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	//driver := base64Captcha.DefaultDriverDigit
+	// 自定义验证码样式
+	var driver base64Captcha.Driver
+	driverString := base64Captcha.DriverString{
+		Height:          60,    //高度
+		Width:           120,   //宽度
+		NoiseCount:      0,     //干扰数
+		ShowLineOptions: 2 | 4, //展示个数
+		Length:          4,     //长度
+		//Source:          "1234567890qwertyuioplkjhgfdsazxcvbnm", //验证码随机字符串来源
+		Source: "1234567890", //验证码随机字符串来源
+		BgColor: &color.RGBA{ // 背景颜色
+			R: 0,
+			G: 0,
+			B: 0,
+			A: 0,
+		},
+		Fonts: []string{"wqy-microhei.ttc"}, // 字体
+	}
+	driver = driverString.ConvertFonts()
+
+	// 生成验证码
+	store := services.CaptchaRedis{}
+	captcha := base64Captcha.NewCaptcha(driver, store)
+	id, b64s, err := captcha.Generate()
+	if err != nil {
+		br.Msg = "生成失败"
+		br.ErrMsg = "生成验证码失败, Err: " + err.Error()
+		return
+	}
+
+	type CaptchaResult struct {
+		Id         string
+		Base64Blob string
+	}
+	res := new(CaptchaResult)
+	res.Id = id
+	res.Base64Blob = b64s
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+	br.Data = res
+}
+
+// GetVerifyCode
+// @Title 获取短信/邮箱验证码
+// @Description 获取短信/邮箱验证码
+// @Param	request	body VerifyCodeReq true "type json string"
+// @Success 200 Ret=200 获取成功
+// @router /verify_code [post]
+func (this *UserLoginController) GetVerifyCode() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	type VerifyCodeReq struct {
+		VerifyType  int    `description:"验证方式: 1-手机号; 2-邮箱"`
+		CaptchaId   string `description:"验证码ID"`
+		CaptchaCode string `description:"图形验证码"`
+		Mobile      string `description:"手机号"`
+		TelAreaCode string `description:"手机区号"`
+		Email       string `description:"邮箱"`
+		Source      int    `description:"来源:1-登录;2-异常登录校验;3-忘记密码"`
+	}
+	var req VerifyCodeReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	if req.VerifyType != 1 && req.VerifyType != 2 {
+		br.Msg = "验证方式有误"
+		br.ErrMsg = "验证方式有误"
+		return
+	}
+	if req.Source != 1 && req.Source != 2 && req.Source != 3 {
+		br.Msg = "来源有误"
+		br.ErrMsg = "来源有误"
+		return
+	}
+	// 忘记密码图形校验在前一步, 这一步不再校验图形验证码
+	if req.Source != 3 {
+		if req.CaptchaId == "" || req.CaptchaCode == "" {
+			br.Msg = "请输入图形验证码"
+			return
+		}
+	}
+	if req.VerifyType == 1 {
+		if req.TelAreaCode == "" {
+			br.Msg = "请选择区号"
+			return
+		}
+		if req.Mobile == "" {
+			br.Msg = "请输入手机号"
+			return
+		}
+		if req.TelAreaCode == "86" && !utils.ValidateMobileFormatat(req.Mobile) {
+			br.Msg = "您的手机号输入有误, 请检查"
+			return
+		}
+		_, e := system.GetSysUserByMobile(req.Mobile)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "您的手机号未绑定账号, 请检查"
+				return
+			}
+			br.Msg = "您的手机号未绑定账号, 请检查"
+			br.ErrMsg = "手机号获取用户失败, Err:" + e.Error()
+			return
+		}
+	}
+	if req.VerifyType == 2 {
+		if req.Email == "" {
+			br.Msg = "请输入邮箱"
+			return
+		}
+		if !utils.ValidateEmailFormatat(req.Email) {
+			br.Msg = "您的邮箱输入有误, 请检查"
+			return
+		}
+		_, e := system.GetSysUserByEmail(req.Email)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "您的邮箱未绑定账号, 请检查"
+				return
+			}
+			br.Msg = "您的邮箱未绑定账号, 请检查"
+			br.ErrMsg = "邮箱获取用户失败, Err:" + e.Error()
+			return
+		}
+	}
+	if req.Source != 3 {
+		store := services.CaptchaRedis{}
+		ok := store.Verify(req.CaptchaId, req.CaptchaCode, true)
+		if !ok {
+			br.Msg = "图形验证码错误, 请重新输入"
+			return
+		}
+	}
+
+	// 限制最多60s获取一次
+	var lockKey string
+	if req.VerifyType == 1 {
+		lockKey = fmt.Sprint(utils.CaptchaCachePrefix, req.Mobile)
+	}
+	if req.VerifyType == 2 {
+		lockKey = fmt.Sprint(utils.CaptchaCachePrefix, req.Email)
+	}
+	locked := utils.Rc.SetNX(lockKey, 1, time.Minute)
+	if !locked {
+		br.Msg = "请勿频繁发送"
+		return
+	}
+
+	// 发送验证码
+	sendRes := false
+	if req.VerifyType == 1 {
+		r, e := services.SendAdminMobileVerifyCode(req.Source, req.Mobile, req.TelAreaCode)
+		if e != nil {
+			br.Msg = "发送失败"
+			br.ErrMsg = "发送短信验证码失败, Err: " + e.Error()
+			return
+		}
+		sendRes = r
+	}
+	if req.VerifyType == 2 {
+		r, e := services.SendAdminEmailVerifyCode(req.Source, req.Email)
+		if e != nil {
+			br.Msg = "发送失败"
+			br.ErrMsg = "发送邮箱验证码失败, Err: " + e.Error()
+			return
+		}
+		sendRes = r
+	}
+	if !sendRes {
+		br.Msg = "发送失败"
+		br.ErrMsg = "发送短信验证码失败"
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "发送成功"
+}
+
+// Login
+// @Title 用户登录
+// @Description 用户登录
+// @Param	request	body UserLoginReq true "type json string"
+// @Success 200 {object} models.LoginResp
+// @router /login [post]
+func (this *UserLoginController) Login() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg != "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	// 入参
+	type UserLoginReq struct {
+		LoginType  int    `description:"登录方式: 1-账号; 2-手机号; 3-邮箱"`
+		Username   string `description:"账号"`
+		Password   string `description:"密码"`
+		Mobile     string `description:"手机号"`
+		Email      string `description:"邮箱"`
+		VerifyCode string `description:"验证码"`
+	}
+	var req UserLoginReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.Username = strings.TrimSpace(req.Username)
+	req.Mobile = strings.TrimSpace(req.Mobile)
+	if req.Mobile != "" {
+		if !utils.ValidateMobileFormatat(req.Mobile) {
+			br.Msg = "您的手机号输入有误, 请检查"
+			return
+		}
+	}
+	req.Email = strings.TrimSpace(req.Email)
+	if req.Email != "" {
+		if !utils.ValidateEmailFormatat(req.Email) {
+			br.Msg = "您的邮箱输入有误, 请检查"
+			return
+		}
+	}
+	req.VerifyCode = strings.TrimSpace(req.VerifyCode)
+	if req.LoginType != 1 && req.LoginType != 2 && req.LoginType != 3 {
+		br.Msg = "登录方式有误"
+		br.ErrMsg = fmt.Sprintf("登录方式有误, Type: %d", req.LoginType)
+		return
+	}
+
+	sysUser := new(system.Admin)
+	// 账号密码登录
+	if req.LoginType == 1 {
+		if req.Username == "" {
+			br.Msg = "请输入账号"
+			return
+		}
+		if req.Password == "" {
+			br.Msg = "请输入密码"
+			return
+		}
+
+		// 判断账号是否为异常登录, 算作异常的话就需要换另外的方式去做登录了
+		abnormalKey := fmt.Sprint(utils.CACHE_ABNORMAL_LOGIN, req.Username)
+		isAbnormal, _ := utils.Rc.RedisString(abnormalKey)
+		if isAbnormal != "" {
+			br.Ret = models.BaseRespCodeAbnormalLogin
+			br.Msg = "账号异常, 请进行手机号/邮箱校验"
+			return
+		}
+
+		// 账号密码校验
+		errPassKey := fmt.Sprint(utils.CACHE_LOGIN_ERR_PASS, req.Username)
+		accountUser, e := system.CheckSysUser(req.Username, req.Password)
+		if e != nil {
+			br.Ret = models.BaseRespCodeLoginErr
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "登录失败, 账号或密码错误"
+				if isAbnormal != "" {
+					return
+				}
+				// 错误密码计数, 超过6次标记异常
+				if !utils.Rc.IsExist(errPassKey) {
+					_ = utils.Rc.Put(errPassKey, 1, utils.GetTodayLastSecond())
+					return
+				}
+				errNum, _ := utils.Rc.RedisInt(errPassKey)
+				errNum += 1
+				if errNum >= 6 {
+					br.Ret = models.BaseRespCodeAbnormalLogin
+					br.Msg = "账号异常, 请进行手机号/邮箱校验"
+					// 标记异常登录, 重置计数
+					_ = utils.Rc.Put(abnormalKey, "true", utils.GetTodayLastSecond())
+					_ = utils.Rc.Delete(errPassKey)
+					return
+				}
+				_ = utils.Rc.Put(errPassKey, errNum, utils.GetTodayLastSecond())
+				return
+			}
+			br.Msg = "登录失败, 账号或密码错误"
+			br.ErrMsg = "登录失败, Err:" + e.Error()
+			return
+		}
+		if accountUser.Enabled == 0 {
+			br.Msg = "您的账号已被禁用, 如需登录, 请联系管理员"
+			br.ErrMsg = fmt.Sprintf("账号已被禁用, 登录账号: %s, 账户名称: %s", accountUser.AdminName, accountUser.RealName)
+			return
+		}
+
+		// 异常登录-是否登录间隔大于60天
+		if isAbnormal == "" {
+			abnormalTime := time.Now().AddDate(0, 0, -60)
+			lastLogin, _ := time.ParseInLocation(utils.FormatDateTime, accountUser.LastLoginTime, time.Local)
+			if !lastLogin.IsZero() && lastLogin.Before(abnormalTime) {
+				br.Msg = "请进行异常登录校验"
+				br.Ret = models.BaseRespCodeAbnormalLogin
+				// 标记异常登录
+				_ = utils.Rc.Put(abnormalKey, "true", utils.GetTodayLastSecond())
+				return
+			}
+		}
+
+		// 登录成功, 清除异常标记
+		_ = utils.Rc.Delete(abnormalKey)
+		_ = utils.Rc.Delete(errPassKey)
+		sysUser = accountUser
+	}
+
+	// 手机号
+	if req.LoginType == 2 {
+		if req.Mobile == "" {
+			br.Msg = "请输入手机号"
+			return
+		}
+		if !utils.ValidateMobileFormatat(req.Mobile) {
+			br.Msg = "您的手机号输入有误, 请检查"
+			return
+		}
+		if req.VerifyCode == "" {
+			br.Msg = "请输入验证码"
+			return
+		}
+		expiredTime := time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute).Format(utils.FormatDateTime)
+		recordCond := ` AND source = ? AND mobile = ? AND code = ? AND expired_time <= ?`
+		recordPars := make([]interface{}, 0)
+		recordPars = append(recordPars, system.AdminVerifyCodeRecordSourceLogin, req.Mobile, req.VerifyCode, expiredTime)
+		recordOb := new(system.AdminVerifyCodeRecord)
+		_, e := recordOb.GetItemByCondition(recordCond, recordPars)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "验证码错误, 请重新输入"
+				return
+			}
+			br.Msg = "验证码错误, 请重新输入"
+			br.ErrMsg = "获取手机号登陆验证码失败, Err: " + e.Error()
+			return
+		}
+
+		mobileUser, e := system.GetSysUserByMobile(req.Mobile)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "您的手机号未绑定账号, 请检查"
+				return
+			}
+			br.Msg = "您的手机号未绑定账号, 请检查"
+			br.ErrMsg = "手机号获取用户失败, Err:" + e.Error()
+			return
+		}
+		if mobileUser.Enabled == 0 {
+			br.Msg = "您的账号已被禁用, 如需登录, 请联系管理员"
+			br.ErrMsg = fmt.Sprintf("账号已被禁用, 登录手机号: %s", req.Mobile)
+			return
+		}
+		sysUser = mobileUser
+	}
+
+	// 邮箱登录
+	if req.LoginType == 3 {
+		if req.Email == "" {
+			br.Msg = "请输入邮箱"
+			return
+		}
+		if !utils.ValidateEmailFormatat(req.Email) {
+			br.Msg = "您的邮箱输入有误, 请检查"
+			return
+		}
+		if req.VerifyCode == "" {
+			br.Msg = "请输入验证码"
+			return
+		}
+		expiredTime := time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute).Format(utils.FormatDateTime)
+		recordCond := ` AND source = ? AND email = ? AND code = ? AND expired_time <= ?`
+		recordPars := make([]interface{}, 0)
+		recordPars = append(recordPars, system.AdminVerifyCodeRecordSourceLogin, req.Email, req.VerifyCode, expiredTime)
+		recordOb := new(system.AdminVerifyCodeRecord)
+		_, e := recordOb.GetItemByCondition(recordCond, recordPars)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "验证码错误, 请重新输入"
+				return
+			}
+			br.Msg = "验证码错误, 请重新输入"
+			br.ErrMsg = "获取手机号登陆验证码失败, Err: " + e.Error()
+			return
+		}
+
+		emailUser, e := system.GetSysUserByEmail(req.Email)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "您的邮箱未绑定账号, 请检查"
+				return
+			}
+			br.Msg = "您的邮箱未绑定账号, 请检查"
+			br.ErrMsg = "邮箱获取用户失败, Err:" + e.Error()
+			return
+		}
+		if emailUser.Enabled == 0 {
+			br.Msg = "您的账号已被禁用, 如需登录, 请联系管理员"
+			br.ErrMsg = fmt.Sprintf("账号已被禁用, 登录邮箱: %s", req.Email)
+			return
+		}
+		sysUser = emailUser
+	}
+
+	// 登录成功(无论哪种方式登录), 清除异常标记
+	abnormalCache := fmt.Sprint(utils.CACHE_ABNORMAL_LOGIN, sysUser.AdminName)
+	errPassCache := fmt.Sprint(utils.CACHE_LOGIN_ERR_PASS, sysUser.AdminName)
+	_ = utils.Rc.Delete(abnormalCache)
+	_ = utils.Rc.Delete(errPassCache)
+
+	account := utils.MD5(sysUser.AdminName)
+	token := utils.GenToken(account)
+	sysSession := new(system.SysSession)
+	sysSession.UserName = req.Username
+	sysSession.SysUserId = sysUser.AdminId
+	sysSession.ExpiredTime = time.Now().AddDate(0, 0, 90)
+	sysSession.IsRemember = 0 // 均需要做过期校验
+	sysSession.CreatedTime = time.Now()
+	sysSession.LastUpdatedTime = time.Now()
+	sysSession.AccessToken = token
+	if e := system.AddSysSession(sysSession); e != nil {
+		br.Msg = "登录失败"
+		br.ErrMsg = "新增session信息失败, Err:" + e.Error()
+		return
+	}
+
+	// 修改最后登录时间
+	{
+		sysUser.LastLoginTime = time.Now().Format(utils.FormatDateTime)
+		sysUser.LastUpdatedTime = time.Now().Format(utils.FormatDateTime)
+		_ = sysUser.Update([]string{"LastLoginTime", "LastUpdatedTime"})
+	}
+
+	resp := new(system.LoginResp)
+	resp.Authorization = token
+	resp.Authorization = "authorization=" + token + "$account=" + account
+	resp.RealName = sysUser.RealName
+	resp.AdminName = sysUser.AdminName
+	resp.RoleName = sysUser.RoleName
+	resp.SysRoleTypeCode = sysUser.RoleTypeCode //系统角色编码
+	resp.RoleTypeCode = sysUser.RoleTypeCode
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_GROUP {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_TEAM {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_FICC_DEPARTMENT {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_RAI_GROUP {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_RAI_SELLER
+	}
+	if sysUser.RoleTypeCode == utils.ROLE_TYPE_CODE_RAI_DEPARTMENT {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_RAI_SELLER
+	}
+	if sysUser.RoleName == utils.ROLE_NAME_FICC_DIRECTOR {
+		resp.RoleTypeCode = utils.ROLE_TYPE_CODE_FICC_SELLER
+	}
+	resp.AdminId = sysUser.AdminId
+	var productName string
+	productId := services.GetProductId(sysUser.RoleTypeCode)
+	if productId == 1 {
+		productName = utils.COMPANY_PRODUCT_FICC_NAME
+	} else if productId == 2 {
+		productName = utils.COMPANY_PRODUCT_RAI_NAME
+	} else {
+		productName = "admin"
+	}
+	resp.ProductName = productName
+	resp.Authority = sysUser.Authority
+
+	// 设置redis缓存
+	{
+		// 获取不可信的登录态,并将该登录态重置掉,不允许多次登录
+		noTrustLoginKey := fmt.Sprint(utils.CACHE_ACCESS_TOKEN_LOGIN_NO_TRUST, sysUser.AdminId)
+		noTrustLoginId, _ := utils.Rc.RedisString(noTrustLoginKey)
+		if noTrustLoginId != `` { // 如果存在不可信设备,那么将其下架
+			oldNoTrustLoginKey := fmt.Sprint(utils.CACHE_ACCESS_TOKEN_LOGIN, noTrustLoginId)
+			_ = utils.Rc.Put(oldNoTrustLoginKey, "0", 30*time.Minute)
+		}
+
+		// 如果当前是不可信设备,那么将其加入到不可信名单
+		loginKey := fmt.Sprint(utils.CACHE_ACCESS_TOKEN_LOGIN, sysSession.Id)
+		_ = utils.Rc.Put(loginKey, "1", 30*time.Minute)
+		_ = utils.Rc.Put(noTrustLoginKey, sysSession.Id, 30*time.Minute)
+	}
+
+	// 新增登录记录
+	go func() {
+		record := new(system.SysUserLoginRecord)
+		record.Uid = sysUser.AdminId
+		record.UserName = req.Username
+		record.Ip = this.Ctx.Input.IP()
+		record.Stage = "login"
+		record.CreateTime = time.Now()
+		_ = system.AddSysUserLoginRecord(record)
+	}()
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "登录成功"
+}
+
+// ForgetAccountGet
+// @Title 忘记密码-账号校验
+// @Description 忘记密码-账号校验
+// @Param	request	body ForgetAccountGetReq true "type json string"
+// @Success 200 Ret=200 获取成功
+// @router /forget/account_get [post]
+func (this *UserLoginController) ForgetAccountGet() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	type ForgetAccountGetReq struct {
+		CaptchaId   string `description:"验证码ID"`
+		CaptchaCode string `description:"图形验证码"`
+		UserName    string `description:"用户名"`
+	}
+	var req ForgetAccountGetReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.UserName = strings.TrimSpace(req.UserName)
+	if req.UserName == "" {
+		br.Msg = "请输入账号"
+		return
+	}
+	if req.CaptchaId == "" || req.CaptchaCode == "" {
+		br.Msg = "请输入图形验证码"
+		return
+	}
+	store := services.CaptchaRedis{}
+	ok := store.Verify(req.CaptchaId, req.CaptchaCode, true)
+	if !ok {
+		br.Msg = "验证码错误, 请重新输入"
+		return
+	}
+
+	sysUser, e := system.GetSysUserByAdminName(req.UserName)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "用户不存在, 请检查"
+			return
+		}
+		br.Msg = "账号错误, 请重新输入"
+		br.ErrMsg = "用户名获取用户失败, Err: " + e.Error()
+		return
+	}
+
+	type ForgetAccountCheckResp struct {
+		Mobile      string `description:"手机号"`
+		Email       string `description:"邮箱"`
+		TelAreaCode string `description:"手机区号"`
+	}
+	resp := ForgetAccountCheckResp{
+		Mobile:      sysUser.Mobile,
+		Email:       sysUser.Email,
+		TelAreaCode: sysUser.TelAreaCode,
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// ForgetCodeVerify
+// @Title 忘记密码-验证码校验
+// @Description 忘记密码-验证码校验
+// @Param	request	body ForgetCodeVerifyReq true "type json string"
+// @Success 200 Ret=200 获取成功
+// @router /forget/code_verify [post]
+func (this *UserLoginController) ForgetCodeVerify() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	type ForgetCodeVerifyReq struct {
+		FindType   int    `description:"密码找回方式: 1-手机号; 2-邮箱"`
+		VerifyCode string `description:"验证码"`
+		UserName   string `description:"用户名"`
+		Mobile     string `description:"手机号"`
+		Email      string `description:"邮箱"`
+	}
+	var req ForgetCodeVerifyReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.UserName = strings.TrimSpace(req.UserName)
+	if req.UserName == "" {
+		br.Msg = "请输入账号"
+		return
+	}
+	if req.VerifyCode == "" {
+		br.Msg = "请输入验证码"
+		return
+	}
+
+	// 手机号找回
+	var verifyRes bool
+	if req.FindType == 1 {
+		req.Mobile = strings.TrimSpace(req.Mobile)
+		if req.Mobile == "" {
+			br.Msg = "请输入手机号"
+			return
+		}
+		if !utils.ValidateMobileFormatat(req.Mobile) {
+			br.Msg = "您的手机号输入有误, 请检查"
+			return
+		}
+		expiredTime := time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute).Format(utils.FormatDateTime)
+		recordCond := ` AND source = ? AND mobile = ? AND code = ? AND expired_time <= ?`
+		recordPars := make([]interface{}, 0)
+		recordPars = append(recordPars, system.AdminVerifyCodeRecordSourceForget, req.Mobile, req.VerifyCode, expiredTime)
+		recordOb := new(system.AdminVerifyCodeRecord)
+		_, e := recordOb.GetItemByCondition(recordCond, recordPars)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "验证码错误, 请重新输入"
+				return
+			}
+			br.Msg = "验证码错误, 请重新输入"
+			br.ErrMsg = "获取手机号登陆验证码失败, Err: " + e.Error()
+			return
+		}
+
+		verifyRes = true
+	}
+
+	// 邮箱找回
+	if req.FindType == 2 {
+		req.Email = strings.TrimSpace(req.Email)
+		if req.Email == "" {
+			br.Msg = "请输入邮箱"
+			return
+		}
+		if !utils.ValidateEmailFormatat(req.Email) {
+			br.Msg = "您的邮箱输入有误, 请检查"
+			return
+		}
+		expiredTime := time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute).Format(utils.FormatDateTime)
+		recordCond := ` AND source = ? AND email = ? AND code = ? AND expired_time <= ?`
+		recordPars := make([]interface{}, 0)
+		recordPars = append(recordPars, system.AdminVerifyCodeRecordSourceForget, req.Email, req.VerifyCode, expiredTime)
+		recordOb := new(system.AdminVerifyCodeRecord)
+		_, e := recordOb.GetItemByCondition(recordCond, recordPars)
+		if e != nil {
+			if e.Error() == utils.ErrNoRow() {
+				br.Msg = "验证码错误, 请重新输入"
+				return
+			}
+			br.Msg = "验证码错误, 请重新输入"
+			br.ErrMsg = "获取手机号登陆验证码失败, Err: " + e.Error()
+			return
+		}
+
+		verifyRes = true
+	}
+
+	if verifyRes {
+		successKey := fmt.Sprint(utils.CACHE_FIND_PASS_VERIFY, req.UserName)
+		_ = utils.Rc.Put(successKey, "true", utils.GetTodayLastSecond())
+	}
+
+	br.Data = verifyRes
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// ForgetResetPass
+// @Title 忘记密码-重置密码
+// @Description 忘记密码-重置密码
+// @Param	request	body ForgetResetPassReq true "type json string"
+// @Success 200 Ret=200 获取成功
+// @router /forget/reset_pass [post]
+func (this *UserLoginController) ForgetResetPass() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	type ForgetResetPassReq struct {
+		UserName   string `description:"用户名"`
+		Password   string `description:"密码"`
+		RePassword string `description:"重复密码"`
+	}
+	var req ForgetResetPassReq
+	err := json.Unmarshal(this.Ctx.Input.RequestBody, &req)
+	if err != nil {
+		br.Msg = "参数解析异常!"
+		br.ErrMsg = "参数解析失败,Err:" + err.Error()
+		return
+	}
+	req.UserName = strings.TrimSpace(req.UserName)
+	if req.UserName == "" {
+		br.Msg = "请输入账号"
+		return
+	}
+	req.Password = strings.TrimSpace(req.Password)
+	if req.Password == "" {
+		br.Msg = "请输入新密码"
+		return
+	}
+	req.RePassword = strings.TrimSpace(req.RePassword)
+	if req.RePassword == "" {
+		br.Msg = "请确认新密码"
+		return
+	}
+	if req.Password != req.RePassword {
+		br.Msg = "两次密码输入不一致, 请检查"
+		return
+	}
+
+	// 接上一步-验证码校验成功后才允许修改
+	successKey := fmt.Sprint(utils.CACHE_FIND_PASS_VERIFY, req.UserName)
+	verifyOk, _ := utils.Rc.RedisString(successKey)
+	if verifyOk != "true" {
+		br.Msg = "验证码校验失效, 请重新验证"
+		br.ErrMsg = "重置密码异常, 验证码校验已失效"
+		return
+	}
+
+	sysUser, e := system.GetSysAdminByName(req.UserName)
+	if e != nil {
+		if e.Error() == utils.ErrNoRow() {
+			br.Msg = "用户不存在, 请检查"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = "密码找回获取用户失败, Err:" + e.Error()
+		return
+	}
+
+	b, e := base64.StdEncoding.DecodeString(req.Password)
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = "解析密码失败, Err:" + e.Error()
+		return
+	}
+	pwd := string(b)
+	pwd = utils.MD5(pwd)
+
+	sysUser.Password = pwd
+	sysUser.LastUpdatedPasswordTime = time.Now().Format(utils.FormatDateTime)
+	// 重置密码后更新一下登录时间, 不然账号密码登录如果超过60天还会被异常校验再校验一次, 影响体验
+	sysUser.LastLoginTime = time.Now().Format(utils.FormatDateTime)
+	sysUser.LastUpdatedTime = time.Now().Format(utils.FormatDateTime)
+	if e = sysUser.Update([]string{"Password", "LastUpdatedPasswordTime", "LastLoginTime", "LastUpdatedTime"}); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = "更新密码失败, Err: " + e.Error()
+		return
+	}
+
+	// 如果有异常标记也清除掉
+	abnormalKey := fmt.Sprint(utils.CACHE_ABNORMAL_LOGIN, req.UserName)
+	_ = utils.Rc.Delete(abnormalKey)
+	_ = utils.Rc.Delete(successKey)
+
+	br.Data = true
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// AreaCodeList
+// @Title 手机号区号列表
+// @Description 手机号区号列表
+// @Success 200 Ret=200 获取成功
+// @router /area_code/list [get]
+func (this *UserLoginController) AreaCodeList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+
+	type AreaCodeListResp struct {
+		Name  string `description:"地区"`
+		Value string `description:"区号"`
+	}
+	resp := make([]AreaCodeListResp, 0)
+	confAuth, e := company.GetConfigDetailByCode(company.ConfAreaCodeListKey)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "获取手机号区号配置失败, Err: " + e.Error()
+		return
+	}
+	if confAuth.ConfigValue == "" {
+		br.Msg = "获取失败"
+		br.ErrMsg = "手机号区号配置为空"
+		return
+	}
+	if e := json.Unmarshal([]byte(confAuth.ConfigValue), &resp); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "手机号区号配置有误"
+		return
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 3 - 0
go.mod

@@ -16,6 +16,7 @@ require (
 	github.com/gonum/stat v0.0.0-20181125101827-41a0da705a5b
 	github.com/gorilla/websocket v1.4.2
 	github.com/kgiannakakis/mp3duration v0.0.0-20191013070830-d834f8d5ed53
+	github.com/mojocn/base64Captcha v1.3.5
 	github.com/nosixtools/solarlunar v0.0.0-20211112060703-1b6dea7b4a19
 	github.com/olivere/elastic/v7 v7.0.30
 	github.com/rdlucklib/rdluck_tools v1.0.3
@@ -46,6 +47,7 @@ require (
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/garyburd/redigo v1.6.3 // indirect
 	github.com/go-redis/redis/v8 v8.11.5 // indirect
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac // indirect
 	github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
@@ -82,6 +84,7 @@ require (
 	github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
 	github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
 	golang.org/x/crypto v0.8.0 // indirect
+	golang.org/x/image v0.5.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

+ 5 - 0
go.sum

@@ -178,6 +178,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -322,6 +324,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/mojocn/base64Captcha v1.3.5 h1:Qeilr7Ta6eDtG4S+tQuZ5+hO+QHbiGAJdi4PfoagaA0=
+github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G6I7NcP2WwcmY=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
@@ -513,6 +517,7 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
 golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=

+ 5 - 0
models/base.go

@@ -1,5 +1,10 @@
 package models
 
+const (
+	BaseRespCodeAbnormalLogin = 4011 // 异常登录状态码
+	BaseRespCodeLoginErr      = 4012 // 账号或密码输入错误
+)
+
 type BaseResponse struct {
 	Ret         int
 	Msg         string

+ 5 - 1
models/company/company_config.go

@@ -4,6 +4,10 @@ import (
 	"github.com/beego/beego/v2/client/orm"
 )
 
+const (
+	ConfAreaCodeListKey = "area_code_list" // 手机号区号列表
+)
+
 type CrmConfig struct {
 	ConfigValue string `description:"详情"`
 }
@@ -15,7 +19,7 @@ func GetConfigValueByCode(configCode string) (total int, err error) {
 	return
 }
 
-//修改
+// 修改
 func CrmConfigUpdate(newValue, configCode string) (err error) {
 	o := orm.NewOrm()
 	sql := `UPDATE crm_config SET  config_value=?   WHERE config_code=  ?`

+ 2 - 1
models/db.go

@@ -88,7 +88,8 @@ func initSystem() {
 		new(system.SysUserLoginRecord),
 		new(system.SysSession),
 		new(system.Admin),
-		new(system.AdminConfig), //系统用户配置表
+		new(system.AdminConfig),           //系统用户配置表
+		new(system.AdminVerifyCodeRecord), // 用户短信邮箱验证码记录表
 	)
 }
 

+ 126 - 0
models/system/admin_verify_code_record.go

@@ -0,0 +1,126 @@
+package system
+
+import (
+	"fmt"
+	"github.com/beego/beego/v2/client/orm"
+	"strings"
+	"time"
+)
+
+const (
+	AdminVerifyCodeRecordTypeMobile = 1 // 验证方式-手机号
+	AdminVerifyCodeRecordTypeEmail  = 2 // 验证方式-邮箱
+
+	AdminVerifyCodeRecordSourceLogin    = 1 // 验证码来源-登录
+	AdminVerifyCodeRecordSourceAbnormal = 2 // 验证码来源-异常登录校验
+	AdminVerifyCodeRecordSourceForget   = 3 // 验证码来源-忘记密码
+
+	AdminVerifyCodeRecordStatusSuccess = 1 // 验证码发送状态-已发送
+	AdminVerifyCodeRecordStatusFail    = 2 // 验证码发送状态-发送失败
+)
+
+// AdminVerifyCodeRecord 短信邮箱验证码记录表
+type AdminVerifyCodeRecord struct {
+	Id          int       `orm:"column(id);pk"`
+	VerifyType  int       `description:"验证方式:1-手机号;2-邮箱"`
+	Source      int       `description:"来源:1-登录;2-异常登录校验;3-忘记密码"`
+	Mobile      string    `description:"手机号"`
+	Email       string    `description:"邮箱"`
+	Code        string    `description:"验证码"`
+	ExpiredTime time.Time `description:"验证码过期时间"`
+	SendResult  string    `description:"发送结果"`
+	SendStatus  int       `description:"发送状态:0-待发送;1-已发送;2-发送失败"`
+	CreateTime  time.Time `description:"创建时间"`
+	ModifyTime  time.Time `description:"更新时间"`
+}
+
+func (m *AdminVerifyCodeRecord) TableName() string {
+	return "admin_verify_code_record"
+}
+
+func (m *AdminVerifyCodeRecord) PrimaryId() string {
+	return "id"
+}
+
+func (m *AdminVerifyCodeRecord) Create() (err error) {
+	o := orm.NewOrm()
+	id, err := o.Insert(m)
+	if err != nil {
+		return
+	}
+	m.Id = int(id)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) CreateMulti(items []*AdminVerifyCodeRecord) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	o := orm.NewOrm()
+	_, err = o.InsertMulti(len(items), items)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) Update(cols []string) (err error) {
+	o := orm.NewOrm()
+	_, err = o.Update(m, cols...)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) Del() (err error) {
+	o := orm.NewOrm()
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.PrimaryId())
+	_, err = o.Raw(sql, m.Id).Exec()
+	return
+}
+
+func (m *AdminVerifyCodeRecord) GetItemById(id int) (item *AdminVerifyCodeRecord, err error) {
+	o := orm.NewOrm()
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.PrimaryId())
+	err = o.Raw(sql, id).QueryRow(&item)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) GetItemByCondition(condition string, pars []interface{}) (item *AdminVerifyCodeRecord, err error) {
+	o := orm.NewOrm()
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s LIMIT 1`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&item)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	o := orm.NewOrm()
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = o.Raw(sql, pars).QueryRow(&count)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AdminVerifyCodeRecord, err error) {
+	o := orm.NewOrm()
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY create_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars).QueryRows(&items)
+	return
+}
+
+func (m *AdminVerifyCodeRecord) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*AdminVerifyCodeRecord, err error) {
+	o := orm.NewOrm()
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY create_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	_, err = o.Raw(sql, pars, startSize, pageSize).QueryRows(&items)
+	return
+}

+ 52 - 6
models/system/sys_user.go

@@ -61,6 +61,7 @@ type Admin struct {
 	City                      string    `description:"市"`
 	CityCode                  string    `description:"市编码"`
 	EmployeeId                string    `description:"员工工号(钉钉/每刻报销)"`
+	TelAreaCode               string    `description:"手机区号"`
 }
 
 // Update 更新用户基础信息
@@ -129,12 +130,12 @@ func GetAdminList() (items []*Admin, err error) {
 }
 
 // GetSysUserByMobile 根据手机号获取管理信息
-func GetSysUserByMobile(mobile string) (item *Admin, err error) {
-	sql := `SELECT * FROM admin WHERE mobile = ? LIMIT 1`
-	o := orm.NewOrm()
-	err = o.Raw(sql, mobile).QueryRow(&item)
-	return
-}
+//func GetSysUserByMobile(mobile string) (item *Admin, err error) {
+//	sql := `SELECT * FROM admin WHERE mobile = ? LIMIT 1`
+//	o := orm.NewOrm()
+//	err = o.Raw(sql, mobile).QueryRow(&item)
+//	return
+//}
 
 // 通过用户姓名跟身份获取管理员信息
 func CheckSysUserByName(userName, roleTypeCode string) (item *Admin, err error) {
@@ -202,3 +203,48 @@ func GetAdminListByIdListWithoutEnable(idList []int) (items []*Admin, err error)
 	_, err = o.Raw(sql, idList).QueryRows(&items)
 	return
 }
+
+// GetSysUserByMobile 手机号获取用户
+func GetSysUserByMobile(mobile string) (item *Admin, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT
+				a.*, b.role_type_code
+			FROM
+				admin AS a
+			INNER JOIN sys_role AS b ON a.role_id = b.role_id
+			WHERE
+				a.mobile = ?
+			LIMIT 1`
+	err = o.Raw(sql, mobile).QueryRow(&item)
+	return
+}
+
+// GetSysUserByEmail 邮箱获取用户
+func GetSysUserByEmail(email string) (item *Admin, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT
+				a.*, b.role_type_code
+			FROM
+				admin AS a
+			INNER JOIN sys_role AS b ON a.role_id = b.role_id
+			WHERE
+				a.email = ?
+			LIMIT 1`
+	err = o.Raw(sql, email).QueryRow(&item)
+	return
+}
+
+// GetSysUserByAdminName 账号获取用户
+func GetSysUserByAdminName(adminName string) (item *Admin, err error) {
+	o := orm.NewOrm()
+	sql := `SELECT
+				a.*, b.role_type_code
+			FROM
+				admin AS a
+			INNER JOIN sys_role AS b ON a.role_id = b.role_id
+			WHERE
+				a.admin_name = ?
+			LIMIT 1`
+	err = o.Raw(sql, adminName).QueryRow(&item)
+	return
+}

+ 63 - 0
routers/commentsRouter.go

@@ -1708,4 +1708,67 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "AreaCodeList",
+            Router: `/area_code/list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "ForgetAccountGet",
+            Router: `/forget/account_get`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "ForgetCodeVerify",
+            Router: `/forget/code_verify`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "ForgetResetPass",
+            Router: `/forget/reset_pass`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "GenerateCaptcha",
+            Router: `/get_captcha`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "Login",
+            Router: `/login`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"] = append(beego.GlobalControllerRouter["hongze/hongze_ETA_mobile_api/controllers:UserLoginController"],
+        beego.ControllerComments{
+            Method: "GetVerifyCode",
+            Router: `/verify_code`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
 }

+ 6 - 0
routers/router.go

@@ -146,6 +146,12 @@ func init() {
 				&english_report.EnPermissionController{},
 			),
 		),
+		web.NSNamespace("/user_login",
+			web.NSInclude(
+				&controllers.UserLoginController{},
+				&controllers.UserLoginAuthController{},
+			),
+		),
 	)
 	web.AddNamespace(ns)
 }

+ 48 - 0
services/captcha_redis.go

@@ -0,0 +1,48 @@
+package services
+
+import (
+	"fmt"
+	"hongze/hongze_ETA_mobile_api/utils"
+	"time"
+)
+
+// CaptchaRedis Redis验证器
+type CaptchaRedis struct{}
+
+// Set 实现验证器set方法
+func (r CaptchaRedis) Set(id string, value string) (err error) {
+	if utils.Rc == nil {
+		err = fmt.Errorf("redis config err")
+		return
+	}
+	key := utils.CaptchaCachePrefix + id
+	b := utils.Rc.SetNX(key, value, time.Minute*5)
+	if !b {
+		err = fmt.Errorf("redis setnx err")
+	}
+	return
+}
+
+// Get 实现原验证器get方法
+func (r CaptchaRedis) Get(id string, clear bool) (code string) {
+	if utils.Rc == nil {
+		return
+	}
+	key := utils.CaptchaCachePrefix + id
+	val, err := utils.Rc.RedisString(key)
+	if err != nil {
+		return
+	}
+	code = val
+	// clear为true时验证通过删除验证码
+	if clear {
+		_ = utils.Rc.Delete(key)
+	}
+	return
+}
+
+// Verify 实现原验证器verify方法
+func (r CaptchaRedis) Verify(id, answer string, clear bool) bool {
+	v := r.Get(id, clear)
+	return v == answer
+}

+ 148 - 0
services/sms.go

@@ -0,0 +1,148 @@
+package services
+
+import (
+	"encoding/json"
+	"fmt"
+	"hongze/hongze_ETA_mobile_api/services/alarm_msg"
+	"hongze/hongze_ETA_mobile_api/utils"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+)
+
+// SendSmsCode 发送国内短信
+func SendSmsCode(mobile, vCode, tplId string) (flag bool) {
+	if mobile == "" || vCode == "" || tplId == "" {
+		return
+	}
+
+	var (
+		err error
+		res string
+	)
+	defer func() {
+		if err != nil {
+			tips := fmt.Sprintf("短信验证码发送失败, Err: %s; Result: %s", err.Error(), res)
+			utils.FileLog.Info("%s", tips)
+			go alarm_msg.SendAlarmMsg(tips, 2)
+		}
+	}()
+
+	result, e := sendSms(mobile, tplId, vCode)
+	if e != nil {
+		err = fmt.Errorf("send sms err: %s", e.Error())
+		return
+	}
+	res = string(result)
+
+	var netReturn map[string]interface{}
+	if e = json.Unmarshal(result, &netReturn); e != nil {
+		err = fmt.Errorf("json unmarshal err: %s", e.Error())
+		return
+	}
+	errCode, ok := netReturn["error_code"].(float64)
+	if !ok {
+		err = fmt.Errorf("result code err")
+		return
+	}
+	// 忽略错误的手机号码这种错误
+	if errCode != 0 && errCode != 205401 {
+		err = fmt.Errorf("err code %f", errCode)
+		return
+	}
+	// 发送成功
+	if errCode == 0 {
+		flag = true
+	}
+	return
+}
+
+// sendSms 发送国内短信
+func sendSms(mobile, tplId, code string) (rs []byte, err error) {
+	var Url *url.URL
+	apiURL := "http://v.juhe.cn/sms/send"
+	//初始化参数
+	param := url.Values{}
+	//配置请求参数,方法内部已处理urlencode问题,中文参数可以直接传参
+	param.Set("mobile", mobile) //接受短信的用户手机号码
+	param.Set("tpl_id", tplId)  //您申请的短信模板ID,根据实际情况修改
+	tplVal := fmt.Sprintf(`#code#=%s&#m#=%d`, code, utils.VerifyCodeExpireMinute)
+	param.Set("tpl_value", tplVal)     //您设置的模板变量,根据实际情况
+	param.Set("key", utils.JhGnAppKey) //应用APPKEY(应用详细页查询)
+
+	Url, err = url.Parse(apiURL)
+	if err != nil {
+		fmt.Printf("解析url错误:\r\n%v", err)
+		return nil, err
+	}
+	//如果参数中有中文参数,这个方法会进行URLEncode
+	Url.RawQuery = param.Encode()
+	resp, err := http.Get(Url.String())
+	if err != nil {
+		fmt.Println("err:", err)
+		return nil, err
+	}
+	defer resp.Body.Close()
+	return ioutil.ReadAll(resp.Body)
+}
+
+// SendSmsCodeGj 发送国际短信
+func SendSmsCodeGj(mobile, vCode, areaNum string) bool {
+	flag := false
+	result, err := sendSmsGj(mobile, vCode, areaNum)
+	if err != nil {
+		fmt.Println("发送短信失败")
+		return false
+	}
+	fmt.Println("result", string(result))
+	var netReturn map[string]interface{}
+	err = json.Unmarshal(result, &netReturn)
+	if err != nil {
+		//go SendEmail("短信验证码发送失败", "err:"+err.Error()+" result"+string(result), utils.EmailSendToUsers)
+		go alarm_msg.SendAlarmMsg("短信验证码发送失败, Err:"+err.Error()+";Result:"+string(result), 2)
+		flag = false
+	}
+	if netReturn["error_code"].(float64) == 0 {
+		fmt.Printf("接口返回result字段是:\r\n%v", netReturn["result"])
+		flag = true
+	} else {
+		// 忽略错误的手机号码这种错误
+		if netReturn["error_code"].(float64) != 205401 {
+			go alarm_msg.SendAlarmMsg("短信验证码发送失败, Result:"+string(result), 2)
+		}
+		flag = false
+	}
+	return flag
+}
+
+// sendSmsGj 发送国际短信
+func sendSmsGj(mobile, code, areaNum string) (rs []byte, err error) {
+	var Url *url.URL
+	apiURL := "http://v.juhe.cn/smsInternational/send.php"
+	//初始化参数
+	param := url.Values{}
+	//配置请求参数,方法内部已处理urlencode问题,中文参数可以直接传参
+	param.Set("mobile", mobile)           //接受短信的用户手机号码
+	param.Set("tplId", "10054")           //您申请的短信模板ID,根据实际情况修改
+	param.Set("tplValue", "#code#="+code) //您设置的模板变量,根据实际情况
+	param.Set("key", utils.JhGjAppKey)    //应用APPKEY(应用详细页查询)
+	param.Set("areaNum", areaNum)         //应用APPKEY(应用详细页查询)
+
+	Url, err = url.Parse(apiURL)
+	if err != nil {
+		fmt.Printf("解析url错误:\r\n%v", err)
+		return nil, err
+	}
+	//如果参数中有中文参数,这个方法会进行URLEncode
+	Url.RawQuery = param.Encode()
+	resp, err := http.Get(Url.String())
+	if err != nil {
+		fmt.Println("err:", err)
+		return nil, err
+	}
+	utils.FileLog.Info("sendSmsGj:param:" + Url.String())
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	utils.FileLog.Info("sendSmsGj:result:" + string(body))
+	return body, err
+}

+ 123 - 0
services/user_login.go

@@ -0,0 +1,123 @@
+package services
+
+import (
+	"encoding/json"
+	"fmt"
+	"hongze/hongze_ETA_mobile_api/models"
+	"hongze/hongze_ETA_mobile_api/models/company"
+	"hongze/hongze_ETA_mobile_api/models/system"
+	"hongze/hongze_ETA_mobile_api/utils"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// SendAdminMobileVerifyCode 发送用户手机验证码
+func SendAdminMobileVerifyCode(source int, mobile, areaCode string) (ok bool, err error) {
+	verifyCode := utils.GetRandDigit(6)
+	record := new(system.AdminVerifyCodeRecord)
+	record.VerifyType = system.AdminVerifyCodeRecordTypeMobile
+	record.Mobile = mobile
+	record.Source = source
+	record.Code = verifyCode
+	record.ExpiredTime = time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute)
+	record.CreateTime = time.Now().Local()
+	record.ModifyTime = time.Now().Local()
+	if e := record.Create(); e != nil {
+		err = fmt.Errorf("新增验证码记录失败, Err: %s", e.Error())
+		return
+	}
+
+	tplId := utils.SmsNewLoginTplId
+	if areaCode == "86" {
+		ok = SendSmsCode(mobile, verifyCode, tplId)
+	} else {
+		ok = SendSmsCodeGj(mobile, verifyCode, areaCode)
+	}
+	record.SendStatus = system.AdminVerifyCodeRecordStatusSuccess
+	if !ok {
+		record.SendStatus = system.AdminVerifyCodeRecordStatusFail
+	}
+	cols := []string{"SendStatus"}
+	if e := record.Update(cols); e != nil {
+		err = fmt.Errorf("更新验证码记录失败, Err: %s", e.Error())
+	}
+	return
+}
+
+// SendAdminEmailVerifyCode 发送用户邮箱验证码
+func SendAdminEmailVerifyCode(source int, email string) (ok bool, err error) {
+	verifyCode := utils.GetRandDigit(6)
+	record := new(system.AdminVerifyCodeRecord)
+	record.VerifyType = system.AdminVerifyCodeRecordTypeEmail
+	record.Email = email
+	record.Source = source
+	record.Code = verifyCode
+	record.ExpiredTime = time.Now().Add(utils.VerifyCodeExpireMinute * time.Minute)
+	record.CreateTime = time.Now().Local()
+	record.ModifyTime = time.Now().Local()
+	if e := record.Create(); e != nil {
+		err = fmt.Errorf("新增验证码记录失败, Err: %s", e.Error())
+		return
+	}
+
+	// 获取邮件配置
+	authKey := "english_report_email_conf"
+	emailConf, e := company.GetConfigDetailByCode(authKey)
+	if e != nil {
+		err = fmt.Errorf("获取群发邮件权限失败, Err: %s", e.Error())
+		return
+	}
+	if emailConf.ConfigValue == "" {
+		err = fmt.Errorf("邮件配置为空, 不可推送")
+		return
+	}
+	conf := new(models.EnglishReportEmailConf)
+	if e = json.Unmarshal([]byte(emailConf.ConfigValue), &conf); e != nil {
+		err = fmt.Errorf("邮件配置有误, 不可推送")
+		return
+	}
+
+	// 获取邮箱模板
+	confKey := "admin_verify_code_email_tmp"
+	confTmp, e := company.GetConfigDetailByCode(confKey)
+	if e != nil {
+		err = fmt.Errorf("获取邮件模板失败, Err: %s", e.Error())
+		return
+	}
+	if confTmp.ConfigValue == `` {
+		err = fmt.Errorf("邮件模板为空, 不可推送")
+		return
+	}
+
+	req := new(EnglishReportSendEmailRequest)
+	req.Subject = "弘则研究登录验证"
+	req.Email = email
+	req.FromAlias = conf.FromAlias
+	// 填充模板
+	t := time.Now().Format("2006年01月02日")
+	ct := confTmp.ConfigValue
+	ct = strings.Replace(ct, "{{VERIFY_CODE}}", verifyCode, 1)
+	ct = strings.Replace(ct, "{{EXPIRED_MINUTE}}", strconv.Itoa(utils.VerifyCodeExpireMinute), 1)
+	ct = strings.Replace(ct, "{{DATE_TIME}}", t, 1)
+	req.HtmlBody = ct
+
+	aliEmail := new(AliyunEmail)
+	o, result, e := aliEmail.SendEmail(req)
+	if e != nil {
+		err = fmt.Errorf("邮箱推送失败, Err: %s", e.Error())
+		return
+	}
+	ok = o
+
+	record.SendStatus = system.AdminVerifyCodeRecordStatusSuccess
+	if !ok {
+		record.SendStatus = system.AdminVerifyCodeRecordStatusFail
+	}
+	record.SendResult = result
+	cols := []string{"SendStatus", "SendResult"}
+	if e = record.Update(cols); e != nil {
+		err = fmt.Errorf("更新验证码记录失败, Err: %s", e.Error())
+	}
+	return
+}

+ 33 - 15
utils/constants.go

@@ -48,6 +48,7 @@ const (
 	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管理员
@@ -174,21 +175,24 @@ const (
 
 // 缓存key
 const (
-	CACHE_KEY_LOGS                    = "HZ_ADMIN_CACHE_KEY_LOGS"         //api用户操作日志队列
-	CACHE_KEY_ADMIN                   = "calendar:admin:list"             //系统用户列表缓存key
-	CACHE_KEY_ADMIN_ID                = "calendar:admin:id:list"          //系统用户列表缓存key
-	CACHE_KEY_OLD_REPORT_PUBLISH      = "HZ_CACHE_KEY_OLD_REPORT_PUBLISH" //老后台报告发布队列
-	CACHE_ADMIN_YB_CONFIG             = "admin:yb_config:"                //研报配置相关缓存前缀
-	CACHE_WIND_URL                    = "CACHE_WIND_URL"                  //指标与wind服务器的绑定关系
-	CACHE_CHART_INFO_DATA             = "chart:info:data:"                //图表数据
-	CACHE_CHART_CLASSIFY              = "chart:classify"                  //图表分类数据
-	CACHE_IMPORT_MANUAL_DATA          = "import:manual:data"              //手工数据导入后刷新
-	CACHE_ACCESS_TOKEN_LOGIN          = "pc_admin:login:"                 //管理后台登录
-	CACHE_ACCESS_TOKEN_LOGIN_NO_TRUST = "pc_admin:login:no_trust:"        //管理后台登录(不可信登录态)
-	CACHE_KEY_COMPANY_MATCH_PRE       = "admin:company:match:"            //客户名单匹配
-	CACHE_KEY_MYSTEEL_REFRESH         = "mysteel_chemical:refresh"        //钢联化工刷新
-	CACHE_KEY_DAYNEW_REFRESH          = "admin:day_new:refresh"           //每日资讯拉取企业微信聊天记录
-	CACHE_KEY_DAYNEW_TRANSLATE        = "admin:day_new:translate"         //每日资讯中翻英
+	CACHE_KEY_LOGS                    = "HZ_ADMIN_CACHE_KEY_LOGS"           //api用户操作日志队列
+	CACHE_KEY_ADMIN                   = "calendar:admin:list"               //系统用户列表缓存key
+	CACHE_KEY_ADMIN_ID                = "calendar:admin:id:list"            //系统用户列表缓存key
+	CACHE_KEY_OLD_REPORT_PUBLISH      = "HZ_CACHE_KEY_OLD_REPORT_PUBLISH"   //老后台报告发布队列
+	CACHE_ADMIN_YB_CONFIG             = "admin:yb_config:"                  //研报配置相关缓存前缀
+	CACHE_WIND_URL                    = "CACHE_WIND_URL"                    //指标与wind服务器的绑定关系
+	CACHE_CHART_INFO_DATA             = "chart:info:data:"                  //图表数据
+	CACHE_CHART_CLASSIFY              = "chart:classify"                    //图表分类数据
+	CACHE_IMPORT_MANUAL_DATA          = "import:manual:data"                //手工数据导入后刷新
+	CACHE_ACCESS_TOKEN_LOGIN          = "mobile_eta_admin:login:"           //管理后台登录
+	CACHE_ACCESS_TOKEN_LOGIN_NO_TRUST = "mobile_eta_admin:login:no_trust:"  //管理后台登录(不可信登录态)
+	CACHE_ABNORMAL_LOGIN              = "mobile_eta_admin:login:abnormal:"  //管理后台登录-异常登录
+	CACHE_LOGIN_ERR_PASS              = "mobile_eta_admin:login:errPass:"   //管理后台登录-输入错误密码次数
+	CACHE_FIND_PASS_VERIFY            = "mobile_eta_admin:findPass:verify:" //找回密码校验成功标记
+	CACHE_KEY_COMPANY_MATCH_PRE       = "admin:company:match:"              //客户名单匹配
+	CACHE_KEY_MYSTEEL_REFRESH         = "mysteel_chemical:refresh"          //钢联化工刷新
+	CACHE_KEY_DAYNEW_REFRESH          = "admin:day_new:refresh"             //每日资讯拉取企业微信聊天记录
+	CACHE_KEY_DAYNEW_TRANSLATE        = "admin:day_new:translate"           //每日资讯中翻英
 )
 
 // 模板消息推送类型
@@ -326,3 +330,17 @@ const (
 var FrequencyDaysMap = map[string]int{
 	"天": 1, "周": 7, "月": 30, "季": 90, "年": 365,
 }
+
+// 验证码
+const (
+	CaptchaCachePrefix     = "captcha:lock:crm_" // 验证码缓存Key
+	VerifyCodeExpireMinute = 15                  // 短信/邮箱验证码过期时间-分钟
+	SmsLoginTplId          = "65692"             // 【弘则研究】您的验证码是XXX,如非本人操作,请忽略本短信
+	SmsNewLoginTplId       = "254663"            // 【弘则研究】您的验证码是XXX,有效期15分钟
+)
+
+// 聚合短信
+var (
+	JhGnAppKey = "4c8504c49dd335e99cfd7b6a3a9e2415" //聚合国内AppKey
+	JhGjAppKey = "3326ad2c1047a4cd92ace153e6044ca3" //聚合国内AppKey
+)