拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3疫情隔离管理系统开发实战:从零搭建到部署全流程

SpringBoot2+Vue3疫情隔离管理系统开发实战:从零搭建到部署全流程

最近把一个疫情隔离管理系统从零到一完整做了一遍,技术栈就是标题里那套——SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0。说实话,这类系统放在平时看起来不复杂,无非就是人员登记、健康监测、房间管理、物资管理几块业务,但真正从设计表结构到前后端联调,再到打包部署、写文档准备交付,整个过程里踩过的坑远比想象中多。这篇文章不聊虚的,就把我做这个项目的完整思路、表结构设计、后端核心封装、前端页面交互、环境部署实测记录,以及那些文档里绝对不会写的排查经验全部摊开讲清楚。项目源码附带的说明文档我看了,写得中规中矩,但还不够细,所以我决定自己重新整理一篇,记录更接近真实开发过程的经验。

不管你是准备拿这个项目做课程设计、毕业设计,还是单纯想练手SpringBoot2 + Vue3的组合玩法,这篇内容都能帮你省下不少摸索时间。基础要求不高,会一点Java和JavaScript语法就能跟上,我会把关键代码和配置直接贴出来,你照着做就能跑起来。

1. 业务梳理与技术选型背后的逻辑

1.1 这个系统到底解决什么问题

疫情隔离管理这个场景,核心痛点在于信息分散。隔离人员入住登记靠纸质表格,每日体温监测靠人工汇报,房间占用情况靠打电话问,物资消耗靠月底盘库存。一旦人数上来,数据一多,管理就完全失控。

所以这个系统的定位就是把这些分散操作集中到一个平台里,让不同角色在各自权限范围内完成自己的工作。管理员负责全局配置和数据查看,医护人员负责录入隔离人员的健康监测信息,普通用户或隔离人员则可以通过系统查看自己的状态和通知。核心业务流程其实只有一条:人员登记入驻、分配房间、每日健康记录、到期解除隔离。围绕这条链路,再延伸出物资管理、公告通知、数据统计等辅助模块。

1.2 技术栈选型:不追新,选成熟

为什么选SpringBoot2而不是SpringBoot3?我的考虑很简单。SpringBoot2.7.x配合JDK8,是当前最稳定的企业级组合,网上资料多,遇到问题基本一搜就有答案。SpringBoot3强制要求JDK17,如果你的电脑只装了JDK8,那就要多一道环境配置的槛。对于课程设计和毕业设计这种场景,稳定性优先于新版本特性,另外MyBatis-Plus对SpringBoot2.x的支持也是最成熟的,代码生成器、分页插件、条件构造器都是直接可用。

Vue3选择了Composition API + Element Plus的组合。Vue3对比Vue2最大的变化就是组合式API,逻辑复用更清晰,配合Vite构建速度极快,开发体验比webpack时代好太多。Element Plus是Vue3生态里最完整的UI组件库,后台管理系统的表格、表单、弹窗、标签页都能一站解决。

MySQL8.0在这个项目里其实发挥了几个关键特性。默认字符集是utf8mb4,中文存储不会出现乱码问题;窗口函数和公共表表达式这些新特性虽然在这个项目里没用上,但以后做数据分析报表扩展时直接就能用;另外MySQL8.0对JSON字段类型支持很好,后面如果要扩展自定义字段也方便。最重要的是,学校机房和企业内部环境现在普遍装的就是8.0,选它意味着部署不折腾。

1.3 软件架构与核心模块划分

整个系统采用经典前后端分离架构。后端提供RESTful API,前端通过HTTP请求调用,两者之间通过JSON交换数据。后端SpringBoot负责业务逻辑和数据库操作,前端Vue3负责页面渲染和用户交互。

系统功能模块我划分为六大块:

