前言:
本项目是在大二下学期接到的宜昌市第一人民医院的外包项目,主要用于医院内部的设备故障上报。 然后抽取了其中的部分模块作为本学期Java高级的课设项目
GitHub - dromara/Sa-Token: 这可能是史上功能最全的Java权限认证框架!目前已集成——登录认证、权限认证、分布式Session会话、微服务网关鉴权、单点登录、OAuth2.0、踢人下线、Redis集成、前后台分离、记住我模式、模拟他人账号、临时身份切换、账号封禁、多账号认证体系、注解式鉴权、路由拦截式鉴权、花式token生成、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成…
Sa-Token
顺便推荐一波Sa-Token鉴权框架,相比于Spring Sercuriy Apache Shiro来说,Sa-Token使用起来更加容易上手,阅读文档过后就能轻松上手,不需要过多的配置,极大地简化了开发
并且目前已经有了一定的生态,是一个很不错的开源项目
下面就开始介绍项目吧~
一、项目架构
这个项目的接取时间大概是:3月中旬 但是因为忙其他的事情去了,实际的开发时间应该是:5月初到6月中旬这大概一个多月的时间 在开始写之前其实有感觉会是一个挑战,但是实际写起来是真的让人抓狂
有些技术之前也没有使用过,陆陆续续将一些技术对接进来,最后算是将整个系统做了下来。
后面可能还会对一些细节进行优化,比较是真实外包的落地使用项目
二、界面展示
前端页面
系统数据监控
报障面板
移动端硬件报障页面
后端代码
采用的多模块聚合(按照应用层级拆分)的方案编写
每一个模块下,按照功能/业务进行拆分,将代码尽可能解耦,减少代码的耦合度。也方便后续拆分成微服务
实体类 | 工具类 | 控制器 | 定时任务 |
---|---|---|---|
前端代码
对组件二次封装
请求转发,处理跨域
存储、部署
图片、文件存服务器对象存储
微信云托管部署
Gitee组织
三、数据库设计
数据库关联图
四、框架介绍
前端展示层
主要框架:Vue3以及其全家桶(Vuex,vue-router)
UI组件:Element-Plus
网络请求:对axios进行配置、封装
Vue3全家桶
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面,无论任务是简单还是复杂。 Vue 是一个框架和生态,功能覆盖了大部分前端开发常见的需求。但 Web 世界又是十分多样化的,我们在 Web 上构建的东西可能在形式和规模上有很大不同。考虑到这一点,Vue 被设计成具有灵活性和可逐步集成的特点。根据你的需求场景,Vue 可以按不同的方式使用:
- 增强静态的 HTML 而无需构建步骤
- 在任何页面中作为 Web Components 嵌入
- 单页应用 (SPA)
- 全栈 / 服务端渲染 (SSR)
- Jamstack / 静态站点生成 (SSG)
- 目标为桌面端、移动端、WebGL,甚至是命令行终端
Element Plus
https://element-plus.gitee.io/zh-CN/
Element Plus是ElementUI的Vue3版本 样式简洁好看
后端
代码规范:Alibaba Coding Guidelines,尽可能拆分解耦
持久层框架:SpringData JPA
复杂查询:Querydsl For JPA
具体框架总结如下:
SpringData JPA & Querydsl
Spring Data JPA - Reference Documentation
Spring Data JPA provides repository support for the Java Persistence API (JPA). It eases development of applications that need to access JPA data sources. Spring Data JPA为Java Persistence API(JPA)提供存储库支持。它简化了需要访问JPA数据源的应用程序的开发。 可以通过Java代码来管理数据库脚本,设置实体类和实体类之间的映射关系,以及数据表、数据库的管理
Querydsl仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查询。
Querydsl可以通过一组通用的查询API为用户构建出适合不同类型ORM框架或者是SQL的查询语句,也就是说Querydsl是基于各种ORM框架以及SQL之上的一个通用的查询框架。
通过Querydsl对JPA的整合,我们可以很方便的处理一些复杂的查询(必须不定字段的查询、复杂多表关联查询)
Querydsl整合JPA
在
pom.xml
中引入相关依赖<!--querydsl依赖-->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
添加maven插件
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<!--如果不想每一次编译都生成一遍Q实体,可以改为test-process-->
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
<options>
<!--包名前缀-->
<querydsl.packagePrefix>com.clevesheep.test</querydsl.packagePrefix>
<!--包名后缀-->
<querydsl.packageSuffix>.dsl</querydsl.packageSuffix>
</options>
</configuration>
</execution>
</executions>
</plugin>
创建实体类 ```java package com.clevesheep.entity;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;
import javax.persistence.*; import java.io.Serializable;
/**
- Created By Intellij IDEA *
- @author Xinrui Yu
@date 2022/4/4 20:42 星期一 */ @Table(name = “t_roles”) @Data @Entity @ApiModel(value = “用户类型(角色)”) @AllArgsConstructor @NoArgsConstructor public class Role implements Serializable {
private static final long serialVersionUID = -381486554729993356L;
@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = “USER_ROLE_SEQ”) @SequenceGenerator(name = “USER_ROLE_SEQ”, sequenceName = “user_role_seq”, allocationSize = 1) @Column(name = “r_id”, length = 10, unique = true) @ApiModelProperty(value = “角色编号”, required = true) private Integer id;
@ApiModelProperty(value = “角色名称”, required = true) @Column(name = “r_name”, nullable = false, unique = true) private String roleName;
}
4. 运行插件,生成实体类
使用`compile`<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/21436600/1655175324060-cc2092b3-15b7-45e9-8c01-7d843f2b71cb.png#clientId=u6904fc70-6fcd-4&from=paste&height=240&id=u2d6bcd7e&originHeight=300&originWidth=362&originalType=binary&ratio=1&rotation=0&showTitle=false&size=21056&status=done&style=none&taskId=u1da44d94-1047-4288-a50c-614d09aa123&title=&width=289.6)<br />生成的Q实体会在target/generated-sources/java文件夹下<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/21436600/1655175388740-6a7449dc-542d-4681-bf59-1a8dfe5f81f6.png#averageHue=%233f3422&clientId=u6904fc70-6fcd-4&from=paste&height=117&id=ucf2b70e9&originHeight=146&originWidth=444&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9338&status=done&style=none&taskId=uad8a7e52-54e5-4cc1-ba84-0d0fda1ee37&title=&width=355.2)
```java
package com.clevesheep.entity.dsl;
import static com.querydsl.core.types.PathMetadataFactory.*;
import com.clevesheep.entity.Role;
import com.querydsl.core.types.dsl.*;
import com.querydsl.core.types.PathMetadata;
import javax.annotation.Generated;
import com.querydsl.core.types.Path;
/**
* QRole is a Querydsl query type for Role
*/
@Generated("com.querydsl.codegen.EntitySerializer")
public class QRole extends EntityPathBase<Role> {
private static final long serialVersionUID = 826515792L;
public static final QRole role = new QRole("role");
public final NumberPath<Integer> id = createNumber("id", Integer.class);
public final StringPath roleName = createString("roleName");
public QRole(String variable) {
super(Role.class, forVariable(variable));
}
public QRole(Path<? extends Role> path) {
super(path.getType(), path.getMetadata());
}
public QRole(PathMetadata metadata) {
super(Role.class, metadata);
}
}
- 使用Q实体进行查询
通过Querydsl实现简单的分页查询
// 首先注入Baen
@Autowired
private JPAQueryFactory jpaQueryFactory;
/**
* 获取所有用户的信息
*
* @param pageNum 页码
* @param pageSize 每页的容量
* @return 所有用户的信息
*/
@Override
public Result getAll(Integer pageNum, Integer pageSize) {
if (pageNum == null || pageSize == null) {
pageNum = 1;
pageSize = 10;
}
QUser user = QUser.user;
QueryResults<User> fetchResults = jpaQueryFactory.select(user)
.from(user)
.where(user.isDeleted.eq(false))
.orderBy(user.id.asc())
.offset((long) (pageNum - 1) * pageSize)
.limit(pageSize)
.fetchResults();
fetchResults.getResults().forEach(User::setPwdNull);
return Result.success(fetchResults);
}
- 接口测试
JustAuth实现第三方登录
对第三方OAuth登录进行封装,依据少量代码即可接入第三方登录
Easyexcel
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。
五、技术点梳理
Axios请求封装
Sa-token权限认证
Sa-Token中,大部分的权限操作都被封装到静态类
Stputil
中 通过此静态类进行鉴权、授权等相关操作
登录认证
// .. 参数校验和密码鉴别的代码进行省略
StpUtil.login(id);
String tokenValue = StpUtil.getTokenInfo().getTokenValue();
String tokenName = StpUtil.getTokenInfo().getTokenName();
// 将实体转换为dto返回给前端
data.setPassword(null);
// 将token和登录用户的信息返回给前端
HashMap<String, Object> map = new HashMap<>(16);
map.put("token", tokenValue);
map.put("tokenHeader", tokenName);
// map.put("user", data);
return Result.success("登录成功", map);
角色认证
在进行角色认证之前,需要获取当前账号的角色列表
实际用户和角色的对应关系是存储在数据库中的,因此在此Bean中请求数据库查询此用户角色即可
package com.clevesheep.service.impl;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.clevesheep.dao.user.UserDao;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/4/10 15:14 星期日
*/
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
private static final Log log = LogFactory.get(StpInterfaceImpl.class);
private final UserDao userDao;
@Override
public List<String> getPermissionList(Object loginId, String s) {
return null;
}
@Override
public List<String> getRoleList(Object loginId, String s) {
List<String> roles = new ArrayList<>();
log.info("获取工号为:" + loginId + "的角色信息成功");
Integer id = Integer.valueOf(String.valueOf(loginId));
return userDao.getUserRole(id);
}
}
可以通过StpUtil
来校验当前账号是否拥有某种角色
// 判断:当前账号是否拥有指定角色, 返回true或false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
Querydsl进行查询
数据表格导出导入
导出
引入相关依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${alibaba.easyexcel.version}</version>
</dependency>
在需要被导出数据的实体类上添加相关注解 | | | | —- | —- | | @ExcelProperty | 对应渲染到Excel中的某一列 | | @ExcelIgnore | 不渲染此属性(字段) |
由于使用到的是JPA,那么某个实体类中可能还关联了其他的实体类
而这种非基本类型的属性,我们需要使用自定义转换器,来告诉EasyExcel这种类型的属性如何去转换成表格数据
@ExcelProperty(value = "用户角色", converter = RoleConverter.class)
@ApiModelProperty(value = "用户角色")
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "role_id",referencedColumnName = "r_id")
private Role userRole;
package com.clevesheep.converter;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.clevesheep.entity.Role;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/5/13 11:08 星期五
*/
public class RoleConverter implements Converter<Role> {
@Override
public Class<Role> supportJavaTypeKey() {
return Role.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public WriteCellData<String> convertToExcelData(Role role, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
String roleName;
switch(role.getRoleName()){
case "normal_user": {
roleName = "普通用户";
break;
}
case "control_man": {
roleName = "信息科人员";
break;
}
case "super_admin": {
roleName = "超级管理员";
break;
}
default: {
roleName = "";
break;
}
}
return new WriteCellData<>(roleName);
}
}
导入
定义导入事件监听器
package com.clevesheep.service.listener.excel;
import cn.dev33.satoken.secure.SaSecureUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.clevesheep.dao.user.UserDao;
import com.clevesheep.entity.User;
import com.clevesheep.enums.UserRoleEnum;
import lombok.RequiredArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* Created By Intellij IDEA
*
* @author Xinrui Yu
* @date 2022/5/13 17:29 星期五
*/
@RequiredArgsConstructor
public class UserDataImportListener extends AnalysisEventListener<User> {
private static final String PRIVATE_KEY = "dfgfqwertyuibnmv!ffyqwertyuibnmv";
private final UserDao userDao;
private static final String DEFAULT_PASSWORD = "123456";
List<User> userList = new ArrayList<>();
Integer maxCount = 20;
@Override
public void invoke(User user, AnalysisContext analysisContext) {
user.setPassword(SaSecureUtil.aesEncrypt(PRIVATE_KEY,DEFAULT_PASSWORD));
user.setCreateTime(LocalDateTime.now());
user.setStatus(true);
user.setIsChairMan(false);
user.setIsNurseBoos(false);
user.setCreateTime(LocalDateTime.now());
user.setUserRole(UserRoleEnum.NORMAL_USER.role);
user.setIsDeleted(false);
userList.add(user);
// 防止同时加载大量数据在内存中导致OOM
if(userList.size() > maxCount) {
saveData();
userList.clear();
}
}
private void saveData(){
userDao.saveAll(userList);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
saveData();
}
}
调用
@Override
public Result importExcel(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new UserDataImportListener(userDao)).sheet().doRead();
return Result.success("导入成功");
}
第三方Github登录
六、解决问题
复杂查询
本项目需求中,对查询的要求其实是比较高的,查询的字段很多,并且哪些字段会参与查询是不定的。可能需要用户名+性别查询,也可能性别+职位查询,也可能只用用户名进行查询,以此类推。 如果不做任何处理,那么可能需要几十行代码进行判断、拼接SQL
但,经过查询相关的资料,发现Querydsl
可以很好的帮我们解决这个问题。
经过对实体类和数据访问(dao)层的相关配置,即可根据实体类中的所有属性进行查询!
只要是实体类中有的属性,都可以参与到查询中
Querydsl会自动帮我们生成安全的SQL语句
并发问题
本项目会投入实际使用,并且是单体单机应用。因此需要考虑到后续如果使用人数过多造成的高并发问题。