diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index ac9cc240..de96c80e 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -329,8 +329,10 @@ func (h *UserHandler) CLogin(c *gin.Context) { // CLoginCallback 第三方登录回调 func (h *UserHandler) CLoginCallback(c *gin.Context) { - loginType := h.GetTrim(c, "login_type") - code := h.GetTrim(c, "code") + loginType := c.Query("login_type") + code := c.Query("code") + userId := h.GetInt(c, "user_id", 0) + action := c.Query("action") var res types.BizVo apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL) @@ -355,11 +357,34 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) { // login successfully data := res.Data.(map[string]interface{}) - session := gin.H{} var user model.User - tx := h.DB.Debug().Where("openid", data["openid"]).First(&user) - if tx.Error != nil { // user not exist, create new user - // 检测最大注册人数 + if action == "bind" && userId > 0 { + err = h.DB.Where("openid", data["openid"]).First(&user).Error + if err == nil { + resp.ERROR(c, "该微信已经绑定其他账号,请先解绑") + return + } + + err = h.DB.Where("id", userId).First(&user).Error + if err != nil { + resp.ERROR(c, "绑定用户不存在") + return + } + + err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error + if err != nil { + resp.ERROR(c, "更新用户信息失败,"+err.Error()) + return + } + + resp.SUCCESS(c, gin.H{"token": ""}) + return + } + + session := gin.H{} + tx := h.DB.Where("openid", data["openid"]).First(&user) + if tx.Error != nil { + // create new user var totalUser int64 h.DB.Model(&model.User{}).Count(&totalUser) if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum { @@ -534,7 +559,7 @@ func (h *UserHandler) UpdatePass(c *gin.Context) { resp.SUCCESS(c) } -// ResetPass 重置密码 +// ResetPass 找回密码 func (h *UserHandler) ResetPass(c *gin.Context) { var data struct { Username string `json:"username"` @@ -572,11 +597,11 @@ func (h *UserHandler) ResetPass(c *gin.Context) { } } -// BindUsername 重置账号 -func (h *UserHandler) BindUsername(c *gin.Context) { +// BindMobile 绑定手机号 +func (h *UserHandler) BindMobile(c *gin.Context) { var data struct { - Username string `json:"username"` - Code string `json:"code"` + Mobile string `json:"mobile"` + Code string `json:"code"` } if err := c.ShouldBindJSON(&data); err != nil { resp.ERROR(c, types.InvalidArgs) @@ -584,7 +609,7 @@ func (h *UserHandler) BindUsername(c *gin.Context) { } // 检查验证码 - key := CodeStorePrefix + data.Username + key := CodeStorePrefix + data.Mobile code, err := h.redis.Get(c, key).Result() if err != nil || code != data.Code { resp.ERROR(c, "验证码错误") @@ -593,19 +618,54 @@ func (h *UserHandler) BindUsername(c *gin.Context) { // 检查手机号是否被其他账号绑定 var item model.User - res := h.DB.Where("username = ?", data.Username).First(&item) + res := h.DB.Where("mobile", data.Mobile).First(&item) if res.Error == nil { - resp.ERROR(c, "该账号已经被其他账号绑定") + resp.ERROR(c, "该手机号已经绑定了其他账号,请更换手机号") return } - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } + userId := h.GetLoginUserId(c) - err = h.DB.Model(&user).UpdateColumn("username", data.Username).Error + err = h.DB.Model(&item).Where("id", userId).UpdateColumn("mobile", data.Mobile).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + _ = h.redis.Del(c, key) // 删除短信验证码 + resp.SUCCESS(c) +} + +// BindEmail 绑定邮箱 +func (h *UserHandler) BindEmail(c *gin.Context) { + var data struct { + Email string `json:"email"` + Code string `json:"code"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + // 检查验证码 + key := CodeStorePrefix + data.Email + code, err := h.redis.Get(c, key).Result() + if err != nil || code != data.Code { + resp.ERROR(c, "验证码错误") + return + } + + // 检查手机号是否被其他账号绑定 + var item model.User + res := h.DB.Where("email", data.Email).First(&item) + if res.Error == nil { + resp.ERROR(c, "该邮箱地址已经绑定了其他账号,请更邮箱地址") + return + } + + userId := h.GetLoginUserId(c) + + err = h.DB.Model(&item).Where("id", userId).UpdateColumn("email", data.Email).Error if err != nil { resp.ERROR(c, err.Error()) return diff --git a/api/main.go b/api/main.go index ead2032b..58a428be 100644 --- a/api/main.go +++ b/api/main.go @@ -231,7 +231,8 @@ func main() { group.GET("profile", h.Profile) group.POST("profile/update", h.ProfileUpdate) group.POST("password", h.UpdatePass) - group.POST("bind/username", h.BindUsername) + group.POST("bind/mobile", h.BindMobile) + group.POST("bind/email", h.BindEmail) group.POST("resetPass", h.ResetPass) group.GET("clogin", h.CLogin) group.GET("clogin/callback", h.CLoginCallback) diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index 2c72d0fd..586dc701 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1723434190230') format('woff2'), - url('iconfont.woff?t=1723434190230') format('woff'), - url('iconfont.ttf?t=1723434190230') format('truetype'); + src: url('iconfont.woff2?t=1723593727785') format('woff2'), + url('iconfont.woff?t=1723593727785') format('woff'), + url('iconfont.ttf?t=1723593727785') format('truetype'); } .iconfont { @@ -13,6 +13,14 @@ -moz-osx-font-smoothing: grayscale; } +.icon-email:before { + content: "\e670"; +} + +.icon-mobile:before { + content: "\e79a"; +} + .icon-drag:before { content: "\e8ec"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index 8d38c060..886819cf 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index 5fbfb566..0d177ae5 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,20 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "15838472", + "name": "email", + "font_class": "email", + "unicode": "e670", + "unicode_decimal": 58992 + }, + { + "icon_id": "6151052", + "name": "mobile-alt", + "font_class": "mobile", + "unicode": "e79a", + "unicode_decimal": 59290 + }, { "icon_id": "15617554", "name": "drag", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 887cbd6a..6e879bd8 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index 79f8c16d..ef68b85f 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index ab7aa757..f33f132c 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/components/BindEmail.vue b/web/src/components/BindEmail.vue new file mode 100644 index 00000000..1ae9e773 --- /dev/null +++ b/web/src/components/BindEmail.vue @@ -0,0 +1,108 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/BindMobile.vue b/web/src/components/BindMobile.vue index 9fa4d54e..41eceb3f 100644 --- a/web/src/components/BindMobile.vue +++ b/web/src/components/BindMobile.vue @@ -2,24 +2,24 @@
-
当前已绑手机号:{{ mobile }}
+
当前已绑手机号:{{ mobile }}
- + - + - - + + @@ -37,12 +37,11 @@ diff --git a/web/src/components/LoginDialog.vue b/web/src/components/LoginDialog.vue index c04da964..9c9de180 100644 --- a/web/src/components/LoginDialog.vue +++ b/web/src/components/LoginDialog.vue @@ -66,7 +66,7 @@ @@ -249,6 +249,8 @@ import {arrayContains} from "@/utils/libs"; import {getSystemInfo} from "@/store/cache"; import Captcha from "@/components/Captcha.vue"; import ResetPass from "@/components/ResetPass.vue"; +import {setRoute} from "@/store/system"; +import {useRouter} from "vue-router"; // eslint-disable-next-line no-undef const props = defineProps({ @@ -282,9 +284,10 @@ const emits = defineEmits(['hide', 'success']); const action = ref("login") const enableVerify = ref(false) const showResetPass = ref(false) +const router = useRouter() onMounted(() => { - const returnURL = `${location.protocol}//${location.host}/login/callback` + const returnURL = `${location.protocol}//${location.host}/login/callback?action=login` httpGet("/api/user/clogin?return_url="+returnURL).then(res => { wechatLoginURL.value = res.data.url }).catch(e => { diff --git a/web/src/components/RedeemVerify.vue b/web/src/components/RedeemVerify.vue index 4b173239..a11e4ebe 100644 --- a/web/src/components/RedeemVerify.vue +++ b/web/src/components/RedeemVerify.vue @@ -2,7 +2,6 @@ { canSend.value = false httpPost('/api/sms/code', {receiver: props.receiver, key: data.key, dots: data.dots, x:data.x}).then(() => { showMessageOK('验证码发送成功') - let time = 120 + let time = 60 btnText.value = time const handler = setInterval(() => { time = time - 1 diff --git a/web/src/components/ThirdLogin.vue b/web/src/components/ThirdLogin.vue new file mode 100644 index 00000000..cbfe9a9d --- /dev/null +++ b/web/src/components/ThirdLogin.vue @@ -0,0 +1,93 @@ + + + + + \ No newline at end of file diff --git a/web/src/store/system.js b/web/src/store/system.js index 6b6eee80..2c6c15ae 100644 --- a/web/src/store/system.js +++ b/web/src/store/system.js @@ -6,6 +6,7 @@ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import Storage from "good-storage"; +import {useRouter} from "vue-router"; const MOBILE_THEME = process.env.VUE_APP_KEY_PREFIX + "MOBILE_THEME" const ADMIN_THEME = process.env.VUE_APP_KEY_PREFIX + "ADMIN_THEME" @@ -62,4 +63,12 @@ export function FormatFileSize(bytes) { const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +export function setRoute(path) { + Storage.set(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_',path) +} + +export function getRoute() { + return Storage.get(process.env.VUE_APP_KEY_PREFIX + 'ROUTE_') } \ No newline at end of file diff --git a/web/src/views/Login.vue b/web/src/views/Login.vue index 6545274a..fea058da 100644 --- a/web/src/views/Login.vue +++ b/web/src/views/Login.vue @@ -114,7 +114,7 @@ onMounted(() => { }).catch(() => { }) - const returnURL = `${location.protocol}//${location.host}/login/callback` + const returnURL = `${location.protocol}//${location.host}/login/callback?action=login` httpGet("/api/user/clogin?return_url="+returnURL).then(res => { wechatLoginURL.value = res.data.url }).catch(e => { diff --git a/web/src/views/LoginCallback.vue b/web/src/views/LoginCallback.vue index d6d4573b..3d089a26 100644 --- a/web/src/views/LoginCallback.vue +++ b/web/src/views/LoginCallback.vue @@ -39,9 +39,10 @@ import {useRouter} from "vue-router" import {ElMessage, ElMessageBox} from "element-plus"; import {httpGet} from "@/utils/http"; import {setUserToken} from "@/store/session"; -import {isMobile} from "@/utils/libs"; import Clipboard from "clipboard"; import {showMessageError, showMessageOK} from "@/utils/dialog"; +import {getRoute} from "@/store/system"; +import {checkSession, removeUserInfo} from "@/store/cache"; const winHeight = ref(window.innerHeight) const loading = ref(true) @@ -52,12 +53,25 @@ const password = ref('') const code = router.currentRoute.value.query.code +const action = router.currentRoute.value.query.action if (code === "") { ElMessage.error({message: "登录失败:code 参数不能为空",duration: 2000, onClose: () => router.push("/")}) } else { + checkSession().then(user => { + // bind user + doLogin(user.id) + }).catch(() => { + doLogin(0) + }) +} + +const doLogin = (userId) => { // 发送请求获取用户信息 - httpGet("/api/user/clogin/callback",{login_type: "wx",code: code}).then(res => { - setUserToken(res.data.token) + httpGet("/api/user/clogin/callback",{login_type: "wx",code: code, action:action, user_id: userId}).then(res => { + removeUserInfo() + if (res.data.token) { + setUserToken(res.data.token) + } if (res.data.username) { username.value = res.data.username password.value = res.data.password @@ -96,11 +110,7 @@ onUnmounted(() => { const finishLogin = () => { show.value = false - if (isMobile()) { - router.push('/mobile') - } else { - router.push('/chat') - } + router.push(getRoute()) } diff --git a/web/src/views/Member.vue b/web/src/views/Member.vue index 3a1475f4..fcf359f9 100644 --- a/web/src/views/Member.vue +++ b/web/src/views/Member.vue @@ -7,13 +7,19 @@ - 修改密码 + 绑定邮箱 绑定手机 + + 第三方登录 + + + 修改密码 + - 兑换码核销 + 卡密兑换 @@ -95,6 +101,9 @@ + + +