基于Spring Boot的校园失物招领系统全栈开发实战
2026/9/2 16:12:59 网站建设 项目流程

还在为食堂丢卡、教室丢书、操场丢钥匙发愁?校园里的失物招领信息往往散落在表白墙、班级群、公告栏里,找一件东西要翻几十条聊天记录,捡到东西也不知道交给谁。本文将围绕校园失物招领系统从需求分析、数据库设计、后端接口到前端页面完整走一遍开发流程,帮助你把寻物和招领信息统一管理起来。

文章适合正在做课程设计、毕业设计,或者想练习 Spring Boot 全栈开发的同学阅读。代码基于 Spring Boot 2.7 + MyBatis-Plus + Thymeleaf + MySQL 实现,思路清晰、可以直接运行,也方便二次扩展。

1. 项目背景与核心概念

1.1 校园失物招领的现状与痛点

大学校园是一个流动性很强的环境,教室、图书馆、食堂、操场都是失物高发区。大多数学校目前仍然依靠线下失物招领处、QQ 群消息、朋友圈转发来扩散信息。这种方式有几个明显的问题:

  • 信息分散:失物信息散落在各个群聊和平台,学生不知道该去哪里查。
  • 时效性差:一条招领信息发出后很快被新消息淹没。
  • 缺少闭环:物品被认领后,发布者不会回来更新状态,其他人还在反复询问。
  • 信息可信度低:仅凭一张图片或一句话,很难判断物品是否还在、该联系谁。

校园失物招领系统要解决的,就是把这些散落的信息集中到一个平台上,让“丢失登记”和“捡到登记”有统一的入口,并且能跟踪从发布到认领的完整状态。

1.2 系统要具备哪些能力

一个最小可用的校园失物招领系统,需要覆盖以下几个核心场景:

  • 学生发布“寻物启事”(我丢了东西)。
  • 学生发布“失物招领”(我捡到了东西)。
  • 浏览者可以按关键词、类型筛选信息。
  • 拾主或失主可以在详情页提交认领/留言。
  • 信息发布者可以确认认领并标记“已完成”。

围绕这些场景,系统至少要包含用户、失物信息、认领记录三块数据模型。如果只做一个纯展示页面,其实意义不大,关键是“发布—发现—认领—确认”这条业务闭环要跑通。

1.3 本文的技术方案选型

考虑到教学和课程设计的需要,本文采用一套“单体应用”方案:

技术说明
Java 8+后端开发语言
Spring Boot 2.7.x应用框架,简化配置和部署
MyBatis-Plus 3.5.x数据访问层框架,内置 CRUD 方法
MySQL 5.7 / 8.0关系型数据库
Thymeleaf服务端模板引擎,页面直接渲染数据
Bootstrap 5前端样式框架,通过 CDN 引入

这套组合的好处是结构简单、开发效率高,不需要额外搭建前端工程,适合单体应用快速落地。如果你后续想改造成前后端分离,只要把 Controller 层改成返回 JSON 的 REST 接口即可。

2. 系统分析与功能设计

2.1 角色与功能拆分

系统只涉及两类角色:

  • 普通用户:注册登录后可以发布信息、提交认领申请。
  • 信息发布者:对自己发布的信息有管理权限,可以查看认领申请、确认完成。

按功能模块划分如下:

模块功能点
用户模块注册、登录、退出登录
信息模块发布寻物/招领、列表展示、关键词搜索、类型筛选
详情模块查看详情、提交认领申请
管理模块发布者确认认领、标记信息完成

这里刻意没有做管理员后台。对课程设计来说,先把用户维度的闭环跑通更关键,管理员审核、数据统计可以作为后续扩展。

2.2 业务闭环流程

整个系统的核心流程可以归纳为:

  1. 用户注册登录。
  2. 用户发布一条“寻物启事”或“失物招领”。
  3. 其他用户通过首页搜索、筛选找到这条信息。
  4. 用户进入详情页,提交认领申请并附上线索说明。
  5. 发布者查看认领申请,确认某条认领后,信息状态变为“已完成”。

这个流程看起来简单,但值得注意的点在于:认领不是直接暴露联系方式,而是通过申请-确认机制,避免别有用心的人冒领物品。这也是校园失物招领系统区别于普通信息发布网站的核心。

2.3 项目结构规划

后端统一使用com.example.lostfound作为包名:

src/main/java/com/example/lostfound/ ├── LostFoundApplication.java # 启动类 ├── config/ │ └── WebConfig.java # 登录拦截器配置 ├── controller/ │ ├── HomeController.java # 首页与搜索 │ ├── PublishController.java # 发布、详情、认领 │ └── UserController.java # 登录注册 ├── entity/ │ ├── User.java │ ├── LostItem.java │ └── ClaimRecord.java ├── mapper/ │ ├── UserMapper.java │ ├── LostItemMapper.java │ └── ClaimRecordMapper.java └── service/ ├── UserService.java ├── UserServiceImpl.java ├── LostItemService.java └── LostItemServiceImpl.java

资源文件目录:

