前后端分离项目申报系统实战:SpringBoot+Vue+MyBatis全栈开发
1. 项目拆解为什么这套前后端分离申报系统值得做先说结论凡是想把 SpringBoot Vue MyBatis MySQL 这一整套技术栈串起来的开发者这套“web 项目申报系统”几乎是绕不开的练手题。原因很简单它是典型的业务系统样貌有用户角色有表单提交有审批流转有状态管理有文件上传还有权限控制。你想在简历上写“熟悉前后端分离开发”那你总得有一个拿得出手的完整项目而不是那种只写了登录注册的 demo。这套系统解决的核心问题用大白话说就是把“项目申报”这件事从线下表格搬到线上让申报人、审批人、管理员各干各的活数据统一落到数据库里流程一眼看得清。你需要的技术栈恰好是当前 Java 生态里最主流、就业市场最认可的那一套后端用SpringBoot做接口服务简化配置快速起项目持久层用MyBatisSQL 自己掌控复杂的多表查询写起来不憋屈前端用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很多人问我学了 SSMSpring 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 与 MySQLSQL 掌控感是核心持久层选 MyBatis 而非 JPA/Hibernate核心原因是对于申报系统这种多表关联、条件查询比较多的场景手写 SQL 能做到最直观的掌控。比如“按申报人姓名模糊查询 按状态筛选 按申报时间排序”这种动态 SQL 在 MyBatis 里用if标签拼起来非常顺手你在 XML 里能一眼看出最终执行的 SQL 长什么样子。而在 JPA 里你要去猜方法名、去理解自动生成的 SQL出了问题反而不容易定位。MyBatis 的核心知识点这个项目里全都会用到Mapper 接口 XML 映射接口定义方法XML 写 SQL参数传递Param注解实现多参数传递动态 SQLwhere、if、foreach处理条件查询和批量操作ResultMap处理数据库字段create_time和 Java 实体属性createTime的驼峰映射一级缓存 / 二级缓存理解 MyBatis 的缓存机制避免查询脏数据再说 MySQL这个项目用到的表结构不算复杂但涉及多张表关联用户表、项目申报表、审批记录表、附件表、通知表等等。设计的时候每张表都要明确主键策略、索引、外键逻辑。MySQL 的安装和配置如果你还没搞定我强烈建议你用MySQL 8.0.x记得下载 MSI 安装包不推荐绿色版密码认证方式注意选caching_sha2_password同时在连接 URL 里加上useSSLfalseserverTimezoneAsia/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和JWTJSON Web Token。我强烈建议这个项目用 JWT前后端分离架构下Cookie 处理跨域比较麻烦而 JWT 用请求头Authorization携带 token非常干净JWT 天然适合无状态服务后端不需要保存会话信息重启服务也不会导致用户掉线登录成功后前端把 JWT 存在 localStorage 或者 Pinia 里每次请求通过 axios 拦截器带上一个典型的 JWT 流程是这样的用户访问登录接口提交用户名和密码后端校验通过生成一个 JWT里面包含用户 ID、用户名、角色等用 secret 签名前端拿到 JWT保存起来此后每次请求都带上Authorization: Bearer token后端的拦截器解析 token校验有效性并把用户信息放到请求上下文里如果 token 过期或非法返回 401前端跳回登录页这里我要特别提醒一个坑JWT 的 secret 一定不要用简单的字符串如 “123456” 放在代码里即使这是一个学习项目你也要养成从配置文件中读取的习惯。密码加密同理不要用明文用 Spring Security 的 BCrypt 或者至少用 MD5 加盐不过我更推荐 BCrypt安全等级不是一个级别。3. 实操断层拆解从建库建表到核心接口的实现细节3.1 数据库设计申报系统的表结构应该怎么设计在我做过的很多类似系统里第一件事永远是设计表结构。表结构设计得好后面所有代码都写得很顺如果一开始草率后面查数据、改需求的时候你会想骂人。一个完整的项目申报系统至少需要这几张核心表表名说明重要字段sys_user用户表id, username, password, real_name, role, dept_name, statusproject_declare项目申报表id, project_name, project_type, applicant_id, budget, begin_time, end_time, content, statusapproval_record审批记录表id, project_id, approver_id, opinion, result, create_timesys_file附件表id, biz_type, biz_id, file_name, file_url, create_timebase_dict数据字典表项目类型等id, dict_type, dict_label, dict_value面有三点必须注意主键统一用bigint自增或者雪花ID不要图省事用 int项目一大数据量上来会吃紧。这个体量无所谓但你养成习惯就好凡是外键逻辑字段加索引例如applicant_id、project_id因为你会频繁按这些字段去关联查询状态字段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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意几个细节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 ResultT { private Integer code; // 200成功500失败 private String message; // 提示信息 private T data; // 数据 }并且提供一个静态方法方便返回public static T ResultT success(T data) { ResultT 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 groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency然后在 Service 中使用PageHelper.startPage(pageNum, pageSize); ListProjectVO list projectMapper.selectProjectList(query); PageInfoProjectVO pageInfo new PageInfo(list);这里要注意PageHelper.startPage必须紧跟需要分页的那条查询语句中间千万不要执行其他 SQL 查询否则分页会作用到错误的查询上。我踩过这个坑调试了半天最后发现是先读取了字典表再查询列表导致分页错乱。3.5 JWT 拦截器实现十分钟写好的关键防线在 SpringBoot 里实现权限控制最简便的方式是编写一个 HandlerInterceptorpublic 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; } } }然后注册到 WebMvcConfigurerConfiguration 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 ResultString 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/listNginx 都统一分发到index.html才能正确渲染。4. 完整部署教程从源码到线上可访问4.1 打包前端Vue 项目的产物是什么前端最终交付的是一堆静态文件HTML、JS、CSS。打包命令npm run build打包完成后生成dist目录里面的文件包括index.htmlassets/目录打包后的 js/css 文件名称会带 hash如index.abc123.js你需要把整个dist目录内容上传到服务器的 Nginx 配置的root目录下。有一个常见问题为什么我服务器上页面白屏很大概率是因为路由模式问题。Vue Router 默认是createWebHistoryhistory 模式这种模式下刷新非根路径页面会 404解决方式有两个改路由模式为createWebHashHistoryhash 模式URL 会多个#但稳定或者在 Nginx 配try_files $uri $uri/ /index.html;我更推荐第二种因为 URL 好看体验也好。4.2 打包后端SpringBoot 三种打包方式SpringBoot 后端打包有几件事要说清楚。如果你在 pom.xml 里配了build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build执行mvn package就能打出一个可执行 jar 包里面内嵌 Tomcat直接运行java -jar project-system.jar --spring.profiles.activeprod如果是传统方式打成 war 包扔进外部 Tomcat那就需要修改启动类继承 SpringBootServletInitializerSpringBootApplication 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你需要按顺序安装JDK 1.8 或更高MySQL 8.0NginxJDK 安装简单用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.sql4.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] DescriptionProject System Afternetwork.target [Service] Userroot WorkingDirectory/opt/app ExecStart/usr/bin/java -jar /opt/app/project-system.jar Restartalways RestartSec10 EnvironmentSPRING_PROFILES_ACTIVEprod [Install] WantedBymulti-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 连接与时区问题典型报错 1java.sql.SQLException: Could not create connection to database server.多数是因为 MySQL 8.x 驱动和连接 URL 参数的问题。解决办法是在application.yml里写全参数spring: datasource: url: jdbc:mysql://localhost:3306/project_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruecharacterEncodingutf8mb4allowPublicKeyRetrievaltrue是 MySQL 8.0 使用caching_sha2_password插件时的必要参数不加会偶发连接失败。这个坑网络教程里十有八九不会提。典型报错 2Public Key Retrieval is not allowed就是上面的问题。记住三个参数一起加useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue。5.2 MyBatis 一堆奇怪的 bind、Invalid bound statement 错误如果你启动 SpringBoot 后调用 mapper 时报Invalid bound statement (not found): com.example.project.mapper.ProjectMapper.selectList这根本原因多半是两个接口和 XML 的 namespace 对不上。检查 XML 文件里的 namespace 是否等于 Mapper 接口的全限定名XML 没有被打包进 classpath。Maven 默认只把 src/main/resources 下的 XML 打进包里你的 mapper XML 如果放在 src/main/java 目录下就不在扫描范围内。解决方法很简单在 pom.xml 中配置resources resource directorysrc/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.entity5.3 MyBatis 查询返回字段全是 null这是老生常谈数据库字段明明是create_timeJava 属性是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三步排查Nginx 是否把文件和静态资源路径分开代理如果后端接口和文件都走/api/那是你自己把资源路径配置错误SpringBoot 是否配置了资源映射addResourceHandlers有没有匹配到/file/**文件是否写入了你预期上传目录检查绝对路径很多新手把相对路径写进去当前的工作目录一变就 4045.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-serverutf8mb4连接 URL 再加characterEncodingutf8mb4数据库字段类型用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: 300005.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 拦截器 getMyBatis 批量插入报 SQL 语法错误foreach 写错检查foreach的 separator、open、close 是否配对Vue 打包后白屏base 路径或路由模式问题设置 base: ./ 或 Nginx try_files后端启动太慢扫描包范围过大精确设置SpringBootApplication(scanBasePackages)这个表建议收藏遇到问题先查这里大多数情况能省下不少时间。7. 一些真正提升“完成度”的进阶建议当你把基础版本完整跑通之后可以按优先级做这些增强每一个都能让你的项目在考核、面试中有质的提升。7.1 加入 Spring Security 的 BCrypt 密码加密这个优先级最高。现在的系统如果还是明文密码存储面试官一眼就会否定你的安全意识。方案是引入 Spring Security 的 crypto 模块只用它来做加密不引入完整的安全框架避免复杂化dependency groupIdorg.springframework.security/groupId artifactIdspring-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. 我最后想分享的几句话这个项目做完你一定会有一种“原来后端开发是这么回事”的通透感。我在实际开发中最大的体会是前后端分离的本质不是技术上的炫技而是团队协作模式的变革。你学会了接口设计、联调、部署这一整套流程之后再去接触分布式、微服务这些高级概念会发现很多都是从这个基础长出来的。如果你正在做这个项目我强烈建议你按下面的顺序完成先花两天时间把数据库建好所有表、索引、字典数据全部灌进去后端先实现登录、申报、审批三大核心接口用 Postman 自己测通前端从登录页开始写每写完一个页面就和后端联调一次不要攒到最后全部通了以后再部署到服务器把前端打包、后端 jar、Nginx 反向代理这些全部走一遍最后再挑一个模块比如多级审批做增强面试时重点讲这个踩过几次坑之后你会发现自己对报错信息变得非常敏感看一眼控制台就能猜出问题方向这种能力比会背任何框架知识点都值钱。项目源码就在手边随跑随调祝你把这套系统跑通、吃透变成自己真正能讲清楚的作品。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →