☰
前后端分离项目申报系统实战:SpringBoot+Vue+MyBatis全栈开发
2026/10/2 20:10:06 网站建设 项目流程

1. 项目拆解:为什么这套前后端分离申报系统值得做

先说结论:凡是想把 SpringBoot + Vue + MyBatis + MySQL 这一整套技术栈串起来的开发者,这套“web 项目申报系统”几乎是绕不开的练手题。

原因很简单,它是典型的业务系统样貌:有用户角色,有表单提交,有审批流转,有状态管理,有文件上传,还有权限控制。你想在简历上写“熟悉前后端分离开发”,那你总得有一个拿得出手的完整项目,而不是那种只写了登录注册的 demo。

这套系统解决的核心问题,用大白话说就是:把“项目申报”这件事从线下表格搬到线上,让申报人、审批人、管理员各干各的活,数据统一落到数据库里,流程一眼看得清。

你需要的技术栈,恰好是当前 Java 生态里最主流、就业市场最认可的那一套:

  • 后端用SpringBoot做接口服务,简化配置,快速起项目
  • 持久层用MyBatis,SQL 自己掌控,复杂的多表查询写起来不憋屈
  • 前端用Vue(配合 Element UI / Element Plus 这类组件库),做单页应用,体验比传统 JSP 不知道好到哪里去
  • 数据库用MySQL,开源、免费、资料多,从开发到部署都不会断粮

这套面向谁?

  • 刚学完 Java 基础、Servlet 那套,想升级到企业级开发的在校生
  • 毕业设计选题选了“某某申报系统”“某某管理系统”的应届生
  • 转行做 Java 开发,需要一份能讲清楚前后端分离原理的实战经验的求职者
  • 甚至是一些中小企业,真的需要一套内部项目申报、经费申请的工具流程,拿它改改就能用

我当年自己刚接触前后端分离的时候,最大的困惑就是:前端跑一个端口,后端跑一个端口,它们怎么配合?数据怎么传?权限怎么控制?部署到服务器上又该怎么办?

这些问题,如果你只是零散地看教程,永远是一团浆糊。但当你把一个完整的“申报系统”从零到一落地,你会非常清晰地建立起整个 web 开发的全局观。后面每一步,我都会把思路拆开讲,不仅告诉你“怎么做”,更重要的是让你明白“为什么这么做”。

2. 整体设计与技术选型:每个决定背后都有讲究

2.1 前后端分离的本质:各干各的,各管各的

先纠正一个很多新人会搞错的概念:前后端分离不是指前端一个文件夹、后端一个文件夹,而是指两者在运行时完全独立。

开发阶段,前端用 Vue 的脚手架跑在 8080(或者 5173),后端 SpringBoot 跑在 8080,两者通过 HTTP/JSON 通信。前端静态资源、路由跳转、页面渲染全部由浏览器里的 Vue 应用负责;后端只提供 RESTful 接口,处理业务逻辑、数据库读写、鉴权校验。

这种模式带来的直接好处是:前后端可以并行开发,只要你把接口文档定好,前端不用等后端,后端也不用等前端。出了问题也更好排查——到底是接口返回错了,还是页面渲染错了,一条请求就能看清楚。

但代价也很明确:开发环境必须处理跨域问题。你的前端页面跑在http://localhost:8081,后端接口在http://localhost:8080,浏览器会拦截跨域请求,这个时候就需要在 SpringBoot 里配置 CORS,告诉浏览器“这个来源的请求可以放行”。

我见过不少新手,项目没跑起来第一反应就是“代码报错”,其实八成是跨域没配好。这一块我会在后面的实操章节详细给出配置方式。

2.2 为什么后端选 SpringBoot 而不是 SSM

很多人问我,学了 SSM(Spring + SpringMVC + MyBatis)还有必要学 SpringBoot 吗?

这个问题这么看:SSM 是“把三块拼起来”,SpringBoot 是“三块已经焊死,你只管用”。SpringBoot 内置了 Tomcat,自动装配了一堆常用配置,还提供了application.yml这一套集中的配置方式。你从 SSM 迁移到 SpringBoot,本质上是工作量的大幅缩减,而不是技术的推倒重来。

