feat(user): 添加用户信息修改功能及对应验证码校验
- 在管理员页面新增修改用户信息表单,支持姓名、手机号、密码修改 - 实现验证码发送倒计时与发送状态管理 - 新增接口支持用户信息更新,包含密码和手机号校验 - 后端校验验证码有效性,编码密码后更新用户信息 - 修改用户信息后强制登出,确保安全性 - 优化登录状态判断,登出后跳转至登录页 - 取消部分日志打印,调整发送验证码缓存过期时间为5分钟
This commit is contained in:
@@ -1,16 +1,22 @@
|
|||||||
package com.yinlihupo.enlish.service.controller;
|
package com.yinlihupo.enlish.service.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
import com.yinlihupo.enlish.service.model.vo.user.FindUserInfoRspVO;
|
||||||
|
import com.yinlihupo.enlish.service.model.vo.user.UpdateUserInfoReqVO;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
|
import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
|
||||||
import com.yinlihupo.framework.common.response.Response;
|
import com.yinlihupo.framework.common.response.Response;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user/")
|
@RequestMapping("/user/")
|
||||||
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
@@ -27,4 +33,23 @@ public class UserController {
|
|||||||
return Response.success(findUserInfoRspVO);
|
return Response.success(findUserInfoRspVO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("update-user-info")
|
||||||
|
@ApiOperationLog(description = "修改密码")
|
||||||
|
public Response<String> updatePassword(@RequestBody UpdateUserInfoReqVO updateUserInfoReqVO) {
|
||||||
|
try {
|
||||||
|
String code = updateUserInfoReqVO.getCode();
|
||||||
|
String newPassword = updateUserInfoReqVO.getNewPassword();
|
||||||
|
String phone = updateUserInfoReqVO.getPhone();
|
||||||
|
String name = updateUserInfoReqVO.getName();
|
||||||
|
userService.updateUserInfo(newPassword, code, phone, name);
|
||||||
|
|
||||||
|
StpUtil.logout();
|
||||||
|
|
||||||
|
return Response.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("修改密码失败 {}", e.getMessage());
|
||||||
|
return Response.fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public interface UserDOMapper {
|
|||||||
|
|
||||||
void insert(UserDO userDO);
|
void insert(UserDO userDO);
|
||||||
|
|
||||||
|
void updatePassword(@Param("id") Long id, @Param("password") String password);
|
||||||
|
|
||||||
|
void updateUserInfo(@Param("id") Long id, @Param("name") String name, @Param("password") String password, @Param("phone") String phone);
|
||||||
|
|
||||||
UserDO selectById(Long id);
|
UserDO selectById(Long id);
|
||||||
|
|
||||||
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
List<UserDO> selectUserDOList(@Param("name") String name, @Param("startIndex") Integer startIndex, @Param("pageSize") Integer pageSize);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.yinlihupo.enlish.service.model.vo.user;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@Data
|
||||||
|
public class UpdateUserInfoReqVO {
|
||||||
|
|
||||||
|
private String newPassword;
|
||||||
|
private String name;
|
||||||
|
private String phone;
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
@@ -13,4 +13,6 @@ public interface UserService {
|
|||||||
Integer findUserTotal();
|
Integer findUserTotal();
|
||||||
|
|
||||||
void createUser(UserDO userDO);
|
void createUser(UserDO userDO);
|
||||||
|
|
||||||
|
void updateUserInfo(String password, String reqCode, String phone, String name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -83,7 +84,9 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
@Override
|
@Override
|
||||||
public void sendVerificationCode(String phone) {
|
public void sendVerificationCode(String phone) {
|
||||||
String code = RandomUtil.randomNumbers(6);
|
String code = RandomUtil.randomNumbers(6);
|
||||||
redisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
|
String key = UserRedisConstants.buildUserLoginCode(phone);
|
||||||
|
redisTemplate.opsForValue().set(key, code);
|
||||||
|
redisTemplate.expire(key, 5, TimeUnit.MINUTES);
|
||||||
String signName = "短信测试";
|
String signName = "短信测试";
|
||||||
String templateCode = "SMS_154950909";
|
String templateCode = "SMS_154950909";
|
||||||
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
String templateParam = String.format("{\"code\":\"%s\"}", code);
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
package com.yinlihupo.enlish.service.service.user;
|
package com.yinlihupo.enlish.service.service.user;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
|
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
|
||||||
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
|
||||||
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
import com.yinlihupo.enlish.service.domain.mapper.UserDOMapper;
|
||||||
import com.yinlihupo.enlish.service.service.UserService;
|
import com.yinlihupo.enlish.service.service.UserService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -17,8 +21,10 @@ public class UserServiceImpl implements UserService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserDOMapper userDOMapper;
|
private UserDOMapper userDOMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
@Resource
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDO findUser() {
|
public UserDO findUser() {
|
||||||
@@ -43,4 +49,21 @@ public class UserServiceImpl implements UserService {
|
|||||||
userDOMapper.insert(userDO);
|
userDOMapper.insert(userDO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUserInfo(String password, String reqCode, String phone, String name) {
|
||||||
|
long id = Integer.parseInt(String.valueOf(StpUtil.getLoginId()));
|
||||||
|
UserDO userDO = userDOMapper.selectById(id);
|
||||||
|
|
||||||
|
String key = UserRedisConstants.buildUserLoginCode(userDO.getPhone());
|
||||||
|
String code = Objects.requireNonNull(redisTemplate.opsForValue().get(key)).toString();
|
||||||
|
if (code == null || !code.equals(reqCode)) {
|
||||||
|
throw new RuntimeException("验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password != null) {
|
||||||
|
password = passwordEncoder.encode(password);
|
||||||
|
}
|
||||||
|
userDOMapper.updateUserInfo(id, name, password, phone);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ sa-token:
|
|||||||
is-share: true
|
is-share: true
|
||||||
# 是否输出操作日志
|
# 是否输出操作日志
|
||||||
is-log: true
|
is-log: true
|
||||||
logging:
|
#logging:
|
||||||
level:
|
# level:
|
||||||
com.yinlihupo.enlish.service.domain.mapper: debug
|
# com.yinlihupo.enlish.service.domain.mapper: debug
|
||||||
@@ -20,6 +20,28 @@
|
|||||||
values (#{phone}, #{name}, #{password})
|
values (#{phone}, #{name}, #{password})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePassword">
|
||||||
|
update user
|
||||||
|
set password = #{password}
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateUserInfo">
|
||||||
|
update user
|
||||||
|
<set>
|
||||||
|
<if test="password != null">
|
||||||
|
password = #{password},
|
||||||
|
</if>
|
||||||
|
<if test="name != null">
|
||||||
|
`name` = #{name},
|
||||||
|
</if>
|
||||||
|
<if test="phone != null">
|
||||||
|
phone = #{phone},
|
||||||
|
</if>
|
||||||
|
</set>
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
<select id="selectByPhone" resultMap="BaseResultMap">
|
<select id="selectByPhone" resultMap="BaseResultMap">
|
||||||
select *
|
select *
|
||||||
from user
|
from user
|
||||||
|
|||||||
@@ -15,3 +15,7 @@ export function getVerificationCode(data) {
|
|||||||
export function getUserInfo() {
|
export function getUserInfo() {
|
||||||
return axios.post("/user/info")
|
return axios.post("/user/info")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserInfo(data) {
|
||||||
|
return axios.post("/user/update-user-info", data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,9 +83,14 @@ async function refreshUser() {
|
|||||||
try {
|
try {
|
||||||
const r = await getUserInfo()
|
const r = await getUserInfo()
|
||||||
const d = r?.data
|
const d = r?.data
|
||||||
userName.value = d?.success ? (d?.data?.name || '') : ''
|
console.log("header" + d.success)
|
||||||
|
if (d?.success) {
|
||||||
|
userName.value = d?.data?.name || ''
|
||||||
|
} else {
|
||||||
|
handleLogout()
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
userName.value = ''
|
handleLogout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
@@ -96,7 +101,7 @@ async function handleLogout() {
|
|||||||
userName.value = ''
|
userName.value = ''
|
||||||
menuOpen.value = false
|
menuOpen.value = false
|
||||||
showMessage('已退出登录', 'success')
|
showMessage('已退出登录', 'success')
|
||||||
router.push('/')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function onDocClick(e) {
|
function onDocClick(e) {
|
||||||
|
|||||||
@@ -48,6 +48,40 @@
|
|||||||
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
<el-alert type="success" :closable="false" :title="`邀请码:${inviteCode}`" show-icon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-shell p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-lg font-semibold">修改用户信息</span>
|
||||||
|
</div>
|
||||||
|
<el-form :model="pwForm" :rules="pwRules" ref="pwFormRef" label-width="120px">
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="pwForm.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input v-model="pwForm.newPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
|
<el-input v-model="pwForm.confirmPassword" type="password" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="pwForm.phone" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="验证码" prop="code">
|
||||||
|
<el-input v-model="pwForm.code">
|
||||||
|
<template #append>
|
||||||
|
<el-button :disabled="codeCountdown > 0 || codeSending || !pwForm.phone"
|
||||||
|
@click="sendCode">
|
||||||
|
{{ codeCountdown > 0 ? `${codeCountdown}s` : '获取验证码' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="pwLoading" @click="submitPw">修改用户信息</el-button>
|
||||||
|
<el-button class="ml-2" @click="resetPw">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
<el-dialog v-model="createVisible" title="新增用户" width="420px">
|
||||||
@@ -76,8 +110,9 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Header from '@/layouts/components/Header.vue'
|
import Header from '@/layouts/components/Header.vue'
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
import { getUserList, createUser, createInvitationCode } from '@/api/admin'
|
||||||
|
import { updateUserInfo, getVerificationCode } from '@/api/user'
|
||||||
import { showMessage } from '@/composables/util.js'
|
import { showMessage } from '@/composables/util.js'
|
||||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||||
|
|
||||||
@@ -203,4 +238,109 @@ function submitInvite() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pwForm = reactive({ name: '', phone: '', newPassword: '', confirmPassword: '', code: '' })
|
||||||
|
const pwFormRef = ref()
|
||||||
|
const pwLoading = ref(false)
|
||||||
|
const pwRules = {
|
||||||
|
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||||
|
newPassword: [],
|
||||||
|
confirmPassword: [
|
||||||
|
{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (pwForm.newPassword) {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请确认密码'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value !== pwForm.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPw() {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
codeCountdown.value = 0
|
||||||
|
codeSending.value = false
|
||||||
|
pwForm.name = ''
|
||||||
|
pwForm.phone = ''
|
||||||
|
pwForm.newPassword = ''
|
||||||
|
pwForm.confirmPassword = ''
|
||||||
|
pwForm.code = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeCountdown = ref(0)
|
||||||
|
const codeSending = ref(false)
|
||||||
|
let codeTimer = null
|
||||||
|
|
||||||
|
async function sendCode() {
|
||||||
|
if (!pwForm.phone) {
|
||||||
|
showMessage('请输入手机号', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (codeSending.value || codeCountdown.value > 0) return
|
||||||
|
codeSending.value = true
|
||||||
|
try {
|
||||||
|
const r = await getVerificationCode({ phone: pwForm.phone })
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('验证码已发送', 'success')
|
||||||
|
codeCountdown.value = 60
|
||||||
|
codeTimer = setInterval(() => {
|
||||||
|
if (codeCountdown.value > 0) {
|
||||||
|
codeCountdown.value -= 1
|
||||||
|
} else {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '发送验证码失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
codeSending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (codeTimer) {
|
||||||
|
clearInterval(codeTimer)
|
||||||
|
codeTimer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function submitPw() {
|
||||||
|
pwFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
pwLoading.value = true
|
||||||
|
try {
|
||||||
|
const r = await updateUserInfo({
|
||||||
|
newPassword: pwForm.newPassword || '',
|
||||||
|
name: pwForm.name || '',
|
||||||
|
phone: pwForm.phone,
|
||||||
|
code: pwForm.code
|
||||||
|
})
|
||||||
|
const d = r?.data
|
||||||
|
if (d?.success) {
|
||||||
|
showMessage('用户信息修改成功', 'success')
|
||||||
|
resetPw()
|
||||||
|
} else {
|
||||||
|
showMessage(d?.message || '用户信息修改失败', 'error')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pwLoading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user