1、若依框架介绍
- 官网地址:http://ruoyi.vip
- RuoYi是一个后台管理系统,基于经典技术组合(Spring Boot、Apache Shiro、MyBatis、Thymeleaf)主要目的让开发者注重专注业务,降低技术难度,从而节省人力成本,缩短项目周期,提高软件安全质量
- 若依是给女儿取的名字(寓意:你若不离不弃,我必生死相依)
目前提供的版本有:
- RuoYi(SpringBoot+Bootstrap)
- RuoYi-Vue(SpringBoot+Vue)
- RuoYi-Cloud(SpringCloud+Vue)
- RuoYi-App(Uniapp+Vue)
我们这里主要介绍RuoYi-Vue这个前后端分离的版本
2、本地运行介绍
2.1、下载代码
访问网站:https://gitee.com/y_project/RuoYi-Vue
我这里采用第一种方式,先将代码克隆一份到本地磁盘
然后再使用IDEA打开该项目即可:
第一次下载相关依赖jar包可能需要的时间稍微长一点,耐心等一会即可
2.2、准备数据库
说明:我这里本机数据库的版本是:5.5.20
将项目目录中sql文件夹中的ry_20220822.sql文件拿出来到Navicat工具中跑一下(我这里就没有执行quartz.sql,因为我们这里暂时用不到定时任务相关表)
脚本执行完毕后,数据库表情况如上图所示
2.3、准备Redis
想要启动项目,必须要准备一台Redis服务器,我这里在本地直接启动一个Redis,密码是123456,所以需要修改【application.yml】文件:
2.4、启动即可
数据库和Redis准备好之后,就可以运行项目了,直接找到启动类,执行main方法就可以启动项目了:
运行成功:
可以清楚看到,项目启动成功了,端口是8080
2.5、前端运行
运行npm install后,根据网速不同,需要耐心等待一会
安装完毕后,继续执行命令:npm run dev,运行成功后就会自动打开登录首页:
看到此页面,说明前端运行成功
账号密码都是admin,直接登录即可
注意:如果发现密码不对的话,我这里自己生成一个123456的密码:
$2a$10$fi5GezS2kaXH4Bhk7ZzgnOJiR4ywlLBkyQ5kiK1bMr8C40w7u.kSe
将上面值直接复制粘贴到sys_user表中的password中即可,这样我们就可以使用admin/123456进行登录了,登录成功后的首页如下:
至此,我们就将若依的前后台都运行完成,在本地成功跑起来了
是不是很简单呢,哈哈,赶紧动手做一做吧
3、架构介绍和功能说明
3.1、文件结构介绍
2.2、技术栈介绍
- 前端采用Vue、Element UI
- 后端采用Spring Boot、Spring Security、Redis & Jwt
- 权限认证使用Jwt,支持多终端认证系统
- 支持加载动态权限菜单,多方式轻松权限控制
- 高效率开发,使用代码生成器可以一键生成前后端代码
2.3、功能介绍
- 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
- 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
- 岗位管理:配置系统用户所属担任职务。
- 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
- 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
- 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- 参数管理:对系统动态配置常用参数。
- 通知公告:系统通知公告信息发布维护。
- 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
- 登录日志:系统登录日志记录查询包含登录异常。
- 在线用户:当前系统中活跃用户状态监控。
- 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
- 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
- 系统接口:根据业务代码自动生成相关的api接口文档。
- 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
- 缓存监控:对系统的缓存信息查询,命令统计等。
- 在线构建器:拖动表单元素生成相应的HTML代码。
- 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
4、核心功能讲解
4.1、用户管理
表结构如下:
4.2、角色管理
表结构如下:
4.3、菜单管理
表结构如下:
4.4、部门管理
表结构如下:
4.5、岗位管理
表结构如下:
4.6、RBAC模型
- 若依框架的权限管理功能是基于【RBAC】来实现的,即:系统中所有的权限,都是基于角色来控制的
- 框架对权限的控制,不仅支持菜单的功能,还支持菜单中的每一个按钮的权限控制
RBAC(基于角色的访问控制)模型包含的表有下面五张:
- 用户表
- 角色表
- 菜单表
- 用户角色关联表
- 角色菜单关联表
关系如下:
查询当前登录人有权限看到的菜单,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、数据字典
数据字典功能由两张表组成:
- sys_dict_type:字典类型表
- sys_dict_data:字典数据表
两者之间的关系:sys_dict_data表的dict_type字段关联sys_dict_type的dict_type字段
表结构如下:
sys_dict_type:
sys_dict_data:
查询某个数据字典类型的详情,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)
})
这个前置拦截器实际上做了这么几件事:
- 在请求头中添加token
- get请求映射params参数
- 阻止重复请求重复提交(虽然前端做了,但一般后台也需要做接口幂等性校验)
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)
}
)
在响应拦截器中做了下面这几件事:
- 对响应数据进行判断,如果是二进制数据则直接返回
- 对响应状态码进行了判断,不同的状态码做不同的处理
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、登录流程梳理
- 请求登录请求首先会给到后台的SysLoginController类的login方法
- 然后会执行SysLoginService的login方法
- 然后会执行UserDetailsServiceImpl的loadUserByUsername方法
- loadUserByUsername方法主要做了下面几件事:
- 校验登录账号是否在数据库中存在
- 校验登录账号是否已被删除
- 校验登录账号是否已停用
- 然后再调用SysPasswordService类的validate方法,主要做了下面几件事:
- 校验登录密码输入错误次数是否超限
- 校验密码是否正确
- 然后调用SysPermissionService类的getMenuPermission方法,获取登录人的菜单数据权限
- 最后记录相关登录日志(用线程池异步方式记录到数据库的,提高效率)
流程如下:
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方法作用:
- 从请求头中获取到token,然后解析得到我们设置进去的UUID
- 然后再以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);
}
上面代码逻辑如下:
- 通过SecurityUtils工具类从Security上下文中获取到登录用户信息
- 然后从用户信息中获取到该用户所拥有的权限字符串集合
- 然后做对比,看其中是否包含【@ss.hasPermi(‘system:user:export’)】中的权限字符串
- 包含就可以方法该方法,不包含就不能访问该方法
实际上若依提供的注解有下面这些:(看PermissionService类就知道了)
使用示例:
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') >= 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') <= 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、自动生成代码
在本地数据库中新建上面两个表后,就可以访问系统直接生成代码了,如下:
下载下来的压缩包加压后包含下面文件:
这样前后台代码就都已经准备好了,菜单脚本也有了,接下来我们就直接将文件拷贝到项目中就可以了
开始拷贝
8.4、前端页面
将下载好的前端页面按照上图所示拷贝到前端项目的system目录即可,然后再拷贝js文件,如下图所示:
这样就可以了,先什么都不改,就这样就可以重启一下前端项目了
8.5、后台代码
先拷贝controller代码到指定目录即可,如下:
再拷贝domain、mapper、service代码到对应目录,如下:
在拷贝xml文件到对应目录,如下:
拷贝完毕之后,什么都不需要改,直接重启项目即可
8.6、菜单配置
我们先看下生成SQL脚本,下面是t_score表的SQL:
下面是t_course表的SQL:
很明显两个表菜单都挂在了ID为3的菜单下面,查看数据库:
也就是说,我这两个表的菜单都会默认挂在【系统工具】菜单下面,然后所有脚本直接执行一下即可
最终数据库中就会创建出如下数据了:
那我们菜单也就准备好了
我们登录系统后,点开目录【系统工具】就可以看到我们刚执行SQL新增的菜单了:
OK,菜单配置成功
8.7、配置角色和用户
目前菜单有了,但是老师和学生还看不到,所以需要新增角色并赋予相应权限才能看到
我们先添加两个角色:老师和学生
进入【系统管理】-【角色管理】菜单,点击【新增】按钮,新增老师角色并勾上赋予其的相关菜单按钮权限即可:
再新建学生角色,并勾上赋予其的相关菜单按钮权限即可:
然后我们再新增两个用户,一个老师,一个学生,进入【系统管理】-【用户管理】菜单,点击【新增】按钮
再新增一个学生:
到这,我添加的用户信息如下:
老师角色:张三,登录账号:zs,登录密码:123456
学生角色:王天霸,登录账号:wtb,登录密码:123456
我们现在登录一下zs账号:
现在作为老师角色的用户,就可以对课程信息和分数信息进行CRUD了,只是可能有些字段需要微调一下,我们接下来就完善一下相关功能
8.8、前后端联调测试&功能完善
默认的前后台代码还需要微调一下,我们开始根据需求调整一下吧
8.8.1、课程管理
首先前端页面目前新增框是这样的:
- 这里【创建人姓名】应该是后台接口中获取当前登录人姓名赋值即可,不由前端给值,所以该字段这里可以删除
- 课程表还有一个字段,叫做课程状态(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刷新页面,现在表单就是这样的了:
我们新增完数据后的列表就是这样的:
但是有个问题,创建人姓名里面没有值,这个值需要后台新增操作时,就将当前登录人的姓名存入数据库即可,所以我们修改后台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));
}
8.8.2、分数管理
自动生成出来的页面中,添加分数时是这样的表单:
明显不符合要求
- 【创建人姓名】应该是后台接口中给值,不由前端传值,这里应该删除
- 【课程ID】应该是一个下拉框,选择需要添加哪门课程
- 【用户ID】应该是一个下拉框,选择所有学生
首先后台需要提供两个接口:
- 查询所有状态为可用的课程数据,前端下拉框去动态显示所有课程数据
- 查询所有角色为学生的数据,前端下拉框去动态显示所有学生信息
两个接口我都写到了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刷新,新增表单就可以正常选择了:
选择学生:
新增成功后列表展示如下:
新增成功了,但是课程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;
}
然后页面字段修改一下即可(上面源码已经给出),最终效果如下:
到此我们的功能就做完了
9、总结
希望本文对大家有所帮助
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/116613.html