
2020到2022这几年社区、街道、园区、中小型企业里到处都在部署疫情信息管理系统。那阵子我接了好几个类似的单子从村委会到物业公司需求五花八门但底层逻辑高度一致居民基础信息管理、健康状态上报、出入登记、检测记录、统计看板再加一套角色权限。现在这波需求过去了回头再看这套系统反而成了最典型的SpringBootVue全栈实战项目——业务模型不复杂但五脏俱全权限、CRUD、报表、联调部署全都覆盖特别适合拿来练手或者作为毕业设计二次改造。这篇文章我就把这套系统的完整设计思路、核心代码实现和踩坑记录全部梳理一遍给正准备动手的同学一份能直接落地的参考。1. 项目定位与整体设计思路1.1 这类系统的本质是什么很多人一看到管理系统就觉得很简单无非是几个页面加上增删改查。但实际做过的人都知道小型业务系统的难点从来不在某个单点功能而在于把一条完整的业务数据链理顺。疫情信息管理系统表面上是一堆表单本质上是一条闭环居民建档 → 每日健康上报 → 异常预警 → 检测登记 → 出入管理 → 统计报表每一环的数据都要能被追溯每一个状态变化都要有记录。我在做需求调研时发现社区和物业真正关心的其实就三件事第一辖区内到底有多少人每个人的底数清不清楚第二每天有没有人发烧咳嗽、有没有异常结果能不能第一时间发现第三上级要日报数据时能不能十分钟导出一份准确报表。搞清楚这三点系统设计的主线就出来了以居民档案为主数据以健康记录和检测记录为动态数据以统计报表为输出。功能清单可以列得很长但核心逃不出这几块。1.2 需求拆解与模块划分我习惯先把角色画出来再定功能。这套系统里我设计了三种角色权限边界很清楚角色职责范围典型操作系统管理员系统级配置用户管理、角色管理、社区管理、数据初始化社区工作人员业务核心操作用户居民建档、每日上报审核、出入登记、数据导出居民自助填报与查询本人健康上报、查看检测记录、查看社区通知对应的功能模块就是系统管理、居民档案管理、健康监测、检测管理、出入管理、通知公告、统计报表。其中统计报表是容易被忽视但绝不能省的模块因为管理者的日常动作是看数据不是录数据。一个系统如果只能录不能看用三天就会被弃用。设计上还有一个容易被忽略的点居民和登录用户要做成两张表。很多人图省事直接在一张表里加角色字段居民既是用户也是档案这样短期能跑但后面做批量导入、身份证号唯一校验、一人多社区关联时会非常痛苦。账号是账号档案是档案两表通过关联字段绑定这才是能长期维护的结构。2. 技术选型为什么是SpringBootVueMyBatisMySQL2.1 后端选型逻辑后端选SpringBoot几乎不用犹豫。SpringBoot框架的好处是起步快、生态成熟、招人好招最重要的是社区资料多遇到问题一搜就有解。我用的版本是2.7.x搭配JDK 8。这里必须提醒一句如果你的项目是新开的千万别一上来就上SpringBoot 3.x除非你已经准备好面对javax和jakarta命名空间的全面替换以及JDK 17的要求。网上大量教程和现成代码都是基于2.x写的版本太高反而让你陷入照着抄都报错的尴尬。等到对这套体系熟了再迁移3.x不迟。持久层选MyBatis而不是JPA是我的个人偏好也是这类系统的实际需求决定的。业务系统里最常写的是多条件组合查询、动态更新、复杂统计SQL这些正好是MyBatis的强项。动态SQL写在XML里可读性好DBA同事也能直接接手优化。而JPA的自动映射在简单CRUD上确实爽但在统计报表场景下你要么写原生SQL要么面对一堆Specification代码维护成本反而高。2.2 前端选型逻辑前端我用的是Vue配合Element UI组件库。Vue在国内中小系统里的普及率很高上手曲线平缓Element UI的表单、表格、弹窗组件基本覆盖了管理后台的常见界面需求。工程用Vue CLI搭建路由用Vue RouterHTTP请求用Axios图表用ECharts这套组合到今天依然能打。需要说明的是如果你现在从零开始可以考虑Vue 3 Element Plus组件库和生态都已经很成熟。但我这篇讲的项目是基于Vue 2.7的因为当时这套代码是在Vue 2生态下交付的而且存量项目迁移成本高很多公司至今还在用Vue 2维护旧系统。两种都可以关键在于把路由权限和组件化拆分的思路吃透这部分知识是通用的。2.3 数据库与整体架构形态数据库选MySQL没什么悬念。5.7和8.0我都用过5.7稳定8.0功能多安装配置教程也随处可查。唯一要注意的是驱动类名和连接串差异8.0要用com.mysql.cj.jdbc.Driver连接串必须带serverTimezoneAsia/Shanghai不然时间字段能把你搞得怀疑人生。整体架构就是标准的前后端分离后端SpringBoot提供RESTful API运行在8081端口前端Vue开发服务器跑在8080通过代理转发请求。生产环境则把前端构建产物打进SpringBoot的static目录一个jar包直接部署。这种形态最省事也给后面部署留了回旋余地。3. 数据库设计与核心表结构3.1 核心表设计一览数据库设计是我认为整个项目中最值得花时间的部分。表结构一旦定下来后面所有代码都是在为它打工。这套系统我设计了七张核心业务表加两张基础表表名用途关键字段sys_user登录账号username, password, role_code, statussys_role角色定义role_code, role_name, descriptioncommunity社区信息name, address, manager_idresident居民档案name, id_card, phone, community_id, building_no, unit_no, room_no, household_typehealth_record每日健康上报resident_id, body_temp, symptom, health_status, report_timetest_record检测记录resident_id, test_type, test_result, test_date, test_orgentry_exit_record出入登记resident_id, entry_type, purpose, destination, body_temp, event_timevaccination_record接种记录resident_id, vaccine_brand, dose_no, dose_date, vaccination_pointnotice通知公告title, content, publisher_id, publish_time这九张表少一张都不完整。我见过有人把检测结果和健康上报合成一张表省事是省事但统计口径会打架健康上报是每天每人一条检测记录是按批次多人一条两种数据频率不同、粒度不同硬揉在一起查询会很别扭。3.2 字段设计的关键决策先说居民表。id_card也就是身份证号是全系统最重要的关联字段必须加唯一索引而且不能为空。实际业务里经常遇到同一身份证在多个社区重复建档的情况所以代码里要做去重校验查询条件里也永远把id_card放在第一位。房屋信息用building_no、unit_no、room_no三段式不要合成一个住址字符串否则后面按楼栋筛选、按单元统计时只能LIKE匹配性能和数据准确性都堪忧。再说状态字段。这类系统的所有状态我都用status表示后端存0和1之类的数字前端展示时通过数据字典翻译成文案。比如检测结果0-正常、1-异常、2-待复核。这里有个经验状态字段永远不要用中文直接存否则改需求时得写一堆UPDATE接口也容易因为拼写问题出错。用数字加字典是成本最低的方案。时间字段统一用datetime创建时间create_time、更新时间update_time直接由后端在插入更新时填好不要依赖数据库默认值这样日志追溯时能明确是哪个服务写入的。3.3 查询与统计的SQL思路业务查询最重要的两个场景我用SQL展示一下核心思路。一是多条件分页模糊查询。前端传keyword、communityId、status后端用MyBatis动态SQL拼条件select idselectResidentPage resultTypecom.example.entity.Resident SELECT r.*, c.name AS community_name FROM resident r LEFT JOIN community c ON r.community_id c.id where if testkeyword ! null and keyword ! AND (r.name LIKE CONCAT(%, #{keyword}, %) OR r.id_card LIKE CONCAT(%, #{keyword}, %) OR r.phone LIKE CONCAT(%, #{keyword}, %)) /if if testcommunityId ! null AND r.community_id #{communityId} /if if teststatus ! null AND r.status #{status} /if /where ORDER BY r.create_time DESC /select这里有个MySQL排序的细节如果你用ORDER BY带中文别名或者对LEFT JOIN的字段排序注意别选错字段最好在SQL中用表别名限定避免多个表都有相同字段名时报Column id in order clause is ambiguous。二是统计看板的聚合查询。比如统计每天上报人数SELECT DATE(report_time) AS report_date, COUNT(*) AS cnt FROM health_record WHERE report_time #{startDate} AND report_time #{endDate} GROUP BY DATE(report_time) ORDER BY report_date这类SQL要注意DATE()函数会让索引失效数据量小无所谓数据量大时建议加一个report_date冗余字段写入时直接存日期查询走索引会快得多。这类优化我在真实项目里吃过亏百万级数据下一天时间差从十几秒压到几百毫秒。4. 后端核心模块实现4.1 工程结构与Maven配置后端工程我是按标准分层来建的简单清晰com.example.epidemic ├── controller # 接口层 ├── service # 业务层接口实现 ├── mapper # MyBatis接口 ├── entity # 数据库实体 ├── dto # 请求/响应对象 ├── config # 配置类拦截器、跨域等 ├── common # 统一返回、异常、工具类 └── resources ├── mapper # MyBatis XML文件 └── application.ymlpom.xml里核心依赖就五个spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、jjwtJWT库、lombok。最多再加个poi做Excel导出。Maven项目构建方法SpringBoot项目时有个常见错误默认打的jar包不包含第三方依赖运行java -jar会报No main manifest attribute。解决方式是在pom里加spring-boot-maven-plugin的repackage配置这个插件会把所有依赖重新打包进fat jar。application.yml里有几个必须配好的项spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/epidemic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.epidemic.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置必须开因为数据表字段是下划线风格create_timeJava实体是驼峰createTime不开的话查出来全是null。log-impl配置成StdOutImpl可以在开发阶段把SQL打印到控制台方便排查问题上线前改成Slf4jImpl或者删掉。4.2 登录认证与权限控制认证方案用的是JWT。流程是登录接口校验用户名密码通过后生成一个带用户ID和角色代码的token返回给前端前端每次请求在Header里带Authorization: Bearer xxx后端拦截器解析token把用户信息放到请求上下文中后续接口根据角色代码判断是否有权限。JWT工具类核心逻辑很简单我用jjwt实现public String generateToken(String userId, String roleCode) { return Jwts.builder() .setSubject(userId) .claim(role, roleCode) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 12 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }拦截器配置里要做两件事放行登录接口和静态资源其余接口全部拦截拦截后解析token失败统一返回401前端收到401就跳回登录页。这里有个很容易踩的坑拦截器放行路径写错会导致前端页面能打开但接口全部401尤其是当你把前端dist包放在static目录后静态资源路径也要加到白名单里否则连js文件都加载不出来。权限控制我没用Shiro或Spring Security因为这套系统的角色就三种拦截器里加一个简单的角色判断足够用。如果你后续要扩展更细粒度的权限建议再引入Security但就当前项目而言过度设计反而增加学习成本。4.3 两类容易写崩的接口第一类是居民档案的综合查询接口。上面SQL已经给了对应Mapper接口和Service的实现就按标准三层走。这里我想强调一个习惯接口入参不要直接用Map必须定义DTO对象。用Map接收虽然开发快但参数校验、字段改名、接口文档生成全都会出问题。定义ResidentQueryDTO加上pageNum和pageSize配合PageHelper分页插件代码会清爽很多。第二类是统计报表接口这类接口的返回值往往是聚合后的DTO而非实体类。比如各楼栋异常上报统计返回的是MapString, Integer或者自定义的StatItemDTO。注意Mapper接口方法返回值类型和SQL查询列之间的映射如果SQL返回的是多列聚合结果最好建一个对应的DTO类接收不要滥用Map因为Map里的key大小写、类型不匹配问题排查起来极其痛苦。还有登录密码的存储必须用BCrypt加密千万别用MD5。MD5撞库成本太低社区系统的账号如果泄露用户的习惯密码也会跟着遭殃。Spring的BCryptPasswordEncoder直接可用加密后存60位字符串校验用matches方法即可。5. 前端核心模块实现5.1 环境搭建与工程初始化前端部分先讲环境。Vue安装及环境配置这块很多新手卡在Node版本上。我的建议是Node 14到16都行对应Vue 2.7和Vue CLI 4/5都能流畅跑。装好Node后执行npm install -g vue/cli然后vue create frontend创建工程。创建时勾选Router和AxiosCSS预处理器可要可不要。工程目录按功能拆分不是按页面堆src ├── api # 按模块封装的请求方法 ├── router # 路由配置 ├── store # Vuex或Pinia存用户信息 ├── views # 页面组件 ├── components # 公共组件 └── utils # axios封装、工具函数utils/request.js是必须花的功夫。对Axios做一个统一封装请求拦截器里给所有请求加上token响应拦截器里统一处理后端返回的{code, message, data}结构遇到401自动清空本地token并跳转登录页遇到业务错误用Element UI的Message提示。这样每个页面里的请求代码只需要写业务逻辑不需要重复处理错误状态。5.2 路由守卫与动态权限路由这块我在Vue Router里配置了两种路由公共路由登录页、找回密码页和需要登录的页面。用router.beforeEach做守卫逻辑很简单router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else if (token to.path /login) { next(/) } else { next() } })更精细的权限控制可以根据用户角色在守卫里判断目标路由的meta.roles是否包含当前角色不包含就重定向到401页面。我这个项目里路由是静态写好的只做菜单级控制。如果做动态路由需要登录后根据后端返回的菜单列表addRoute这一步比较复杂建议先把静态路由权限玩明白再进阶。这里提一下vue路由的坑如果生产环境用history模式用户刷新页面时Nginx或SpringBoot找不到对应的路径会出现404。解决方案有两个一是改用hash模式地址会带个#号但永不404二是在Nginx配置try_files $uri $uri/ /index.html。如果你按我下面的单jar部署方案走用hash模式最省心。5.3 核心页面的实现要点居民管理页面是整个系统的门面。实现上用Element UI的el-table展示列表el-formel-input做查询条件el-pagination做分页。这里最关键的分页参数管理pageNum、pageSize、查询条件必须和路由的query同步这样用户刷新页面后还能停留在原来的查询结果上。这个细节很多教程不提但真实使用中极其重要。统计看板页面用ECharts。我一般用两个图一个折线图展示近两周每日上报数量趋势一个饼图展示检测结果的正常/异常比例。图表数据从后端统计接口拿接口返回结构统一为[{name: 2024-01-01, value: 120}, ...]这种格式前端直接映射到ECharts数据源。需要注意在组件销毁时调用chart.dispose()释放实例否则页面切换几次后内存会涨得很快。表单校验也值得说。居民档案的身份证号格式、手机号格式前端rules里必须做校验身份证号还要按照18位长度和后六位特征做简单验证防止用户把手机号填进身份证栏。这些规则很机械但能帮后端挡掉80%的脏数据后端接口再校验一次就够了。6. 打包部署与常见问题排查6.1 前后端一体化的打包部署开发阶段前后端分离跑两个服务生产环境我希望只跑一个jar包。流程是前端先执行npm run build生成dist目录然后把dist里的所有文件复制到后端src/main/resources/static下重新打包后端。这样SpringBoot会把前端静态资源作为classpath下的文件直接托管访问http://ip:8081就是系统首页接口路径用/api开头做区分不冲突。如果你不想手动复制可以在后端pom.xml里配置maven-resources-plugin把../frontend/dist作为资源目录一起打进去。两者的效果一样我实际更喜欢手动复制加一个写好的脚本因为每次改动前端不需要重新构建后端也能单独调试。单jar部署的好处是运维成本极低拷过去直接java -jar epidemic.jar --server.port8081就跑起来了。内网环境配个MySQL就行不需要额外装Tomcat。6.2 高频报错速查表做这套系统的过程中我积累了一份高频报错清单贴出来给大家当避坑手册报错现象根本原因解决方案MySQL连接报SSL连接错误或Public Key Retrieval错误8.0默认要求SSL且caching_sha2_password需要公钥连接串加useSSLfalse和allowPublicKeyRetrievaltrue查询结果全是null驼峰和下划线映射没开mybatis配置map-underscore-to-camel-case: trueMapper方法找不到StatementXML的namespace写错或mapper-locations没扫对核对namespace接口全限定名和XML文件路径java -jar报No main manifest attribute没有配置spring-boot-maven-plugin的repackagepom里加插件并重新package前端打包后放static里刷新404history模式下路由没有fallback改用hash模式或后端转发到index.htmlSpringBoot版本太高javax包不存在Boot 3.x改成了jakarta命名空间项目用2.7.x或迁移所有import到jakarta端口被占用本地起了多个服务换端口或netstat -ano查进程后kill其中MySQL SSL连接错误是出现频率最高的因为很多人照抄旧教程的连接串去连8.0直接报通讯链路错误。加useSSLfalse是最快解法内网环境对传输加密要求不高的场景完全够用。6.3 性能优化与安全细节数据量起来以后性能问题会逐渐暴露。最常见的坑来自MyBatis缓存。MyBatis一级缓存是SqlSession级别的每个请求自动开启一般没问题二级缓存是跨SqlSession的这个项目里我建议直接关掉。因为二级缓存默认对查询结果做引用如果一个Mapper里嵌套了查询并且目标表被频繁更新会出现脏读排查起来比性能收益划算得多。如果要优化查询性能优先加索引、优化SQL、用分页而不是开缓存。大批量数据导入场景比如一次性导入几千条居民Excel档案不要循环调insert单条接口那样又慢又容易超时。用MyBatis的Batch模式或者直接用foreach拼批量insert语句一次插500条效率能提升一个数量级。Excel解析用EasyExcel比POI省一半内存2000条数据解析下来几乎无感。安全方面必须做的密码BCrypt加密存储、前端登录失败次数限制、接口层参数校验、管理端接口做操作日志记录。这个项目虽然属于内部系统但涉及居民身份证号和健康数据属于敏感个人信息上线前至少要把操作日志和数据库权限这两项补齐这是底线。这套系统我在实际交付中最大的体会是业务系统的价值不在技术炫技而在数据链路是否完整、权限边界是否清晰、运维是否省心。把居民档案做好、把统计口径定准、把部署方式简化比引入任何花哨的中间件都实在。如果你打算拿这套架构练手我的建议是别只照着抄代码找一个真实的社区场景把自己当成社区工作人员走一遍日常流程——录一个居民、报一条健康信息、查一张报表、导一次数据你会发现很多最初设计时想不到的问题。这个项目后续还可以扩展的地方不少比如接入企业微信通知、增加地图网格化展示、把审批流程独立成工作流引擎。起点不高但往上走的空间很大。