feat: 短信验证码功能已完成,手机端同步实现。

This commit is contained in:
RockYang 2023-07-03 15:18:15 +08:00
parent ce0267e25b
commit a49d54d66c
13 changed files with 267 additions and 26 deletions

View File

@ -184,7 +184,7 @@ func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
}
session := sessions.Default(c)
var value interface{}
if strings.Contains(c.Request.URL.Path, "/api/admin/") {
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
value = session.Get(types.SessionAdmin)
} else {
value = session.Get(types.SessionUser)

View File

@ -62,8 +62,6 @@ func (h *UserHandler) Register(c *gin.Context) {
if err != nil || int(code.(float64)) != data.Code {
resp.ERROR(c, "短信验证码错误")
return
} else {
_ = h.levelDB.Delete(key) // 删除短信验证码
}
// check if the username is exists
@ -117,6 +115,7 @@ func (h *UserHandler) Register(c *gin.Context) {
return
}
_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码
resp.SUCCESS(c, user)
}
@ -230,6 +229,7 @@ type userProfile struct {
Id uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
ChatConfig types.ChatConfig `json:"chat_config"`
Calls int `json:"calls"`
@ -334,3 +334,46 @@ func (h *UserHandler) Password(c *gin.Context) {
resp.SUCCESS(c)
}
// BindMobile 绑定手机号
func (h *UserHandler) BindMobile(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Code int `json:"code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 检查手机号是否被其他账号绑定
var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
if res.Error == nil {
resp.ERROR(c, "该手机号已经被其他账号绑定")
return
}
// 检查验证码
key := CodeStorePrefix + data.Mobile
code, err := h.levelDB.Get(key)
if err != nil || int(code.(float64)) != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
_ = h.levelDB.Delete(key) // 删除短信验证码
resp.SUCCESS(c)
}

View File

@ -129,6 +129,7 @@ func main() {
group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate)
group.POST("password", h.Password)
group.POST("bind/mobile", h.BindMobile)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/")

View File

@ -2,24 +2,28 @@
<el-dialog
v-model="showDialog"
:close-on-click-modal="false"
:show-close="false"
:show-close="mobile !== ''"
:before-close="close"
:title="title"
>
<div class="form" id="password-form">
<div class="form" id="bind-mobile-form">
<el-alert v-if="mobile !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
<p>当前用户已绑定手机号{{ mobile }}, 绑定其他手机号之后自动解绑该手机号</p>
</el-alert>
<el-form :model="form" label-width="120px">
<el-form-item label="手机号码">
<el-input v-model="form.mobile" type="password"/>
<el-input v-model="form.mobile"/>
</el-form-item>
<el-form-item label="手机验证码">
<el-row :gutter="10">
<el-col :span="12">
<el-input v-model="form.code" type="password"/>
<el-input v-model.number="form.code" maxlength="6"/>
</el-col>
<el-col :span="12">
<send-msg size="large" v-model:mobile="form.mobile"/>
<send-msg size="" :mobile="form.mobile"/>
</el-col>
</el-row>
</el-form-item>
</el-form>
</div>
@ -35,31 +39,51 @@
</template>
<script setup>
import {ref} from "vue";
import {checkSession} from "@/action/session";
import {computed, ref} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {validateMobile} from "@/utils/validate";
const props = defineProps({
show: Boolean,
mobile: String
});
const showDialog = computed(() => {
return props.show
})
const title = ref('绑定手机号')
const showDialog = ref(false)
const form = ref({
mobile: '',
code: ''
})
checkSession().then(user => {
if (user.mobile === '') {
showDialog.value = true
}
}).catch(e => {
console.log(e.message)
})
const emits = defineEmits(['hide']);
const save = () => {
if (!validateMobile(form.value.mobile)) {
return ElMessage.error({message: "请输入正确的手机号码", appendTo: "#bind-mobile-form"});
}
if (form.value.code === '') {
return ElMessage.error({message: "请输入短信验证码", appendTo: "#bind-mobile-form"});
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
ElMessage.success({
message: '绑定成功',
appendTo: '#bind-mobile-form',
duration: 1000,
onClose: () => emits('hide', false)
})
}).catch(e => {
ElMessage.error({message: "绑定失败:" + e.message, appendTo: "#bind-mobile-form"});
})
}
const sendMsg = () => {
const close = function () {
emits('hide', false);
}
</script>

View File

@ -28,6 +28,10 @@
<el-input v-model="form.username" readonly disabled/>
</el-form-item>
<el-form-item label="绑定手机号">
<el-input v-model="form.mobile" readonly disabled/>
</el-form-item>
<el-form-item label="聊天上下文">
<el-switch v-model="form.chat_config.enable_context"/>
</el-form-item>
@ -96,6 +100,7 @@ const form = ref({
username: '',
nickname: '',
avatar: '',
mobile: '',
calls: 0,
tokens: 0,
chat_configs: {}

View File

@ -55,6 +55,7 @@ const save = function () {
ElMessage.success({
message: '更新成功',
appendTo: '#password-form',
duration: 1000,
onClose: () => emits('logout', false)
})
}).catch((e) => {

View File

@ -1,5 +1,5 @@
<template>
<el-button type="primary" class="sms-btn" :disabled="!canSend" :size="size" @click="sendMsg" plain>{{
<el-button type="primary" :disabled="!canSend" :size="props.size" @click="sendMsg" plain>{{
btnText
}}
</el-button>

View File

@ -0,0 +1,76 @@
<template>
<van-dialog v-model:show="showDialog"
:title="title"
:show-cancel-button="mobile !== ''"
@confirm="save"
@cancel="close">
<van-cell-group inset>
<van-field
v-model="form.mobile"
label="手机号"
placeholder="请输入手机号"
/>
<van-field
v-model.number="form.code"
center
clearable
label="短信验证码"
placeholder="请输入短信验证码"
>
<template #button>
<!-- <van-button size="small" type="primary">发送验证码</van-button>-->
<send-msg size="small" :mobile="form.mobile"/>
</template>
</van-field>
</van-cell-group>
</van-dialog>
</template>
<script setup>
import {computed, ref} from "vue";
import SendMsg from "@/components/mobile/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {validateMobile} from "@/utils/validate";
import {showNotify} from "vant";
const props = defineProps({
show: Boolean,
mobile: String
});
const showDialog = computed(() => {
return props.show
})
const title = ref('绑定手机号')
const form = ref({
mobile: '',
code: ''
})
const emits = defineEmits(['hide']);
const save = () => {
if (!validateMobile(form.value.mobile)) {
return showNotify({type: 'danger', message: '请输入正确的手机号码'});
}
if (form.value.code === '') {
return showNotify({type: "danger", message: '请输入短信验证码'})
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
showNotify({type: 'success', message: '绑定成功', duration: 1000, onClose: emits('hide', false)});
}).catch(e => {
showNotify({type: 'danger', message: '绑定失败:' + e.message, duration: 2000});
})
}
const close = function () {
emits('hide', false);
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,62 @@
<template>
<van-button size="small"
type="primary"
:disabled="!canSend"
:size="props.size"
@click="sendMsg">{{ btnText }}
</van-button>
</template>
<script setup>
//
import {ref} from "vue";
import {validateMobile} from "@/utils/validate";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {showNotify} from "vant";
const props = defineProps({
mobile: String,
size: String,
});
const btnText = ref('发送验证码')
const canSend = ref(true)
const sendMsg = () => {
if (!canSend.value) {
return
}
if (!validateMobile(props.mobile)) {
return showNotify({type: 'danger', message: '请输入合法的手机号'})
}
canSend.value = false
httpGet('/api/verify/token').then(res => {
httpPost('/api/verify/sms', {token: res.data, mobile: props.mobile}).then(() => {
showNotify({type: 'success', message: '短信发送成功'})
let time = 120
btnText.value = time
const handler = setInterval(() => {
time = time - 1
if (time <= 0) {
clearInterval(handler)
btnText.value = '重新发送'
canSend.value = true
} else {
btnText.value = time
}
}, 1000)
}).catch(e => {
canSend.value = true
showNotify({type: 'danger', message: '短信发送失败:' + e.message})
})
}).catch(e => {
console.log('failed to fetch token: ' + e.message)
})
}
</script>
<style scoped>
</style>

View File

@ -60,6 +60,13 @@
<span>修改密码</span>
</el-dropdown-item>
<el-dropdown-item @click="showBindMobileDialog = true">
<el-icon>
<Iphone/>
</el-icon>
<span>绑定手机号</span>
</el-dropdown-item>
<el-dropdown-item @click="clearAllChats">
<el-icon>
<Delete/>
@ -188,7 +195,8 @@
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
@logout="logout"/>
<bind-mobile/>
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="loginUser.mobile"
@hide="showBindMobileDialog = false"/>
</div>
@ -202,7 +210,7 @@ import {
Check,
Close,
Delete,
Edit,
Edit, Iphone,
Plus,
Promotion,
RefreshRight,
@ -242,6 +250,7 @@ const newChatItem = ref(null);
const router = useRouter();
const showConfigDialog = ref(false);
const showPasswordDialog = ref(false);
const showBindMobileDialog = ref(false);
const isLogin = ref(false)
if (isMobile()) {
@ -253,6 +262,9 @@ onMounted(() => {
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
if (user.mobile === '') {
showBindMobileDialog.value = true
}
//
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
roles.value = res.data;

View File

@ -9,7 +9,7 @@
<div class="header">{{ title }}</div>
<div class="content">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="rules">
<el-form :model="formData" label-width="120px" ref="formRef">
<div class="block">
<el-input placeholder="请输入用户名(4-30位)"
size="large" maxlength="30"
@ -115,7 +115,7 @@ const title = ref('ChatGPT-PLUS 用户注册');
const formData = ref({
username: '',
password: '',
mobile: '18575670125',
mobile: '',
code: '',
repass: '',
})

View File

@ -63,6 +63,9 @@
</template>
</van-picker>
</van-popup>
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="loginUser.mobile"
@hide="showBindMobileDialog = false"/>
</div>
</template>
@ -74,6 +77,7 @@ import {checkSession} from "@/action/session";
import router from "@/router";
import {setChatConfig} from "@/store/chat";
import {removeArrayItem} from "@/utils/libs";
import BindMobile from "@/components/mobile/BindMobile.vue";
const title = ref("会话列表")
const chatName = ref("")
@ -88,10 +92,14 @@ const roles = ref([])
const models = ref([])
const showPicker = ref(false)
const columns = ref([roles.value, models.value])
const showBindMobileDialog = ref(false)
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
if (user.mobile === '') {
showBindMobileDialog.value = true
}
//
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
if (res.data) {

View File

@ -13,6 +13,14 @@
disabled
placeholder="用户名"
/>
<van-field
v-model="form.mobile"
name="手机号"
label="手机号"
readonly
disabled
placeholder="手机号"
/>
<van-field
v-model="form.nickname"
name="昵称"
@ -61,6 +69,7 @@ const title = ref('用户设置')
const form = ref({
username: '',
nickname: '',
mobile: '',
avatar: '',
calls: 0,
tokens: 0