一、项目背景与意义
在高校校园环境中,学生、教职工遗失物品(如校园卡、钥匙、书本、电子产品等)的情况时有发生。传统的失物招领方式主要依靠公告栏张贴启事、校园广播或微信群发布信息,存在信息传播范围有限、查找效率低下、认领流程繁琐、物品状态难以追踪等问题。
“物归原主”校园失物招领管理系统旨在利用现代Web技术,构建一个线上信息发布、智能匹配、流程跟踪的一站式平台。该系统能够:
- 提升效率:实现失物信息的快速发布与精准检索,缩短物品归还周期。
- 优化体验:为拾主和失主提供便捷的线上沟通与认领渠道,简化线下流程。
- 促进文明:营造互帮互助的校园氛围,提升校园信息化管理水平。
- 数据沉淀:积累失物招领数据,为校园安全管理提供分析依据。
二、技术栈选型
本项目采用前后端分离架构,后端基于Spring Boot框架快速构建,具体技术栈如下:
后端技术栈
- 核心框架:Spring Boot 3.x
- Web框架:Spring MVC
- 数据访问:Spring Data JPA + Hibernate
- 数据库:MySQL 8.0
- 安全认证:Spring Security + JWT (JSON Web Token)
- API文档:SpringDoc OpenAPI (Swagger UI)
- 缓存:Redis (用于会话管理或热点数据)
- 文件存储:本地存储或集成OSS(如阿里云OSS)用于存放失物图片
- 构建工具:Maven
- 单元测试:JUnit 5, Mockito
前端技术栈(可选)
- 框架:Vue 3 / React 18
- UI组件库:Element Plus / Ant Design
- 状态管理:Pinia / Redux
- 构建工具:Vite / Webpack
- HTTP客户端:Axios
开发与部署
- 版本控制:Git
- 容器化:Docker, Docker Compose
- 部署:Linux服务器,Nginx反向代理
三、系统核心功能模块设计
系统主要分为前台用户端和后台管理端。
3.1 用户端功能
- 用户注册/登录:支持校园统一身份认证或手机号注册。
- 发布失物信息:填写物品名称、类别、遗失时间地点、特征描述、上传图片。
- 发布招领信息:拾主填写拾获物品信息、联系方式、保管地点。
- 信息浏览与搜索:按类别、时间、地点、关键词筛选和搜索失物/招领信息。
- 智能匹配推荐:系统根据物品特征、时间地点相似度,向用户推荐可能匹配的信息。
- 在线沟通:通过站内信或虚拟联系方式(保护隐私)与对方联系。
- 认领流程跟踪:从“待认领”到“已确认”、“已取回”的状态跟踪。
- 个人中心:管理自己发布的信息、查看历史记录、消息通知。
3.2 管理端功能
- 信息审核:审核用户发布的失物/招领信息,防止虚假或违规内容。
- 用户管理:管理用户账号、查看用户行为。
- 数据统计:统计各类物品遗失频率、高发地点、归还率等。
- 公告管理:发布系统公告或温馨提示。
- 类别管理:动态管理物品分类(如证件、电子产品、书籍等)。
四、数据库核心表设计
-- 用户表 CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL', `role` varchar(20) DEFAULT 'USER' COMMENT '角色:USER, ADMIN', `status` tinyint DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 失物信息表 CREATE TABLE `lost_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '发布者ID', `title` varchar(100) NOT NULL COMMENT '物品名称', `category_id` bigint DEFAULT NULL COMMENT '物品分类ID', `lost_time` datetime DEFAULT NULL COMMENT '遗失时间', `lost_place` varchar(200) DEFAULT NULL COMMENT '遗失地点', `description` text COMMENT '详细描述', `image_urls` json DEFAULT NULL COMMENT '图片URL数组', `contact_info` varchar(200) DEFAULT NULL COMMENT '联系方式', `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态:PENDING-待认领,MATCHED-已匹配,RETURNED-已归还,CLOSED-已关闭', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='失物信息表'; -- 招领信息表 CREATE TABLE `found_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '拾主ID', `title` varchar(100) NOT NULL, `category_id` bigint DEFAULT NULL, `found_time` datetime DEFAULT NULL COMMENT '拾获时间', `found_place` varchar(200) DEFAULT NULL COMMENT '拾获地点', `description` text, `image_urls` json DEFAULT NULL, `contact_info` varchar(200) DEFAULT NULL, `status` varchar(20) DEFAULT 'PENDING' COMMENT 'PENDING-待认领,MATCHED-已匹配,RETURNED-已归还,CLOSED-已关闭', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招领信息表'; -- 物品分类表 CREATE TABLE `item_category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', `icon` varchar(100) DEFAULT NULL COMMENT '图标', `sort` int DEFAULT '0' COMMENT '排序', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物品分类表'; -- 匹配记录表(记录失主与拾主的匹配关系) CREATE TABLE `match_record` ( `id` bigint NOT NULL AUTO_INCREMENT, `lost_item_id` bigint NOT NULL, `found_item_id` bigint NOT NULL, `match_score` decimal(5,2) DEFAULT NULL COMMENT '匹配度分数', `status` varchar(20) DEFAULT 'PENDING' COMMENT 'PENDING-待确认,CONFIRMED-已确认,FAILED-匹配失败', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_lost_found` (`lost_item_id`,`found_item_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='匹配记录表';五、Spring Boot后端核心代码示例
5.1 实体类 (Entity)
import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; import java.time.LocalDateTime; @Entity @Table(name = "lost_item") @Data public class LostItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; @Column(nullable = false, length = 100) private String title; @ManyToOne @JoinColumn(name = "category_id") private ItemCategory category; private LocalDateTime lostTime; private String lostPlace; @Column(columnDefinition = "TEXT") private String description; @Column(columnDefinition = "JSON") private String imageUrls; // 存储JSON数组字符串,如["url1","url2"] private String contactInfo; @Enumerated(EnumType.STRING) private ItemStatus status = ItemStatus.PENDING; @CreationTimestamp private LocalDateTime createTime; @UpdateTimestamp private LocalDateTime updateTime; } public enum ItemStatus { PENDING, MATCHED, RETURNED, CLOSED }5.2 数据访问层 (Repository)
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; import java.util.List; public interface LostItemRepository extends JpaRepository<LostItem, Long> { Page<LostItem> findByStatusOrderByCreateTimeDesc(ItemStatus status, Pageable pageable); Page<LostItem> findByCategoryIdAndStatus(Long categoryId, ItemStatus status, Pageable pageable); @Query("SELECT li FROM LostItem li WHERE " + "(:keyword IS NULL OR li.title LIKE %:keyword% OR li.description LIKE %:keyword%) " + "AND (:categoryId IS NULL OR li.category.id = :categoryId) " + "AND (:status IS NULL OR li.status = :status) " + "AND (:startTime IS NULL OR li.lostTime >= :startTime) " + "AND (:endTime IS NULL OR li.lostTime <= :endTime) " + "ORDER BY li.createTime DESC") Page<LostItem> search(@Param("keyword") String keyword, @Param("categoryId") Long categoryId, @Param("status") ItemStatus status, @Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime, Pageable pageable); // 智能匹配:查找时间地点相近的招领信息 @Query(value = "SELECT fi.*, " + "(CASE WHEN fi.found_place LIKE CONCAT('%', :lostPlace, '%') THEN 0.3 ELSE 0 END + " + " CASE WHEN ABS(TIMESTAMPDIFF(HOUR, fi.found_time, :lostTime)) < 24 THEN 0.4 ELSE 0 END + " + " CASE WHEN fi.category_id = :categoryId THEN 0.3 ELSE 0 END) AS score " + "FROM found_item fi " + "WHERE fi.status = 'PENDING' " + "ORDER BY score DESC LIMIT 10", nativeQuery = true) List<Object[]> findPotentialMatches(@Param("lostPlace") String lostPlace, @Param("lostTime") LocalDateTime lostTime, @Param("categoryId") Long categoryId); }5.3 服务层 (Service)
import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @Service @RequiredArgsConstructor public class LostItemService { private final LostItemRepository lostItemRepository; private final FoundItemRepository foundItemRepository; private final MatchRecordRepository matchRecordRepository; @Transactional public LostItem publishLostItem(LostItemDTO dto, Long userId) { LostItem item = new LostItem(); // 省略属性拷贝... item.setUser(new User(userId)); item.setStatus(ItemStatus.PENDING); LostItem saved = lostItemRepository.save(item); // 发布后尝试智能匹配 tryMatchForLostItem(saved); return saved; } public Page<LostItemVO> searchLostItems(String keyword, Long categoryId, ItemStatus status, LocalDateTime startTime, LocalDateTime endTime, Pageable pageable) { Page<LostItem> page = lostItemRepository.search(keyword, categoryId, status, startTime, endTime, pageable); return page.map(this::convertToVO); } private void tryMatchForLostItem(LostItem lostItem) { List<Object[]> potentialMatches = lostItemRepository.findPotentialMatches( lostItem.getLostPlace(), lostItem.getLostTime(), lostItem.getCategory() != null ? lostItem.getCategory().getId() : null ); for (Object[] row : potentialMatches) { FoundItem foundItem = foundItemRepository.findById((Long) row[0]).orElse(null); if (foundItem != null && foundItem.getStatus() == ItemStatus.PENDING) { MatchRecord record = new MatchRecord(); record.setLostItem(lostItem); record.setFoundItem(foundItem); record.setMatchScore((Double) row[1]); record.setStatus(MatchStatus.PENDING); matchRecordRepository.save(record); // 可发送站内信通知双方 } } } }5.4 控制层 (Controller)
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @RestController @RequestMapping("/api/lost-items") @RequiredArgsConstructor @Tag(name = "失物管理", description = "失物信息发布与查询相关接口") public class LostItemController { private final LostItemService lostItemService; @PostMapping @Operation(summary = "发布失物信息") public ResponseEntity<LostItemVO> publish(@RequestBody @Valid LostItemDTO dto, @RequestAttribute Long userId) { LostItemVO vo = lostItemService.publishLostItem(dto, userId); return ResponseEntity.ok(vo); } @GetMapping("/search") @Operation(summary = "搜索失物信息") public ResponseEntity<Page<LostItemVO>> search( @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) ItemStatus status, @RequestParam(required = false) LocalDateTime startTime, @RequestParam(required = false) LocalDateTime endTime, @PageableDefault(size = 10, sort = "createTime,desc") Pageable pageable) { Page<LostItemVO> page = lostItemService.searchLostItems(keyword, categoryId, status, startTime, endTime, pageable); return ResponseEntity.ok(page); } @GetMapping("/{id}") @Operation(summary = "获取失物详情") public ResponseEntity<LostItemVO> getDetail(@PathVariable Long id) { LostItemVO vo = lostItemService.getDetail(id); return ResponseEntity.ok(vo); } @PutMapping("/{id}/status") @Operation(summary = "更新失物状态") public ResponseEntity<Void> updateStatus(@PathVariable Long id, @RequestParam ItemStatus status, @RequestAttribute Long userId) { lostItemService.updateStatus(id, status, userId); return ResponseEntity.ok().build(); } }六、系统特色与创新点
- 智能匹配算法:基于时间、地点、类别的加权匹配,提高物品归还概率。
- 隐私保护:站内信沟通,避免直接暴露手机号等敏感信息。