ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot + Vue 3 校园失物招领系统:从设计到部署完整实践

2026/9/7 16:24:19 拓冰建站 浏览量
Spring Boot + Vue 3 校园失物招领系统:从设计到部署完整实践 校园里最常见的求助信息不是查成绩不是选课而是失物招领。校园卡、钥匙、耳机、课本甚至笔记本电脑每天都在不同教室和食堂之间丢失。过去靠公告栏贴告示靠班级群和“万能墙”转发照片信息散落、无法检索、缺少核对一件失物从丢失到找回往往只能靠运气。这篇文章不打算泛泛介绍一个概念而是从一个可以直接跑通的完整项目入手用 Spring Boot、MyBatis-Plus、MySQL 和 Vue 3 实现一个校园失物招领系统。读完你会掌握需求分析、表结构设计、业务接口开发、认领流程状态流转和项目部署验证的完整链路也可以直接作为毕业设计或实训项目二次开发。通篇文章围绕一条主线展开失物信息如何被发布、被检索、被准确认领而不是在信息爆炸里越搅越乱。1. 校园失物招领到底难在哪里在写代码之前先要把业务问题讲清楚否则只是照着模板敲 CRUD做完也不知道自己在解决什么问题。从校园实际场景看当前失物招领有几个痛点信息渠道分散。失主可能把信息发在班级群捡到物品的人贴在公告栏两者互不可见。系统要解决的核心问题就是“统一的信息发布和检索入口”。关键词不一致。有人写“校园卡”有人写“一卡通”有人上传照片但不写任何文字。如果没有统一的分类和搜索策略即使所有信息都在一个平台上也无法匹配。认领过程缺少验证。现实中“冒领”并不少见尤其是校园卡、钱包这类容易被冒领的物品。所以认领流程必须加上“凭证校验”和“人工确认”环节。状态不透明。失主不知道自己的物品是否被找到捡拾人不知道是否有人申请认领管理员无法统计丢失率。必须设计清晰的状态机让每个环节都有迹可循。因此这套系统的核心价值不只是“存储失物记录”而是让所有失物、招领信息结构化提供可检索、可筛选的查询入口用认领申请加审核机制降低冒领风险用状态流转让整个流程透明。判断一个失物招领系统做得好不好就看这四条是否闭环。2. 系统设计与数据库建模2.1 角色与功能划分系统按最小但有完整性的模型设计方便后期扩展。学生或普通用户注册、登录发布失物信息发布招领信息浏览、搜索、筛选物品对物品提交认领申请填写证明信息审核自己发布的招领申请管理员管理用户状态删除违规物品信息查看统计数据和丢失率2.2 业务流程设计用文字描述主流程用户 A 丢失校园卡发布一条失物类型信息状态设为PUBLISHED。用户 B 在图书馆捡到校园卡发布一条招领类型信息状态设为PUBLISHED。用户 A 在首页搜索“校园卡”找到 B 发布的招领记录。用户 A 提交认领申请填写学号、姓名等证明描述。用户 B 收到申请核对信息无误后审核通过。系统更新该招领信息状态为CLAIMED线下归还后标记为RESOLVED。这里要说明一个设计决策失物和招领信息放在一张表还是两张表如果字段差异大、独立操作多拆两张表更清晰如果统一处理搜索、分页和权限一张表加type字段更简单。考虑到校园项目规模单表item加type字段足以支撑还可以复用同一套列表和搜索接口所以我选择统一表结构设计。2.3 数据库表结构系统设计了用户表、物品表、认领记录表三张核心表。用户表 sys_user字段类型说明idbigint主键自增usernamevarchar(50)登录用户名唯一passwordvarchar(100)加密后的密码nicknamevarchar(50)昵称phonevarchar(20)联系方式rolevarchar(20)STUDENT / ADMINcreated_atdatetime创建时间物品表 item字段类型说明idbigint主键自增typevarchar(10)LOST / FOUNDtitlevarchar(100)标题descriptionvarchar(500)详细描述locationvarchar(100)丢失或拾取地点item_typevarchar(50)物品分类如校园卡、钱包、耳机image_urlvarchar(255)图片地址可空contact_phonevarchar(20)联系手机statusvarchar(20)PUBLISHED / CLAIMED / RESOLVEDuser_idbigint发布人 IDcreated_atdatetime创建时间updated_atdatetime更新时间认领记录表 claim_record字段类型说明idbigint主键自增item_idbigint关联物品 IDclaimant_idbigint认领人用户 IDproof_descriptionvarchar(500)认领证明材料statusvarchar(20)PENDING / APPROVED / REJECTEDhandler_idbigint处理人 IDcreated_atdatetime创建时间完整建表 SQL 如下-- 文件路径doc/schema.sql CREATE DATABASE IF NOT EXISTS campus_lost_found DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE campus_lost_found; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(100) NOT NULL COMMENT 加密后的密码, nickname VARCHAR(50) COMMENT 昵称, phone VARCHAR(20) COMMENT 联系方式, role VARCHAR(20) NOT NULL DEFAULT STUDENT COMMENT STUDENT/ADMIN, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT用户表; CREATE TABLE item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, type VARCHAR(10) NOT NULL COMMENT LOST/FOUND, title VARCHAR(100) NOT NULL COMMENT 标题, description VARCHAR(500) COMMENT 详细描述, location VARCHAR(100) COMMENT 丢失/拾取地点, item_type VARCHAR(50) COMMENT 物品分类, image_url VARCHAR(255) COMMENT 图片地址, contact_phone VARCHAR(20) COMMENT 联系手机, status VARCHAR(20) NOT NULL DEFAULT PUBLISHED COMMENT PUBLISHED/CLAIMED/RESOLVED, user_id BIGINT NOT NULL COMMENT 发布人ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_type_status (type, status), KEY idx_user_id (user_id), KEY idx_created_at (created_at) ) ENGINEInnoDB COMMENT失物/招领物品表; CREATE TABLE claim_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, item_id BIGINT NOT NULL COMMENT 关联物品ID, claimant_id BIGINT NOT NULL COMMENT 认领人ID, proof_description VARCHAR(500) COMMENT 认领证明材料, status VARCHAR(20) NOT NULL DEFAULT PENDING COMMENT PENDING/APPROVED/REJECTED, handler_id BIGINT COMMENT 处理人ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_item_id (item_id), KEY idx_claimant_id (claimant_id) ) ENGINEInnoDB COMMENT认领记录表;这里有两个细节值得新手注意。第一item表的status字段被设计为PUBLISHED、CLAIMED、RESOLVED三个状态而不是简单的“未认领 / 已认领”。PUBLISHED表示信息可被检索CLAIMED表示认领申请已通过但还没有完成线下交接RESOLVED才是最终完成态。如果不设置中间状态就无法支撑“审核通过、等待取回”这个真实环节。第二claim_record表的status是PENDING / APPROVED / REJECTED和item.status是两套状态机。认领申请通过后才根据业务需要更新item.status。二者有关联但不完全一致很多新手在这里把状态混在一起导致逻辑混乱。3. 技术选型与项目结构3.1 为什么选这套技术栈失物招领系统没有特别复杂的并发和计算场景业务重点在 CRUD、检索、状态流转。选技术栈的第一原则是“团队熟悉、生态成熟、能快速跑通”。所以推荐JDK 1.8Spring Boot 2.7.xMyBatis-Plus 3.5.xMySQL 5.7JWT 做无状态登录Vue 3 Element Plus 做前端Spring Boot 把 Web、事务、参数校验、日志都封装得很顺手。MyBatis-Plus 大幅减少纯粹的 CRUD 模板代码分页、条件查询开箱即用适合学习阶段和中小型项目。JWT 解决认证问题。用户在登录后拿到 token后续请求带上 token 即可识别身份比传统 Session 更适合前后端分离。3.2 后端项目结构后端项目命名campus-lost-found-server包名com.campus.lostfound目录结构如下campus-lost-found-server ├── pom.xml ├── src/main/java/com/campus/lostfound │ ├── LostFoundApplication.java │ ├── common │ │ ├── Result.java │ │ └── BusinessException.java │ ├── config │ │ ├── MybatisPlusConfig.java │ │ ├── JwtInterceptor.java │ │ └── WebMvcConfig.java │ ├── controller │ │ ├── AuthController.java │ │ └── ItemController.java │ ├── service │ │ ├── AuthService.java │ │ ├── ItemService.java │ │ └── ClaimService.java │ ├── mapper │ │ ├── UserMapper.java │ │ ├── ItemMapper.java │ │ └── ClaimMapper.java │ ├── entity │ │ ├── User.java │ │ ├── Item.java │ │ └── ClaimRecord.java │ ├── dto │ │ ├── LoginDTO.java │ │ ├── RegisterDTO.java │ │ └── ItemDTO.java │ └── util │ └── JwtUtil.java └── src/main/resources └── application.yml这个结构已经足够清晰。实际项目中可以继续拆分vo、enums、aspect但第一版不必过度设计。4. 项目初始化与基础配置4.1 pom.xml 依赖创建 Spring Boot 项目后核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version3.19.2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies版本选择说明Spring Boot 2.7.18 和 MyBatis-Plus 3.5.3.1 是目前兼容性比较好的组合。如果你的项目使用 Spring Boot 3.x需要同步调整 MyBatis-Plus 的依赖包名和部分 API尽量保证主依赖版本匹配避免启动时出现类找不到的异常。4.2 application.yml 配置# 文件路径src/main/resources/application.yml server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/campus_lost_found?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto jwt: secret: campus-lost-found-demo-secret-key expire-days: 7配置注意事项map-underscore-to-camel-case开启后数据库字段created_at自动映射到 Java 实体的createdAt省去大量TableField注解。log-impl配置为 StdOutImpl 后每次 SQL 都会打印到控制台联调阶段非常有帮助生产环境建议关闭或调整日志级别。url 中的serverTimezoneAsia/Shanghai必须保留否则 MySQL 驱动会报时区错误。4.3 统一返回结果类为了避免每个接口手写返回结构定义一个统一结果类// 文件路径src/main/java/com/campus/lostfound/common/Result.java Data public class ResultT { private Integer code; 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; } public static T ResultT error(String message) { ResultT r new Result(); r.setCode(500); r.setMessage(message); return r; } }这段代码在实际项目中会继续扩展比如增加错误码枚举、分页包装等。但初版够用即可不需要一开始就引入过多抽象。5. 后端核心功能代码实现后端核心业务集中在ItemController、ItemService、ClaimService中。逐个功能实现。5.1 实体类定义// 文件路径src/main/java/com/campus/lostfound/entity/Item.java Data TableName(item) public class Item { TableId(type IdType.AUTO) private Long id; /** LOST 表示失物FOUND 表示招领 */ private String type; private String title; private String description; /** 丢失或拾取地点 */ private String location; /** 物品分类如 校园卡、钱包、耳机 */ private String itemType; private String imageUrl; private String contactPhone; /** PUBLISHED / CLAIMED / RESOLVED */ private String status; private Long userId; private LocalDateTime createdAt; private LocalDateTime updatedAt; }TableName(item)指定对应数据表TableId(type IdType.AUTO)标识主键自增其余字段通过驼峰映射自动绑定。5.2 发布失物和招领信息发布接口是系统最基础的功能// 文件路径src/main/java/com/campus/lostfound/controller/ItemController.java RestController RequestMapping(/api/items) public class ItemController { Resource private ItemService itemService; PostMapping public ResultItem createItem(RequestBody ItemDTO itemDTO, RequestAttribute(userId) Long userId) { Item item itemService.createItem(itemDTO, userId); return Result.success(item); } }// 文件路径src/main/java/com/campus/lostfound/service/ItemService.java Service public class ItemService { Resource private ItemMapper itemMapper; public Item createItem(ItemDTO dto, Long userId) { if (!LOST.equals(dto.getType()) !FOUND.equals(dto.getType())) { throw new BusinessException(type 必须是 LOST 或 FOUND); } if (!StringUtils.hasText(dto.getTitle())) { throw new BusinessException(标题不能为空); } Item item new Item(); BeanUtils.copyProperties(dto, item); item.setUserId(userId); item.setStatus(PUBLISHED); itemMapper.insert(item); return item; } }这段逻辑的关键是把当前登录用户 ID 绑定到物品记录并固定初始状态为PUBLISHED。状态字段不能由前端传入否则用户可以伪造状态绕过认领流程。5.3 分页搜索接口检索是失物招领系统使用频率最高的接口// 文件路径src/main/java/com/campus/lostfound/service/ItemService.java public PageItem pageItems(String keyword, String type, Integer page, Integer size) { LambdaQueryWrapperItem wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(type)) { wrapper.eq(Item::getType, type); } if (StringUtils.hasText(keyword)) { wrapper.and(w - w .like(Item::getTitle, keyword) .or().like(Item::getDescription, keyword) .or().like(Item::getLocation, keyword) .or().like(Item::getItemType, keyword)); } wrapper.orderByDesc(Item::getCreatedAt); return itemMapper.selectPage(new Page(page, size), wrapper); }这里容易踩坑的是or条件。如果直接写多个like().or().like()而前面又有type条件SQL 很可能变成type ? OR title LIKE ?导致类型筛选失效。用and(w - ...)把多个or包起来才能生成括号保证查询逻辑正确。分页还需要在 MyBatis-Plus 配置类中注册分页插件否则selectPage不会真正分页。后面常见问题部分会专门说明。5.4 提交认领申请认领申请是整个系统的关键业务逻辑// 文件路径src/main/java/com/campus/lostfound/service/ClaimService.java Service public class ClaimService { Resource private ItemMapper itemMapper; Resource private ClaimMapper claimMapper; Transactional(rollbackFor Exception.class) public void applyClaim(Long itemId, Long claimantId, String proofDescription) { Item item itemMapper.selectById(itemId); if (item null) { throw new BusinessException(物品不存在); } if (!PUBLISHED.equals(item.getStatus())) { throw new BusinessException(该物品当前状态不可申请认领); } Long pendingCount claimMapper.selectCount(new LambdaQueryWrapperClaimRecord() .eq(ClaimRecord::getItemId, itemId) .eq(ClaimRecord::getClaimantId, claimantId) .eq(ClaimRecord::getStatus, PENDING)); if (pendingCount 0) { throw new BusinessException(你已经提交过认领申请请耐心等待审核); } ClaimRecord claimRecord new ClaimRecord(); claimRecord.setItemId(itemId); claimRecord.setClaimantId(claimantId); claimRecord.setProofDescription(proofDescription); claimRecord.setStatus(PENDING); claimMapper.insert(claimRecord); } }这里有两个重要设计第一状态校验。只有PUBLISHED状态下的物品才允许申请认领。如果物品已经进入CLAIMED或RESOLVED状态再提交申请没有意义。第二重复申请控制。同一用户、同一物品、同一待审核状态下只允许存在一条申请记录。否则用户重复提交会给审核人带来困扰。实际项目中还可以在数据库层面加唯一索引但这里用业务校验已经足够。5.5 处理认领申请处理接口把认领申请状态和物品状态联动起来// 文件路径src/main/java/com/campus/lostfound/service/ClaimService.java Transactional(rollbackFor Exception.class) public void handleClaim(Long claimId, Long handlerId, String action) { ClaimRecord claimRecord claimMapper.selectById(claimId); if (claimRecord null) { throw new BusinessException(认领记录不存在); } if (!PENDING.equals(claimRecord.getStatus())) { throw new BusinessException(该申请已处理不能重复操作); } Item item itemMapper.selectById(claimRecord.getItemId()); if (item null) { throw new BusinessException(关联物品不存在); } if (!item.getUserId().equals(handlerId)) { throw new BusinessException(只有物品发布人才能处理认领申请); } if (APPROVE.equals(action)) { claimRecord.setStatus(APPROVED); item.setStatus(CLAIMED); } else if (REJECT.equals(action)) { claimRecord.setStatus(REJECTED); } else { throw new BusinessException(非法操作); } claimMapper.updateById(claimRecord); itemMapper.updateById(item); }审核通过后认领记录状态变为APPROVED物品状态变为CLAIMED。审核拒绝后认领记录变为REJECTED物品状态保持不变仍然可以被其他人申请。从实际业务看审核通过后往往还有一个“确认归还”动作此时再把item.status更新为RESOLVED。这样就能清楚记录“谁申请、谁审核、什么时候完成”的完整链路。5.6 注册与登录认证部分用 JWT 实现注册时使用 BCrypt 加密密码// 文件路径src/main/java/com/campus/lostfound/service/AuthService.java Service public class AuthService { Resource private UserMapper userMapper; Resource private JwtUtil jwtUtil; public void register(RegisterDTO dto) { Long exist userMapper.selectCount(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (exist 0) { throw new BusinessException(用户名已存在); } User user new User(); user.setUsername(dto.getUsername()); user.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); user.setNickname(dto.getNickname()); user.setRole(STUDENT); userMapper.insert(user); } public String login(LoginDTO dto) { User user userMapper.selectOne(new LambdaQueryWrapperUser() .eq(User::getUsername, dto.getUsername())); if (user null || !BCrypt.checkpw(dto.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } return jwtUtil.generateToken(user.getId(), user.getUsername()); } }密码加密是安全底线任何真实项目都不能明文存储密码。5.7 JWT 拦截器为了让受保护接口拿到当前登录用户需要写一个拦截器// 文件路径src/main/java/com/campus/lostfound/config/JwtInterceptor.java Component public class JwtInterceptor implements HandlerInterceptor { Resource private JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String auth request.getHeader(Authorization); if (auth ! null auth.startsWith(Bearer )) { String token auth.substring(7); Long userId jwtUtil.parseUserId(token); if (userId ! null) { request.setAttribute(userId, userId); return true; } } response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }然后在 WebMvcConfig 中注册并放行登录注册接口// 文件路径src/main/java/com/campus/lostfound/config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login, /api/auth/register, /api/items/**); } }这里放行了/api/items/**下的 GET 查询接口游客也能浏览失物招领信息。发布和认领接口则必须登录。具体放行范围可以根据安全需求灵活调整。6. 前端页面设计与接口联调后端接口具备后前端负责展示和交互。下面以 Vue 3 Element Plus 为例。6.1 列表页核心思路列表页需要做到三件事展示物品信息、支持关键词搜索、支持类型筛选。!-- 文件路径frontend/src/views/ItemList.vue -- template div el-input v-modelkeyword placeholder搜索校园卡、钱包、耳机... clearable / el-select v-modeltype clearable placeholder类型筛选 el-option label失物 valueLOST / el-option label招领 valueFOUND / /el-select el-button typeprimary clicksearch搜索/el-button el-table :dataitems el-table-column proptitle label标题 / el-table-column proptype label类型 width100 / el-table-column propitemType label分类 width120 / el-table-column proplocation label地点 width160 / el-table-column propstatus label状态 width120 / el-table-column label操作 width200 template #default{ row } el-button v-ifrow.status PUBLISHED sizesmall clickopenApply(row) 申请认领 /el-button el-button sizesmall clickviewDetail(row)详情/el-button /template /el-table-column /el-table /div /templateopenApply和viewDetail在完整项目中会继续实现弹窗和详情页这里展示的是主流程结构。搜索请求通过封装的request工具发起携带 JWT token拿到数据后渲染表格。6.2 发布表单发布失物和招领信息共用一个表单只是type字段不同。!-- 文件路径frontend/src/views/PublishItem.vue -- template el-form :modelform label-width100px el-form-item label信息类型 el-radio-group v-modelform.type el-radio labelLOST丢失物品/el-radio el-radio labelFOUND捡到物品/el-radio /el-radio-group /el-form-item el-form-item label标题 el-input v-modelform.title placeholder例如图书馆三楼捡到蓝色校园卡 / /el-form-item el-form-item label物品分类 el-select v-modelform.itemType el-option label校园卡 value校园卡 / el-option label钱包 value钱包 / el-option label电子设备 value电子设备 / el-option label证件 value证件 / el-option label其他 value其他 / /el-select /el-form-item el-form-item label地点 el-input v-modelform.location placeholder例如一食堂二楼 / 图书馆三楼自习区 / /el-form-item el-form-item label详细描述 el-input v-modelform.description typetextarea :rows4 / /el-form-item el-form-item label联系电话 el-input v-modelform.contactPhone / /el-form-item el-button typeprimary clicksubmitForm发布/el-button /el-form /template script setup import { reactive } from vue import { ElMessage } from element-plus import request from /utils/request const form reactive({ type: LOST, title: , itemType: , location: , description: , contactPhone: }) async function submitForm() { const { data } await request.post(/api/items, form) ElMessage.success(发布成功) } /script实际项目还会加上图片上传把图片 URL 存入image_url字段。图片存储建议使用对象存储或服务器静态目录不要直接转成 Base64 存数据库否则数据库体积会迅速膨胀。7. 运行验证与接口测试整个项目运行前先确认 MySQL 中已经执行建表 SQL。然后启动后端mvn spring-boot:run也可以打成 jar 包运行mvn clean package -DskipTests java -jar target/campus-lost-found-server-0.0.1-SNAPSHOT.jar启动成功后控制台会看到 Tomcat started on port(s): 8080。接下来用 curl 做一次完整功能验证。7.1 注册并登录curl -X POST http://localhost:8080/api/auth/register \ -H Content-Type: application/json \ -d {username: student01, password: 123456, nickname: 张三} curl -X POST http://localhost:8080/api/auth/login \ -H Content-Type: application/json \ -d {username: student01, password: 123456}登录响应示例{ code: 200, message: success, data: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoic3R1ZGVudDAxIn0... }拿到 token 后后续请求在 Header 中携带。7.2 发布招领信息curl -X POST http://localhost:8080/api/items \ -H Content-Type: application/json \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... \ -d { type: FOUND, title: 图书馆三楼捡到蓝色校园卡, description: 卡片背面有姓名贴纸联系方式请私信, location: 图书馆三楼, itemType: 校园卡, contactPhone: 13800000000 }预期返回中可以看到status为PUBLISHEDuserId为当前登录用户 ID。7.3 搜索接口curl http://localhost:8080/api/items?keyword%E6%A0%A1%E5%9B%AD%E5%8D%A1page1size10服务端返回匹配的分页数据。这是游客可访问的接口不需要携带 token所以之前放行了/api/items/**的 GET 路径。7.4 认领申请curl -X POST http://localhost:8080/api/claim \ -H Content-Type: application/json \ -H Authorization: Bearer 另一用户token \ -d {itemId: 1, proofDescription: 卡面姓名是张三学号后四位是1234}如果重复提交会看到错误提示你已经提交过认领申请请耐心等待审核。8. 常见运行问题与排查方法问题现象可能原因排查方式解决方案启动报驱动类找不到使用 MySQL 5.x 但驱动版本不匹配查看 maven 依赖树在 pom 中明确驱动版本调整 url中文乱码表字符集不是 utf8mb4或接口响应编码不对查看 MySQL 表字符集、HTTP 响应头建库使用 utf8mb4连接串增加 characterEncodingutf8时间差 8 小时数据库连接未设置 serverTimezone查看 MySQL 时间字段url 增加 serverTimezoneAsia/Shanghai接口返回 401token 缺失或过期检查请求头 Authorization重新登录确认拦截器排除路径正确重复认领未被拦截未做唯一的待审核状态校验查看 claim_record 表数据增加重复申请查询逻辑或加唯一索引MyBatis-Plus 分页失效未配置分页插件查看控制台 SQL 是否有 LIMIT注册 PaginationInnerInterceptor分页插件是新手容易遗漏的一步。如果不注册PaginationInnerInterceptorselectPage并不会真正分页而是查出全量数据。对应配置如下// 文件路径src/main/java/com/campus/lostfound/config/MybatisPlusConfig.java Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }9. 项目扩展方向与最佳实践如果只是练习以上功能已经够用。但如果要放到真实校园环境中运行还有几个方向值得继续完善。9.1 图片上传物品照片是找回率的重要影响因素。图片上传推荐使用对象存储或服务器独立静态目录前端上传成功后只把 URL 提交到后端。上传接口要做类型和大小限制避免上传超大文件或非图片内容。9.2 消息通知认领申请提交后物品发布人应该收到通知。可以用 WebSocket 做实时通知也可以用站内信、短信或邮件做离线通知。对校园项目来说站内信已经足够设计一张notification表即可。9.3 数据统计管理员后台可以统计每日新增失物、招领数量已认领和已完成比例高发丢失地点排行。这些统计基于item表按时间、地点、分类做 GROUP BY 就能实现不需要引入重型 BI 组件。9.4 权限与安全当前实现只区分了普通用户和管理员。真实项目中建议做到用户只能编辑自己发布的物品删除操作使用软删除保留操作日志所有写接口做参数校验生产环境必须更换 JWT secret并设置合理过期时间定期备份数据库防止误操作。9.5 使用 Redis 做热点数据缓存在食堂、图书馆这类高频率检索场景下热门物品数据可能被频繁查询。可以引入 Redis 缓存列表页数据缓存 key 按keyword type page组合物品状态变更时失效缓存。这部分依赖缓存失效策略建议在基础功能稳定后再接入不推荐第一版就引入。10. 总结校园失物招领系统的核心难点不在代码量而在业务状态设计。如果你开发过类似的流程管理系统会发现大多数时间都在处理“信息如何结构化”“状态如何流转”“权限如何控制”这几件事。本文用一张物品表、一张认领表和一套状态机完整实现了从发布、搜索、申请、审核到完成的闭环流程。整套项目适合作为 Spring Boot 学习者的第二个完整项目第一个项目通常做登录注册和简单 CRUD第二个项目就应该尝试带业务流程、多表关联和状态流转的功能系统。失物招领系统正好覆盖了这些点而且领域模型容易理解不需要额外学习复杂的业务背景。建议部署时按这个顺序验证先建表、再启动后端、用 curl 跑通注册登录和发布接口、最后接入前端页面。任何一步出现问题优先看控制台 SQL 和 HTTP 状态码不要急着改代码。基础流程跑通之后再逐步增加图片上传、消息通知和管理员统计。如果你正在找毕业设计课题这套项目在原有基础上增加小程序前端、物品照片识别、地图定位附近失物等功能都能构成完整且有亮点的选题。