模块核心功能设计说明
用户管理登录注册、角色分配、账号维护采用JWT做无状态认证,角色分管理员/医护人员/普通用户
人员管理隔离人员登记、解除、查询核心业务入口,包含入住和解除两个关键动作
房间管理楼栋/房间信息维护、分配状态房间状态驱动分配逻辑,空闲/占用/消杀三种状态
健康监测体温记录、异常标记、连续监控按人员维度记录每日健康数据,支持历史曲线查询
物资管理物资出入库、库存预警核心是库存台账的准确性,预警阈值可配置
公告管理通知发布、列表查看面向所有角色,支持按发布时间排序

模块之间的依赖关系也很清晰,人员管理依赖房间管理来确定入住房间,健康监测依赖人员管理来确定记录归属,统计报表又依赖前面所有模块的数据。设计时把公共字段(创建时间、更新时间、逻辑删除标记)统一放进基础实体类,避免每个表都重复写一遍。

2. 数据库设计与初始化脚本要点

2.1 核心表结构设计

数据库是整套系统的地基。我在设计表结构的时候,一切都是围绕“业务状态如何流转”来做的。最终确定的六张核心表是:用户表、隔离人员表、房间表、健康记录表、物资表、公告表。这里挑几张关键表详细说一下。

隔离人员表是最复杂的一张表,它承载了整条业务主线的状态。字段包括:姓名、证件类型、证件号码、手机号、来源地、入住时间、计划解除时间、实际解除时间、入住房间号、状态。其中状态字段是关键设计,我用了tinyint类型的status,0表示待入住,1表示隔离中,2表示已解除。为什么不直接用字符串?因为后端做状态条件查询时,整型比较比字符串匹配效率更高,而且配合MyBatis-Plus的枚举类型就能在Java代码里做统一转换。

房间表的设计重点在状态字段和编号规则。房间编号我采用了“楼栋号-楼层-房间号”的组合格式,比如2-3-05表示2栋3层05号房。这样设计的好处是前端展示时不用额外拼接,后端做楼栋统计时也能通过模糊查询实现。

健康记录表相对简单,但索引设计很关键。由于系统要按人员查历史记录,我创建了联合索引(人员ID,记录日期),否则数据量上来之后按人员查询会全表扫描。

物资表的重点在于库存字段的约束检查。库存不能为负数,我在数据库层直接加了CHECK (quantity >= 0)的约束,在应用层用事务控制出入库操作的原子性,双保险。

2.2 建库建表与初始化数据脚本示例

数据库名称定义为isolation_db,字符集指定为utf8mb4。我个人习惯在MySQL8.0上把排序规则设为utf8mb4_unicode_ci,这个排序规则对中文文本比较友好。

CREATE DATABASE IF NOT EXISTS isolation_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

用户表建表语句我直接给出来,注意密码字段不要用varchar(50),因为BCrypt加密后的密码长度是60位,长度不够会导致认证失败:

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', nickname VARCHAR(50) COMMENT '用户昵称', role VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '角色: ADMIN/DOCTOR/USER', status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 1启用 0禁用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', INDEX idx_username (username) ) ENGINE=InnoDB COMMENT='系统用户表';

这里有一个非常容易被忽略的坑:MySQL8.0默认的认证插件是caching_sha2_password,而很多老版本驱动只支持mysql_native_password。如果你在连接数据库时报认证插件错误,要么改用8.0对应的驱动版本,要么在建用户时指定认证插件。我的做法是在项目初始化时统一使用mysql-connector-j8.0.x版本,代码里指定驱动为com.mysql.cj.jdbc.Driver。

2.3 初始化管理员的正确姿势

初始数据里必须要有一个管理员账号。我用BCrypt加密工具生成密码串,而不是明文存入数据库。具体做法是写一个CommandLineRunner,在SpringBoot启动时检测sys_user表是否为空,如果为空就自动插入默认管理员。

