完成”AI零代码应用生成平台“的用户模块扩展
扩展思路 里提到了一些可以继续完善的功能,这里给出一套完整实现,包含:
- 已逻辑删除账号可重新注册
- 管理员编辑用户信息
- 用户个人中心 / 个人设置
- 基于路由 meta 的全局权限管理
下面分别讲解。
1、已逻辑删除账号可重新注册
问题分析
当前用户表中,userAccount 有唯一索引:
▼sql复制代码UNIQUE KEY uk_userAccount (userAccount)
虽然用户删除是逻辑删除,但数据库唯一索引仍然会占用原账号,所以会出现一种情况:
- 某用户被逻辑删除
- 前端再次用原账号注册
- 业务层查询可能查不到有效用户
- 但数据库插入时依然会因为唯一索引冲突而失败
所以,不能只在注册时“忽略逻辑删除数据”,还要想办法 释放唯一键占用 。
实现方案
最稳妥的做法有 2 步:
- 删除用户前,先把该用户原本的
userAccount改写成一个带后缀的唯一值 - 注册用户时,再兜底处理历史已删除但未释放账号的脏数据
1)Mapper 层补充方法
在 UserMapper 中增加 2 个方法:
▼java复制代码public interface UserMapper extends BaseMapper<User> { /** * 查询已逻辑删除的同账号用户 * * @param userAccount 账号 * @return 用户 */ User selectDeletedByUserAccount(@Param("userAccount") String userAccount); /** * 改写已删除用户账号,释放唯一键占用 * * @param id id * @param userAccount 新账号 * @return 影响行数 */ int updateUserAccountById(@Param("id") Long id, @Param("userAccount") String userAccount); }
对应的 UserMapper.xml:
▼xml复制代码<select id="selectDeletedByUserAccount" resultType="com.iceblyte.aicodemother.model.entity.User"> select id, userAccount, userPassword, userName, userAvatar, userProfile, userRole, editTime, createTime, updateTime, isDelete from user where userAccount = #{userAccount} and isDelete = 1 limit 1 </select> <update id="updateUserAccountById"> update user set userAccount = #{userAccount} where id = #{id} </update>
2)注册时兜底处理历史脏数据
修改 UserServiceImpl.userRegister:
▼java复制代码@Override public long userRegister(String userAccount, String userPassword, String checkPassword) { // 1. 校验 if (StrUtil.hasBlank(userAccount, userPassword, checkPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空"); } if (userAccount.length() < 4) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号过短"); } if (userPassword.length() < 8 || checkPassword.length() < 8) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码过短"); } if (!userPassword.equals(checkPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "两次输入的密码不一致"); } // 2. 检查是否重复 QueryWrapper queryWrapper = QueryWrapper.create().eq("userAccount", userAccount); long count = this.mapper.selectCountByQuery(queryWrapper); if (count > 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号重复"); } // 兼容历史逻辑删除数据仍占用唯一键的情况 User deletedUser = this.mapper.selectDeletedByUserAccount(userAccount); if (deletedUser != null) { renameDeletedUserAccount(deletedUser); } // 3. 加密 String encryptPassword = getEncryptPassword(userPassword); // 4. 插入数据 User user = new User(); user.setUserAccount(userAccount); user.setUserPassword(encryptPassword); user.setUserName("无名"); user.setUserRole(UserRoleEnum.USER.getValue()); boolean saveResult = this.save(user); if (!saveResult) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误"); } return user.getId(); }
补充一个私有方法:
▼java复制代码private void renameDeletedUserAccount(User user) { String deletedUserAccount = String.format("%s_deleted_%s", user.getUserAccount(), IdUtil.getSnowflakeNextIdStr()); int updateRows = this.mapper.updateUserAccountById(user.getId(), deletedUserAccount); if (updateRows <= 0) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "释放已删除账号失败"); } }
3)删除用户时释放账号
仅在注册时兜底还不够,最好在管理员删除用户时就释放账号占用。修改 UserController.deleteUser:
▼java复制代码@PostMapping("/delete") @AuthCheck(mustRole = UserConstant.ADMIN_ROLE) public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest) { if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User user = userService.getById(deleteRequest.getId()); ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR); user.setUserAccount(String.format("%s_deleted_%d", user.getUserAccount(), user.getId())); boolean updateResult = userService.updateById(user); ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "释放账号失败"); boolean b = userService.removeById(deleteRequest.getId()); return ResultUtils.success(b); }
这样处理后:
- 正常存在的账号仍然不能重复注册
- 已逻辑删除账号可以重新注册
- 历史脏数据也能被兼容处理
2、个人中心 / 个人设置
需求:让当前登录用户可以修改自己的基础信息,比如昵称、头像、简介。
注意,这类接口和管理员更新任意用户资料的接口不同,必须限制为 只能修改当前登录用户本人 。
1)新增请求类
在 model.dto.user 下新建 UserUpdateMyRequest:
▼java复制代码@Data public class UserUpdateMyRequest implements Serializable { /** * 用户昵称 */ private String userName; /** * 用户头像 */ private String userAvatar; /** * 简介 */ private String userProfile; private static final long serialVersionUID = 1L; }
2)Service 增加方法
在 UserService 中声明:
▼java复制代码/** * 当前登录用户更新个人信息 * * @param userUpdateMyRequest 更新请求 * @param request 请求 * @return 是否成功 */ boolean updateMyUser(UserUpdateMyRequest userUpdateMyRequest, HttpServletRequest request);
实现类中编写代码:
▼java复制代码@Override public boolean updateMyUser(UserUpdateMyRequest userUpdateMyRequest, HttpServletRequest request) { if (userUpdateMyRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } User loginUser = this.getLoginUser(request); User updateUser = new User(); updateUser.setId(loginUser.getId()); updateUser.setUserName(userUpdateMyRequest.getUserName()); updateUser.setUserAvatar(userUpdateMyRequest.getUserAvatar()); updateUser.setUserProfile(userUpdateMyRequest.getUserProfile()); return this.updateById(updateUser); }
这里的关键点是:不从前端接收用户 id,而是直接从当前登录态里拿登录用户 id ,避免越权修改其他用户。
3)Controller 增加接口
在 UserController 中新增:
▼java复制代码/** * 更新当前登录用户 */ @PostMapping("/update/my") public BaseResponse<Boolean> updateMyUser(@RequestBody UserUpdateMyRequest userUpdateMyRequest, HttpServletRequest request) { ThrowUtils.throwIf(userUpdateMyRequest == null, ErrorCode.PARAMS_ERROR); boolean result = userService.updateMyUser(userUpdateMyRequest, request); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR); return ResultUtils.success(true); }
4)前端增加个人中心页
新增页面 src/pages/user/UserProfilePage.vue:
▼vue复制代码<template> <div id="userProfilePage"> <a-card title="个人中心"> <a-form layout="vertical" :model="formState" @finish="handleSubmit"> <a-form-item label="账号"> <a-input :value="loginUserStore.loginUser.userAccount" disabled /> </a-form-item> <a-form-item label="用户名"> <a-input v-model:value="formState.userName" placeholder="请输入用户名" /> </a-form-item> <a-form-item label="头像地址"> <a-input v-model:value="formState.userAvatar" placeholder="请输入头像地址" /> </a-form-item> <a-form-item label="个人简介"> <a-textarea v-model:value="formState.userProfile" placeholder="介绍一下自己" :auto-size="{ minRows: 4, maxRows: 6 }" /> </a-form-item> <a-form-item> <a-button type="primary" html-type="submit">保存资料</a-button> </a-form-item> </a-form> </a-card> </div> </template>
页面逻辑:
▼typescript复制代码const handleSubmit = async () => { const res = await updateMyUser({ ...formState, }) if (res.data.code === 0) { await loginUserStore.fetchLoginUser() message.success('保存成功') } else { message.error('保存失败,' + res.data.message) } }
更新成功后重新调用 fetchLoginUser(),这样右上角头像、昵称等全局展示信息可以立即刷新。
3、管理员编辑用户信息
除了删除用户,后台用户管理页面还可以增加“编辑用户”能力。
相比表格行内编辑,直接用弹窗表单实现会更稳一些,也更适合新手。
1)表格操作列增加编辑按钮
在 UserManagePage.vue 的操作列中增加:
▼vue复制代码<template v-else-if="column.key === 'action'"> <a-space> <a-button type="link" @click="openEditModal(record)">编辑</a-button> <a-button danger @click="doDelete(record.id)">删除</a-button> </a-space> </template>
2)新增编辑弹窗
页面中增加 a-modal:
▼vue复制代码<a-modal v-model:open="editModalOpen" title="编辑用户" ok-text="保存" cancel-text="取消" @ok="handleEditSubmit" > <a-form layout="vertical" :model="editForm"> <a-form-item label="用户名"> <a-input v-model:value="editForm.userName" placeholder="请输入用户名" /> </a-form-item> <a-form-item label="头像地址"> <a-input v-model:value="editForm.userAvatar" placeholder="请输入头像地址" /> </a-form-item> <a-form-item label="个人简介"> <a-textarea v-model:value="editForm.userProfile" placeholder="请输入简介" :auto-size="{ minRows: 3, maxRows: 5 }" /> </a-form-item> <a-form-item label="用户角色"> <a-select v-model:value="editForm.userRole" :options="userRoleOptions" /> </a-form-item> </a-form> </a-modal>
3)打开弹窗时回填数据
▼typescript复制代码const openEditModal = (record: API.UserVO) => { editForm.id = record.id editForm.userName = record.userName editForm.userAvatar = record.userAvatar editForm.userProfile = record.userProfile editForm.userRole = record.userRole editModalOpen.value = true }
4)保存编辑结果
▼typescript复制代码const handleEditSubmit = async () => { const res = await updateUser({ ...editForm, }) if (res.data.code === 0) { message.success('更新成功') editModalOpen.value = false await fetchData() } else { message.error('更新失败,' + res.data.message) } }
这样管理员就可以在后台修改:
- 用户昵称
- 用户头像
- 用户简介
- 用户角色
4、基于路由 meta 的全局权限管理
前面我们已经提过一种简单权限控制方式:在权限文件中通过路由前缀(比如 /admin)判断是否允许访问。
但这种方式扩展性一般,页面越来越多时,会出现:
- 权限规则分散
- 菜单和权限逻辑要维护两份
- 后续新增页面时容易漏改
更好的做法是:把每个页面的权限要求直接写在路由配置里 。
1)新建 access 目录
在 src/access/ 下新建 3 个文件:
accessEnum.tscheckAccess.tsindex.ts
权限枚举:
▼typescript复制代码const ACCESS_ENUM = { NOT_LOGIN: 'notLogin', USER: 'user', ADMIN: 'admin', } as const export default ACCESS_ENUM
权限校验函数:
▼typescript复制代码import ACCESS_ENUM from '@/access/accessEnum' const checkAccess = ( loginUser: API.LoginUserVO | undefined, needAccess: string = ACCESS_ENUM.NOT_LOGIN, ) => { const loginUserAccess = loginUser?.userRole ?? ACCESS_ENUM.NOT_LOGIN if (needAccess === ACCESS_ENUM.NOT_LOGIN) { return true } if (needAccess === ACCESS_ENUM.USER) { return loginUserAccess !== ACCESS_ENUM.NOT_LOGIN } if (needAccess === ACCESS_ENUM.ADMIN) { return loginUserAccess === ACCESS_ENUM.ADMIN } return true } export default checkAccess
2)路由配置中声明页面权限
在 router/index.ts 中配置:
▼typescript复制代码{ path: '/user/profile', name: '个人中心', component: UserProfilePage, meta: { access: ACCESS_ENUM.USER, }, }, { path: '/admin/userManage', name: '用户管理', component: UserManagePage, meta: { access: ACCESS_ENUM.ADMIN, }, }, { path: '/noAuth', name: '无权限', component: NoAuthPage, meta: { hideInMenu: true, }, },
这里的 meta.access 表示页面所需权限,meta.hideInMenu 表示该页面不显示在导航菜单中。
3)编写全局权限守卫
在 src/access/index.ts 中:
▼typescript复制代码import router from '@/router' import ACCESS_ENUM from '@/access/accessEnum' import checkAccess from '@/access/checkAccess' import { useLoginUserStore } from '@/stores/loginUser' router.beforeEach(async (to) => { const loginUserStore = useLoginUserStore() let loginUser = loginUserStore.loginUser if (!loginUser.userRole) { await loginUserStore.fetchLoginUser() loginUser = loginUserStore.loginUser } const needAccess = (to.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN if (needAccess === ACCESS_ENUM.NOT_LOGIN) { return true } if (!loginUser.userRole || loginUser.userRole === ACCESS_ENUM.NOT_LOGIN) { return `/user/login?redirect=${encodeURIComponent(to.fullPath)}` } if (!checkAccess(loginUser, needAccess)) { return '/noAuth' } return true })
这样处理后:
- 公开页面直接放行
- 需要登录的页面,未登录时跳转登录页
- 已登录但权限不足时跳转无权限页
4)配合登录态 Store 支持自动登录
为了让权限守卫生效,loginUserStore 默认要区分“未获取过登录态”和“明确未登录”。
修改默认值:
▼typescript复制代码const loginUser = ref<API.LoginUserVO>({ userName: '未登录', userRole: ACCESS_ENUM.NOT_LOGIN, })
并在 fetchLoginUser 中处理未登录场景:
▼typescript复制代码async function fetchLoginUser() { try { const res = await getLoginUser() if (res.data.code === 0 && res.data.data) { loginUser.value = res.data.data return } } catch (_error) { // ignore } loginUser.value = { userName: '未登录', userRole: ACCESS_ENUM.NOT_LOGIN, } }
5)移除 App.vue 中重复请求登录态的代码
由于权限守卫已经负责全局自动登录,可以删除 App.vue 中原本的:
▼typescript复制代码const loginUserStore = useLoginUserStore() loginUserStore.fetchLoginUser()
避免重复请求。
5、登录回跳与无权限页面
1)登录回跳
当未登录用户访问受保护页面时,最好在登录成功后自动返回原页面。
全局请求拦截器可以这样改:
▼typescript复制代码const redirect = `${window.location.pathname}${window.location.search}${window.location.hash}` window.location.href = `/user/login?redirect=${encodeURIComponent(redirect)}`
登录页读取 redirect 参数:
▼typescript复制代码const route = useRoute() const handleSubmit = async (values: any) => { const res = await userLogin(values) if (res.data.code === 0 && res.data.data) { await loginUserStore.fetchLoginUser() message.success('登录成功') const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/' router.push({ path: redirect, replace: true, }) } else { message.error('登录失败,' + res.data.message) } }
2)新增无权限页面
新增 NoAuthPage.vue:
▼vue复制代码<template> <div id="noAuthPage"> <a-result status="403" title="403" sub-title="你没有权限访问当前页面"> <template #extra> <a-button type="primary" @click="router.push('/')">返回首页</a-button> </template> </a-result> </div> </template>
这样当用户虽然已登录,但访问了超出权限范围的页面时,会比直接跳登录页更合理。
6、根据路由配置控制菜单显隐
如果页面权限已经配置在路由里,菜单显隐也应当复用同一份配置,而不是再单独维护一份管理员路由前缀判断。
修改 GlobalHeader.vue,先定义菜单项:
▼typescript复制代码const originItems: MenuProps['items'] = [ { key: '/', label: '主页', title: '主页', }, { key: '/user/profile', label: '个人中心', title: '个人中心', }, { key: '/admin/userManage', label: '用户管理', title: '用户管理', }, ]
根据路由配置过滤菜单:
▼typescript复制代码const routeMap = computed(() => { const entries = appRouter.getRoutes().map((item) => [item.path, item] as const) return Object.fromEntries(entries) }) const menuItems = computed<MenuProps['items']>(() => { return originItems?.filter((menu) => { const menuKey = menu?.key as string if (!menuKey || !menuKey.startsWith('/')) { return true } const routeItem = routeMap.value[menuKey] if (!routeItem) { return true } if (routeItem.meta?.hideInMenu) { return false } const needAccess = (routeItem.meta?.access as string) ?? ACCESS_ENUM.NOT_LOGIN return checkAccess(loginUserStore.loginUser, needAccess) }) })
这样做的好处是:
- 权限和菜单只维护一份配置
- 新增页面时只需要补路由
meta - 导航栏会自动根据当前用户权限显示可访问菜单
7、右上角增加“个人中心”
用户下拉菜单中可以直接增加入口:
▼vue复制代码<template #overlay> <a-menu> <a-menu-item @click="router.push('/user/profile')">个人中心</a-menu-item> <a-menu-item @click="doLogout"> <LogoutOutlined /> 退出登录 </a-menu-item> </a-menu> </template>
这样用户编辑个人信息的入口就更加自然了。
8、实现效果总结
完成以上改造后,用户模块除了最基础的注册、登录、注销、管理用户外,还额外拥有了这些能力:
- 已被逻辑删除的账号支持重新注册
- 管理员可以在后台编辑用户信息
- 用户可以在个人中心修改自己的资料
- 支持登录回跳
- 支持无权限页面
- 支持通过路由
meta统一做全局权限管理 - 支持根据权限自动隐藏无权限菜单
