前言:

本项目是在大二下学期接到的宜昌市第一人民医院的外包项目,主要用于医院内部的设备故障上报。 然后抽取了其中的部分模块作为本学期Java高级的课设项目

GitHub - dromara/Sa-Token: 这可能是史上功能最全的Java权限认证框架!目前已集成——登录认证、权限认证、分布式Session会话、微服务网关鉴权、单点登录、OAuth2.0、踢人下线、Redis集成、前后台分离、记住我模式、模拟他人账号、临时身份切换、账号封禁、多账号认证体系、注解式鉴权、路由拦截式鉴权、花式token生成、自动续签、同端互斥登录、会话治理、密码加密、jwt集成、Spring集成、WebFlux集成…
Sa-Token
顺便推荐一波Sa-Token鉴权框架,相比于Spring Sercuriy Apache Shiro来说,Sa-Token使用起来更加容易上手,阅读文档过后就能轻松上手,不需要过多的配置,极大地简化了开发
并且目前已经有了一定的生态,是一个很不错的开源项目
image.png
下面就开始介绍项目吧~

一、项目架构

Java高级课设--医院报障管理系统 - 图2

这个项目的接取时间大概是:3月中旬 但是因为忙其他的事情去了,实际的开发时间应该是:5月初到6月中旬这大概一个多月的时间 在开始写之前其实有感觉会是一个挑战,但是实际写起来是真的让人抓狂
有些技术之前也没有使用过,陆陆续续将一些技术对接进来,最后算是将整个系统做了下来。

后面可能还会对一些细节进行优化,比较是真实外包的落地使用项目

二、界面展示

前端页面

image.png
image.pngimage.png
image.png

系统数据监控

image.png

报障面板

image.png

移动端硬件报障页面

