news 2026/9/6 5:33:29

SpringBoot3+Vue3开发失物招领系统:前后端分离实战教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot3+Vue3开发失物招领系统:前后端分离实战教程

站在 2024 年回头看,前后端分离已经不算是新概念了,但真正能让你“毕业后直接上手公司项目”的完整训练,依然少得可怜。如果你正在准备毕业设计,或者想用一个项目把 Java、Spring Boot、Vue 这条链路彻底打通,与其去网上拼凑一堆过时的 SSM 代码,不如自己从零搭一个业务完整、设计合理的失物招领系统。

失物招领不是一个复杂的电商系统,但它恰好覆盖了企业级开发中最常见的能力点:用户认证、物品信息管理、图片上传、状态流转、关键字检索、前后端联调。这些能力拆开看都是面试八股文里的常客,合在一起就是“一个业务系统从 0 到 1 的完整落地”。这篇文章会用 JAVA + SpringBoot3 + Vue.js3 + MySQL 这套组合,把失物招领系统从需求拆解到前后端实现完整走一遍,并且指出哪些地方是新手最容易踩坑的。

我会尽量把文章写成“可以直接照着做”的实战教程,而不是只贴一堆代码。读完你应该能回答这几个问题:为什么用 SpringBoot3 而不是老版本?Vue3 的 Composition API 在实际业务里怎么组织?MySQL 的表结构怎么设计才能支撑失物招领的典型查询?联调时经常出问题的跨域、日期格式、枚举字段又该怎么处理?

1. 这篇失物招领实战真正要解决的问题

很多初级开发者在学完 Spring Boot 和 Vue 的基础语法后,会遇到一个共同的瓶颈:单个知识点都见过,但不知道如何组织成一个可运行的项目。

失物招领系统就是打破这个瓶颈的理想案例。它的业务模型不复杂,不需要像电商那样处理订单状态机、库存扣减、支付回调,但又不至于简单到只有一个 CRUD。一个合理的失物招领系统,至少要处理以下业务规则:

  • 游客可以浏览失物和招领信息,但只有登录用户才能发布信息。
  • 发布信息时,需要填写物品名称、丢失或拾取地点、时间、描述,并上传图片。
  • 失物信息的状态可以流转:待认领、已认领、已归还等;招领信息同样有“待认领”“已被认领”的状态变化。
  • 用户可以在详情页留言,为失主和拾主建立联系渠道。
  • 用户只能管理自己发布的内容,管理员可以处理违规信息。

这些需求放到技术层面,会自然带出 SpringBoot3 的 Starter 自动配置、Spring Security 或 JWT 登录认证、JPA/MyBatis 的持久层设计、Vue Router 的前端路由守卫、Axios 的请求拦截器、Element Plus 的组件化页面开发。

所以,本文真正解决的问题不是“怎么写一个 Hello World”,而是“如何把一个实际业务拆成技术方案,再拆成代码”。如果你能跟着完成一遍,后续做课程设计或接手公司中小型管理系统,会顺畅得多。

2. 技术选型规划:为什么是这套组合

在开始写代码之前,先花一点时间说清楚技术选型的理由。不是越新的技术越好,而是要看这套组合能覆盖多少真实场景。

技术组件版本倾向选择理由
JDK17 及以上SpringBoot3 强制要求 JDK17,这也是当前企业升级的主流方向
Spring Boot3.x官方全新一代框架,支持 Jakarta EE 命名空间,配置更简洁
MySQL8.x主流开源关系型数据库,8.0 版本在窗口函数、JSON 支持上更成熟
MyBatis-Plus3.5.x 或最新稳定版兼顾 SQL 可控性与开发效率,适合管理系统这类偏业务查询的项目
Vue.js3.xComposition API 组合式风格,代码复用性和 TypeScript 友好度更高
Element Plus最新稳定版Vue3 生态事实标准组件库,表格、表单、上传组件齐全
认证方案JWT前后端分离场景最常用的无状态认证方案

