前阵子整理了一套中小社区疫情信息管理系统的源码,技术栈是SpringBoot + Vue + MySQL,整体说不上复杂,但胜在结构清楚、能直接跑起来。对正在学JavaWeb全栈、想找一套完整项目练手的朋友来说,这套源码的价值在于:业务不绕,代码不花哨,却把社区台账、登记、统计、异常上报这几个核心环节完整串起来了。今天这篇文章不聊泛泛的架构理论,就按这套系统实际的实现思路来拆一遍,从选型、数据库设计、后端接口,到前端页面和最终打包部署,每一步都讲清楚“为什么这么做”,顺便把整理过程中踩过的坑也一并写出来。
1. 项目整体设计与技术选型解析
1.1 为什么是SpringBoot + Vue + MySQL这套组合
先说一个很多新手会踩的误区:看到一个管理系统就想着上Spring Cloud、Kafka、Redis,觉得组件越多越“高级”。实际做中小社区这种体量的项目,一台普通PC或者小服务器就够了,引入微服务和消息队列只会让部署复杂、排错困难、成本翻倍。
SpringBoot的优势在于“自带运行环境”。内嵌Tomcat,打成一个jar包就能跑,不需要单独部署容器;starter依赖体系把常见的数据库、缓存、安全组件都封装好了,一个CRUD项目基本不用写一堆XML配置。Vue这边,单页应用开发后台管理页面效率很高,配合Element UI组件库,表单、表格、弹窗、分页这些场景都是现成组件直接拼装。MySQL则是社区场景里最常见的数据库,运维人员熟悉,服务器上基本都有,备份和迁移资料也最容易找到。
把这三种组合放在一起看:
- 学习门槛低:前后端分离的经典结构,每个技术点都有大量资料
- 运行成本低:开发机就能跑,服务器配置要求不高
- 招人容易:SpringBoot和Vue的开发者非常多,后续维护不愁
对比一下其他方案更明显:用Python + Flask或Django也能做,但Java在后端稳定性和部署生态上更合适;用Vue 3 + Vite替代Vue 2 + Webpack当然也行,不过Vue 2 + Element UI的组件生态更成熟,网上现成的后台模板也多。至于数据库,换成PostgreSQL也能跑,但中小社区场景MySQL已经绰绰有余,没必要为了“新技术”给自己找麻烦。
1.2 核心业务模块与角色权限划分
这套系统最核心的业务思路就是“台账”。什么台账?每个居民的基本信息、每次健康登记的记录、每天的出入情况、每条异常上报的处置状态,都要有时间、有操作人、有结果。信息管理系统本质上就是把这个台账线上化,减少手工登记和口头传达造成的遗漏。
围绕这个思路,系统分成四个基础模块:
- 基础档案:居民信息维护,包括姓名、证件号、住址、联系方式、状态标记
- 健康登记:居民自主上报或社区代录,记录体温、身体状态、备注信息
- 出入管理:出入口登记,记录进出时间、事由、陪同人员等
- 异常上报:异常情况登记、处置流程跟踪、结果回填
角色权限方面,我在设计时没有做太复杂的RBAC模型,就三种角色:
| 角色 | 核心权限 | 说明 |
|---|---|---|
| 社区管理员 | 所有模块,维护档案、处理异常上报 | 日常业务的主要使用者 |
| 居民用户 | 查看个人信息、提交健康登记、查看自己记录 | 系统覆盖到居民自助场景 |
| 系统维护员 | 用户管理、日志查看、参数配置 | 负责系统运行维护 |
很多教程项目喜欢搞“用户表-角色表-菜单表-按钮表”四件套,但对中小社区来说,两级权限足矣。权限模型越复杂,前端路由和按钮控制就越繁琐,最后往往出现“配置了权限但没人用”的尴尬局面。
1.3 “可直接运行”到底意味着什么
拿到源码后能不能跑起来,取决于项目是否具备三个条件:初始化数据齐全、依赖版本兼容、运行路径清晰。这套源码把这三件事都解决了。
首先是初始化数据。源码自带完整的SQL脚本,里面包含默认管理员账号、示例居民数据、基本配置参数。导入数据库后系统就有东西可看,不用从空白页面开始摸索。其次是依赖版本,pom.xml和package.json里都锁定了经过验证的版本,不需要自己猜版本号。最后是运行路径,我整理了开发环境启动顺序和生产环境打包方案,后面章节会展开细说。
准备环境时注意:JDK 1.8以上、Maven 3.6以上、MySQL 5.7或8.x、Node.js 14以上。这套组合对机器要求不高,4G内存的笔记本就能流畅跑起来。
2. 数据模型设计与数据库表结构拆解
2.1 居民档案表:为什么把楼栋单元单独拆字段
设计数据库表结构时,最容易犯的错是“图省事”。比如住址信息,习惯性用一个address字段存储“3栋2单元501室”,看起来没什么问题,但等到要统计“3栋有多少人”时,就只能靠LIKE '%3栋%'模糊匹配,效率低不说,还容易匹配到“13栋”“30栋”这种数据。
所以我设计居民档案表时,单独拆出了building_no、unit_no、room_no三个字段,用楼栋号、单元号、房间号分别存放。这样做有两个直接好处:查询时可以用WHERE building_no = '3栋'直接精确匹配,索引也能用上;页面筛选时可以按楼栋下拉选择,不用让用户手输字符串。
居民档案建表语句核心部分如下:
CREATE TABLE resident_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '姓名', id_card VARCHAR(18) NOT NULL COMMENT '身份证号', phone VARCHAR(20) COMMENT '手机号', building_no VARCHAR(10) COMMENT '楼栋号', unit_no VARCHAR(10) COMMENT '单元号', room_no VARCHAR(10) COMMENT '房间号', resident_type TINYINT COMMENT '1常住 2租住 3临时', status TINYINT DEFAULT 1 COMMENT '1正常 2异常 3关注', deleted TINYINT DEFAULT 0 COMMENT '逻辑删除', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_id_card (id_card) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='居民档案表';说几个字段设计的细节。id_card加了唯一索引,目的是防止重复建档,这是居民系统的命根子。status字段单独拎出来,是为了让管理员能快速筛选“当前需要重点关注的居民”,如果这个状态信息放在业务记录里而不是档案表里,每次查列表都要关联子查询,很痛苦。deleted字段是MyBatis-Plus逻辑删除的标准做法,数据不物理删除,保证历史台账还可以追溯。
2.2 业务表:关联设计与状态流转机制
有了居民档案这张主表之后,健康登记、出入记录、异常上报都是典型的“业务表”。业务表的核心设计原则是:必带resident_id外键,同时冗余一个name字段。
CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, resident_id BIGINT NOT NULL COMMENT '居民ID', name VARCHAR(50) NOT NULL COMMENT '冗余姓名', temperature DECIMAL(4,1) COMMENT '体温', health_status TINYINT COMMENT '1正常 2干咳 3乏力 4其他不适', remark VARCHAR(255) COMMENT '备注', create_by BIGINT COMMENT '操作人ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康登记表';为什么冗余name字段?因为列表页展示时,如果每次都去JOIN居民表查姓名,数据量小的时候无所谓,数据量大了以后每页20条记录就要多出20次关联查询。冗余一列,空间成本几乎为零,查询效率却高很多。这种冗余是可接受的,只要保证写入时同时更新resident_id和name,不要让两个值不一致。
出入记录表和异常上报表的结构与健康登记表类似,区别在于各自有专属字段。出入记录多一个direction字段标记进或出;异常上报多一个handle_status字段表示未处理、处理中、已处理。
状态流转是这类系统的灵魂。居民状态不能只改status字段,必须同时写入一条异常上报记录,记录操作人、操作时间、处理结果。这背后的思想是“留痕”——任何状态变化都要能回答三个问题:谁改的、什么时候改的、为什么改。
比如一个居民的体温连续两次偏高,管理员把该居民状态从“正常”改为“重点关注”,此时系统必须自动生成一条异常记录,关联居民ID、当前状态、操作人ID。如果只是简单UPDATE,后续追查的时候就什么线索都没有了。
2.3 初始化SQL脚本的细节与导入方法
源码中的SQL脚本主要做三件事:建库建表、插入默认管理员、插入示例数据。脚本虽然简单,但有几个容易踩的坑值得单独说。
第一是字符集。建库语句必须明确写CHARSET=utf8mb4,不能依赖数据库默认值。这几年MySQL默认字符集虽然已经是utf8mb4,但老版本服务器可能还是utf8mb3,中文写入时会出现编码异常或者乱码。排序规则用utf8mb4_unicode_ci就能满足绝大多数场景。
第二是默认管理员的密码。用户表里存的不能是明文密码,要用BCrypt加密后的字符串。BCrypt加密每次生成的结果都不同,因为它自带随机盐,所以不要试图手工拼接一个加密串。正确做法是用后端的工具类生成一次加密结果,复制到SQL脚本里。
第三是admin用户相关的角色关联数据要一起插入,否则系统启动后登录成功却没有任何菜单权限,白白排查半天。
导入SQL很简单,命令行执行:
mysql -u root -p < /path/to/init.sql或者用Navicat、DataGrip之类的可视化工具直接打开脚本执行。导入完成后先查一下resident_info表有没有示例数据,确认导入成功再启动后端。
3. 后端核心模块实现(SpringBoot)
3.1 项目目录结构与登录鉴权流程
后端项目结构不搞花哨的分层,就按标准三层走:
src/main/java/com/community/ ├── config/ # 配置类:WebMvcConfig、MybatisPlusConfig、JwtInterceptor ├── controller/ # 接口层,只做参数接收和结果返回 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis-Plus的Mapper接口 ├── entity/ # 实体类 ├── common/ # 统一返回结果Result、全局异常处理 └── util/ # JWT工具类、加密工具类这样的分包对中小项目足够清晰。controller里不写SQL,service里不写SQL拼字符串,entity只做映射,每个类职责单一,新人接手也能快速定位文件。
登录鉴权是这个系统的安全底线。流程是:用户提交用户名和密码,后端查用户表,用BCrypt校验密码,校验通过后签发JWT令牌返回前端。前端把令牌存在localStorage里,之后每次请求都在Authorization头里带上,后端拦截器统一校验。
拦截器核心代码逻辑是这样的:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = JwtUtil.parse(token.substring(7)); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { response.setStatus(401); return false; } }这里有个细节值得一说:校验失败时返回401状态码而不是重定向到登录页。原因很简单,前后端分离场景下,页面跳转逻辑应该由前端路由守卫负责,后端只要明确告诉前端“你未认证”就够了。前端拦截到401后自动清理token并跳转登录页,这个链路比后端重定向更符合SPA应用的习惯。
JWT令牌里只放userId和username两个字段,不要放密码、手机号等敏感信息。过期时间设在2小时比较合适,太短会让用户体验差,太长有token泄露风险。加密密钥放在application.yml里,不要硬编码在代码中。
3.2 基于MyBatis-Plus的列表查询与统计接口
后台管理系统里数量最多的就是列表查询接口。居民档案、健康登记、出入记录、异常上报,每一个模块都有一张列表页,每张列表页都有搜索条件、分页、排序。MyBatis-Plus在这一块帮了大忙。
典型的居民分页查询实现:
@GetMapping("/resident/page") public Result<IPage<ResidentInfo>> page(@RequestParam Integer pageNum, @RequestParam Integer pageSize, @RequestParam(required = false) String keyword, @RequestParam(required = false) Integer status) { LambdaQueryWrapper<ResidentInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), ResidentInfo::getName, keyword) .eq(status != null, ResidentInfo::getStatus, status) .orderByDesc(ResidentInfo::getCreateTime); return Result.success(residentService.page(new Page<>(pageNum, pageSize), wrapper)); }这个方法里最核心的技巧是LambdaQueryWrapper的条件拼接,like和eq的第一个参数是布尔值,当条件为false时该条件自动忽略。这样就避免了传统方式里写一堆if判断再拼接SQL的麻烦。
有人可能担心LambdaQueryWrapper的写法会不会导致SQL注入,实际上MyBatis-Plus底层用的是预编译参数绑定,参数值不会直接拼进SQL,安全性有保障。
除了列表查询,仪表盘还需要统计数据。比如按日期统计健康登记人次,这种聚合查询就不适合用Wrapper拼了,直接在Mapper接口里写SQL更清晰:
@Select("SELECT DATE(create_time) AS day, COUNT(*) AS cnt " + "FROM health_record " + "WHERE create_time >= #{start} AND create_time < #{end} " + "GROUP BY DATE(create_time) ORDER BY day") List<Map<String, Object>> countByDay(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end);用Map接收查询结果,后端不定义额外的VO类,前端拿到的就是[{day: '2025-01-01', cnt: 12}]这种结构,直接扔给图表组件渲染。数据量小的时候这种写法效率很高,代码也很简洁。
3.3 定时任务与数据归档的稳健做法
社区系统有一个隐蔽但很重要的需求:定期提醒。每天早上统计昨天未登记的居民,生成提醒消息;每10分钟扫描异常上报中超过2小时未处理的记录,提醒处理人。这些需求用Spring自带的@Scheduled就能搞定,不需要引入Quartz或者XXL-Job这种重量级框架。
定义一个统计未登记居民的定时任务,核心逻辑是:查询昨天有登记记录的居民ID集合,再查出所有状态为正常的居民,两者取差集,就是“昨天未登记”的名单,生成提醒记录。
写定时任务时很容易犯两个错误。第一个是任务方法里直接调用耗时的大接口,如果社区有上万居民,一个任务执行几分钟,整个应用线程都被占住。解决办法是分批处理,每次查500条处理完再查下一批。第二个是忘记处理异常,定时任务里抛异常会导致任务中断,而且Spring默认不会自动恢复。稳妥做法是任务方法内部用try-catch包住,记录日志,保证单个批次失败不影响后续批次。
数据归档方面,我的建议是:业务数据不要物理删除,用逻辑删除。比如出入记录超过半年后,可以定期把老数据标记为deleted=1,查询列表时默认过滤掉,但数据还在,后续排查还能翻出来。如果不做归档,一张表积累几年数据后,查询速度会明显下降,索引再优化也顶不住。
4. 前端页面功能拆解(Vue + Element UI)
4.1 路由规划与登录守卫
前端工程是标准的Vue 2 + Element UI + Vue Router + Vuex结构。页面清单如下:
| 路由路径 | 对应组件 | 页面功能 | 访问权限 |
|---|---|---|---|
| /login | Login.vue | 登录页 | 匿名 |
| /dashboard | Dashboard.vue | 仪表盘,统计数据可视化 | 登录用户 |
| /resident | ResidentList.vue | 居民档案管理 | 管理员 |
| /health | HealthRecord.vue | 健康登记记录 | 登录用户 |
| /access | AccessRecord.vue | 出入登记 | 管理员 |
| /report | ReportList.vue | 异常上报处理 | 管理员 |
| /user | UserList.vue | 用户管理 | 系统维护员 |
路由结构控制在三级以内,不搞多级嵌套。views目录下页面平铺,每个页面一个文件夹,放对应的列表组件和表单弹窗组件。这样目录清楚,改起来也好找。
路由守卫是整个前端的第一道安全关卡:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path !== '/login' && !token) { next('/login'); } else { next(); } });这个守卫的逻辑很直接:除了登录页之外,其他页面如果没有token就重定向到登录页。有读者可能会问,前端路由守卫防不住什么,后端接口还有JWT拦截器才是真正的安全边界。没错,前端路由守卫主要优化用户体验,真正的安全由后端保证。
4.2 axios封装与API模块划分
前端所有HTTP请求都封装在一个request.js文件里,统一配置axios实例。核心代码如下:
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) { Message.error(res.message || '请求失败'); return Promise.reject(new Error(res.message)); } return res.data; }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error(error.message || '网络异常'); return Promise.reject(error); } );两个细节值得展开。响应拦截器返回的是res.data而不是res,这是因为后端统一返回Result结构,里面真正有用的业务数据在data字段。页面里调用接口时直接拿到业务数据对象,少写一层解构代码。另外,401处理必须放在错误拦截器里,因为后端拦截器校验失败时返回的就是401状态码,这是前后端联动的关键一环。
API模块按照页面划分,每个页面对应一个api文件,比如resident.js里放getResidentPage、saveResident、deleteResident等接口函数。这样做的好处是,后端接口路径变了只需要改一个文件,页面组件里的代码不用动。
4.3 核心页面组件拆分与ECharts可视化
列表页是后台管理系统的“门面”,这套系统的列表页统一用顶部搜索区加表格区加分页器加操作列的布局。搜索区通过status、keyword等条件筛选,点击查询后重新请求第一页数据,点击重置后清空条件并刷新列表。表格里操作列一般是“编辑”“删除”两个按钮,删除用Popconfirm二次确认,防止误操作。
表格列定义中有个容易被忽略的点:日期格式化。后端返回的LocalDateTime默认序列化成ISO格式字符串,前端直接用的话没问题,但如果后端某个字段返回的是数组结构(Jackson默认序列化LocalDateTime的格式问题),前端就显示不了。这个坑在联调时很常见,后面会展开说。
给一个表格操作列的小示例:
<el-table-column label="操作" width="150"> <template slot-scope="scope"> <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button> <el-popconfirm title="确认删除该记录吗?" @confirm="handleDelete(scope.row.id)"> <el-button type="text" slot="reference" style="color: #f56c6c">删除</el-button> </el-popconfirm> </template> </el-table-column>仪表盘页面用的是ECharts,主要展示两类图表:按日期展示健康登记人次趋势的折线图,按状态展示居民分布的饼图。有一个经验值得记下:ECharts的图表容器必须设置固定高度,否则初始化时高度为0,图表渲染出来是空白。另外,数据请求要在组件mounted时发起,拿到数据后立即初始化图表,图表实例要合并到data中,组件销毁时记得调用chart.dispose()释放资源。
5. 部署运行与前后端联调
5.1 本地开发环境的启动顺序与代理配置
拿到源码后第一次启动,顺序非常重要。正确顺序是:先启动MySQL服务,确认数据库已导入;再启动SpringBoot后端,确认8081端口正常监听;最后再启动Vue开发服务器。如果顺序反了,前端先启动后请求后端一直失败,容易误判成自己的环境问题。
SpringBoot后端端口默认8081,避免和前端开发服务器的8080冲突。启动后先用浏览器访问一下后端Swagger文档或接口文档(如果有),确认后端没问题再动前端。
Vue开发环境解决跨域问题的标准方法是Vite代理,在vite.config.js中配置:
server: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true } } }这个配置的含义是:前端发出的所有以/api开头的请求,都转发到localhost:8081。这样浏览器里看到的请求地址始终是同源的,不存在跨域问题。很多新手在开发环境遇到跨域报错,核心原因就是代理配置没生效或者baseURL写错了,比如写成http://localhost:8081就绕过了代理直接请求后端,此时跨域问题才会出现。
5.2 生产环境打包:前端资源并入后端jar包
生产环境用两种方式都可以,我这套源码默认支持第一种方式。
第一种方式是“一体化打包”:前端执行npm run build生成dist目录,把dist目录下的内容全部复制到SpringBoot项目的src/main/resources/static目录下,然后执行mvn package打jar包。启动jar包后,访问端口就是后端端口,前端页面由SpringBoot的静态资源处理器提供,前后端完全同源,不存在跨域问题。
这种方式有个大坑:Vue源码中axios的baseURL必须用相对路径'/api',不能写死成http://localhost:8081。如果写死了,打包后浏览器访问页面时请求的是用户的localhost,自然是404或者跨域。
第二种方式是“前后端分离部署”:前端dist部署到Nginx,后端jar包部署到服务器,通过Nginx反向代理转发/api请求到后端端口。这种方式的优点是可以分别扩展和维护,缺点是部署复杂度稍高。
对比两种方式:
| 部署方式 | 优点 | 缺点 |
|---|---|---|
| 一体化打包 | 部署简单,只跑一个jar包 | 前端更新需要重新打包jar包 |
| Nginx分离部署 | 前后端独立更新 | 需要配置Nginx,多一套运维工作 |
对中小社区场景,我推荐一体化打包。原因很简单:用这套系统的社区大概率没有专职运维工程师,能跑一个jar包绝不跑两个服务。
5.3 联调实战:处理日期格式、token过期和资源路径
前后端联调阶段,最容易出问题的几个点基本是固定的。
第一个是日期格式。如果后端实体类直接用LocalDateTime字段,Jackson默认序列化的结果是一些数字数组,前端拿到后显示一串看不懂的数字。正确做法是统一配置Jackson的日期格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8或者在实体类的日期字段上直接加@JsonFormat注解,效果相同。不要等到前端排查问题时才意识到是后端序列化问题,提前在全局配置里解决。
第二个是token过期后的提示。前端axios拦截器已经处理了401跳转登录页的逻辑,但是有一种情况要注意:如果多个请求同时在401后触发跳转,会重复跳转多次。稳妥做法是加一个isRedirecting标志,避免重复跳转带来的路由警告。
第三个是打包后首页白屏。如果一体化打包后打开页面只有title没有内容,八成是publicPath配置问题。Vite中配置base: './',Vue Router用history模式时还要加上fallback配置。历史模式有个特点,直接刷新非根路径会404,需要在后端加一个默认转发规则,把所有非静态资源路径转发到index.html。
6. 常见问题与排查技巧实录
6.1 数据库与连接类问题速查
这套系统运行中遇到的数据库问题,大部分集中在下面几个场景,整理成表格方便对照排查:
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| 启动后端报Communications link failure | MySQL服务未启动或端口不对 | 确认MySQL已启动,检查jdbc url端口号 |
| 连接报Public Key Retrieval is not allowed | MySQL 8的认证插件限制 | jdbc url加allowPublicKeyRetrieval=true参数 |
| 乱码问题 | 数据库字符集不正确 | 建库和表时统一utf8mb4 |
| 中文内容报Incorrect string value | 字段字符集为latin1 | ALTER TABLE修改字段字符集为utf8mb4 |
| 时区报错 | 未设置serverTimezone | jdbc url加serverTimezone=Asia/Shanghai |
MySQL 8和5.7的认证方式不同,如果在8.0环境出现认证相关错误,优先检查连接参数中是否配置了allowPublicKeyRetrieval=true和useSSL=false。第一次连接时会要求获取公钥,默认不允许就抛异常。
另一个不太常见但很影响使用的问题是max_allowed_packet太小。如果初始化SQL脚本比较大,导入时会提示max_allowed_packet相关错误。解决方案是临时修改会话级别的参数,或者用source命令分步导入。
6.2 前端启动、打包与资源路径问题
前端开发阶段问题主要集中在几个环节:Node版本不兼容、依赖安装失败、代理不生效、打包后白屏。
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
| npm install报错ERESOLVE | Node版本过高导致依赖树解析冲突 | 使用Node 14或16版本,或加--legacy-peer-deps |
| 开发环境请求接口404 | 代理配置未生效 | 检查vite.config.js的proxy配置,确认baseURL以/api开头 |
| 打包后白屏 | publicPath绝对路径问题 | Vite配置base: './' |
| 刷新页面404 | 路由history模式未配置后端转发 | 后端添加SPA路由转发规则 |
| 图片或静态资源加载不全 | 静态资源路径错误 | 资源引用使用相对路径,不要写以/开头的绝对路径 |
说一个容易忽略的细节:开发环境一切正常,打包部署后接口能通但页面上的图标不显示,最常见的原因是Element UI的字体文件在打包时路径处理不当。在Vite配置中把静态资源base设置好,这个问题基本不会出现。
6.3 后端运行与Maven构建常见坑
后端启动失败和Maven构建失败,新手遇到时容易慌,其实绝大多数都是版本或依赖问题。
第一个坑是Maven仓库下载慢或下载失败。解决方案是配置阿里云镜像,在settings.xml中设置mirror。第一次拉依赖时耐心等几分钟,不要反复用Ctrl+C中断。
第二个坑是JDK版本不匹配。SpringBoot 2.x兼容JDK 8和11、17,但有些旧版依赖在用JDK 17时会有module访问错误。建议统一使用JDK 8,这是目前中小社区服务器上最常见也最稳定的版本。
第三个坑是MyBatis-Plus分页不生效。如果用了PaginationInnerInterceptor但忘记在MybatisPlusConfig中注册,分页查询会返回全部数据。检查配置类中是否配置了:
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }第四个坑是端口被占用。SpringBoot默认8080端口常被其他开发程序占用,启动报Address already in use。先用命令查一下占用进程再决定改端口还是杀进程,不要盲目重启。
6.4 几个我在实际整理源码时特别想提醒的经验
这些经验不在教科书里,但每一个都可能帮你省下两小时排错时间。
第一,拿到源码后不要先急着改代码,先把数据库导进去、把后端跑起来、把前端跑起来,确认“原样能跑”再开始修改。一个常见的悲剧是:读完源码觉得某个设计不合理,直接大刀阔斧改,结果数据库表结构对不上,代码里有一堆编译错误,根本分不清是自己改坏的还是源码本来就有问题。
第二,排查接口问题时要建立“从前端请求看起”的习惯。浏览器F12打开Network面板,先看请求发出没有、URL对不对、响应状态码是什么。很多问题其实一眼就能看出来:URL拼错了、token没带上、参数格式不对。不要一上来就盯着后端日志看半天。
第三,这套系统的初始化SQL脚本是“开箱即用”的关键,但其中的示例数据千万不要直接用到正式环境。示例身份证号和手机号是编造的,正式使用前必须清空重新导入真实数据。这一步没有做好,上线第一天就会出严重的数据混乱。
第四,所有配置文件中的密码、密钥都不要使用默认值。尤其要注意JWT加密密钥、MySQL密码、管理员初始密码,上线前全部换掉。
我个人在实际整理这套源码时的最大体会是:中小系统最忌讳过度设计。功能可以以后再加,但业务模型如果设计错了,后面返工成本远高于一开始多花点时间想清楚。社区疫情信息管理系统的核心是台账稳定、数据可靠、操作简单,这三条做到了,系统就算合格。拿到源码后建议沿用原有表结构和业务逻辑跑通一遍,再尝试修改一两个自己感兴趣的点,比如增加一个导出Excel功能、调整仪表盘展示维度,这比从头写一遍更有学习价值。