柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
      • 接口相关
        • 登录接口 /login
        • 退登接口 /logout
        • 个人信息 /getInfo
        • 路由接口 /getRouters
        • 导出接口 /export
        • 导入接口 /import
      • 拦截器相关
      • 异常相关
    • ruoyi-vue-plus-权限控制
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2023-11-14
目录

ruoyi-vue-plus-基础功能

# 接口相关

# 登录接口 /login

大致步骤

  1. 登录请求 /login
  2. 验证码验证
  3. 查用户
  4. 验证密码 , 明文加密比较
  5. 生成Token
  6. 记录登录日志
  7. 响应token

整体登录业务步骤 SysLoginService#login()

public String login(String username, String password, String code, String uuid) {
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    // 验证码开关
    if (captchaEnabled) {
        validateCaptcha(username, code, uuid);
    }
    // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
    SysUser user = loadUserByUsername(username);
    checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
    // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
    LoginUser loginUser = buildLoginUser(user);
    // 生成token
    LoginHelper.loginByDevice(loginUser, DeviceType.PC);

    recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
    recordLoginInfo(user.getUserId(), username);
    return StpUtil.getTokenValue();
}

密码验证 SysLoginService#checkLogin()

private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
    String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
    String loginFail = Constants.LOGIN_FAIL;

    // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
    int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
    // 锁定时间内登录 则踢出
    if (errorNumber >= maxRetryCount) {
        // 日志记录
        recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
        throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
    }

    if (supplier.get()) {
        // 错误次数递增
        errorNumber++;
        RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
        // 达到规定错误次数 则锁定登录
        if (errorNumber >= maxRetryCount) {
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        } else {
            // 未达到规定错误次数
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
            throw new UserException(loginType.getRetryLimitCount(), errorNumber);
        }
    }

    // 登录成功 清空错误次数
    RedisUtils.deleteObject(errorKey);
}

生成token

通过sa-token实现

public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
    SaStorage storage = SaHolder.getStorage();
    // 缓存 用户信息
    storage.set(LOGIN_USER_KEY, loginUser);
    // 缓存 用户ID
    storage.set(USER_KEY, loginUser.getUserId());
    SaLoginModel model = new SaLoginModel();
    if (ObjectUtil.isNotNull(deviceType)) {
        model.setDevice(deviceType.getDevice());
    }
	// 登录 , 并拓展记录配置信息 登录类型 , 用户ID
    StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
    StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}

# 退登接口 /logout

大致步骤 :

  1. 退出登录请求 /logout
  2. sa-token方法退出 , StpUtil#logout()
  3. 记录退出登录日志
  4. 前端清空token缓存

# 个人信息 /getInfo

登录成功后会在路由守卫调用此 /getInfo接口 获取用户自身的基本信息 , 并存到 vuex 中...

响应结构

  • permissions (数组) : 权限文本标识
  • roles (数组) : 身份文本标识
  • user (SysUser对象) : 个人用户对象
{
    "code": 200,
    "msg": "操作成功",
    "data": {
        "permissions": [
            "system:oss:download",
            "system:oss:upload",
            "demo:tree:query"
        ],
        "roles": [
            "teacher"
        ],
        "user": {...}
    }
}

# 路由接口 /getRouters

登录成功后会获取路由路径 , 它也是在路由守卫和 /getInfo接口 一同调用

后端主要结构体

库表结构 : SysMenu

响应结构 : RouterVo

后端主要方法

核心封装方法

  • 树型封装 : SysMenuServiceImpl#getChildPerms()
  • 封装转化 : SysMenuServiceImpl#buildMenus()

开放应用接口 SysLoginController#getRouters()

@GetMapping("getRouters")
public R<List<RouterVo>> getRouters() {
    Long userId = LoginHelper.getUserId();
    List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
    return R.ok(menuService.buildMenus(menus));
}

获取库菜单主要业务方法 SysMenuServiceImpl#selectMenuTreeByUserId()

@Override
public List<SysMenu> selectMenuTreeByUserId(Long userId) {
    List<SysMenu> menus = null;
    if (LoginHelper.isAdmin(userId)) {
        menus = baseMapper.selectMenuTreeAll();
    } else {
        menus = baseMapper.selectMenuTreeByUserId(userId);
    }
    return getChildPerms(menus, 0);
}

getChildPerms()方法 , 将列表的所有节点以树型结构封装 , 其结构自行和思路查阅

前端动态实现

通过 /getRouters 接口的 数据的大致结构

