mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 17:13:54 +08:00
Merge pull request #1314 from xinnan-tech/web-forget-password
增加找回密码的功能
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
package xiaozhi.common.exception;
|
package xiaozhi.common.exception;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.apache.shiro.authz.UnauthorizedException;
|
import org.apache.shiro.authz.UnauthorizedException;
|
||||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
@@ -13,8 +16,6 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
/**
|
/**
|
||||||
* 异常处理器
|
* 异常处理器
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -67,7 +68,6 @@ public class RenExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||||
String errorMsg = allErrors.stream()
|
String errorMsg = allErrors.stream()
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
package xiaozhi.modules.sys;
|
package xiaozhi.modules.sys;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.modules.security.controller.LoginController;
|
import xiaozhi.modules.security.controller.LoginController;
|
||||||
import xiaozhi.modules.security.dto.LoginDTO;
|
import xiaozhi.modules.security.dto.LoginDTO;
|
||||||
|
import xiaozhi.modules.security.dto.SmsVerificationDTO;
|
||||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||||
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
@ActiveProfiles("dev")
|
@ActiveProfiles("dev")
|
||||||
@@ -28,23 +29,27 @@ class loginControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSmsVerification(){
|
public void testSmsVerification() {
|
||||||
try {
|
try {
|
||||||
loginController.smsVerification("手机号码");
|
SmsVerificationDTO smsVerificationDTO = new SmsVerificationDTO();
|
||||||
}catch (Exception e){
|
smsVerificationDTO.setPhone("手机号码");
|
||||||
|
smsVerificationDTO.setCaptchaId("123456");
|
||||||
|
smsVerificationDTO.setCaptcha("123456");
|
||||||
|
loginController.smsVerification(smsVerificationDTO);
|
||||||
|
} catch (Exception e) {
|
||||||
System.out.println(e.getMessage());
|
System.out.println(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRetrievePassword(){
|
public void testRetrievePassword() {
|
||||||
try {
|
try {
|
||||||
RetrievePasswordDTO retrievePasswordDTO = new RetrievePasswordDTO();
|
RetrievePasswordDTO retrievePasswordDTO = new RetrievePasswordDTO();
|
||||||
retrievePasswordDTO.setCode("123456");
|
retrievePasswordDTO.setCode("123456");
|
||||||
retrievePasswordDTO.setPhone("手机号码");
|
retrievePasswordDTO.setPhone("手机号码");
|
||||||
retrievePasswordDTO.setPassword("密码");
|
retrievePasswordDTO.setPassword("密码");
|
||||||
loginController.retrievePassword(retrievePasswordDTO);
|
loginController.retrievePassword(retrievePasswordDTO);
|
||||||
}catch (Exception e){
|
} catch (Exception e) {
|
||||||
System.out.println(e.getMessage());
|
System.out.println(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,4 +169,28 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
// 找回用户密码
|
||||||
|
retrievePassword(passwordData, callback, failCallback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/user/retrieve-password`)
|
||||||
|
.method('PUT')
|
||||||
|
.data({
|
||||||
|
phone: passwordData.phone,
|
||||||
|
code: passwordData.code,
|
||||||
|
password: passwordData.password
|
||||||
|
})
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
failCallback(err);
|
||||||
|
})
|
||||||
|
.networkFail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.retrievePassword(passwordData, callback, failCallback);
|
||||||
|
});
|
||||||
|
}).send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ const routes = [
|
|||||||
return import('../views/register.vue')
|
return import('../views/register.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/retrieve-password',
|
||||||
|
name: 'RetrievePassword',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/retrievePassword.vue')
|
||||||
|
}
|
||||||
|
},
|
||||||
// 设备管理页面路由
|
// 设备管理页面路由
|
||||||
{
|
{
|
||||||
path: '/device-management',
|
path: '/device-management',
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
<div
|
<div
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||||
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||||
|
<div style="cursor: pointer;" @click="goToForgetPassword">忘记密码?</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-btn" @click="login">登录</div>
|
<div class="login-btn" @click="login">登录</div>
|
||||||
@@ -214,6 +215,9 @@ export default {
|
|||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
goToPage('/register')
|
||||||
},
|
},
|
||||||
|
goToForgetPassword() {
|
||||||
|
goToPage('/retrieve-password')
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome" @keyup.enter="retrievePassword">
|
||||||
|
<el-container style="height: 100%;">
|
||||||
|
<!-- 保持相同的头部 -->
|
||||||
|
<el-header>
|
||||||
|
<div style="display: flex;align-items: center;margin-top: 15px;margin-left: 10px;gap: 10px;">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" style="width: 45px;height: 45px;" />
|
||||||
|
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" style="height: 18px;" />
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div class="login-person">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/login/register-person.png" style="width: 100%;" />
|
||||||
|
</div>
|
||||||
|
<el-main style="position: relative;">
|
||||||
|
<form @submit.prevent="retrievePassword">
|
||||||
|
<div class="login-box">
|
||||||
|
<!-- 修改标题部分 -->
|
||||||
|
<div style="display: flex;align-items: center;gap: 20px;margin-bottom: 39px;padding: 0 30px;">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/login/hi.png" style="width: 34px;height: 34px;" />
|
||||||
|
<div class="login-text">重置密码</div>
|
||||||
|
<div class="login-welcome">
|
||||||
|
PASSWORD RETRIEVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 0 30px;">
|
||||||
|
<!-- 手机号输入 -->
|
||||||
|
<div class="input-box">
|
||||||
|
<div style="display: flex; align-items: center; width: 100%;">
|
||||||
|
<el-select v-model="form.areaCode" style="width: 220px; margin-right: 10px;">
|
||||||
|
<el-option v-for="item in mobileAreaList" :key="item.key"
|
||||||
|
:label="`${item.name} (${item.key})`" :value="item.key" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="form.mobile" placeholder="请输入手机号码" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/shield.png" />
|
||||||
|
<el-input v-model="form.captcha" placeholder="请输入验证码" style="flex: 1;" />
|
||||||
|
</div>
|
||||||
|
<img loading="lazy" v-if="captchaUrl" :src="captchaUrl" alt="验证码"
|
||||||
|
style="width: 150px; height: 40px; cursor: pointer;" @click="fetchCaptcha" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手机验证码 -->
|
||||||
|
<div style="display: flex; align-items: center; margin-top: 20px; width: 100%; gap: 10px;">
|
||||||
|
<div class="input-box" style="width: calc(100% - 130px); margin-top: 0;">
|
||||||
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/phone.png" />
|
||||||
|
<el-input v-model="form.mobileCaptcha" placeholder="请输入手机验证码" style="flex: 1;" maxlength="6" />
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" class="send-captcha-btn" :disabled="!canSendMobileCaptcha"
|
||||||
|
@click="sendMobileCaptcha">
|
||||||
|
<span>
|
||||||
|
{{ countdown > 0 ? `${countdown}秒后重试` : '发送验证码' }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新密码 -->
|
||||||
|
<div class="input-box">
|
||||||
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
|
<el-input v-model="form.newPassword" placeholder="请输入新密码" type="password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 确认新密码 -->
|
||||||
|
<div class="input-box">
|
||||||
|
<img loading="lazy" alt="" class="input-icon" src="@/assets/login/password.png" />
|
||||||
|
<el-input v-model="form.confirmPassword" placeholder="请确认新密码" type="password" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改底部链接 -->
|
||||||
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;margin-top: 20px;">
|
||||||
|
<div style="cursor: pointer;" @click="goToLogin">返回登录</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改按钮文本 -->
|
||||||
|
<div class="login-btn" @click="retrievePassword">立即修改</div>
|
||||||
|
|
||||||
|
<!-- 保持相同的协议声明 -->
|
||||||
|
<div style="font-size: 14px;color: #979db1;">
|
||||||
|
同意
|
||||||
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《用户协议》</div>
|
||||||
|
和
|
||||||
|
<div style="display: inline-block;color: #5778FF;cursor: pointer;">《隐私政策》</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</el-main>
|
||||||
|
|
||||||
|
<!-- 保持相同的页脚 -->
|
||||||
|
<el-footer>
|
||||||
|
<version-footer />
|
||||||
|
</el-footer>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
|
import { getUUID, goToPage, showDanger, showSuccess, validateMobile } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'retrieve',
|
||||||
|
components: {
|
||||||
|
VersionFooter
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister,
|
||||||
|
mobileAreaList: state => state.pubConfig.mobileAreaList
|
||||||
|
}),
|
||||||
|
canSendMobileCaptcha() {
|
||||||
|
return this.countdown === 0 && validateMobile(this.form.mobile, this.form.areaCode);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
areaCode: '+86',
|
||||||
|
mobile: '',
|
||||||
|
captcha: '',
|
||||||
|
captchaId: '',
|
||||||
|
smsCode: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
},
|
||||||
|
captchaUrl: '',
|
||||||
|
countdown: 0,
|
||||||
|
timer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchCaptcha();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 复用验证码获取方法
|
||||||
|
fetchCaptcha() {
|
||||||
|
this.form.captchaId = getUUID();
|
||||||
|
Api.user.getCaptcha(this.form.captchaId, (res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
const blob = new Blob([res.data], { type: res.data.type });
|
||||||
|
this.captchaUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('验证码加载异常:', error);
|
||||||
|
showDanger('验证码加载失败,点击刷新');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 封装输入验证逻辑
|
||||||
|
validateInput(input, message) {
|
||||||
|
if (!input.trim()) {
|
||||||
|
showDanger(message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 发送手机验证码
|
||||||
|
sendMobileCaptcha() {
|
||||||
|
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||||
|
showDanger('请输入正确的手机号码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证图形验证码
|
||||||
|
if (!this.validateInput(this.form.captcha, '请输入图形验证码')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除可能存在的旧定时器
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
this.countdown = 60;
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
if (this.countdown > 0) {
|
||||||
|
this.countdown--;
|
||||||
|
} else {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 调用发送验证码接口
|
||||||
|
Api.user.sendSmsVerification({
|
||||||
|
phone: this.form.areaCode + this.form.mobile,
|
||||||
|
captcha: this.form.captcha,
|
||||||
|
captchaId: this.form.captchaId
|
||||||
|
}, (res) => {
|
||||||
|
showSuccess('验证码发送成功');
|
||||||
|
}, (err) => {
|
||||||
|
showDanger(err.data.msg || '验证码发送失败');
|
||||||
|
this.countdown = 0;
|
||||||
|
this.fetchCaptcha();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改逻辑
|
||||||
|
retrievePassword() {
|
||||||
|
// 验证逻辑
|
||||||
|
if (!validateMobile(this.form.mobile, this.form.areaCode)) {
|
||||||
|
showDanger('请输入正确的手机号码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.form.captcha) {
|
||||||
|
showDanger('请输入图形验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.form.mobileCaptcha) {
|
||||||
|
showDanger('请输入短信验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.form.newPassword !== this.form.confirmPassword) {
|
||||||
|
showDanger('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.user.resetPassword({
|
||||||
|
phone: this.form.areaCode + this.form.mobile,
|
||||||
|
newPassword: this.form.newPassword,
|
||||||
|
smsCode: this.form.mobileCaptcha,
|
||||||
|
captcha: this.form.captcha,
|
||||||
|
captchaId: this.form.captchaId
|
||||||
|
}, (res) => {
|
||||||
|
showSuccess('密码重置成功');
|
||||||
|
goToPage('/login');
|
||||||
|
}, (err) => {
|
||||||
|
showDanger(err.data.msg || '重置失败');
|
||||||
|
this.fetchCaptcha();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
goToLogin() {
|
||||||
|
goToPage('/login')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import './auth.scss';
|
||||||
|
|
||||||
|
.send-captcha-btn {
|
||||||
|
margin-right: -5px;
|
||||||
|
min-width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: rgb(87, 120, 255);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #c0c4cc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user