对于“申报系统”这种典型 CRUD + 业务状态的系统,SpringBoot 的优势非常明显:

  • 快速创建项目(start.spring.io 三秒钟生成)
  • 集成 MyBatis 只需要一个依赖加一个@MapperScan
  • 自带参数校验、拦截器、全局异常处理机制
  • 部署时打一个 jar 包,扔到服务器上就能跑,不用再装 Tomcat

当然,SpringBoot 版本选择上有个坑:版本太高会导致一些兼容性问题。我自己遇到过 SpringBoot 2.4 以后对跨域配置、路径匹配策略的调整,还有和 MyBatis-Spring 版本不匹配导致报错的情况。所以我一般建议新手使用SpringBoot 2.7.x 系列(例如 2.7.18),这是一个极其稳定的版本,在网上能查到的资料最多,踩坑案例也最全。后面我会专门讲版本为什么不是越高越好。

2.3 MyBatis 与 MySQL:SQL 掌控感是核心

持久层选 MyBatis 而非 JPA/Hibernate,核心原因是:对于申报系统这种多表关联、条件查询比较多的场景,手写 SQL 能做到最直观的掌控。

比如“按申报人姓名模糊查询 + 按状态筛选 + 按申报时间排序”,这种动态 SQL 在 MyBatis 里用<if>标签拼起来非常顺手,你在 XML 里能一眼看出最终执行的 SQL 长什么样子。而在 JPA 里你要去猜方法名、去理解自动生成的 SQL,出了问题反而不容易定位。

MyBatis 的核心知识点,这个项目里全都会用到:

  • Mapper 接口 + XML 映射:接口定义方法,XML 写 SQL
  • 参数传递:@Param注解实现多参数传递
  • 动态 SQL:<where>、<if>、<foreach>处理条件查询和批量操作
  • ResultMap:处理数据库字段(create_time)和 Java 实体属性(createTime)的驼峰映射
  • 一级缓存 / 二级缓存:理解 MyBatis 的缓存机制,避免查询脏数据

再说 MySQL,这个项目用到的表结构不算复杂,但涉及多张表关联:用户表、项目申报表、审批记录表、附件表、通知表等等。设计的时候每张表都要明确主键策略、索引、外键逻辑。MySQL 的安装和配置,如果你还没搞定,我强烈建议你用MySQL 8.0.x,记得下载 MSI 安装包(不推荐绿色版),密码认证方式注意选caching_sha2_password,同时在连接 URL 里加上useSSL=false&serverTimezone=Asia/Shanghai,否则很容易碰到奇怪的连接报错。

2.4 前端 Vue 的大纲思路:不只是组件套页面

Vue 在这个项目里的角色,简单说是“负责所有用户可见的东西”:登录页、申报表单页面、申报列表页面、审批页面、用户管理页面。

你需要掌握的基础知识,我会在实操章节展开:

  • Vue 实例与生命周期:什么时候发请求、什么时候渲染数据
  • 组件化开发:把重复的部分(如表单、表格、分页条)抽成组件
  • Vue Router 路由:不同的 URL 对应不同的页面组件,动态路由传参
  • Axios 封装:统一处理请求头(token)、响应状态码、错误提示
  • 状态管理(Pinia / Vuex):各组件共享登录用户信息
  • Element UI / Element Plus:表格、表单、弹窗、消息提示,靠这一套组件库实现高颜值后台

前端最关键的体验就是:页面的“假跳转”。传统网页每次跳转都要刷新,而 Vue Router 是前端路由,切换页面只是替换当前页面里的某个组件,不会整页刷新,所以体验非常好。这也是前后端分离在外面看起来“高级”的一个面。

2.5 权限控制:为什么一定要用 JWT 而不能用 session

