ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Spring Boot的校园失物招领系统:从数据库设计到部署完整实践

基于Spring Boot的校园失物招领系统:从数据库设计到部署完整实践 大学里丢东西是常态我自己也是从那时候过来的一卡通丢过两次第一次去线下失物招领处找登记本上密密麻麻写了几十行管理员翻了半天告诉我“没有”结果东西就搁在角落的箱子里登记本上写的是“棕色皮包”我找的却是“卡包”。这个细节让我印象特别深。后来自己做了程序员接到校园数字化项目需求时我第一个想到的就是做一套基于SpringBoot的校园失物招领系统Java生态做后台SpringBoot管接口和事务再配一个轻量前端把信息登记、分类浏览、认领申请、管理员审核全流程搬到线上。这篇文章我打算把整套系统的设计思路、数据库建模、核心接口实现、图片存储方案和部署踩坑记录完整拆一遍无论你是做课程设计、毕业设计还是真打算帮学校把这个系统落地都能直接拿来参考。1. 校园失物招领的真实痛点线下模式为什么撑不住1.1 三个被忽视的问题线下失物招领处最常见的配置是一本登记簿、一张桌子、一个储物柜。管理员把捡到的物品登记在册失主来了翻本子、去柜子里找。听起来简单实际操作里全是问题。第一个问题就是信息不对称。每个院系、宿舍楼、食堂门口的失物招领处其实是信息孤岛。物品可能在东区食堂的失物箱里但失主只去了西区图书馆的服务台。就算某个登记本写得很详细失主看不到等于白登记。所以线上系统要解决的第一个问题不是“管理”而是让信息打破物理空间的限制统一进入一个可以被搜索的库。第二个问题是物品状态没有任何跟踪。东西被捡到之后谁负责保管、有没有被同学认错拿走、最终是归还了还是过期清理了线下基本没有记录。很多物品在角落里放几个月既占空间又容易损坏。系统化之后物品从“待认领”到“已归还”的每一步都应该有时间和责任人。第三个问题是认领环节缺乏规则。线下靠工作人员肉眼判断遇到相似物品往往分不清该不该给。系统设计里认领人需要提交能证明物品归属的信息比如购买记录截图、照片中某个细节、辅导员联系方式等。这不是为了防君子而是让整个流程有据可查。真出了纠纷管理员能看到完整的认领申请和审核记录而不是一句“我当时看着像就给他了”。1.2 最小化闭环三种角色和四个状态一个基础版本的失物招领系统不需要做得很浮夸核心就是三个角色拾得者或失主、认领者、管理员。拾得者发布招领信息失主发布寻物启事管理员对认领申请做核实。我建议把状态机控制在四个以内不要一上来就搞十几个状态否则你自己都会被绕晕待认领物品信息已发布没有认领申请或者认领申请被驳回。认领中已经有人提交认领申请正在核实物品归属。已归还管理员确认身份后物品交还成功。已撤销信息发布者发现物品已找到或确认不需要继续寻找主动撤销这条信息。这四个状态覆盖了绝大多数实际场景后续做列表筛选、统计报表也都很好写。真正要避免的是那种“数据在表里随便改”的做法所有状态变更必须经过明确的接口和逻辑不能让人直接改数据库字段。1.3 哪些功能应该留到二期做这类系统我的原则是“先做必须的再说想要的”。基础版本里必做的是发布、列表搜索、详情、认领申请、管理员审核这五件事。积分奖励、失物排行榜、微信小程序推送、物品过期自动下架这些全部放到二期。为什么因为一期只要把核心闭环跑通就能看到真实使用数据后面再按数据迭代远比一开始功能堆砌更稳。很多同学做课程设计会忍不住把所有想到的功能全塞进去最后数据库几十张表、接口几百个但没有一个流程是完整走通的。实战项目首先要的是闭环不是功能数量。2. 技术选型逻辑为什么挑Spring Boot为什么配Vue2.1 Spring Boot解决的Java Web历史包袱如果回到十年前用Java做Web项目通常是一大堆配置web.xml、spring.xml、springmvc.xml还得自己操心Tomcat版本、日志框架到底用哪个。Spring Boot把这些历史包袱全卸掉了。它通过自动装配机制让一个Maven工程依赖了spring-boot-starter-web之后直接就能启动一个内嵌Tomcat的Web应用。自动装配的原理想想并不复杂核心是EnableAutoConfiguration这个注解配合spring.factories或Import加载大量XXXAutoConfiguration类再用ConditionalOnMissingBean、ConditionalOnClass这些条件注解判断“当前环境里有没有这个类”“容器里有没有这个Bean”有就自动配置。这就让你不用关心Tomcat怎么配、JSON序列化器怎么注册只管业务逻辑怎么写。明白这个原理对实战项目挺重要。很多面试题会问“Spring Boot自动装配原理是什么”但在实际项目里它还能帮你解决一个实际问题当你引入第三方starter时如果配置生效顺序不对你能更快定位到是哪个AutoConfiguration给挡住了而不是瞎试。2.2 Java生态在校园系统里的独特优势有人会问做一个失物招领系统用Python的Flask或者Node.js的Express不是更轻量吗确实更轻但我一般不建议这样选。校园系统的生命周期通常比较长先有课程设计后面可能被学院拿去改成正式项目再过一两年还有不同院系的人参与维护。Java技术栈的生态、社区文档、中间件支持都是最稳定的出了问题随便搜一下就有大量现成方案。更重要的是Java在高校课程体系里是主流。用Spring Boot做后端对在校同学来说既是项目实践也是秋招时的面试素材一举两得。哪怕只是想学点东西跟着这套技术栈走一遍收益也远比用一个小众框架做完就跑要高。2.3 前端选Vue不是因为流行前端我比较推荐Vue倒不是“生态好”“社区大”这种空话而是这类系统的页面形态非常适合Vue表单多、列表多、交互状态多。发布物品页要动态显示图片预览认领申请页要根据物品状态切换按钮管理员后台要做到列表和详情的联动Vue的双向绑定和组件化让这些逻辑都非常直观。通常用Vue 3 Vite Element Plus就够了。如果不想把前端工程搭得太重也可以直接通过CDN引入Vue单文件方式把项目简化到极致。但工程化规范起见建议还是用Vite创建一个标准前端项目这样后续加路由、加状态管理都方便。2.4 单人开发的工程结构建议做这种中小型项目我不建议用多模块Maven工程去搞service-api、service-impl、common、admin一堆模块。一个人开发时模块越多心智负担越大。单模块Maven项目就足够了按目录清晰分层config配置类包括跨域、上传、MinIO等。controller接口层只做参数接收和结果封装。service业务逻辑层状态流转、事务控制都在这里。dao / mapper数据访问层负责SQL交互。entity / domain实体类与数据库表对应。dto / vo入参和出参对象避免把Entity直接暴露给前端。common统一返回结构、异常处理、工具类。resources配置文件、Mapper XML如果有。这个三层结构看上去老套但在这种体量的系统里职责清晰比架构新颖重要得多。等真的需要拆分微服务时再重构也不迟。3. 数据库建模同一张物品表装下失物和招领3.1 用户与角色不需要复杂的权限系统很多同学做项目会单独设计一张权限表再引入Spring Security或者Shiro做完整鉴权。对失物招领系统来说这属于过度设计。其实只需要在用户表里加一个role字段就够了0是普通用户1是管理员。CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(200) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户 1管理员, create_time DATETIME, update_time DATETIME );密码加密用BCrypt别存明文。在Spring Boot里只需要引入spring-security-crypto一个依赖就能用BCryptPasswordEncoder不需要把整套Security请进来。管理员账号可以提前初始化到数据库里用户注册接口直接默认role0简单可靠。3.2 物品表失物和招领为什么要合并成一张表这是很多人容易犹豫的地方寻物启事和失物招领是两类信息要不要拆成两张表我的答案是合并到一张goods表用一个type字段区分。理由有三个。第一两类的核心字段高度重合都是标题、描述、分类、地点、图片、联系方式拆开会导致大量字段重复维护。第二用户搜索时通常会同时关注寻物和招领比如我丢了校园卡我会去招领里找有没有人捡到我捡到校园卡我也会去寻物里找有没有人丢。拆表会导致联合查询非常麻烦。第三管理员后台要统一管理所有物品拆表以后统计、筛选、下架都是两套逻辑。CREATE TABLE goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL COMMENT 1寻物启事 2失物招领, user_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL, description TEXT, category VARCHAR(32) COMMENT 分类如校园卡/电子设备/证件/其他, location VARCHAR(100) COMMENT 丢失或拾获地点, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待认领 1认领中 2已归还 3已撤销, image_urls TEXT COMMENT 多张图片用逗号分隔, contact VARCHAR(50), create_time DATETIME, update_time DATETIME, KEY idx_type_status (type, status), KEY idx_category (category), KEY idx_create_time (create_time) );这里有几个细节值得说明。image_urls用TEXT存多张图片的URL用逗号分隔虽然不算严格范式但在这个场景里省一次关联查询比追求规范化更实际反正图片数量一般不会超过五张。status加索引是因为列表页最常按typestatus去筛选create_time加索引是因为列表几乎都按时间倒序这两个字段的组合是高频查询路径。3.3 认领记录表让每一次认领都有据可查认领是失物招领系统的核心动作。它不只是一个按钮而是一条包含理由、证明材料、审核意见的完整记录。我设计了claim_record表CREATE TABLE claim_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL, user_id BIGINT NOT NULL, reason VARCHAR(500) COMMENT 认领说明, proof_urls TEXT COMMENT 证明材料图片, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核 1已通过 2已驳回, handle_user_id BIGINT COMMENT 审核管理员ID, handle_time DATETIME, remark VARCHAR(200) COMMENT 审核备注, create_time DATETIME );这张表是状态机闭环的关键。goods状态从“待认领”变成“认领中”必须由一条claim_record触发变成“已归还”也必须由管理员处理这条claim_record触发。这样我们随时能回答两个问题谁在什么时候申请了认领谁在什么时候做了决定。很多初学设计会在goods表里加一个claim_user_id字段表示“当前认领人”这有隐患同一件物品可以被多个人申请只存一个字段会丢失信息。正确做法是让goods表只保留当前状态所有详细过程都放到claim_record里。3.4 通知与订阅表留给匹配提醒功能用如果后续要做“关键词匹配提醒”可以提前准备两张表。keyword_subscribe存用户订阅的关键词notification存站内通知。这个我在第6章会展开讲现在只要知道表结构不要太复杂两张简单表就能跑通整个提醒链路就够了。4. 后端核心接口实现状态机下的完整认领链路4.1 状态枚举与统一响应后端接口本身不复杂难的是所有操作都围绕状态机走。我建议先定义一个枚举类把状态常量收敛起来public enum GoodsStatus { WAITING(0, 待认领), CLAIMING(1, 认领中), FINISHED(2, 已归还), CANCELED(3, 已撤销); private final int value; private final String desc; GoodsStatus(int value, String desc) { this.value value; this.desc desc; } public int getValue() { return value; } public String getDesc() { return desc; } }这样写的好处是接口里不会出现魔法数字比如goods.getStatus() 1这种代码尽量少出现。虽然代码量会多几行但维护的时候收益很大。统一返回结构用泛型Result类例如Result.success(data)、Result.error(xxx)前端拿到后统一判断code字段处理逻辑就清爽很多。4.2 发布接口把“发布”和“图片上传”拆开发布物品时我建议前端先单独调用文件上传接口拿到图片URL再调用发布接口把URL一起提交。这样做的好处是上传失败不会导致整个发布流程回滚用户也可以先看到图片预览再提交。发布接口的核心逻辑PostMapping(/goods) public ResultGoodsVO publish(RequestBody GoodsDTO dto) { Goods goods new Goods(); goods.setType(dto.getType()); goods.setUserId(CurrentUser.getId()); goods.setTitle(dto.getTitle()); goods.setDescription(dto.getDescription()); goods.setCategory(dto.getCategory()); goods.setLocation(dto.getLocation()); goods.setContact(dto.getContact()); goods.setImageUrls(String.join(,, dto.getImageUrls())); goods.setStatus(GoodsStatus.WAITING.getValue()); goods.setCreateTime(LocalDateTime.now()); goods.setUpdateTime(LocalDateTime.now()); goodsMapper.insert(goods); return Result.success(convertToVO(goods)); }这里有个容易忽略的细节设置user_id时不能从前端传必须从登录态里取。否则用户可以把别人的id传过来造成越权发布。这类“信后端不信前端”的习惯在实战项目里要刻意养成。每次写新增接口时都要习惯性思考一下这个字段该由前端提供还是由后端根据身份推导需要后端推导的一律拒绝尝试。4.3 认领接口同一件物品不能被多人同时锁死认领申请看起来只是个insert但需要处理并发问题。设想一个场景有同学丢了一台笔记本电脑系统里有一条招领信息。此时A和B几乎同时提交认领申请如果代码只做插入就会出现两条“待审核”的认领记录后面管理员只能人工判断哪条更靠谱很容易产生纠纷。更合理的做法是插入认领记录之前先检查物品当前状态同时把状态从“待认领”改为“认领中”。如果两个请求同时到达就靠数据库的条件更新来保证只有一个人能成。我采用事务加CASCompare And Set的方式。goods表增加一个version字段在更新状态时用UPDATE ... SET status1, versionversion1 WHERE id? AND status0 AND version?如果影响行数为0说明状态已经被别人改了。代码实现起来大概是这样Transactional(rollbackFor Exception.class) public Long applyClaim(ClaimRequestDTO dto) { // 1. 校验物品存在 Goods goods goodsMapper.selectById(dto.getGoodsId()); if (goods null) { throw new BizException(物品不存在); } // 2. 只能对“待认领”状态的物品提交认领 int rows goodsMapper.compareAndSetStatus( dto.getGoodsId(), GoodsStatus.WAITING.getValue(), GoodsStatus.CLAIMING.getValue()); if (rows 0) { throw new BizException(该物品已被他人认领或已下架); } // 3. 插入认领记录 ClaimRecord record new ClaimRecord(); record.setGoodsId(dto.getGoodsId()); record.setUserId(CurrentUser.getId()); record.setReason(dto.getReason()); record.setProofUrls(String.join(,, dto.getProofUrls())); record.setStatus(0); record.setCreateTime(LocalDateTime.now()); claimRecordMapper.insert(record); return record.getId(); }compareAndSetStatus返回0说明CAS更新失败整个事务回滚前面的更新不会生效。这个方案在单体和集群部署下都够用不需要引入分布式锁。对应的SQL长这样UPDATE goods SET status #{newStatus}, version version 1 WHERE id #{id} AND status #{oldStatus}4.4 管理员审核接口不能只改物品状态还要回写认领记录认领通过时管理员要做两件事把claim_record的status改为已通过记录管理员ID和审核时间把goods表状态改为已归还。驳回时把claim_record改为已驳回同时把goods状态改回待认领供下一个人申请。这两处更新必须在同一个事务里否则会出现“记录显示已通过但物品状态还是认领中”的不一致。代码写成两个独立方法Transactional(rollbackFor Exception.class) public void approveClaim(Long claimId, Long adminId) { ClaimRecord record claimRecordMapper.selectById(claimId); if (record null || record.getStatus() ! 0) { throw new BizException(认领记录不存在或已审核); } claimRecordMapper.updateStatus(claimId, 1, adminId, LocalDateTime.now()); goodsMapper.updateStatus(record.getGoodsId(), GoodsStatus.FINISHED.getValue()); }驳回的方法类似只是把claim_record状态改成2goods状态改回0。这里要明确一个认知真正的“归还动作”发生在线下系统只是记录结果。审核通过不代表系统替你把东西给了人它只是把线上状态和线下事实对齐。所以管理员在线下交还物品时务必核实对方身份。5. 图片存储本地目录、MinIO还是OSS怎么选5.1 本地存储的边界能做但要知道限制失物招领系统几乎离不开图片。捡到东西的人会拍照上传失主也可能上传购买凭证。图片存储方案是第一个工程决策。最简单的是存本地。在application.yml里配置一个自定义上传目录upload: path: /data/found/然后写一个上传Controller把MultipartFile保存到目录再通过WebMvcConfigurer配置虚拟路径映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${upload.path}) private String uploadPath; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadPath /); } }本地存储的好处是部署简单、零依赖但你得知道它的边界在哪应用扩容后图片没办法共享服务器磁盘会被日志和图片一起占满备份得单独处理。校园系统如果只在实验室一台电脑上跑本地存储完全没问题。一旦要上生产环境我建议直接用对象存储。5.2 接入MinIO自建对象存储的开源方案MinIO兼容S3协议可以部署在内网。先用Docker启动一个实例docker run -d --name minio -p 9000:9000 -p 9001:9001 \ -e MINIO_ROOT_USERadmin \ -e MINIO_ROOT_PASSWORDadmin123456 \ -v /data/minio:/data \ minio/minio server /data --console-address :9001然后在Spring Boot的pom.xml里引入dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.7/version /dependency配置类里创建MinioClientConfiguration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }上传文件的核心代码public String upload(MultipartFile file) { String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); String objectName UUID.randomUUID().toString() . ext; try { minioClient.putObject( PutObjectArgs.builder() .bucket(found-items) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint /found-items/ objectName; } catch (Exception e) { throw new BizException(上传失败请重试); } }这里用UUID作为对象名可以避免中文文件名、重名文件带来的各种问题。中文文件名在部分浏览器和HTTP客户端解析URL时会乱码存库的时候看到的是一串UUID访问时就稳定多了。5.3 别忽略两个限制大小和类型上传接口必须限制大小。Spring Boot默认支持单文件1MB通常要改大一点spring: servlet: multipart: max-file-size: 5MB max-request-size: 10MB代码层同样需要校验一次if (file.getSize() 5 * 1024 * 1024) { throw new BizException(图片大小不能超过5MB); } String ext StringUtils.getFilenameExtension(file.getOriginalFilename()); if (!Arrays.asList(jpg, jpeg, png, webp).contains(ext.toLowerCase())) { throw new BizException(仅支持jpg/png/webp格式); }只检查扩展名是不够的因为用户可以改后缀。更严格一点可以用文件头判断JPEG以FFD8开头PNG以89504E47开头WebP以RIFF开头。读取文件前几个字节来验证真实格式能防止有人把一个可执行文件伪装成jpg传上来。对于内部使用的失物招领系统扩展名加MIME校验通常已经够用但知道有文件头这条路关键时刻能救你一次。6. “东西可能是你的”自动匹配提醒机制6.1 用户真正需要的是等待中的通知而不是反复刷新失物招领系统上线之后我发现一个很直接的痛点用户把寻物启事发上去后如果没人管他只能隔三差五回来搜索关键词体验很差。反过来捡到东西的人发布招领后也想尽快找到失主。系统如果能在两者之间自动牵线价值会提升很多。基础版的做法是“订阅关键词”。用户可以在个人中心维护一个关键词列表比如“校园卡”“充电宝”“黑色钱包”。一旦有任意一条新发布的招领物品标题或描述命中这些关键词系统自动给用户发一条站内信告诉他有一条新物品可能匹配。订阅表结构很简单CREATE TABLE keyword_subscribe ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, keyword VARCHAR(50) NOT NULL, create_time DATETIME );6.2 匹配触发建议放在发布逻辑之后异步执行匹配逻辑放在发布接口之后强烈建议用异步执行。因为它不是核心流程如果同步做用户发布物品要多等几百毫秒而且万一订阅逻辑报错不应该导致发布失败。用Async注解或者手动提交线程池都行小项目里我用一个简单的ThreadPoolTaskExecutor避免自己new裸线程Async(notificationExecutor) public void matchAndNotify(Long goodsId) { Goods goods goodsMapper.selectById(goodsId); if (goods null) { return; } ListString keywords keywordSubscribeMapper.selectAllKeywords(); for (String keyword : keywords) { if (goods.getTitle().contains(keyword) || goods.getDescription().contains(keyword)) { notifyUser(goods, keyword); } } }注意这里遍历的是“所有用户的全部关键词”。校园系统几千个用户每人五六个关键词也就是几万行在内存里循环一遍完全没压力。但如果系统做大到几百万关键词就需要考虑倒排索引了那是另一个量级的故事。6.3 更进一步按分类和地点缩小匹配范围关键词匹配有个明显缺陷误报率比较高。比如关键词是“书”几乎所有物品都能命中。这时可以增加两个筛选维度分类一致、地点相似。分类直接用equals比较地点用字符串包含匹配因为“三号食堂一楼”和“三号食堂二楼”没法精确相等。一个实用的策略是给每个候选物品计算匹配分标题包含关键词10分描述包含关键词5分分类一致3分地点包含2分总分大于等于某个阈值比如10分才触发通知。这样既能覆盖大部分场景又能把无意义通知压到最低。提醒用户订阅时建议他用“分类物品名”组合比如“电子设备 充电宝”而不是单独一个“充电宝”效果要好很多。6.4 通知落点站内信足够别一开始就折腾短信邮件一提到通知很多人自然想到短信、邮件、公众号模板消息。对校园场景来说站内信是最容易落地、也最不容易被忽略的方式。用户下次打开系统右上角有一个红点点开就是通知列表。通知表设计很简单CREATE TABLE notification ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, goods_id BIGINT, content VARCHAR(500), is_read TINYINT DEFAULT 0, create_time DATETIME );用户看到通知后点进去就能跳转到对应物品详情页然后提交认领申请。这个闭环非常顺畅完全不需要处理短信服务商的审核和费用问题。7. 部署、优化与避坑记录把系统真正跑起来7.1 打包部署打成可执行JARSpring Boot应用部署时用mvn clean package打成JAR包在服务器上执行nohup java -jar school-found-1.0.0.jar --spring.profiles.activeprod app.log 21 日志重定向到app.log不影响进程退出。生产环境的数据库密码不要写在application.yml里用环境变量或外部配置覆盖spring: datasource: url: ${DB_URL} username: ${DB_USERNAME} password: ${DB_PASSWORD}部署前把前端dist目录里的静态文件交给Nginx然后反向代理到后端的8080端口即可。跨域配置如果不想依赖前端Nginx代理后端也要开启CORS并指定允许来源不要用*通配符同时又带credentials那样浏览器会直接报错。7.2 数据库索引优化别等卡了再做校园系统的数据量正常情况下不会特别大一张物品表可能就几千到几万条。但就算在这个量级也要注意索引使用。最典型的场景是首页列表查询SELECT * FROM goods WHERE type 2 AND status 0 ORDER BY create_time DESC LIMIT 10;如果没有索引表数据量上来后这条查询会明显变慢。按type、status、create_time建组合索引性能基本不需要再担心。建索引时有几个原则等值查询的字段放左边比如type、status。排序字段create_time放在最后让排序直接走索引。避免在索引列上做函数运算WHERE create_time DATE_SUB(NOW(), INTERVAL 7 DAY)没问题但WHERE DATE(create_time) 2025-01-01会导致索引失效。这个体量的系统有这三个索引细节基本就够用了不用过度优化。7.3 时区、JSON序列化、上传超时这些隐形坑Java MySQL最容易踩的就是时区坑。连接串没有指定时区时Java 8的LocalDateTime在存储和读取可能偏移8小时。连接串里加serverTimezoneAsia/Shanghai同时保持JVM时区一致。Spring Boot的Jackson配置也要注意LocalDateTime的序列化格式否则前端拿到的是数组格式或者默认ISO字符串页面解析会直接报错可以在application.yml里统一设置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai还有一个容易被忽略的是Nginx上传超时。如果前端通过Nginx转发上传请求默认的client_max_body_size只有1MB传大点图片就会收到413错误。在Nginx的server块里加上client_max_body_size 10m;这个坑我见很多人踩过后端明明设置了5MB限制用户传一张3MB的照片还是失败排查半天发现是Nginx在门口就挡住了。7.4 上线前值得过一遍的检查清单最后分享一份我自己做类似项目时的检查清单登录认证至少用拦截器统一校验登录状态白名单放行登录、注册、公告接口。越权防护用户只能操作自己的数据管理员操作要有角色校验。统一异常处理用RestControllerAdvice兜底异常不要把异常堆栈直接打到前端也别把数据库错误信息裸露给用户。关键操作日志物品审核、删除、分配这类敏感操作建议记录操作人、操作时间、操作前后状态出纠纷时能查。数据库备份mysqldump定时执行图片目录定期同步校园项目也要有备份意识。密码安全BCrypt加密前端不要传明文之外的任何可逆编码。这些条目看起来琐碎但每一项在真实上线时都有可能变成事故。特别是越权防护我以前见过一个系统用户改一下URL里的id就能删掉别人的失物信息这种问题在验收测试时不一定暴露但上线后迟早会出事。整套系统做下来我的体会是真正困难的不是某个接口怎么写、某张表怎么设计而是把一个线下的模糊流程抽象成线上清晰状态机的过程。做完整套项目你对Spring Boot的自动装配、事务机制、文件上传、部署运维都会形成更立体的认知这是看多少教程都学不来的东西。如果现在重新做一遍下一步我会考虑给物品详情页增加相似物品推荐或者把证件类物品的OCR识别加进去让捡到的校园卡可以直接匹配到失主。不过那是另一个故事了先把基础闭环做好比什么扩展都重要。
返回列表