若依框架入门+项目实战

导读:本篇文章讲解 若依框架入门+项目实战,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

1、若依框架介绍

  1. 官网地址:http://ruoyi.vip
  2. RuoYi是一个后台管理系统,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf)主要目的让开发者注重专注业务,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量
  3. 若依是给女儿取的名字(寓意:你若不离不弃,我必生死相依)

目前提供的版本有:

  • RuoYi(SpringBoot+Bootstrap)
  • RuoYi-Vue(SpringBoot+Vue)
  • RuoYi-Cloud(SpringCloud+Vue)
  • RuoYi-App(Uniapp+Vue)

我们这里主要介绍RuoYi-Vue这个前后端分离的版本

imgRuoYi-App(Uniapp+Vue)

2、本地运行介绍

2.1、下载代码

访问网站:https://gitee.com/y_project/RuoYi-Vue

image-20221206132810278

我这里采用第一种方式,先将代码克隆一份到本地磁盘

image-20221206132952254

然后再使用IDEA打开该项目即可:

image-20221206133228019

第一次下载相关依赖jar包可能需要的时间稍微长一点,耐心等一会即可

2.2、准备数据库

说明:我这里本机数据库的版本是:5.5.20

image-20221206133600100

将项目目录中sql文件夹中的ry_20220822.sql文件拿出来到Navicat工具中跑一下(我这里就没有执行quartz.sql,因为我们这里暂时用不到定时任务相关表)

image-20221206134644454

脚本执行完毕后,数据库表情况如上图所示

2.3、准备Redis

想要启动项目,必须要准备一台Redis服务器,我这里在本地直接启动一个Redis,密码是123456,所以需要修改【application.yml】文件:

image-20221206134039654

2.4、启动即可

数据库和Redis准备好之后,就可以运行项目了,直接找到启动类,执行main方法就可以启动项目了:

image-20221206134827145

运行成功:

image-20221206134902312

可以清楚看到,项目启动成功了,端口是8080

2.5、前端运行

image-20221206142329699

运行npm install后,根据网速不同,需要耐心等待一会

image-20221206142853960

安装完毕后,继续执行命令:npm run dev,运行成功后就会自动打开登录首页:

image-20230221164714785

看到此页面,说明前端运行成功

账号密码都是admin,直接登录即可

注意:如果发现密码不对的话,我这里自己生成一个123456的密码:

$2a$10$fi5GezS2kaXH4Bhk7ZzgnOJiR4ywlLBkyQ5kiK1bMr8C40w7u.kSe

将上面值直接复制粘贴到sys_user表中的password中即可,这样我们就可以使用admin/123456进行登录了,登录成功后的首页如下:

image-20221206143633643

至此,我们就将若依的前后台都运行完成,在本地成功跑起来了

是不是很简单呢,哈哈,赶紧动手做一做吧

3、架构介绍和功能说明

3.1、文件结构介绍

image-20221206144427707

2.2、技术栈介绍

  • 前端采用Vue、Element UI
  • 后端采用Spring Boot、Spring Security、Redis & Jwt
  • 权限认证使用Jwt,支持多终端认证系统
  • 支持加载动态权限菜单,多方式轻松权限控制
  • 高效率开发,使用代码生成器可以一键生成前后端代码

2.3、功能介绍

  1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
  2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
  3. 岗位管理:配置系统用户所属担任职务。
  4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
  7. 参数管理:对系统动态配置常用参数。
  8. 通知公告:系统通知公告信息发布维护。
  9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  10. 登录日志:系统登录日志记录查询包含登录异常。
  11. 在线用户:当前系统中活跃用户状态监控。
  12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
  13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
  14. 系统接口:根据业务代码自动生成相关的api接口文档。
  15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
  16. 缓存监控:对系统的缓存信息查询,命令统计等。
  17. 在线构建器:拖动表单元素生成相应的HTML代码。
  18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。

4、核心功能讲解

4.1、用户管理

image-20221229160938443

表结构如下:

image-20221229161005534

4.2、角色管理

image-20221229161018423

表结构如下:

image-20221229161142054

4.3、菜单管理

image-20221229161159468

表结构如下:

image-20221229161126131

4.4、部门管理

image-20221229161159468

表结构如下:

image-20221229161214941

4.5、岗位管理

image-20221229161300397

表结构如下:

image-20221229161322663

4.6、RBAC模型

  • 若依框架的权限管理功能是基于【RBAC】来实现的,即:系统中所有的权限,都是基于角色来控制的
  • 框架对权限的控制,不仅支持菜单的功能,还支持菜单中的每一个按钮的权限控制

RBAC(基于角色的访问控制)模型包含的表有下面五张:

  1. 用户表
  2. 角色表
  3. 菜单表
  4. 用户角色关联表
  5. 角色菜单关联表

关系如下:

image-20230221164557100
查询当前登录人有权限看到的菜单,SQL语句:(已知条件是登录用户ID)

select t1.* from sys_menu t1
left join sys_role_menu t2 on t1.menu_id = t2.menu_id
left join sys_user_role t3 on t2.role_id = t3.role_id
where t3.user_id = 2