2.1 SpringBoot3 的变化点

SpringBoot3 和 SpringBoot2 看起来只是版本号递进,但底层变化非常大。最核心的是javax.*包名换成了jakarta.*。如果你在网上搜到老教程,代码里写的import javax.persistence.*,在 SpringBoot3 项目里是编译不过的,需要全部改成import jakarta.persistence.*

另外,SpringBoot3 内置的 Spring Security 6.x 对配置方式做了调整,很多旧写法已经废弃。对于失物招领这类管理系统,如果不想一开始就在 Security 配置上消耗太多时间,也可以先用 JWT + 拦截器的方式做登录认证,后续再升级 Security。

2.2 ORM 框架选择

失物招领系统涉及用户的发布列表、关键字搜索、按地点和状态筛选,这些查询用 MyBatis 写 SQL 很直观。这里推荐 MyBatis-Plus,因为它在 MyBatis 基础上提供了通用 Mapper 和 LambdaQueryWrapper,单表 CRUD 几乎不用写 XML,复杂查询再手写 SQL,兼顾速度和可控性。

JPA 也能做,而且 Spring Data JPA 在关联查询上有它的优势,但管理系统里复杂条件筛选通常写 SQL 更清晰。不想在相关查询上绕圈子的话,MyBatis-Plus 是更容易上手的方案。

2.3 Vue3 与 Element Plus

Vue3 的 Composition API 带来的最大变化,是把“数据、方法、生命周期”按功能聚合,而不是按选项散落。失物招领系统的发布表单、物品列表、状态标签这些业务组件,用refreactivecomputed组织起来,比 Options API 更直观。

Element Plus 则提供了现成的表格、表单校验、上传组件和消息提示,能避免自己造大量 UI 轮子。项目里会使用el-table展示物品信息,el-form做发布表单,el-upload处理图片上传。

3. 数据库设计:失物招领系统的表结构

数据库设计是这类系统最容易出错的地方。设计不好,后面写查询时会频繁拼接条件,性能差还容易出 bug。下面给出一个满足核心业务且具备扩展性的表结构。

3.1 用户表(sys_user)

字段类型说明
idbigint主键,自增
usernamevarchar(50)用户名,唯一
passwordvarchar(100)BCrypt 加密后的密码
nicknamevarchar(50)昵称
avatarvarchar(255)头像地址
rolevarchar(20)角色:USER / ADMIN
create_timedatetime创建时间

3.2 物品信息表(item_info)

字段类型说明
idbigint主键
item_typevarchar(10)类型:LOST(丢失)/ FOUND(拾取)
titlevarchar(100)物品标题,如“蓝色钱包”
descriptiontext详细描述
placevarchar(100)丢失或拾取地点
happen_timedatetime丢失或拾取时间
image_urlvarchar(255)图片地址
statusvarchar(20)状态:PENDING / CLAIMED / FINISHED
publisher_idbigint发布人 ID
create_timedatetime发布时间
update_timedatetime更新时间

3.3 留言表(item_comment)

字段类型说明
idbigint主键
item_idbigint关联物品 ID
user_idbigint留言用户 ID
contentvarchar(500)留言内容
create_timedatetime留言时间

建表 SQL 可以按下面的脚本执行。这里故意把字符集设为utf8mb4,因为 MySQL 8.0 默认字符集虽然是 utf8mb4,但如果你使用的是旧版本或自定义实例,最好在建库时显式指定,否则中文和 emoji 可能出现乱码。

CREATE DATABASE IF NOT EXISTS lost_found DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE lost_found; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, role VARCHAR(20) NOT NULL DEFAULT 'USER', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE item_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, item_type VARCHAR(10) NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, place VARCHAR(100), happen_time DATETIME, image_url VARCHAR(255), status VARCHAR(20) NOT NULL DEFAULT 'PENDING', publisher_id BIGINT NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_type_status (item_type, status), KEY idx_publisher (publisher_id) ); CREATE TABLE item_comment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, item_id BIGINT NOT NULL, user_id BIGINT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_item (item_id) );

这里要解释一个设计意图:为什么status字段用字符串而不是直接写死中文?因为在前后端分离项目中,前端需要根据状态展示不同标签颜色,后端也需要做状态流转校验。使用统一的英文枚举值,前端再映射成中文或标签,比在数据库里存“待认领”“已被认领”这种中文字段更规范,也更容易扩展。比如后续要增加“已过期”状态,只要加一个枚举值即可。

另一个关键设计是item_typestatus的联合索引。失物招领首页最常见的操作是“按类型浏览”,例如只看失物或只看招领,同时按状态筛选。这个联合索引能直接命中查询。

4. SpringBoot3 后端环境搭建与核心流程

后端环境相对简单。只要你本机安装了 JDK17 和 Maven,再准备一个 MySQL 8.x 实例即可。IDE 推荐 IntelliJ IDEA,社区版也够用。

4.1 创建 SpringBoot3 项目

如果你不熟悉 Spring Initializr,可以直接访问 start.spring.io,或者使用 IDEA 自带的 Spring Initializr。项目坐标建议取com.example.lostfound,关键依赖选择:

  • Spring Web
  • MySQL Driver
  • Lombok
  • Validation

MyBatis-Plus 需要手动添加依赖,因为默认的 Initializr 没有把它列在选项里。在pom.xml中加入:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency>

注意,MyBatis-Plus 老版本的 starter 是基于javax.*的,SpringBoot3 项目必须使用带spring-boot3标识的依赖坐标。这里是一个容易踩的版本坑。

4.2 配置文件

application.yml是后端最核心的配置文件。需要配置端口、数据库连接、MyBatis-Plus 逻辑删除和 JSON 序列化规则。

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/lost_found?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

这里重点说明日期格式配置。前端 Vue 展示时间时,如果后端返回的是时间戳或默认的 ISO 格式,处理起来会很麻烦。统一配置jackson.date-format和时区,可以让接口直接返回yyyy-MM-dd HH:mm:ss格式,减少前端格式化成本。

4.3 实体类与通用结果封装

后端代码不建议直接返回数据库实体给前端,而是定义一个统一响应体Result,包含codemessagedata三部分。这个习惯在接手正式项目时会非常有用,因为无论是登录失效、参数校验失败还是系统异常,都能用统一的 JSON 结构告知前端。

package com.example.lostfound.common; import lombok.Data; @Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> fail(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

对应的ItemInfo实体类:

package com.example.lostfound.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("item_info") public class ItemInfo { @TableId(type = IdType.AUTO) private Long id; private String itemType; private String title; private String description; private String place; private LocalDateTime happenTime; private String imageUrl; private String status; private Long publisherId; private LocalDateTime createTime; private LocalDateTime updateTime; }

4.4 发布物品的核心接口

发布物品是失物招领系统最典型的写操作。它不只是执行一次 insert,还需要做归属校验和状态初始化。接口设计如下:

package com.example.lostfound.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.lostfound.common.Result; import com.example.lostfound.entity.ItemInfo; import com.example.lostfound.service.ItemInfoService; import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.time.LocalDateTime; @RestController @RequestMapping("/api/item") public class ItemInfoController { @Resource private ItemInfoService itemInfoService; @PostMapping("/publish") public Result<ItemInfo> publish(@Valid @RequestBody ItemInfo item) { item.setCreateTime(LocalDateTime.now()); item.setUpdateTime(LocalDateTime.now()); // 新发布的物品默认处于待认领状态 item.setStatus("PENDING"); itemInfoService.save(item); return Result.ok(item); } @GetMapping("/list") public Result<List<ItemInfo>> list( @RequestParam(required = false) String itemType, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<ItemInfo> wrapper = new LambdaQueryWrapper<>(); if (StrUtil.isNotBlank(itemType)) { wrapper.eq(ItemInfo::getItemType, itemType); } if (StrUtil.isNotBlank(keyword)) { wrapper.and(w -> w.like(ItemInfo::getTitle, keyword) .or().like(ItemInfo::getDescription, keyword) .or().like(ItemInfo::getPlace, keyword)); } wrapper.orderByDesc(ItemInfo::getCreateTime); return Result.ok(itemInfoService.list(wrapper)); } }

发布接口里有一个很关键的逻辑:不能信任前端传过来的status字段。新发布物品的状态必须由后端强制设置为PENDING,否则用户直接伪造请求把一个失物标记为“已归还”,会破坏业务规则。这也是前后端分离项目里必须养成的安全意识:所有影响业务状态的字段,后端要自己做兜底。

如果想更严谨,可以在 Controller 层加@PreAuthorize或 JWT 拦截器,确保只有登录用户才能调用/publish接口。初学者可以先在拦截器里做校验,后续再引入 Spring Security。

5. Vue3 + Element Plus 前端实现

前端项目的搭建推荐使用 Vite,它是 Vue3 官方推荐构建工具,启动速度比 Webpack 快很多。在命令行执行:

npm create vite@latest lost-found-web -- --template vue cd lost-found-web npm install npm install axios element-plus vue-router@4

5.1 项目目录结构

前端代码不建议全部堆在App.vue里。按照功能边界拆成视图、组件、API、路由和工具模块,后续维护会轻松很多。

src/ api/ item.js user.js assets/ components/ ItemCard.vue CommentList.vue router/ index.js views/ HomeView.vue PublishView.vue DetailView.vue LoginView.vue UserCenterView.vue App.vue main.js

5.2 封装 Axios 请求

Axios 的封装是前后端分离项目的基础。需要在请求拦截器里带上 token,在响应拦截器里统一处理错误码和登录失效。这样业务代码里就不用每次都写if (code !== 200)这种重复逻辑。

// src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: 'http://localhost:8080/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) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request

这里有一个细节:后端如果已经用Result<T>封装了返回值,那么 Axios 拦截器可以直接把data字段取出来返回,业务页面拿到的是干净的业务数据,而不是res.data.data这种嵌套结构。这个设计能减少很多重复代码。

5.3 发布表单页面

使用 Element Plus 的el-formel-upload实现发布页面。发布表单需要校验标题、地点、时间、描述等必填字段,图片上传成功后将返回的 URL 绑定到表单字段里。

<!-- src/views/PublishView.vue --> <template> <div class="publish-container"> <el-card> <template #header> <span>发布失物 / 招领信息</span> </template> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" > <el-form-item label="信息类型" prop="itemType"> <el-radio-group v-model="form.itemType"> <el-radio value="LOST">失物</el-radio> <el-radio value="FOUND">招领</el-radio> </el-radio-group> </el-form-item> <el-form-item label="标题" prop="title"> <el-input v-model="form.title" placeholder="例如:蓝色钱包" /> </el-form-item> <el-form-item label="地点" prop="place"> <el-input v-model="form.place" placeholder="丢失/拾取地点" /> </el-form-item> <el-form-item label="时间" prop="happenTime"> <el-date-picker v-model="form.happenTime" type="datetime" placeholder="选择时间" /> </el-form-item> <el-form-item label="描述" prop="description"> <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请尽量详细描述物品特征" /> </el-form-item> <el-form-item label="图片"> <el-upload action="http://localhost:8080/api/upload" :headers="uploadHeaders" :on-success="handleUploadSuccess" list-type="picture" > <el-button type="primary">上传图片</el-button> </el-upload> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSubmit">提交</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup> import { ref, reactive } from 'vue' import { ElMessage } from 'element-plus' import { publishItem } from '../api/item' const formRef = ref() const form = reactive({ itemType: 'LOST', title: '', place: '', happenTime: '', description: '', imageUrl: '' }) const rules = { itemType: [{ required: true, message: '请选择信息类型', trigger: 'change' }], title: [{ required: true, message: '请填写标题', trigger: 'blur' }], place: [{ required: true, message: '请填写地点', trigger: 'blur' }], happenTime: [{ required: true, message: '请选择时间', trigger: 'change' }], description: [{ required: true, message: '请填写描述', trigger: 'blur' }] } const uploadHeaders = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` } function handleUploadSuccess(response) { form.imageUrl = response.data ElMessage.success('图片上传成功') } async function handleSubmit() { await formRef.value.validate() const res = await publishItem(form) ElMessage.success('发布成功') // 跳转到详情页或列表页 } </script>

前端最容易忽略的一个问题是el-radio在 Element Plus 新版本中的用法。老版本用的是label属性作为值,新版本推荐使用value属性。如果你在运行表单时发现单选框无法正确选中,优先排查是不是这个属性写错了。

5.4 列表页与状态标签映射

物品列表页是失物招领系统的门面。为了提高浏览效率,可以用el-tabs切换“全部 / 失物 / 招领”,再通过关键字输入框搜索。状态标签用 Element Plus 的el-tag动态渲染。

<template> <div class="home-container"> <el-input v-model="keyword" placeholder="搜索物品名称、描述或地点" clearable style="margin-bottom: 20px" @keyup.enter="loadList" /> <el-tabs v-model="activeType" @tab-change="loadList"> <el-tab-pane label="全部" name="" /> <el-tab-pane label="失物" name="LOST" /> <el-tab-pane label="招领" name="FOUND" /> </el-tabs> <el-row :gutter="20"> <el-col v-for="item in itemList" :key="item.id" :span="8" > <el-card class="item-card" @click="router.push(`/detail/${item.id}`)" > <img :src="item.imageUrl || fallbackImage" class="item-image" /> <h3>{{ item.title }}</h3> <p>{{ item.place }}</p> <el-tag :type="statusMap[item.status].type"> {{ statusMap[item.status].text }} </el-tag> </el-card> </el-col> </el-row> </div> </template> <script setup> import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { getItemList } from '../api/item' const router = useRouter() const itemList = ref([]) const keyword = ref('') const activeType = ref('') const statusMap = { PENDING: { type: 'warning', text: '待认领' }, CLAIMED: { type: 'info', text: '已被认领' }, FINISHED: { type: 'success', text: '已归还' } } async function loadList() { const data = await getItemList({ itemType: activeType.value, keyword: keyword.value }) itemList.value = data } onMounted(loadList) </script>

这里要注意el-tabsv-model绑定值。如果你用activeType并设置name=""表示全部,和后端接口的itemType直接对接,不需要额外转换。这是前后端字段设计统一的收益。

6. 前后端联调与效果验证

前后端分离开发的最大痛点是联调。写完前端和后端,不代表系统就能跑起来。下面整理一套标准联调流程。

6.1 后端启动验证

确保 MySQL 服务启动,并且已执行第 3 节的建表脚本。然后在项目根目录运行:

mvn spring-boot:run

如果看到类似下面的日志,说明后端启动成功:

Tomcat started on port 8080 (http) with context path '' Started LostFoundApplication in 3.2 seconds

使用 Postman 或 Apifox 调用发布接口:

POST http://localhost:8080/api/item/publish Content-Type: application/json { "itemType": "LOST", "title": "蓝色双肩包", "description": "在图书馆二楼丢失的蓝色背包,内有笔记本电脑", "place": "图书馆二楼大厅", "happenTime": "2024-11-10 15:30:00" }

预期返回:

{ "code": 200, "message": "success", "data": { "id": 1, "itemType": "LOST", "title": "蓝色双肩包", "status": "PENDING", "publisherId": null } }

这里注意,后端如果没有做登录认证,publisherId会是null。在实际项目中,publisherId应该从 JWT 令牌中解析获取,而不是由前端传值。

6.2 前端启动验证

进入前端项目目录执行:

npm run dev

浏览器访问http://localhost:5173,先进入发布页面,填写表单后提交。如果一切正常,页面会自动跳转到列表页,并且新发布的物品出现在列表第一行。

此时如果发现前端请求失败,第一步不是看浏览器控制台的报错,而是先打开开发者工具(F12)的 Network 面板,确认请求的 URL、Method、Request Payload 是否与后端接口一致。90% 的联调问题都出在 URL 拼错、字段名不一致或 Content-Type 不对。

6.3 跨域问题处理

前后端分离开发时,前端默认运行在 5173 端口,后端在 8080 端口,浏览器会拦截跨域请求。最简单的解决方式是在后端添加全局跨域配置。

package com.example.lostfound.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

生产环境建议把allowedOriginPatterns收窄到具体的域名,不能为了省事全部放开。

7. 失物招领系统常见问题与排查方法

做这类项目时,最容易卡住新手的往往不是业务逻辑本身,而是一些环境、版本和配置层面的问题。下面整理一张高频问题排查表,建议收藏备用。

问题现象可能原因排查方式解决方案
后端启动失败,报ClassNotFoundException: javax.servlet.FilterJDK 版本低于 17,或引入了基于 javax 的旧依赖执行java -version检查版本,查看依赖树升级 JDK17,检查依赖坐标是否为spring-boot3版本
MyBatis-Plus 依赖冲突同时引入了旧版mybatis-plus-boot-startermvn dependency:tree查看依赖树替换为mybatis-plus-spring-boot3-starter
前端访问接口报 CORS 错误后端未配置跨域,或配置了但不允许当前请求头查看浏览器 Network 中响应头是否有Access-Control-Allow-Origin添加全局跨域配置,检查请求方法是否在允许列表
前端提交表单后日期字段为 null前端传的日期格式和后端LocalDateTime解析规则不一致查看 Network 中 Request Payload 的日期格式配置jackson.date-format,或使用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
列表页中文乱码MySQL 数据库或表字符集不是 utf8mb4SHOW CREATE TABLE item_info查看字符集建库建表时统一使用 utf8mb4,连接串带characterEncoding=utf8
上传图片后无法访问上传文件保存到本地磁盘,但 Spring Boot 没有映射静态资源路径访问图片 URL 看返回 404 还是 500配置spring.web.resources.static-locations,或使用独立文件服务 / OSS
刷新页面后路由 404前端路由使用 history 模式,需要服务端配合直接访问域名根路径再跳转,对比路径是否一致开发环境使用createWebHashHistory,或配置 Nginxtry_files
JWT 拦截器导致登录接口也被拦截拦截器配置时没有排除/api/user/login等白名单路径查看后端日志中请求是否进入拦截器在拦截器注册时添加excludePathPatterns

8. 项目工程化与安全实践

失物招领系统虽然不复杂,但把它当成一个“小型的正式项目”来要求自己,才是这门实战课真正的意义。以下几个实践点,是区分“会写接口”和“会做项目”的分水岭。

8.1 配置文件环境隔离

开发环境和生产环境的配置不应该写死在同一份application.yml里。规范的作法是拆成三份:

  • application-dev.yml:本地开发,数据库密码写本机测试密码。
  • application-prod.yml:生产环境,使用环境变量注入敏感配置。
  • application.yml:只配置spring.profiles.active
spring: profiles: active: dev

生产环境的数据库密码通过启动参数传入,例如:

java -jar lost-found.jar \ --spring.profiles.active=prod \ --spring.datasource.password=${DB_PASSWORD}

这样能避免把真实密码提交到代码仓库。

8.2 图片上传的保存策略

本地上传文件有一个容易被忽略的问题:重启服务后,上传的图片是否还在?如果图片保存在项目目录下,重新打包部署可能会覆盖掉。

更稳妥的策略是把上传目录放到操作系统独立目录,比如/data/lost-found/upload,并在配置文件中定义upload.path。然后用静态资源映射暴露访问路径。

@Configuration public class UploadConfig implements WebMvcConfigurer { @Value("${upload.path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath); } }

初学者做课设时,将图片和代码放在同一目录也能跑通,但到了正式项目或多人协作场景,建议尽早切换到独立目录或云存储方案。

8.3 状态流转的服务端校验

失物招领系统的状态不应该允许前端随意修改。例如,已经“已认领”(CLAIMED)的物品,不能直接跳回“待认领”(PENDING)。合理的做法是在 Service 层写一个状态流转方法,只有符合业务规则的状态变化才允许执行。

public boolean changeStatus(Long itemId, String targetStatus, Long operatorId) { ItemInfo item = getById(itemId); if (!item.getPublisherId().equals(operatorId)) { throw new BusinessException("不是该信息的发布者"); } String current = item.getStatus(); boolean allowed = "PENDING".equals(current) && ("CLAIMED".equals(targetStatus) || "FINISHED".equals(targetStatus)); if (!allowed) { throw new BusinessException("非法的状态流转"); } item.setStatus(targetStatus); return updateById(item); }

这种服务端校验是业务系统安全性的核心体现,不能依赖前端按钮的显隐。

8.4 JWT 登录认证的最小实现

完整引入 Spring Security 需要写较多配置。对于课程设计或小型系统,可以先使用拦截器 + JWT 做认证,这是一种轻量且可理解的方案。

JWT 实现主要包含三个部分:

  1. 登录成功后生成 token,并返回给前端。
  2. 拦截器校验请求头中的 token,解析出用户 ID。
  3. Controller 层通过UserContext工具类获取当前登录用户。
public class JwtUtil { private static final SecretKey KEY = Keys.hmacShaKeyFor("your-secret-key-please-change-2024".getBytes()); public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(KEY) .build() .parseClaimsJws(token) .getBody(); } }

密钥不要硬编码在代码里。从配置文件或环境变量读取,是更安全的做法。

9. 总结:这套实战能带给你什么

通过失物招领系统的完整实现,你可以把散落的知识点串成一条完整的技术链路。数据库设计阶段,你学会分析业务实体和建立查询索引;后端开发阶段,你掌握 SpringBoot3 的项目搭建、MyBatis-Plus 的 CRUD、JWT 认证和文件上传;前端开发阶段,你用 Vue3 的 Composition API 和 Element Plus 完成页面交互;最后通过前后端联调,把跨域、请求封装、状态码这些真实问题逐个解决。

对于正在准备毕业设计或想积累项目经验的开发者,建议不要停留在抄代码这一步。下一步可以做两件事:一是把状态流转、留言功能做完善,让系统更贴近真实业务;二是给项目加上单元测试和 Docker 部署脚本,放在简历上会成为明显的加分项。动手是最快的成长路径,希望这篇实战能帮你把技术栈真正跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 5:32:55

智谱GLM-5.3发布与订阅额度重置:开发者接入和验证实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 5:29:51

从机械计算到互联网:计算机发展史中的关键转折与设计抉择

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 5:28:40

软件本地环境配置与草稿管理:从下载到稳定运行的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 5:25:03

喷雾干燥器设计计算详解:从物料衡算到防粘壁工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 5:20:10

AI幻觉攻击与防御:从生成机制到工具调用网关的全面治理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 5:18:55

从SW到国产三维CAD:替代软件推荐与迁移指南

SolidWorks是全球应用最广泛的中端三维CAD软件之一&#xff0c;在通用机械、非标自动化、消费电子等行业拥有庞大的用户基础。近年来&#xff0c;随着国产工业软件的快速崛起和自主可控需求的提升&#xff0c;越来越多的企业开始评估从SolidWorks向国产三维CAD迁移的可行性。替…

作者头像 李华