
很多朋友拿到一套前后端分离源码第一反应就是赶紧跑起来但真正导入之后会发现要么数据库连不上要么前端端口对不上要么登录接口直接跨域折腾半天还在原地打转。我基于SpringBoot后端Vue前端MySQL这套经典组合做了一版面向中小社区的疫情信息管理系统整套源码整理成了可导入、可运行的完整工程数据库脚本、后端接口、前端页面全部齐全。这篇文章就把整个系统从设计思路、数据库建模、后端实现、前端交互到部署运行完整拆开讲一遍。正在做毕业设计、接外包项目或者刚学完SpringBoot和Vue想找个完整项目练手的同学都可以直接照着这里的思路去改自己的项目。1. 整体设计与方案选型1.1 为什么选 SpringBoot Vue MySQL这个项目当初的定位就很明确中小社区使用数据量不大但要求稳定部署环境可能只有一台普通机器使用人员也不是专业IT所以技术栈不能玩得太花。SpringBoot负责后端接口和业务逻辑Vue负责页面渲染和交互MySQL负责数据持久化这套组合几乎就是目前前后端分离项目里最成熟、最容易找参考资料的方案。SpringBoot的好处在于开箱即用内嵌Tomcat不用单独配置服务器打包成jar直接扔到服务器上就能跑这对中小社区的信息化场景来说特别友好。社区管理人员不需要理解什么web容器、集群、负载均衡只要把jar跑起来前端页面能访问所有功能就都能用了。Vue这边我选的是Vue 3 Vite Element Plus的搭配。Vue 3的Composition API写业务逻辑更清晰Vite启动速度比旧版Webpack快很多开发体验好。Element Plus的组件风格统一表格、表单、弹窗这些管理系统里最常见的界面元素都有现成组件不用自己从头写UI对于个人开发者来说是省时间的关键。MySQL选择的是5.7及以上版本为什么不用8.0并不是8.0不好而是很多社区服务器用的还是老环境5.7兼容性更好且对于这套系统用到的表结构、索引类型5.7和8.0在性能上没有任何肉眼可见的差别。我用的是5.7写的建表语句其中涉及到的一些时间默认值写法在8.0里也完全可以兼容。1.2 系统功能模块划分在做任何项目之前先想清楚功能边界这个系统我最开始规划了六个核心模块分开看都比较简单合在一起就成了一个完整的管理闭环用户管理管理员账号体系支持新增系统用户、修改密码、分配不同角色。居民信息管理维护社区内居民的基本档案包含姓名、门牌号、手机号、身份证号等关键字段。健康信息上报居民或者管理人员每天提交健康状态、体温、行程等数据系统对重复上报做拦截。数据统计看板通过图表展示每日上报人数、异常比例、趋势变化方便社区管理者快速掌握情况。公告发布管理管理员发布社区通知居民端能查看公告列表和详情。系统配置包含基础参数设置和操作日志记录。这些模块单独拎出来任何一个都不算难但把它们结合到一起就要求后端接口设计、数据库表关联、前端页面跳转都要有全局考虑。比如统计看板需要聚合健康上报表数据公告列表需要关联用户表查询发布人姓名这种跨表、跨模块的联动才是真正锻炼项目能力的地方。1.3 项目目录结构说明拿到源码后如果看懂了目录结构基本就成功了一半。后端我按SpringBoot的标准分层来组织controller层只负责接收参数和返回结果service层处理业务逻辑mapper层操作数据库entity层放数据库实体对象。这样分层的好处是业务逻辑和接口入口解耦后面加功能的时候不会改一个地方就牵连一片。我习惯在util包里放工具类包括统一返回结果封装、日期处理、全局异常处理。config包里放配置类包括跨域配置、拦截器注册、MyBatis-Plus分页配置。项目里还用到了一种常见的规范凡是前端需要的数据结构后端统一用Result对象包装里面包含code、message、data三个字段前端根据code判断业务是否成功这样前后端联调时对数据格式有统一预期。前端目录结构同样重要views目录按页面模块分文件夹login、dashboard、resident、report、notice、user每个功能模块独立一个文件夹api目录统一存放接口请求函数按模块拆分成不同文件router目录维护所有前端路由store目录用Pinia管理全局状态主要存用户信息和token。2. 数据库设计与核心表结构2.1 用户表与角色设计一个信息系统如果没有权限控制功能再多也是摆设。这套系统的用户体系我设计得比较轻量没有引入复杂的RABC权限框架而是通过用户表里的role字段来区分管理员和普通操作人员。sys_user表的核心字段包括id、username、password、real_name、role、phone、status、create_time。password字段我存的是BCrypt加密后的密文绝对不存明文。BCrypt的好处是每次加密生成的密文都不同即使两个用户密码一样数据库里的密文也不同能有效防止彩虹表破解。role字段我用字符串类型admin代表系统管理员operator代表社区工作人员。虽然这种设计扩展性不如独立的角色权限表但对于中小社区的管理场景已经完全够用。如果后面真的需要增加角色权限细分只需要把role改为角色ID再增加一张sys_role表做关联即可。2.2 居民档案表设计居民信息是整个系统的数据基础。community_resident表我特意设计成包含常用档案字段和扩展字段两部分。核心字段包括id、name、gender、id_card、phone、building、unit、room_number、health_status、create_time、update_time。building、unit、room_number这三个字段我拆开了没有合并成一个大字符串这点在实际使用中非常关键。因为统计报表经常要按照楼栋、单元来汇总数据如果信息全部塞在一个字段里SQL聚合就得靠字符串截取既麻烦又容易出错。拆成三个独立字段后一条GROUP BY building就能出整栋楼的统计结果查询效率和代码可读性都会好很多。health_status字段用于标记居民当前的健康状态比如normal、observe、abnormal这个字段会被统计模块频繁查询所以我给它建了普通索引。另外身份证号id_card字段也加了唯一索引防止一个人被重复录入档案。2.3 健康上报表设计健康上报表是整个系统里数据量增长最快的表因为每天每个居民可能都会有一条记录。health_report表的核心字段包括id、resident_id、report_date、temperature、health_status、symptom_desc、travel_history、contact_history、reporter_id、create_time。居民ID和时间日期这两个字段联合起来做了唯一索引也就是说同一个居民同一天只能上报一次第二次提交会被数据库直接拒绝。这个设计的价值在后端接口里体现得很明显不需要先查一次有没有记录再决定是插入还是更新直接执行插入语句如果报唯一索引冲突就提示用户当天已上报。symptom_desc、travel_history、contact_history这些文本字段我没有给它们建索引因为这类字段主要用于查看详情很少用于条件查询建索引反而浪费存储空间。数据库索引设计的核心原则就是查询频繁且区分度高的字段才值得建索引那种随便建一堆索引的做法写数据的时候会明显变慢。2.4 物资与公告表设计物资管理表material_info和公告表notice_info相对简单但这也是实际项目中很常见的模块。material_info表包含id、material_name、total_count、used_count、remaining_count、unit、supplier、update_time。剩余数量我没有单独建字段而是通过total_count减去used_count计算得出这样可以避免两个字段同时维护导致数据不一致的情况。notice_info表包含id、title、content、publisher_id、publish_time、status。publisher_id关联用户表查询公告列表时用LEFT JOIN把发布人姓名也带出来前端列表上直接显示发布人不用发二次请求。公告的content会包含较长文本所以字段类型我用的是text在MySQL里text类型最大能存64KB的文本对社区公告来说绰绰有余。3. 后端关键功能实现3.1 项目初始化与依赖配置后端项目我用的SpringBoot 2.7.x版本搭配Java 8这个组合在目前的生产环境里是兼容性最稳的。SpringBoot 3.x需要Java 17虽然功能更新但很多老服务器、老教程、老插件还是基于Java 8生态对于一套面向中小社区的系统稳定和易维护比追新更重要。pom.xml里的核心依赖我列一下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependency /dependencies这里有几个细节要提醒。第一个是MyBatis-Plus的版本要和SpringBoot版本匹配3.5.x对应SpringBoot 2.x没问题。第二个是mysql-connector-java在较新版本里的groupId改成了com.mysql但用mysql这个groupId在2.7.x里仍然可以使用。第三个是jjwt这个库0.9.1版本在解析token时依赖jaxbJDK 8自带没问题但如果用JDK 11以上就需要额外引入jaxb依赖这点容易踩坑。application.yml的配置也简单直接server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0URL里必须带useSSLfalse和serverTimezoneAsia/Shanghai这两个参数是很多新手连接失败的直接原因。不带serverTimezone会报时区错误useSSL不设false的话MySQL 8.0在某些环境下会强制要求SSL证书校验。3.2 登录认证与安全拦截登录状态我用JWT来处理流程是客户端提交用户名密码后端校验成功后生成一个token返回前端把这个token存在本地之后每次请求都带着token后端通过拦截器统一校验。这种方法最大的好处是后端不存储Session适合前后端分离和横向扩展场景。我这里直接写一个拦截器的核心逻辑public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); try { Claims claims Jwts.parser() .setSigningKey(community-secret-key) .parseClaimsJws(token) .getBody(); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }注意两个细节。第一个是对于OPTIONS请求要直接放行因为浏览器发起跨域请求之前会先发送一个预检请求这个请求不带token如果拦截器拦了它前端就永远收不到真实响应。第二个是拦截到token后要把用户ID和角色存到request里后面的Controller就能直接从request里取当前用户信息不用每次查数据库。JWT的密钥我写在了代码里这是为了方便演示生产环境一定要把密钥放到配置文件中并且使用足够长的随机字符串。如果密钥太短攻击者可以暴力破解token内容伪造一个管理员身份的token后果非常严重。3.3 健康信息上报接口实现健康上报是系统的核心业务接口设计的核心就是一个防重复和快速插入。Controller层的代码非常简单PostMapping(/report) public Result? submitReport(RequestBody HealthReportDTO dto, HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); Long residentId determineResidentId(dto, userId); healthReportService.submitReport(residentId, dto); return Result.success(); }真正有逻辑的是service层我先判断居民是否存在再判断当天是否已经上报然后插入数据。虽然我在数据库层面加了唯一索引但代码层面也要做好预判断因为数据库唯一索引报错返回的异常信息不够友好直接返回给前端用户会看到一堆堆栈信息。异常处理我统一用了全局异常机制用一个ControllerAdvice类来捕获业务异常、参数校验异常和未知异常统一包装成Result对象返回。这样做的好处是前端axios拦截器只需要处理一个统一的响应结构不管是成功还是失败都有固定格式可以解析。上传体温、症状描述等信息时后端要做基础校验体温范围我限制在35到45摄氏度之间超出这个范围直接返回参数异常。这个校验逻辑可能在很多项目里会被忽略但它能防止明显不合理的数据污染统计报表比如有人误把37填成370一条异常数据就会把整栋楼的统计数据拉偏。3.4 数据统计接口设计统计看板需要三个核心数据今日上报总数、近七天上报趋势、各楼栋上报人数TOP10。这三个数据全部通过SQL聚合完成不需要Java代码去内存里做二次计算。近七天上报趋势我用一条SQL搞定SELECT report_date, COUNT(*) AS total_count FROM health_report WHERE report_date DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY report_date ORDER BY report_date;这条SQL查询出来后日期和数量是成对返回的。但实际业务中可能某一天没有任何上报记录那当天就不会出现在结果里前端画折线图就会少一个点、图像断开。解决办法是在Java代码里做一次日期补全把近七天的日期循环一遍没有数据的日期补0这样前端拿到的永远是完整的七天数据。这个补全逻辑看似多余但实际使用中非常影响体验。我第一次写这个接口的时候没做补全结果前端折线图一会儿七个点一会儿五个点后面接需求的时候就把“数据完整性”作为统计接口的设计原则固定下来了所有统计接口都保证返回时间段内连续的数据。4. 前端页面开发实录4.1 Vue工程创建与基础配置前端我选择Vite作为构建工具和Vue 3搭配几乎是官方推荐方案。创建工程只需要一条命令npm create vitelatest community-web -- --template vue创建完成后进入目录安装基础依赖然后安装Element Plus、Vue Router、Pinia和Axiosnpm install element-plus vue-router pinia axiosElement Plus按需引入能有效减小打包体积但中小项目为了省事可以直接全量引入。全量引入的代价是打包体积多几百KB但对于内网部署的项目来说影响微乎其微反而换来了开发效率不用每用一个组件就检查一次导入语句。我在main.js里统一注册Element Plus并在assets里引一个全局样式表然后初始化Pinia和Routerimport { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router import { createPinia } from pinia const app createApp(App) app.use(ElementPlus) app.use(router) app.use(createPinia()) app.mount(#app)4.2 路由配置与登录守卫前端路由我按页面功能划分登录页的路由设成公开其他页面统一走后端鉴权。Vue Router的beforeEach守卫是控制访问权限的关键位置我在这里读取Pinia里存的token如果没有token且访问的不是登录页就强制跳转到登录页。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else if (!token) { next(/login) } else { next() } })这种前端守卫只能做页面级的访问控制真正安全还是要靠后端拦截器。前端守卫防的是正常用户误入无权限页面后端拦截器防的是绕过前端直接调接口。两者配合前端体验好后端安全有保障。动态路由这个功能热搜词里很多人提到但它不太适合这套系统。中小社区的管理员角色就两三种路由表几乎是固定的用动态路由反而增加复杂度。如果项目真的需要根据用户角色显示不同菜单可以在侧边栏渲染时根据用户角色做v-if判断比动态添加路由要简单得多。4.3 Axios封装与请求拦截前端所有接口请求我都统一走封装好的request工具核心逻辑是给每个请求自动加上token并在收到响应时统一处理状态码。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code 200) { return res } 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) ElMessage.error(登录状态已过期请重新登录) } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } )baseURL设置成/api配合开发环境的代理转发可以完美解决跨域问题。前端的/api请求会被Vite代理到后端的8080端口浏览器根本感知不到跨域的存在。生产环境可以把前端构建后的文件交给Nginx托管再配置一个反向代理把/api转发到后端服务前端代码不需要做任何修改。4.4 统计看板与表单页实现统计看板用ECharts来画图这个库在Vue 3里使用很简单每个图表单独封装成一个组件。我在dashboard页面里放了一张折线图和一张柱状图分别展示上报趋势和楼栋分布。图表组件的核心是监听数据变化后重新设置optionwatch(() props.seriesData, (newVal) { chart.setOption({ xAxis: { data: props.dateList }, series: [{ data: newVal }] }) })健康上报表单页面用的Element Plus表单组件选的是日期选择器、输入框、单选按钮的排列布局。表单提交前会做一次前端校验必填项没填的用validate直接拦截减少无意义的网络请求。这里有个体验细节表单提交成功后我希望用户看到明显的成功反馈但又不希望页面刷新后清空所有填写内容于是提交成功后只清空部分字段并且给成功提示。具体做法是调用ElMessage.success弹出一个两秒后自动消失的提示条比旧式alert弹窗要柔和得多。5. 环境准备与一键启动5.1 本地环境要求在跑这个项目之前先把环境准备好省得中途报错找不到原因。我推荐本地环境配置如下JDK1.8或11不能低于8不需要高于11。Maven3.6以上用于下载后端依赖和打包。Node.js16.18以上推荐18或20Vite 4以上对Node版本有硬性要求。MySQL5.7或8.05.5以下版本会有语法兼容问题。开发工具后端用IDEA前端用VS Code这两个是目前最主流最好用的组合。环境变量的配置这里不多说网上教程一大把只说一句Java环境配置要确认JAVA_HOME路径正确Node安装要注意npm镜像源这两个地方是新手最容易卡住的。5.2 数据库初始化流程拿到源码后我在database文件夹里放了一个init.sql文件里面包含了全部建库、建表和初始化数据语句。用Navicat或者命令行执行这个SQL脚本整个数据库结构和基础数据就全部建好了。用命令行初始化数据库很简单mysql -u root -p init.sql按下回车后输入MySQL的root密码脚本会自动完成建库、建表、插入管理员账号等所有操作。跑完脚本之后最好验证一下执行SHOW TABLES能看到全部表清单再执行SELECT * FROM sys_user能看到初始管理员账号。我初始化的管理员用户名是admin密码是admin123是BCrypt加密后的密文直接插到表里的。第一次登录系统后建议立刻修改密码这个我放在系统文档的注意事项里特别标注了。5.3 后端启动步骤后端启动有两种方式。第一种最简单在IDEA里直接打开项目等待Maven依赖下载完毕后运行主启动类上的main方法控制台会打印SpringBoot启动Logo和Tomcat端口信息。第二种是打包成jar运行更接近生产环境mvn clean package -DskipTests java -jar target/community-system.jar第一次打包会比较慢因为要下载所有依赖到本地仓库。打包成功的标志是target目录下出现community-system.jar文件jar文件大小通常在60MB左右。运行起来后访问http://localhost:8080/api/hello如果能返回提示信息说明后端已经正常工作了。启动过程中如果报错端口被占用可以用这个方法先查端口netstat -ano | findstr 8080查到占用的PID后在任务管理器里把对应进程结束掉或者修改application.yml里的server.port改用其他端口。5.4 前端启动与联调前端启动相对简单进入前端项目目录后先安装依赖npm install如果网络比较慢建议先设置镜像源再install这一步能节约大量时间。安装完成后执行npm run dev看到Vite输出的本地访问地址http://localhost:5173后浏览器打开就能进入登录页。输入admin/admin123页面会跳转打开主界面当我第一次完整跑通前后端联调的时候那种成就感是刷一百个教程视频都换不来的体验。注意前端根目录下的vite.config.js里面已经配置了代理转发server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这个配置的意思是前端所有请求/api开头的地址都被Vite服务转发到后端的8080端口这样前端开发环境和后端之间不会出现跨域错误。如果后端端口改过同步改这里的target就行。6. 常见问题与排查技巧实录6.1 数据库连接报错排查这个项目运行中报错频率最高的一类就是数据库连接问题常见报错有Access denied for user、Communications link failure、Unknown database。这三个报错分别对应三种原因逐一排查就好。Access denied是用户名或密码不对检查application.yml里的username和password。Communications link failure是数据库没有启动或者地址端口不对先确认MySQL服务是否已经启动再确认连接地址里localhost:3306是否正确。Unknown database说明连接串里的数据库名不对确认一下init.sql里创建的库名是不是community_system。我把排查思路整理成一个速查表遇到此类报错时直接对照报错关键字可能原因排查方法Access denied账号密码错误或没有远程权限用命令行连接一次测试Communications link failureMySQL未启动或端口被占用查看服务状态测试3306端口Unknown database数据库名写错或未执行初始化脚本SHOW DATABASES检查Public Key Retrieval is not allowedMySQL 8.0的加密规则问题在链接参数里加allowPublicKeyRetrievaltrue6.2 跨域请求问题解决前端页面上能打开但登录接口一直报错浏览器控制台显示CORS错误这是典型的跨域问题。出现这个问题的原因通常有两种。第一种是没有使用我配置的前端代理方式而是直接在前端代码里把baseURL写成了http://localhost:8080这种请求从5173端口发到8080端口必然触发跨域限制。解决方法是改为使用/api相对路径和Vite代理。第二种是部署到生产环境后前端静态文件和后端服务在不同域名下这种需要后端配置CORS跨域策略或者用Nginx反向代理统一入口。我的后端代码里已经封装了CorsConfig类允许所有来源的请求跨域访问。开发环境下配合Vite代理等于上了双保险前后端联调基本不会因为跨域问题再折腾。6.3 token过期与登录失效处理系统运行一段时间后用户反馈登录状态突然失效要重新登录。这个问题的根源在于JWT的过期时间设置。我在生成token的时候过期时间设为8小时。如果管理员长期在线不做操作超过8小时后token过期下一次请求接口就会被拦截器拦截返回401状态码。前端axios封装里已经处理了401响应收到后自动清除本地token并跳转到登录页。这种设计的体验不能说好但安全性和易用性需要平衡。时间设短了用户频繁登录很烦时间设长了token泄露后的风险增大。如果觉得8小时不够用可以把JWT的过期时间改成24小时甚至7天但建议配合刷新token机制一起用才是正规做法。6.4 前端依赖安装报错处理npm install时经常出现两种报错。第一种是ERESOLVE unable to resolve dependency tree这种多数是Node版本太新或者太旧导致依赖版本冲突。解决办法是先删掉node_modules目录和package-lock.json再重新install还不行就切换Node版本。第二种是canvas相关的报错这种经常出现在某些图表库的安装过程中。解决方法是在操作系统里安装对应的编译工具或者在package.json里把canvas版本固定成一个稳定版。这个报错在Windows系统上比较常见装个windows-build-tools能解决大部分问题。还有一个小技巧如果npm install过程非常慢多半是源的问题执行npm config set registry https://registry.npmmirror.com换到国内镜像源速度会有明显提升。这个操作对后续任何前端项目都通用值得养成习惯。我个人在实际操作中的体会是能跑通一个完整的前后端分离项目对你理解SpringBoot和Vue的帮助远比看几十个单独的知识点视频大。你在调通接口的那一刻才会真正理解什么是请求、什么是响应、什么是状态码才会明白数据库的表结构和业务逻辑之间的关系。这套系统目前已经跑通了完整流程后端接口可以继续扩展比如增加Excel导出、消息通知这些模块前端页面也能继续美化做成一套完整的中小社区数字化管理平台。如果遇到具体报错自己先对着日志分析一遍再回来看这篇文章的排查列表大概率都能找到方向。