1. 项目概览:这套系统到底能做什么
先说结论:这是一套典型的前后端分离信息管理系统,技术栈是 SpringBoot + Vue + MyBatis + MySQL,核心功能围绕“人员隔离管理”场景展开,包含登录鉴权、角色权限、隔离人员信息登记、每日健康状态上报、隔离周期管理、到期提醒、数据统计看板等完整闭环。
很多人看到“疫情隔离管理系统”这个名字,第一反应是“这东西现在还用得着吗”。我的看法是:把它当成一个业务背景完整的管理系统案例来学习,价值完全不在具体业务本身。它覆盖了绝大多数据管理类项目都会遇到的问题——多角色权限、一对多数据建模、动态条件查询、前后端接口联调、生产环境部署。这套骨架学会了,换成“学生宿舍管理系统”“志愿者活动管理系统”“客户信息登记系统”,改改字段和页面就能落地。
对三类人尤其适合:
- 正在准备毕设/课程设计的学生:功能完整、技术主流、有部署教程,答辩时能讲清楚设计思路和关键实现。
- 想系统入门前后端分离的开发者:这个项目不是那种只有登录注册的玩具demo,而是真实多表关联、带权限控制的完整小系统,踩坑点和生产场景高度一致。
- 想做通用信息管理底座的个人开发者:把业务模块抽掉,剩下的权限、统一返回、异常处理、部署方案可以直接复用到下一个项目。
我基于这套架构完整走了一遍开发、联调、打包、部署的全流程,下面把经验和代码全部拆开讲。内容不吹不黑,只讲实际踩过的坑和验证过的方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型的取舍与整体架构思路
2.1 为什么是这套组合,而不是别的
现在做前后端分离,方案很多:前端可以是 React、Vue,甚至 Svelte;后端可以是 SpringBoot、SpringCloud、Node.js、Go。这个项目选择 SpringBoot + Vue + MyBatis + MySQL,不是偶然,而是信息管理类系统里性价比最高的组合。
先看后端。SpringBoot 相比传统 SSM(Spring + SpringMVC + MyBatis)最大的优势是约定优于配置。内嵌 Tomcat,不用额外部署 war 包到外部容器;起步依赖一套,Maven 自动把版本管理好;application.yml 一个文件搞定数据源、端口、日志等配置。对比一下:SSM 时代搭一个能跑的环境要配 web.xml、spring-mvc.xml、mybatis-config.xml、数据源 bean,一上午没了;SpringBoot 起步 spring-boot-starter-web + mybatis-spring-boot-starter,十分钟就能启动一个带数据库连接的项目。
再看前端。Vue 相比 React 学习曲线更平缓,模板语法直观,组件化开发模式对中小型系统非常友好。配合 Element UI 这种组件库,表格、表单、弹窗、分页这些管理系统高频组件开箱即用,不需要自己从零封装 UI。React 当然也强,但在这个体量的项目中,Vue 的开发效率明显更高。
MyBatis 和 MySQL 就更不用纠结了。MyBatis 的定位是“半自动 ORM”,SQL 掌握在开发者手里,复杂多表关联、动态条件查询都能精确控制,比起完全自动化的 JPA/Hibernate,遇到性能问题时排查思路更清晰。MySQL 则是开源关系型数据库里最普及的选择,部署简单、文档全、社区问题答案多。
一句话总结这套组合的定位:SpringBoot 省配置,Vue 省页面开发时间,MyBatis 省 SQL 失控的担忧,MySQL 省部署成本——四个省叠加在一起,就是信息管理系统最快落地路径。
2.2 前后端分离架构的运行链路
很多初学者对“前后端分离”的理解停留在“前端用 Vue,后端用 SpringBoot”,但真正面试或者自己动手做的时候,需要把请求链路讲清楚。
整条链路是这样的:
- 浏览器访问前端站点(开发环境是
http://localhost:8080,生产环境是 Nginx 托管的静态资源) - 前端 Vue Router 根据 URL 加载对应页面组件
- 页面中的 JS 通过 Axios 发起 HTTP 请求,比如
GET /api/person/list?page=1&size=10 - 请求到达后端 SpringBoot 的 Controller,经 Service 层处理业务逻辑,Mapper 层读写数据库
- 后端返回统一格式的 JSON 数据,前端拿到后渲染到页面上
关键点在于:前端和后端是两个独立应用,只通过 HTTP 接口通信。前端不直接连数据库,后端不生成 HTML 页面。这意味着前端开发可以用 mock 数据,后端开发可以用 Postman 调试接口,两边并行推进,只要接口约定一致,最后联调成本很低。
这里有一个容易踩坑的细节:因为在开发环境中,前端和后端跑在不同端口(Vue 默认 8080,SpringBoot 默认 8080 或 8081),浏览器会发起跨域请求。跨域不是后端拒绝,而是浏览器层面的同源策略拦截响应。这个问题我在第 4 节详细展开,这里先记住结论——开发环境用 Vue 的代理转发,生产环境用 Nginx 反向代理,基本能绕开 90% 的跨域困扰。
2.3 前端工程结构设计
前端沿用 Vue CLI 创建的标准工程结构,但我会根据管理系统特点做分层:
text复制src/
├── api/ // api 接口层
│ ├── login.js
│ ├── person.js
│ └── record.js
├── assets/
├── components/ // 公共组件
├── router/
│ └── index.js // 路由配置,含路由守卫
├── store/ // Vuex 状态管理
├── utils/
│ └── request.js // Axios 二次封装
└── views/ // 页面组件
├── Login.vue
├── Layout.vue
├── person/
├── record/
└── dashboard/
这个结构的好处是:api 层集中管理所有接口请求,页面里不直接出现 Axios 调用。改接口地址时只动 api 目录,后端接口变更时只排查一个文件,维护成本极低。utils/request.js 统一封装了请求拦截器(自动带 Token)、响应拦截器(统一处理错误码和 401 跳登录),这属于必做项,能避免每个页面重复写错误处理逻辑。
3. 核心功能实现与重难点解析
3.1 后端分层架构与统一响应封装
后端我采用的是经典 Controller-Service-Mapper 三层结构,同时把请求参数和返回数据用 DTO/VO 做了隔离。没有过度设计,但对这个体量的项目刚好合适。
text复制com.example.quarantine
├── controller/ // 接收请求,参数校验
├── service/ // 业务逻辑
├── mapper/ // MyBatis 数据访问接口
├── entity/ // 数据库实体类
├── dto/ // 请求参数对象
├── vo/ // 响应数据对象
├── config/ // 配置类(跨域、拦截器)
├── common/ // 统一响应、异常处理、工具类
└── QuarantineApplication.java
统一响应体是我特别建议所有信息管理系统都做的第一件事。提前定义好约定,前后端联调才能顺畅:
java复制public class Result<T> {
private Integer code; // 200成功,其他失败
private String msg; // 提示信息
private T data; // 业务数据
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
return result;
}
}
前端 Axios 响应拦截器里统一判断 code === 200,不是就直接 Message.error 弹出 msg。这样后端所有接口只需要关心业务逻辑,异常处理交给全局异常处理器兜底,不会出现“接口报错但前端收不到明确提示”的情况。
配套的全局异常处理器也要写:
java复制@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统繁忙,请稍后重试");
}
}
这样做的好处是:数据库异常、空指针等不该暴露给前端的细节被吞掉了,前端拿到的永远是 {code, msg, data} 三段式结构,解析逻辑非常统一。我见过很多项目接口返回格式五花八门,有的返回 Map,有的直接返回数组,有的错误时返回字符串,前端每个接口单独处理,维护起来想死。
3.2 数据库设计:核心表结构与关联关系
这个系统的数据模型是一个典型的一对多结构。核心是三张表:用户表(登录账号)、隔离人员信息表(人员基础信息)、每日健康记录表(每天一条健康状况)。
sql复制-- 用户表
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
`real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
`role` varchar(20) NOT NULL DEFAULT 'user' COMMENT '角色: admin/user',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态: 1启用 0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 隔离人员信息表
CREATE TABLE `quarantine_person` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '姓名',
`id_card` varchar(18) DEFAULT NULL COMMENT '身份证号',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`address` varchar(200) DEFAULT NULL COMMENT '隔离地址',
`start_date` date DEFAULT NULL COMMENT '隔离开始日期',
`end_date` date DEFAULT NULL COMMENT '隔离结束日期',
`status` varchar(20) DEFAULT 'isolating' COMMENT '状态: isolating 隔离中/completed 已解除',
`user_id` bigint(20) DEFAULT NULL COMMENT '关联用户id',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='隔离人员信息表';
-- 每日健康记录表
CREATE TABLE `daily_health_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`person_id` bigint(20) NOT NULL COMMENT '关联人员id',
`temperature` decimal(4,2) DEFAULT NULL COMMENT '体温',
`cough` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否咳嗽: 0否 1是',
`fatigue` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否乏力: 0否 1是',
`other_symptom` varchar(500) DEFAULT NULL COMMENT '其他症状',
`record_date` date NOT NULL COMMENT '记录日期',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_person_date` (`person_id`, `record_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每日健康记录表';
为什么把人员和每日记录拆开,而不是十天的记录都放在人员表里塞一堆字段?因为每天的体温、症状是一组不断增长的数据,属于典型的“一对多”关系。如果都塞在一张表里,要么字段冗余,要么每天更新同一行造成数据覆盖,要么只能记最新一天,历史记录丢失。拆表之后,人员表只管基础信息,记录表按人按天存储,查某个人的近 7 天记录就是一条带条件的分组查询,逻辑非常干净。
数据库设计中还有一个细节值得提:idx_person_date 这个联合索引。因为系统频繁执行“查某个人员某天/某时间段的健康记录”这种查询,联合索引可以同时走 person_id 和 record_date 两个条件,避免全表扫描。数据量小时体会不明显,等记录数量到了几十万条,这个索引能省出好几个数量级的查询时间。
3.3 MyBatis 动态 SQL:组合查询的关键实现
管理系统的列表页几乎都带筛选条件,比如“按姓名模糊查”“按下拉状态查”“按时间段查”。如果每个条件都写一个 Mapper 方法,组合起来会爆炸。MyBatis 的动态 SQL 就是解决这个问题的。
xml复制<select id="selectPersonList" resultType="com.example.quarantine.vo.PersonVO">
SELECT p.*, u.real_name AS register_user
FROM quarantine_person p
LEFT JOIN sys_user u ON p.user_id = u.id
<where>
<if test="name != null and name != ''">
AND p.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="status != null and status != ''">
AND p.status = #{status}
</if>
<if test="startDate != null">
AND p.start_date >= #{startDate}
</if>
<if test="endDate != null">
AND p.start_date <= #{endDate}
</if>
</where>
ORDER BY p.create_time DESC
</select>
<where> 标签会自动处理第一个条件前面的 AND,不需要手动写 WHERE 1=1。这个细节很重要,我见过新手手动拼 SQL,条件一多就容易出现 WHERE AND name = ... 的语法错误,而 <where> 标签就是专门解决这个问题的。
另外注意,小于号 小于 在 XML 里不能直接写 <,要用 < 转义。这也是 MyBatis XML 文件里最容易报错的点——写了个 <= 然后解析报错,最后发现是需要转义。
Mapper 接口和 XML 的绑定关系也要说一句:接口方法的全限定名(包名 + 接口名 + 方法名)必须和 XML 的 namespace + id 完全一致,这个我在第 5 节的错误列表里会单独展开。
分页方面,这个小项目我建议直接用 MySQL 的 LIMIT,手动计算偏移量即可,不一定要引入 PageHelper。原因是 PageHelper 这类分页插件通过拦截器改写 SQL,用得好确实方便,但偶尔会出现页码被 ThreadLocal 带串、count 查询异常等隐蔽问题。数据量几百条到几万条的系统裸 LIMIT 完全扛得住,少一个依赖就少一类坑。
3.4 单机登录鉴权:JWT + 拦截器
登录模块我用的方案是 JWT(JSON Web Token)+ HandlerInterceptor,没有引入 Spring Security / Shiro。原因是:这个系统只有 admin 和 user 两种角色,权限模型简单,引入安全框架要先学配置再看源码,反而增加了理解成本。自己写一个 Token 签发 + 校验的链路,反而能把原理吃透。
登录成功后后端返回一个签发的 Token,前端把 Token 存到 localStorage 里,每次请求在拦截器中附加到请求头:
javascript复制// utils/request.js
import axios from '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.msg || '请求失败')
return Promise.reject(new Error(res.msg))
}
return res
},
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
Message.error('网络异常,请稍后重试')
return Promise.reject(error)
}
)
后端拦截器校验 Token:
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 放行预检请求
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 = JwtUtil.parseToken(token);
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("role", claims.get("role"));
return true;
} catch (Exception e) {
// Token 无效或过期
}
}
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}");
return false;
}
}
拦截器只拦截需要登录的接口,登录接口本身和静态资源要放行。这个白名单配置在 WebMvcConfigurer 里:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/api/auth/login", "/error");
}
}
这里有一个项目上线时容易踩的坑:前端把 Token 放到 Authorization 请求头后,后端要确认跨域配置允许了这个自定义头。如果是生产环境 Nginx 反向代理,问题不大;如果直接前端跨域访问后端且用 CORS 配置,allowedHeaders("*") 一旦没加上,浏览器会先发 OPTIONS 预检请求,预检响应里如果没有 Access-Control-Allow-Headers: Authorization,真实的 GET/POST 请求永远发不出去。这个问题排查起来非常隐蔽,我第一次做前后端分离项目时卡了整整一个下午。
权限控制上,admin 可以查看全部人员和所有记录,user 只能看自己名下的人员和记录。这个通过在 Service 层判断角色实现:如果是 admin,查询不带 user_id 条件;如果是 user,强制拼接 WHERE user_id = 当前登录用户ID。比在 SQL 里写死要灵活,也比在前端隐藏按钮靠谱——接口层的权限校验才是底线。
3.5 前端关键页面实现思路
登录页:表单校验 + 调 login 接口 + 存储 Token 和用户信息 + 跳转首页。这里我建议把用户基本信息(昵称、角色)也存到 Vuex 和 localStorage,后续每个页面判断角色、显示用户名都需要,不然每次刷新页面用户信息就丢了。
首页统计看板:通过一个聚合接口返回几个核心指标,比如总隔离人数、今日健康上报人数、隔离中人数、已解除人数。前端用卡片组件展示数字,再用柱状图展示近 7 日体温异常趋势。图表用的 ECharts,按需引入,避免全量打包导致体积过大。
人员管理页:搜索表单 + 表格 + 分页 + 新增/编辑弹窗 + 删除确认。新增和编辑我复用了同一个弹窗组件,通过 isEdit 区分是新增还是修改,回显时把行数据传入组件。表单校验用 Element UI 自带的 rules,主要是姓名必填、身份证格式、手机号格式。
每日健康上报页:这是 user 角色每天都要用的页面。核心逻辑是先判断今天是否已上报,如果上报过了就回显记录并允许修改,否则显示空表单。日期用 new Date() 生成当天日期,传给后端做唯一性判断。
一个比较值得分享的前端细节是 Vue 组件拆分。页面和弹窗分开写,弹窗只负责表单逻辑,页面负责列表逻辑。父组件通过 visible.sync 控制弹窗显隐,弹窗提交成功后通过 this.$emit('refresh') 通知父组件刷新列表。这套模式在管理系统里可以无限复用,新页面基本都是“搜索表单 + 表格 + 弹窗”三个组件的排列组合。
4. 完整部署实操:从源码到线上可用
4.1 环境版本匹配建议
这一节直接关系到你能不能跑起来。很多人项目部署失败不是代码问题,而是版本不匹配。我给的组合是经过验证的:
| 软件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | SpringBoot 2.x 最稳的组合,3.x 才需要 JDK 17 |
| Maven | 3.6.x | 兼容性好,镜像源用阿里云 |
| Node.js | 14.x 或 16.x | Vue CLI 项目在这两个版本下构建最稳 |
| MySQL | 5.7 或 8.0 | 8.0 需要注意驱动和时区配置 |
| SpringBoot | 2.5.x ~ 2.7.x | 不要一上来就 3.x,很多教程和依赖不兼容 |
特别提醒:不要为了赶时髦选 SpringBoot 3.x。SpringBoot 3 底层是 Spring Framework 6,要求 JDK 17,而且一些老版本的 mybatis-spring-boot-starter 不兼容。我之前见人用 JDK 8 直接跑 SpringBoot 3 项目,启动就报 UnsupportedClassVersionError,白白浪费半小时。做学习项目,新不如稳。
4.2 数据库初始化与后端配置
第一步,创建数据库并导入 SQL 脚本:
bash复制mysql -u root -p
# 输入密码后执行
CREATE DATABASE quarantine_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE quarantine_system;
SOURCE /path/to/quarantine.sql;
必须用 utf8mb4,不是 utf8。utf8 在 MySQL 里最多存 3 个字节,遇到 emoji 或者部分生僻字直接报 Incorrect string value,而 utf8mb4 是完整的 4 字节存储。信息管理系统名字、备注里难免有特殊字符,这是经验之谈。
第二步,修改后端 application.yml:
yaml复制server:
port: 8080
servlet:
context-path: /api
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/quarantine_system?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 你自己的密码
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.quarantine.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.example.quarantine.mapper: debug
有几个配置项必须说清楚:
serverTimezone=Asia/Shanghai:MySQL 8.0 默认时区是 UTC,不设置这个参数,datetime字段读写会差 8 个小时,排查起来极其痛苦。allowPublicKeyRetrieval=true:MySQL 8.0 使用 caching_sha2_password 认证时,初次连接会报Public Key Retrieval is not allowed,加这个参数解决。map-underscore-to-camel-case: true:开启下划线转驼峰,数据库字段real_name自动映射到 Java 属性realName,省去大量 resultMap 配置。
第三步,打包运行:
bash复制mvn clean package -DskipTests
java -jar target/quarantine-system-0.0.1-SNAPSHOT.jar
启动完看到 Started QuarantineApplication 日志就是成功了。本地验证:浏览器访问 http://localhost:8080/api/auth/login,POST 一个 JSON 测试登录接口是否有响应。
4.3 前端构建与 Nginx 发布
前端开发时直接 npm run serve 本地调试,但生产环境必须构建成静态文件交给 Nginx 托管。
先改一个关键配置:Vue 生产环境 API 地址。开发环境用代理转发,构建时 Axios 的 baseURL 也要对应调整。
我这里比较推荐的方案是 Axios 统一用相对路径 /api,开发环境由 vue.config.js 代理转发到后端 8080,生产环境由 Nginx 把 /api 反向代理到后端。这样前端代码里不需要区分环境,一处配置全局可用。
开发环境代理配置:
javascript复制// vue.config.js
module.exports = {
devServer: {
port: 8081,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
// 注意:这里没有配置 pathRewrite,因为前后端都带 /api 前缀
}
}
}
}
为什么没有 pathRewrite?因为后端我配置了 server.servlet.context-path: /api,所以后端接口本身就是 /api/auth/login 这种路径。前端请求 /api/auth/login,代理转发到 http://localhost:8080/api/auth/login,路径完全匹配,不需要重写。
生产构建:
bash复制npm install
npm run build
构建完成后 dist/ 目录就是需要部署的静态文件。Nginx 配置如下:
nginx复制server {
listen 80;
server_name localhost;
# 前端静态资源
location / {
root /usr/share/nginx/html/dist;
index index.html;
try_files $uri $uri/ /index.html; # 支持 history 路由
}
# 后端接口反向代理
location /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 模式,不带 hash 的 URL(比如 /dashboard)在刷新页面时,Nginx 会去磁盘找 /dashboard 这个文件,找不到就 404。try_files 的作用是“找不到就回退到 index.html”,由前端路由接管并渲染对应页面。
第二个坑是 proxy_pass http://127.0.0.1:8080/; 末尾的 /。这是 Nginx 代理里最经典的走位问题:location /api/ 会将 URL 中匹配到的 /api/ 部分替换为 proxy_pass 中的 URI。
- 如果
proxy_pass http://127.0.0.1:8080;(没有斜杠),请求/api/auth/login会原样转发为http://127.0.0.1:8080/api/auth/login。 - 如果
proxy_pass http://127.0.0.1:8080/;(带斜杠),请求/api/auth/login会去掉/api前缀,转发为http://127.0.0.1:8080/auth/login。
因为我的后端设置了 context-path: /api,所以 Nginx 必须不带斜杠转发,否则后端会因为找不到 /api/api/auth/login 而 404。这块逻辑不复杂,但方向搞反就是 404,方向对了立马通,配置的时候一定想清楚。
最后浏览器输入服务器 IP 访问,能打开登录页、能登录、列表能出来、增删改查没问题,部署就算完成了。
5. 常见问题排查与避坑速查
这部分是所有实操人的精华,我把开发到部署全流程里最容易翻车的点整理成了一张速查表,再挑几个重点讲透。
| 现象 | 原因 | 解决方式 |
|---|---|---|
| 前端请求后端接口报跨域 | 开发环境未配置代理,或生产环境未配置反向代理 | 开发用 vue.config.js proxy,生产用 Nginx location /api 反代 |
后端报 Public Key Retrieval is not allowed |
MySQL 8.0 认证方式问题 | JDBC URL 加 allowPublicKeyRetrieval=true |
| 时间字段相差 8 小时 | MySQL 时区默认 UTC | URL 加 serverTimezone=Asia/Shanghai,也可改 MySQL 全局时区 |
MyBatis 启动报 Invalid bound statement (not found) |
Mapper 接口和 XML 的 namespace/id 不匹配 | 检查 mapper-locations 路径,检查 namespace 是否为接口全限定名 |
| 列表页刷新就 404 | Vue history 路由未配置 try_files | Nginx location / 里加 try_files $uri $uri/ /index.html; |
| 登录接口请求头 Authorization 丢失 | 跨域预检请求被后端拦截 | 拦截器放行 OPTIONS 请求,CORS 允许 Authorization 请求头 |
| Nginx 代理后端 404 | proxy_pass 末尾斜杠导致前缀被吞 | 根据后端 context-path 确认是否需要带斜杠 |
| 数据库中文乱码 | 数据库字符集不是 utf8mb4 | 建库用 utf8mb4,连接 URL 加 characterEncoding=UTF-8 |
| 前端新版依赖构建报错 | Node 版本过高或过低 | 统一用 Node 14/16,vue-cli 项目别直接用 Node 20 |
5.1 跨域问题:开发和生产两个层面
跨域是我见过拦住新手时间最长的问题,没有之一。这里给出一个明确的排查顺序:
先分清你在哪个环境。开发环境跑的是 http://localhost:8081(Vue)和 http://localhost:8080(SpringBoot),两个端口不同,浏览器必然拦截。这个阶段的解法是 Vue 代理,前端代码请求 /api/xxx 时会自动转发到 8080 端口,浏览器看到的是同源请求。
生产环境用 Nginx 把 80 端口的前端请求里的 /api 转发到 8080,同样不存在跨域。两种方式都把跨域问题解决在“入口”层面,后端代码里不一定要配 CORS。
如果确实要用后端 CORS 解决,正确姿势是注册一个 CorsFilter 并允许 Authorization 自定义头,只放行前后端地址,不要无脑 allowedOriginPatterns("*")。生产环境配了通配跨域,其实就是把自己后端接口完全暴露给任何站点调用,存在安全风险。
5.2 MyBatis 与数据库典型坑
Invalid bound statement 这个错出现频率极高。Maven 项目有个经典问题:src/main/java 目录下的 XML 文件默认不会被打进 target 的 classpath。如果 Mapper 接口和 XML 放在同一个包下,需要在 pom.xml 里加资源配置,或者把 XML 统一放到 src/main/resources/mapper/ 目录,再用 mybatis.mapper-locations: classpath:mapper/*.xml 指定。这个坑的本质是:MyBatis 在运行时是根据接口全限定名去找 XML 的,找不到就报绑定错误。
数据库字段 is_deleted、create_time 映射 Java 属性时,只要开启了 map-underscore-to-camel-case: true,createTime 自动对应。但注意 resultType 映射只对查出来的列名起效,如果 SQL 里写了别名,别名必须和 Java 驼峰属性一致才能自动映射。
5.3 部署阶段容易翻车的细节
部署看起来就是几个命令行,实际操作时问题都在细节上:
- 打包前先清依赖:前端换了环境或者
node_modules不干净时,删除node_modules和package-lock.json重新npm install,能解决 80% 的幽灵依赖问题。 - 后端打包前先跑测试:本地
mvn test会启动 Spring 上下文,如果数据源连不上,打包过程在测试阶段就挂了。用-DskipTests跳过测试能快速出包,但跳过不等于问题不存在,上线前测试还是要过一遍。 - Linux 上部署注意端口和防火墙:SpringBoot 默认 8080,Windows 本机能通,Linux 上浏览器访问不了多半是防火墙或云安全组没放行,这个和代码无关但最容易忽略。
- 数据库连接串的密码别写死在代码里:小项目图省事写 yml 里没问题,但生产环境还是建议用环境变量:
password: ${DB_PASSWORD}。一旦项目传到公共仓库,密码泄露是安全事故。
5.4 关于 Nginx 代理的一个真实案例
我之前部署时遇到过一次很典型的“前端能打开登录页,一登录就 404”的情况。现象是:静态页面正常加载,POST 登录接口返回 HTML 而不是 JSON。
排查过程:
- 浏览器 F12 看 Network,登录请求的响应内容是 Nginx 的 404 HTML 页面。
- 用
curl直接访问后端http://localhost:8080/api/auth/login,返回正常 JSON。 - 说明问题出在 Nginx 转发环节。
- 检查 Nginx 配置,发现写的是:
nginx复制location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
这个配置会把 /api/auth/login 转发成 http://127.0.0.1:8080/auth/login,而后端接口实际是 /api/auth/login(context-path 是 /api),所以后端返回 404,Nginx 直接把这个 404 HTML 抛给了前端。
最后改成不带斜杠:
nginx复制location /api/ {
proxy_pass http://127.0.0.1:8080;
}
重启 Nginx,问题解决。这个案例的教训是:配置代理时,先想清楚后端实际接口路径,再决定 proxy_pass 是否保留斜杠。类似的“路径被吞”问题,在网关、代理场景里屡见不鲜,有成体系的排查思路很重要。
6. 写在最后的一点个人心得
这套项目从头到尾自己搭一遍,最大的收获不是学会 SpringBoot 怎么写 Controller、Vue 怎么写页面,而是建立起了对全链路的感觉:浏览器发请求 → Nginx 转发 → SpringBoot 处理 → MyBatis 查库 → 数据原路返回 → 前端渲染。任何一个环节出了错,都要顺着这条链路去排查,而不是孤立地看前端代码或后端日志。
给后来者的一个建议:第一次做这种完整项目,不要只盯着业务功能,先跑通一条主链路。比如先实现最小闭环——登录 + 人员新增 + 列表查询,部署到服务器上确认通,再逐步加健康上报、统计看板这些功能。主链路通了,后面所有功能都只是往框架里填代码;主链路不通,所有功能都白做,排查时还分不清是前后端哪个环节的问题。
我这个项目里最花时间的部分,说实话不是写代码,而是联调和部署。前端调不通后端接口、Nginx 代理路径错误、MySQL 时区偏移——这些问题是教科书上不会细讲的,却又真实消耗开发时间。希望这篇梳理能帮你少踩几个坑。项目源码按前面章节的结构和配置来写,跑通是完全没有问题的。