可以扫描生成的设备二维码,来获取设备的信息
PZO}F(M$R`%0VF6C[UFG[KI.jpg](https://cdn.nlark.com/yuque/0/2022/jpeg/21436600/1655104357389-78220bf1-9748-4889-8b0f-673d798792cf.jpeg#averageHue=%23f0f0ef&clientId=ucef7b6ed-1785-4&from=paste&height=660&id=uc6b3f9e7&originHeight=2532&originWidth=1170&originalType=binary&ratio=1&rotation=0&showTitle=false&size=606978&status=done&style=none&taskId=u0197ceb0-658a-44c3-aaae-8c3cd9d028e&title=&width=305)![Q_@}]G0JB$IP%1C$0(K9W`Y.jpg

后端代码

image.png
采用的多模块聚合(按照应用层级拆分)的方案编写

image.pngimage.png
每一个模块下,按照功能/业务进行拆分,将代码尽可能解耦,减少代码的耦合度。也方便后续拆分成微服务

实体类 工具类 控制器 定时任务
image.png image.png image.png image.png

前端代码

对组件二次封装

image.png

请求转发,处理跨域

image.png

存储、部署

图片、文件存服务器对象存储

image.png

微信云托管部署

image.png

Gitee组织

image.png

三、数据库设计

数据库关联图

image.png

四、框架介绍

前端展示层

主要框架:Vue3以及其全家桶(Vuex,vue-router)
UI组件:Element-Plus
网络请求:对axios进行配置、封装

Vue3全家桶

image.png
image.png

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/
image.png

Element Plus是ElementUI的Vue3版本 样式简洁好看

后端

代码规范:Alibaba Coding Guidelines,尽可能拆分解耦
持久层框架:SpringData JPA
复杂查询:Querydsl For JPA
具体框架总结如下:

SpringData JPA & Querydsl

Spring Data JPA - Reference Documentation
image.png

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代码来管理数据库脚本,设置实体类和实体类之间的映射关系,以及数据表、数据库的管理

image.png

Querydsl仅仅是一个通用的查询框架,专注于通过Java API构建类型安全的SQL查询。

Querydsl可以通过一组通用的查询API为用户构建出适合不同类型ORM框架或者是SQL的查询语句,也就是说Querydsl是基于各种ORM框架以及SQL之上的一个通用的查询框架。

通过Querydsl对JPA的整合,我们可以很方便的处理一些复杂的查询(必须不定字段的查询、复杂多表关联查询)

Querydsl整合JPA

  1. pom.xml中引入相关依赖

    1. <!--querydsl依赖-->
    2. <dependency>
    3. <groupId>com.querydsl</groupId>
    4. <artifactId>querydsl-jpa</artifactId>
    5. <version>${querydsl.version}</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>com.querydsl</groupId>
    9. <artifactId>querydsl-apt</artifactId>
    10. <version>${querydsl.version}</version>
    11. <scope>provided</scope>
    12. </dependency>
  2. 添加maven插件

    1. <plugin>
    2. <groupId>com.mysema.maven</groupId>
    3. <artifactId>apt-maven-plugin</artifactId>
    4. <version>1.1.3</version>
    5. <executions>
    6. <execution>
    7. <goals>
    8. <!--如果不想每一次编译都生成一遍Q实体,可以改为test-process-->
    9. <goal>process</goal>
    10. </goals>
    11. <configuration>
    12. <outputDirectory>target/generated-sources/java</outputDirectory>
    13. <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
    14. <options>
    15. <!--包名前缀-->
    16. <querydsl.packagePrefix>com.clevesheep.test</querydsl.packagePrefix>
    17. <!--包名后缀-->
    18. <querydsl.packageSuffix>.dsl</querydsl.packageSuffix>
    19. </options>
    20. </configuration>
    21. </execution>
    22. </executions>
    23. </plugin>
  3. 创建实体类 ```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;

}

  1. 4. 运行插件,生成实体类
  2. 使用`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)
  3. ```java
  4. package com.clevesheep.entity.dsl;
  5. import static com.querydsl.core.types.PathMetadataFactory.*;
  6. import com.clevesheep.entity.Role;
  7. import com.querydsl.core.types.dsl.*;
  8. import com.querydsl.core.types.PathMetadata;
  9. import javax.annotation.Generated;
  10. import com.querydsl.core.types.Path;
  11. /**
  12. * QRole is a Querydsl query type for Role
  13. */
  14. @Generated("com.querydsl.codegen.EntitySerializer")
  15. public class QRole extends EntityPathBase<Role> {
  16. private static final long serialVersionUID = 826515792L;
  17. public static final QRole role = new QRole("role");
  18. public final NumberPath<Integer> id = createNumber("id", Integer.class);
  19. public final StringPath roleName = createString("roleName");
  20. public QRole(String variable) {
  21. super(Role.class, forVariable(variable));
  22. }
  23. public QRole(Path<? extends Role> path) {
  24. super(path.getType(), path.getMetadata());
  25. }
  26. public QRole(PathMetadata metadata) {
  27. super(Role.class, metadata);
  28. }
  29. }
  1. 使用Q实体进行查询

通过Querydsl实现简单的分页查询

  1. // 首先注入Baen
  2. @Autowired
  3. private JPAQueryFactory jpaQueryFactory;
  4. /**
  5. * 获取所有用户的信息
  6. *
  7. * @param pageNum 页码
  8. * @param pageSize 每页的容量
  9. * @return 所有用户的信息
  10. */
  11. @Override
  12. public Result getAll(Integer pageNum, Integer pageSize) {
  13. if (pageNum == null || pageSize == null) {
  14. pageNum = 1;
  15. pageSize = 10;
  16. }
  17. QUser user = QUser.user;
  18. QueryResults<User> fetchResults = jpaQueryFactory.select(user)
  19. .from(user)
  20. .where(user.isDeleted.eq(false))
  21. .orderBy(user.id.asc())
  22. .offset((long) (pageNum - 1) * pageSize)
  23. .limit(pageSize)
  24. .fetchResults();
  25. fetchResults.getResults().forEach(User::setPwdNull);
  26. return Result.success(fetchResults);
  27. }
  1. 接口测试

image.png

JustAuth实现第三方登录

image.png
image.png
对第三方OAuth登录进行封装,依据少量代码即可接入第三方登录

Easyexcel

image.png

EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。

五、技术点梳理

Axios请求封装

image.png

Sa-token权限认证

Sa-Token中,大部分的权限操作都被封装到静态类Stputil中 通过此静态类进行鉴权、授权等相关操作

登录认证

  1. // .. 参数校验和密码鉴别的代码进行省略
  2. StpUtil.login(id);
  3. String tokenValue = StpUtil.getTokenInfo().getTokenValue();
  4. String tokenName = StpUtil.getTokenInfo().getTokenName();
  5. // 将实体转换为dto返回给前端
  6. data.setPassword(null);
  7. // 将token和登录用户的信息返回给前端
  8. HashMap<String, Object> map = new HashMap<>(16);
  9. map.put("token", tokenValue);
  10. map.put("tokenHeader", tokenName);
  11. // map.put("user", data);
  12. return Result.success("登录成功", map);

角色认证

在进行角色认证之前,需要获取当前账号的角色列表

实际用户和角色的对应关系是存储在数据库中的,因此在此Bean中请求数据库查询此用户角色即可

  1. package com.clevesheep.service.impl;
  2. import cn.dev33.satoken.stp.StpInterface;
  3. import cn.hutool.log.Log;
  4. import cn.hutool.log.LogFactory;
  5. import com.clevesheep.dao.user.UserDao;
  6. import lombok.RequiredArgsConstructor;
  7. import org.springframework.stereotype.Component;
  8. import java.util.ArrayList;
  9. import java.util.List;
  10. /**
  11. * Created By Intellij IDEA
  12. *
  13. * @author Xinrui Yu
  14. * @date 2022/4/10 15:14 星期日
  15. */
  16. @Component
  17. @RequiredArgsConstructor
  18. public class StpInterfaceImpl implements StpInterface {
  19. private static final Log log = LogFactory.get(StpInterfaceImpl.class);
  20. private final UserDao userDao;
  21. @Override
  22. public List<String> getPermissionList(Object loginId, String s) {
  23. return null;
  24. }
  25. @Override
  26. public List<String> getRoleList(Object loginId, String s) {
  27. List<String> roles = new ArrayList<>();
  28. log.info("获取工号为:" + loginId + "的角色信息成功");
  29. Integer id = Integer.valueOf(String.valueOf(loginId));
  30. return userDao.getUserRole(id);
  31. }
  32. }

可以通过StpUtil来校验当前账号是否拥有某种角色

  1. // 判断:当前账号是否拥有指定角色, 返回true或false
  2. StpUtil.hasRole("super-admin");
  3. // 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
  4. StpUtil.checkRole("super-admin");
  5. // 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
  6. StpUtil.checkRoleAnd("super-admin", "shop-admin");
  7. // 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
  8. StpUtil.checkRoleOr("super-admin", "shop-admin");

Querydsl进行查询

数据表格导出导入

导出

  1. 引入相关依赖

    1. <dependency>
    2. <groupId>com.alibaba</groupId>
    3. <artifactId>easyexcel</artifactId>
    4. <version>${alibaba.easyexcel.version}</version>
    5. </dependency>
  2. 在需要被导出数据的实体类上添加相关注解 | | | | —- | —- | | @ExcelProperty | 对应渲染到Excel中的某一列 | | @ExcelIgnore | 不渲染此属性(字段) |

由于使用到的是JPA,那么某个实体类中可能还关联了其他的实体类
而这种非基本类型的属性,我们需要使用自定义转换器,来告诉EasyExcel这种类型的属性如何去转换成表格数据

  1. @ExcelProperty(value = "用户角色", converter = RoleConverter.class)
  2. @ApiModelProperty(value = "用户角色")
  3. @ManyToOne(fetch = FetchType.EAGER)
  4. @JoinColumn(name = "role_id",referencedColumnName = "r_id")
  5. private Role userRole;
  1. package com.clevesheep.converter;
  2. import com.alibaba.excel.converters.Converter;
  3. import com.alibaba.excel.enums.CellDataTypeEnum;
  4. import com.alibaba.excel.metadata.GlobalConfiguration;
  5. import com.alibaba.excel.metadata.data.WriteCellData;
  6. import com.alibaba.excel.metadata.property.ExcelContentProperty;
  7. import com.clevesheep.entity.Role;
  8. /**
  9. * Created By Intellij IDEA
  10. *
  11. * @author Xinrui Yu
  12. * @date 2022/5/13 11:08 星期五
  13. */
  14. public class RoleConverter implements Converter<Role> {
  15. @Override
  16. public Class<Role> supportJavaTypeKey() {
  17. return Role.class;
  18. }
  19. @Override
  20. public CellDataTypeEnum supportExcelTypeKey() {
  21. return CellDataTypeEnum.STRING;
  22. }
  23. @Override
  24. public WriteCellData<String> convertToExcelData(Role role, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
  25. String roleName;
  26. switch(role.getRoleName()){
  27. case "normal_user": {
  28. roleName = "普通用户";
  29. break;
  30. }
  31. case "control_man": {
  32. roleName = "信息科人员";
  33. break;
  34. }
  35. case "super_admin": {
  36. roleName = "超级管理员";
  37. break;
  38. }
  39. default: {
  40. roleName = "";
  41. break;
  42. }
  43. }
  44. return new WriteCellData<>(roleName);
  45. }
  46. }

导入

定义导入事件监听器

  1. package com.clevesheep.service.listener.excel;
  2. import cn.dev33.satoken.secure.SaSecureUtil;
  3. import com.alibaba.excel.context.AnalysisContext;
  4. import com.alibaba.excel.event.AnalysisEventListener;
  5. import com.clevesheep.dao.user.UserDao;
  6. import com.clevesheep.entity.User;
  7. import com.clevesheep.enums.UserRoleEnum;
  8. import lombok.RequiredArgsConstructor;
  9. import java.time.LocalDateTime;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. /**
  13. * Created By Intellij IDEA
  14. *
  15. * @author Xinrui Yu
  16. * @date 2022/5/13 17:29 星期五
  17. */
  18. @RequiredArgsConstructor
  19. public class UserDataImportListener extends AnalysisEventListener<User> {
  20. private static final String PRIVATE_KEY = "dfgfqwertyuibnmv!ffyqwertyuibnmv";
  21. private final UserDao userDao;
  22. private static final String DEFAULT_PASSWORD = "123456";
  23. List<User> userList = new ArrayList<>();
  24. Integer maxCount = 20;
  25. @Override
  26. public void invoke(User user, AnalysisContext analysisContext) {
  27. user.setPassword(SaSecureUtil.aesEncrypt(PRIVATE_KEY,DEFAULT_PASSWORD));
  28. user.setCreateTime(LocalDateTime.now());
  29. user.setStatus(true);
  30. user.setIsChairMan(false);
  31. user.setIsNurseBoos(false);
  32. user.setCreateTime(LocalDateTime.now());
  33. user.setUserRole(UserRoleEnum.NORMAL_USER.role);
  34. user.setIsDeleted(false);
  35. userList.add(user);
  36. // 防止同时加载大量数据在内存中导致OOM
  37. if(userList.size() > maxCount) {
  38. saveData();
  39. userList.clear();
  40. }
  41. }
  42. private void saveData(){
  43. userDao.saveAll(userList);
  44. }
  45. @Override
  46. public void doAfterAllAnalysed(AnalysisContext analysisContext) {
  47. saveData();
  48. }
  49. }

调用

  1. @Override
  2. public Result importExcel(MultipartFile file) throws IOException {
  3. EasyExcel.read(file.getInputStream(), User.class, new UserDataImportListener(userDao)).sheet().doRead();
  4. return Result.success("导入成功");
  5. }

第三方Github登录

六、解决问题

复杂查询

本项目需求中,对查询的要求其实是比较高的,查询的字段很多,并且哪些字段会参与查询是不定的。可能需要用户名+性别查询,也可能性别+职位查询,也可能只用用户名进行查询,以此类推。 如果不做任何处理,那么可能需要几十行代码进行判断、拼接SQL

但,经过查询相关的资料,发现Querydsl可以很好的帮我们解决这个问题。
经过对实体类和数据访问(dao)层的相关配置,即可根据实体类中的所有属性进行查询!
只要是实体类中有的属性,都可以参与到查询中
Querydsl会自动帮我们生成安全的SQL语句

并发问题

本项目会投入实际使用,并且是单体单机应用。因此需要考虑到后续如果使用人数过多造成的高并发问题。

  • 使用redis作为数据库的前置缓存,将热点、常用数据保存到redis中,减少数据库查询压力

    数据一致性问题

    为了解决并发问题中,引入了redis作为数据库的前置缓存,那么如何保证缓存中数据和数据库中数据的一致性,是十分重要的。 如果处理不当,那么用户获取到的数据可能是脏数据 主要采用的是如下的缓存更新策略

Java高级课设--医院报障管理系统 - 图33