4.7、数据字典

数据字典功能由两张表组成:

  1. sys_dict_type:字典类型表
  2. sys_dict_data:字典数据表

两者之间的关系:sys_dict_data表的dict_type字段关联sys_dict_type的dict_type字段

表结构如下:

sys_dict_type:

image-20221229162342468

sys_dict_data:

image-20221229162352830

查询某个数据字典类型的详情,SQL如下:(比如查询性别有哪些数据)

select t1.* from sys_dict_data t1
left join sys_dict_type t2 on t1.dict_type = t2.dict_type
where t2.dict_type = 'sys_user_sex';

5、拦截器讲解

5.1、前端拦截器

5.1.1、前置拦截器

前置拦截器的代码写在了request.js文件中,路径为:src – utils – request.js,代码如下:

// request拦截器
service.interceptors.request.use(config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  // 是否需要防止数据重复提交
  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
  // get请求映射params参数
  if (config.method === 'get' && config.params) {
    let url = config.url + '?' + tansParams(config.params);
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
      time: new Date().getTime()
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    } else {
      const s_url = sessionObj.url;                  // 请求地址
      const s_data = sessionObj.data;                // 请求数据
      const s_time = sessionObj.time;                // 请求时间
      const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)
        return Promise.reject(new Error(message))
      } else {
        cache.session.setJSON('sessionObj', requestObj)
      }
    }
  }
  return config
}, error => {
    console.log(error)
    Promise.reject(error)
})

这个前置拦截器实际上做了这么几件事:

  1. 在请求头中添加token
  2. get请求映射params参数
  3. 阻止重复请求重复提交(虽然前端做了,但一般后台也需要做接口幂等性校验)

5.1.2、响应拦截器

响应拦截器的代码也写在request.js文件中,代码如下:

// 响应拦截器
service.interceptors.response.use(res => {
    // 未设置状态码则默认成功状态
    const code = res.data.code || 200;
    // 获取错误信息
    const msg = errorCode[code] || res.data.msg || errorCode['default']
    // 二进制数据则直接返回
    if(res.request.responseType ===  'blob' || res.request.responseType ===  'arraybuffer'){
      return res.data
    }
    if (code === 401) {
      if (!isRelogin.show) {
        isRelogin.show = true;
        MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
          isRelogin.show = false;
          store.dispatch('LogOut').then(() => {
            location.href = '/index';
          })
      }).catch(() => {
        isRelogin.show = false;
      });
    }
      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
    } else if (code === 500) {
      Message({ message: msg, type: 'error' })
      return Promise.reject(new Error(msg))
    } else if (code === 601) {
      Message({ message: msg, type: 'warning' })
      return Promise.reject('error')
    } else if (code !== 200) {
      Notification.error({ title: msg })
      return Promise.reject('error')
    } else {
      return res.data
    }
  },
  error => {
    console.log('err' + error)
    let { message } = error;
    if (message == "Network Error") {
      message = "后端接口连接异常";
    } else if (message.includes("timeout")) {
      message = "系统接口请求超时";
    } else if (message.includes("Request failed with status code")) {
      message = "系统接口" + message.substr(message.length - 3) + "异常";
    }
    Message({ message: message, type: 'error', duration: 5 * 1000 })
    return Promise.reject(error)
  }
)

在响应拦截器中做了下面这几件事:

  1. 对响应数据进行判断,如果是二进制数据则直接返回
  2. 对响应状态码进行了判断,不同的状态码做不同的处理

5.2、后端拦截器

若依框架的后端也有一些拦截器来处理一些公共的判断逻辑

5.2.1、防止重复提交拦截器

该拦截器所在目录是:ruoyi-framework工程中的com.ruoyi.framework.interceptor包下,代码如下:

package com.ruoyi.framework.interceptor;

import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ServletUtils;

/**
 * 防止重复提交拦截器
 *
 * @author ruoyi
 */
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
    {
        if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
            if (annotation != null)
            {
                if (this.isRepeatSubmit(request, annotation))
                {
                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                    ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                    return false;
                }
            }
            return true;
        }
        else
        {
            return true;
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request
     * @return
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

isRepeatSubmit方法的实现如下:

package com.ruoyi.framework.interceptor.impl;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpHelper;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;

/**
 * 判断请求url和数据是否和上一次相同,
 * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
 * 
 * @author ruoyi
 */
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";

    // 令牌自定义标识
    @Value("${token.header}")
    private String header;

    @Autowired
    private RedisCache redisCache;

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
    {
        String nowParams = "";
        if (request instanceof RepeatedlyRequestWrapper)
        {
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
        }

        // body参数为空,获取Parameter的数据
        if (StringUtils.isEmpty(nowParams))
        {
            nowParams = JSON.toJSONString(request.getParameterMap());
        }
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 唯一值(没有消息头则使用请求地址)
        String submitKey = StringUtils.trimToEmpty(request.getHeader(header));

        // 唯一标识(指定key + url + 消息头)
        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;

        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        if (sessionObj != null)
        {
            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
            if (sessionMap.containsKey(url))
            {
                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
                {
                    return true;
                }
            }
        }
        Map<String, Object> cacheMap = new HashMap<String, Object>();
        cacheMap.put(url, nowDataMap);
        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }

    /**
     * 判断参数是否相同
     */
    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
    {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }

    /**
     * 判断两次间隔时间
     */
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
    {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        if ((time1 - time2) < interval)
        {
            return true;
        }
        return false;
    }
}

