☰
基于SpringBoot+微信小程序+LayUI的失物招领系统毕业设计实战
2026/10/27 7:27:29 网站建设 项目流程

简介:这是一套面向高校计算机相关专业学生的失物招领系统毕业设计完整源码,采用Java+SpringBoot后端、微信小程序与LayUI前端、MySQL数据库的技术组合,适合作为毕业设计、课程设计或自学练手项目。压缩包共411个文件,约24.89MB,涵盖Java源码、JSP页面、微信小程序wxml/wxss/wxs文件、前端js与css、数据库sql脚本、jar依赖及properties配置等,并附使用文档与部署教程,结构完整、层次清晰。该项目为个人高分毕业设计,已通过导师指导与答辩评审,得分97分,并在Windows 10/11环境下严格调试,下载即可运行。内容覆盖管理员、用户、订单、上传配置等核心模块,便于读者理解失物招领业务流程与前后端交互逻辑,快速搭建可运行环境并对照学习。目前已有222人学习下载,适合需要完整赛题方案与排错参考的读者。

1. 失物招领系统为什么值得做成一套完整毕业设计:从丢饭卡说起

校园里最常见的求助信息不是「求带饭」,而是「谁在二食堂捡到一张尾号 3721 的饭卡」。这类信息平时散落在班级群、表白墙、宿舍楼栋群,发出去两小时就被刷没了,捡到的人想还也找不到失主。失物招领系统要解决的就是这个信息撮合问题:让丢东西的人能发布、能检索、能收到通知,让捡到东西的人能低成本登记,最好还能按地点和时间筛一遍。这套基于 java+springboot+微信小程序+LayUI 的方案,本质上是「小程序做用户端入口 + SpringBoot 做业务后端 + LayUI 做管理后台」的三段式结构,适合计算机专业毕业设计,也适合想练一套完整前后端分离项目的 java 工程师。它不追求高并发,追求的是链路完整:登录、发布、审核、检索、认领、通知,每一环都能跑通、能演示、能写进论文。

2. 技术选型与工程骨架:为什么是 SpringBoot + 小程序 + LayUI

2.1 三个技术点各自承担什么角色

先把职责分清楚,后面写代码才不会乱。微信小程序负责 C 端交互,用户扫码即用,不用装 App,天然适合校园场景;它通过wx.login拿到 code,换 openid,再换后端签发的 token。SpringBoot 负责所有业务逻辑和数据持久化,对外暴露 REST 接口,对内用 MyBatis-Plus 操作 MySQL。LayUI 负责管理后台,管理员在这里审核失物信息、处理认领申请、管理分类和用户。三者之间只靠 HTTP + JSON 通信,边界清晰。

为什么后端选 SpringBoot 而不是别的?因为毕业设计周期短,SpringBoot 的自动配置能省掉大量 XML,起步依赖把 Web、MyBatis、Redis 一次拉齐。热搜里常出现「springboot版本太高」的抱怨,这里给个明确建议:用 2.7.x 这条线,不要一上来就 3.x。3.x 要求 JDK17,很多学校机房还是 JDK8,而且部分老教程的依赖在 3.x 下会报jakarta包名不兼容。2.7.x 配 JDK8 或 JDK11 都稳。

小程序端为什么不用 uniapp?uniapp 能一套代码多端打包,但毕业设计答辩时老师更认原生小程序结构,app.json、pages、wx.request这些一眼能看懂。原生开发调试也直接,微信开发者工具里改完即刷新,不用等编译。

2.2 数据库表怎么设计才够用又不臃肿

失物招领的核心表其实就五张,设计过度反而增加答辩解释成本。下面这张表是我一般会用的最小集合:

表名作用关键字段
user用户(含管理员)id, openid, nickname, role, phone
lost_item失物/招领信息id, type, title, description, place, happen_time, status, publisher_id
item_image图片附件id, item_id, url, sort
claim认领申请id, item_id, applicant_id, evidence, status
category分类字典id, name, icon, sort

lost_item.type用 0 表示失物、1 表示招领,这样一张表同时承载两种信息,检索时按 type 过滤即可。status用 0 待审核、1 已发布、2 已认领、3 已关闭,状态机简单但够用。claim表单独存认领申请,是因为一个物品可能有多人申请,需要管理员或发布者挑选,不能直接改物品状态了事。