申报系统里存在不同的角色:学生/申报人可以提交项目,老师/领导可以审批,管理员可以管理用户和分类。这就意味着你的接口不能裸奔,必须让后端能识别出“你是谁,你有没有权限做这个操作”。

两个主流方案:Session + Cookie和JWT(JSON Web Token)。

我强烈建议这个项目用 JWT:

  • 前后端分离架构下,Cookie 处理跨域比较麻烦,而 JWT 用请求头Authorization携带 token,非常干净
  • JWT 天然适合无状态服务,后端不需要保存会话信息,重启服务也不会导致用户掉线
  • 登录成功后,前端把 JWT 存在 localStorage 或者 Pinia 里,每次请求通过 axios 拦截器带上

一个典型的 JWT 流程是这样的:

  1. 用户访问登录接口,提交用户名和密码
  2. 后端校验通过,生成一个 JWT(里面包含用户 ID、用户名、角色等,用 secret 签名)
  3. 前端拿到 JWT,保存起来
  4. 此后每次请求都带上Authorization: Bearer <token>
  5. 后端的拦截器解析 token,校验有效性,并把用户信息放到请求上下文里
  6. 如果 token 过期或非法,返回 401,前端跳回登录页

这里我要特别提醒一个坑:JWT 的 secret 一定不要用简单的字符串如 “123456” 放在代码里,即使这是一个学习项目,你也要养成从配置文件中读取的习惯。密码加密同理,不要用明文,用 Spring Security 的 BCrypt 或者至少用 MD5 加盐(不过我更推荐 BCrypt,安全等级不是一个级别)。

3. 实操断层拆解:从建库建表到核心接口的实现细节

3.1 数据库设计:申报系统的表结构应该怎么设计

在我做过的很多类似系统里,第一件事永远是设计表结构。表结构设计得好,后面所有代码都写得很顺;如果一开始草率,后面查数据、改需求的时候你会想骂人。

一个完整的项目申报系统,至少需要这几张核心表:

表名说明重要字段
sys_user用户表id, username, password, real_name, role, dept_name, status
project_declare项目申报表id, project_name, project_type, applicant_id, budget, begin_time, end_time, content, status
approval_record审批记录表id, project_id, approver_id, opinion, result, create_time
sys_file附件表id, biz_type, biz_id, file_name, file_url, create_time
base_dict数据字典表(项目类型等)id, dict_type, dict_label, dict_value

面有三点必须注意:

  1. 主键统一用bigint自增或者雪花ID,不要图省事用 int,项目一大数据量上来会吃紧。这个体量无所谓,但你养成习惯就好
  2. 凡是外键逻辑字段,加索引,例如applicant_id、project_id,因为你会频繁按这些字段去关联查询
  3. 状态字段status用 int 类型配合状态枚举,不要用字符串存“待审核/已通过/已驳回”,否则后期统计 SQL 很难写

举个例子,project_declare的建表 SQL,我会这样写:

CREATE TABLE `project_declare` ( `id` bigint NOT NULL AUTO_INCREMENT, `project_name` varchar(200) NOT NULL COMMENT '项目名称', `project_type` varchar(50) DEFAULT NULL COMMENT '项目类型(字典)', `applicant_id` bigint NOT NULL COMMENT '申请人id', `budget` decimal(10, 2) DEFAULT 0.00 COMMENT '预算金额', `begin_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `content` text COMMENT '项目内容', `status` int NOT NULL DEFAULT 0 COMMENT '0草稿 1待审核 2已通过 3已驳回', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_applicant_id` (`applicant_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意几个细节:

  • utf8mb4而不是utf8,否则你存个 emoji 表情直接报错
  • ON UPDATE CURRENT_TIMESTAMP自动维护更新时间
  • 索引命名规范idx_开头,一眼识别

3.2 后端工程结构:包结构规范是给别人看的

SpringBoot 项目包结构,决定后面维护项目的难度。我的常用结构是这样:

com.example.project ├── config # 配置类(CORS、MyBatis配置、WebMvcConfig) ├── controller # 接口层,只处理请求和响应 ├── service # 业务层,具体逻辑 │ └── impl ├── mapper # MyBatis接口 ├── entity # 数据库实体 ├── dto # 数据传输对象(前端传参)和VO(返回给前端) ├── common # 统一响应结果、全局异常处理、常量类 ├── interceptor # JWT拦截器 ├── utils # 工具类(JWT工具、文件上传工具) └── ProjectApplication.java

这里我想强调一下为什么不把业务逻辑写在 controller 里。很多新手为了图快,直接在 Controller 里查询数据库然后返回结果,一两个接口没问题,但一旦业务复杂起来——例如申报要写审批记录、要更新项目状态、要发送通知——你的 Controller 会膨胀到无法阅读,而且没法复用。

正确的做法是:Controller 只负责参数校验、调用 Service、返回结果;Service 层写具体业务逻辑;Mapper 层只做数据访问。

这叫做“职责单一”,它不是说代码变少了,而是让代码的可敬强度增加。

3.3 统一返回格式:前后端对接的契约

前后端分离最大的坑就是“前端拿到的数据结构和我预期不一致”。你在返回每个接口时,如果没有统一格式,前端每个请求都要单独解析,非常痛苦。

所以第一件事,定义统一的返回类:

@Data public class Result<T> { private Integer code; // 200成功,500失败 private String message; // 提示信息 private T data; // 数据 }

并且提供一个静态方法方便返回:

public static <T> Result<T> success(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; }

你有心的话,再加一个Result.fail(String message)。这样前端封装 axios 响应拦截器时,只需要判断res.data.code === 200,否则弹出message即可。

3.4 分页查询:你一定会用到,别自己手写 limit

申报列表、用户列表、审批列表,所有列表都要分页,否则数据一多页面就卡。

MyBatis 分页有几种做法,最推荐用的是PageHelper 插件:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>

然后在 Service 中使用:

PageHelper.startPage(pageNum, pageSize); List<ProjectVO> list = projectMapper.selectProjectList(query); PageInfo<ProjectVO> pageInfo = new PageInfo<>(list);

这里要注意:PageHelper.startPage必须紧跟需要分页的那条查询语句,中间千万不要执行其他 SQL 查询,否则分页会作用到错误的查询上。我踩过这个坑,调试了半天最后发现是先读取了字典表再查询列表导致分页错乱。

3.5 JWT 拦截器实现:十分钟写好的关键防线

在 SpringBoot 里实现权限控制,最简便的方式是编写一个 HandlerInterceptor:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // 放行登录接口 if (request.getRequestURI().contains("/login")) { return true; } // 获取 token String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}"); return false; } String jwt = token.substring(7); try { Claims claims = JwtUtil.parseToken(jwt); // 将用户信息放入 request attribute 中,后续 controller 里可以取 request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"token无效或过期\"}"); return false; } } }

然后注册到 WebMvcConfigurer:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns("/api/login", "/api/register", "/file/**"); } }

这样做的好处是,你不用在每个 Controller 方法里手动写“判断用户是否已登录”,拦截器统一搞定。而角色权限(比如审批接口只能由审批角色访问),可以在 Service 层从 request attribute 中取出 role 再判断。

3.6 文件上传:申报附件用 MinIO 还是本地存储?

很多申报系统需要上传附件,比如项目书、预算表。这一步有很多方案,但新手阶段我建议:本地磁盘存储 + MySQL 记录路径就够了,不用急于上 MinIO / OSS。

本地存储的实现:

# application.yml file: upload-dir: /opt/project/upload/ access-url: /file/**

上传接口:

@PostMapping("/file/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { // 生成存储文件名,防止重名 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadDir + newFileName)); // 访问 URL 返回给前端 return Result.success("/file/" + newFileName); }

然后再配置一个资源映射,让前端能直接打开图片、下载附件:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/file/**") .addResourceLocations("file:" + uploadDir); }

等你以后项目规模大了,再把 MinIO 接到 SpringBoot 中,接口形式都不用变,替换一下存储实现就行。这个“先本地、后替换”的思路,在真实项目里也很常见。

3.7 Vue 前端核心实现:从脚手架到完整页面

前端工程的结构建议同样清晰:

src ├── api # 接口定义 │ ├── login.js │ ├── project.js │ └── user.js ├── assets ├── components # 公用组件(表格、分页等) ├── router # 路由配置 ├── store # Pinia 状态管理 ├── utils # axios 封装、工具函数 ├── views │ ├── Login.vue │ ├── ProjectApply.vue │ ├── ProjectList.vue │ ├── ProjectAudit.vue │ ├── UserManage.vue │ └── Dashboard.vue ├── App.vue └── main.js

这里我挑几个重点讲。

axios 封装:

// utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', // 开发环境用 vite 代理,生产环境也用 /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( res => { if (res.data.code === 200) { return res.data.data } ElMessage.error(res.data.message) return Promise.reject(new Error(res.data.message)) }, err => { if (err.response && err.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络错误') return Promise.reject(err) } )

为什么 axios 返回res.data.data?因为后端统一结果包了一层,前端把解包的逻辑收敛到拦截器里,之后的业务代码拿到的就是纯数据,不用每处都加res.data.data。

Vue Router 与动态路由:

路由比较简单,按角色动态加载是一个更高级的玩法。简单系统可以直接用路由守卫判断 token 是否存在以及点击侧边栏时根据角色判断是否显示。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { next('/login') } else { next() } })
3.8 前后端联调:开发环境代理 vs 生产环境部署

联调是新手最容易卡住的一大步。这里有两条路:

开发环境:vite 配置代理

// vite.config.js server: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端代码里的请求/api/login,会被 Vite 转发到后端的http://localhost:8080/api/login,完美绕开跨域问题。

生产环境:不需要代理,用 Nginx 反向代理:

server { listen 80; server_name your-server-ip; # 前端 Vue 打包后的静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口转发 location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

注意try_files那行,Vue 路由是不会在服务器上真实存在的文件路径,所以凡是前端路由 URL,比如/project/list,Nginx 都统一分发到index.html才能正确渲染。

4. 完整部署教程:从源码到线上可访问

4.1 打包前端:Vue 项目的产物是什么

前端最终交付的是一堆静态文件(HTML、JS、CSS)。打包命令:

npm run build

打包完成后,生成dist目录,里面的文件包括:

  • index.html
  • assets/目录(打包后的 js/css 文件名称会带 hash,如index.abc123.js)

你需要把整个dist目录内容上传到服务器的 Nginx 配置的root目录下。

有一个常见问题:为什么我服务器上页面白屏?

很大概率是因为路由模式问题。Vue Router 默认是createWebHistory(history 模式),这种模式下刷新非根路径页面会 404;解决方式有两个:

  • 改路由模式为createWebHashHistory(hash 模式),URL 会多个#,但稳定
  • 或者在 Nginx 配try_files $uri $uri/ /index.html;

我更推荐第二种,因为 URL 好看,体验也好。

4.2 打包后端:SpringBoot 三种打包方式

SpringBoot 后端打包,有几件事要说清楚。

如果你在 pom.xml 里配了:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

执行mvn package,就能打出一个可执行 jar 包,里面内嵌 Tomcat,直接运行:

java -jar project-system.jar --spring.profiles.active=prod

如果是传统方式打成 war 包扔进外部 Tomcat,那就需要修改启动类继承 SpringBootServletInitializer:

@SpringBootApplication public class ProjectApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(ProjectApplication.class); } }

然后打包成 war,丢到 tomcat/webapps 下,再调整contextPath访问。

如果你只是本地跑学习项目,jar 包已经够用。部署 jar 包比部署 war 包简单太多了,这也是从 SpringBoot 时代开始,我几乎没用过外部 Tomcat 的原因。

4.3 服务器环境搭建:MySQL、JDK、Maven 这些都要配好

部署一台干净的 Linux 服务器(比如 CentOS 7 / Ubuntu),你需要按顺序安装:

  1. JDK 1.8 或更高
  2. MySQL 8.0
  3. Nginx

JDK 安装简单,用yum install java-1.8.0-openjdk就行,但项目用了 SpringBoot 2.7 需要 JDK 8+,所以没问题。MySQL 安装强烈建议你从官网下载 MySQL 8.0 的 RPM 包或用 apt 源,用系统源可能装到旧版本导致兼容问题。

MySQL 安装之后的几个关键步骤:

-- 创建数据库 CREATE DATABASE project_system DEFAULT CHARACTER SET utf8mb4; -- 创建专用账号,不要用 root 直连 CREATE USER 'project'@'%' IDENTIFIED BY 'YourStrongPassword'; GRANT ALL PRIVILEGES ON project_system.* TO 'project'@'%'; FLUSH PRIVILEGES;

然后导入 SQL 脚本:

mysql -uproject -p project_system < init.sql

4.4 Nginx 托管前端 + 反向代理后端 = 全流程跑通

服务器上的部署步骤,我按顺序手把手写一遍:

# 1. 安装 Nginx yum install nginx -y # 2. 上传前端 dist 到 /usr/share/nginx/html # 3. 修改 Nginx 配置(/etc/nginx/conf.d/default.conf) # 4. 测试配置 nginx -t # 5. 启动 Nginx systemctl start nginx # 6. 上传后端 jar 到 /opt/app cd /opt/app java -jar project-system.jar &

启动后访问http://服务器IP就能看到前端页面了。登录请求走/api/login,前端自动代理到http://127.0.0.1:8080/api/login,用户看到的是一个完整可用的系统。

这里强烈建议你启用 Nginx 的 Gzip 压缩,前端静态资源体积能减小 60% 以上:

gzip on; gzip_types text/plain text/css application/json application/javascript;

4.5 使用 systemd 守护进程:别再用 nohup 裸跑了

你可能会看到很多教程教你nohup java -jar xxx.jar &,这在学习环境没问题,但一旦服务器重启或者进程意外退出,你得手动重新启动。

更好的方式是写一个 systemd 服务:

[Unit] Description=Project System After=network.target [Service] User=root WorkingDirectory=/opt/app ExecStart=/usr/bin/java -jar /opt/app/project-system.jar Restart=always RestartSec=10 Environment=SPRING_PROFILES_ACTIVE=prod [Install] WantedBy=multi-user.target

保存到/etc/systemd/system/project.service,然后:

systemctl daemon-reload systemctl enable project systemctl start project

之后查看日志用journalctl -u project -f,重启用systemctl restart project。这样运维体验一下子正规了很多。

4.6 部署完成的最终验证清单

部署结束,别急着说是“完成了”,按这份清单逐项检查:

  • 浏览器访问 IP 地址,能打开登录页
  • 用测试账号登录,跳转首页正常
  • 提交一个项目申报,数据库出现该条记录,状态为待审核
  • 用审批账号登录,能看到该条申报,通过后状态变更为已通过
  • 申报人账号重新登录,看到状态变化
  • 刷新页面,会话仍然有效(JWT 未过期)
  • 杀掉后端进程再启动,前端无需重新登录

5. 高频报错排查与避坑实录

这部分是实战中最值钱的内容,每一条都是我在开发这类系统时真实踩过的坑。

5.1 MySQL 连接报错:SSL 连接与时区问题

典型报错 1:

java.sql.SQLException: Could not create connection to database server.

多数是因为 MySQL 8.x 驱动和连接 URL 参数的问题。解决办法是在application.yml里写全参数:

spring: datasource: url: jdbc:mysql://localhost:3306/project_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4

allowPublicKeyRetrieval=true是 MySQL 8.0 使用caching_sha2_password插件时的必要参数,不加会偶发连接失败。这个坑网络教程里十有八九不会提。

典型报错 2:

Public Key Retrieval is not allowed

就是上面的问题。记住三个参数一起加:useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。

5.2 MyBatis 一堆奇怪的 bind、Invalid bound statement 错误

如果你启动 SpringBoot 后,调用 mapper 时报:

Invalid bound statement (not found): com.example.project.mapper.ProjectMapper.selectList

这根本原因多半是两个:

  1. 接口和 XML 的 namespace 对不上。检查 XML 文件里的 namespace 是否等于 Mapper 接口的全限定名
  2. XML 没有被打包进 classpath。Maven 默认只把 src/main/resources 下的 XML 打进包里,你的 mapper XML 如果放在 src/main/java 目录下就不在扫描范围内。解决方法很简单,在 pom.xml 中配置:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources>

但更常规的做法是把 XML 统一放在src/main/resources/mapper/目录下,然后在配置里指明:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.project.entity

5.3 MyBatis 查询返回字段全是 null

这是老生常谈:数据库字段明明是create_time,Java 属性是createTime。如果你没有配置驼峰映射,MyBatis 默认不会自动转换。

配置:

mybatis: configuration: map-underscore-to-camel-case: true

这样就能自动把create_time映射到createTime。如果你依然有部分字段映射不上,用 ResultMap 手动指定即可。

5.4 前端跨域彻底搞不定:要么配代理,要么配 CORS

如果你不想用 Vite 代理,那么后端 CORS 必须配好。两种方式你任选一种,但记住不要同时混乱使用。

SpringBoot 全局 CORS 配置:

@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); } }

注意:allowCredentials(true)时,allowedOrigins("*")在某些 SpringBoot 版本不允许,要用allowedOriginPatterns("*")。

更推荐的方式还是开发环境用 Vite 代理,生产环境用 Nginx 代理,后端永远不需要配置 CORS,这也是真实项目的常见做法。

5.5 上传文件后访问 404

三步排查:

  1. Nginx 是否把文件和静态资源路径分开代理?如果后端接口和文件都走/api/,那是你自己把资源路径配置错误
  2. SpringBoot 是否配置了资源映射?addResourceHandlers有没有匹配到/file/**
  3. 文件是否写入了你预期上传目录?检查绝对路径,很多新手把相对路径写进去,当前的工作目录一变就 404

5.6 Vue 项目打包后页面路径失效

打包后的index.html里有资源引用路径/assets/xxx.js,如果你的站点部署在子路径如http://ip/project/,就会 404。需要在vite.config.js设置:

export default defineConfig({ base: './' // 相对路径打包 })

这样资源引用变成相对路径,放在任意子目录都行。如果是根路径部署,用默认/即可。

5.7 JWT 过期后的前端处理不及时

如果你没有在 axios 拦截器统一判断 401,那么用户 token 过期之后,前端会一直显示请求失败,但页面迟迟不跳转登录页。这就是拦截器没写好的原因。记得在响应拦截器里判断err.response.status === 401并跳转。

5.8 MySQL 字符集乱码

建库、建表、连接 URL、前端页面全都要用 UTF-8。打开 MySQL 配置文件,确认:

[mysqld] character-set-server=utf8mb4

连接 URL 再加characterEncoding=utf8mb4,数据库字段类型用varchar而不是char,基本就不会乱码。

5.9 数据库连接池连接泄漏导致的“Too many connections”

这是很隐蔽的性能问题。如果你用 SpringBoot 默认的 HikariCP,连接池最大连接数默认是 10;在高并发或者有大量慢查询时,很容易打到上限报错。你可以调大,但更本质的是检查你的代码里有没有Connection没有关闭——当然用 JdbcTemplate、MyBatis 一般不会出现这种问题,但如果你自己手写 JDBC 就得注意。配置参考:

spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000

5.10 降低 SpringBoot 版本坑:为什么我用 2.7 而不是 3.x

网上搜“springboot 版本太高”非常多,SpringBoot 3.x 已经全面要求 Java 17 及以上,并采用 Jakarta EE 命名空间(包名从javax改成jakarta),很多基于旧 API 的第三方库、教程都不兼容。如果你是为了快速完成系统、跑通业务,选 2.7.x 是风险最低的选择。等 Java 17 + SpringBoot 3 生态成熟了,你再升级也不迟。

6. 常见问题速查表(遇到直接翻)

问题原因解决方案
启动报 Failed to configure a DataSource没配数据库连接在 application.yml 配置数据源或排除自动配置
前端请求 403拦截器拦截了登录请求在拦截器 excludePathPatterns 中加入登录接口
前端请求 404 且控制台无请求Nginx try_files 没配加上try_files $uri $uri/ /index.html;
前端请求 405跨域请求预检失败检查 CORS 配置 allowedMethods 是否包含 OPTIONS
数据库中文乱码字符集不统一统一 utf8mb4,包括建库、连接、MySQL 配置
上传附件超过大小报错默认 1MB 限制在配置中设置spring.servlet.multipart.max-file-size和max-request-size
登录成功但后续请求 401token 未正确保存或未携带检查前端 localStorage set 与 axios 拦截器 get
MyBatis 批量插入报 SQL 语法错误foreach 写错检查<foreach>的 separator、open、close 是否配对
Vue 打包后白屏base 路径或路由模式问题设置 base: './' 或 Nginx try_files
后端启动太慢扫描包范围过大精确设置@SpringBootApplication(scanBasePackages)

这个表建议收藏,遇到问题先查这里,大多数情况能省下不少时间。

7. 一些真正提升“完成度”的进阶建议

当你把基础版本完整跑通之后,可以按优先级做这些增强,每一个都能让你的项目在考核、面试中有质的提升。

7.1 加入 Spring Security 的 BCrypt 密码加密

这个优先级最高。现在的系统如果还是明文密码存储,面试官一眼就会否定你的安全意识。方案是引入 Spring Security 的 crypto 模块,只用它来做加密,不引入完整的安全框架,避免复杂化:

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency>

注册时:

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodePwd = encoder.encode(password);

登录时:

if (encoder.matches(rawPassword, user.getPassword())) { // 登录成功 }

7.2 加入操作日志

每次登录、申报、审批、删除都记录到一张op_log表。用 AOP 实现一个@Log注解,方法执行完后自动记录操作人、操作类型、IP、耗时。这一手很能体现工程素养,前端加一张日志查询页面就更完整了。

7.3 审批流程的状态机设计

现在审批只有一段:待审核/通过/驳回。你可以扩展成多级审批,比如“教研处审批 → 财务处审批 → 校领导审批”。这时不要把状态简单地用 int 存死,而是设计一张approval_flow表,动态设置当前审批节点。这样你的系统从一个“单级审批”进化成了“工作流雏形”,面试发挥空间大很多。

7.4 项目类型等字典表管理

固定选项不要写死在代码里,维护到base_dict表中,前端动态拉取并渲染。这样以后新增“重点项目类型”不用改代码,后台管理一个页面就能搞定。

8. 我最后想分享的几句话

这个项目做完,你一定会有一种“原来后端开发是这么回事”的通透感。

我在实际开发中最大的体会是:前后端分离的本质不是技术上的炫技,而是团队协作模式的变革。你学会了接口设计、联调、部署这一整套流程之后,再去接触分布式、微服务这些高级概念,会发现很多都是从这个基础长出来的。

如果你正在做这个项目,我强烈建议你按下面的顺序完成:

  1. 先花两天时间把数据库建好,所有表、索引、字典数据全部灌进去
  2. 后端先实现登录、申报、审批三大核心接口,用 Postman 自己测通
  3. 前端从登录页开始写,每写完一个页面就和后端联调一次,不要攒到最后
  4. 全部通了以后,再部署到服务器,把前端打包、后端 jar、Nginx 反向代理这些全部走一遍
  5. 最后再挑一个模块(比如多级审批)做增强,面试时重点讲这个

踩过几次坑之后,你会发现自己对报错信息变得非常敏感,看一眼控制台就能猜出问题方向,这种能力比会背任何框架知识点都值钱。项目源码就在手边,随跑随调,祝你把这套系统跑通、吃透,变成自己真正能讲清楚的作品。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询