@Component public class AdminInitializer implements CommandLineRunner { @Autowired private SysUserMapper userMapper; @Override public void run(String... args) { Long count = userMapper.selectCount(null); if (count > 0) return; SysUser admin = new SysUser(); admin.setUsername("admin"); admin.setPassword(new BCryptPasswordEncoder().encode("admin123")); admin.setNickname("系统管理员"); admin.setRole("ADMIN"); userMapper.insert(admin); log.info("默认管理员账号已创建: admin / admin123"); } }

这样设计的好处是,别人拿到源码后不需要手动执行SQL插入数据就能启动系统,体验会好很多。

3. 后端核心实现:SpringBoot2 + MyBatis-Plus的实战技巧

3.1 项目骨架与统一结果封装

后端项目的标准包结构是这样的:controller、service、mapper、entity、common、config。controller只做参数接收和结果响应,service写业务逻辑,mapper就是MyBatis-Plus的BaseMapper接口,entity对应数据库表,common放通用工具类和结果封装,config放配置类。

所有接口返回统一的数据结构,这是前后端协作质量提升的关键一步。封装的返回体包含三个字段:code(状态码)、message(提示信息)、data(业务数据)。

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 200; result.message = "操作成功"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }

统一的返回格式解决了前后端沟通成本问题。前端axios拦截器只需要判断code是否为200,再决定是走业务逻辑还是弹出错误提示,代码逻辑会非常干净。

3.2 基于MyBatis-Plus的通用CRUD服务封装

MyBatis-Plus这个框架最大的价值不是帮你写SQL,而是帮你省掉那些无状态的单表增删改查代码。基于BaseMapper接口,你不需要写任何SQL就能完成单表的插入、更新、条件查询、分页查询。但如果每个Service都重复写一套selectList、insert、updateById,那也没有真正省事。

我的做法是封装一个通用Service接口和实现类,把常见的CRUD操作抽象出来。这个思路在项目里效果非常明显,六个业务模块的Service层加起来不到两百行业务代码。

public interface CommonService<T> { boolean save(T entity); boolean update(T entity); boolean removeById(Long id); T getById(Long id); List<T> list(Wrapper<T> queryWrapper); IPage<T> page(Page<T> page, Wrapper<T> queryWrapper); }

通用实现类基于IService<T>来做,业务模块的Service只需要继承这个通用实现类,再补充各自独有的业务方法即可。值得注意的是,通用CRUD虽然方便,但只适合单表简单操作。多表关联或复杂统计,我会在mapper里写自定义SQL,使用@Select注解或XML文件实现。千万不要所有业务都用通用CRUD硬套,比如物资出库时要同时扣减库存并新增出入库记录,这种就必须用事务方法处理。

3.3 分页查询的正确打开方式

MyBatis-Plus分页需要先配置分页插件,这个步骤容易漏,漏了之后你会发现page方法返回的数据永远是全量的。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

配置完成后,Service层调用分页,前端只需要传递当前页码pageNum和每页条数pageSize两个参数即可。分页插件会自动生成count查询和limit语句,你不需要手动写SQL。

除了分页,MyBatis-Plus的LambdaQueryWrapper也很好用。比如做人员列表的条件查询,姓名和状态都可能为空,用条件构造器配合StringUtils.hasText()判断,就能避免拼接SQL时空条件的坑。

LambdaQueryWrapper<IsolationPerson> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(personName), IsolationPerson::getName, personName) .eq(personStatus != null, IsolationPerson::getStatus, personStatus) .orderByDesc(IsolationPerson::getCreateTime);

用LambdaQueryWrapper还有一层好处,就是编译期能检查字段名,字段写错了会直接编译报错,不会等到运行期才告警,这一点对开发效率影响非常大。

3.4 登录鉴权与角色权限的控制实现

认证方案我选了JWT无状态方案,没有用Session,主要考虑到前后端分离架构下Session跨域处理麻烦,而JWT天然无状态,前端存储token,每次请求在请求头里携带即可。流程是这样:用户登录成功后,后端生成token返回给前端;前端后续请求在请求头中加入Authorization: Bearer <token>;后端拦截器解析token,获取用户ID和角色,放行或拒绝请求。

