package users import ( "crypto/aes" "crypto/cipher" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "net/http" "net/url" "os" "path/filepath" "recook/internal/back" "recook/internal/cache" "recook/internal/dbc" . "recook/internal/dbc" "recook/internal/define" "recook/internal/model/user" service "recook/internal/service/app" "recook/internal/static_path" "recook/internal/v2/model/company" "recook/tools" "github.com/gin-gonic/gin" "github.com/go-redis/redis" "github.com/golangkit/formatime" "github.com/jinzhu/gorm" ) type weChatLogin struct { WxType string `json:"wxType"` Code string `json:"code" validate:"required"` } type mobileLogin struct { Mobile string `json:"mobile" validate:"required,len=11"` SMS string `json:"sms" validate:"required,len=4"` UnionID string `json:"unionid"` } type autoLoginParam struct { UserId uint `json:"userId"` } type unionIDLoginParam struct { UnionID string `json:"unionid"` } type BasicAuth struct { ID uint `json:"id" form:"id" validate:"required"` Token string `json:"token" form:"token" validate:"len=32"` } type rtnPack struct { Auth BasicAuth `json:"auth"` Info *DetailInfo `json:"info"` Status int `json:"status"` } type DetailInfo struct { user.Information UserLevel int8 `json:"userLevel"` RoleLevel int `json:"roleLevel"` IsVerified bool `json:"isVerified"` RealInfoStatus bool `json:"realInfoStatus"` IsSetPayPwd bool `json:"isSetPayPwd"` TeacherWechatNo string `gorm:"column:teacher_wechat_no;size:30" json:"teacherWechatNo"` } func (r *DetailInfo) ReflectedBy(u *user.Information, v bool) *DetailInfo { // todo 修改user加字段 或者放缓存 var userWallet user.Wallet DB.Where("user_id = ?", u.ID).First(&userWallet) if len(userWallet.Password) > 0 { r.IsSetPayPwd = true } if u.RealInfoStatus == user.RealInfoPass { r.RealInfoStatus = true } r.ID = u.ID r.Nickname = u.Nickname r.HeadImgUrl = u.HeadImgUrl r.WxUnionId = u.WxUnionId r.Mobile = u.Mobile r.Gender = u.Gender r.Birthday = u.Birthday r.InvitationNo = u.InvitationNo r.CreatedAt = u.CreatedAt //r.Role = u.Role r.IsVerified = v r.Phone = u.Phone r.WechatNo = u.WechatNo r.RealName = u.RealName r.IDCard = u.IDCard return r } type weChatAccess struct { AccessToken string `json:"access_token" validate:"required"` Openid string `json:"openid" validate:"required"` ErrMsg string `json:"errmsg"` } type weChatUserInfo struct { OpenID string `json:"openid" validate:"required"` Nickname string `json:"nickname" validate:"required,min=1"` UnionId string `json:"unionId" validate:"required,min=28"` Sex uint `json:"sex" validate:"required"` HeadImgUrl string `json:"headImgUrl" validate:"required"` } type miniLoginParam struct { AuthCode string `json:"auth_code" validator:"required"` AvatarUrl string `json:"avatar_url" validator:"required"` Gender uint `json:"gender" validator:"required"` Nickname string `json:"nickname" validator:"required"` } type openidResp struct { OpenID string `json:"openid" validate:"required"` UnionId string `json:"unionId" validate:"required"` SessionKey string `json:"session_key"` ErrMsg string `json:"errmsg"` } const WxMiniUserOpenIDUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=%v&secret=%v&js_code=%v&grant_type=authorization_code" const WxUnionIDURL = "https://api.weixin.qq.com/sns/userinfo?access_token=%v&openid=%v" const WxOauth2URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%v&secret=%v&code=%v&grant_type=authorization_code" // LoginByWeChat 微信登陆 step1 func LoginByWeChat(c *gin.Context) { var p weChatLogin err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } // 1.获取access_token var access weChatAccess { url := "" if p.WxType == "" { url = fmt.Sprintf(WxOauth2URL, define.WxAppID, define.WxAppSecret, p.Code) } else { wxConfig := define.WxConfig[p.WxType] if wxConfig.Type == "wxapp" { url = fmt.Sprintf(WxMiniUserOpenIDUrl, wxConfig.AppId, wxConfig.AppSecret, p.Code) } else { url = fmt.Sprintf(WxOauth2URL, wxConfig.AppId, wxConfig.AppSecret, p.Code) } } response1, err := http.Get(url) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = response1.Body.Close() }() body1, err := ioutil.ReadAll(response1.Body) if err != nil { log.Println(string(body1)) back.Err(c, err.Error()) return } err = json.Unmarshal(body1, &access) if err != nil { back.Err(c, err.Error()) return } if len(access.ErrMsg) > 0 { back.Err(c, access.ErrMsg) return } } /* 2.拿到了微信的openid后 再去请求uinonid 依据unionid判断用户是否存在 */ var userInfo weChatUserInfo { response2, err := http.Get(fmt.Sprintf(WxUnionIDURL, access.AccessToken, access.Openid)) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = response2.Body.Close() }() body2, err := ioutil.ReadAll(response2.Body) if err != nil { back.Err(c, err.Error()) return } err = json.Unmarshal(body2, &userInfo) if err != nil { back.Err(c, err.Error()) return } log.Println(string(body2)) marshal, _ := json.Marshal(access) log.Println(string(body2)) log.Println(string(marshal)) if userInfo.UnionId == "" { back.Err(c, "获取UnionId失败") return } } // 之后开始做用户是否绑定的验证,绑定了手机了, 就直接登陆 // 未绑定,需要绑定手机 var info user.Information err = DB.Where(user.Information{ WxUnionId: userInfo.UnionId, }).Last(&info).Error if err != nil && !gorm.IsRecordNotFoundError(err) { back.Err(c, err.Error()) return } // 在数据库中查找是否存在用户, 且已经有手机号和邀请码, 直接登陆 if info.ID > 0 && len(info.InvitationNo) > 0 && len(info.Mobile) > 0 { // todo 感觉可以不确定wxAppOpenId if len(info.WxAppOpenId) == 0 { err = DB.Model(&info).Update(user.Information{WxMiniOpenId: access.Openid}).Error if err != nil { back.Err(c, err.Error()) return } } login(c, &info) return } { // 在缓存中查找是否已经之前导入过 var ur = service.NewUserWithRedis() _, err := ur.GetUserInRedis(info.WxUnionId) // 不为nil的err,直接返回。 if err != nil && err != redis.Nil { back.Err(c, "内部错误602"+err.Error()) return } // 未在redis中找到数据----》用户不存在 则获取授权信息 var headImgPath string // 下载头像 { if userInfo.HeadImgUrl != "" { headResp, err := http.Get(userInfo.HeadImgUrl) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = headResp.Body.Close() }() hash := tools.MD5(userInfo.HeadImgUrl) imgName := fmt.Sprintf("%v.jpg", hash) headImgPath = filepath.Join(static_path.Dir.Photo, imgName) dst := filepath.Join(static_path.Dir.Root, headImgPath) out, err := os.Create(dst) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = out.Close() }() _, err = io.Copy(out, headResp.Body) if err != nil { back.Err(c, err.Error()) return } } else { headImgPath = "/default/officaillogo-1.png" } } // 不存在用户设置信息 info = user.Information{ Nickname: userInfo.Nickname, HeadImgUrl: headImgPath, WxUnionId: userInfo.UnionId, WxAppOpenId: userInfo.OpenID, Gender: userInfo.Sex, InvitationNo: "", Birthday: formatime.NewSecondFrom(define.DefaultBirthday), } // redis放入wx信息 ok, err := ur.SetUserInRedis(info.WxUnionId, info, 0) if ok != "OK" || err != nil { back.Err(c, "内部错误601"+err.Error()) return } // 将wx信息放入缓存中后,返回需要填写手机号码 back.Suc(c, "请填写手机号码", gin.H{ "status": 0, //0 -> 填写 手机号 1 -> 完整用户信息 "bindingMobile": 0, "info": gin.H{ "nickname": info.Nickname, "wxUnionId": userInfo.UnionId, }, }) } } // LoginByMiniProgram 通过小程序登录 func LoginByMiniProgram(c *gin.Context) { var p miniLoginParam if err := tools.ParseParams(&p, c); err != nil { back.Fail(c, err.Error()) return } response, err := http.Get(fmt.Sprintf(WxMiniUserOpenIDUrl, define.WxMiniProgramAppID, define.WxMiniProgramAppSecret, p.AuthCode)) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = response.Body.Close() }() body, err := ioutil.ReadAll(response.Body) if err != nil { back.Fail(c, err.Error()) return } var resp openidResp if err = json.Unmarshal(body, &resp); err != nil { back.Fail(c, err.Error()) return } if len(resp.ErrMsg) > 0 { back.Fail(c, "微信授权失败"+resp.ErrMsg) return } if len(resp.UnionId) == 0 { back.Fail(c, "不可用:请等待小程序客户端升级") return } // 获取到openid 判断数据库是否有 var info user.Information err = DB.Where(user.Information{ WxUnionId: resp.UnionId, }).First(&info).Error if err != nil && !gorm.IsRecordNotFoundError(err) { back.Fail(c, err.Error()) return } if info.ID == 0 { // 用户不存在 则获取授权信息 var headImgPath string // 下载头像 { headResp, err := http.Get(p.AvatarUrl) if err != nil { back.Fail(c, err.Error()) return } defer func() { _ = headResp.Body.Close() }() hash := tools.MD5(p.AvatarUrl) imgName := fmt.Sprintf("%v.jpg", hash) headImgPath = filepath.Join(static_path.Dir.Photo, imgName) dst := filepath.Join(static_path.Dir.Root, headImgPath) out, err := os.Create(dst) if err != nil { back.Err(c, err.Error()) return } defer func() { _ = out.Close() }() _, err = io.Copy(out, headResp.Body) if err != nil { back.Err(c, err.Error()) return } } info = user.Information{ Nickname: p.Nickname, HeadImgUrl: headImgPath, WxUnionId: resp.UnionId, WxMiniOpenId: resp.OpenID, Gender: p.Gender, InvitationNo: "", Birthday: formatime.NewSecondFrom(define.DefaultBirthday), } err = DB.Create(&info).Error if err != nil { back.Err(c, err.Error()) } else { back.Suc(c, "请填写邀请码", gin.H{ "status": 0, "info": gin.H{ "id": info.ID, "nickname": info.Nickname, }, }) } } else { if len(info.InvitationNo) > 0 { if len(info.Mobile) > 0 { if len(info.WxMiniOpenId) == 0 { err = DB.Model(&info).Update(user.Information{WxMiniOpenId: resp.OpenID}).Error if err != nil { back.Err(c, err.Error()) return } } // 小程序登录时 直接返回鉴权令牌 var realInfo user.RealInfo DB.First(&realInfo, "user_id = ?", info.ID) isVerified := false if realInfo.ID > 0 { isVerified = true } deviceType := cache.GetDeviceType(c) var login user.Login DB.First(&login, "user_id=? and device_type = ?", info.ID, deviceType) if info.ID == 0 { login = user.Login{ Token: tools.Token(), IP: c.ClientIP(), LoginTime: formatime.NewSecondNow(), DeviceType: cache.GetDeviceType(c), UserID: info.ID, } err := DB.Create(&login).Error if err != nil { back.Err(c, err.Error()) return } } cache.SetUserLoginCache(&login) back.Suc(c, "登录成功", &rtnPack{ BasicAuth{ ID: login.ID, Token: login.Token, }, (&DetailInfo{}).ReflectedBy(&info, isVerified), 1, }) } else { back.Suc(c, "请填写手机号码", gin.H{ "status": 1, "bindingMobile": 0, "info": gin.H{ "id": info.ID, "nickname": info.Nickname, }, }) } } else { back.Suc(c, "请填写邀请码", gin.H{ "status": 0, "info": gin.H{ "id": info.ID, "nickname": info.Nickname, }, }) } } } // LoginByMobile 通过手机号登录,如果没有推荐码,就跳转到注册方法 func LoginByMobile(c *gin.Context) { var p mobileLogin err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, "参数错误:"+err.Error()) return } // 留的登陆后门, 在密码在redis中 // redis中设置5分钟更新一次验证码 pwd, _ := Rds.Get(cache.SuperKey).Result() if pwd != p.SMS { // 这里是正常用户的业务逻辑 v := cache.GetUserSMSCode(p.Mobile) if v != p.SMS { back.Fail(c, "验证码错误") return } } var info user.Information err = DB.First(&info, "mobile=?", p.Mobile).Error if err != nil { if gorm.IsRecordNotFoundError(err) { back.Suc(c, "请填写邀请码", gin.H{ "status": 0, }) // 新用户 没有填写邀请码 } else { back.Err(c, "用户不存在:"+err.Error()) } //只要没有数据,就去插入用户,写一个插入用户的方法,指定手机号的,并调用login方法,用来返回数据 } else { if len(p.UnionID) > 0 { DB.Model(&info).Where("id=?", info.ID).Update("wx_union_id", p.UnionID) DB.First(&info, "mobile=?", p.Mobile) } login(c, &info) } } type ArgsLoginInfo struct { Name string `json:"name"` Password string `json:"password"` } func LoginByName(c *gin.Context) { var p ArgsLoginInfo err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } var obj company.Info if err := dbc.DB.First(&obj, "shop_name= ?", p.Name).Error; err != nil { back.Fail(c, err.Error()) return } if obj.State != 2 { err := errors.New("未通过审核") back.Fail(c, err.Error()) return } if obj.Status != 1 { err := errors.New("被禁用") back.Fail(c, err.Error()) return } if obj.Password != p.Password { err := errors.New("密码不正确") back.Fail(c, err.Error()) return } var info user.Information err = DB.First(&info, "id = ?", obj.ID).Error if err != nil { back.Err(c, "用户不存在:"+err.Error()) } else { login(c, &info) } } // AutoLogin 自动登录 func AutoLogin(c *gin.Context) { var p autoLoginParam err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } if p.UserId > 0 { var l = user.Login{ID: p.UserId} err = DB.Model(&l).Updates(user.Login{LoginTime: formatime.NewSecondNow()}).Error if err != nil { back.Fail(c, err.Error()) return } } back.Suc(c, "", nil) } // UnionID 通过unionID登录 func UnionID(c *gin.Context) { var p unionIDLoginParam err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } var info user.Information err = DB.First(&info, "wx_union_id=?", p.UnionID).Error if err != nil { back.Err(c, "用户不存在:"+err.Error()) } else { login(c, &info) } } //上面的几种方式调用的 func login(c *gin.Context, info *user.Information) { deviceType := cache.GetDeviceType(c) // 老用户 当主动退出登录或更换手机号码后 再次登录需要重置 var login user.Login DB.First(&login, "user_id=? and device_type = ?", info.ID, deviceType) token := tools.Token() if login.ID > 0 { err := DB.Model(&login).Updates(user.Login{ Token: token, IP: c.ClientIP(), LoginTime: formatime.NewSecondNow(), }).Error if err != nil { back.Err(c, err.Error()) return } } else { login = user.Login{ Token: token, IP: c.ClientIP(), LoginTime: formatime.NewSecondNow(), DeviceType: cache.GetDeviceType(c), UserID: info.ID, } err := DB.Create(&login).Error if err != nil { back.Err(c, err.Error()) return } } cache.SetUserLoginCache(&login) var realInfo user.RealInfo DB.First(&realInfo, "user_id = ?", info.ID) isVerified := false if realInfo.ID > 0 { isVerified = true } if info.Level == 0 && info.ParentID != 0 { if err := DB.Table(info.TableName()).Where("id=?", info.ID).Update("level", 1).Error; err != nil { back.Fail(c, err.Error()) } } //追加app登录状态 if err := DB.Table(info.TableName()).Where("id=?", info.ID).Update("login_app", 1).Error; err != nil { back.Fail(c, err.Error()) } back.Suc(c, "登录成功", &rtnPack{ BasicAuth{ ID: login.ID, Token: login.Token, }, (&DetailInfo{}).ReflectedBy(info, isVerified), 1, }) } func Aes128Decrypt(crypted, key []byte, IV []byte) ([]byte, error) { if key == nil || len(key) != 16 { return nil, nil } if IV != nil && len(IV) != 16 { return nil, nil } block, err := aes.NewCipher(key) if err != nil { return nil, err } blockSize := block.BlockSize() blockMode := cipher.NewCBCDecrypter(block, IV[:blockSize]) origData := make([]byte, len(crypted)) blockMode.CryptBlocks(origData, crypted) origData = PKCS7UnPadding(origData) return origData, nil } func PKCS7UnPadding(origData []byte) []byte { length := len(origData) unpadding := int(origData[length-1]) return origData[:(length - unpadding)] } const ( AppIDH5 = "wx0a67d8af4a8252b0" AppSecretH5 = "de4494be68c27fb306448f46718cbd13" APIKeyH5 = "83f8932eb742257316e3168ba9e920dk" MchIDH5 = "1545449631" ) // H5GetCode h5获取code func H5GetCode(c *gin.Context) { type req struct { RedirectUrl string `json:"redirectUrl"` } var p req err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } url := fmt.Sprintf("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect", AppIDH5, url.QueryEscape(p.RedirectUrl)) back.Suc(c, "操作成功", gin.H{ "url": url, }) return } type Response struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` RefreshToken string `json:"refresh_token"` OpenId string `json:"openid"` Scope string `json:"scope"` Errcode int `json:"errcode"` Errmsg string `json:"errmsg"` } // H5GetOpenId h5获取openid func H5GetOpenId(c *gin.Context) { type req struct { Code string `json:"code" validate:"required"` } var p req err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } url1 := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", AppIDH5, AppSecretH5, p.Code) resp, err := http.Get(url1) if err != nil { panic(err) } defer resp.Body.Close() respBody, err := ioutil.ReadAll(resp.Body) if err != nil { back.Fail(c, err.Error()) return } var respStruct Response //iponid的结构体 err = json.Unmarshal(respBody, &respStruct) unionid := GetUser(respStruct) back.Suc(c, "", gin.H{ "unionid": unionid, }) return } //获取unionID type wxUserInfo struct { UnionID string `json:"unionid"` } // GetUser 获取用户信息 func GetUser(respStruct Response) string { url := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN", respStruct.AccessToken, respStruct.OpenId) resp, err := http.Get(url) if err != nil { //panic(err) //log.Println(err) } defer resp.Body.Close() respBody, err := ioutil.ReadAll(resp.Body) if err != nil { } var user_struct wxUserInfo //iponid的结构体 err = json.Unmarshal(respBody, &user_struct) return user_struct.UnionID } type getTokenByToken struct { Uid uint `json:"uid"` } // GetTokenByUid 通过uid获取token func GetTokenByUid(c *gin.Context) { var p getTokenByToken err := tools.ParseParams(&p, c) if err != nil { back.Fail(c, err.Error()) return } var userLogin user.Login DB.First(&userLogin, "user_id=?", p.Uid) back.Suc(c, "", &gin.H{ "token": userLogin.Token, }) }