☰
SpringBoot2+Vue3社区疫情信息管理系统开发复盘
2026/10/10 14:17:00 网站建设 项目流程

这套系统做完已经有段时间了,源码和文档都整理齐全,正好写一篇完整的复盘。技术栈是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,面向的场景是中小社区的疫情信息管理,覆盖居民台账、健康申报、出入报备、通知公告和数据统计这些核心业务。如果你正准备做类似的Java Web方向毕设,或者公司里需要一个轻量级的基层信息采集与上报系统,这篇文章应该能帮你省掉不少调研和踩坑的时间。我会把从需求拆解、表结构设计、后端接口开发到前端页面联调、最终部署上线的完整过程都讲一遍,包括我实际遇到过的问题和对应的排查思路。

先说一句,这套系统不只是一个花架子页面。它按真实使用场景设计了三类角色:居民、社区工作人员、系统管理员,每一类角色看到的菜单、能调的接口都不一样。文档里包含了需求文档、数据库设计说明和部署手册,拿到源码之后照着部署文档配好环境,基本一两小时内就能跑起来。下面我按照做项目的实际顺序来拆解。

1. 项目全貌与需求拆解

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

中小社区的信息管理,痛点往往不是技术,而是数据太散。今天有人说自己从外地回来,明天有人说体温偏高,后天又要统计谁打过疫苗,如果全靠微信群接龙加Excel登记,信息根本没法核对,更别提给街道上报数据。这个系统的核心价值就是把“人、时间、地点、状态”四条线统一管理起来。

我把需求拆成了四个模块。人员台账是底座,每一条居民记录都要归属到楼栋和网格,后台可以按楼栋筛选导出;健康申报是居民端最主要的交互入口,每天填报体温、症状、是否接触过风险人群;出入报备记录每一次跨区域流动的时间和去向;通知公告由社区工作人员发布,居民登录后就能看到。最后所有数据汇总到统计页面,按日期、楼栋、状态做可视化展示。

这样拆分的好处是每个模块之间边界清晰,开发时可以切分成独立任务并行推进,也能对应到毕业设计的几个核心功能点。评审老师问起来,每一块都有明确的业务价值和实现逻辑。

1.2 技术选型背后的取舍

选这套技术栈不是因为它热门,而是每一环都有对应的理由。

后端用 SpringBoot2 而不是 SpringBoot3,主要原因是生态更稳定。很多老牌工具库、教程、排查方案都是基于 SpringBoot2 的,遇到问题网上能查到大量现成答案。而 SpringBoot3 强制要求 JDK17,且部分第三方组件的兼容性还没完全跟上来,对毕设和中小项目来说没必要冒这个风险。

ORM 框架选了 MyBatis-Plus,说白了就是看中它“少写重复代码”的能力。常规的单表增删改查、分页查询、条件拼接,它能直接给到现成方法,省掉了写大量XML映射文件的时间。这一点后面我会详细展开。

前端用 Vue3 是因为组件化开发效率高,配合 Vite 冷启动速度很快,开发调试体验比传统 JSP 加 jQuery 好太多。版本上 Vue3.4 加 Vite5 搭配 Element Plus 2.x,官方文档和社区资料都比较全。

MySQL8.0 在性能和功能上都比5.7强,窗口函数、公用表表达式这些特性虽然在这个项目里没用到,但后续扩展SQL分析场景时不需要换库。数据库选型这块,初期就要想清楚字符集和时区的问题,后面会专门说。

1.3 系统角色与核心模块清单

角色权限是这类管理系统的骨架。我设计了三张核心表来支撑权限模型:用户表、角色表、用户角色关联表。没有做细粒度的按钮级权限控制,一套基于RBAC思想的“角色-菜单”模型够用了,复杂度控制在合理范围内,而且实现起来不容易出错。

居民的日常操作集中在健康申报、出入报备、消息查看三个页面。社区工作者端则包含居民管理、审核报备、发布公告、数据统计。系统管理员多一个用户管理和角色配置权限。菜单不是写死的,而是根据角色动态渲染,这个点在答辩时通常是个加分项。

模块清单固定下来之后,前后端并行开发就有了依据。后端任务按模块拆,前端按页面拆,最后统一联调,整个节奏就很顺。

2. 数据库设计与后端核心实现

2.1 表结构设计与MySQL 8.0初始化

数据库一共设计了10张表,核心几张分别是:居民信息表、用户表、健康申报记录表、出入报备表、通知公告表、楼栋网格表。其中居民信息表和用户表通过关联字段绑定,健康申报表以居民ID作为外键关联。