生成token我用的是hutool工具包的JWTUtil,省去手写JJWT的繁琐代码。token里只放user id和role两个字段,过期时间设置为24小时。

public class JwtUtils { private static final String SECRET = "your-secret-key"; public static String generateToken(Long userId, String role) { return JWT.create() .setPayload("userId", userId) .setPayload("role", role) .setExpiresAt(DateUtil.offsetHour(new Date(), 24)) .setKey(SECRET.getBytes()) .sign(); } public static JWTValidator validateToken(String token) { return JWTValidator.of(token).validateAlgorithm(JWTSignerUtil.hs256(SECRET.getBytes())); } }

权限控制通过拦截器实现,拦截所有/api/**请求,白名单放行登录接口。拦截器里解析token后把用户信息放入ThreadLocal,后续Service层就能直接获取当前操作人是谁。

说实话,JWT的坑也有。最典型的问题是token无法主动失效,如果用户修改了密码,旧token在24小时内仍然有效。我的解决方案是引入一个token_version字段,用户修改密码时版本号加1,JWT中携带版本号,拦截器对比数据库中的版本号,不一致就拒绝访问。这个设计在文档里没有,但我觉得对于真实交付系统来说很必要。

3.5 核心业务状态流转的实现

隔离人员从登记到解除,状态流转必须严谨。我的设计是状态字段加上操作时间的联动。登记入住时,如果房间为空闲,就同时执行三个操作:更新人员状态为隔离中,更新房间状态为占用,写入一条操作日志。这三个操作必须在一个事务里,否则就会出现人员已登记但房间没被占用的数据不一致问题。

@Transactional(rollbackFor = Exception.class) public Result<Void> checkIn(CheckInRequest request) { // 校验房间状态 Room room = roomMapper.selectById(request.getRoomId()); if (!"FREE".equals(room.getStatus())) { return Result.error("该房间当前不可用"); } // 登记入住 IsolationPerson person = new IsolationPerson(); person.setName(request.getName()); person.setRoomNo(room.getRoomNo()); person.setStatus(1); person.setCheckInTime(LocalDateTime.now()); isolationPersonMapper.insert(person); // 占用房间 room.setStatus("OCCUPIED"); roomMapper.updateById(room); // 写日志 OperationLog log = new OperationLog(); log.setContent("人员【" + person.getName() + "】入住房间【" + room.getRoomNo() + "】"); operationLogMapper.insert(log); return Result.success(null); }

@Transactional注解强调了事务边界。如果这里不加事务,第二步房间更新失败后,第一步已经写入了数据库,后面再做解除隔离、统计报表,数据就会错乱。这类状态型业务一定要把事务放在Service层,在Controller层加事务注解是无效的,因为事务要通过Spring代理生效,Controller交给Service调用的入口在代理对象内部,多层的调用链容易出现代理失效。

解除隔离的流程同理,需要同时更新人员状态为已解除、房间状态为消杀中、生成解除记录。我把解除逻辑和登记逻辑做了对称设计,保证状态流转闭环。

4. 前端实现:Vue3 + Element Plus怎么把后台页面做顺手

4.1 Vite初始化与项目目录划分

前端我用Vite创建Vue3项目,命令是npm create vite@latest isolation-admin -- --template vue。选Vite不选webpack的原因很直接,Vite的开发服务器冷启动几乎秒开,热更新速度也快,Vue3官方推荐的脚手架就是Vite。

项目目录结构我按后端模块做了对应划分,views下面每个模块一个文件夹,每个文件夹里放index.vue和对应的子组件。这样维护起来非常直观,后端的接口路径和前端页面的目录结构一一对应。

src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── views/ # 页面组件 │ ├── login/ │ ├── dashboard/ │ ├── person/ │ ├── room/ │ ├── health/ │ ├── material/ │ └── notice/ └── utils/ # 工具方法

4.2 路由守卫与权限控制

后台管理系统的路由不能随便访问,没登录的人直接访问首页必须被拦截跳转到登录页。这个逻辑放在路由守卫里实现。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else if (!token) { next('/login') } else { next() } })

除了登录状态,角色权限我用了前端按钮级控制。比如只有管理员能看系统用户管理页面,医护人员只能看到健康管理和自己的工作台。方案是在Pinia的userStore里存当前用户的role,然后页面上通过v-if判断是否渲染特定按钮或菜单项。后端的接口也要做同样的权限校验,前端控制只是提升体验,真正保证安全的是后端。

4.3 Axios封装与接口管理的设计思路

前端请求必须有统一的拦截器,这是我在项目里花时间最多但收益最大的部分。拦截器主要做三件事:请求头注入token、响应统一处理、错误统一提示。

const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use(response => { const res = response.data if (res.code === 200) { return res } else { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } else { ElMessage.error('网络请求异常,请稍后重试') } return Promise.reject(error) })

这里重点说下401处理。JWT过期后,后端拦截器会返回401状态码,前端此时要做的就是清掉本地token并跳回登录页。如果不在这个统一拦截器里处理,那每个页面都要单独判断登录失效,代码量会成倍增加。统一拦截器一次性解决这个问题,这也是后端返回状态码要统一的原因。

接口管理上,我把每个接口导出为独立的函数,页面上只关心中间调用逻辑,不关心URL拼接。这种把接口层、页面组件层、状态管理层分离的做法,在系统功能扩展时会非常受益。比如以后后端接口地址变了,只需要改api目录下对应文件就行,页面代码完全不用动。

4.4 核心页面的交互设计:以人员登记与健康上报为例

人员登记页面是这个系统里最复杂的表单。字段有姓名、证件号、手机号、来源地、房间选择、计划解除时间。表单验证直接用Element Plus的表单校验规则,手机上必须符合11位数字,证件号按身份证格式做正则校验。

房间选择用级联选择器实现,先选楼栋,再选楼层,最后选具体房间。关键点是:房间下拉框的数据应该只显示空闲状态的前端实时请求房间列表,根据status字段过滤。房间列表数据量一般不大,直接查询全部再在前端过滤也能接受,但更标准的做法是后端提供room/freeList接口,只返回空闲房间。

健康上报页面的重点在于连续记录。前端做成一个日期选择器加表单的结构,用户选择日期,填报体温、是否咳嗽、是否乏力。后端保存时会自动关联当前人员和登录用户,不需要前端传用户ID。这个细节很重要,如果前端能把用户ID传给后端,攻击者就能伪造别人的健康记录。所以后端一定要从token里取用户信息,不能相信前端传过来的任何用户标识。

数据统计页面我用了ECharts绘制体温趋势折线图和每日新增隔离人员柱状图。ECharts的Vue3封装库叫vue-echarts,使用起来非常方便。要注意的是ECharts图表的容器必须有明确高度,否则图表初始化后高度为0,什么都看不到。这个是ECharts最常见的坑,比配置项出错还多。

4.5 前端联调的跨域问题处理

开发阶段前端跑在5173端口,后端跑在8080端口,两者之间跨域。最方便的方案是在Vite的配置文件里配代理,而不是在后端开CORS。代理的好处是前端请求的URL是相对路径,后面部署到同一域名下时不需要改任何代码。

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

后端也可以配置CorsFilter,但我在项目里还是推荐用Vite代理。原因很简单:开发环境用代理,生产环境把前端打包后的dist目录和后端jar包放在一起,由SpringBoot统一提供静态资源服务,天然就没有跨域问题。前后端分离部署另说,但课程设计和毕设项目一般都部署在一台机器上,代理方案最省心。

5. 环境准备与部署实测记录

5.1 MySQL8.0安装与初始化记录

我这次用的是在Docker中安装MySQL8.0的方式,部署起来非常干净。Docker安装MySQL8.0的关键参数是端口映射、root密码、数据目录挂载。

docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e TZ=Asia/Shanghai \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0

这个命令有几个值得注意的地方。-e TZ=Asia/Shanghai指定时区,不加的话MySQL默认使用UTC时区,这样会导致数据库存储的当前时间和北京时间差8个小时。数据目录挂载到宿主机,是为了容器删除重建时数据不丢失。

当然如果你本机已经装了MySQL,直接用本机的也可以。我的建议是装原生版还是Docker版看你自己情况,关键是数据库版本必须是8.0,因为MyBatis-Plus的DbType.MYSQL和driver的配置都跟版本强相关。

初始化数据库时执行建表脚本,然后记得确认用户表权限。如果用的是root用户连接,一般没问题;但如果新建了专门用户,需要授权:

CREATE USER 'isolation'@'%' IDENTIFIED BY 'isolation123'; GRANT ALL PRIVILEGES ON isolation_db.* TO 'isolation'@'%'; FLUSH PRIVILEGES;

5.2 后端配置文件实测清单

后端配置文件application.yml是项目能跑起来的命脉,我把关键配置项逐项列出来。首先是数据源配置,MySQL8.0的driver-class-name是com.mysql.cj.jdbc.Driver,不是老版的com.mysql.jdbc.Driver,写错就启动失败。URL里必须带serverTimezone=Asia/Shanghai,否则连接会报时区错误。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/isolation_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

mybatis-plus.configuration.log-impl这里我开了SQL日志打印,开发阶段能直观看到每条执行的SQL语句,排查问题非常方便。等打包上线前可以注释掉,避免日志刷屏。

逻辑删除配置也很关键,deleted字段配合逻辑删除配置后,MyBatis-Plus在执行delete操作时会自动转为update语句,更新deleted为1,查询时会自动加上deleted=0条件。这个机制避免了物理删除导致的历史数据无法追溯问题。

5.3 前端打包与服务集成

开发完成后,前端需要打包成静态资源,然后放到后端工程里统一提供服务,这样整个系统只需要启动一个Java进程就能访问。

npm run build

构建完成后,dist目录下就是打包产物。把dist目录里的内容复制到SpringBoot项目的src/main/resources/static目录下,重新打包后端即可。

mvn clean package -DskipTests java -jar target/isolation-system.jar

启动成功后访问http://localhost:8080就能看到登录页面。前端静态资源交给SpringBoot托管后,路由需要做处理。Vue Router默认使用history模式,直接访问/person会返回404。我的做法是使用hash模式,URL带#号,虽然不太美观,但最大的好处是不需要额外的服务器配置。毕设答辩阶段演示系统时,hash模式是最稳妥的选择。

部署过程中实测需要特别留意的,就是jar包运行时不能直接双击打开,要用命令行java -jar启动。如果想在服务器后台运行,用nohup java -jar target/isolation-system.jar > app.log 2>&1 &把日志输出到文件中,方便排查问题。

6. 常见问题与排查技巧实录

6.1 MySQL8.0连接报错实战排查

我在联调阶段遇到最多的问题就是数据库连接失败。总的来说有三类错误最典型。第一类是Public Key Retrieval is not allowed,解决方案是连接URL加allowPublicKeyRetrieval=true,另外把useSSL=false也加上,测试环境能省很多麻烦。第二类是Unknown database 'isolation_db',说明数据库没创建或名字拼错,在SQL客户端中执行SHOW DATABASES;确认。第三类是Access denied for user,说明用户名密码错误或授权不到位,重新授权即可。

时区错误也是高频问题,错误信息里包含serverTimezone字样。出现这个问题说明连接URL里没带时区参数,在上面5.2节的配置里已经解决了。

6.2 MyBatis-Plus的常见误用

第一个坑是分页插件没配置,查出的数据不是分页结果而是全表数据。这属于配置遗漏,加上PaginationInnerInterceptor就好了。第二个坑是逻辑删除配置了但实体类字段名不匹配,提示找不到deleted字段,检查实体类是否标注了@TableLogic注解,或全局配置中logic-delete-field是否和实体字段名一致。第三个坑是条件构造器直接new QueryWrapper后暴露SQL注入风险,我建议统一使用LambdaQueryWrapper或LambdaUpdateWrapper,用方法引用代替字符串列名,既安全又能编译期发现问题。

还有一个小技巧:MyBatis-Plus的selectCount方法传入null参数表示查询全表,但没人管的话清空表数据很危险。我对这个项目中所有统计类操作都显式传入条件构造器,而且加上了deleted=0条件,避免把逻辑删除的数据也算进去。

6.3 前后端跨域与鉴权联调问题

前端页面打开后接口报跨域,首选方案就是检查Vite的proxy配置,确认target地址是否正确,端口是否对应后端实际启动端口;其次确认后端Controller的@RequestMapping前缀是否和proxy的/api前缀一致,不一致会导致代理转发后404。另外还要检查SpringBoot的context-path设置,如果配置了context-path,后端接口的实际访问路径会变长,代理也要相应调整。

登录接口调用成功但拿不到用户信息,优先检查后端拦截器排除名单是否写对。如果拦截器拦截了/api/user/login,登录请求直接401,那问题就很明显。JWT解析报错通常是密钥不一致导致的,检查前端拿到的token和后端生成token时用的密钥是否匹配。

6.4 前端开发调试的那些坑

Vue3开发过程中最容易出问题的是响应式数据的误用。如果你的数据在页面中修改后视图不更新,多半是用普通变量代替了ref或reactive,这是Vue3响应式原理的基本功。另外在Composition API中,直接在setup函数里写setTimeout并修改数据对象属性,不需要额外处理响应式,但如果用了解构赋值把响应式对象拆成普通变量,那修改就失效了。

表格数据加载不出来时,先用浏览器开发者工具的Network面板看接口是否返回正常数据,重点看后端返回的data字段结构是否符合el-table的prop绑定。常见错误是后端返回字段名是createTime,前端绑定的是create_time,两者不一致导致表格列全空。

Element Plus组件库版本要注意和Vue版本匹配。Element Plus不支持Vue2,必须用Vue3项目。如果引入后页面空白且控制台报组件未注册错误,检查是否忘记了app.use(ElementPlus)。

7. 这套源码里的隐藏价值与后续扩展建议

说实话,这套系统最值得研究的部分不是功能有多花哨,而是它完整展示了SpringBoot2 + Vue3从零搭建一个后端管理系统的标准流程。表结构设计、统一结果封装、通用CRUD抽象、JWT鉴权、前端路由守卫、Axios拦截器,这些都是以后不管做什么管理类系统都会反复用到的技术点。把这套源码吃透,你就能举一反三。

如果后续想给这个项目加分,我个人建议优先扩展三个方向。第一个是Excel导入导出功能,人员批量登记、健康记录导出,用EasyExcel就能实现,这个功能在答辩演示时非常出效果。第二个是ECharts大屏展示,把隔离人员趋势、房间占用率、物资消耗汇总做成可视化看板,整个系统的档次一下就上去了。第三个是Redis缓存优化,给用户权限和房间列表加上缓存,提升接口响应速度。

技术学习这块,做完整个项目之后我的最大感受是:一定要自己动手把整个流程走通一遍。看别人分享的crud封装、分页配置、jwt拦截器,看的时候觉得很简单,真正动手时才发现一堆细节问题。特别是事务边界放在哪一层、Vite代理怎么配、静态资源如何统一部署,这些问题只有自己操作一次才能真正记住。

最后再分享一个小技巧:项目开发过程中每完成一个模块,记得把运行效果截图保存下来。无论是写课程设计文档还是答辩PPT,这些截图都是最有说服力的素材。另外数据库设计文档用Navicat或DataGrip导出一份Word版,连注释一起导出,这个文档比你自己重新画表结构快得多,而且格式规范,导师看了也会觉得你做得扎实。

返回列表