src/main/resources/ ├── application.yml # 应用配置文件 └── templates/ ├── index.html # 首页列表 ├── login.html # 登录页 ├── register.html # 注册页 ├── publish.html # 发布页 └── detail.html # 详情页

3. 环境准备与数据库设计

3.1 开发环境说明

本文示例以 Java 8 + Spring Boot 2.7.x + MySQL 8.0 为常见环境,实际版本可以根据你的项目情况调整。需要提前安装:

  • JDK 1.8 或以上
  • Maven 3.6 或以上
  • MySQL 5.7 或 8.0
  • IDEA 或 Eclipse

如果本机没有安装 MySQL,也可以用 Docker 快速启动一个示例数据库,避免污染本机环境。

3.2 数据表结构设计

系统一共设计三张表:

  • user:用户表,存储账号、密码、昵称、手机号。
  • lost_item:失物信息表,通过type字段区分“寻物”和“招领”。
  • claim_record:认领记录表,记录谁在哪条信息下提交了认领申请。

lost_item中的status字段表示处理状态:0 为待处理,1 为已完成。这里没有用两张表分别存“寻物”和“招领”,而是用类型字段区分,查询和扩展都更方便。

建表 SQL 如下,可以直接在 MySQL 中执行:

CREATE DATABASE IF NOT EXISTS lost_found DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE lost_found; CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL, `nickname` VARCHAR(50) DEFAULT NULL, `phone` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `lost_item` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `type` VARCHAR(10) NOT NULL COMMENT 'lost-寻物 found-招领', `title` VARCHAR(100) NOT NULL, `description` TEXT, `category` VARCHAR(30) DEFAULT NULL, `place` VARCHAR(100) DEFAULT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-待处理 1-已完成', `contact_info` VARCHAR(100) DEFAULT NULL, `image_url` VARCHAR(255) DEFAULT NULL, `deleted` TINYINT DEFAULT 0, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_type` (`type`), KEY `idx_status` (`status`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `claim_record` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `item_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `nickname` VARCHAR(50) DEFAULT NULL, `content` VARCHAR(500) DEFAULT NULL, `contact_info` VARCHAR(100) DEFAULT NULL, `status` TINYINT DEFAULT 0 COMMENT '0-待确认 1-已同意', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_item_id` (`item_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

说明:lost_item表中的deleted字段用于 MyBatis-Plus 逻辑删除。这样信息被删除后不会真的从数据库消失,便于以后追溯,也符合校内应用“谨慎删除”的诉求。

3.3 创建 Spring Boot 工程并添加依赖

创建工程时选择 Maven 项目,然后在pom.xml中添加核心依赖:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里额外引入了spring-security-crypto,只用于密码的 BCrypt 加密,不会引入完整的 Spring Security 权限框架,避免增加学习成本。

接着在application.yml中配置数据源和 MyBatis-Plus:

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: 123456 thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

需要注意,password要改成你自己本机 MySQL 的密码。serverTimezone=Asia/Shanghai是为了解决 MySQL 8.x 的时区报错问题。

4. 后端核心代码实现

4.1 实体类与 Mapper 层

使用 MyBatis-Plus 时,实体类通过注解映射数据库表。先看用户实体:

// 文件路径:src/main/java/com/example/lostfound/entity/User.java 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("user") public class User { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; private String nickname; private String phone; private LocalDateTime createTime; }

失物信息实体:

// 文件路径:src/main/java/com/example/lostfound/entity/LostItem.java 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("lost_item") public class LostItem { @TableId(type = IdType.AUTO) private Long id; private Long userId; /** lost-寻物 found-招领 */ private String type; private String title; private String description; private String category; private String place; /** 0-待处理 1-已完成 */ private Integer status; private String contactInfo; private String imageUrl; private Integer deleted; private LocalDateTime createTime; private LocalDateTime updateTime; }

认领记录实体:

// 文件路径:src/main/java/com/example/lostfound/entity/ClaimRecord.java 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("claim_record") public class ClaimRecord { @TableId(type = IdType.AUTO) private Long id; private Long itemId; private Long userId; private String nickname; private String content; private String contactInfo; /** 0-待确认 1-已同意 */ private Integer status; private LocalDateTime createTime; }

Mapper 层代码非常简洁,直接继承BaseMapper就拥有了单表 CRUD 能力:

// 文件路径:src/main/java/com/example/lostfound/mapper/UserMapper.java package com.example.lostfound.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.lostfound.entity.User; public interface UserMapper extends BaseMapper<User> { }

LostItemMapperClaimRecordMapper结构完全一样,这里不再重复贴出。

4.2 Service 层实现

Service 接口继承 MyBatis-Plus 的IService,实现类继承ServiceImpl。先看用户模块:

// 文件路径:src/main/java/com/example/lostfound/service/UserService.java package com.example.lostfound.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.lostfound.entity.User; public interface UserService extends IService<User> { User login(String username, String password); /** * 注册,成功返回 null,失败返回错误提示 */ String register(User user); }

实现类:

