我在学校帮同学做课程设计的时候,见过太多失物招领的需求了。今天想详细聊聊一个基于 SpringBoot+Vue+MyBatis+MySQL 的前后端分离失物招领平台,从技术选型到数据库设计,从后端接口到前端页面,再到最后的部署上线,把整个实现过程拆开揉碎了讲一遍。项目不大,但五脏俱全,非常适合作为毕业设计、课程设计,或者想完整走一遍前后端分离开发流程的练手项目。
1. 项目整体设计与技术选型思路
1.1 为什么选前后端分离架构
失物招领平台的业务逻辑并不复杂,无非是发布失物信息、发布招领信息、提交认领申请、管理员审核这几条主线。但选择前后端分离,并不是因为业务有多复杂,而是为了把分工和后续扩展的路径打开。
前后端分离最直观的好处是职责清晰。后端只需要提供一套 RESTful API,把所有业务逻辑以接口的形式暴露出来;前端通过 HTTP 请求调用这些接口,渲染页面、处理交互。两边可以并行开发,只要提前把接口文档定义好,后端写接口的时候前端完全可以自己造假数据调通页面流程。
另外一个现实原因是部署和运维的灵活性。后端打包成一个 JAR 包,想跑在哪台机器上都可以;前端编译成静态文件,用 Nginx 托管即可。以后如果真的要把系统做成多端——比如再加一个微信小程序或者移动端——前端是完全独立的,后端接口直接复用,不需要改动一行 Java 代码。这种扩展性,是传统的 JSP 或者 Thymeleaf 模板开发模式给不了的。
当然,前后端分离也不是没有代价。最大的问题是开发和调试的复杂度上升了,跨域请求要处理,联调成本也增加了。所以在这个项目里,我会把前后端联调的细节和跨域配置的坑单列出来讲,这些都是实际开发中一定会遇到的问题。
1.2 技术栈选型的逐个拆解
项目标题里明确写了 SpringBoot+Vue+MyBatis+MySQL,这套组合基本是国内 Java 后端项目的主流搭配,每个组件在其中的角色如下表所示:
| 技术组件 | 版本建议 | 在项目中的角色 |
|---|---|---|
| Spring Boot | 2.7.x | 后端核心框架,负责接口开发、业务逻辑、依赖注入 |
| Vue | 2.x 或 3.x | 前端框架,负责页面交互、路由、状态管理 |
| MyBatis | 3.5.x | 持久层框架,负责 SQL 操作和数据库映射 |
| MySQL | 8.0+ | 数据存储,保存用户、失物、招领、认领记录等数据 |
| Maven | 3.6+ | 后端依赖管理和构建工具 |
| Node.js | 14+ | 前端构建环境,用于安装依赖和打包 |
| Element UI / Element Plus | 对应 Vue 版本 | 前端 UI 组件库,快速搭建管理后台和列表页面 |
Spring Boot 选择 2.7.x 而不是 3.x,主要考虑稳定性。2.7.x 是 Spring Boot 2 系列的最后一个大版本,配套资料海量,遇到问题非常容易搜到解决方案。JDK 用 8 或者 11 都可以,别用太新的版本给自己找不痛快。
MyBatis 和 MyBatis Plus 之间,项目选择了原生 MyBatis。虽然 MyBatis Plus 内置了通用 CRUD 和条件构造器,能省不少代码,但对于一个学习型项目来说,自己手写 XML 映射文件反而能帮你把 SQL 的基础功练扎实。失物招领平台涉及不少多表联查和动态 SQL 筛选的场景,自己写一遍,对 MyBatis 的 #{}, ${} 语法、ResultMap 映射、动态 SQL 标签的掌握会提升一个台阶。
Vue 部分,如果选 Vue 2 就搭配 Element UI,选 Vue 3 就配 Element Plus。建议新入手的朋友直接用 Vue 3 + Element Plus,毕竟 Vue 2 已经停止维护了。但如果你是拿这个项目做课程设计,学校环境里 Vue 2 的资料更多,也可以按自己熟悉的方向来。下面讲解中我会以前端为基础的 Vue 3 语法展开,但核心思路与版本无关。
2. 数据库设计与核心业务逻辑实现
2.1 数据表设计的核心思路
失物招领平台的数据建模,核心是四张表加若干张辅助表。我这里把最重要的几张表设计思路讲清楚。
第一张是用户表,字段包括用户 ID、用户名、密码、手机号、邮箱、头像地址、角色、创建时间。密码必须加密存储,项目里用 BCrypt 算法,具体来说就是 Spring Security 里自带的 BCryptPasswordEncoder。这里有一个非常常见的坑:很多同学直接把明文密码放数据库,一旦数据泄露就是灾难性的;而 BCrypt 加密后每次校验只需要调用 matches 方法,使用成本很低,没有理由不用。
第二张和第三张表分别是失物表(lost)和招领表(found)。因为两者属性高度相似,有的人会做成一张表加类型字段区分,但实际开发中我更建议分开建表。原因很现实:失物的核心字段是丢失地点和时间,招领的核心字段是拾获地点和保管位置,属性差异会在后续业务扩展中越来越大,分开建模更清爽。两张表共有的核心字段有:标题、描述、物品分类(校园卡/证件/手机/钱包/其他)、图片 URL、状态、发布时间。
第四张是认领记录表(claim_record),这张表是整个系统的业务核心。它记录的是从“用户申请认领”到“最终确认归还”的全过程。字段包括认领人 ID、关联的失物 ID 或招领 ID、认领说明、状态变更记录、审核人 ID、各状态的更新时间。
值得注意的还有状态设计。失物表的状态我设计为:0-待认领、1-认领审核中、2-已认领、3-已找回并撤销。招领表的状态是:0-待认领、1-被申请认领、2-已完成归还。为什么这么设计?因为失物信息是丢失方发布的,招领信息是拾取方发布的,两个方向的状态流转虽然相似但语义不同,拆开设计会让接口逻辑更符合真实业务场景。
2.2 防冒领机制:状态机与审核流程
失物招领最怕的是东西被冒领。所以系统不能设计成“看到信息就可以一键领取”,必须加入人工介入的审核环节,具体流程如下:
- 拾取方发布招领信息,状态为 待认领(0)
- 丢失方看到招领信息后,提交认领申请,需要填写丢失物品特征、丢失时间地点等备注信息,方便拾取方核实
- 此时招领信息状态变为 被申请认领(1),失物信息状态变为 认领审核中(1)
- 拾取方收到申请后,在系统里查看申请说明,线下核实身份或物品细节
- 核实通过,拾取方点击确认归还,状态变为 已完成(2);核实不通过,驳回申请,状态回到 待认领(0)
这个流程在数据库层面怎么保证数据一致性?关键在于认领记录表的状态和失物招领表的状态需要联动更新。实现上我建议在同一个事务里完成:更新认领记录状态的同时,更新招领表/失物表的状态。任何一步失败就整体回滚,避免出现认领记录显示已认领、但主表还是待认领这种脏数据。
这里还要考虑一个细节:同一件物品能否被多个人同时申请认领?从防冒领的角度,应该允许提交多份申请,因为不同申请人的描述可能都近似,拾取方需要对比后才能判断。所以在认领记录表里,对“同一件招领物品只能存在一条待审核申请”这一点的约束要慎重。我的做法是:允许多条申请记录存在,但每一条的状态独立流转。当拾取方最终确认某一个申请时,系统同时把该物品其他待审核状态的申请自动标记为“已驳回”,保证最终只有一个成功认领者。这个逻辑用 Service 层代码控制即可,不需要过度依赖数据库唯一索引。
2.3 核心表结构的 SQL 参考
用户表和认证相关的 SQL 设计,这里给出关键部分。这一块我在实际给学生讲解的时候都会强调,创建表的时候就把字符集和排序规则定好,不然插入中文直接变成乱码,排查半天发现是表的不对。
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录用户名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `phone` varchar(20) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL COMMENT '头像图片URL', `role` tinyint NOT NULL DEFAULT 1 COMMENT '1-普通用户 2-管理员', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;招领表和失物表的核心字段这里只用招领表做示例:
CREATE TABLE `found_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '发布招领信息的用户ID', `title` varchar(100) NOT NULL, `description` text COMMENT '详细描述', `category` varchar(20) NOT NULL DEFAULT '其他', `location` varchar(100) NOT NULL COMMENT '拾获地点', `pickup_time` datetime DEFAULT NULL COMMENT '拾获时间', `image_url` varchar(500) DEFAULT NULL COMMENT '图片地址,多个用逗号分隔', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0-待认领 1-被申请认领 2-已完成归还', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;一个容易忽略的点:图片字段我用了逗号分隔的字符串来存多张图片的 URL,这个设计在数据量小的时候省事,前端拿到后 split 一下就行。但如果你要做更正规的项目,建议拆一张子表出来存图片。考虑到失物招领的图片量不大,用逗号分隔的方案在课程设计阶段完全够用,这也是一种务实的取舍。
3. 后端核心功能模块实现
3.1 工程结构与统一返回格式
后端的包结构我习惯按照“controller/service/mapper/entity/common”来组织,扩大一点再增加 config、security 包。这里不贴完整目录树了,直接按模块说。
Controller 层只负责接收参数和返回结果,Service 层处理业务逻辑,Mapper 层访问数据库。所有返回结果都包装在一个统一的 Result 类里,格式是 code、message、data。code 用 200 表示成功,401 表示未认证,403 表示无权限,500 表示服务器异常。这样做的好处是前端拦截器只需要判断 code 就能统一处理错误,不需要每个接口单独判断。
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { ... } public static <T> Result<T> success() { ... } public static <T> Result<T> error(Integer code, String message) { ... } }这个统一的返回结构是前后端协作的锚点,一旦定下来,双方都要严格遵循。后端不能今天返回 {code: 200},明天返回 {status: 0},前端会疯掉的。
3.2 登录认证方案:从 Session 到 JWT
失物招领平台的后端认证,我推荐使用 JWT(JSON Web Token)方案,而不是传统的 Session。原因很简单:前后端分离之后,前端可能部署在 A 域名,后端在 B 域名,Session 的 Cookie 跨域问题很麻烦。JWT 把用户信息加密放在 Token 里,前端拿到之后存起来,每次请求放在请求头 Authorization 字段里带过来,后端只需要校验签名就能确认用户身份,完全绕开了跨域 Cookie 的问题。
JWT 的核心逻辑是这样:用户登录成功后,后端生成一个 Token,包含用户 ID、用户名、角色、过期时间,用密钥签名后返回。以后每一次请求,后端从请求头中取出 Token,验证签名和过期时间,如果通过就在请求上下文里放一份当前用户信息。具体实现可以用 Spring Boot 的拦截器(HandlerInterceptor),在 preHandle 方法里完成 Token 解析和校验,配置 WebMvcConfigurer 时将需要鉴权的接口路径加入拦截范围,把放行登录注册接口、静态资源等路径单独排除。
要注意,JWT 有一个天生的问题:服务端无法主动让 Token 失效。如果用户点退出登录,只要 Token 没过期,被人拿到还是能用的。在实际的失物招领平台里,这个问题影响不大,Token 过期时间设短一点(比如 7 天),加上前端退出时删除本地 Token 就够了。如果你要做更严谨的版本,可以引入 Token 黑名单机制,用 Redis 存储已经失效的 Token,但这样就额外增加了部署复杂度,项目初期不建议。
密码加密方面,Spring Security 的 BCryptPasswordEncoder 直接用。注意 BCrypt 每次生成的哈希值都不同,但 matches 方法是可以正确校验的,不要因为两次加密结果不一样就以为出了问题。
3.3 核心接口设计与分页查询实现
项目的主要接口大致如下:
| 接口路径 | 请求方式 | 功能说明 | 是否需要登录 |
|---|---|---|---|
| /api/auth/login | POST | 登录 | 否 |
| /api/auth/register | POST | 注册 | 否 |
| /api/lost/add | POST | 发布失物信息 | 是 |
| /api/lost/list | GET | 分页查询失物列表 | 否 |
| /api/lost/detail/{id} | GET | 失物详情 | 是 |
| /api/found/add | POST | 发布招领信息 | 是 |
| /api/found/list | GET | 分页查询招领列表 | 否 |
| /api/found/detail/{id} | GET | 招领详情 | 是 |
| /api/claim/apply | POST | 提交认领申请 | 是 |
| /api/claim/list | GET | 认领记录列表 | 是 |
| /api/upload | POST | 图片上传 | 是 |
分页查询是这类列表页面的核心。MyBatis 推荐搭配 PageHelper 插件。用法极其简洁:查询前调用 PageHelper.startPage(pageNum, pageSize),紧接着的 Mapper 查询会自动加上 LIMIT 语句,查询结果封装成 PageInfo 对象,里面有 total、pageNum、lists、pages 等完整的分页信息。前端拿到之后直接渲染分页组件。
一个常见错误是 PageHelper 的拦截范围问题。startPage 必须紧跟查询语句,中间不能穿插其他查询操作。比如你在 startPage 之后、第一次 select 之前,又执行了一次别的查询,PageHelper 的插件拦截到的就是上一次查询,分页数据就完全错了。这个坑我们团队第一次做的时候踩过,排查了半天。
3.4 图片上传的本地存储方案
失物招领平台涉及物品图片上传。最省事的方案是买个云存储服务,但考虑到项目部署在自己服务器上,我建议先用本地存储。
实现思路:后端接收 MultipartFile 文件,按照日期生成存储路径,把文件写入到服务器的指定目录,然后把访问 URL 存进数据库。注意两点:
第一,上传目录不要放在项目内部。如果把文件写在 JAR 包同级的 STATIC 目录里,后期系统升级重新部署的时候文件还在,但如果是把文件写到临时目录里,重启就丢文件了。推荐的做法是单独指定一个绝对路径,比如 /data/upload/,通过配置文件注入。
第二,前端上传请求时需要在请求头携带 JWT,同时后端需要适当限制文件大小。Spring Boot 默认单文件上传上限是 1MB,如果你不加配置,稍微大一点的图片上传就会报 MaxUploadSizeExceededException。在 application.yml 里把 max-file-size 调到 5MB 或 10MB,max-request-size 同步调大。
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB3.5 事务一致性与异常处理
认领审核这个操作涉及多个表的更新,必须加上事务。在 Service 方法上加一个 @Transactional 注解,可以确保方法内的所有数据库操作要么全部成功、要么全部回滚。这一点对于实现“确认某个人认领成功,同时自动驳回其他人申请”的逻辑尤其重要——如果分两次更新且没有事务保护,第一次更新成功后第二次失败,数据就处于一个非常尴尬的中间状态。
全局异常处理方面,用 @RestControllerAdvice 加 @ExceptionHandler 统一捕获异常。Controller 里不 try-catch 业务异常,而是直接抛出,由全局处理器统一转换成 Result 对象返回。我在实践中遇到最多的情况是:参数校验失败返回 400,业务逻辑冲突返回 409,其余异常统一返回 500,日志里保留堆栈信息方便排查。
4. 前端页面与对接实现要点
4.1 Vue 工程结构与路由组织
前端部分用 Vue CLI 或 Vite 创建工程都可以。个人更推荐 Vite,因为构建速度快,开发体验流畅。工程结构按页面模块划分,核心目录如下:
- src/api:所有接口请求方法统一放在这里,每个模块一个文件
- src/router:路由配置文件,含嵌套路由和路由守卫
- src/store:状态管理(Pinia 或 Vuex),用于存放当前用户信息
- src/views:页面组件,按照 auth、home、lost、found、me、admin 等目录组织
- src/utils:工具函数,比如 request.js(axios 封装)、auth.js(Token 存取)
路由设计上,首页需要展示失物列表和招领列表,所以路由路径是 /;发布页需要登录后访问,路由守卫里判断没有 Token 就跳转到登录页;后台管理只有角色为管理员的用户才能进入,这个判断在前端路由守卫和后端接口鉴权两个层面都要做,前端拦截只是体验优化,后端才是真正的安全边界。
4.2 axios 封装与 Token 注入
前端和后端交互,最核心的就是 request.js。我给所有请求创建一个 axios 实例,基础 URL 指向后端接口地址,并配置拦截器。请求拦截器统一从 localStorage 里取出 Token,放到请求头 Authorization 字段。响应拦截器统一处理返回值。
import axios from 'axios' const request = axios.create({ baseURL: '/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( response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.message || '请求失败')) } return res }, error => { return Promise.reject(error) } )注意上面 baseURL 我写的是 /api,这是一个很多初学者会忽略的细节。生产环境部署时,前端静态资源由 Nginx 托管,后端 API 在另一个端口或路径。通过 Nginx 配置反向代理,把 /api 开头的请求转发到后端服务器端口,前端代码里只需要使用相对路径,开发环境再单独配置 Vite 的 proxy 转向 localhost:8080。这样的好处是前端代码不用区分环境,打包产物在所有环境都能正常工作。
4.3 核心页面组件设计
首页是列表展示,使用 Element Plus 的 el-card 加栅格布局。每个卡片展示物品标题、分类标签、地点和缩略图。这里前端需要处理几种状态展示:待认领的显示绿色标签,审核中的显示黄色标签,已完成认领的灰色置灰。
发布页面用 el-form 做表单校验,图片上传使用 el-upload 组件,注意配置 http-request 自定义上传逻辑,把 Token 加到请求头,成功后将后端返回的图片 URL 放入表单数据。列表页的分页可以直接用 el-pagination,current-page 和 page-size 与后端接口参数保持一致。
个人中心页面展示我发布的失物、我发布的招领、我提交的认领申请。认领申请列表的每一行需要有查看详情和状态跟踪,如果是管理员,还需要在这里增加审核按钮,实现“通过”或“驳回”操作。这个页面涉及前端组件最多的交互逻辑,按键的 loading 状态、二次确认弹窗(el-message-box)、操作后刷新列表,这些细节虽然琐碎,但直接影响系统的可用性。
4.4 联调阶段的经验与注意事项
前后端联调是整个项目最容易让人崩溃的阶段。我的经验是,先定接口文档,再开发功能。简单的接口用 Postman 或者 Apifox 管理,每个接口的自测通过后,再开始前端对接。
联调中最常见的问题有三个:
第一个是跨域。后端接口地址是 http://localhost:8080,前端开发环境是 http://localhost:5173,如果不做任何处理,浏览器请求会被拦截。解决方式是在后端配置 CORS,允许所有来源访问;或者在前端 Vite 配置走代理,绕开浏览器的跨域校验。两种方式对比,一旦上线走 Nginx 反向代理,前端配置代理更贴近真实生产环境,所以我个人更推荐前端代理方案,但开发调试阶段后端直接开 CORS 会更省事。
第二个是字段名不一致。后端实体类属性是 camelCase(如 pickupTime),前端传 JSON 时用的却是 snake_case(如 pickup_time),导致数据始终为空。联调前要约定好统一使用 camelCase 还是 snake_case,否则排查起来非常痛苦。
第三个是后端返回的数据结构多层嵌套时,前端取数据路径容易写错。比如分页查询返回 { list: [...], total: 100 },前端拿到 response.data.list 用起来没问题;但如果后端在 Result 里再包一层,变成 { data: { list: [...], total: 100 } },前端取值就要多套一层。统一返回值结构,是减少联调摩擦成本的关键。
5. 从本地到云端的完整部署流程
5.1 本地环境搭建与前后端启动
在本机把项目跑起来,是能够继续部署到服务器的第一步。先后端,再前端,顺序很重要。
后端启动前要修改 application.yml 里的数据库连接信息。这里一个容易被忽略的坑是 MySQL 8 的时区问题。连接串必须加上 serverTimezone=Asia/Shanghai,否则会报 CST 时区错误。数据库连接配置如下:
spring: datasource: url: jdbc:mysql://localhost:3306/lost_found?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver先创建数据库:CREATE DATABASE lost_found DEFAULT CHARACTER SET utf8mb4;,然后执行项目里提供的 SQL 脚本导入表结构。我用 Maven 打包时建议先用 mvn clean package,跳过测试:mvn clean package -DskipTests,这样生成的 JAR 包直接 java -jar 启动。
前端启动前 npm install 安装依赖,本地开发 npm run dev。此时浏览器访问前端开发服务器地址,登录后发布一条失物信息,前端通过 devServer 的 proxy 把请求转发到后端 8080,全链路就打通了。
5.2 云服务器部署:从 0 到 1 的完整步骤
将项目部署到云服务器,核心就是做四件事:装环境、导数据、传代码、配转发。
第一步,在服务器上安装 JDK 8、MySQL 8、Nginx、Node.js。JDK 用 apt 或 yum 安装后验证版本,MySQL 设置 root 密码,Nginx 直接默认配置即可。Node.js 只需要在构建前端时用,如果你本地已经能打包出 dist,服务器上甚至可以不装 Node。
第二步,本地导出数据库:mysqldump -u root -p lost_found > lost_found.sql,把生成的 SQL 文件传到服务器,在服务器执行 mysql -u root -p lost_found < lost_found.sql 导入数据。注意导出时加上 --default-character-set=utf8mb4,防止中文乱码。
第三步,把后端的 JAR 包传到服务器任意目录。后端进程要长期运行,直接执行 java -jar 会被终端关闭时杀掉,需要用 nohup 让它在后台运行:
nohup java -jar lost-found-backend.jar --server.port=8080 > app.log 2>&1 &你需要关注 app.log 这个日志文件。启动失败时先看日志,绝大多数问题都能从堆栈信息里定位出来。部署阶段可以先把 8080 端口在安全组里放开,用 curl http://localhost:8080/api/found/list 测一下接口能否访问。
第四步,前端打包。在本地执行 npm run build,生成 dist 目录,把 dist 里面的所有文件上传到服务器上的某个目录,比如 /home/www/lost_found_front。然后配置 Nginx:
server { listen 80; server_name 你的域名或IP; # 前端静态资源 location / { root /home/www/lost_found_front; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传的图片访问 location /upload/ { alias /data/upload/; } client_max_body_size 10m; }这里有一个很关键的地方:location / 里必须写 try_files $uri $uri/ /index.html;。Vue Router 默认使用 history 模式,路由 /lost 这种路径在真实文件系统里是不存在的,如果不做 try_files 回退,刷新页面就会报 404。这个问题我见得非常频繁,很多新手部署完访问首页没问题,一点进详情页再刷新就白屏了,就是这个原因。
Nginx 配置完成后执行 nginx -t 检查语法,无误后 nginx -s reload 生效。此时通过浏览器访问服务器 IP,输入账号密码登录,发布一条招领信息试试图片能否上传。如果图片上传失败,多数是 /data/upload 目录不存在或权限不足,mkdir -p /data/upload && chmod 755 /data/upload 解决。
5.3 服务器安全配置的经验补全
上线之后还要做几件安全加固的事情,虽然项目本身是课程设计级别,但有些基础习惯要养成。
第一,MySQL 不要用默认端口 3306 对外暴露。如果你的服务器安全组把 3306 开给了 0.0.0.0/0,数据库等于裸奔在公网上。正确做法是安全组只放行 80 和 443,数据库只允许本机访问。后端应用和 MySQL 在同一台服务器时,连接地址直接写 localhost 或 127.0.0.1 就行。
第二,后端接口不要以 8080 端口对外暴露。Nginx 已经做了反向代理,8080 端口只需要监听本机。在安全组里不开放 8080,外部流量无法直接打到后端接口,所有请求都走 Nginx 的过滤层。
第三,对后端的一些敏感操作接口做操作日志记录。这个项目里可以简单地加一张操作日志表,在用户发布信息、提交认领、审核认领时记录用户 ID、操作内容、IP 和操作时间。如果出现线下纠纷(比如两个人争同一个失物),日志是排查问题的重要依据。
第四,定期备份数据库。MySQL 的 mysqldump 命令加 crontab 定时任务,每天凌晨备份一次,保留最近 7 天的备份文件即可。整个平台的代码和数据是课程设计或毕设的核心交付物,数据丢了最大的损失其实是你自己。
6. 典型问题与排查技巧实录
6.1 跨域问题
浏览器控制台出现 No 'Access-Control-Allow-Origin' header is present,就是跨域被拦截了。这时检查两个方向:后端有没有配置 CORS,或前端有没有走代理。我当时实际排查过的一个案例是后端配置了 CORS,但前端请求的是一个未被代理的完整 URL(http://后端IP:8080/api/xxx),浏览器直接跨域访问。这种方案在后端配 CORS 时确实能通,但一旦后端接口改为走服务器本机 + Nginx 代理,线上可能就不需要 CORS 了。所以我更建议前端开发环境用代理,线上用 Nginx 反代,后端统一不放行跨域,逻辑更干净。
6.2 MyBatis 常见报错
BadSqlGrammarException 或 Invalid bound statement,大概率是 SQL 写错了或者 XML 映射文件没有扫描到。映射文件放在 resource 目录时确认 MyBatis 的 mapper-locations 配置是否正确指向 XML 文件路径;实体属性和数据库字段的映射不对时,用 ResultMap 指定映射关系,而不是依赖默认的驼峰转换。
6.3 数据库连接失败与中文乱码
后端启动报 Access denied for user,检查数据库账号密码和远程授权。本地连接正常但服务器上连不上的,在 MySQL 里执行 GRANT ALL PRIVILEGES ON lost_found.* TO 'username'@'localhost' IDENTIFIED BY 'password',注意用户 host 部分要匹配应用所在的主机。
中文乱码问题,排查顺序是:数据库表是不是 utf8mb4(不对就转换表字符集)、连接串有没有 characterEncoding=utf8(加上)、前端页面有没有设置 meta charset="utf-8"(加上)。三个地方全部对了基本不会乱码。
6.4 上传文件失败的三种可能
图片上传接口返回 500,检查三个点:上传目录是否存在、目录写权限是否正确、Nginx 的 client_max_body_size 是否够大。我遇到最多的情况是 Nginx 默认 1MB 限制挡住了大图上传,返回的却是 413 Request Entity Too Large。这个错误提示不像跨域那么显眼,但实际影响非常大,配置里加上 client_max_body_size 10m 就行。
6.5 前端白屏或页面路由 404
白屏问题先按 F12 看 Console 有没有报错。最常见的两种情况是:JS 资源路径不对导致加载失败,或者 Vue Router 的 history 模式刷新 404。前者检查打包后的 index.html 中资源引用路径是否是绝对路径 /assets/xxx.js,配合 Nginx 的 root 配置是否对应;后者加 try_files 回退到 index.html。还有一种情况是本地开发正常、打包上线后接口 404,此时检查 baseURL 在开发和生产环境是否走的是同一个 /api 路径,并确认 Nginx 的 proxy_pass 有没有配置正确。
6.6 启动日志直接滚出异常时的排查套路
后端启动时报错,第一步看堆栈第一行的异常类型。数据库连接失败会看到 Communications link failure;端口被占用会看到 Port 8080 was already in use;依赖缺失会看到 ClassNotFoundException。每一个异常信息其实已经把原因写得很直白,新手最容易犯的错误是一眼扫过去全慌,其实逐行读一遍堆栈,问题基本都能定位。
表格式的问题速查:
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| 前端请求 404 | 代理路径写错 / 后端启动失败 | 检查 proxy 配置和 Java 进程状态 |
| 查询列表接口 500 | MyBatis SQL 语法错误 | 查看后台日志定位具体 SQL |
| 登录后接口全部 401 | Token 未传到后端 | 检查 axios 请求拦截器是否注入 Token |
| 图片上传超时 | 文件过大 / 服务器带宽不足 | 调整上传限制,压缩图片 |
| 部署后刷新页面白屏 | Router history 模式未配置 try_files | Nginx 里补上 try_files 回退 |
7. 对后续扩展方向的一些实际建议
这个失物招领平台做完后,其实可以继续做一些非常有价值的扩展,我列几个自己觉得最值得动手的:
第一个是微信小程序端。前后端分离的意义在这里就能体现出来——后端接口完全不动,只需要新写一个小程序前端。小程序端的价值在于,失物招领的核心用户是学校学生或园区员工,他们使用小程序的频率远高于打开浏览器访问网页。发布失物、拍照上传、接收认领审核的消息推送,放在微信生态里会自然很多。
第二个是消息通知模块。目前系统没有做站内通知,用户发布招领信息后,一旦有人提交认领申请,发布者没有任何感知。加一个通知表,在提交申请时生成一条通知记录,前端在导航栏显示未读数量,用户点开即可查看详情。这个功能不复杂,但对系统体验的提升非常明显。
第三个是物品分类的标签化搜索。现在的列表页只有按分类筛选和简单关键词搜索。可以升级为标签系统,给物品打多个标签,搜索时做多条件组合筛选。比如搜索“蓝色钱包 图书馆”,能够同时匹配分类、地点、标题和描述。实现上可以在原有表上加索引,或者引入全文索引优化查询。
第四个是管理后台的数据统计。增加统计面板,展示每日新发布的失物/招领数量、认领成功率、高发物品分类排行、失物丢失高频地点排行。这些统计对学校保卫处或园区物业非常有用,也能让演示和答辩的时候更有说服力。
扩展功能的第一原则是稳。每一轮扩展都先把核心链路跑通,再迭代细节。比如加消息通知时,先保证用户 A 提交申请后用户 B 能收到通知,至于通知已读未读、批量已读、消息列表分页,都是后面锦上添花的事情。
根据我个人带团队做这类项目的经验,最想提醒的一点是:不要一上来就追求大而全。先把一个最小可用的闭环打通——注册登录、发布失物、发布招领、提交认领、管理员审核、状态全流程跑完,把这个闭环上的每一段代码都理解透彻,再考虑加功能。这个项目最适合的定位是一个“麻雀虽小五脏俱全”的完整前后端实践,而不是一个功能堆砌的展示品。把 Spring Boot 的自动配置逻辑、MyBatis 的 SQL 映射机制、Vue 的组件通信、Nginx 的反向代理这些核心原理通过亲手实现理解到位了,你收获的东西比任何花哨的功能都要值钱。