SpringBoot+Vue+MyBatis校园失物招领系统:前后端分离实战
2026/9/10 3:11:46 网站建设 项目流程

大学里丢过校园卡的人应该都有这种体会:失物招领处永远在一楼值班室角落的纸箱子里,你跑去问的时候,值班阿姨只能让你自己翻。更难受的是捡到东西的人,一张学生卡被扔在桌上,想找失主只能等对方来找。后来我做了这个前后端分离的校园失物招领系统,用SpringBoot+Vue+MyBatis+MySQL这套组合,把“丢失”和“拾获”两条信息流真正打通了。这篇文章会从需求拆解、技术选型、数据库设计、前后端实现到部署上线,把整个项目的完整思路和实操步骤讲清楚。无论你是想完成毕业设计、课设项目,还是第一次接触前后端分离项目想找一份拿来就能跑的源码,这篇都能帮你省掉大量踩坑时间。

1. 从纸箱子到信息流:校园失物招领到底要解决什么问题

1.1 传统失物招领的痛点在哪里

很多校园项目一上来就追求功能多,但做失物招领系统之前,先得想明白一件事:这个系统的真正痛点是什么。

传统模式最大的问题是信息不对称。失主丢了东西,只能靠自己去食堂、图书馆、保安室挨个问;捡到东西的人,也不知道该把东西交给谁。两边都在找,却互相看不见。所以这个系统第一个要做的事情,就是搭一个公共的信息池——丢失信息和拾获信息都往里放,让双方可以搜索、比对、联系。

第二个痛点是认证和信任问题。校园场景里,谁也不能随便把别人捡到的物品拿走。系统里必须有一个身份标识,让用户知道对面是本校学生,同时留联系方式。我设计了最简单的学生认证方式:注册时填写学号和手机号,配合后台管理员的审核操作,既保证门槛不高,又确保信息可追查。

第三个痛点是状态管理。一个物品从丢失/捡拾到最终归还,中间的状态必须清晰:待认领、申请中、已归还、已关闭。没有状态机,列表里全是陈年旧信息,系统就废了。

1.2 系统角色与核心流程梳理

这个系统涉及的参与者比较清晰:普通学生用户、后台管理员。

普通用户的核心操作是四件事:

  • 发布失物信息(我丢了什么)
  • 发布招领信息(我捡到了什么)
  • 浏览和搜索(能不能找到匹配)
  • 提交认领申请(我觉得这条信息对应的就是我的东西)

管理员的核心操作是:

  • 审核用户、管理异常信息
  • 维护物品分类
  • 查看平台整体数据

认领流程需要重点设计。整个闭环是:失主发布失物 → 捡到者发布招领 → 一方发现匹配信息 → 提交认领申请 → 对方确认并联系 → 线下核实身份 → 状态更新。这里面最关键的是让“提交申请”和“确认状态”两个动作在系统里形成闭环,而不是让双方下载手机号后在线下解决,最后连物品状态都没人更新。

1.3 功能模块拆解:不堆大而全,只做够用

我把系统拆成六个模块,不多不少:

模块功能点
用户模块注册、登录、个人中心、学号认证
失物模块失物发布、列表浏览、关键字搜索、分类筛选、状态更新
招领模块招领发布、列表浏览、关键字搜索、分类筛选、状态更新
认领模块认领申请、申请审核、我的申请列表
分类管理物品分类维护(卡类、电子设备、证件等)
后台管理用户管理、信息审核、数据统计

这里有一个设计取舍:很多类似项目会做“失物匹配推荐”“相似物品智能识别”,但对一个校园失物招领系统来说,这些功能投入产出比并不高。我更倾向于做扎实的搜索和分类筛选,让用户自己判断是否匹配。系统本身的定位是信息撮合平台,而不是自动判案系统。

2. 技术栈选型:为什么这套组合是校园项目的最优解

2.1 SpringBoot:一个人也能撑起后端

后端选型是SpringBoot,这个选择基本没有争议。

SpringBoot把Spring繁琐的XML配置全部干掉了,内嵌Tomcat,一个java -jar就能把服务跑起来。对校园项目来说,这意味着你可以把精力集中在业务逻辑上,而不是折腾SSH框架的配置地狱。这个项目我用的版本是2.7.18,对应JDK8,这两个版本组合在兼容性和稳定性上经过了非常充分的验证,不太会出现网上说的各种奇怪问题。