建表时有两个细节必须注意。第一是字符集和排序规则,如果当时没用 utf8mb4,后面插入表情符号或者生僻字会直接报错。建库语句我建议在最开始就用:

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

第二是时区问题。MySQL8.0 默认时区经常导致 JDBC 连接报错,或者存入的时间比本机时间少8小时。我的解决办法是在连接参数里显式指定 serverTimezone=Asia/Shanghai,同时数据库端设置时区为东八区。这两个动作同时做了,就不会出现时间对不上的诡异现象。

关于表设计,我想多说一句设计经验:状态字段不要用字符串随便写,建议统一用数字枚举。比如申报状态 0=正常、1=异常、2=待复核,代码里定义常量或枚举类,前端用字典映射显示中文。这样后续写统计SQL时,直接对数字字段做分组统计,可比对字符串高效得多,也不容易因为命名不统一而埋bug。

2.2 用MyBatis-Plus把重复CRUD写少一点

MyBatis-Plus 在整个项目里承担了大部分数据访问层的工作。最常用的几个能力我梳理一下:

  • 内置 BaseMapper 提供 insert、deleteById、selectById、updateById 等现成方法
  • LambdaQueryWrapper 解决条件查询时字符串硬编码问题
  • 分页插件只需配置一个拦截器
  • 逻辑删除用 @TableLogic 注解,删除操作自动变成 update 语句

分页插件配置是关键之一,不配置的话分页查询实际不会生效,只是查出全量数据再在内存里切一刀。正确的配置方式如下:

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

setMaxLimit 是我额外加的,防止有人把 pageSize 传成很大的数字导致数据库压力过大,这个细节属于上线后才会意识到的隐患。

条件查询用 LambdaQueryWrapper 写起来非常直观。举个例子,查询某栋楼某天异常申报的记录:

LambdaQueryWrapper<HealthReport> wrapper = Wrappers.lambdaQuery(); wrapper.eq(HealthReport::getBuildingId, buildingId) .ge(HealthReport::getReportDate, startDate) .le(HealthReport::getReportDate, endDate) .eq(HealthReport::getStatus, 1) .orderByDesc(HealthReport::getReportDate);

这种写法不用手写SQL,也不容易因为字段改名后忘记同步字符串而报错。条件特别复杂的统计场景,直接上 @Select 注解写原生SQL,依然保留在Mapper接口里,不会和通用方法冲突。

2.3 统一返回与全局异常:后端代码的“门面”

刚开始写接口时最容易犯的毛病是每个Controller各返回各的格式,有人直接返回实体对象,有人返回Map,前端联调非常痛苦。我一开始就定了统一返回结构,所有接口都返回 Result ,包含 code、message、data 三部分。

这样前端拿到响应后先判断 code 是否为 200,再做后续处理。登录失效返回 401,业务异常返回 400,系统错误返回 500,规则统一之后,Axios 拦截器里写逻辑就非常清晰。

全局异常处理用 @RestControllerAdvice 实现,处理三类异常:自定义业务异常、参数校验异常、兜底的系统异常。自定义业务异常是用的最多的一类,比如“该居民还未绑定账号”“申报时间已过”这类提示,都在业务代码里抛出,异常处理器统一转成标准返回格式。

这里有个容易被忽略的点:参数校验不要全写在Controller里手动if判断。我在实体类字段上加了 @NotBlank、@Size 这类校验注解,Controller 参数用 @Validated 触发校验,异常处理器统一接住。代码干净很多,也显得有工程素养。

3. 登录鉴权与权限控制

3.1 为什么没直接套Spring Security

项目初期我也纠结过要不要引入 Spring Security + JWT 那套组合。后来评估下来,这套系统只有三种固定角色,接口数量也不多,Spring Security 的过滤器链配置反而增加了学习成本和排查难度。我自己实践下来选了更轻的方案:手动写一个登录接口,登录成功后签发 Token,再写一个拦截器统一校验。毕业设计答辩时,评委更看重的是你能把鉴权原理讲清楚,而不是配置有多复杂。

3.2 Token设计与拦截器实现

Token 我用的方案是 UUID 加 Redis 存储,登录成功后把 Token 作为 key、用户信息作为 value 写入 Redis,并设置过期时间,后续请求带上 Token,拦截器去 Redis 查询。这样做的好处是服务端能主动让 Token 失效,用户修改密码或管理员强制下线时,直接删除 Redis 里的对应 key 就行。

