完成”AI零代码应用生成平台“的用户模块扩展

扩展思路 里提到了一些可以继续完善的功能,这里给出一套完整实现,包含:

  • 已逻辑删除账号可重新注册
  • 管理员编辑用户信息
  • 用户个人中心 / 个人设置
  • 基于路由 meta 的全局权限管理

下面分别讲解。

1、已逻辑删除账号可重新注册

问题分析

当前用户表中,userAccount 有唯一索引:

sql
复制代码
UNIQUE KEY uk_userAccount (userAccount)

虽然用户删除是逻辑删除,但数据库唯一索引仍然会占用原账号,所以会出现一种情况:

  1. 某用户被逻辑删除
  2. 前端再次用原账号注册
  3. 业务层查询可能查不到有效用户
  4. 但数据库插入时依然会因为唯一索引冲突而失败

所以,不能只在注册时“忽略逻辑删除数据”,还要想办法 释放唯一键占用

实现方案

最稳妥的做法有 2 步:

  1. 删除用户前,先把该用户原本的 userAccount 改写成一个带后缀的唯一值
  2. 注册用户时,再兜底处理历史已删除但未释放账号的脏数据

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.ts
  • checkAccess.ts
  • index.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 })

这样处理后:

  1. 公开页面直接放行
  2. 需要登录的页面,未登录时跳转登录页
  3. 已登录但权限不足时跳转无权限页

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、实现效果总结

完成以上改造后,用户模块除了最基础的注册、登录、注销、管理用户外,还额外拥有了这些能力:

  1. 已被逻辑删除的账号支持重新注册
  2. 管理员可以在后台编辑用户信息
  3. 用户可以在个人中心修改自己的资料
  4. 支持登录回跳
  5. 支持无权限页面
  6. 支持通过路由 meta 统一做全局权限管理
  7. 支持根据权限自动隐藏无权限菜单
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP