基于Springboot+MybatisPlus+Layui实现小型教育网站
项目简介:
该项目采用Springboot框架实现疫情下学生和教师通过一个教育平台来进行教学与交流,该项目主要涵盖的技术点是使用Echarts平台上开源图标进行数据的可视化图表呈现,网站UI整体采用Layui开源网页代码,数据库采用MySQL(后期可能会引入Redis缓存),MybatisPlus实现数据的增删改查,集成Shiro实现登陆功能,以及不同用户之间菜单栏的动态显示,用户主要分为:超级管理员,管理员,老师和学生,并实现权限的动态设置,由Maven进行依赖管理
开发环境:
**Java:**JDK1.8
**开发工具:**IDEA(2022.1.3),Maven,Sequel Pro(连接数据库)
**前端框架:**Layui,Echart图表
**后端框架:**Springboot,MybatisPlus,Shiro
**数据库:**MySQL 5.7
一、集成Shiro完成登陆和资源控制
1.1 Shiro简介
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。相对于传统的session后台查找模式吗,Shiro是一个轻量框架,能够减轻服务端的压力,并且能够轻松完成web应用的权限管理,Shiro主要由三部分组成:
Subject**:**主体,应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject , 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager**:**安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm**:**域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
1.2 引入pom依赖
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--shiro和thymeleaf集成的扩展依赖,为了能在页面上使用xsln:shiro的标签-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
1.3 编写UserRealm
在主程序中创建名为realm的文件来写Shiro对认证和配置的方法
public class UserRealm extends AuthorizingRealm {
@Autowired
@Lazy
private UserService userService;
@Override
public String getName() {
return this.getClass().getSimpleName();
}
/**
* 认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.查询数据库
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",token.getPrincipal().toString());
User user = userService.getOne(queryWrapper);
if (null != user){
//盐 时用户uuid生成的
//ByteSource salt = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
return info;
}
return null;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
return null;
}
}
1.4 ShiroConfig
在主程序下的Config文件包下去配置Shiro的基本配置(拦截和过滤)
package com.example.springboot2022nocv.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.example.springboot2022nocv.realm.UserRealm;
import lombok.Data;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(value = { SecurityManager.class })
@ConfigurationProperties(prefix = "shiro")
@Data
public class ShiroConfig {
private static final String SHIRO_DIALECT = "shiroDialect";
private static final String SHIRO_FILTER = "shiroFilter";
// 加密方式
//private String hashAlgorithmName = "md5";
// 散列次数
//private int hashIterations = 2;
// 默认的登陆页面
private String loginUrl = "/china.html";
private String[] anonUrls; // 放行的路径
private String logOutUrl; // 登出的地址
private String[] authcUlrs; // 拦截的路径
/**
* 声明凭证匹配器
*/
/*@Bean("credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(hashAlgorithmName);
credentialsMatcher.setHashIterations(hashIterations);
return credentialsMatcher;
}*/
/**
* 声明userRealm
*/
@Bean("userRealm")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
return userRealm;
}
/**
* 配置SecurityManager
*/
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入userRealm
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 配置shiro的过滤器
*/
@Bean(SHIRO_FILTER)
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
factoryBean.setSecurityManager(securityManager);
// 设置未登陆的时要跳转的页面
factoryBean.setLoginUrl(loginUrl);
Map<String, String> filterChainDefinitionMap = new HashMap<>();
// 设置放行的路径
if (anonUrls != null && anonUrls.length > 0) {
for (String anon : anonUrls) {
filterChainDefinitionMap.put(anon, "anon");
}
}
// 设置登出的路径
if (null != logOutUrl) {
filterChainDefinitionMap.put(logOutUrl, "logout");
}
// 设置拦截的路径
if (authcUlrs != null && authcUlrs.length > 0) {
for (String authc : authcUlrs) {
filterChainDefinitionMap.put(authc, "authc");
}
}
Map<String, Filter> filters=new HashMap<>();
factoryBean.setFilters(filters);
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factoryBean;
}
/**
* 注册shiro的委托过滤器,相当于之前在web.xml里面配置的
* @return
*/
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> delegatingFilterProxy() {
FilterRegistrationBean<DelegatingFilterProxy> filterRegistrationBean = new FilterRegistrationBean<DelegatingFilterProxy>();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName(SHIRO_FILTER);
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/* 加入注解的使用,不加入这个注解不生效--开始 */
/**
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/* 加入注解的使用,不加入这个注解不生效--结束 */
/**
* 这里是为了能在html页面引用shiro标签,上面两个函数必须添加,不然会报错
*
* @return
*/
@Bean(name = SHIRO_DIALECT)
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
1.5 static下的静态index.html跳转页面
写在static文件夹下的index.html文件主要用作页面跳转
<!--用来跳转-->
<script type="text/javascript">
window.location.href="/toLogin";
</script>
1.6 编写yml配置过滤路径
在application.yml文件中去编写程序的拦截和过滤文件,即决定了我们必须先登陆认证,才能获得进入主程序的进一步权限访问
一言以概之,Shiro框架就是: 认证 => 授权
# shiro配置路径
shiro:
anon-urls:
- /toLogin*
- /login.html*
- /login/login
- /login/getCode
- /css/**
- /echarts/**
- /images/**
- /layui/**
- /layui_ext/**
- /js/**
- /sass/**
- /fonts/**
- /dummy/**
- /video/**
login-url: /index.html
login-out-url: /login/logout*
authc-ulrs:
- /**
1.7 验证码逻辑
引入hutool工具类进行生成验证码,验证验证码的工作
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.8</version>
</dependency>
@RequestMapping("/getCode")
public void getCode(HttpServletResponse response, HttpSession session) throws IOException {
//HuTool定义图形验证码的长和宽,验证码的位数,干扰线的条数
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(116, 36,4,10);
session.setAttribute("code",lineCaptcha.getCode());
try {
ServletOutputStream outputStream = response.getOutputStream();
lineCaptcha.write(outputStream);
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
二、以课程为例实现后台基本的增删改查
2.1引入相关依赖
在本次项目中使用Springboot+MyBatisPlus去实现一个后台的业务逻辑,在创建项目之前只选择了四个依赖: Sping Web, Lombok, Thymeleaf, JDBC API
然后在项目中去添加MybatisPlus和Springboot所需要的依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2.2 连接数据库
在MySQL数据库中先创建一个名为edu的数据库,再在其中创建一个名为course的数据表,配置如下
变量名 | 变量类型 | 变量说明 |
---|---|---|
id | INT | 主键 |
course_name | VARCHAR | 课程名称 |
teacher | VARCHAR | 任课老师 |
student_num | INT | 上课人数 |
course_num | INT | 总课时数 |
start_time | DATETIME | 开始时间 |
end_time | DATETIME | 结束时间 |
content | VARCHAR | 课程概况 |
数据库创建完然后在我们的项目中进行数据库的连接,在application.yml进行数据库配置
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/edu?serverTimezone=UTC&useSSL=false&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
2.3 创建数据库实体(Entity)对象
在entity包中用于存放我们的实体类,与数据库中的属性值基本保持一致。
@Data
@TableName("course")
public class Course {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String courseName;
private String teacher;
private Integer studentNum;
private Integer courseNum;
private Date startTime;
private Date endTime;
private String content;
}
@TableName的注解用来链接数据表
我们在MySQL中一般采用下划线命名,而在Java中我们必须严格遵循驼峰命名法,在MySQL中是不区分大小写的,这样才能完成数据库数据的导入
2.4 创建Mapper对象
在mapper包(或者dao包)中, 对数据库进行数据持久化操作,他的方法语句是直接针对数据库操作的,可以使用**@Select、@Delet、@Insert**等注解对数据库直接操作
ublic interface CourseMapper extends BaseMapper<Course> {
}
2.5 创建Service包
service包下的类是针对 controller层的 controller,也就是针对我们使用者。service的 impl 是把mapper和service进行整合的文件
先创建接口(Interface)
public interface CourseService extends IService<Course> {
}
再创建应用类(Impl)
@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {
}
2.6 分页配置MyBatisPlusConfig
与Shiro框架一样,我们在前台进行分页展示的时候,先进行MyBatisPlusConfig的配置
@Configuration
@ConditionalOnClass(value = {PaginationInterceptor.class})
public class MybatisPlusConfig {
@Bea@Service
public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseService {
}
n
public PaginationInterceptor paginationInterceptor(){
return new PaginationInterceptor();
}
}
2.7 Layui返回的数据格式 DataView
在工程中再去创建名为vo的包,用作前台文件展示,会直接返回给前台
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DataView {
private Integer code = 0;
private String msg = "";
private Long count = 0L;
private Object data;
public DataView(Long count,Object data){
this.count = count;
this.data = data;
}
public DataView(Object data){
this.data = data;
}
}
主要存放返回错误码,返回提示信息和分页信息
2.8 编写Controller文件
本次项目中没有使用RestFul接口风格,所以采用最原始的@RequestMapping和@ResponseBody来实现最基础的功能
1.展示数据(带模糊查询)
//注入
@Autowired
private CourseService courseService;
//带有模糊查询的内容展示
@RequestMapping("/listDataByPage")
@ResponseBody
public DataView listDataByPage(CourseVo courseVo) {
//1.创建分页对象 限制每页展示内容数量
IPage<Course> page = new Page<>(courseVo.getPage(), courseVo.getLimit());
//2.创建模糊查询条件
QueryWrapper<Course> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(courseVo.getCourseName()), "courseName", courseVo.getCourseName());
courseService.page(page, queryWrapper);
return new DataView(page.getTotal(), page.getRecords());
}
2.删除数据
//删除课程
@RequestMapping("/deleteById")
@ResponseBody
public DataView deleteById(Integer id) {
boolean save = courseService.removeById(id);
DataView dataView = new DataView();
if (save) {
dataView.setCode(200);
dataView.setMsg("删除成功!");
return dataView;
}
dataView.setCode(100);
dataView.setMsg("删除失败!");
return dataView;
}
3.增加或删除数据
//新增或修改id
@RequestMapping("/addOrUpdateCourse")
@ResponseBody
public DataView addOrUpdateCourse(Course course) {
boolean save = courseService.saveOrUpdate(course);
DataView dataView = new DataView();
if (save) {
dataView.setCode(200);
dataView.setMsg("新增课程数据成功!");
return dataView;
}
dataView.setCode(100);
dataView.setMsg("新增课程数据失败!");
return dataView;
}
三、实现拖拽Excel导入课程信息
3.1 引入pom依赖
<!--引入poi-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.0</version>
</dependency>
3.2 引入layui的拖拽上传Excel组件
基本思路
- Excel的拖拽或者点击上传
- 1.前台页面发送一个请求,上传文件MutilpartFile HTTP
- 2.Controller,上传文件MutilpartFile 参数
- 3.POI 解析文件,里面的数据一行一行全部解析出来
- 4.每一条数据插入数据库
- 5.mybatiplus 批量saveBatch(list)
前端代码:
<div class="layui-upload-drag" id="test10">
<i class="layui-icon"></i>
<p>点击上传,或将文件拖拽到此处</p>
<div class="layui-hide" id="uploadDemoView">
<hr>
<img src="" alt="上传成功后渲染" style="max-width: 196px">
</div>
</div>
//js 代码
layui.use(['upload','jquery'],function(){
var layer = layui.layer //弹层
,$ = layui.jquery
,upload = layui.upload
//拖拽上传
upload.render({
elem: '#test10'
,url: '/excelImport'
,accept: 'file' //普通文件
,done: function(res){
layer.msg('上传成功');
console.log(res);
}
});
3.3 编写Controller
//Excel上传文件
@RequestMapping("/excelImportCourse")
@ResponseBody
public DataView excelImportCourse(@RequestParam("file") MultipartFile file) throws Exception {
DataView dataView = new DataView();
//1.判断文件是否为空
if (file.isEmpty()) {
dataView.setMsg("文件为空,不能上传!");
}
//2.POI 获取Excel数据
XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream());
XSSFSheet sheet = wb.getSheetAt(0);
//3.定义程序集合来接收文件内容
List<Course> list = new ArrayList<>();
XSSFRow row = null;
//4.接收数据 装入集合中
for (int i = 0; i < sheet.getPhysicalNumberOfRows(); i++) {
Course course = new Course();
row = sheet.getRow(i);
course.setCourseName(row.getCell(0).getStringCellValue());
course.setTeacher(row.getCell(1).getStringCellValue());
course.setStudentNum(Integer.valueOf(row.getCell(2).getStringCellValue()));
course.setCourseNum(Integer.valueOf(row.getCell(3).getStringCellValue()));
course.setStartTime(new SimpleDateFormat("yyyy-MM-dd").parse(row.getCell(4).getStringCellValue()));
course.setEndTime(new SimpleDateFormat("yyyy-MM-dd").parse(row.getCell(5).getStringCellValue()));
course.setContent(row.getCell(6).getStringCellValue());
list.add(course);
}
boolean save = courseService.saveBatch(list);
if (save) {
dataView.setCode(200);
dataView.setMsg("数据表插入成功!");
return dataView;
}
dataView.setCode(100);
dataView.setMsg("数据表插入失败!");
return dataView;
}
3.4 数据导出的Excel功能
基本思路
1.前端发送请求
2.后端查询数据库,封装数据Excel实体
3.返回数据建立输出,写出浏览器文件
/导出Excel表
@RequestMapping("/excelOutPortCourse")
@ResponseBody
public void excelOutPortChina(HttpServletResponse response) {
//1. 查询数据库 [查询所有,符合条件的数据查询出来]
List<Course> list = courseService.list();
//2.建立Excel对象,封装数据
response.setCharacterEncoding("UTF-8");
//2.1创建Excel对象
XSSFWorkbook wb = new XSSFWorkbook();
//2.3创建sheet对象
XSSFSheet sheet = wb.createSheet("中国疫情数据sheet1");
//2.3创建表头
XSSFRow xssfRow = sheet.createRow(0);
xssfRow.createCell(0).setCellValue("课程名称");
xssfRow.createCell(1).setCellValue("任课老师");
xssfRow.createCell(2).setCellValue("学生人数");
xssfRow.createCell(3).setCellValue("总课时数");
xssfRow.createCell(4).setCellValue("开课时间");
xssfRow.createCell(5).setCellValue("结课时间");
xssfRow.createCell(6).setCellValue("课程概要");
//3.遍历数据,封装Excel工作对象
for(Course data: list) {
XSSFRow dataRow = sheet.createRow(sheet.getLastRowNum() + 1);
dataRow.createCell(0).setCellValue(data.getCourseName());
dataRow.createCell(1).setCellValue(data.getTeacher());
dataRow.createCell(2).setCellValue(data.getStudentNum());
dataRow.createCell(3).setCellValue(data.getCourseNum());
dataRow.createCell(4).setCellValue(data.getStartTime());
dataRow.createCell(5).setCellValue(data.getEndTime());
dataRow.createCell(6).setCellValue(data.getContent());
}
//4.建立输出流,输出浏览器文件
OutputStream os = null;
//4.1 设置Excel名字,数据类型编码
try {
response.setContentType("application/octet-stream;chartset=utf8");
response.setHeader("Content-Disposition", "attachment;filename=" + new String("课程统计表".getBytes(), "iso-8859-1") + ".xls");
//4.2 输出文件
os = response.getOutputStream();
wb.write(os);
os.flush();
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
if (os != null)
os.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
四、数据表的设计
4.1 数据库设计
1.menu菜单
id | pid | type | title | permission | icon | href | open | ordernum | available |
---|---|---|---|---|---|---|---|---|---|
1 | 0 | menu | 课程管理 | ; | /course/toCourse | 0 | 1 | 1 | |
11 | 0 | menu | 系统管理 | ; | 0 | 11 | 1 | ||
12 | 11 | menu | 菜单管理 | ; | /menu/toMenu | 0 | 12 | 1 |
2.role角色
id | name | remark |
---|---|---|
1 | 超级管理员 | 拥有所有权限 |
2 | 老师 | 查看新增修改 |
3 | 学生 | 查看 |
3.role_menu 关联关系表
rid | mid |
---|---|
1 | 1 |
1 | 2 |
4.user 用户表【老师,学生,管理员】
id | username | password | … | role_id | ban_ji_id | xue_yuan_id | teacher_id |
---|---|---|---|---|---|---|---|
1 | admin | 123456 | 1 | 1 | 1 | 0 |
5.ban_ji 班级表
id | name | xue_yuan_id |
---|---|---|
1 | 软件工程1班 | 1 |
6.xue_yuan学院表
id | name |
---|---|
1 | 计算机学院 |
4.2 Java实体的编写
五、修改index主题样式、菜单的增删改查
整个项目最后希望能够实现一个便捷化的交互方式,能够在前台就简单实现菜单的增加和删除,在菜单下增加子菜单,并且动态给予每个角色不同的菜单角色
5.1 引入pom依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
5.2 菜单的插入
创建名为utils包,里面放入菜单修改的Tree配置
TreeNode
package com.example.demo.controller;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* @Author:
* @Date: 2019/11/22 15:25
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TreeNode {
private Integer id;
@JsonProperty("parentId") //返回的json的名称 parentId ,为了确定层级关系
private Integer pid;
private String title;
private String icon;
private String href;
private Boolean spread;
private List<TreeNode> children = new ArrayList<TreeNode>();
/**
* 0为不选中 1为选中
*/
private String checkArr="0";
/**
* 首页左边导航菜单的构造器
*/
public TreeNode(Integer id, Integer pid, String title, String icon, String href, Boolean spread) {
this.id = id;
this.pid = pid;
this.title = title;
this.icon = icon;
this.href = href;
this.spread = spread;
}
/**
* 部门 dtree的构造器
* @param id id
* @param pid 父亲parentId
* @param title 名称
* @param spread 是否展开
*/
public TreeNode(Integer id, Integer pid, String title, Boolean spread) {
this.id = id;
this.pid = pid;
this.title = title;
this.spread = spread;
}
/**
* 给角色分配权限的构造器
*/
public TreeNode(Integer id, Integer pid, String title, Boolean spread, String checkArr) {
this.id = id;
this.pid = pid;
this.title = title;
this.spread = spread;
this.checkArr = checkArr;
}
}
TreeBuilder
public class TreeNodeBuilder {
public static List<TreeNode> build(List<TreeNode> treeNodes, Integer topPid) {
List<TreeNode> nodes = new ArrayList<TreeNode>();
for (TreeNode n1 : treeNodes) {
if (n1.getPid()==topPid){
nodes.add(n1);
}
for (TreeNode n2 : treeNodes) {
if (n1.getId()==n2.getPid()){
n1.getChildren().add(n2);
}
}
}
return nodes;
}
}
5.3 菜单栏的增删改查
1.菜单栏的展示(加载,刷新,回显)
//加载所有菜单
@RequestMapping("loadAllMenu")
@ResponseBody
public DataView loadAllMenu(MenuVo menuVo) {
IPage<Menu> page = new Page<>(menuVo.getPage(), menuVo.getLimit());
QueryWrapper<Menu> queryWrapper= new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(menuVo.getTitle()), "title", menuVo.getTitle());
queryWrapper.orderByAsc("ordernum");
menuService.page(page);
return new DataView(page.getTotal(), page.getRecords());
}
//加载下拉菜单数据 初始化下拉树
@RequestMapping("loadMenuManagerLeftTreeJson")
@ResponseBody
public DataView loadMenuManagerLeftTreeJson() {
List<Menu> list = menuService.list();
List<TreeNode> treeNodes = new ArrayList<>();
for (Menu menu : list) {
boolean open = menu.getOpen() == 1 ? true : false;
treeNodes.add(new TreeNode(menu.getId(), menu.getPid(), menu.getTitle(), open));
}
return new DataView(treeNodes);
}
//赋值最大的排序码加一
//条件查询:倒叙排序,取一条数据 +1
@RequestMapping("/loadMenuMaxOrderNum")
@ResponseBody
public Map<String, Object> loadMenuMaxOrderNum() {
Map<String, Object> map = new HashMap<>();
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("ordernum");
IPage<Menu> page = new Page<>(1, 1);
List<Menu> list = menuService.page(page, queryWrapper).getRecords();
map.put("value", list.get(0).getOrdernum() + 1);
return map;
}
2.新增菜单
//新增菜单
@RequestMapping("/addMenu")
@ResponseBody
public DataView addMenu(Menu menu) {
DataView dataView = new DataView();
menu.setType("menu");
boolean save = menuService.save(menu);
if (!save) {
dataView.setCode(100);
dataView.setMsg("数据插入失败!");
return dataView;
}
dataView.setMsg("菜单插入成功!");
dataView.setCode(200);
return dataView;
}
3.更新菜单
//更新菜单
@RequestMapping("/updateMenu")
@ResponseBody
public DataView updateMenu(Menu menu) {
menuService.updateById(menu);
DataView dataView = new DataView();
dataView.setCode(200);
dataView.setMsg("更新菜单成功!");
return dataView;
}
4.删除菜单(判断 删除)
删除菜单分两种情况:1.有子类ID 不能删除
2.没有子类ID 直接删除
//判断是否有父级ID
//没有子类ID,可以删除 【】
@RequestMapping("/checkMenuHasChildrenNode")
@ResponseBody
public Map<String, Object> checkChildrenNode (Menu menu) {
Map<String, Object> map = new HashMap<>();
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("pid", menu.getId());
List<Menu> list = menuService.list(queryWrapper);
if (list.size() > 0) {
map.put("value", true);
} else {
map.put("value", false);
}
return map;
}
//删除
@RequestMapping("/deleteMenu")
@ResponseBody
public DataView deleteMenu(Menu menu) {
menuService.removeById(menu.getId());
DataView dataView = new DataView();
dataView.setCode(200);
dataView.setMsg("删除菜单成功!");
return dataView;
}
5.4 修改index主菜单为动态查库
1.修改样式 引入 js css
2.配置yml放行js包
3.原项目修改index.html 为 china.html 删除 commonmenu.html 引入 静态资源包里面的 index.html
4.去掉其它页面 的 引入,添加
去除所有的
模块5.修改 indexcontroller 的请求/路径,添加一个/self/index
6.修改数据库/self/index
7.编写Controller
/**
* 加载最外层index菜单
*/
@RequestMapping("loadIndexLeftMenuJson")
@ResponseBody
public DataView loadIndexLeftMenuJson(Menu permissionVo){
//查询所有菜单
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
List<Menu> list = menuService.list();
List<TreeNode> treeNodes = new ArrayList<>();
for (Menu p : list) {
Integer id =p.getId();
Integer pid = p.getPid();
String title = p.getTitle();
String icon = p.getIcon();
String href = p.getHref();
Boolean spread = p.getOpen().equals(1)?true:false;
treeNodes.add(new TreeNode(id,pid,title,icon,href,spread));
}
//构造层级关系
List<TreeNode> list2 = TreeNodeBuilder.build(treeNodes,0);
return new DataView(list2);
}
六、角色【管理员、学生、老师】CRUD,分配菜单权限
6.1 角色的增删改查【条件查询带有分页】
1.引入role的静态页面
页面进行菜单的增加
6.2 为角色分配菜单权限
1.分配权限 menu【菜单的操作资源】id
2.分配角色 role【用户 管理员 学生 教师】id
3.关联表role_menu:【全都可以为空,不能有主键,都是外键属性】
rid mid
1 1
1 2
select mid from role_menu where rid = ?
List 所具有的菜单栏权限
/**
* 1.初始化下拉列表的权限
*/
@Autowired
private MenuService menuService;
@RequestMapping("/initPermissionByRoleId")
@ResponseBody
public DataView initPermissionByRoleId(Integer roleId){
//查询所有菜单和权限
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
List<Menu> allPermissions = menuService.list();
//1.首先根据角色id查询出当前角色所拥有的所有菜单的ID和权限的ID
List<Integer> currentRolePermissions = roleService.queryRolePermissionIdsByRid(roleId);
//2.根据查询出来的菜单ID和权限ID,再查询出菜单的数据和权限的数据
List<Menu> currentPermissions = null;
//如果根据角色id查询出来了菜单ID或权限ID,就去查询
if (currentRolePermissions.size()>0){
queryWrapper.in("id",currentRolePermissions);
currentPermissions = menuService.list(queryWrapper);
}else {
currentPermissions = new ArrayList<>();
}
//3.构造List<TreeNode>
List<TreeNode> nodes = new ArrayList<>();
for (Menu allPermission : allPermissions) {
String checkArr = "0";
for (Menu currentPermission : currentPermissions) {
if (allPermission.getId().equals(currentPermission.getId())){
checkArr = "1";
break;
}
}
Boolean spread = (allPermission.getOpen()==null||allPermission.getOpen()==1)?true:false;
nodes.add(new TreeNode(allPermission.getId(),allPermission.getPid(),allPermission.getTitle(),spread,checkArr));
}
return new DataView(nodes);
}
@Select("select mid from role_menu where rid = #{roleId}")
List<Integer> queryRolePermissionIdsByRid(Integer roleId);
//分配菜单权限【角色与菜单之间的关系】
// 1.分配菜单栏之前删除所有的rid数据
@Delete("delete from role_menu where rid = #{rid}")
void deleteRoleByRid(Integer rid);
// 2.保存分配 角色 与 菜单 的关系
@Insert("insert into role_menu(rid,mid) values (#{rid},#{mid})")
void saveRoleMenu(Integer rid, Integer mid);
6.3 不同角色登陆看到不同的菜单栏
// 查询的所有菜单栏 按照条件查询【管理员,学生 老师【条件查询】】
QueryWrapper<Menu> queryWrapper = new QueryWrapper<>();
List<Menu> list = null;
// 1.取出session中的用户ID
User user = (User) session.getAttribute("user");
Integer userId = user.getId();
// 2.根据用户ID查询角色ID
List<Integer> currentUserRoleIds = roleService.queryUserRoleById(userId);
// 3.去重
Set<Integer> mids = new HashSet<>();
for (Integer rid : currentUserRoleIds){
// 3.1.根据角色ID查询菜单ID
List<Integer> permissionIds = roleService.queryAllPermissionByRid(rid);
// 3.2.菜单栏ID和角色ID去重
mids.addAll(permissionIds);
}
// 4.根据角色ID查询菜单ID
if (mids.size()>0){
queryWrapper.in("id",mids);
list = menuService.list(queryWrapper);
}