6、登录流程讲解

6.1、登录流程梳理

  1. 请求登录请求首先会给到后台的SysLoginController类的login方法
  2. 然后会执行SysLoginService的login方法
  3. 然后会执行UserDetailsServiceImpl的loadUserByUsername方法
  4. loadUserByUsername方法主要做了下面几件事:
    • 校验登录账号是否在数据库中存在
    • 校验登录账号是否已被删除
    • 校验登录账号是否已停用
  5. 然后再调用SysPasswordService类的validate方法,主要做了下面几件事:
    • 校验登录密码输入错误次数是否超限
    • 校验密码是否正确
  6. 然后调用SysPermissionService类的getMenuPermission方法,获取登录人的菜单数据权限
  7. 最后记录相关登录日志(用线程池异步方式记录到数据库的,提高效率)

流程如下:

image-20221206151740219

6.2、登录技术栈分析

在上面登录流程中,登录成功后,最后会给前端返回一个token,会调用TokenService的createToken方法:如下:

/**
     * 创建令牌
     *
     * @param loginUser 用户信息
     * @return 令牌
     */
public String createToken(LoginUser loginUser)
{
    String token = IdUtils.fastUUID();
    loginUser.setToken(token);
    setUserAgent(loginUser);
    refreshToken(loginUser);

    Map<String, Object> claims = new HashMap<>();
    claims.put(Constants.LOGIN_USER_KEY, token);
    return createToken(claims);
}

return的createToken方法代码如下:

/**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
private String createToken(Map<String, Object> claims)
{
    String token = Jwts.builder()
        .setClaims(claims)
        .signWith(SignatureAlgorithm.HS512, secret).compact();
    return token;
}

实际上createToken方法就是采用JWT的方式生成令牌返回给前端

6.3、token的校验

登录成功后,我们每一个请求到后台时,都需要对token进行权限校验,若依框架对于token的校验是用过滤器实现的

过滤器位置:ruoyi-framework工程下com.ruoyi.framework.security.filter包中的JwtAuthenticationTokenFilter,代码如下:

package com.ruoyi.framework.security.filter;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;

/**
 * token过滤器 验证token有效性
 * 
 * @author ruoyi
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

getLoginUser方法作用:

  1. 从请求头中获取到token,然后解析得到我们设置进去的UUID
  2. 然后再以UUID作为key去Redis中获取到登录账号信息

代码如下:

/**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
public LoginUser getLoginUser(HttpServletRequest request)
{
    // 获取请求携带的令牌
    String token = getToken(request);
    if (StringUtils.isNotEmpty(token))
    {
        try
        {
            Claims claims = parseToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
            String userKey = getTokenKey(uuid);
            LoginUser user = redisCache.getCacheObject(userKey);
            return user;
        }
        catch (Exception e)
        {
        }
    }
    return null;
}

verifyToken方法作用:校验令牌超时时间和当前时间的差值,如果小于20分钟的话,就会刷新令牌的超时时间

代码如下:

/**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     *
     * @param loginUser
     * @return 令牌
     */
public void verifyToken(LoginUser loginUser)
{
    long expireTime = loginUser.getExpireTime();
    long currentTime = System.currentTimeMillis();
    if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
    {
        refreshToken(loginUser);
    }
}

最后下面这几行代码就是将用户信息再次设置到Security的上下文中:

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

6.4、小结

经过对登录流程的分析,我们不难发现,若依框架的权限框架实现方案就是采用:SpringSecurity+JWT实现的,对token的校验是采用过滤器拦截校验处理的

7、按钮权限控制讲解

若依框架的权限不仅是针对菜单,还可以到按钮级别,粒度非常细了,那是如何实现的呢?下面从前端和后台两个角度来分析一下

7.1、前端

首先我们随便找一个按钮看一下:

<el-row :gutter="10" class="mb8">
    <el-col :span="1.5">
        <el-button
                   type="primary"
                   plain
                   icon="el-icon-plus"
                   size="mini"
                   @click="handleAdd"
                   v-hasPermi="['system:user:add']"
                   >新增</el-button>
    </el-col>
    <el-col :span="1.5">
        <el-button
                   type="success"
                   plain
                   icon="el-icon-edit"
                   size="mini"
                   :disabled="single"
                   @click="handleUpdate"
                   v-hasPermi="['system:user:edit']"
                   >修改</el-button>
    </el-col>
    <el-col :span="1.5">
        <el-button
                   type="danger"
                   plain
                   icon="el-icon-delete"
                   size="mini"
                   :disabled="multiple"
                   @click="handleDelete"
                   v-hasPermi="['system:user:remove']"
                   >删除</el-button>
    </el-col>
    <el-col :span="1.5">
        <el-button
                   type="info"
                   plain
                   icon="el-icon-upload2"
                   size="mini"
                   @click="handleImport"
                   v-hasPermi="['system:user:import']"
                   >导入</el-button>
    </el-col>
    <el-col :span="1.5">
        <el-button
                   type="warning"
                   plain
                   icon="el-icon-download"
                   size="mini"
                   @click="handleExport"
                   v-hasPermi="['system:user:export']"
                   >导出</el-button>
    </el-col>
    <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>

当你看过很多按钮之后,你会发现几乎都有一个【v-hasPermi=“[‘xxx:xxx:xxx’]”】属性,这是做什么的呢?

其实el-button上的这个属性v-hasPermi,实际上就是vue的自定义指令,属性值就是创建按钮的时候定义的那个权限标志

其定义在src/directive/permission/hasPermi.js文件,代码如下:

/**
 * v-hasPermi 操作权限处理
 * Copyright (c) 2019 ruoyi
 */

import store from '@/store'

export default {
    inserted(el, binding, vnode) {
        const { value } = binding
        const all_permission = "*:*:*";
        const permissions = store.getters && store.getters.permissions

        if (value && value instanceof Array && value.length > 0) {
            const permissionFlag = value

            const hasPermissions = permissions.some(permission => {
                return all_permission === permission || permissionFlag.includes(permission)
            })

            if (!hasPermissions) {
                el.parentNode && el.parentNode.removeChild(el)
            }
        } else {
            throw new Error(`请设置操作权限标签值`)
        }
    }
}

注意代码 el.parentNode && el.parentNode.removeChild(el),可以看到,如果没有按钮权限,则会将按钮本身从dom中移除,也就是说,登录人所拥有的权限字符串集合中如果不包含【v-hasPermi=“[‘xxx:xxx:xxx’]”】中的权限字符串,页面是看不到这个按钮的,是不是很nice呢

7.2、后台

7.2.1、接口权限

我们在controller中随便找一个接口为例看下:

@Log(title = "用户管理", businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('system:user:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, SysUser user)
{
    List<SysUser> list = userService.selectUserList(user);
    ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
    util.exportExcel(response, list, "用户数据");
}

其中有下面这一行:

@PreAuthorize("@ss.hasPermi('system:user:export')")

进入hasPermi方法中,代码如下:

/**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
public boolean hasPermi(String permission)
{
    if (StringUtils.isEmpty(permission))
    {
        return false;
    }
    LoginUser loginUser = SecurityUtils.getLoginUser();
    if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
    {
        return false;
    }
    PermissionContextHolder.setContext(permission);
    return hasPermissions(loginUser.getPermissions(), permission);
}

上面代码逻辑如下:

  1. 通过SecurityUtils工具类从Security上下文中获取到登录用户信息
  2. 然后从用户信息中获取到该用户所拥有的权限字符串集合
  3. 然后做对比,看其中是否包含【@ss.hasPermi(‘system:user:export’)】中的权限字符串
  4. 包含就可以方法该方法,不包含就不能访问该方法

实际上若依提供的注解有下面这些:(看PermissionService类就知道了)

image-20221230145448253

使用示例:

1、数据权限示例

// 符合system:user:list权限要求
@PreAuthorize("@ss.hasPermi('system:user:list')")
 
// 不符合system:user:list权限要求
@PreAuthorize("@ss.lacksPermi('system:user:list')")
 
// 符合system:user:add或system:user:edit权限要求即可
@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")

2、角色权限示例

// 属于user角色
@PreAuthorize("@ss.hasRole('user')")
 
// 不属于user角色
@PreAuthorize("@ss.lacksRole('user')")
 
// 属于user或者admin之一
@PreAuthorize("@ss.hasAnyRoles('user,admin')")

注意:超级管理员拥有所有权限,不受权限约束

7.2.2、数据权限

数据权限实现的关键在于com.ruoyi.framework.aspectj.DataScopeAspect

该类是一个切面类,凡是加上com.ruoyi.common.annotation.DataScope注解的方法,在执行的时候都会被它拦截

DataScopeAspect切面类定义了五种权限范围,代码如下:

package com.ruoyi.framework.aspectj;

import java.util.ArrayList;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.ruoyi.common.annotation.DataScope;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.context.PermissionContextHolder;

/**
 * 数据过滤处理
 *
 * @author ruoyi
 */
@Aspect
@Component
public class DataScopeAspect
{
    /**
     * 全部数据权限
     */
    public static final String DATA_SCOPE_ALL = "1";

    /**
     * 自定数据权限
     */
    public static final String DATA_SCOPE_CUSTOM = "2";

    /**
     * 部门数据权限
     */
    public static final String DATA_SCOPE_DEPT = "3";

    /**
     * 部门及以下数据权限
     */
    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";

    /**
     * 仅本人数据权限
     */
    public static final String DATA_SCOPE_SELF = "5";

    /**
     * 数据权限过滤关键字
     */
    public static final String DATA_SCOPE = "dataScope";

    @Before("@annotation(controllerDataScope)")
    public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
    {
        clearDataScope(point);
        handleDataScope(point, controllerDataScope);
    }

    protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
    {
        // 获取当前的用户
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNotNull(loginUser))
        {
            SysUser currentUser = loginUser.getUser();
            // 如果是超级管理员,则不过滤数据
            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
            {
                String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
                        controllerDataScope.userAlias(), permission);
            }
        }
    }

    /**
     * 数据范围过滤
     *
     * @param joinPoint 切点
     * @param user 用户
     * @param deptAlias 部门别名
     * @param userAlias 用户别名
     * @param permission 权限字符
     */
    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
    {
        StringBuilder sqlString = new StringBuilder();
        List<String> conditions = new ArrayList<String>();

        for (SysRole role : user.getRoles())
        {
            String dataScope = role.getDataScope();
            if (!DATA_SCOPE_CUSTOM.equals(dataScope) && conditions.contains(dataScope))
            {
                continue;
            }
            if (StringUtils.isNotEmpty(permission) && StringUtils.isNotEmpty(role.getPermissions())
                    && !StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
            {
                continue;
            }
            if (DATA_SCOPE_ALL.equals(dataScope))
            {
                sqlString = new StringBuilder();
                break;
            }
            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
            {
                sqlString.append(StringUtils.format(
                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
                        role.getRoleId()));
            }
            else if (DATA_SCOPE_DEPT.equals(dataScope))
            {
                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
            }
            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
            {
                sqlString.append(StringUtils.format(
                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
                        deptAlias, user.getDeptId(), user.getDeptId()));
            }
            else if (DATA_SCOPE_SELF.equals(dataScope))
            {
                if (StringUtils.isNotBlank(userAlias))
                {
                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
                }
                else
                {
                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
                    sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
                }
            }
            conditions.add(dataScope);
        }

        if (StringUtils.isNotBlank(sqlString.toString()))
        {
            Object params = joinPoint.getArgs()[0];
            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
            {
                BaseEntity baseEntity = (BaseEntity) params;
                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
            }
        }
    }

    /**
     * 拼接权限sql前先清空params.dataScope参数防止注入
     */
    private void clearDataScope(final JoinPoint joinPoint)
    {
        Object params = joinPoint.getArgs()[0];
        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
        {
            BaseEntity baseEntity = (BaseEntity) params;
            baseEntity.getParams().put(DATA_SCOPE, "");
        }
    }
}

简单理解,这段代码的逻辑就是用户所在的部门权限越高,数据权限范围越大,查出来的结果集将会越大

DataScope注解分别加到了部门列表查询、角色列表查询、用户列表查询的接口上,很明显,这几个接口需要根据不同的人查出不同的结果

以用户列表查询为例,执行SQL为:

<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
    select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u
    left join sys_dept d on u.dept_id = d.dept_id
    where u.del_flag = '0'
    <if test="userId != null and userId != 0">
        AND u.user_id = #{userId}
    </if>
    <if test="userName != null and userName != ''">
        AND u.user_name like concat('%', #{userName}, '%')
    </if>
    <if test="status != null and status != ''">
        AND u.status = #{status}
    </if>
    <if test="phonenumber != null and phonenumber != ''">
        AND u.phonenumber like concat('%', #{phonenumber}, '%')
    </if>
    <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
        AND date_format(u.create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
    </if>
    <if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
        AND date_format(u.create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
    </if>
    <if test="deptId != null and deptId != 0">
        AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE find_in_set(#{deptId}, ancestors) ))
    </if>
    <!-- 数据范围过滤 -->
    ${params.dataScope}
</select>

其中,有这么一段代码:

<!-- 数据范围过滤 -->
${params.dataScope}

实际上DataScopeAspect切面就只干了填充params的dataScope属性这么一件事情

8、项目实战编码

8.1、项目背景和需求说明

学生成绩管理系统

  • 对学校的【课程信息】和【分数信息】进行管理
  • 老师角色的人登录系统后,可以对【课程信息】和【分数信息】进行维护,进行增删改查
  • 学生角色的人登录系统后,可以查看【课程信息】和【分数信息】,但是不能进行修改和删除操作

就这么一个简单的需求,接下来,我们看看利用若依框架,如何快速的进行开发

8.2、项目表结构分析

表结构我们先简单创建一下:

DROP TABLE IF EXISTS `t_score`;
CREATE TABLE `t_score`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `create_user_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人姓名',
  `course_id` bigint(2) NULL DEFAULT NULL COMMENT '课程ID',
  `user_id` bigint(1) NULL DEFAULT NULL COMMENT '用户ID',
  `score` int(11) NULL DEFAULT NULL COMMENT '分数',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分数表' ROW_FORMAT = Compact;

DROP TABLE IF EXISTS `t_course`;
CREATE TABLE `t_course`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `create_user_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建人姓名',
  `course_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '课程名称',
  `course_status` int(1) NULL DEFAULT NULL COMMENT '课程状态(1:不可用;2:可用)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '课程表' ROW_FORMAT = Compact;

8.3、自动生成代码

在本地数据库中新建上面两个表后,就可以访问系统直接生成代码了,如下:

image-20221206180008395

image-20221207103606711

image-20221207103648216

下载下来的压缩包加压后包含下面文件:

image-20221207103825102

这样前后台代码就都已经准备好了,菜单脚本也有了,接下来我们就直接将文件拷贝到项目中就可以了

开始拷贝

8.4、前端页面

image-20221207104122872

将下载好的前端页面按照上图所示拷贝到前端项目的system目录即可,然后再拷贝js文件,如下图所示:

image-20221207104225585

这样就可以了,先什么都不改,就这样就可以重启一下前端项目了

8.5、后台代码

先拷贝controller代码到指定目录即可,如下:

image-20221207104434633

再拷贝domain、mapper、service代码到对应目录,如下:

image-20221207104550576

在拷贝xml文件到对应目录,如下:

image-20221207104640840

拷贝完毕之后,什么都不需要改,直接重启项目即可

8.6、菜单配置

我们先看下生成SQL脚本,下面是t_score表的SQL:

image-20221207104816481

下面是t_course表的SQL:

image-20221207104823920

很明显两个表菜单都挂在了ID为3的菜单下面,查看数据库:

image-20221207093757037

也就是说,我这两个表的菜单都会默认挂在【系统工具】菜单下面,然后所有脚本直接执行一下即可

最终数据库中就会创建出如下数据了:

image-20221207113723208

那我们菜单也就准备好了

我们登录系统后,点开目录【系统工具】就可以看到我们刚执行SQL新增的菜单了:

image-20221207105341764

OK,菜单配置成功

8.7、配置角色和用户

目前菜单有了,但是老师和学生还看不到,所以需要新增角色并赋予相应权限才能看到

我们先添加两个角色:老师和学生

进入【系统管理】-【角色管理】菜单,点击【新增】按钮,新增老师角色并勾上赋予其的相关菜单按钮权限即可:

image-20221207113836197

再新建学生角色,并勾上赋予其的相关菜单按钮权限即可:

image-20221207113939214

然后我们再新增两个用户,一个老师,一个学生,进入【系统管理】-【用户管理】菜单,点击【新增】按钮

image-20221207100250397

再新增一个学生:

image-20221207100542830

到这,我添加的用户信息如下:

老师角色:张三,登录账号:zs,登录密码:123456

学生角色:王天霸,登录账号:wtb,登录密码:123456

我们现在登录一下zs账号:

image-20221207124653997

现在作为老师角色的用户,就可以对课程信息和分数信息进行CRUD了,只是可能有些字段需要微调一下,我们接下来就完善一下相关功能

8.8、前后端联调测试&功能完善

默认的前后台代码还需要微调一下,我们开始根据需求调整一下吧

8.8.1、课程管理

首先前端页面目前新增框是这样的:

image-20221207125001404

  • 这里【创建人姓名】应该是后台接口中获取当前登录人姓名赋值即可,不由前端给值,所以该字段这里可以删除
  • 课程表还有一个字段,叫做课程状态(1:不可用;2:可用),所以需要添加一个单选框

课程管理的页面改完之后的代码如下:

<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="创建人姓名" prop="createUserName">
        <el-input
          v-model="queryParams.createUserName"
          placeholder="请输入创建人姓名"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="课程名称" prop="courseName">
        <el-input
          v-model="queryParams.courseName"
          placeholder="请输入课程名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['system:course:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="mini"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['system:course:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="mini"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['system:course:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="mini"
          @click="handleExport"
          v-hasPermi="['system:course:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <el-table v-loading="loading" :data="courseList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="主键ID" align="center" prop="id" />
      <el-table-column label="创建人姓名" align="center" prop="createUserName" />
      <el-table-column label="课程名称" align="center" prop="courseName" />
      <el-table-column label="课程状态" align="center" prop="courseStatus" >
        <template slot-scope="scope">
          <el-tag v-if="scope.row.courseStatus == 2" type="success">可用</el-tag>
          <el-tag v-if="scope.row.courseStatus == 1" type="danger">不可用</el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['system:course:edit']"
          >修改</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['system:course:remove']"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total>0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改课程对话框 -->
    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="课程状态" prop="courseStatus">
          <!--课程状态(1:不可用;2:可用)-->
          <el-radio-group v-model="form.courseStatus">
            <el-radio :label="1">不可用</el-radio>
            <el-radio :label="2">可用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="课程名称" prop="courseName">
          <el-input v-model="form.courseName" placeholder="请输入课程名称" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { listCourse, getCourse, delCourse, addCourse, updateCourse } from "@/api/system/course";

export default {
  name: "Course",
  data() {
    return {
      // 遮罩层
      loading: true,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 课程表格数据
      courseList: [],
      // 弹出层标题
      title: "",
      // 是否显示弹出层
      open: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        createUserName: null,
        courseName: null,
        courseStatus: null
      },
      // 表单参数
      form: {},
      // 表单校验
      rules: {
      }
    };
  },
  created() {
    this.getList();
  },
  methods: {
    /** 查询课程列表 */
    getList() {
      this.loading = true;
      listCourse(this.queryParams).then(response => {
        this.courseList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },
    // 取消按钮
    cancel() {
      this.open = false;
      this.reset();
    },
    // 表单重置
    reset() {
      this.form = {
        id: null,
        createTime: null,
        createUserName: null,
        courseName: null,
        courseStatus: 0
      };
      this.resetForm("form");
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id)
      this.single = selection.length!==1
      this.multiple = !selection.length
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加课程";
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      this.reset();
      const id = row.id || this.ids
      getCourse(id).then(response => {
        this.form = response.data;
        this.open = true;
        this.title = "修改课程";
      });
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          if (this.form.id != null) {
            updateCourse(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addCourse(this.form).then(response => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      const ids = row.id || this.ids;
      this.$modal.confirm('是否确认删除课程编号为"' + ids + '"的数据项?').then(function() {
        return delCourse(ids);
      }).then(() => {
        this.getList();
        this.$modal.msgSuccess("删除成功");
      }).catch(() => {});
    },
    /** 导出按钮操作 */
    handleExport() {
      this.download('system/course/export', {
        ...this.queryParams
      }, `course_${new Date().getTime()}.xlsx`)
    }
  }
};
</script>

然后F5刷新页面,现在表单就是这样的了:

image-20221207114628935

我们新增完数据后的列表就是这样的:

image-20221207114658060

但是有个问题,创建人姓名里面没有值,这个值需要后台新增操作时,就将当前登录人的姓名存入数据库即可,所以我们修改后台TCourseController接口:

/**
     * 新增课程
     */
@PreAuthorize("@ss.hasPermi('system:course:add')")
@Log(title = "课程", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody TCourse tCourse)
{
    tCourse.setCreateTime(new Date());//设置创建时间
    tCourse.setCreateUserName(SecurityUtils.getLoginUser().getUser().getNickName());//设置创建人姓名(我这里取的是昵称不是登录账号)
    return toAjax(tCourseService.insertTCourse(tCourse));
}

image-20221207115043956

8.8.2、分数管理

自动生成出来的页面中,添加分数时是这样的表单:

image-20221207125135957

明显不符合要求

  • 【创建人姓名】应该是后台接口中给值,不由前端传值,这里应该删除
  • 【课程ID】应该是一个下拉框,选择需要添加哪门课程
  • 【用户ID】应该是一个下拉框,选择所有学生

首先后台需要提供两个接口:

  1. 查询所有状态为可用的课程数据,前端下拉框去动态显示所有课程数据
  2. 查询所有角色为学生的数据,前端下拉框去动态显示所有学生信息

两个接口我都写到了SysUserController类中了

第一个接口(获取状态为可用的课程数据):

@Autowired
private ITCourseService tCourseService;

/**
     * 获取状态为可用的课程数据
     */
@GetMapping("/getNormalCourse")
public AjaxResult getNormalCourse(TCourse tCourse)
{
    List<TCourse> list = tCourseService.selectTCourseList(tCourse);
    return AjaxResult.success(list);
}

第二个接口(获取角色为学生的数据):

/**
     * 获取角色为学生的数据
     */
@GetMapping("/getAllStudent")
public AjaxResult getAllStudent()
{
    List<SysUser> list = userService.getAllStudent();
    return AjaxResult.success(list);
}
//查询所有角色为学生的数据
List<SysUser> getAllStudent();
//查询所有角色为学生的数据
@Override
public List<SysUser> getAllStudent() {
    return userMapper.getAllStudent();
}
//查询所有角色为学生的数据
List<SysUser> getAllStudent();
<!--查询所有角色为学生的数据-->
<select id="getAllStudent" resultMap="SysUserResult">
    select t1.user_id, t1.nick_name from sys_user t1
    left join sys_user_role t2 on t1.user_id = t2.user_id
    where t2.role_id = 103
</select>

分数管理的前端vue页面改造完毕后是这样的:

<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="创建人姓名" prop="createUserName">
        <el-input
          v-model="queryParams.createUserName"
          placeholder="请输入创建人姓名"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="课程" prop="courseId">
        <el-select v-model="queryParams.courseId" filterable placeholder="请选择课程" style="width: 100%;">
          <el-option v-for="obj in this.courseData"
                     :label="obj.courseName"
                     :value="obj.id"
                     :key="obj.id">
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="学生" prop="userId">
        <el-select v-model="queryParams.userId" filterable placeholder="请选择学生" style="width: 100%;">
          <el-option v-for="obj in this.studentData"
                     :label="obj.nickName"
                     :value="obj.userId"
                     :key="obj.userId">
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="分数" prop="score">
        <el-input
          v-model="queryParams.score"
          placeholder="请输入分数"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['system:score:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="mini"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['system:score:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="mini"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['system:score:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="mini"
          @click="handleExport"
          v-hasPermi="['system:score:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <el-table v-loading="loading" :data="scoreList" @selection-change="handleSelectionChange">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="主键ID" align="center" prop="id" />
      <el-table-column label="创建人姓名" align="center" prop="createUserName" />
      <el-table-column label="课程" align="center" prop="courseName" />
      <el-table-column label="学生姓名" align="center" prop="nickName" />
      <el-table-column label="分数" align="center" prop="score" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['system:score:edit']"
          >修改</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['system:score:remove']"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total>0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改分数对话框 -->
    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="课程" prop="courseId">
          <el-select v-model="form.courseId" filterable placeholder="请选择课程" style="width: 100%;">
            <el-option v-for="obj in this.courseData"
                       :label="obj.courseName"
                       :value="obj.id"
                       :key="obj.id">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="学生" prop="userId">
          <el-select v-model="form.userId" filterable placeholder="请选择学生" style="width: 100%;">
            <el-option v-for="obj in this.studentData"
                       :label="obj.nickName"
                       :value="obj.userId"
                       :key="obj.userId">
            </el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="分数" prop="score">
          <el-input v-model="form.score" placeholder="请输入分数" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { listScore, getScore, delScore, addScore, updateScore, getAllStudent, getNormalCourse} from "@/api/system/score";

export default {
  name: "Score",
  data() {
    return {
      // 遮罩层
      loading: true,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 分数表格数据
      scoreList: [],
      // 弹出层标题
      title: "",
      // 是否显示弹出层
      open: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        createUserName: null,
        courseId: null,
        userId: null,
        score: null
      },
      // 表单参数
      form: {},
      courseData: [], //课程数据集合
      studentData: [], //学生数据集合
      // 表单校验
      rules: {
      }
    };
  },
  created() {
    this.getList();
    this.getNormalCourse();
    this.getAllStudent();
  },
  methods: {

    /** 获取状态为可用的课程数据 */
    getNormalCourse() {
      let courseData = {
        courseStatus: 2,
        pageNum: 1,
        pageSize: 10,
        createUserName: null,
        courseId: null,
        userId: null,
        score: null
      };
      getNormalCourse(courseData).then(response => {
        this.courseData = response.data;
      });
    },

    /** 获取角色为学生的数据 */
    getAllStudent() {
      getAllStudent().then(response => {
        this.studentData = response.data;
      });
    },

    /** 查询分数列表 */
    getList() {
      this.loading = true;
      listScore(this.queryParams).then(response => {
        this.scoreList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },
    // 取消按钮
    cancel() {
      this.open = false;
      this.reset();
    },
    // 表单重置
    reset() {
      this.form = {
        id: null,
        createTime: null,
        createUserName: null,
        courseId: null,
        userId: null,
        score: null
      };
      this.resetForm("form");
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.id)
      this.single = selection.length!==1
      this.multiple = !selection.length
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加分数";
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      this.reset();
      const id = row.id || this.ids
      getScore(id).then(response => {
        this.form = response.data;
        this.open = true;
        this.title = "修改分数";
      });
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          if (this.form.id != null) {
            updateScore(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addScore(this.form).then(response => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      const ids = row.id || this.ids;
      this.$modal.confirm('是否确认删除分数编号为"' + ids + '"的数据项?').then(function() {
        return delScore(ids);
      }).then(() => {
        this.getList();
        this.$modal.msgSuccess("删除成功");
      }).catch(() => {});
    },
    /** 导出按钮操作 */
    handleExport() {
      this.download('system/score/export', {
        ...this.queryParams
      }, `score_${new Date().getTime()}.xlsx`)
    }
  }
};
</script>

然后score.js文件中新增如下内容:

// 获取角色为学生的数据
export function getAllStudent() {
  return request({
    url: '/system/user/getAllStudent',
    method: 'get'
  })
}

// 获取状态为可用的课程数据
export function getNormalCourse(query) {
  return request({
    url: '/system/user/getNormalCourse',
    method: 'get',
    params: query
  })
}

页面F5刷新,新增表单就可以正常选择了:

image-20221207150053414

选择学生:

image-20221207150450335

新增成功后列表展示如下:

image-20221207150559745

新增成功了,但是课程ID和用户ID显示有问题,应该是显示对应的课程名称和用户昵称,那就是查询接口需要改造一下,需要连表查询,修改TScoreMapper.xml文件中的selectTScoreList方法,修改后如下:

<select id="selectTScoreList" parameterType="TScore" resultMap="TScoreResult">
    select t1.*, t2.nick_name as nickName, t3.course_name as courseName
    from t_score t1
    left join sys_user t2 on t1.user_id = t2.user_id
    left join t_course t3 on t1.course_id = t3.id
    <where>  
        <if test="createUserName != null  and createUserName != ''"> and t1.create_user_name like concat('%', #{createUserName}, '%')</if>
        <if test="courseId != null "> and t1.course_id = #{courseId}</if>
        <if test="userId != null "> and t1.user_id = #{userId}</if>
        <if test="score != null "> and t1.score = #{score}</if>
    </where>
</select>

TScore实体类需要新增下面字段:

/** 用户昵称 */
private String nickName;
/** 课程名称 */
private String courseName;

public String getNickName() {
    return nickName;
}
public void setNickName(String nickName) {
    this.nickName = nickName;
}
public String getCourseName() {
    return courseName;
}
public void setCourseName(String courseName) {
    this.courseName = courseName;
}

然后页面字段修改一下即可(上面源码已经给出),最终效果如下:

image-20221207151410280

到此我们的功能就做完了

9、总结

image-20221230151823215

希望本文对大家有所帮助

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116613.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!