前后端分离的校园失物招领系统,算得上是Java全栈练手里非常典型的场景了。SpringBoot做后端接口、Vue做管理页面、MyBatis操作MySQL,一套下来覆盖了增删改查、文件上传、权限区分、跨域联调、打包部署这些日常开发躲不开的环节。我最近完整过了一遍这套源码,把从数据库建表到前端调通再到服务器部署的整个过程踩的坑都记了下来,这篇就按实际项目落地顺序,把核心设计和操作细节拆开讲清楚。
1. 系统设计与架构拆解
1.1 为什么选前后端分离而不是传统JSP
校园失物招领这类系统,核心就是两类人用:丢东西的学生要登记寻物启事、捡到东西的学生要发布失物信息,管理员则负责审核和匹配。业务不算复杂,但涉及的状态流转、信息匹配、图片存储这些需求,用传统JSP+Servlet硬怼也能做完,但后续想加功能、换页面样式会非常痛苦。
前后端分离的核心好处在于职责边界清晰。后端只负责业务逻辑和数据接口,接口返回固定格式的JSON;前端只负责页面渲染和交互,通过Axios调接口拿数据。这样带来的直接收益是:做毕设答辩的时候,前端展示和后端逻辑可以分开讲;接手别人代码的时候,问题定位也快得多。对校园场景还有一个很实际的点,学生用户主要用手机浏览,管理端在电脑上操作,分离架构天然适配多端场景——同一套后端接口,可以同时支持移动端H5和PC管理页面。
另一个容易被忽视的点是部署方式的灵活性。传统JSP项目必须丢进Tomcat的webapps目录,前后端分离后,后端只是一个SpringBoot的jar包,前端构建后是纯静态文件,可以扔到Nginx里,也可以挂到任意静态托管平台。数据库独立部署,三个组件各管各的,出了问题不会互相拖累。
1.2 技术选型的取舍逻辑
这套系统的技术栈组合其实很有讲究,每个选择背后都有明确的理由:
| 组件 | 选择 | 理由 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.x | 相比Spring MVC需要大量XML配置,SpringBoot的自动配置和起步依赖能把项目初始化时间压缩到几分钟 |
| ORM框架 | MyBatis | SQL由自己掌控,多表联查、动态条件筛选时尤其好用,比JPA更直观 |
| 数据库 | MySQL 5.7 / 8.0 | 免费、稳定,失物招领这类读多写少的场景完全够用 |
| 前端框架 | Vue 2 + Element UI | 组件化开发效率高,Element UI自带表格、表单、弹窗、上传组件,做管理后台几乎是开箱即用 |
| 构建工具 | Maven + npm | 后端依赖管理和前端包管理各司其职 |
SpringBoot版本这里特别提醒一句:别一上来就选最新版。我见过太多人直接用SpringBoot 3.x,结果MyBatis的starter、一些老教程里的写法全都对不上,光是兼容性问题就够折腾半天。这套系统用稳定且资料最多的2.7.x最稳妥。原因在于SpringBoot 3是基于Jakarta EE的,包名从javax换成了jakarta,MyBatis官方适配也经历了版本更迭,对新手非常不友好。2.7.x这一代资料多、坑少,作为学习项目是最优解。
1.3 功能模块的边界划分
失物招领系统表面上功能不多,但设计时要考虑角色权限和业务闭环。整个系统我拆成了五个核心模块:
- 用户模块:学生注册/登录、个人信息维护,密码加密用MD5加盐处理
- 失物登记模块:发布寻物启事(丢的东西长什么样、在哪丢的、联系方式)、发布失物招领(捡到的东西、现在存放位置)
- 审核管理模块:管理员审核信息,决定是否在前台展示,避免虚假信息和恶意发布
- 匹配与认领模块:搜索匹配、发起认领申请、确认归还、完成状态归档
- 公告与统计模块:站内公告发布,物品分类统计、认领率统计,给管理员做数据参考
模块之间通过状态字段串联。比如一件失物从"待审核"到"已发布"到"认领中"再到"已完成",这个流程在后端用枚举值控制,前端根据状态值渲染不同的标签颜色和操作按钮。这样设计的好处是代码逻辑清晰,CURD各司其职,答辩时也方便画业务流程图。
2. 核心实现细节与开发链路
2.1 数据库表结构怎么设计才合理
失物招领系统的数据表设计,我实际建了6张核心表。最关键的是失物信息表,字段设计充分考虑了实际场景:
CREATE TABLE `lost_item` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL COMMENT '标题', `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '类型:0寻物 1招领', `item_name` varchar(50) NOT NULL COMMENT '物品名称', `description` text COMMENT '详细描述', `location` varchar(100) DEFAULT NULL COMMENT '丢失/拾获地点', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0待审核 1已发布 2认领中 3已完成 4已下架', `image_url` varchar(255) DEFAULT NULL COMMENT '图片地址', `contact` varchar(50) DEFAULT NULL COMMENT '联系方式', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_type_status` (`type`, `status`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个容易踩的坑。第一个是字符集必须用utf8mb4而不是utf8,不然用户发布的内容里带个emoji表情就直接报错"Incorrect string value"。第二个是时间字段用datetime而不要用varchar,虽然看起来存什么都可以,但后续做"三天内新发布的失物"这种时间筛选时,varchar就无能为力了。
联合索引idx_type_status是我实际优化过的。最初没加索引的时候,前端页面按类型筛选列表,数据量到几千条就有明显的延迟,加上联合索引后查询基本是毫秒级。这个经验放到面试里也很有说服力。
2.2 后端接口设计:状态机驱动业务流转
后端接口我用RESTful风格统一设计,基础路径就是/api/lost、/api/user、/api/admin这样按资源划分。所有接口返回统一的数据结构,Rest风格的完整定义如下:
@Data public class Result<T> { private Integer code; // 200成功 400业务错误 401未登录 500系统错误 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } }我特意要求所有接口都返回这个结构,是因为前端Axios拦截器可以统一处理错误码,不用每个页面都写一遍错误判断。前端拿到code不为200就直接弹Toast提示,代码能少写很多。
业务上最关键的是发布失物接口的完整校验链路。我贴一下核心逻辑骨架:
@PostMapping("/publish") public Result<?> publish(@RequestBody LostItem item, @RequestAttribute("userId") Integer userId) { // 1. 参数基础校验:标题、物品名称、联系方式必填 if (StringUtils.isBlank(item.getTitle()) || StringUtils.isBlank(item.getItemName())) { return Result.error(400, "标题和物品名称不能为空"); } // 2. 校验联系方式格式,手机号或QQ号简单正则匹配 // 3. 类型参数校验,只能传0或1 // 4. 如果传了图片,检查文件大小和格式(JPG/PNG,不超过5MB) // 5. 组装实体,状态置为0(待审核),存入数据库 return Result.success(null); }这套校验逻辑看起来繁琐,但能挡住大量脏数据。实际运营中最大的问题是联系方式乱填和信息描述太短让人看不懂,前端表单做了一次校验,后端必须再做一次。前端校验防君子,后端校验防小人——如果有人绕过前端直接调接口写垃圾数据,至少后端能挡住。
认领流程我设计了认领申请表,用户对某条失物信息发起认领申请时必须填写"物品特征描述"作为凭证,管理员比对后确认是否通过。这个设计既模拟了真实的招领流程,又给系统加了审核业务,功能和功能之间有了联动——比单纯的增删改查高级很多,答辩时也更有东西可讲。
2.3 MyBatis动态SQL:筛选查询的核心利器
这个系统的列表页筛选条件非常多:按类型、按状态、按物品关键词、按时间范围、按地点。如果每种组合都写一条SQL,那代码量直接爆炸。MyBatis的动态SQL正好解决这个问题。以分页查询为例:
<select id="selectPage" resultType="com.example.entity.LostItem"> SELECT * FROM lost_item <where> <if test="type != null"> AND type = #{type} </if> <if test="status != null"> AND status = #{status} </if> <if test="keyword != null and keyword != ''"> AND (item_name LIKE CONCAT('%', #{keyword}, '%') OR description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="location != null and location != ''"> AND location LIKE CONCAT('%', #{location}, '%') </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理掉第一个AND,多条件组合时不会出现SQL语法错误。分页这里我没引入PageHelper插件,直接用MySQL的LIMIT做物理分页,因为系统数据量不大,没必要多引入依赖。
这里要吐槽一个常见问题:很多人实现搜索功能时直接用${}拼接字符串,这极其危险,会有SQL注入漏洞。比如传入的keyword是' OR '1'='1,整个表的记录都会被查出来。用#{}预编译能彻底规避这个问题,这是MyBatis的基本素养,也是面试必问的点。
MyBatis缓存这个点很多人理解浮于表面,正好这个系统可以验证一下。MyBatis一级缓存是SqlSession级别的,同一个SqlSession内执行相同的两个查询,第二次直接走缓存,这在同一个事务里能省一次数据库查询。但注意:只要执行了增删改操作,一级缓存就会失效,这就是MyBatis的做法,防的是缓存了脏数据。二级缓存默认不开启,涉及到跨SqlSession共享,还要考虑脏读问题,我个人建议是学习阶段保持默认关闭状态即可,把精力放在业务上。这个点面试官问到的时候你能讲清除"一级缓存何时失效",已经能证明你不是背八股文。
2.4 手写拦截器实现登录鉴权
登录鉴权这块,我用的是**JWT(JSON Web Token)**方案,没有引入Spring Security,核心原因是不想给学习项目增加太多复杂度。Spring Security那套过滤器链、UserDetailsService、权限表达式,理解成本很高,对失物招领这种轻量场景属于杀鸡用牛刀。
实现方式很直接:登录成功后,后端生成一个token返回给前端,前端每次请求在Header里带上。后端写一个拦截器统一校验:
@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { throw new BusinessException(401, "未登录"); } try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { throw new BusinessException(401, "登录已过期"); } } }这里有个技术细节容易踩坑:我用的jjwt版本是0.9.1,它依赖javax.xml.bind.DatatypeConverter,在高版本JDK(9+)里这个类被移除了。解决办法有两种:一是引入jakarta.xml.bind-api依赖,二是像我这样用0.11.5以上版本,API写法略有不同(用Keys.hmacShaKeyFor()构建密钥),但不需要额外加依赖。如果你用JDK8,倒是无所谓选哪个版本。
拦截器配置里要注意放行白名单。登录接口、注册接口、验证码接口、前台的公开查询接口都需要放行,否则用户还没登录就连登录页面都进不去。这个顺序搞错会导致非常诡异的"登录接口返回401"问题,排查半天才发现是拦截器把自己拦了。
2.5 前端到底怎么组织才不乱
Vue项目我最怕看到的就是所有页面逻辑全部堆在一个巨型组件里,一个文件几千行,加个功能找不到改哪里。这套失物招领管理后台,我按照**"视图-组件-API"三层结构**来组织:
src/ ├── api/ // 所有后端接口调用统一封装 │ ├── lost.js // 失物相关接口 │ ├── user.js // 用户相关接口 │ └── admin.js // 管理相关接口 ├── router/ // 路由配置,含路由守卫 ├── views/ │ ├── home/ // 首页 │ ├── lost/ // 失物招领列表、详情 │ ├── publish/ // 发布信息 │ ├── user/ // 个人中心 │ └── admin/ // 管理后台 ├── components/ // 通用组件 │ ├── ImageUpload.vue // 图片上传组件 │ └── StatusTag.vue // 状态标签组件 └── utils/ └── request.js // Axios封装api目录单独拆出来的价值是:接口地址集中管理。后端接口变更时只改一个文件,不用全局搜索替换,多人协作时也能减少合并冲突。
路由守卫也是一个关键点。Vue Router的beforeEach钩子里去判断用户角色和登录状态:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path.startsWith('/admin')) { const role = localStorage.getItem('role') if (role !== '1') { next('/login') } else { next() } } else if (!token && to.path !== '/login') { next('/login') } else { next() } })如果你的路由用的是Vue Router 4.x,注意beforeEach和next的用法有变化——Vue Router 4里不强制调用next(),可以直接返回目标路径。我遇到过一个线上问题,就是前端路由守卫里用了next(),控制台一直警告"next was called multiple times",排查发现是某个版本的一个路由匹配失败、另一个路由又触发守卫,循环了。
vue.config.js的代理配置是最容易出错的地方,也是最值得讲清楚的。开发环境下前端页面跑在localhost:8080,后端接口在localhost:8081,浏览器直接跨域。最简单的处理是配置devServer代理:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8081', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这里pathRewrite要不要写,取决于后端接口实际路径有没有/api前缀。我的后端统一用了/api前缀(通过server.servlet.context-path配置),所以不需要rewrite。如果后端没有配置context-path,而前端代理只匹配了/api开头的路径,就需要rewrite把前缀去掉。这两种我都踩过,最后选了后端加context-path方案,前端代理逻辑最简单,接口一眼能看出哪些是走代理的。
Vue组件通信这个点也是实际开发中绕不开的。图片上传组件和表单组件之间,我是用$emit把上传成功的URL抛给父组件,父组件把URL存进表单数据里。如果层级嵌套很深,就要考虑Vuex或EventBus。这个系统层级不深,用$emit就够了。这里有必要补充一句:新手写Vue,最常见的错误就是什么都放data里,然后到处用$refs改来改去。组件化和数据流的设计,宁可多花半小时规划,也别动手就写。
2.6 图片上传方案实现
校园失物招领里图片是刚需——丢的东西长什么样、捡到的东西放哪,没有图很难描述清楚。我的方案是后端写一个通用的文件上传接口:
@PostMapping("/upload") public Result<String> upload(MultipartFile file) { // 1. 校验文件是否为空 // 2. 校验文件大小,限制5MB // 3. 校验文件后缀,只接受jpg/png // 4. 用UUID重命名文件,避免文件名冲突 // 5. 保存到服务器本地指定目录 // 6. 返回可访问的URL地址 }注意事项是:上传目录要独立配置,不能随便写死绝对路径,不然后面部署到服务器要改代码重新打包。正确做法是在application.yml里配置一个上传路径属性,部署时通过外部配置覆盖,这样换环境就不用改代码。存储到本地磁盘的方式在单机部署下完全够用,但如果以后扩展成正式项目,可以考虑OSS对象存储,原理都是返回一个URL,前端无感。
3. 环境搭建与完整部署实录
3.1 开发环境准备:JDK、MySQL、Node这些坑怎么避
这套系统的开发环境,我列一下实际用的版本组合,尽量和前文保持一致,避免不同版本间互相扯皮:
- JDK 1.8
- Maven 3.6.3
- MySQL 5.7.44(Windows下安装教程很多,核心是记住root密码)
- Node.js 16.x(Vue 2项目最好用16,Vue 3可以在18+上跑)
- 开发工具 IDEA + VSCode(IDEA开发后端,VSCode开发前端,也可以都堆在IDEA里)
MySQL安装的说法我已经说了很多遍了,这里只强调最关键的几点。MySQL 5.7.44的Windows安装解压版比安装版更省心——下载zip包解压后,以管理员身份运行cmd,进入bin目录,执行mysqld --initialize-insecure生成data目录(免密码),再执行mysqld install注册服务,net start mysql启动服务,最后mysql -u root -p回车直接进。这个流程我走了无数遍,比安装版少踩很多坑。
SpringBoot版本兼容性是新手最头疼的关卡。我建议直接锁定2.7.18,这是2.x系列的最终版本,修复了大量历史bug,同时保留了2.x的配置习惯。使用3.x的话,不仅要改javax为jakarta,还有一堆starter的坐标命名变了,本来就复杂的学习过程会被版本兼容问题白白占用大量时间。用2.7.18配MyBatis 2.3.x,几乎没有任何兼容性坑。
Node和Vue环境配置你们查到的很多教程都是基于命令行脚手架,其实现在最标准的流程是:
npm install -g @vue/cli vue create lost-found-web cd lost-found-web npm install element-ui --save npm install axios --save npm run serve这里有一个实际常见的坑:国内网络环境下npm install经常超时,要么卡在"idealTree"半天没反应,要么报某个包下载失败。解决方案是配置淘宝镜像:
npm config set registry https://registry.npmmirror.com设置完一定要检查一下npm config get registry确认生效,不然又是漫长的等待。还有一个坑是npm的缓存导致的安装失败,清理缓存用npm cache clean --force,或者直接删掉node_modules重新install。Element UI按需引入还是完整引入的问题,新手阶段我建议直接用完整引入,省配置;熟练之后再考虑babel-plugin-component按需加载优化打包体积。
3.2 项目初始化流程:从零到能跑起来
后端项目的初始化是IDEA里新建Spring Initializr项目,这个流程很标准但有几个关键勾选项:
- 开发语言选Java
- 打包方式选Jar
- Java版本选8
- 依赖勾选:Spring Web、MyBatis Framework、MySQL Driver
生成完项目后需要补两个手动依赖:Lombok(减少实体类getter/setter样板代码)、JWT相关依赖(jjwt)。Lombok在IDEA里必须装插件才能在编译期正常处理注解,这个别漏了,不然满屏飘红。
application.yml的核心配置我用的是最干净的那套:
server: port: 8081 servlet: context-path: /api spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lost_found?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置特别值得讲清楚。数据库字段是create_time这种下划线风格,Java实体类是createTime这种驼峰风格,不开启这个配置的话,每次查询出来的对象的时间字段全是null,让人一度以为是SQL写错了。开启后MyBatis会自动做映射转换,数据库设计和Java实体可以各自保持自己最自然的命名习惯。
MySQL 8.0的驱动类名要改成com.mysql.cj.jdbc.Driver,5.7用com.mysql.jdbc.Driver也行但会有Deprecation警告。URL里的serverTimezone=Asia/Shanghai是必须加的,不然JDBC连接时区报错会导致连接失败,这是一个几乎所有窗口期项目必踩的坑。
MyBatis配置打印SQL这个技巧一定得掌握。开发阶段在配置里加:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样控制台就能打印出执行的SQL语句和参数,排查问题效率翻倍。生产环境再关掉,避免日志刷屏和SQL泄露。这个开关可以放在application-dev.yml里,通过spring.profiles.active=dev切换环境配置,比手动改主配置优雅得多。
前端初始化时的另一个常见报错是failed to load tsconfig '@vue/tsconfig/tsconfig.web.json'——这个坑跟Vue3+TypeScript项目有关,从GitHub拉下来的Vue3+TS项目经常遇到。原因是npm安装依赖时@vue/tsconfig包没有正常安装。解决办法是升级或重装这个包:
npm install -D @vue/tsconfig@latest装完再删除node_modules重新装一遍,这个问题大概率就消失了。如果是自己用Vue CLI创建的项目,默认不会踩到这个,只有从仓库拉代码时才会遇到。
3.3 后端打包与常见坑:jar包跑不起来的N种姿势
开发环境跑通之后,部署环节是大多数人的噩梦。后端打包就一个命令:
mvn clean package -DskipTests生成的jar包在target目录下。然后把jar包扔到服务器上运行:
java -jar lost-found.jar --spring.profiles.active=prod生产环境的数据库地址、账号密码通过--参数或application-prod.yml覆盖,不用改代码。这里必然遇到的坑就是:开发环境的数据库地址是localhost,服务器上跑直接连本机,数据库都没装,所以部署顺序一定要先装好MySQL,再启动后端。
宝塔面板部署SpringBoot是Linux服务器小白的救星,操作流程如下:先把jar包上传到/www/wwwroot下某个目录,然后在宝塔"网站"里添加Java项目,选择jar文件路径、JDK版本、项目端口,启动命令用默认的就行。宝塔会帮你做进程守护,进程崩了会自动拉起,这个能力比裸java -jar强太多。
宝塔部署还有一个高级技巧,就是端口放行。云服务器厂商的安全组要放行8081端口,宝塔面板的安全设置里也要放行,两层都配好外部才能访问。很多人部署完了本地访问不通,八成是安全组没放行,而不是代码问题。
Docker部署SpringBoot也是常见的选择,但如果是新手,我建议先跑通传统的jar部署,再考虑容器化。因为Docker涉及镜像构建、容器网络、数据卷挂载这些额外概念,学习曲线陡峭。宝塔里的Docker管理器确实好用,但每一步都比直接丢jar复杂一些。
3.4 前端打包与Nginx配置实战
前端打包比后端简单:
npm run build构建产物在dist目录,是一堆纯静态文件(HTML/CSS/JS/图片)。部署方式两个主流选择:
- 方式一:Nginx直接托管,适合纯静态部署,配置简单
- 方式二:Nginx托管静态文件 + 反向代理后端API,适合前端页面和后端接口在一起部署的情况
实际项目中我用的是方式二,一个Nginx同时承担静态文件服务和API反向代理,部署结构清晰,一个nginx.conf搞定:
server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /www/wwwroot/lost-found-web/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里最关键的就是前端路由使用history模式时,try_files $uri $uri/ /index.html;这行必须有。因为前端路由跳转是JavaScript控制的,用户刷新/lost/123这个URL时,Nginx得把这个请求指向index.html,让Vue Router接管路由,不然会直接404。这行配置是我排查了很久才搞明白的。
部署完还有一个必做的验证:在服务器上执行curl http://127.0.0.1:8081/api/lost/list,确认后端接口返回JSON而不是错误页面。然后再从外网访问域名或IP验证前端页面加载,最后按F12看Network面板确认API请求没有跨域报错。三层验证走完,部署才算真正成功。
4. 联调阶段的坑与常见问题排查
4.1 前后端联调最经典的跨域问题
前后端联调的阶段最经典的坑就是跨域。开发模式下我已经通过vue.config.js代理解决了,但如果部署时没配好Nginx反向代理,就会出现经典报错:
Access to XMLHttpRequest at 'http://localhost:8081/api/lost/list' from origin 'http://localhost:8080' has been blocked by CORS policy
这个问题的本质是浏览器同源策略——协议、域名、端口任一不同都会被浏览器拦截。后端配置CORS过滤器也算一种解法,但最优雅的还是让前端请求走代理。线上环境用Nginx反向代理,把同源的/api请求转发给后端,浏览器端就没有跨域问题,因为前端页面和请求都是同一个域下的。
有人会用@CrossOrigin注解一个个接口加CORS,我有句话要说:CORS配置的allowedOriginPatterns在SpringBoot 2.4以上版本必须用这个新方法,不能用allowedOrigins("*"),新版中allowedOrigins("*")会被拒绝。如果你看到"Origin is not allowed by Access-Control-Allow-Origin"的报错,一个可能的原因就是版本更新后的这个配置变动。
页面能打开但接口404也是联调常见问题。这种情况大概率是后端context-path没配或者前端代理的pathRewrite写反了。检查的思路是:先在浏览器直接访问http://localhost:8081/api/xxx,如果404,问题在后端的context-path或Controller路由;如果能通但前端访问不通,问题在前端代理配置。
4.2 登录状态失效与Token过期的坑
系统联调时最容易出现的第二轮问题集中在登录状态上。用户登录后,前端把token存在localStorage里,每次请求在拦截器里带上,后端校验通过才能继续。
一个非常容易踩的坑是刷新页面后登录状态丢失。原因是前端把用户信息只存在了Vuex里,页面刷新Vuex重新初始化,数据全没了。解决办法是把用户基本信息也持久化到localStorage,刷新后从localStorage恢复。
还有一个坑是token过期后接口报401,但前端没有任何友好提示。解决方案是Axios响应拦截器里统一处理401状态码:
service.interceptors.response.use( response => { return response.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') router.push('/login') } return Promise.reject(error) } )拦截器里跳转登录页时注意别死循环——如果当前已经在登录页了就不要再跳。这个处理只要稍微粗心一点,就会在登录页和主页面之间无限刷新。
4.3 MyBatis常见问题速查
MyBatis这块的坑相对隐蔽,很多问题报错信息不直观,我把最常见的几个整理出来:
| 症状 | 原因 | 解决办法 |
|---|---|---|
| 实体类字段全是null | 未开启驼峰映射 | 配置map-underscore-to-camel-case: true |
Invalid bound statement (not found) | Mapper接口和XML没绑定上 | 检查mapper-locations配置是否指向正确的XML目录 |
TooManyResultsException | 一条SQL查出多行但接口返回单个对象 | 检查方法返回类型,改成List或加查询条件 |
| SQL注入风险 | 用了${}拼接 | 一律改成#{}预编译 |
| 中文乱码 | 数据库/连接串/页面编码不一致 | 统一用utf8mb4,URL加上characterEncoding=utf8 |
MyBatis的一级缓存和二级缓存我再展开一次。一级缓存是SqlSession级别的,同一个SqlSession执行相同的查询会直接取缓存,但注意:如果两次查询之间执行了任何增删改操作,或者显式调用了sqlSession.clearCache(),一级缓存就会失效。二级缓存是namespace级别的,需要在Mapper XML里配置<cache/>标签,但说实话实际项目中使用要非常谨慎,因为缓存的是查询结果,一旦关联表数据更新而缓存没失效,就会出现脏读。
生产环境强烈建议关闭二级缓存,不要为了面试背的东西盲目开启。面试要展示的是一个"I know when to use it and when to avoid it"的开发者,而不是八股文复读机。
4.4 部署环境与本地环境行为不一致的排查
还有一个很典型的场景,本地跑得稳稳的,一上服务器就各种问题。这类问题首先排查的就是环境差异,几个最常见的来源:
- JDK版本不一致:本地JDK8,服务器JDK17,SpringBoot 2.7在JDK17下编译或运行会出现反射访问报错,虽然能跑但日志里的警告很干扰排查
- MySQL版本不同:本地8.0,服务器5.7,
utf8mb4用法一致但排序规则可能有差异;MySQL 8.0的某些窗口函数或JSON函数在5.7里不支持 - 文件路径分隔符:Windows用
\,Linux用/,上传文件保存路径拼接时不能用硬编码的File.separator,否则服务器上目录结构错误 - 上传目录权限:Linux下Tomcat/Java进程默认用户可能没有写权限,Upload目录要提前
mkdir并设置权限
排查思路有规律可循。先把后端日志打出来(--logging.level.com.example.mapper=debug),重点看SQL执行是否报错;然后确认数据库连接是否正常(SELECT 1测试);确认前端静态资源加载是否完整(F12看Network,重点看404的请求)。按这个顺序排查,绝大多数部署问题都能定位到。
5. 从练手项目到简历项目,还能怎么进阶
项目做完能跑之后,别急着扔一边。作为学习项目和简历项目之间有一道明显的分水岭。在我帮你把这个分水岭拔高之前,先把现在这个状态的价值讲清楚——一个标准的SpringBoot+Vue前后端分离完整项目,有用户角色权限区分、有状态机审核流程、有图片上传、有部署实操记录,这个完整度已经超过了相当一部分毕业设计。
但如果你想让项目在简历或面试中更有份量,还有几个可以低成本接入的优化方向:
第一个方向:引入Redis缓存热点数据。失物招领场景下首页列表是访问最高的接口,可以把热门物品列表缓存到Redis,设置过期时间比如30分钟,数据更新时删除对应缓存。这样面试被问"缓存穿透、缓存雪崩、缓存一致性"时,你能拿这个项目举例,比背定义强太多。
第二个方向:整合消息通知能力。目前系统是用户主动刷新查看审核结果和认领反馈,体验不够主动。可以引入简单的邮件通知:审核通过/拒绝时自动给发布者发邮件。基于JavaMail直接在Service层调用发送,或者后续引入消息队列。这个功能不算复杂但能体现你考虑了"用户没有被及时通知"这个真实痛点。
第三个方向:引入定时统计任务。比如每天凌晨统计失物招领数据(发布量、找回量、找回率),生成日报表,用Spring Task的@Scheduled注解就能实现,无需额外依赖。这个会让你的系统有"运营后台"的雏形,而且代码量很少。
这三个方向都是建立在现有项目之上、不改变系统整体架构就能落地的,进阶成本低、面试收益大。
还有一个小细节值得注意:代码规范和数据校验规范要从一开始就立起来。后端Controller都做参数校验、统一异常处理用@RestControllerAdvice、前端组件目录结构清晰、接口命名恒定为动词+资源名。这些"工程质量感"是面试官看代码仓库时最在意的。
坦白讲,我见过太多人毕设做失物招领、二手交易这类系统,大多停在"能跑"的层面,一旦被追问"为什么数据库用InnoDB""MyBatis动态SQL的底层原理""JWT和Session的优缺点对比"就答不上来。做完项目后的复盘比做项目本身更重要——把每个模块翻出来问自己一遍"为什么这么设计",想不清楚的地方就回去改代码、看源码、做实验。这套完整走下来,你拿到的不只是一份源码和部署教程,而是一套真正内化了的前后端分离项目方法论。
最后我的体会很直白:失物招领这类系统技术难度不拔尖,但覆盖面极广,几乎把Java Web开发的所有核心环节都走了一遍。认真把它做完做透,比草草刷十个半成品项目的收获大得多。遇到报错不要急着百度,先让程序告诉你它哪里不满意,日志看全、断点打好,再动手改。这个过程踏踏实实走下来,调试能力绝对会上一个台阶。