拦截器的核心逻辑其实很简单:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (StringUtils.isBlank(token)) { throw new BusinessException(401, "未登录"); } LoginUser loginUser = redisService.get(token); if (loginUser == null) { throw new BusinessException(401, "登录过期"); } UserContextHolder.set(loginUser); return true; }

UserContextHolder 是个基于 ThreadLocal 的工具类,请求进入时写入用户信息,响应结束后记得清理,否则线程池复用会导致数据串号。这个坑我亲眼见过,网上也有很多案例,加一个 afterCompletion 清理方法就能避免。

角色权限校验我放在需要做二次校验的接口上,用了自定义注解 @RequireRole 配合拦截器。比如删除居民的操作,只有管理员和社区工作人员能执行。这部分不难,重点是拦截器注册时注意排除登录接口、静态资源路径。

3.3 前端路由守卫与用户状态

后端做鉴权,前端也不能把所有页面都挂出来。Vue Router 里配置了动态路由,登录后根据后端返回的菜单列表生成路由表,用 addRoute 动态挂载。用户刷新页面时,从 Pinia 里读不到菜单数据会导致路由空白,所以我写了初始化逻辑:页面刷新后先调一次“获取当前用户信息”的接口,重建路由再进入目标页面。

前端路由守卫里的处理大概是:校验本地有没有 Token,没有就跳登录页,有就请求用户信息,成功则放行,失败就清空 Token 回到登录页。这套流程至少能让用户感知层面做到“该进的页面进得去,不该进的看不到菜单”。

4. Vue3前端实现与前后端联调

4.1 工程初始化与目录规划

前端工程用 Vite 创建,命令也简单:

npm create vite@latest frontend -- --template vue

装依赖时建议直接把 Element Plus、Axios、Pinia、Vue Router、ECharts 一起装上。构建完基础工程后,第一件事是规划目录。我按功能分包:api 目录放接口请求,views 目录放页面组件,router 目录放路由配置,store 目录放全局状态,utils 目录放工具函数,components 目录放公共组件。

这个目录规划花不了十分钟,但后期维护时非常省脑子。找接口去 api 里翻,公共弹窗去 components 里找,页面组件各自独立,不会出现文件越堆越乱的问题。

4.2 Composition API 封装与Axios拦截

Vue3 里我全面用的组合式 API,也就是 script setup 写法。相比选项式 API,它的优势在逻辑复用。比如表格页的“加载列表、搜索、分页、重置搜索”四件套,我抽成了一个 composable 函数 useTable,每个页面只需要传接口函数和查询参数,返回表格数据和操作函数,代码量直接缩减一大半。

Axios 封装是整个前端最值得花时间写的一块。我在 utils/request.js 里统一了 baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器给每个请求带 Token,响应拦截器统一处理 code 码为 200 的情况,401 跳转登录页,其他错误弹 ElMessage 提示。

baseURL 这块有个关键点:开发环境不要写死完整后端地址,用环境变量,vite 的 .env.development 文件里配置 VITE_API_BASE_URL。这样后面部署到测试环境、生产环境,只需要改环境变量文件,代码一行都不用动。

4.3 核心页面与图表展示

居民管理页面是最典型的“表格+搜索+弹窗表单”组合,Element Plus 的 el-table、el-pagination、el-dialog 基本能满足需求。刚开始写表格时建议把列配置和数据请求解耦,列的显示逻辑单独维护,后面加字段、调整宽度不用去翻数据逻辑。

健康申报统计页面用 ECharts 做可视化,我做了三个图:按日期折线图展示每日申报数量变化,按楼栋的柱状图展示异常申报分布,饼图展示状态占比。ECharts 在 Vue3 里的接入也很顺,在组件 onMounted 里 new 图表实例,然后 watch 数据变化调用 setOption 更新。

这里给个实用建议:图表数据不要前端自己拼复杂结构,让后端提供已经聚合好的接口,比如按日期返回 [{date: '2024-01-01', count: 128}]。前端只负责渲染,职责边界清晰,也方便以后换图表库。

4.4 开发期联调:Vite代理与跨域

前后端分离项目,开发期最烦人的就是跨域。Vite 自带 proxy 配置,把前端的接口请求转发到后端服务,浏览器里看请求是同源的,不需要后端配 CORS。配置在 vite.config.js 里:

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

注意 target 里的端口要和后端 application.yml 里 server.port 保持一致。这个方案我只在开发环境用,生产环境是 Nginx 统一反向代理,前后端部署在同一域名下,压根不存在跨域问题。这也是我建议所有前后端分离项目采用的方式。

5. 部署上线与问题排查实录

5.1 构建打包与服务器部署

后端打包如果用的是 Maven,一条命令搞定:

mvn clean package -DskipTests

打包产物是 target 目录下的 jar 包。运行命令是:

nohup java -jar community-system.jar --spring.profiles.active=prod > app.log 2>&1 &

生产环境的配置放在 application-prod.yml 里,数据库地址、Redis 地址都改成真实服务器的。需要注意日志配置要单独加,否则长时间运行单个日志文件会变得很大,排查问题时很不方便。我在这里加了 logback 配置,按天滚动生成日志文件,保留最近30天。

前端打包更简单,npm run build之后生成 dist 目录,把整个目录传到服务器,Nginx 配置好 root 路径和 try_files 重写规则就行。唯一容易漏的是刷新页面后白屏,这是因为 Vue Router 用了 history 模式,Nginx 需要把未知路径都转发到 index.html :

location / { try_files $uri $uri/ /index.html; }

5.2 后端常见问题速查

我把这次项目里遇到过的后端问题整理成了一张速查表,每个问题都是实际触发过的:

现象原因解决方案
启动报 Unable to load authentication plugin 'caching_sha2_password'MySQL8.0 默认认证插件与旧驱动不兼容升级 mysql-connector-java 到 8.x 版本
时间比实际少8小时数据库时区未设置JDBC 参数加 serverTimezone=Asia/Shanghai,数据库设置时区东八区
分页查询不生效,返回全量数据没配置 MyBatis-Plus 分页插件添加 PaginationInnerInterceptor 到 MybatisPlusInterceptor
更新数据时部分字段变 nullupdateById 只更新非null字段,传了null的字段被忽略需要更新null字段时用 UpdateWrapper 显式 set
逻辑删除后唯一索引冲突逻辑删除的数据仍然占着唯一索引值唯一索引字段设计中添加删除标记或采用其他策略

最后一条是个隐蔽问题。比如居民的手机号做了唯一索引,用户删除后记录还在表里,再新增同手机号的人就会冲突。我当时通过把逻辑删除字段也拼入唯一索引解决,例如唯一索引改为“手机号+deleted 字段”的组合索引。这个设计值得你在建表时就想清楚。

5.3 前端常见问题速查

前端踩到的坑相对少一些,但也都很有代表性:

现象原因解决方案
接口 404开发环境 Vite proxy 没生效确认请求前缀是否匹配代理配置的 /api
刷新页面后白屏动态路由没重新生成刷新时先请求用户信息再重建路由
跨域报 CORS 错误前端地址和后端地址不同源开发用代理,生产用 Nginx 同源部署
打包后图片资源 404静态资源引用路径错误Vite 配置 base: './' 使打包路径变相对路径
表格数据量多时页面卡顿一次性渲染太多 DOM分页之外配合虚拟滚动,或限制默认每页条数

关于“Vite 代理没生效”这个问题我再强调一下:经常有人把 axios 请求路径写成了绝对地址http://localhost:8080/api/...,代理配置就没意义了。接口路径必须以/api开头走相对路径,才能命中代理规则。

5.4 留给后来者的几条实战心得

这套系统从立项到上线,我最大的体会有三点。

第一,数据库设计一定要认真做。表结构基本决定开发效率的上限。字段命名统一用下划线风格,状态字段用枚举数字,公共字段如 create_time、update_time 每个表都带上,这些习惯能省非常多的事。

第二,接口联调一定要前置。不要等后端全部写完才开始写前端,开发和接口文档同步推进,才能尽早暴露格式不一致的问题。我在项目中期用 Apifox 维护接口文档,后端完成一个接口就标记一个,前端照着调,效率高了不少。

第三,文档不只是给答辩或者交付用的,更是给你自己保命的。部署文档里我记录了完整的服务器环境配置、初始化命令、常见报错处理方案,过三个月再回头维护这个项目,翻文档比翻代码回忆快多了。

最后再分享一个小技巧:开发期后端热部署加前端热更新都配置好后,改代码到看到效果基本是秒级反馈,这个体验会大幅提升你的开发效率,也更愿意频繁小步迭代,而不是攒一大坨代码一次性调试,那样出了问题定位都费劲。

如果你也准备做类似的 Java Web 管理系统,这套技术栈选型我认为依然值得参考。它没有追逐最新版本,但每一样都是久经考验的成熟方案,组合在一起稳定可靠,且资料丰富。照着我上面说到的设计和实现思路走下来,你可以少踩不少坑,最后交付一个能真正跑起来、讲得出亮点的系统。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询