项目里用到的SpringBoot核心能力包括:Spring MVC处理HTTP请求、Spring Security的核心概念(虽然我用JWT做轻量鉴权)、Transactional事务管理、以及自动配置带来的便捷性。

2.2 Vue:页面变化的愉悦感

前端选Vue 2.6,配合ElementUI组件库。Vue的响应式数据绑定让“页面数据变成真”的过程非常自然:用户发一条失物信息,列表页马上就能看到;搜索时输入关键词,列表实时刷新。

Vue的组件化开发在失物招领系统里收益很明显。失物卡片、招领卡片、筛选栏、分页器这些UI模块,在多个页面中反复复用。我把它们抽成公共组件,每次加功能改样式都只动一个文件。

组件化还有一个特别实际的好处:我一个人维护整个项目时,不需要记住所有页面的细节,打开组件文件就能快速定位问题。后来项目扩展到后台管理页面,我只写了一小部分新代码,大部分组件都是直接从用户端复用的。

2.3 MyBatis与MySQL:SQL在手,数据我有

MyBatis在这个项目里的价值可以概括成四个字:SQL可控。Spring Data JPA虽然也很流行,但它自动生成的SQL在复杂多表查询时往往不够直观,出了问题排查很累。MyBatis让你每条SQL都清清楚楚写在XML文件里,性能问题一眼就能看出来。

MySQL作为数据库,对校园项目来说是性价比最高的选择:免费、稳定、生态成熟。我用的是MySQL 8.0,Java生态对它支持非常友好。

2.4 前后端分离与单体开发的分界线

有一次跟同学聊技术,他说:“前后端分离是不是有点过度设计?我一个人开发,为什么不用一个Thymeleaf模板一把梭?”这个问题问得很好。

我的回答是:前后端分离的核心优势不是“让前后端的人可以并行开发”,而是职责边界清晰。后端只负责返回JSON数据,前端只负责展示和交互。调试时后端可以用Postman测接口,前端可以用Mock数据;部署时前端挂Nginx,后端走Java进程,互不干扰。对校园项目来说,分离架构的调试体验好、可维护性强,虽然前期要处理跨域问题,但收益远大于成本。

这个项目里,前后端完全通过RESTful API通信,后端统一返回{code, message, data}结构,前端根据code判断业务是否成功。这是一个非常标准、也极其好维护的约定。

3. 数据库设计:失物、招领、认领三张表如何互相咬合

3.1 用户表与鉴权字段

数据库设计是整个系统最重要的环节,表结构设计得不好,后面所有功能都要返工。我从用户表开始想办法。

用户表(user)我定义了这些核心字段:

字段名类型说明
idBIGINT主键,自增
usernameVARCHAR(50)登录账号(学号)
passwordVARCHAR(100)BCrypt加密后的密码
nicknameVARCHAR(50)昵称
phoneVARCHAR(20)联系电话
head_imgVARCHAR(255)头像图片URL
roleTINYINT0普通用户,1管理员
statusTINYINT0禁用,1正常
create_timeDATETIME注册时间

这里有个容易踩坑的点:密码字段别用明文,别用MD5,直接用BCryptPasswordEncoder。MD5在彩虹表面前形同虚设,BCrypt自带盐值,是目前最合适的选择。

用户表为什么用学号作为登录账号?因为这个系统本身就是封闭的校园场景,学号天然唯一,身份可信度也更高。后面管理员审核资料时,主要也是核对学号和姓名是否匹配。

3.2 失物表和招领表的设计理念

失物表和招领表是系统里最核心的两张业务表,它们的字段设计有很强的对称性——因为本质上是同一种信息的不同方向。

失物表(lost_item)核心字段:

字段名类型说明
idBIGINT主键
user_idBIGINT发布者用户ID
category_idBIGINT物品分类ID
titleVARCHAR(100)物品名称
descriptionTEXT详细描述(物品特征、丢失时间等)
locationVARCHAR(255)丢失地点
imagesVARCHAR(1000)图片URL列表,逗号分隔
contactVARCHAR(50)联系方式
statusTINYINT0待认领,1已找到,2已关闭
create_timeDATETIME发布时间
update_timeDATETIME更新时间

招领表(found_item)结构几乎一模一样,只有语义不同:location字段记录的是“拾获地点”,status状态从“待认领”变为“待认领/已归还/已关闭”。

有一个字段设计上我纠结过很久:为什么用images逗号分隔的方式存多张图片,而不是新建一张图片表?后来想法是:这个系统的单条记录图片数量很少(最多3张),用逗号分隔存一个字段,可以省掉一次关联查询,前端拿到后split(',')就能直接用。如果哪天图片量大了再拆表,对这个系统来说基本不会发生。

3.3 认领申请记录表:一个表打通认领闭环

认领申请记录表(claim_record)是整个系统业务闭环的关键。

字段名类型说明
idBIGINT主键
item_typeTINYINT0失物申请认领,1招领申请认领
item_idBIGINT关联的失物/招领ID
apply_user_idBIGINT申请者用户ID
owner_user_idBIGINT发布者用户ID
messageVARCHAR(500)申请留言(说明物品特征)
statusTINYINT0待审核,1通过,2拒绝
create_timeDATETIME申请时间
handle_timeDATETIME处理时间

这里用item_type + item_id做多态关联,这是很有争议的一个设计决策。严格的关系型数据库设计会用两张表分别记录“失物认领申请”和“招领认领申请”。但我在实际开发中发现,它们的字段几乎一模一样,硬拆成两张表反而让查询逻辑重复。用item_type字段区分,一张表就能支撑两种业务场景。

owner_user_id为什么要冗余存一个?因为查询“我收到的申请”时可以直接按owner_user_id过滤,不用先根据item_type去查失物表或招领表再反查发布者。这个冗余换取的是性能便利,而且数据一致性在业务逻辑层面就控制了——发布者信息本来就是静态的,不会频繁变化。

3.4 索引设计与SQL注意事项

索引设计是数据库设计中最容易被忽略的部分。很多人在表建完之后就把索引忘了,等到数据量上来才发现查询慢得离谱。

这里我做了三个索引:

  • lost_item.create_time:列表页按时间排序,没有索引会全表排序
  • found_item.create_time:同上
  • claim_record.item_type, item_id:联合索引,支撑认领申请查询

另外有一件特别值得注意的事:MySQL 8.0里utf8mb4才是真正的UTF-8完整实现,utf8字符集在存特殊字符时可能出问题。建库时统一用utf8mb4,排序规则用utf8mb4_general_ci就够用了。

4. 后端实现:从接口规划到文件上传落地

4.1 项目结构与统一返回体

后端项目结构我按功能分包,而不是按技术分层分包。这样做的好处是改动一个功能时,相关的Controller、Service、Mapper都在一起,找代码很快。

com.example.lostfound ├── config # 跨域配置、WebMvc配置 ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis Mapper接口 ├── model # 实体类、DTO、VO ├── common # 统一返回、异常、工具类 └── interceptor # JWT拦截器

Result<T>统一返回体是前后端协作的基础。每个接口都返回{code: 200, message: "success", data: ...},前端拿到code !== 200就弹错误提示,逻辑非常简单统一。这里给出一个比较简洁的写法:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

同时配置全局异常处理器,Controller里就不需要到处try-catch了。业务异常直接throw new BusinessException("xxx"),全局处理器统一转成Result返回。这个设计能让业务代码干净非常多。

4.2 登录鉴权:JWT的配置与拦截器白名单

登录鉴权是前后端分离项目里绕不开的话题。Session方案在分离场景下要处理跨域Cookie,太麻烦;我直接用JWT方式,服务端不存状态,每次请求带上Token,后端解析校验身份。

JWT分为三部分:Header、Payload、Signature。Header声明加密算法,Payload放用户ID和过期时间,Signature用服务端密钥签名。我封装了一个简单的JwtUtil工具类,核心方法就三个:生成Token、解析Token、校验是否过期。

@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.expire}") private Long expire; public String generateToken(Integer userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + expire)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret) .parseClaimsJws(token).getBody(); } }

这里有个特别关键的配置环节:拦截器白名单。注册、登录、列表浏览、搜索这些接口必须放行,否则没登录的用户什么都看不了。我在拦截器里维护一个白名单数组,用AntPathMatcher做路径匹配。这是个很细微但非常重要的逻辑。

针对跨域问题,后端配置了CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600); } }

注意allowCredentials(true)会让浏览器在跨域请求时携带Cookie,如果前端用Token方式认证,其实可以不设这个。但保留它有一个好处:后续如果要扩展记住登录状态的Cookie功能,不用再改。

4.3 核心接口设计与MyBatis映射

后端接口按资源维度划分,下面是核心接口清单:

接口路径方法说明
/api/user/registerPOST用户注册
/api/user/loginPOST用户登录
/api/user/infoGET获取当前用户信息
/api/lost/listGET分页查询失物列表(支持关键字、分类筛选)
/api/lost/addPOST发布失物
/api/lost/updatePUT修改失物信息
/api/lost/statusPUT更新失物状态
/api/found/listGET分页查询招领列表
/api/found/addPOST发布招领
/api/found/updatePUT修改招领信息
/api/found/statusPUT更新招领状态
/api/claim/addPOST提交认领申请
/api/claim/myGET我提交的申请
/api/claim/receivedGET我收到的申请
/api/claim/handlePUT处理认领申请
/api/category/listGET获取分类列表
/api/statistics/summaryGET后台统计汇总

以失物列表接口为例,它的MyBatis XML映射值得重点讲。因为这里的动态SQL逻辑很典型:位置有记录、数字类型区分了相关参数,能够表达搜索、过滤与排序组合的场景:

<select id="selectLostList" resultType="com.example.lostfound.model.vo.LostItemVO"> SELECT li.id, li.title, li.description, li.location, li.images, li.contact, li.status, li.create_time, u.nickname AS publisherName, u.head_img AS publisherAvatar, c.name AS categoryName FROM lost_item li LEFT JOIN user u ON li.user_id = u.id LEFT JOIN category c ON li.category_id = c.id <where> <if test="keyword != null and keyword != ''"> AND (li.title LIKE CONCAT('%', #{keyword}, '%') OR li.description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND li.category_id = #{categoryId} </if> </where> ORDER BY li.create_time DESC </select>

LEFT JOIN而不是INNER JOIN,是为了防止用户信息或分类被删除后导致列表查不出来。外键关联字段,宁可显示NULL,也不能让主数据消失。

分页参数我通过PageHelper插件处理,一行PageHelper.startPage(pageNum, pageSize)就能完成物理分页,返回PageInfo直接带总条数、总页数等信息,前端分页组件可以直接用。

4.4 图片上传:本地存储与访问路径配置

图片上传是一个前后端分离项目里非常容易出问题的环节。上传本身不难,难的是让前端能用URL访问到上传后的图片。

我在后端实现了一个简单的图片上传接口:接收MultipartFile,校验类型和后缀名,生成UUID文件名+原后缀,保存到本地磁盘目录,如/data/lostfound/uploads/,返回给前端一个相对路径/uploads/20250601/xxxxx.jpg

同时需要配置静态资源映射,让SpringBoot能直接对外提供这个目录的访问:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadPath + "/"); } }

这里有个容易踩坑的细节:uploadPath结尾一定要带斜杠,否则路径拼接会出错。我一开始把路径配成了/data/lostfound/uploads,结果映射死活不生效,日志排查了半天才发现是少了最后那个/

关于上传,另一个重要经验是前端必须做限制。我配合ElementUI的el-upload组件,设置accept="image/*",并且在before-upload里检查文件类型和大小。前后端双重校验是安全底线,绝不能只依赖前端。

5. 前端实现:Vue页面从路由到组件落地

5.1 Vue项目结构与环境

前端用Vue CLI创建项目,Node版本建议16.x,npm包管理。需要注意:如果直接装了最新版Node,可能和Vue CLI 4.x的webpack版本冲突,最后会弹出各种奇怪的编译错误。项目结构保持标准方式,但按模块划分了视图组件:

src/ ├── api/ # 接口请求封装 ├── assets/ # 静态资源 ├── components/ # 公共组件(失物卡片、筛选栏等) ├── router/ # 路由配置 ├── store/ # 状态管理(简单用户信息) ├── views/ # 页面组件 │ ├── home/ # 首页 │ ├── lost/ # 失物相关页面 │ ├── found/ # 招领相关页面 │ ├── user/ # 个人中心、登录注册 │ └── admin/ # 后台管理页面 └── utils/ # 工具函数

