这套基于 Spring Boot 和 Vue 的社区健康服务平台,是我花了两周时间从零到一完整落地的一个全栈项目。它不像网上那些只贴半套代码、跑不通的“半吊子工程”,而是真正做到数据表齐全、前后端联调畅通、可以打包部署上线的完整解决方案。如果你正在准备毕业设计、课程设计,或者想通过一个实际项目摸透 Spring Boot + Vue 的开发流程,这篇文章值得你从头看到尾。
整个项目面向的是社区健康服务场景:社区居民可以维护自己的健康档案、在线预约健康服务、查看科普资讯;社区管理员则可以管理辖区居民信息、审核预约、发布通知公告。系统分三种角色——普通用户、社区管理员、系统管理员,权限边界清晰,功能覆盖完整。为了让不同基础的读者都能跟着跑起来,我会从项目设计思路开始讲,拆清楚数据库表为什么这么建、每个核心功能是怎么实现的,最后给出一份可以直接照着敲的部署教程。
先说一点实在的:这种“单体应用”放在生产环境中不算复杂,但作为学习项目,它能把你从前端页面、后端接口、权限控制、数据库设计到服务器部署的整条链路串一遍。下面我直接进入正题,先把项目最关键的架构思路讲透。
1. 项目到底做什么:一个社区健康服务平台的完整画像
1.1 为什么是 Spring Boot + Vue 这个组合
在我做过的几个全栈项目里,Spring Boot + Vue 是目前中小型管理系统里最稳妥、最不容易翻车的搭配,没有之一。
后端用 Spring Boot,最大的收益是“约定大于配置”。你不需要像老 Spring 那样搞一大堆 XML 配置,只要依赖引入对了,启动类一写,内嵌 Tomcat 直接跑起来。配合 MyBatis-Plus,连基础的 CRUD 都能省掉大部分手写 SQL,这在开发进度紧张的时候太重要了。对于需要快速出活儿的学生项目或小型企业系统,这套后端组合的实际体感就是“写得快、改得爽、上线不慌”。
前端选 Vue,核心原因是组件化开发带来的效率提升。社区健康服务平台虽然功能不算重,但页面数量并不少:登录注册页、首页、健康档案表单页、预约管理页、后台管理界面……如果继续用传统的页面拼接方式,维护成本很快会失控。Vue 的双向数据绑定和组件复用规则,让同一个表格组件、表单组件可以多处调用。配合 Element UI,界面能很快达到“看起来像个产品”的水准,而不是“一眼学生管理系统”。
提示:如果你所在的小组或毕设课题硬性要求用 JSP、Thymeleaf 之类模板引擎,也不是不能做,但项目体积和前后端协作的体验会明显差一截。Spring Boot + Vue 的价值在于前后端彻底分离,接口文档一签,两个人可以并行开发,这在团队协作里非常实用。
1.2 角色与核心功能模块拆解
搞清楚用户是谁,才能决定功能怎么设计。我没有一上来就写代码,而是先花半天把角色和模块梳理清楚。
普通用户(社区居民):登录注册、个人信息维护、健康档案的新增/修改/查看、健康服务预约(比如测血压、测血糖、健康咨询)、查看健康资讯、提交留言反馈。
社区管理员:居民信息管理、健康档案管理、预约审核与安排、健康资讯发布、公告通知管理、留言反馈处理。
系统管理员:负责账号数据维护、审核管理员操作、后台基础配置等基础管理工作。
从模块边界看,核心模块可以划分为:认证与用户管理、健康档案管理、预约管理、资讯与公告管理、留言反馈管理。每个模块背后对应着一张或几张核心数据表,表结构我在第 3 节详细展开。
这里我想特别强调一个设计原则:角色的权限控制不做在前端页面里,而是做到后端接口层。比如普通用户发起“新增健康档案”请求时,后端不仅要校验是否登录,还要校验当前用户的角色和这条档案是否属于该用户。前端隐藏按钮只是体验优化,真正的安全边界必须由后端守住。这一点在很多初学者的项目里是最容易被忽略的,但恰恰是答辩和评审时最容易加分的点。
2. 技术选型与项目初始化:每一项选择的理由
2.1 后端技术栈清单
后端我选用的核心组件如下:
| 组件 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 2.7.x | 应用开发框架 |
| MyBatis-Plus | 3.5.x | 持久层 ORM,简化 CRUD |
| MySQL | 8.0.x | 主数据库 |
| Druid | 1.2.x | 数据库连接池与监控 |
| JWT / jjwt | 0.9.1 | 登录令牌签发与校验 |
| Lombok | 1.18.x | 简化实体类代码 |
Spring Boot 2.7.x 兼容性比较稳妥,大部分第三方组件都有匹配的版本,不会出现依赖冲突。MyBatis-Plus 的好处是内置通用 Mapper,像selectById、selectPage、insert这些方法不用再额外写 XML。对于健康档案这种字段多、重复性高的模块,用LambdaQueryWrapper构造条件查询会非常顺手。
JWT 做得是无状态认证。用户登录成功后签发一个带过期时间的 token,前端每次请求在请求头带上这个 token,后端拦截器负责统一校验。相比传统的 Session,JWT 在后端集群部署时无需额外配置会话共享,这也是社区健康服务平台后续如果做横向扩容时更方便的原因。
2.2 前端技术栈清单
前端部分:
| 组件 | 版本 | 用途 |
|---|---|---|
| Vue | 2.6.x | 前端框架 |
| Vue Router | 3.x | 前端路由 |
| Vuex | 3.x | 状态管理 |
| Axios | 1.x | HTTP 请求库 |
| Element UI | 2.15.x | UI 组件库 |
| ECharts | 5.x | 数据可视化图表 |
前端技术选型这里必须说明:我没选 Vue 3 和 Element Plus,有人可能会问,现在 Vue 3 都成熟了为什么不用?我当时的判断依据是资料的丰富程度以及生态的稳定度。Vue 2 + Element UI 的组合在网上能找到大量踩坑记录,对于这个项目体量完全够用。Vue 3 的响应式原理更优,但如果你不是特别熟悉组合式 API,Vue 2 的选项式写法门槛更低,做毕设和课设时踩的坑更少,交付节奏更好控制。
Axios 统一封装在request.js里,设置请求拦截器和响应拦截器。请求拦截器自动从localStorage里读 token 并加到Authorization头;响应拦截器统一处理后端返回的状态码,遇到 401 就清空登录状态并跳转登录页。
2.3 项目目录结构规划
如果你的项目是一片混乱的“随手工程”,后面部署和答辩都会很痛苦。我推荐的目录结构是这样的,前后端完全分离:
community-health-service/ # 后端工程 │ ├─ src/main/java/com/example/health/ │ │ ├─ config/ # 配置类、拦截器注册 │ │ ├─ controller/ # 接口层 │ │ ├─ service/ # 业务逻辑层 │ │ ├─ mapper/ # MyBatis-Plus Mapper接口 │ │ ├─ entity/ # 实体类 │ │ ├─ common/ # 统一返回结果、异常处理 │ │ └─ HealthApplication.java │ └─ src/main/resources/ │ ├─ application.yml # 配置文件 │ └─ mapper/ # 自定义SQL映射(如有) health-web/ # 前端工程 │ ├─ src/ │ │ ├─ api/ # 接口封装目录 │ │ ├─ assets/ │ │ ├─ components/ # 公共组件 │ │ ├─ router/ # 路由配置 │ │ ├─ store/ # Vuex │ │ ├─ views/ # 页面组件(按模块分文件夹) │ │ ├─ utils/request.js # Axios封装 │ │ └─ App.vue │ └─ package.json这种分层的好处是“各司其职”:你改数据库字段时,只需要动 entity、mapper、service 三层,controller 对外暴露的接口路径基本不变,前端页面不用跟着大改。实战中这是我踩过几次坑之后总结出来的最佳实践,尤其适合答辩前临时加功能的情况。
3. 数据库设计:一张表都不能少的完整方案
3.1 数据库总体设计思路
健康服务平台的业务核心其实是“人”,所以数据库设计的初级阶段,我围绕“用户”这个中心实体展开:谁在使用系统、这些人的健康信息如何记录、他们产生了什么行为(预约、反馈),以及平台运营需要什么内容(资讯、公告)。
按这个思路,我拆出了以下核心表:
- 用户表(user)
- 健康档案表(health_record)
- 预约记录表(appointment)
- 健康资讯表(health_article)
- 公告通知表(notice)
- 留言反馈表(feedback)
- 管理员表(admin)
这里有一个设计细节容易遗漏:用户表和管理员表我做了分离,而不是共用一张用户表。原因是系统管理员和社区居民的属性差异很大,管理员更多是工号、权限级别这类属性,社区居民则是身份证号、手机号、住址信息,混在一张表里会产生大量冗余字段。分开之后表结构干净,权限控制也少一层脏逻辑。
3.2 核心表结构详解与建表 SQL
下面直接给核心建表 SQL,这是可运行的一套,你跟着执行就能把数据库骨架搭出来。
用户表(user)
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'MD5加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `gender` tinyint DEFAULT NULL COMMENT '性别 0-女 1-男', `age` int DEFAULT NULL, `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `id_card` varchar(20) DEFAULT NULL COMMENT '身份证号', `address` varchar(200) DEFAULT NULL COMMENT '家庭地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='社区居民用户表';密码我统一用 MD5 加密存储。注意,MD5 本身不算安全方案,更适合带盐的 BCrypt,但这个项目里为了简化部署和避免初学者混淆,我用的是 MD5,如果你要上线真实系统,强烈建议换掉。这里不做文章安全性引导,仅技术讨论。
健康档案表(health_record)
CREATE TABLE `health_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '关联用户ID', `blood_type` varchar(10) DEFAULT NULL COMMENT '血型', `height` decimal(5,2) DEFAULT NULL COMMENT '身高cm', `weight` decimal(5,2) DEFAULT NULL COMMENT '体重kg', `blood_pressure` varchar(20) DEFAULT NULL COMMENT '血压值,如 120/80', `heart_rate` int DEFAULT NULL COMMENT '心率', `allergy_history` varchar(500) DEFAULT NULL COMMENT '过敏史', `chronic_disease` varchar(500) DEFAULT NULL COMMENT '慢性病史', `medical_history` varchar(1000) DEFAULT NULL COMMENT '既往病史', `family_history` varchar(1000) DEFAULT NULL COMMENT '家族病史', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康档案表';这张表是典型的“纵向长表”设计,每条档案记录都附属于某个用户。字段类型上要注意:身高体重用 decimal 保证精度,过敏史、病史这类内容较长的字段用 varchar(500)或 varchar(1000),如果实际内容可能更长,应该换成 text 类型。建表时把idx_user_id索引加上很关键,用户查看自己的档案时所有查询都走这个索引,数据量上来之后不会慢。
预约记录表(appointment)
CREATE TABLE `appointment` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '预约人ID', `service_type` varchar(50) NOT NULL COMMENT '服务类型', `appointment_date` date NOT NULL COMMENT '预约日期', `appointment_time` varchar(20) DEFAULT NULL COMMENT '预约时段,如上午/下午', `status` tinyint DEFAULT '0' COMMENT '状态 0-待审核 1-已确认 2-已完成 3-已取消', `remark` varchar(500) DEFAULT NULL COMMENT '预约备注', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务预约表';预约状态用 tinyint 数字存储,而不是直接用字符串。这样在代码里枚举管理更清晰,数据库查询性能也更好。很多人喜欢直接存“待审核”“已确认”这种汉字,图一时方便,但后续如果要改状态名称,或者做统计报表,会非常难受。
健康资讯表(health_article)
CREATE TABLE `health_article` ( `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `cover` varchar(300) DEFAULT NULL COMMENT '封面图URL', `summary` varchar(500) DEFAULT NULL COMMENT '摘要', `content` text NOT NULL COMMENT '正文', `views` int DEFAULT '0' COMMENT '浏览量', `create_by` bigint DEFAULT NULL COMMENT '发布管理员ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='健康资讯表';公告通知表(notice)和留言反馈表(feedback)结构相对简单,公告表类似title、content、create_time;反馈表是user_id、content、reply、status、create_time,这里留给读者自己练手,设计逻辑和思路与上面几张表一致。
3.3 表关系与设计心得
表与表的关系并不复杂:user表对health_record表是一对多关系,一个用户能有多条档案修改记录(也可以设计成只保留最新一条,我保留历史记录以便追踪变化);user表对appointment表是一对多;admin表对article、notice是一对多。
我在设计过程中坚持了两个原则:
第一,所有表都带create_time和update_time字段,MyBatis-Plus 的字段自动填充功能可以配合使用,后端新增数据留下时间痕迹。后期如果要做“近一周预约数量统计”之类的功能,这个时间字段直接就能用。
第二,外键我尽量不用数据库物理外键,而是在代码层面维护逻辑关系。原因很简单,物理外键在删除数据时会有约束麻烦,而且分库分表场景下外键基本派不上用场。用逻辑外键,配合索引,查询性能不差,维护也灵活。
注意:数据库字符集统一用 utf8mb4,不要用 utf8。utf8mb4 是 utf8 的超集,能正常存储表情符号和生僻字,而且与 MySQL 8.0 的默认配置更匹配。当初我第一次做项目就吃了这个亏,插入一个带符号的标题直接报错,查了半天才定位到字符集问题。
4. 核心功能实现与源码解析
4.1 登录鉴权:JWT + 拦截器
后端登录流程并不复杂,但代码的组织方式值得考究。登录接口接收用户名和密码,先调用AdminService或UserService查库比对,密码一致后签发 JWT token 返回前端。
生成 token 的核心代码如下:
public String generateToken(String username) { Date now = new Date(); Date expireDate = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 过期时间24小时 return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }拦截器是整个鉴权的核心。在 Spring Boot 里只需要实现HandlerInterceptor,在preHandle方法里从请求头获取 token,校验合法性并把用户信息放入request属性中,后续 controller 可以通过参数直接取到当前操作人。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || "".equals(token)) { return returnUnauthorized(response); } try { Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.replace("Bearer ", "")) .getBody(); request.setAttribute("username", claims.getSubject()); return true; } catch (Exception e) { return returnUnauthorized(response); } }这里要注意一个非常容易踩的坑:前端请求时Authorization头的标准写法是Bearer token格式,如果后端解析时没有做replace("Bearer ", "")处理,前端配什么格式后端都要严格对应。我选择统一在前端 axios 拦截器里拼上Bearer前缀,后端解析时剥掉,两边约定清楚,就不会出乱子。
4.2 健康档案模块的实现
健康档案模块的特点是“接口多、字段多、权限要求细”。我把它设计成以下接口清单:
- 查询当前用户档案:
GET /api/healthRecord/my - 新增档案:
POST /api/healthRecord - 修改档案:
PUT /api/healthRecord/{id} - 删除档案:
DELETE /api/healthRecord/{id} - 管理员查询所有档案(分页):
GET /api/healthRecord/page
Service 层写法的关键点是权限校验。用户在新增档案时,后端会把当前登录用户的 ID 强制写入healthRecord实体,而不是信任前端传来的userId字段。因为前端页面完全可以被绕过,接口被直接调用时,如果后端不校验,就存在越权访问风险。
对于管理员的分页查询,用 MyBatis-Plus 的Page对象配合LambdaQueryWrapper,代码非常简洁:
public Page<HealthRecord> getPage(int pageNum, int pageSize, Long userId) { Page<HealthRecord> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<HealthRecord> wrapper = new LambdaQueryWrapper<>(); if (userId != null) { wrapper.eq(HealthRecord::getUserId, userId); } wrapper.orderByDesc(HealthRecord::getCreateTime); return healthRecordMapper.selectPage(page, wrapper); }4.3 预约管理模块的实现
预约管理比档案管理多了一层“状态流转”:用户提出预约(待审核),管理员确认或取消(已确认/已取消),服务完成后标记(已完成)。状态机很简单,但要注意在接口层做好状态合法性校验。
比如用户一旦提交预约,不能自己直接取消已确认的预约;如果状态是“已完成”,任何一方都不能再修改。这些规则我用一个AppointmentStatusEnum枚举管理,Service 里写好校验逻辑,而不是散落各处的 if-else。
前端预约页面里,用户需要选择服务类型、日期和时段。为了避免用户重复预约同一天同一个服务,后端要加唯一性校验:
long count = appointmentMapper.selectCount( new LambdaQueryWrapper<Appointment>() .eq(Appointment::getUserId, appointment.getUserId()) .eq(Appointment::getAppointmentDate, appointment.getAppointmentDate()) .eq(Appointment::getServiceType, appointment.getServiceType()) .ne(Appointment::getStatus, 3) ); if (count > 0) { throw new BusinessException("您已预约该日期的该服务,请勿重复提交"); }4.4 前端页面与接口对接
前端页面按角色分成两个大区域:访客/居民端和后台管理端。居民端包括首页展示、健康档案表单、服务预约、资讯列表、个人中心;管理端包括档案管理、预约审核、资讯发布、留言管理等。
路由配置里重点要加“路由守卫”。Vue Router 的beforeEach钩子里读取 Vuex 中保存的 token 和角色信息,没有 token 一律跳转登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.meta.requiresAuth && !token) { next('/login'); return; } if (to.meta.role && to.meta.role !== localStorage.getItem('role')) { next('/'); return; } next(); });这里说的“角色控制”只是前端体验优化,真正的权限在后端接口层,这个我在前面已经强调过。
Axios 封装部分,我会在request.js里统一做响应拦截和错误提示,避免每个页面重复写Message.error:
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; }, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem('token'); router.push('/login'); } Message.error(error.message || '网络异常'); return Promise.reject(error); } );页面组件方面,健康档案表单是字段最多的一个页面,我直接用 Element UI 的el-form配合rules做表单校验。要注意的是日期控件回显格式必须和后端实体字段类型匹配,如果后端用date类型,前端传"2025-01-01"不会报错,但传带T的 ISO 时间格式就容易触发序列化异常。
5. 从本地到服务器:完整部署教程
5.1 本地环境准备与运行
先把基础环境装齐:JDK 1.8、Maven 3.6+、Node.js 14+、MySQL 8.0。前端依赖安装命令:
cd health-web npm install npm run serve本地开发模式下,后端application.yml里需要配置好数据库连接。如果你的 MySQL 用户密码或者端口变了,改这里即可:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/community_health?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: root password: yourpassword jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8提示:url 参数中
serverTimezone和useSSL这两个参数务必加上。缺少serverTimezone会报时区错误;useSSL=false避免本地连不上时报 SSL 握手警告。
后端启动命令:
cd community-health-service mvn spring-boot:run只要启动日志里看到Tomcat started on port(s): 8080,后端就跑起来了。此时可以先用接口调试工具访问http://localhost:8080/api/user/login验证接口连通性。
前后端联调时,我建议先配好后端的跨域支持。在config包里加入跨域配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }5.2 后端打包与配置文件修改
后端打成可执行 jar 包非常简单:
mvn clean package -DskipTests打包成功后,jar 包位于target/community-health-service-0.0.1-SNAPSHOT.jar。
部署到服务器前,要留意一点:本地配置的是localhost:3306,服务器上的数据库地址通常是内网地址或独立数据库地址,不能照搬本地配置。Spring Boot 支持在启动时用--spring.datasource.url参数覆盖 yml 里的配置,推荐做法是不修改 jar 包,直接命令行指定:
java -jar community-health-service-0.0.1-SNAPSHOT.jar \ --spring.datasource.url="jdbc:mysql://你的数据库地址:3306/community_health?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false" \ --spring.datasource.username=root \ --spring.datasource.password=你的密码这样做的好处是:jar 包可以随时重建发布,数据库配置等环境相关信息都在部署环节动态注入,配置和代码解耦。
后期为了稳定我还建议用nohup让进程在后台跑:
nohup java -jar community-health-service-0.0.1-SNAPSHOT.jar > app.log 2>&1 &查看日志用tail -f app.log,进程异常时日志里的堆栈信息是排查问题的第一手依据。
5.3 前端构建与 Nginx 配置
前端构建之前,要把接口地址改成线上后端地址。我在前端项目里做一个环境判断,生产环境统一走/prod-api前缀,由 Nginx 反向代理到后端。
// .env.production VUE_APP_BASE_API = '/prod-api'构建命令:
npm run build构建完成后,dist目录就是纯静态文件。我把它上传到服务器/usr/share/nginx/html/community-health目录下,然后在 Nginx 配置里做代理和路由:
server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/community-health; index index.html; location / { try_files $uri $uri/ /index.html; } location /prod-api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个关键点。第一,try_files $uri $uri/ /index.html;是为了解决 Vue Router 的 history 模式刷新后 404 的问题;第二,proxy_pass http://127.0.0.1:8080/;结尾带/,访问/prod-api/user/login时会被转发到http://127.0.0.1:8080/user/login,等于把/prod-api前缀剥掉了,正好匹配后端的/api前缀。
如果你的后端接口统一有/api前缀,那前端请求路径就是/prod-api/api/user/login,转发后变成/api/user/login,这样后端 controller 里的@RequestMapping("/api/user")路径就能对上了。这个前缀映射关系我建议提前写下来,前后端对接时反复核对,否则会出现“前端一堆 404 但搞不清是后端没启动还是路径不对”的情况。
改完 Nginx 配置后执行:
nginx -t nginx -s reloadnginx -t先检查语法,通过后再 reload,别直接跳过检测。
5.4 服务器环境搭建与上线
这一步只讲流程思路,具体操作命令都很常规:准备一台 Linux 服务器,安装 JDK、MySQL、Nginx,建库建表导入数据,上传 jar 包和前端静态文件,按前面的步骤配置并启动。
建库建表时有个省事技巧:本地 MySQL 把数据库结构导成 SQL 文件,服务器上直接执行:
mysql -u root -p -e "create database community_health default character set utf8mb4 collate utf8mb4_unicode_ci;" mysql -u root -p community_health < init.sql首次启动后端后,建议立刻通过curl或浏览器访问带鉴权的接口验证 token 流程是否正常,再进前端页面直观检查。这里我不推荐使用任何第三方图形化工具,因为很多服务器的安全策略不允许随意开放 3306 端口,更稳妥的方式是在服务器本地用命令行操作。
上线后,我有两个习惯:
- 查看后端日志是否出现连接池报错。如果数据库连接数配置得太小,并发上来后会报
Cannot get a connection, pool exhausted。解决方案是在 yml 里调大 Druid 连接池的max-active参数,比如max-active: 20。 - 检查前端浏览器控制台是否有跨域请求被拦截的报错。如果你能正常打开登录页但登录接口一直失败,多半是 Nginx 的代理配置或跨域配置没有生效。
6. 常见问题与排查速查表
下面这些坑不是凭空想的,是我前后端联调和部署时真实遇到过的,每一个都花了不少时间定位。
6.1 启动类异常
Q:后端启动报Consider defining a bean of type 'com.example.health.mapper.UserMapper'
一般是 Mapper 接口没有加@Mapper注解,或者主启动类上没有加@MapperScan("com.example.health.mapper")。我建议直接在主启动类加@MapperScan,一劳永逸,每个 Mapper 上就不用重复加注解了。
Q:启动时报Access denied for user 'root'@'localhost'
数据库账号密码错误或权限不足。先确认 MySQL 里用户权限,必要时执行:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' IDENTIFIED BY '密码'; FLUSH PRIVILEGES;Q:JDK 版本冲突导致启动失败
Spring Boot 2.7.x 用 JDK 8 或 11 都行,但如果你服务器装了比较新的 JDK,编译版本不匹配可能报UnsupportedClassVersionError。打包时我在pom.xml里显式指定:
<properties> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>6.2 部署类问题
Q:Nginx 配置后访问/prod-api/user/login报 404
最可能的原因是proxy_pass的路径拼接与你预期不一致。你可以在服务器上用一个临时接口测试,或者查看 Nginx 的 error.log:
tail -f /var/log/nginx/error.log如果日志显示转发路径多了一段/prod-api,说明proxy_pass结尾没有加/或者加了引号。Nginx 路径拼接规则很细,建议先在location /prod-api/里加一行:
proxy_pass http://127.0.0.1:8080/;如果还不够清楚,可以临时在 Nginx 配置里加:
rewrite ^/prod-api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080;这两种写法都能剥掉前缀,根据系统版本和个人习惯选一种即可。
Q:前端刷新某个深层路由变成 404
这是最常见的问题。根因是 Nginx 找不到对应的物理路径,而 Vue Router 的 history 模式需要把所有路径回退到index.html。解决办法就是前面写的:
location / { try_files $uri $uri/ /index.html; }如果你用的 hash 模式,不会出现这个问题,但 URL 里会有#。我一般建议线上用 history 模式加上述配置,视觉更干净。
Q:ECharts 图表在部署后显示空白
检查服务器上资源路径是否带publicPath。由于构建后所有 JS/CSS 默认是相对路径引入,如果前端静态文件没有放在 Nginx 的 root 目录根下,而是子目录里,就需要调整publicPath。Element UI 组件或者 ECharts 初始化失败的空白页面,多数是异步请求时报错被吞了,打开浏览器控制台看网络请求和 JS 报错,比盲猜定位快得多。
Q:后端接口能通,但前端登录时提示“请求失败”
先看后端日志有没有报错。如果日志显示CORS相关的信息,说明是跨域问题。确认后端跨域配置是否允许了当前来源,确认是否在拦截器之前生效。Spring Boot 中跨域配置类如果没生效,可以试试在启动类里加@CrossOrigin,或者把 CorsConfig 独立成配置类而不是被拦截器覆盖。
我个人的排查经验是遵循一条线:先用浏览器开发者工具看请求发出去没有、请求头对不对、服务器返回什么状态码。90% 的问题都能在这一步定位到方向,比直接看代码节省太多时间。
最后分享一点我的实际体会
项目做完后,我最大的感受是:代码本身并不难,真正花时间的是环境、细节和踩坑。比如前后端路径的约定、状态字段的校验规则、Nginx 的转发配置,这些碎片化的细节单独看都算不上什么大问题,但凑在一起如果没有任何经验参考,足以消耗掉一天时间。
如果你是拿这个项目做毕业设计或答辩展示,我建议在完成部署后,自己再从零删库、打包、部署一遍。这个流程走两遍,你对整个系统的掌控感会完全不同。答辩时老师大概率会问“为什么这么设计”“如果并发量大怎么优化”“数据表之间关系怎么理解”,而这些你在亲自动手部署和排查过之后,心里自然有答案。
最后再分享一个小技巧:上线后直接拿全集代码配合这篇部署教程操作。第一次走通本地联调,第二次直接上服务器,第三次如果还有同学需要参考,直接照着步骤跑就行。项目这种东西,跑起来只是第一步,能讲清楚“每一步为什么这么干”,才是真正学到东西的标志。