Spring Boot + MyBatisPlus + ElementUI 实现后台管理系统之 01-需求分析及项目结构搭建
AI 摘要
开发环境
开发环境使用 IDEA 2022.3、Navicat 15、MySQL 5.6、JDK 1.8、Maven 3.6.3、Spring Boot 2.6.13、MyBatis Plus 3.5.3。
接口统一响应规范
在我们早期项目中,控制器返回数据给前端,封装了一个 Map 对象,再将 Map 对象转成 JSON 字符串。
// 定义结果集Map对象
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "获取新闻分类列表成功");
// 调用Service层查询新闻分类列表
List<Classify> list = classifyService.list();
// 将列表数据放入结果集对象中
result.put("data", list);
// 将结果集对象转换成JSON字符串并返回
return JSON.toJSONString(result);为什么需要这个 Map 对象?Map 对象里面为什么要去封装 code、message、data?
主要还是为了统一后端响应给前端的数据格式,不至于后端返回给前端的数据很乱,这是一种接口开发规范。
在实际的开发当中,每一个方法都定义 Map 对象,再进行 JSON 字符串格式化,显得非常麻烦。我们可以选择返回一个统一响应结果实体类(ResponseResult)。方法直接返回统一响应结果实体类,也能够实现和 Map 对象相似的效果。
创建响应状态枚举类 ResponseStatus,用于保存响应状态,比如 200 表示成功,-1 表示失败,500 表示错误,401 表示未登录等。
/**
* 响应状态枚举
*
* @author 多仔ヾ
*/
@AllArgsConstructor
public enum ResponseStatus {
SUCCESS(200, "操作成功"),
FAILURE(-1, "操作失败"),
OF_LOGINED(401, "请先登录"),
FORBIDDEN(403, "暂无权限"),
NOT_FOUND(404, "请求出错啦,系统无法请求该资源"),
ERROR(500, "请求出错啦,系统正在开小差,请稍后再试");
/** 响应码 **/
public final Integer code;
/** 响应信息 **/
public final String message;
}创建响应结果实体类 ResponseResult,用于替代 Map 对象。
public class ResponseResult<T> implements Serializable {
/** 响应码 **/
public Integer code;
/** 响应信息 **/
public String message;
/** 响应数据 **/
@JsonInclude(JsonInclude.Include.NON_NULL)
public T data;
/** 响应时间 **/
public Long timestamp;
/**
* 响应成功
*
* @author 多仔ヾ
* @return 响应结果实体
**/
public static <T> ResponseResult<T> success() {
return resp(ResponseStatus.SUCCESS.code, ResponseStatus.SUCCESS.message, null);
}
/**
* 响应成功
*
* @author 多仔ヾ
* @param message 响应信息
* @return 响应结果实体
**/
public static <T> ResponseResult<T> success(String message) {
return resp(ResponseStatus.SUCCESS.code, message, null);
}
/**
* 响应成功
*
* @author 多仔ヾ
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> success(T data) {
return resp(ResponseStatus.SUCCESS.code, ResponseStatus.SUCCESS.message, data);
}
/**
* 响应成功
*
* @author 多仔ヾ
* @param message 响应信息
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> success(String message, T data) {
return resp(ResponseStatus.SUCCESS.code, message, data);
}
/**
* 响应失败
*
* @author 多仔ヾ
* @return 响应结果实体
**/
public static <T> ResponseResult<T> failure() {
return resp(ResponseStatus.FAILURE.code, ResponseStatus.FAILURE.message, null);
}
/**
* 响应失败
*
* @author 多仔ヾ
* @param message 响应信息
* @return 响应结果实体
**/
public static <T> ResponseResult<T> failure(String message) {
return resp(ResponseStatus.FAILURE.code, message, null);
}
/**
* 响应失败
*
* @author 多仔ヾ
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> failure(T data) {
return resp(ResponseStatus.FAILURE.code, ResponseStatus.FAILURE.message, data);
}
/**
* 响应失败
*
* @author 多仔ヾ
* @param message 响应信息
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> failure(String message, T data) {
return resp(ResponseStatus.FAILURE.code, message, data);
}
/**
* 响应异常
*
* @author 多仔ヾ
* @return 响应结果实体
**/
public static <T> ResponseResult<T> error() {
return resp(ResponseStatus.ERROR.code, ResponseStatus.ERROR.message, null);
}
/**
* 响应异常
*
* @author 多仔ヾ
* @param message 响应信息
* @return 响应结果实体
**/
public static <T> ResponseResult<T> error(String message) {
return resp(ResponseStatus.ERROR.code, message, null);
}
/**
* 响应异常
*
* @author 多仔ヾ
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> error(T data) {
return resp(ResponseStatus.ERROR.code, ResponseStatus.ERROR.message, data);
}
/**
* 响应异常
*
* @author 多仔ヾ
* @param message 响应信息
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> error(String message, T data) {
return resp(ResponseStatus.ERROR.code, message, data);
}
/**
* 响应结果
*
* @author 多仔ヾ
* @param code 响应码
* @param message 响应信息
* @return 响应结果实体
**/
public static <T> ResponseResult<T> resp(Integer code, String message) {
return resp(code, message, null);
}
/**
* 响应结果
*
* @author 多仔ヾ
* @param responseStatus 响应状态
* @return 响应结果实体
**/
public static <T> ResponseResult<T> resp(ResponseStatus responseStatus) {
return resp(responseStatus.code, responseStatus.message, null);
}
/**
* 响应结果
*
* @author 多仔ヾ
* @param code 响应码
* @param msg 响应信息
* @param data 响应数据
* @return 响应结果实体
**/
public static <T> ResponseResult<T> resp(Integer code, String msg, T data) {
ResponseResult<T> responseResult = new ResponseResult<>();
responseResult.code = code;
responseResult.message = msg;
responseResult.data = data;
responseResult.timestamp = System.currentTimeMillis() / 1000L;
return responseResult;
}
}新建测试控制器 DemoController,编写测试代码并进行访问测试。
@GetMapping("/test")
public ResponseResult test() {
// 调用success方法,code=200,message=操作成功
return ResponseResult.success();
}Spring Boot 默认的序列化是 Jackson。在 ResponseResult 中,有一个 @JsonInclude(JsonInclude.Include.NON_NULL),限制 data 对象不为 null 时参与序列化。
HuTool 工具箱
Hutool 是一个小而全的 Java 工具类库,包含了字符串工具、日期时间工具等。实际开发时,我们不会过多地去封装工具类,会用一些现成的工具类库来提高我们的开发效率。
修改 pom.xml,引入 HuTool 依赖。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>参考 HuTool 文档,选取几个工具进行测试。
//当前时间
Date date = DateUtil.date();
//当前时间
Date date2 = DateUtil.date(Calendar.getInstance());
//当前时间
Date date3 = DateUtil.date(System.currentTimeMillis());
//当前时间字符串,格式:yyyy-MM-dd HH:mm:ss
String now = DateUtil.now();
//当前日期字符串,格式:yyyy-MM-dd
String today= DateUtil.today();MD5 盐值加密
一般我们在数据库中存储账号密码,以明文的方式进行存储,数据是不安全的。因此,我们在数据库中存储密码的时候,应该要对密码进行加密。
MD5 是一种加密方式,常用于密码加密。MD5 可以把密码加密成 32 位小写字符串等格式。网上有这样子的一句话“我会口算 MD5”,是一句笑话。MD5 加密之后的密文,是没有办法被反编译成明文的,但是 MD5 加密会出现撞库的情况。也就是说,传统的 MD5 加密,明文和密文都是固定匹配的,这样子也不安全。
怎么解决?可以使用 MD5 盐值(salt)加密。
所谓加 Salt 方法,就是加点“佐料”(Salt 这个单词就是盐的意思)。其基本想法是这样的:当用户首次提供密码时(通常是注册时),由系统自动往这个密码里撒一些“佐料”,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的“佐料”,然后再进行加密比较,已确定密码是否正确。这样也就变成了将密码 + 自定义的盐值来取 MD5。至于这个“佐料”要加在密码的哪一个位置,就由我们来指定了。
新建 MD5 工具类。
/**
* MD5工具类
*
* @author 多仔ヾ
*/
public class MD5Util {
private static MessageDigest messageDigest = null;
private static final char[] hexDigits = {'8', '9', '7', '4', '5', '0', '2', '6', '3', '1', 'C', 'D', 'A', 'B', 'E', 'F'};
private static final char[] hexDigits2 = { '0', '1', '2', '3', '4', '5', '6','7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private static MessageDigest getMessageDigest() {
if (messageDigest == null) {
try {
messageDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
return messageDigest;
}
/**
* MD5普通加密
*
* @author 多仔ヾ
* @param inStr 待加密字符串
* @return 加密结果
**/
public static String encode(String inStr) {
try {
byte[] btInput = inStr.getBytes(StandardCharsets.UTF_8);
getMessageDigest().update(btInput);
byte[] md = getMessageDigest().digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
for (byte byte0 : md) {
str[k++] = hexDigits2[byte0 >>> 4 & 0xf];
str[k++] = hexDigits2[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* MD5盐值加密
*
* @author 多仔ヾ
* @param inStr 待加密字符串
* @param salts 盐值
* @return 加密结果
**/
public static String encode(String inStr, String salts) {
return encode(salts + inStr + salts);
}
/**
* 生成随机盐值
*
* @author 多仔ヾ
* @param length 盐值长度
* @return 盐值
**/
public static String randSalts(int length) {
Random r = new Random();
StringBuilder sb = new StringBuilder();
String vars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for(int i = 0; i < length; i++){
int n = r.nextInt(vars.length());
sb.append(vars.charAt(n));
}
return sb.toString();
}
}在测试类中,对 MD5 工具类提供的方法进行测试。
@Test
void contextLoads() {
// 明文密码
String password = "123456";
// 普通MD5加密
String md5Password = MD5Util.encode(password);
System.out.println("普通MD5加密后的结果:" + md5Password);
// 随机生成盐值
String salt = MD5Util.randSalts(16);
System.out.println("随机生成的盐值是:" + salt);
// 盐值MD5加密
String md5SaltPassword = MD5Util.encode(password, salt);
System.out.println("盐值MD5加密后的结果:" + md5SaltPassword);
}MyBatis Plus
MyBatis-Plus 是一个基于 MyBatis 的增强工具,它对 MyBatis 的基础功能进行了增强,但未做任何改变。使得我们可以可以在 MyBatis 开发的项目上直接进行升级为 MyBatis Plus,正如它对自己的定位,它能够帮助我们进一步简化开发过程,提高开发效率。
MyBatis Plus 其实可以看作是对 MyBatis 的再一次封装,升级之后,对于单表的 CRUD 操作,调用 MyBatis Plus 所提供的 API 就能够轻松实现,此外还提供了各种查询方式、分页等行为。最最重要的,开发人员还不用去编写 XML,这就大大降低了开发难度。
逻辑删除
逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案。原本删除数据使用 delete,现在在每一条数据里面有一个字段 deleted 表示是否删除,0 表示未删除,1 表示已删除。删除操作转变成了 update。
MyBatis Plus 实现逻辑删除,可参考官方文档。
核心业务分析
后台管理系统的核心流程:
后台管理系统的核心功能业务模块:
数据库设计
系统用户账户表:
其中:
- is_super:最高系统用户无法禁用、无法删除。
- role_id:关联角色组 id。
- department_id:关联就诊科室 id。当系统用户关联角色组为门诊医师时,还需要指定这一位门诊医师是哪一个科室的。
- password:MD5 盐值加密后的密码。
- salts:MD5 加密时所需要用到的盐值。
- status:账户状态为禁用时,则禁止登录。
- deleted:用于逻辑删除。
就诊科室表:
系统用户角色组表:
其中:
- is_super:最高角色组无法删除。
- perms:该角色组的权限内容,特指该角色组拥有的权限菜单 id。
系统用户权限菜单表:
其中:
- parent_id:父级权限菜单 id。
- perms:权限菜单标识,用于 Sa-Token 权限验证。
- router:权限菜单访问路径,即 Vue-Router 中的 path。
- component:路由组件路径,即 Vue-Router 中的 component。
- type:权限菜单类型,0 表示目录(菜单分组),1 表示菜单(带有页面、可以访问的菜单),2 表示操作(按钮操作)。
- is_public:是否公共权限菜单,当该菜单被设置为公共权限时,所有的角色组都能访问此菜单。
创建后端项目
使用 IDEA 创建 Spring 项目(ct_springboot),勾选 Spring Web、MySQL Driver、Lombok、Redis 依赖。
修改 pom.xml,导入 MyBatis Plus、HuTool 依赖。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>修改 application.properties,连接到数据库。
# 项目名称
spring.application.name=ct_springboot
# 端口号
server.port=8081
# 数据库连接信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.name=defaultDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/new_hospital?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
# Redis连接信息
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=1根据上文操作,创建 ResponseStatus、ResponseResult、MD5Util。
借助 MyBatisCodeHelperPro 生成 Entity、Mapper、Service。
修改 application.properties、实体类,完善 MyBatis Plus 的逻辑删除配置。具体配置可参考 MyBatis Plus 文档。
# MyBatis逻辑删除
mybatis-plus.global-config.db-config.logic-delete-field=deleted
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0// 实体类逻辑删除属性标记
@TableField(value = "deleted")
@TableLogic
private Integer deleted;创建前端项目
使用 vue ui 创建一个纯净的 Vue-CLI 项目(ct_vuejs),并添加 Router、Vuex、ElementUI、Axios 插件、qs 依赖。
修改 main.js,全局注册 qs。
import qs from 'qs'
Vue.prototype.$qs = qs