5.2 路由设计:信息流页面怎么组织

路由设计对用户体验影响很大。这个系统的路由我设计成这样:

路径页面是否需要登录
/首页(最新失物+招领信息流)
/lost失物大厅(可搜索筛选)
/found招领大厅(可搜索筛选)
/detail/lost/:id失物详情
/detail/found/:id招领详情
/publish/lost发布失物
/publish/found发布招领
/mine个人中心
/mine/claims我的认领申请
/admin后台管理是(管理员)

路由守卫是前端安全的比较重要的组成部分。虽然后端接口有JWT鉴权,但前端依然要做路由守卫,否则未登录用户访问/publish/lost时会直接看到空白页或报错。用router.beforeEach拦截未登录访问:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.meta.requiresAdmin && token && localStorage.getItem('role') !== '1') { next({ path: '/' }) } else { next() } })

登录后跳转回原页面的redirect参数是个很值得做的小功能。用户没登录时点击“发布失物”,被引导到登录页,登录完成后自动回到发布页面,体验流畅很多。

5.3 axios封装与API对接

前端的API请求封装,所有请求都走同一个axios实例,统一拦截器处理Token注入和响应码判断。这是前端项目中必须做好的基础设施。

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL, 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.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response) { if (error.response.status === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') Message.error('登录已过期,请重新登录') router.push('/login') } else { Message.error(error.response.data.message || '请求异常') } } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default service

重点说一下401处理。JWT过期后,后端返回401,前端如果什么都不做,用户会一直停留在旧页面,点任何按钮都报错。我在拦截器里统一判断状态码,过期后清掉本地登录信息,跳转到登录页,同时弹一个“登录已过期”的提示。这个细节看似不起眼,实际体验差异非常大。

API层每个接口单独封装成一个函数,比如失物模块的请求都放在src/api/lost.js

import request from '@/utils/request' export function getLostList(params) { return request({ url: '/api/lost/list', method: 'get', params }) } export function addLost(data) { return request({ url: '/api/lost/add', method: 'post', data }) } export function updateLostStatus(id, status) { return request({ url: '/api/lost/status', method: 'put', data: { id, status } }) }

5.4 页面组件:卡片列表、筛选栏与发布表单

前端页面我做得最用心的是首页信息流。它同时展示最新失物和招领信息,采用上下两块Tab切换,每块都是卡片列表。

失物卡片是复用率最高的组件,展示标题、分类标签、丢失地点、发布时间,点击进入详情。卡片右上角有个状态标签,不同状态显示不同颜色:红色“待认领”、绿色“已找到”、灰色“已关闭”。这个设计让用户扫一眼就能知道信息的时效性。

筛选栏组件也是高度复用的。失物大厅和招领大厅顶部都有搜索框和分类下拉选择,搜索框支持按物品名称、地点关键字搜索,分类下拉从后端加载分类表数据。筛选条件变化后重新请求接口,同时更新URL的query参数,这样用户刷新页面后筛选条件还在。

发布表单做过三个版本,最后确定的是:表单拆分步骤比单独一个大表单体验更好。

  • 第一步:选择发布类型(失物还是招领)
  • 第二步:填写关键信息(物品名称、分类、地点、详细描述、联系方式)
  • 第三步:上传图片(最多三张,可实时预览)

6. 本地跑通与部署上线:关键步骤与踩坑实录

6.1 环境准备:JDK、MySQL、Node、Nginx

部署整套系统前要准备环境,我假设目标服务器是CentOS 7+或Ubuntu 20.04+,整个过程主要依赖命令行操作。

后端运行环境:

  • JDK 1.8(我强烈建议直接用1.8,SpringBoot 2.7系列对它的支持非常成熟)
  • Maven 3.6+(打包用)
  • MySQL 8.0(数据库)

前端构建环境:

  • Node 16.x + npm(打包用)
  • Nginx(作为静态文件服务器)

启动后端前先确认MySQL已启动,并发起建库操作:

mysql -u root -p CREATE DATABASE lostfound DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

然后导入项目自带的SQL脚本,脚本里包含了建表语句和初始化分类数据。

后端application.yml里面需要改成自己环境的配置:

server: port: 9090 servlet: context-path: / spring: datasource: url: jdbc:mysql://127.0.0.1:3306/lostfound?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: yourpassword mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.lostfound.model configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key expire: 604800000

6.2 后端打包与启动:jar包的一站式运行

环境准备好后,后端打包非常简单:

mvn clean package -DskipTests

打包完成后,在target目录下会生成一个lostfound-0.0.1-SNAPSHOT.jar。直接启动:

java -jar lostfound-0.0.1-SNAPSHOT.jar

本地调试时用上面这种方式就够了。部署到服务器后台运行,我建议用nohup

nohup java -jar -Xms256m -Xmx512m /opt/lostfound/lostfound-0.0.1-SNAPSHOT.jar > /opt/lostfound/log/log.out 2>&1 &

-Xms-Xmx设置JVM初始和最大堆内存。校园项目512MB完全够了,不需要给太多,服务器资源也是钱。

启动后验证接口是否正常,可以执行:

curl -X POST http://127.0.0.1:9090/api/user/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"123456"}'

能返回code: 200就说明后端已经跑通了。

6.3 前端打包与Nginx配置:刷新404的坑

前端打包同样简单:

npm install npm run build

生成的dist目录就是需要部署的静态文件。把它拷贝到服务器,我放在/opt/lostfound/dist目录。

Nginx配置是整个部署过程中最常见的“翻车点”,特别是“前端路由刷新404”的问题。Vue Router如果使用默认的hash模式(URL带#号),不会遇到刷新404,但URL很丑。用history模式的话(URL是干净的/lost路径),直接访问http://域名/lost时Nginx会去服务器上找/lost这个文件,找不到就返回404。解决办法是在location /里配置try_files回退到index.html

server { listen 80; server_name yourdomain.com; root /opt/lostfound/dist; index index.html; # 前端路由history模式必须配置 location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 图片等上传文件访问 location /uploads/ { alias /data/lostfound/uploads/; } }

6.4 部署后最容易翻车的三个细节

部署完成后,前端能访问、后端接口也通了,但很多人在测试过程中会遇到几个隐蔽的问题,我可以分享一下经验。

第一个是数据库时区问题。如果你在数据库的DATETIME字段里存的时间比北京时间少了8个小时,多半是数据库连接URL里缺了serverTimezone=Asia/Shanghai。加上之后重启服务就能解决。这是一个非常典型的时序问题,每次重新配置环境都可能遇到。

第二个是图片显示不出来。前端页面上图片的URL是后端返回的相对路径,比如/uploads/xxx.jpg。开发环境下前端通过Vue CLI构建的代理服务器访问,但生产环境必须保证Nginx配置了location /uploads/指向实际存储目录。这里最容易犯的错误是忘记启动后检查上传目录的用户权限,导致Nginx进程没有权限读取该目录。用chmod -R 755或者将上传目录归属改为Nginx运行用户都能解决。

第三个是跨域配置与Nginx代理叠加的逻辑。当你已经配置了Nginx反向代理后,前端请求/api已经走了Nginx转发,所以浏览器里看到的请求是“同源”的——此时后端CORS配置不是必须的。但本地开发时前端跑在8080端口、后端跑在9090端口,就会出现跨域,所以后端CORS配置仍然要保留。这个“开发时跨域、生产时同源”的双重逻辑,很多人一开始会绕晕。

还有一个小经验:Nginx配置修改后一定要记得nginx -s reload,而不是直接重启整个Nginx服务,否则可能出现短暂的服务中断。配置完直接访问测试接口,确认所有功能正常再收工。


这套系统的完整实现到此就全部梳理完了。我个人在做了这个项目之后最大的感受是:前后端分离不是银弹,但它确实把一个人的项目开发效率提升了不少。后端接口不好用了,直接Postman调;前端样式崩了,打开浏览器DevTools就行,两边不用互相扯皮。如果你打算找一个靠谱的课程设计或毕业设计项目,这套SpringBoot+Vue+MyBatis+MySQL的失物招领系统源码和部署流程可以直接拿去做二次开发,数据库表结构、接口设计、前端组件这些基础都铺好了,你要做的就是往里面填充自己的校园特色功能。如果部署过程中遇到什么问题,可以按这篇文章的排查思路一步步过,大部分问题都出在环境配置和路径映射这两个环节。

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

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

立即咨询