// 文件路径:src/main/java/com/example/lostfound/service/UserServiceImpl.java package com.example.lostfound.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.lostfound.entity.User; import com.example.lostfound.mapper.UserMapper; import com.example.lostfound.service.UserService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Resource private PasswordEncoder passwordEncoder; @Override public User login(String username, String password) { User user = getOne(new LambdaQueryWrapper<User>() .eq(User::getUsername, username)); if (user != null && passwordEncoder.matches(password, user.getPassword())) { return user; } return null; } @Override public String register(User user) { long count = count(new LambdaQueryWrapper<User>() .eq(User::getUsername, user.getUsername())); if (count > 0) { return "用户名已存在"; } user.setPassword(passwordEncoder.encode(user.getPassword())); save(user); return null; } }

这里做了两件关键的事:

  1. 注册时用passwordEncoder.encode()对密码加密,数据库里不存明文。
  2. 登录时用matches()校验明文密码和密文是否匹配。

有些教程直接使用 MD5 加密,虽然也能跑通,但 BCrypt 自带随机盐,安全性更高,也符合工程实践要求。

失物信息模块:

// 文件路径:src/main/java/com/example/lostfound/service/LostItemService.java package com.example.lostfound.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.lostfound.entity.LostItem; import java.util.List; public interface LostItemService extends IService<LostItem> { List<LostItem> search(String keyword, String type); }

实现类:

// 文件路径:src/main/java/com/example/lostfound/service/LostItemServiceImpl.java package com.example.lostfound.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.lostfound.entity.LostItem; import com.example.lostfound.mapper.LostItemMapper; import com.example.lostfound.service.LostItemService; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.List; @Service public class LostItemServiceImpl extends ServiceImpl<LostItemMapper, LostItem> implements LostItemService { @Override public List<LostItem> search(String keyword, String type) { return list(new LambdaQueryWrapper<LostItem>() .eq(StringUtils.hasText(type), LostItem::getType, type) .like(StringUtils.hasText(keyword), LostItem::getTitle, keyword) .orderByDesc(LostItem::getCreateTime)); } }

eq(condition, column, value)这种写法会在条件不成立时自动忽略该查询条件。所以当type为空时,不会拼接type = '',避免查询结果异常。

4.3 Controller 层实现

首页 Controller 负责列表展示和搜索:

// 文件路径:src/main/java/com/example/lostfound/controller/HomeController.java package com.example.lostfound.controller; import com.example.lostfound.service.LostItemService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Resource; @Controller public class HomeController { @Resource private LostItemService lostItemService; @GetMapping("/") public String index(@RequestParam(required = false) String keyword, @RequestParam(required = false) String type, Model model) { model.addAttribute("items", lostItemService.search(keyword, type)); model.addAttribute("keyword", keyword); model.addAttribute("type", type); return "index"; } }

用户 Controller:

// 文件路径:src/main/java/com/example/lostfound/controller/UserController.java package com.example.lostfound.controller; import com.example.lostfound.entity.User; import com.example.lostfound.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import javax.annotation.Resource; import javax.servlet.http.HttpSession; @Controller public class UserController { @Resource private UserService userService; @GetMapping("/login") public String loginPage() { return "login"; } @PostMapping("/login") public String doLogin(String username, String password, HttpSession session, Model model) { User user = userService.login(username, password); if (user == null) { model.addAttribute("error", "用户名或密码错误"); return "login"; } session.setAttribute("loginUser", user); return "redirect:/"; } @GetMapping("/register") public String registerPage() { return "register"; } @PostMapping("/register") public String doRegister(User user, Model model) { String msg = userService.register(user); if (msg != null) { model.addAttribute("error", msg); return "register"; } return "redirect:/login"; } @GetMapping("/logout") public String logout(HttpSession session) { session.invalidate(); return "redirect:/"; } }

发布和认领相关的 Controller 是业务重点:

// 文件路径:src/main/java/com/example/lostfound/controller/PublishController.java package com.example.lostfound.controller; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.lostfound.entity.ClaimRecord; import com.example.lostfound.entity.LostItem; import com.example.lostfound.entity.User; import com.example.lostfound.service.ClaimRecordService; import com.example.lostfound.service.LostItemService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.http.HttpSession; import java.util.List; @Controller public class PublishController { @Resource private LostItemService lostItemService; @Resource private ClaimRecordService claimRecordService; @GetMapping("/publish") public String publishPage() { return "publish"; } @PostMapping("/publish") public String doPublish(LostItem item, HttpSession session) { User user = (User) session.getAttribute("loginUser"); item.setUserId(user.getId()); item.setStatus(0); lostItemService.save(item); return "redirect:/"; } @GetMapping("/item/{id}") public String detail(@PathVariable Long id, Model model, HttpSession session) { LostItem item = lostItemService.getById(id); if (item == null) { return "redirect:/"; } List<ClaimRecord> claims = claimRecordService.list( new LambdaQueryWrapper<ClaimRecord>() .eq(ClaimRecord::getItemId, id) .orderByDesc(ClaimRecord::getCreateTime)); User loginUser = (User) session.getAttribute("loginUser"); model.addAttribute("item", item); model.addAttribute("claims", claims); model.addAttribute("isOwner", loginUser != null && item.getUserId().equals(loginUser.getId())); return "detail"; } @PostMapping("/item/{id}/claim")

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

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

立即咨询