点击代码展开
{
    "code": 200,
    "msg": "操作成功",
    "data": [
        {
            "name": "System",
            "path": "/system",
            "hidden": false,
            "redirect": "noRedirect",
            "component": "Layout",
            "alwaysShow": true,
            "meta": {
                "title": "系统管理",
                "icon": "system",
                "noCache": false,
                "link": null
            },
            "children": [
                {
                    "name": "User",
                    "path": "user",
                    "hidden": false,
                    "component": "system/user/index",
                    "meta": {
                        "title": "用户管理",
                        "icon": "user",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Role",
                    "path": "role",
                    "hidden": false,
                    "component": "system/role/index",
                    "meta": {
                        "title": "角色管理",
                        "icon": "peoples",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Menu",
                    "path": "menu",
                    "hidden": false,
                    "component": "system/menu/index",
                    "meta": {
                        "title": "菜单管理",
                        "icon": "tree-table",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Dept",
                    "path": "dept",
                    "hidden": false,
                    "component": "system/dept/index",
                    "meta": {
                        "title": "部门管理",
                        "icon": "tree",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Post",
                    "path": "post",
                    "hidden": false,
                    "component": "system/post/index",
                    "meta": {
                        "title": "岗位管理",
                        "icon": "post",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Dict",
                    "path": "dict",
                    "hidden": false,
                    "component": "system/dict/index",
                    "meta": {
                        "title": "字典管理",
                        "icon": "dict",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Config",
                    "path": "config",
                    "hidden": false,
                    "component": "system/config/index",
                    "meta": {
                        "title": "参数设置",
                        "icon": "edit",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Notice",
                    "path": "notice",
                    "hidden": false,
                    "component": "system/notice/index",
                    "meta": {
                        "title": "通知公告",
                        "icon": "message",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Log",
                    "path": "log",
                    "hidden": false,
                    "redirect": "noRedirect",
                    "component": "ParentView",
                    "alwaysShow": true,
                    "meta": {
                        "title": "日志管理",
                        "icon": "log",
                        "noCache": false,
                        "link": null
                    },
                    "children": [
                        {
                            "name": "Operlog",
                            "path": "operlog",
                            "hidden": false,
                            "component": "monitor/operlog/index",
                            "meta": {
                                "title": "操作日志",
                                "icon": "form",
                                "noCache": false,
                                "link": null
                            }
                        },
                        {
                            "name": "Logininfor",
                            "path": "logininfor",
                            "hidden": false,
                            "component": "monitor/logininfor/index",
                            "meta": {
                                "title": "登录日志",
                                "icon": "logininfor",
                                "noCache": false,
                                "link": null
                            }
                        }
                    ]
                },
                {
                    "name": "Oss",
                    "path": "oss",
                    "hidden": false,
                    "component": "system/oss/index",
                    "meta": {
                        "title": "文件管理",
                        "icon": "upload",
                        "noCache": false,
                        "link": null
                    }
                }
            ]
        },
        {
            "name": "Tool",
            "path": "/tool",
            "hidden": false,
            "redirect": "noRedirect",
            "component": "Layout",
            "alwaysShow": true,
            "meta": {
                "title": "系统工具",
                "icon": "tool",
                "noCache": false,
                "link": null
            },
            "children": [
                {
                    "name": "Build",
                    "path": "build",
                    "hidden": false,
                    "component": "tool/build/index",
                    "meta": {
                        "title": "表单构建",
                        "icon": "build",
                        "noCache": false,
                        "link": null
                    }
                },
                {
                    "name": "Gen",
                    "path": "gen",
                    "hidden": false,
                    "component": "tool/gen/index",
                    "meta": {
                        "title": "代码生成",
                        "icon": "code",
                        "noCache": false,
                        "link": null
                    }
                }
            ]
        }
    ]
}

调用此接口在于 vuex 中的 permission 模块

GenerateRoutes({ commit }) {
  return new Promise(resolve => {
    // 向后端请求路由数据
    getRouters().then(res => {
      // 深拷贝
      const sdata = JSON.parse(JSON.stringify(res.data))
      const rdata = JSON.parse(JSON.stringify(res.data))
      // 侧边栏路由
      const sidebarRoutes = filterAsyncRouter(sdata)
      // 重写路由
      const rewriteRoutes = filterAsyncRouter(rdata, false, true)
      // 过滤路由 , 根据 管理权的权限标识 控制过滤
      const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
      rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
      router.addRoutes(asyncRoutes);
      commit('SET_ROUTES', rewriteRoutes)
      commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
      commit('SET_DEFAULT_ROUTES', sidebarRoutes)
      commit('SET_TOPBAR_ROUTES', sidebarRoutes)
      resolve(rewriteRoutes)
    })
  })
}

GenerateRoutes() 调用在于 路由守卫的前置拦截 src/permission.js

官网文档 : https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html (opens new window)

在路由守卫中 动态添加 路由信息

store.dispatch('GenerateRoutes').then(accessRoutes => {
  // 根据roles权限生成可访问的路由表
  router.addRoutes(accessRoutes) // 动态添加可访问路由表
  next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
})

# 导出接口 /export

采用 用户信息导出导入作为示例应用

大致流程

  1. 数据源拿到集合
  2. 将集合转化对象 , 转化为Vo (Vo中一般有字段映射标识)
  3. 调用工具Excel实现导出下载
@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@SaCheckPermission("system:user:export")
@PostMapping("/export")
public void export(SysUser user, HttpServletResponse response) {
    List<SysUser> list = userService.selectUserList(user);
    List<SysUserExportVo> listVo = BeanUtil.copyToList(list, SysUserExportVo.class);
    for (int i = 0; i < list.size(); i++) {
        SysDept dept = list.get(i).getDept();
        SysUserExportVo vo = listVo.get(i);
        if (ObjectUtil.isNotEmpty(dept)) {
            vo.setDeptName(dept.getDeptName());
            vo.setLeader(dept.getLeader());
        }
    }
    ExcelUtil.exportExcel(listVo, "用户数据", SysUserExportVo.class, response);
}

# 导入接口 /import

采用 用户信息导出导入作为示例应用

大致流程

# 拦截器相关

拦截器实现通过 ResourcesConfig配置类

Sa-Token拦截器 SaTokenConfig类 点击跳转了解相关

时间统计拦截器 PlusWebInvokeTimeInterceptor类 (全局拦截)

  • 前置拦截

    打印请求日志以及参数信息

  • 后置拦截

    计算请求过程时长

PlusWebInvokeTimeInterceptor计时拦截器类

点击展开
@Slf4j
public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {

    private final String prodProfile = "prod";

    private final TransmittableThreadLocal<StopWatch> invokeTimeTL = new TransmittableThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
            String url = request.getMethod() + " " + request.getRequestURI();

            // 打印请求参数
            if (isJsonRequest(request)) {
                String jsonParam = "";
                if (request instanceof RepeatedlyRequestWrapper) {
                    BufferedReader reader = request.getReader();
                    jsonParam = IoUtil.read(reader);
                }
                log.debug("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
            } else {
                Map<String, String[]> parameterMap = request.getParameterMap();
                if (MapUtil.isNotEmpty(parameterMap)) {
                    String parameters = JsonUtils.toJsonString(parameterMap);
                    log.debug("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
                } else {
                    log.debug("[PLUS]开始请求 => URL[{}],无参数", url);
                }
            }

            StopWatch stopWatch = new StopWatch();
            invokeTimeTL.set(stopWatch);
            stopWatch.start();
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (!prodProfile.equals(SpringUtils.getActiveProfile())) {
            StopWatch stopWatch = invokeTimeTL.get();
            stopWatch.stop();
            log.debug("[PLUS]结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTime());
            invokeTimeTL.remove();
        }
    }

    /**
     * 判断本次请求的数据类型是否为json
     *
     * @param request request
     * @return boolean
     */
    private boolean isJsonRequest(HttpServletRequest request) {
        String contentType = request.getContentType();
        if (contentType != null) {
            return StringUtils.startsWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
        }
        return false;
    }

}

# 异常相关

GlobalExceptionHandler全局异常处理器类

异常种类

异常类 说明
NotPermissionException 权限异常
NotRoleException 角色异常
NotLoginException 认证异常
HttpRequestMethodNotSupportedException 请求异常
DuplicateKeyException SQL 主键/唯一 异常
MyBatisSystemException MyBatis异常
ServiceException 业务异常
MissingPathVariableException 请求参数异常
MethodArgumentTypeMismatchException 请求参数类型不匹配
BindException 自定义验证异常 (绑定的参数)
ConstraintViolationException 自定义验证异常 (约束转化的参数)
MethodArgumentNotValidException 自定义验证异常 (对批注的参数)
#ruoyi-vue-plus

← SpringCloud Bus服务总线 ruoyi-vue-plus-权限控制→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式