2.3 后端工程分层与最小可运行配置

工程按 controller / service / mapper / entity / dto 分层,这是最不容易被挑毛病的结构。application.yml里几个必调参数:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/lost_found?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 5MB # 单张图片上限,小程序端也要同步限制 max-request-size: 20MB mybatis-plus: configuration: map-underscore-to-camel-case: true # 数据库下划线转 Java 驼峰 global-config: db-config: logic-delete-field: deleted # 逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0

serverTimezone必须写,否则插入时间会差 8 小时,这是血泪经验。map-underscore-to-camel-case打开后,happen_time能自动映射到happenTime,省掉一堆@Results注解。逻辑删除字段建议一开始就加,后面做「删除」功能时不用改表结构。

启动类加@MapperScan指向 mapper 包,否则 MyBatis-Plus 的 BaseMapper 注入会失败,报Invalid bound statement。这个报错新手最容易卡住,先检查扫描路径。

3. 小程序端登录与发布链路:从 wx.login 到图片上传

3.1 登录换 token 的完整流程

小程序登录不能只在前端拿 openid,必须走后端换取,否则 openid 暴露在前端可被伪造。流程是:小程序wx.login拿 code → 发给后端 → 后端用 code + appid + secret 调微信接口换 openid 和 session_key → 后端查库,没有就注册 → 签发自定义 token 返回。

后端换 openid 的核心代码:

// WxLoginService.java public String login(String code) { String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appId + "&secret=" + appSecret + "&js_code=" + code + "&grant_type=authorization_code"; // 用 RestTemplate 发起 GET,返回 JSON ResponseEntity<String> resp = restTemplate.getForEntity(url, String.class); JSONObject json = JSON.parseObject(resp.getBody()); String openid = json.getString("openid"); if (openid == null) { // errcode 40029 表示 code 无效,常见于 code 被重复使用 throw new BizException("登录失败:" + json.getString("errmsg")); } User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(0); userMapper.insert(user); } // token 用 JWT 或简单 UUID 存 Redis 都行,毕业设计用 JWT 更省事 return jwtUtil.generate(user.getId(), user.getRole()); }

参数说明:appId和appSecret放配置文件,不要硬编码进代码提交到仓库。code只能用一次,前端如果重复提交同一个 code 会拿到 40029,所以前端要在wx.login成功后立即发送,失败就重新wx.login。token 有效期建议 7 天,小程序端存wx.setStorageSync,每次请求放 header。

3.2 发布失物信息的表单与校验

发布页字段:类型(失物/招领)、标题、描述、地点、发生时间、图片(最多 3 张)。前端用picker选类型和时间,input填标题描述,wx.chooseMedia选图。提交前做非空校验,标题不超过 30 字,描述不超过 200 字。

// pages/publish/publish.js submit() { const { type, title, desc, place, happenTime, images } = this.data; if (!title.trim()) return wx.showToast({ title: '请填标题', icon: 'none' }); if (!place.trim()) return wx.showToast({ title: '请填地点', icon: 'none' }); // 先传图,再提交表单,避免表单成功但图片丢失 this.uploadImages(images).then(urls => { wx.request({ url: baseUrl + '/api/item/publish', method: 'POST', header: { 'Authorization': wx.getStorageSync('token') }, data: { type, title, description: desc, place, happenTime, images: urls }, success: res => { if (res.data.code === 200) { wx.showToast({ title: '已提交,等待审核' }); wx.navigateBack(); } } }); }); }

逻辑说明:先传图再提交表单,是因为图片上传可能失败,如果先提交表单再传图,会出现「信息已发布但没图」的脏数据。happenTime用时间戳传,后端统一转LocalDateTime。图片上传接口单独写,接收MultipartFile,存本地磁盘或对象存储,返回可访问 URL。

3.3 图片上传的存储与访问

后端上传接口:

@PostMapping("/api/upload") public Result upload(@RequestParam("file") MultipartFile file) { String original = file.getOriginalFilename(); String ext = original.substring(original.lastIndexOf(".")); // 用 UUID 重命名,避免中文名和重名覆盖 String filename = UUID.randomUUID().toString().replace("-", "") + ext; File dest = new File(uploadDir + File.separator + filename); if (!dest.getParentFile().exists()) dest.getParentFile().mkdirs(); file.transferTo(dest); return Result.ok("/uploads/" + filename); }

参数说明:uploadDir配在 yml 里,指向一个绝对路径,不要用相对路径,否则打包成 jar 后工作目录变化会找不到。返回的 URL 要能被小程序访问,本地开发时小程序开发者工具要勾选「不校验合法域名」,否则http://localhost会被拦。生产环境必须上 HTTPS 并配好域名,这是微信的硬要求。

4. LayUI 管理后台与审核状态机:把「已认领」管明白

4.1 后台页面结构与接口对接

LayUI 后台用最经典的layui-admin布局:左侧菜单、顶部栏、中间iframe或table渲染。失物列表页用table.render拉/api/admin/item/page,带分页和状态筛选。审核按钮绑tool事件,调/api/admin/item/audit。

// admin/item/list.js table.render({ elem: '#itemTable', url: '/api/admin/item/page', method: 'get', page: true, cols: [[ { field: 'id', title: 'ID', width: 60 }, { field: 'title', title: '标题', width: 180 }, { field: 'type', title: '类型', width: 80, templet: d => d.type === 0 ? '失物' : '招领' }, { field: 'place', title: '地点', width: 140 }, { field: 'status', title: '状态', width: 90, templet: d => statusText(d.status) }, { title: '操作', toolbar: '#barDemo', width: 160 } ]] });

逻辑说明:templet用来把数字状态转成中文,避免后端返回一堆 0/1/2 让老师看不懂。分页参数 LayUI 默认传page和limit,后端用 MyBatis-Plus 的Page对象接收,字段名对不上就手动@RequestParam。

4.2 审核与认领的状态流转

状态机是这套系统最容易被问的地方,必须讲清楚。物品从发布到结束经历:待审核(0) → 已发布(1) → 已认领(2) 或 已关闭(3)。认领申请单独有状态:待处理(0) → 通过(1) / 拒绝(2)。当某个认领申请被通过,物品状态才置为 2,同时其他待处理申请自动置为 2(拒绝)。

@Transactional public void approveClaim(Long claimId) { Claim claim = claimMapper.selectById(claimId); if (claim.getStatus() != 0) throw new BizException("该申请已处理"); claim.setStatus(1); claimMapper.updateById(claim); // 物品置为已认领 LostItem item = itemMapper.selectById(claim.getItemId()); item.setStatus(2); itemMapper.updateById(item); // 同物品其他待处理申请全部拒绝 claimMapper.rejectOthers(claim.getItemId(), claimId); }

参数说明:@Transactional必须加,否则物品状态改了但申请状态没改,数据不一致。rejectOthers用一条 update 批量处理,别在循环里单条更新。认领通过后可以给申请人发订阅消息,但订阅消息需要用户先授权,毕业设计里做成「可选」即可,不强求。

4.3 检索与筛选的 SQL 写法

用户端检索要支持关键词、类型、地点、时间范围。MyBatis-Plus 的QueryWrapper拼条件:

public Page<LostItem> search(ItemQuery query, int page, int size) { QueryWrapper<LostItem> qw = new QueryWrapper<>(); qw.eq("status", 1); // 只查已发布 if (query.getType() != null) qw.eq("type", query.getType()); if (StringUtils.hasText(query.getKeyword())) { qw.and(w -> w.like("title", query.getKeyword()) .or().like("description", query.getKeyword())); } if (StringUtils.hasText(query.getPlace())) qw.like("place", query.getPlace()); if (query.getStartTime() != null) qw.ge("happen_time", query.getStartTime()); qw.orderByDesc("create_time"); return itemMapper.selectPage(new Page<>(page, size), qw); }

逻辑说明:qw.and(w -> ...)把关键词的 or 条件包起来,否则会和其他 eq 条件混在一起导致逻辑错误,这是 QueryWrapper 最常见的坑。like默认是%keyword%,数据量大时索引失效,但校园场景数据量小,可以接受。时间范围用ge和le,前端传 ISO 格式字符串。

5. 避坑与排查:那些让答辩翻车的细节

5.1 小程序请求 400 或 401 反复出现

现象:登录后调接口仍返回 401,或偶尔 400。原因通常是 token 没放进 header,或者 header 名大小写不一致。小程序wx.request的 header 是对象,后端@RequestHeader("Authorization")区分大小写。解决:统一用Authorization,前端封装一个request方法,自动带 token,401 时清缓存跳登录页。

5.2 图片上传成功但显示裂图

现象:后台能看到图片 URL,但小程序里image组件不显示。原因是返回的是相对路径/uploads/xxx.jpg,小程序需要完整 URL。解决:后端返回时拼上域名,或前端在src前拼baseUrl。另外本地开发时localhost在小程序真机上不可达,真机调试要用局域网 IP。

5.3 时间字段差 8 小时

现象:发布的信息时间比实际早 8 小时。原因是 JDBC URL 没配serverTimezone,或实体类用了java.util.Date而数据库是datetime。解决:URL 加serverTimezone=Asia/Shanghai,实体统一用LocalDateTime,Jackson 配spring.jackson.time-zone=GMT+8。

5.4 审核后列表不刷新

现象:后台点了审核,状态没变。原因是 LayUI table 不会自动重载,需要手动table.reload。解决:审核回调里调table.reload('itemTable'),或直接location.reload()。更稳的是在 success 回调里 reload,别在 ajax 外面写。

5.5 打包成 jar 后上传目录丢失

现象:本地跑正常,打成 jar 部署后图片传不进去。原因是用了相对路径./uploads,jar 运行时工作目录变了。解决:uploadDir配绝对路径,如/data/lostfound/uploads,启动时检查目录是否存在并创建。

6. 让这套系统在答辩里加分:订阅消息与检索优化

6.1 用订阅消息替代短信通知

毕业设计里做通知,最省钱也最像真实产品的是微信订阅消息。用户发布失物时授权一次,有人发布同地点同类型的招领时,后端调订阅消息接口推一条。申请模板在微信公众平台配置,拿到templateId,后端调subscribeMessage.send。

public void sendClaimNotify(String openid, String itemTitle, Long itemId) { Map<String, Object> data = new HashMap<>(); data.put("thing1", Map.of("value", itemTitle)); data.put("time2", Map.of("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")))); Map<String, Object> body = Map.of( "touser", openid, "template_id", templateId, "page", "pages/detail/detail?id=" + itemId, "data", data ); // 调微信接口,access_token 要缓存,不能每次重新获取 String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + getAccessToken(); restTemplate.postForEntity(url, body, String.class); }

参数说明:access_token有效期 7200 秒,必须缓存,用 Redis 或本地ConcurrentHashMap加过期时间都行,每次重新获取会触发频率限制。data里的字段名要和模板配置一致,thing类型限 20 字,超了会报错。订阅消息是「一次授权一次推送」,用户不授权就发不了,所以只能作为增强,不能作为唯一通知手段。

6.2 检索的进阶做法:加个简单的分词

关键词检索用like在数据量小时够用,但搜「黑色钱包」搜不到「黑钱包」时,可以引入简单分词。热搜里提到 hanlp 分词在 SpringBoot 的用法,思路是:发布时把标题描述分词后存一个keywords字段,检索时也分词再匹配。毕业设计不必上 Elasticsearch,用 hanlp 的StandardTokenizer把词用空格拼起来存进keywords,检索时like匹配即可。

// 发布时生成关键词 public String extractKeywords(String text) { Segment segment = HanLP.newSegment().enableCustomDictionary(false); List<Term> terms = segment.seg(text); return terms.stream() .map(Term::getWord) .filter(w -> w.length() > 1) // 过滤单字,减少噪音 .distinct() .collect(Collectors.joining(" ")); }

逻辑说明:分词后存成空格分隔的字符串,检索时对查询词也分词,然后qw.like("keywords", word)逐个匹配。这样「黑色钱包」和「黑钱包」都能命中「钱包」。注意 hanlp 依赖较大,打包后 jar 会变胖,毕业设计可以接受,生产环境要评估。

6.3 一个我踩过的坑:别在答辩前改依赖版本

我见过太多人在答辩前一天升级 SpringBoot 版本,结果javax变jakarta,一堆 import 报红,连夜回滚。这套系统的依赖版本在开工时就锁死,写进pom.xml的properties里,中途不要动。小程序基础库版本也一样,开发者工具里选一个稳定版,别追最新。答辩演示前,把数据库脚本、配置文件、启动命令写在一张纸上,按顺序跑一遍,比任何优化都管用。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询