
1. 从“丢东西”到“一个完整系统”这个项目的需求拆解与功能边界在校园里丢东西这件事太常见了。一卡通、耳机、雨伞、课本几乎每个学期都有人把东西落在教室、食堂或者操场上。过去学校怎么处理要么靠失物招领处的纸质登记本要么在QQ群、宿舍群里发消息刷屏。这两种方式的问题很直接信息分散、没人整理、也没有一个“物归原主”的闭环。等你想起去招领处看看时东西可能已经被清理掉了。所以做一个校园失物招领网站核心不是“贴个公告栏”而是把一条完整的业务链路跑通丢东西的人能快速发布失物信息拾到东西的人能快速登记招领信息系统根据物品特征自动匹配撮合最后完成认领流程并留下记录。这个链路看起来简单真做起来涉及的环节一点也不少。我在设计这款系统时把它的功能拆成了五个核心块失物登记与招领登记用户注册登录后可以发布“我丢了什么”或“我捡到了什么”附带物品名称、类别、丢失/拾取地点、时间、图片和详细描述。智能匹配与模糊搜索根据物品分类、地点、关键词做匹配比如有人发布了“在二食堂捡到蓝色雨伞”系统自动把这条信息推送给曾经发布过“丢失蓝色雨伞”的用户。认领流程闭环拾主可以对失物发起“待认领”标记失主看到信息后提交认领申请拾主审核后双方线下核验平台记录整个流程状态。校园公告与后台管理管理员可以审核所有发布的失物、招领信息过滤虚假或违规内容发布寻物启事置顶公告。数据统计统计发布量、找回率、热门丢失地点等数据方便学校后勤了解情况也能做成简单的可视化图表。这套系统面向三类用户学生普通用户、学校后勤/保卫处管理员、以及平台的运维人员。技术栈上项目采用前后端分离架构后端统一提供RESTful接口前端通过Axios异步调用所有数据持久化到MySQL数据库。作为项目负责人我当初定技术选型的时候给团队也解释过为什么不把功能堆得更重。失物招领这类公共服务场景核心价值是“快”和“准”你不需要拼单、不需要高并发秒杀、也没有复杂的订单状态机。所以功能设计上一定要克制把认领闭环做扎实把搜索匹配做得顺手比上一个“校园社交广场”有用得多。2. 技术栈选型复盘为什么是 SpringBootVue3MyBatis2.1 后端SpringBoot承担的是“无痛开发”和“快速交付”选SpringBoot几乎是没有悬念的事情。它省去了Spring MVC一堆繁琐的XML配置内嵌Tomcat打包即运行部署成本极低特别适合课程设计、毕业设计或者校园内部的工具型系统。这里有一个经常被新手忽略的点SpringBoot版本选择决定了你后面要踩多少坑。我用的SpringBoot 2.7.x而不是3.x。原因有三2.7.x对javax包命名空间的兼容性更好很多老一点的第三方库和教程代码直接能用3.x强制要求JDK17很多学生的电脑还是JDK8环境不匹配会非常痛苦当前这套项目的核心是业务功能的完整打通不是追求框架特性最新稳定压倒一切。JDK方面我用的是JDK 8对应SpringBoot 2.x系列完全没问题。如果你的机器装的是JDK 11或17编译运行也没有障碍只需要在pom里注意maven.compiler.source和target的版本对齐。很多同学部署失败八成是JDK版本和SpringBoot版本不对应记住一个黄金匹配关系SpringBoot 2.x配JDK8-17SpringBoot 3.x配JDK17。2.2 前端Vue3Element Plus是构建后台管理界面效率最高的组合之一前端用Vue3原因也很直白组合式APIComposition API写业务逻辑更清晰代码复用性比Vue2的Options API好一截。配合Element Plus组件库表格、表单、弹窗、上传组件开箱即用一个后台管理界面两天就能搭出雏形。使用Vite作为构建工具在开发阶段热更新非常快本地起服务基本是即时反馈。注意正式发布时一定要执行npm run build生成dist目录然后交给Nginx或者后端静态资源映射。有的同学直接把npm run dev的地址发给别人用这既不安全性能也差。开发服务器本身带有详细的错误堆栈信息完全暴露内部实现细节显然不适合当生产环境。2.3 持久层为什么是 MyBatis 而不是 MyBatis-Plus这是我在答辩或者面试时最容易被问到的问题。很多同学学了MyBatis-Plus之后就回不去写XML的MyBatis了觉得后者麻烦。但在这个项目里我用的是原生MyBatis理由如下动态SQL能力更强失物招领的查询条件极多物品类型、时间范围、地点、关键词MyBatis的where、if、choose标签能非常精细地控制SQL拼接让每一条查询都贴合业务场景。SQL可控性高原生MyBatis的SQL都写在XML文件里review时一目了然。MyBatis-Plus虽然也有了LambdaQueryWrapper但当条件复杂时逻辑会散落在Java代码里反而不容易定位。面试加分MyBatis是Java后端面试的必考项面试官非常喜欢问“动态SQL标签”“一级缓存二级缓存”“#{}和${}的区别”。如果你说自己用的是MyBatis-Plus但对底层MyBatis不熟在面试中很容易露怯反过来你只要在原生的基础上把Plus搞明白属于降维打击。2.4 数据库MySQL 8.0的坑与配置心得MySQL 8.0现在是绝对主流但安装配置上确实有一些反直觉的地方。很多同学卡在“安装完MySQL命令行登录不上”或者“Navicat连接报错2059”这些都是老生常谈的问题。我在这里分享一个最关键的配置点# application.yml 中的数据库连接配置 spring: datasource: url: jdbc:mysql://localhost:3306/lost_found?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltruerewriteBatchedStatementstrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver其中两个参数是8.0版本特有的必须写serverTimezoneAsia/Shanghai8.0之前的驱动不强制要求8.0之后如果不指定你会在日志里看到The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这种乱码报错本质是服务器和客户端时区不一致。allowPublicKeyRetrievaltrueMySQL 8.0默认使用caching_sha2_password插件认证如果你的连接工具或驱动版本不够会报Public Key Retrieval is not allowed。加上这个参数允许客户端从服务器获取公钥。3. 数据库设计失物招领系统最核心的是表和表之间的“状态流转”3.1 核心表结构与设计思路这个系统的数据库有六张核心表我逐个分析一下设计初衷和关键字段用户表t_user字段类型说明idbigint自增主键usernamevarchar(50)用户名唯一索引passwordvarchar(255)BCrypt加密后的密码real_namevarchar(50)真实姓名student_novarchar(20)学号/工号phonevarchar(20)联系电话roletinyint角色0-普通用户1-管理员avatarvarchar(255)头像URLcreate_timedatetime注册时间失物表t_lost和招领表t_found这两张表结构高度相似可以视作一体两面的设计。每个表都包含物品名称、物品分类、物品描述、图片URL、丢失/拾取地点、发生时间、状态字段status0-待匹配1-已匹配待认领2-已完成3-已关闭。状态字段是整张表的灵魂认领闭环靠的就是这个字段的逐步流转。为什么不把失物和招领合并成一张表加一个type字段区分合并确实可以但会导致字段语义混乱丢失地点和拾取地点的意义不同发布时间与丢失时间的业务含义也不同。两张表分开后各自的查询、统计SQL逻辑更加清晰虽然多了一点冗余代码但从长期维护角度看是划算的。认领记录表t_claim_record这张表是“闭环”的关键设计时我特意增加了几个辅助字段claim_message认领说明、proof_answer核验问题回答。拾主在发布招领时可以设置一个核验问题比如“你的伞是什么颜色的”认领人回答正确后拾主才确认。这一步能极大减少错领、冒领的情况。公告表t_notice管理员发布置顶公告用的字段包括标题、内容、是否置顶、发布时间。操作日志表t_operation_log记录关键操作如发布、认领申请、确认认领、审核删除等。这张表虽然不是用户直接可见的但后期排查问题、做数据统计时非常有用。3.2 状态机设计从发布到完成每一步都有迹可循这里我强烈建议大家在开发前先把状态流转图画出来不是用工具画而是写在需求文档里不然写着写着就乱套了。我这个系统定义了一套非常明确的状态规则发布失物初始状态为“0-待匹配”拾主发布招领后如果系统检测到可能匹配的失物可在后台给双方发送站内信提醒失主看到招领信息后点击“认领申请”此时招领物品状态从“0”变为“1-待确认”拾主查看认领申请和核验回答同意后状态变为“2-已完成”如果超过7天无人认领拾主可手动操作将物品转为“3-已关闭”或选择捐赠处理。这个状态机的好处是前端的操作按钮统一由状态驱动。比如状态为0时展示“申请认领”按钮状态为1时只展示“查看申请”状态为2时页面灰置。代码层面用枚举管理状态不用魔法数字散落在业务代码里。public enum ClaimStatusEnum { WAITING(0, 待匹配), CONFIRMING(1, 待确认), COMPLETED(2, 已完成), CLOSED(3, 已关闭); private final Integer code; private final String desc; // 省略构造函数和getter }3.3 数据库字段设计的三个避坑经验第一个坑图片不要存base64字符串进数据库。这种做法在小项目里很常见但后患无穷。一张手机拍的照片base64后可能有5-10MB直接拖垮数据库性能。我的方案是图片上传后保存到本地磁盘或OSS对象存储数据库中只存储访问URL。本地存储时需要注意不能直接放在static目录下然后打包部署否则重启一下文件就丢了。正确做法是配置外部磁盘路径再通过资源映射暴露给前端访问# application.yml 配置外部文件存储路径 file: upload-dir: D:/lost-found-upload/ # 一个简单的WebMvcConfigurer配置将外部目录映射为URL路径Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(file: uploadDir); } }第二个坑时间字段不要用varchar。我看到过一些同学的数据库时间字段拿字符串存后面做时间范围查询、排序时各种头大。统一用datetimeJava侧用LocalDateTime对应MyBatis会自动完成映射。第三个坑软删除优于物理删除。用户发布的信息也好、管理员删除的记录也好都不要直接执行DELETE。给表加上deleted字段0-正常1-已删除查询时统一加WHERE deleted 0。这样误删了还能恢复做数据统计时也不会因为物理删除导致数据缺口。4. 后端核心逻辑实现认证鉴权、匹配算法与并发问题4.1 JWT登录认证与拦截器设计这个项目为了保持前后端分离的纯粹性没有使用Spring Security而是选择了轻量级的JWT拦截器方案。不是说Spring Security不好而是对于课程设计、小型系统来说Security的过滤器链和配置项太重了学习成本高而且很多同学配完Security-Cors-Session那套组合拳就已经晕了。JWT的核心逻辑很简单用户登录成功后端按固定算法生成一串加密的Token字符串包含用户ID、角色、过期时间三部分前端拿到后存进LocalStorage每次请求时在请求头里带上Authorization: Bearer token。后端写一个拦截器统一校验校验通过就把用户信息放入ThreadLocal业务Controller直接用省得每次查询数据库找用户。注意重点JWT密钥不要硬编码在代码里。# application.yml jwt: # 通过JWT密钥生成网站生成至少32个字符 secret: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 expire-hours: 72我见过有人把secret写成abc123这种密钥在JWT.io网站上粘贴你的Token就能暴力解出来。作为经验之谈密钥要满足两个条件足够长至少32字符、包含大小写字母和数字的混合排列。拦截器还要处理好两个特殊情况预检OPTIONS请求放行和路径白名单。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行跨域预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); // 校验逻辑... } }如果不放行OPTIONS请求前端跨域调接口时会报CORS错误但这不是跨域配置错了而是拦截器把预检请求也拦下来校验了。4.2 失物匹配基于分类关键词地点的多维度评分算法这个模块是整个系统里最有技术含量的一块也是我当时花时间最多的地方。所谓“智能匹配”本质上是一个多条件的相似度打分物品分类相同得分30分。分类维度我预设了大类比如数码产品、证件卡类、服饰类、书籍类、生活用品类。关键词匹配从失物和招领的描述中提取关键词每匹配一个15分。因为中文分词太复杂我没有引入IK分词器而是用简单的前缀后缀匹配比如“雨伞”“U盘”“校园卡”这类高频词。地点匹配丢失地点和拾取地点在同一区域精确到楼栋20分时间维度丢失时间和拾取时间差在24小时内10分。匹配分大于等于50分时系统生成一条匹配提醒通过站内信推送给双方用户。这个方案比传统SQL模糊搜索精准很多网上一些课程设计自己做的时候只会用LIKE %关键词%匹配结果要么过多要么过少体验很差。关于上传图片进行图像识别匹配我曾经在第二版迭代时考虑过把物品图片放进VGG16网络提取特征向量计算相似度。但后来放弃了这个方案原因很实际硬件受限且训练数据难以获取校园失物匹配的核心价值在于文字语义描述而不是图像特征。与其做一个准确率不高的图像识别功能赶工上线不如把关键词匹配和多条件组合做扎实。当然这一点可以作为项目的“扩展亮点”写进答辩PPT里表示自己有深入了解。4.3 并发申请认领的幂等性设计这个分析比较微妙很多人会忽略。假设一个热门物品比如一台笔记本电脑有多个用户同时点击“申请认领”这个时候不做任何处理就可能出现一条招领记录被多人同时认领成功的情况。方案其实不复杂数据库层面控制状态变更。我使用了MySQL乐观锁在招领表里增加一个version字段更新状态时只有当前版本号匹配才允许更新。UPDATE t_found SET status 1, version version 1 WHERE id #{foundId} AND status 0 AND version #{version}如果更新返回的影响行数为0说明已经被其他人抢先申请了此时后到的请求直接返回“该物品已被申请认领”前端弹出提示。这套方案比Java代码里加synchronized或者Redis锁简单得多且天然应对多实例部署场景。4.4 文件上传的落地细节前端用Element Plus的el-upload组件后端接收MultipartFile最简单的做法是在Controller里写一个PostMapping(/upload)接口。这里有几个需要注意的细节文件类型白名单过滤只允许jpg、png、gif、webp否则有人上传一个jsp恶意文件就麻烦大了文件重命名用UUID时间戳重新拼接文件名尽量不要直接用原始文件名避免中文路径和重复文件名的问题文件大小限制在SpringBoot配置中设置spring.servlet.multipart.max-file-size5MB项目根目录或者外部磁盘目录的区分前文已经提过这里不再重复。5. Vue3 前端开发实战页面拆分、状态管理与请求封装5.1 前端页面模块与路由设计前端部分我划分了以下页面首页Home搜索框失物/招领信息流支持分类筛选和时间排序失物大厅/招领大厅列表页面展示所有状态为正常的记录分页加载详情页展示物品图片、描述、发布时间、当前状态、认领按钮个人中心查看我发布的失物/招领、我发出的认领申请、我收到的认领申请、站内消息后台管理管理员专属用户管理、审核管理、公告管理、数据统计。路由结构设计时我做了动态路由的处理普通用户和管理员登录后看到的菜单不同。实现方式有两种一种是前端写死role判断另一种是登录后后端按角色返回菜单列表前端router.addRoute动态注册。我选的是第二种更接近真实企业项目的做法。Vue Router 4配合Vue3使用支持createRouter和createWebHistory模式。这里注意history模式需要后端配合做history fallback否则刷新页面会404。部署到Nginx时需要在location /里配置try_files $uri $uri/ /index.html;或者图省事直接用createWebHashHistory。5.2 请求封装与Token统一管理前端所有HTTP请求我统一封装在utils/request.js中使用Axios作为基础库。拦截器的核心逻辑有三层请求拦截器从LocalStorage取出Token放入请求头响应拦截器判断HTTP状态码是否为200业务状态码是否为200如果Token过期后端返回401清除本地登录信息跳转登录页错误处理统一弹出Message提示避免每个页面重复写catch判断。// utils/request.js 核心代码片段 import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /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.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } )Vite的跨域代理配置也是重头戏。开发阶段直接让浏览器请求后端接口必然跨域我用了Vite的server.proxy配置把/api前缀的请求代理到http://localhost:8080// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }这样一个关键问题被完美规避前端代码里所有接口路径都只写相对路径不写后端完整地址后续部署时只要调整Nginx的转发配置即可做到环境无缝切换。5.3 Vue3组合式API实践以“发布失物表单”为例发布失物表单看起来so easy就是几个输入框加一个上传组件。但用组合式API组织后逻辑复用度高很多。我自定义了一个usePublish组合式函数统一处理表单数据、图片上传进度、校验规则和提交逻辑script setup const formRef ref(null) const form reactive({ itemName: , category: , description: , lostDate: , lostLocation: , images: [] }) const rules { itemName: [ { required: true, message: 请输入物品名称, trigger: blur }, { min: 2, max: 30, message: 长度在2到30个字符之间, trigger: blur } ], category: [{ required: true, message: 请选择物品分类, trigger: change }], lostLocation: [{ required: true, message: 请选择丢失地点, trigger: change }] } const submitForm () { formRef.value.validate(async valid { if (!valid) return // 提交逻辑 }) } /scriptElement Plus的表单校验用的是async-validator自定义校验规则也非常方便。比如手机号校验const phoneValidator (rule, value, callback) { const reg /^1[3-9]\d{9}$/ if (!reg.test(value)) { callback(new Error(手机号格式不正确)) } else { callback() } }6. MyBatis 实践动态SQL、缓存与日志调试6.1 动态SQL解决“条件查询”难题失物大厅的筛选条件非常多按物品名称模糊搜索、按分类、按地点、按时间范围、按状态。如果用Java代码拼接SQL或者到处写if判断代码会非常臃肿。MyBatis的动态SQL标签就是为此设计的。select idsearchLostItems resultTypecom.example.entity.Lost SELECT * FROM t_lost where if testitemName ! null and itemName ! AND item_name LIKE CONCAT(%, #{itemName}, %) /if if testcategory ! null and category ! AND category #{category} /if if testlocation ! null and location ! AND lost_location LIKE CONCAT(%, #{location}, %) /if if teststartDate ! null AND lost_time gt; #{startDate} /if if testendDate ! null AND lost_time lt; #{endDate} /if AND status IN (0, 1) AND deleted 0 /where ORDER BY create_time DESC /select注意我用了where标签它会自动去掉第一个多余的AND避免SQL语法错误。另外有一个重要细节模糊搜索不要用LIKE %${itemName}%必须用CONCAT(%, #{itemName}, %)。前者是字符串拼接存在SQL注入风险后者是预编译参数绑定安全可靠。6.2 删掉还是没有删掉MyBatis的一级缓存和二级缓存这个知识点是面试常客也直接关系项目性能。一级缓存是SqlSession级别的同一个SqlSession中执行相同的查询第二次直接从缓存中取不再查库。但Spring管理下每次Mapper方法执行完SqlSession就关闭了所以一级缓存基本只在同一个事务里生效。默认开启无需配置。二级缓存是Mapper级别的需要在XML映射文件中加cache/配置才能开启。我刚写了二级缓存很快发现了一个问题失物列表数据是高频查询、低频更新适合用缓存但一旦用户发布了新失物旧的列表缓存并不会自动失效。MyBatis的二级缓存失效时机是整个Mapper命名空间执行了增删改才失效这会导致数据一致性延迟。所以我的建议是这个项目不要开启二级缓存。系统单机部署、数据量大不到哪里去MySQL单表十万条内走索引查询都能扛住加了二级缓存反而要处理缓存一致性问题得不偿失。如果你想优化性能在后端Service层加Redis缓存或者在前端保持数据分页加载效果更直接。6.3 打印SQL日志IDEA MyBatis Log Free 插件这个插件解决的是“MyBatis预处理参数拼装后SQL永远显示?占位符”的问题。我遇到的场景是用PreparedStatement预编译后日志里只能看到 Preparing: SELECT * FROM t_lost WHERE item_name LIKE CONCAT(%, ?, %) Parameters: 耳机(String)但我想看到拼装完的真实SQL长什么样方便排查SQL语法错误。如果结合整体开发环境来看IDEA插件MyBatis Log Free可以把PreparedStatement日志还原成全量SQL类似SELECT * FROM t_lost WHERE item_name LIKE CONCAT(%, 耳机, %)。这对调复杂动态SQL帮助特别大。另外还需要在application.yml里开启日志级别logging: level: com.example.lostfound.mapper: debug这里强调一下日志级别要指向Mapper接口所在的包不是随便一个root级别就完事。很多教程让你配logging.level.com.exampledebug这会把Spring框架自身的调试日志全部打印出来日志量大得没法看。精确到mapper包级别才是最优解。6.4 关于“表不存在自动建表”的优雅处理最近很多人在问Spring Boot能不能在运行时不依赖手动导入SQL文件自动建表。可以用Spring Boot的schema.sqldata.sql机制spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql >mysql -u root -p init-db.sql原因很简单自动初始化虽然省事但如果你接入了其他数据库工具如Flyway做版本管理两者相互配合时优先级容易乱。学生项目和中小型系统用带版本号的SQL脚本文件管理是最简单最直观的。7. 部署上线避坑环境配置、跨域、时区与静态资源7.1 后端打包与数据库初始化后端使用mvn clean package打成Jar包执行前注意几个重点确认pom里没有漏掉打包插件。SpringBoot的spring-boot-maven-plugin一定要配否则打出来的Jar包不包含依赖直接报No main manifest attribute错误。数据库初始化顺序。先创建数据库CREATE DATABASE lost_found DEFAULT CHARACTER SET utf8mb4;注意要用utf8mb4而不是utf8因为utf8在MySQL里存不了emoji表情也没法覆盖所有生僻字。生产环境的密码不要写死在yml里。可以改成环境变量引用spring: datasource: password: ${DB_PASSWORD}启动时用java -jar lostfound.jar --DB_PASSWORDxxx或者在服务器上配置环境变量。7.2 Nginx反向代理与前端刷新404部署前后端分离项目最痛苦的就是代理配置。我提供一份能直接用的基础配置server { listen 80; server_name your-domain.com; # 前端静态资源 root /usr/share/nginx/html; index index.html; # 前端history路由刷新404问题 location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 图片等静态资源映射到本地磁盘目录 location /files/ { alias /data/lost-found-upload/; } }proxy_pass http://127.0.0.1:8080/;末尾有一个“/”作用是去掉/api前缀这和后端Controller的RequestMapping路径要匹配好。如果不想去掉前缀就写成没有斜杠的形式。这个细节网上很多人没讲清楚导致代理出现404但后端日志明明有请求到达的诡异现象。7.3 上线前必须检查的三个点跨域配置开发时用Vite代理而生产用Nginx反向代理的话后端Controller加的CrossOrigin或全局CORS配置可以保留但不建议作为生产数据库安全机制来完全依赖因为Nginx层都已统一处理。我见过有的项目启用了allowedOriginPatterns(*)且allowCredentials(true)组合这在浏览器端会有潜在风险实际部署后由于非同源请求被Nginx直接转发问题反而暴露不出来。文件路径大小写Linux系统对路径和文件名严格区分大小写在Windows上开发好好的上传Linux服务器就404。统一约定小写命名可以避免这种问题。MySQL最大连接数默认151如果项目里线程池配置不当或者连接没释放会出现连接数耗尽导致服务假死。可以适当调大到500但更重要的是确保MyBatis的DataSource连接池配置合理HikariCP默认参数在当前系统下够用无需过激调整。8. 从“能跑”到“好用”我做这个项目时的几点总结最后分享一些从实际操作中得出的建议这些内容不会写在课程PPT里但能帮你的项目在演示时加分不少。第一演示前一定要准备一套真实的数据。很多同学做失物招领系统登录进去列表是空的只能现场造数据。我建议提前模拟20条失物、15条招领数据配图用真实感的截图地点覆盖食堂、图书馆、操场等高频区域日期分布在最近一周。这样演示时随便一搜“U盘”就有匹配结果状态流转也能连贯演示出来。第二在答辩或者项目介绍时重点不要讲“我用了什么技术”而要讲“我怎么解决问题”。比如“我用MyBatis动态SQL解决了多条件组合查询问题”“我用乐观锁解决了并发认领覆盖问题”“我用状态机避免了流程混乱”——这些才是体现你工程能力的点。第三这个项目后续的扩展空间很大。接入Redis做缓存和分布式Session管理、引入WebSocket让站内信实时推送、加一个Spring Batch定时任务自动处理超时未认领的物品、用Redis ZSet做热门失物榜单。如果你想拿它继续做就业项目集这些方向随便选一个就能撑起一个新的专题。做这个系统的过程中我最大的体会是校园项目虽然业务量不大但五脏俱全特别是状态管理和权限控制这两块是未来做任何企业级系统的通用能力。把这两块在失物招领这个垂直场景里练扎实了后面接别的系统会快很多。希望这份复盘对你定位需求、理解技术选型和部署调试的时候有实质性的参考价值。