简介:这是一套面向高校计算机专业毕业生的高分毕业设计项目源码,基于Spring Boot与Vue.js实现前后端分离架构,专为解决校园场景下失物信息登记、检索、认领与管理等实际问题而开发。资源包含完整可运行系统,涵盖后端Java核心模块(69个.java文件)、MyBatis映射配置(13个.xml)、前端静态资源(9张图片)、数据库脚本(1个.sql)及基础配置文件(2个.yml),共97个文件,压缩包仅973KB,轻量易部署。已有5203人学习下载,说明其结构清晰、代码规范、实用性广受认可。读者可直接导入IDE与数据库运行,获得含用户角色权限、失物发布审核、图文详情展示、搜索与状态跟踪等功能的完整闭环系统,同时便于理解主流Java全栈开发流程与校园信息化系统设计逻辑。
1. 项目概述:为什么一个校园失物招领系统值得花三个月打磨?
我带过六届毕业设计,每年都会遇到学生问:“老师,做个XX系统能过吗?”——“XX”里出现频率最高的,就是“校园失物招领”。听起来简单,但真把它做成一个能跑通、能演示、能答辩、还能被辅导员拿去试用的系统,远比写个“用户登录+物品发布+列表展示”的三页Demo难得多。它不是功能堆砌,而是一次对工程落地能力的真实压力测试:前端要响应快、交互顺、适配手机;后端要稳、要安全、要可扩展;数据库不能只建几张表就完事,得考虑并发查询、图片存储、状态流转、数据一致性;部署不能只在本地localhost:8080跑起来就算成功,得能在学校那台老服务器上扛住几十人同时刷页面。
这个标题里的关键词——SpringBoot + Vue + 前后端分离 + 源码 + 数据库——每一个都不是装饰词。SpringBoot不是为了凑热门框架,而是因为它能让你在三天内搭好一套具备日志、监控、配置中心雏形的后端骨架,省下两周手动配Tomcat、MyBatis、Logback的时间;Vue不是为了赶时髦,而是因为失物招领这种高频操作(拍照上传、实时刷新、分类筛选)必须依赖响应式视图和组件化开发,jQuery写出来的页面在iPhone上点三次才触发一次事件,答辩时绝对翻车;前后端分离不是概念炫技,是硬性要求——你得让前端同学独立开发调试,后端同学专注接口逻辑和数据库优化,两人不用为“为什么我改了JS他那边报404”扯皮一整天;源码和数据库,意味着它必须是可运行、可验证、可追溯的完整体,不是截图拼凑的PPT,不是导出SQL却缺外键约束的半成品。
我去年指导的一个学生,最初只想做个“发帖+看帖”的静态页面,结果在第三周卡在“失物认领后,如何确保只有失主和管理员能修改状态,且修改记录可查”这个问题上。他查了三天文档,最后发现光靠前端按钮禁用根本没用,必须后端做状态机校验+操作审计日志。这件事让我意识到:校园场景看似简单,实则暗藏大量业务规则细节——捡到饭卡和捡到笔记本的处理流程不同,教学楼A栋和宿舍3号楼的失物领取点不同,毕业生离校前的集中招领和日常零散招领的权限策略也不同。这些,才是毕业设计真正该训练的能力:把模糊的现实需求,翻译成精确的代码逻辑和数据结构。所以,这篇内容不讲“怎么装Node.js”,也不列“SpringBoot十大注解”,而是带你从真实校园痛点出发,拆解这个系统每一层的设计决策、踩过的坑、以及为什么非得这么写。
2. 整体架构设计与技术选型逻辑
2.1 为什么必须是前后端分离?单体架构在这里行不通
很多学生第一反应是“用JSP+Servlet写个传统Web应用”,毕竟课程设计里这么干过。但放到失物招领这个场景,单体架构会立刻暴露出三个致命短板:
开发协作效率低:前端同学想加个“按楼层筛选”的下拉框,得等后端把JSP页面传给他,改完再传回去编译。而前后端分离后,前端直接调用
/api/lost-items?building=A,后端只需保证这个接口返回JSON,双方并行开发,互不阻塞。用户体验断层:传统页面每次筛选都要整页刷新,用户刚点开一张高清失物照片,切到“已认领”标签页,照片就没了,得重新加载。Vue的路由懒加载+组件缓存(
<keep-alive>)能让用户在“待认领”和“历史记录”间秒切,图片、筛选条件全保留,这才是移动端友好的体验。部署与维护僵化:学校信息中心只给一台Linux服务器,但要求“前端静态资源走CDN加速,后端API走内网”。单体应用打包成WAR包,所有东西捆在一起,根本没法拆。而分离架构下,Vue打包生成的
dist文件夹直接扔到Nginx静态目录,SpringBoot JAR包单独运行,运维同学可以分别升级、回滚、扩缩容。
提示:答辩时老师如果问“为什么不用Thymeleaf?”,你可以直接回答:“因为Thymeleaf渲染在服务端,无法实现SPA级别的局部刷新和复杂状态管理,而失物招领需要高频交互(如实时点赞、地图定位、多图轮播),这是用户体验的硬性门槛。”
2.2 SpringBoot版本选择:2.7.x是当前最稳妥的平衡点
网络热词里有“springboot版本太高”、“springboot 4 源码”,这恰恰说明版本陷阱是真实存在的。我实测过多个版本:
SpringBoot 3.x:基于Java 17+,强制使用Jakarta EE 9(包名从
javax.*变成jakarta.*),但学校机房的JDK普遍还是8或11,强行升级会导致Tomcat兼容问题,且MyBatis-Plus 3.5.x对Jakarta支持不完善,连基础CRUD都报错。SpringBoot 2.6.x:默认开启
spring-boot-starter-validation的严格模式,导致@NotBlank校验在空字符串时抛异常而非返回友好提示,学生调试三天找不到原因。SpringBoot 2.7.x(推荐):稳定支持Java 8/11,MyBatis-Plus 3.5.3完美兼容,内置HikariCP连接池默认配置合理(
maximumPoolSize=20),对MySQL 5.7/8.0双兼容,且Actuator健康检查接口路径统一(/actuator/health),方便后续加监控。
关键参数配置示例(application.yml):
spring: datasource: url: jdbc:mysql://localhost:3306/lost_found?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver # HikariCP连接池核心调优(针对校园小并发场景) hikari: maximum-pool-size: 15 # 学校服务器内存有限,设为15比默认20更稳 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000注意:
allowPublicKeyRetrieval=true是MySQL 8.0+必需参数,否则连接报错“Public Key Retrieval is not allowed”。这个细节90%的学生会在部署时栽跟头。
2.3 Vue选型:Vue 2.7是毕业设计的“黄金版本”
热词里有“vue入门”、“vue安装及环境配置”,但实际选型不能只看教程多。Vue 3 Composition API虽新,但生态插件(尤其PDF导出、地图组件)成熟度不如Vue 2。我们最终锁定Vue 2.7.14(最后一个Vue 2版本,兼容Options API且支持部分Composition特性),理由很实在:
Element UI生态成熟:
el-table的exportExcel方法、el-upload的before-upload钩子、el-date-picker的范围选择,都是失物招领刚需,Vue 3的Element Plus在2023年仍存在v-model绑定bug。构建工具链稳定:Vue CLI 4.5.x + webpack 4,比Vue CLI 5(webpack 5)对老旧Windows电脑兼容性更好,学生用i5笔记本编译
node_modules不会卡死。学习成本可控:学生已有HTML/CSS/JS基础,直接上Vue 2的
data()、methods、computed概念,比理解ref()、reactive()、setup()组合式API快得多。
项目脚手架命令(确保全局安装Vue CLI 4):
# 全局安装指定版本CLI(避免自动升级到v5) npm install -g @vue/cli@4.5.15 # 创建项目(选择Manually select features,勾选Babel、Router、Vuex、CSS Pre-processors) vue create lost-found-vue cd lost-found-vue # 安装Element UI(注意:Vue 2必须用element-ui,不是element-plus) npm install element-ui@2.15.14 # 安装axios(统一管理API请求) npm install axios@0.21.42.4 数据库设计:不是建表,而是建“业务状态流”
热词里反复出现“数据库增删改查”、“数据库课程设计”,但失物招领的数据库难点不在CRUD,而在状态驱动的数据关系。我们没用ER图工具画一堆虚线箭头,而是先梳理业务主干:
- 失物发布→
lost_item表(含status字段:0-待审核,1-待认领,2-已认领,3-已归还) - 认领申请→
claim_apply表(关联lost_item_id和user_id,状态:0-待处理,1-已同意,2-已拒绝) - 归还确认→
return_record表(由管理员操作,记录归还时间、领取人、经办人)
关键设计决策:
物理删除 vs 逻辑删除:
lost_item表加is_deleted字段(TINYINT),软删除。因为失物可能被重复发布(同一张饭卡被不同人捡到),硬删除会导致历史认领记录关联失效。图片存储方案:不存二进制大字段!
lost_item表只存image_urls(JSON字符串,如["/upload/20231001/abc.jpg","/upload/20231001/def.png"]),文件实际存服务器/var/www/lost-found/upload/目录。这样数据库轻量,备份快,且便于Nginx直接代理图片请求。索引优化重点:
lost_item(status, created_time)联合索引——首页按状态筛选(如只查“待认领”),再按时间倒序,避免全表扫描。
-- 失物主表(精简核心字段) CREATE TABLE `lost_item` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `title` varchar(100) NOT NULL COMMENT '物品标题', `description` text COMMENT '详细描述', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0-待审核,1-待认领,2-已认领,3-已归还', `image_urls` json DEFAULT NULL COMMENT '图片URL数组', `building` varchar(20) DEFAULT NULL COMMENT '所在楼宇', `floor` varchar(10) DEFAULT NULL COMMENT '所在楼层', `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0-未删,1-已删', PRIMARY KEY (`id`), KEY `idx_status_time` (`status`,`created_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='失物信息表';3. 核心模块实现与关键代码解析
3.1 后端:状态机驱动的业务逻辑(以“认领申请”为例)
很多学生把“用户点击认领按钮”直接写成update lost_item set status=2 where id=?,这是典型错误。真实流程是:用户申请→管理员审核→状态变更→通知双方。必须用事务+状态校验兜底。
SpringBoot Controller层(LostItemController.java):
@PostMapping("/claim/{itemId}") public Result claimItem(@PathVariable Long itemId, @RequestBody ClaimApplyDTO dto) { // 1. 参数校验(非空、用户登录态) if (dto.getUserId() == null || dto.getUserId() <= 0) { return Result.fail("用户ID不能为空"); } // 2. 业务校验:只能对"待认领"状态的物品申请 LostItem item = lostItemService.getById(itemId); if (item == null || item.getStatus() != 1 || item.getIsDeleted() == 1) { return Result.fail("物品不存在或不可认领"); } // 3. 调用Service层处理(含事务) boolean success = claimApplyService.applyForClaim(itemId, dto.getUserId(), dto.getContact()); return success ? Result.success("申请已提交") : Result.fail("申请失败,请重试"); }Service层核心逻辑(ClaimApplyService.java):
@Transactional(rollbackFor = Exception.class) public boolean applyForClaim(Long itemId, Long userId, String contact) { // 查询当前物品状态(防止并发下状态被其他请求修改) LostItem item = lostItemMapper.selectById(itemId); if (item.getStatus() != 1) { throw new BusinessException("物品状态已变更,无法申请"); } // 插入认领申请记录 ClaimApply apply = new ClaimApply(); apply.setLostItemId(itemId); apply.setUserId(userId); apply.setContact(contact); apply.setStatus(0); // 待处理 apply.setCreateTime(new Date()); claimApplyMapper.insert(apply); // 更新物品状态为"已申请"(注意:不是直接变2,而是进入中间态) // 这里用乐观锁避免ABA问题:version字段+1 LostItem updateItem = new LostItem(); updateItem.setId(itemId); updateItem.setStatus(1); // 保持1,表示"有申请待审核" updateItem.setVersion(item.getVersion() + 1); int rows = lostItemMapper.updateById(updateItem); if (rows == 0) { throw new BusinessException("更新失败,请稍后重试"); } return true; }实操心得:
@Transactional必须加在Service方法上,Controller层只做参数校验和结果包装。我见过太多学生把@Transactional加在Controller上,导致事务不生效——因为Spring AOP代理的是Bean对象,Controller默认是单例,代理失效。
3.2 前端:Vue组件化实现“动态表单+图片上传”
失物发布页是用户第一触点,体验差直接劝退。Vue组件LostPublish.vue需解决三个痛点:
多图上传预览:用
el-upload的on-preview和on-remove实现点击放大、拖拽排序、即时删除。楼宇楼层联动:选择“教学楼”后,“楼层”下拉框动态加载1-5层;选择“宿舍楼”后加载1-12层。用
watch监听building变化:
<template> <el-form :model="form" label-width="80px"> <el-form-item label="所在楼宇"> <el-select v-model="form.building" placeholder="请选择"> <el-option label="教学楼A栋" value="A"></el-option> <el-option label="宿舍3号楼" value="3"></el-option> </el-select> </el-form-item> <el-form-item label="所在楼层"> <el-select v-model="form.floor" placeholder="请选择"> <el-option v-for="floor in floorOptions" :key="floor" :label="floor + '层'" :value="floor"> </el-option> </el-select> </el-form-item> </el-form> </template> <script> export default { data() { return { form: { building: '', floor: '' }, floorOptions: [] // 动态楼层选项 } }, watch: { 'form.building': { handler(newVal) { // 根据楼宇类型设置楼层选项 if (newVal === 'A') { this.floorOptions = ['1', '2', '3', '4', '5'] } else if (newVal === '3') { this.floorOptions = Array.from({length: 12}, (_, i) => (i + 1).toString()) } else { this.floorOptions = [] } }, immediate: true // 组件初始化时立即执行 } } } </script>- 富文本描述:用
vue-quill-editor(Vue 2兼容版),但必须处理图片上传——编辑器默认把图片转base64,超大图直接撑爆内存。解决方案:重写handlers.image,调用后端上传接口:
// 在mounted钩子中 this.$nextTick(() => { const quill = this.$refs.quillEditor.quill // 替换图片上传handler quill.getModule('toolbar').addHandler('image', () => { const input = document.createElement('input') input.setAttribute('type', 'file') input.setAttribute('accept', 'image/*') input.click() input.onchange = async () => { const file = input.files[0] const formData = new FormData() formData.append('file', file) try { const res = await this.$http.post('/api/upload/image', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) const url = res.data.url // 后端返回的图片访问路径 const range = quill.getSelection() quill.insertEmbed(range.index, 'image', url) } catch (e) { this.$message.error('图片上传失败') } } }) })3.3 前后端联调:跨域与接口规范的实战避坑
热词里有“前后端分离项目实战”,但90%的联调失败源于两个隐形地雷:
- 跨域配置位置错误:学生常把
@CrossOrigin加在Controller类上,但生产环境Nginx反向代理后,此注解失效。正确做法是在SpringBoot中配置全局CORS:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("http://localhost:8080", "http://192.168.1.100:8080") // 开发环境允许的前端地址 .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) // 允许携带cookie(登录态) .maxAge(3600); } }- 接口返回格式不统一:前端Axios期望
{code: 200, data: {}, msg: "success"},而后端有人直接return list,有人return ResponseEntity.ok(list),导致前端到处写res.data.data或res.data,混乱不堪。必须定义统一响应体:
@Data @AllArgsConstructor @NoArgsConstructor public class Result<T> { private Integer code; // 200成功,500失败 private String msg; private T data; public static <T> Result<T> success(T data) { return new Result<>(200, "success", data); } public static <T> Result<T> fail(String msg) { return new Result<>(500, msg, null); } }Controller层强制返回Result:
@GetMapping("/list") public Result<List<LostItem>> listItems(@RequestParam Integer status) { List<LostItem> list = lostItemService.listByStatus(status); return Result.success(list); }注意:
@CrossOrigin和WebMvcConfigurer二选一即可,混用会导致CORS头重复,浏览器报错。我建议用WebMvcConfigurer,因为它是全局配置,且可动态读取配置文件中的allowedOrigins。
3.4 PDF详情导出:前后端分离下的安全实现
热词里有“前后端分离详情导出pdf实现步骤”、“springboot解决pdf xss攻击”,这需求很真实——辅导员要打印失物清单存档。但直接用jsPDF在前端生成PDF,中文乱码、样式错乱;用后端iText生成,又面临XSS风险(用户输入的HTML描述直接渲染进PDF)。
我们的方案:前端生成HTML模板 → 后端渲染为PDF → 浏览器下载,全程规避XSS:
- 前端用
html2canvas截取详情页DOM(已过滤危险标签):
import html2canvas from 'html2canvas' import { saveAs } from 'file-saver' async downloadPDF() { const element = this.$refs.detailContainer // 指向详情页DOM const canvas = await html2canvas(element, { useCORS: true, // 允许跨域图片 scale: 2 // 提高清晰度 }) const imgData = canvas.toDataURL('image/png') // 发送图片base64到后端 const res = await this.$http.post('/api/export/pdf', { imageBase64: imgData }) const blob = new Blob([res.data], { type: 'application/pdf' }) saveAs(blob, `失物详情_${Date.now()}.pdf`) }- 后端用
Flying Saucer(XHTML+CSS渲染)生成PDF,对输入做严格白名单过滤:
@PostMapping("/export/pdf") public void exportPdf(@RequestBody Map<String, String> payload, HttpServletResponse response) throws Exception { String imageBase64 = payload.get("imageBase64"); // 白名单校验:只允许data:image/png;base64开头 if (!imageBase64.startsWith("data:image/png;base64,")) { throw new IllegalArgumentException("非法图片格式"); } // 构建安全HTML模板(不拼接用户输入,只插入已校验的base64) String html = "<html><body style='font-family:SimSun'>" + "<h2>失物招领详情</h2>" + "<img src='" + imageBase64 + "' style='max-width:100%'/>" + "</body></html>"; // 渲染PDF ITextRenderer renderer = new ITextRenderer(); renderer.setDocumentFromString(html); renderer.layout(); response.setContentType("application/pdf"); response.setHeader("Content-Disposition", "attachment; filename=detail.pdf"); renderer.createPDF(response.getOutputStream()); }关键安全点:绝不将用户输入的
description字段直接写入HTML模板,而是用Jsoup.clean()过滤后存入数据库,PDF只展示已清洗的纯文本。这是答辩时老师最爱问的安全问题。
4. 部署与运维:从本地开发到校园服务器上线
4.1 前端部署:Nginx静态资源托管(非Node.js)
热词里有“前后端分离项目部署”,但学生常误以为Vue必须用Node.js跑。其实生产环境最佳实践是:Nginx托管静态文件 + 反向代理API。
Nginx配置(/etc/nginx/conf.d/lost-found.conf):
server { listen 80; server_name lostfound.school.edu.cn; # 域名(若无域名,用IP) # 前端静态资源 location / { root /var/www/lost-found/dist; # Vue build后的dist目录 try_files $uri $uri/ /index.html; # 支持Vue Router history模式 index index.html; } # API反向代理(指向SpringBoot) location /api/ { proxy_pass http://127.0.0.1:8080/; # SpringBoot默认端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 图片直传(绕过SpringBoot,提升性能) location /upload/ { alias /var/www/lost-found/upload/; expires 7d; } }重启Nginx:
sudo nginx -t && sudo systemctl reload nginx注意:
try_files $uri $uri/ /index.html是关键,否则Vue Router的/detail/123路径刷新会404。这个配置必须写在location /块里,写在server根节点会失效。
4.2 后端部署:JAR包+systemd服务化
SpringBoot打成JAR包后,不能直接java -jar app.jar &后台运行,必须用systemd管理进程、日志、开机自启。
创建服务文件/etc/systemd/system/lost-found.service:
[Unit] Description=Lost and Found System After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/lost-found ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /var/www/lost-found/lost-found-backend.jar Restart=always RestartSec=10 StandardOutput=journal StandardError=journal SyslogIdentifier=lost-found [Install] WantedBy=multi-user.target启用服务:
sudo systemctl daemon-reload sudo systemctl enable lost-found.service sudo systemctl start lost-found.service sudo systemctl status lost-found.service # 查看运行状态日志查看(替代tail -f):
sudo journalctl -u lost-found.service -f # 实时日志 sudo journalctl -u lost-found.service --since "2023-10-01" # 查历史日志实操心得:
-Xms256m -Xmx512m是关键,学校服务器内存通常只有2G,不设堆内存上限,JVM会吃光内存导致MySQL崩掉。Restart=always确保崩溃后自动拉起,比nohup可靠十倍。
4.3 数据库同步与备份:用mysqldump做增量备份
热词里有“数据库同步软件”、“数据库同步工具”,但校园场景无需复杂同步。我们用最朴素的mysqldump每日备份:
编写备份脚本/opt/backup/backup_lost_found.sh:
#!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR="/opt/backup/lost-found" DB_NAME="lost_found" DB_USER="root" DB_PASS="123456" mkdir -p $BACKUP_DIR mysqldump -u$DB_USER -p$DB_PASS --single-transaction --routines --triggers $DB_NAME > $BACKUP_DIR/${DB_NAME}_${DATE}.sql # 保留最近7天备份 find $BACKUP_DIR -name "${DB_NAME}_*.sql" -mtime +7 -delete添加定时任务(每天凌晨2点):
# 编辑crontab sudo crontab -e # 添加一行 0 2 * * * /opt/backup/backup_lost_found.sh注意:
--single-transaction参数对InnoDB表有效,能保证备份时数据一致性,避免锁表。--routines和--triggers确保存储过程和触发器也被导出,这是课程设计答辩加分项。
5. 常见问题与排查技巧实录
5.1 “页面空白/404”问题速查表
这是部署后最高频问题,按优先级排查:
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
Nginx打开首页空白,控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED | SpringBoot服务未启动或端口被占 | sudo systemctl status lost-found.servicesudo lsof -i :8080 | sudo systemctl restart lost-found.service若端口被占, sudo kill -9 PID |
Nginx打开首页显示404 Not Found | Nginx配置的root路径错误或dist目录无index.html | ls -l /var/www/lost-found/dist/sudo nginx -t | 确认Vue已执行npm run build,生成文件在正确路径;检查Nginx配置语法 |
点击按钮跳转/detail/123后刷新404 | Vue Router history模式未配置Nginx | curl http://your-server/detail/123 | 确保Nginxlocation /块中有try_files $uri $uri/ /index.html; |
| 图片不显示,URL返回404 | 图片路径配置错误或Nginx未配置/upload/别名 | ls -l /var/www/lost-found/upload/curl http://your-server/upload/abc.jpg | 检查后端返回的图片URL是否为/upload/xxx.jpg;确认Nginxlocation /upload/配置正确 |
个人经验:90%的404问题源于路径不一致。前端
axios.defaults.baseURL = '/api',后端@RequestMapping("/api"),Nginx必须把/api/代理到http://127.0.0.1:8080/,少一个斜杠就会404。
5.2 “登录后状态丢失”问题根源
学生常抱怨“登录后跳转首页,再点其他页面就提示未登录”。这不是JWT失效,而是Cookie域配置错误:
- 前端发送登录请求时,后端
set-cookie的Domain属性必须匹配前端域名。若前端用http://localhost:8080,后端Cookie Domain应为localhost;若前端用http://192.168.1.100:8080,Domain应为192.168.1.100。
SpringBoot配置(application.yml):
server: servlet: context-path: / # 根路径,避免/api重复 spring: session: store-type: redis # 生产环境用Redis存Session servlet: session: cookie: domain: localhost # 开发环境设为localhost path: / http-only: true secure: false # HTTP环境设false,HTTPS设true注意:
secure: false是开发环境必需,否则Chrome拒绝发送HTTP下的Secure Cookie。答辩演示时务必关掉浏览器的“强制HTTPS”设置。
5.3 “图片上传失败”三步定位法
上传失败通常卡在三处:
前端拦截:检查浏览器控制台Network标签页,看
/api/upload/image请求是否发出、返回什么状态码(400?500?)。后端日志:
sudo journalctl -u lost-found.service -n 50,找MultipartException或FileUploadException关键字。服务器权限:
/var/www/lost-found/upload/目录必须有www-data写权限:
sudo chown -R www-data:www-data /var/www/lost-found/upload/ sudo chmod -R 755 /var/www/lost-found/upload/踩坑实录:某次部署后图片上传总报500,日志显示
java.io.FileNotFoundException: /var/www/lost-found/upload/ (Permission denied)。查ls -ld /var/www/lost-found/upload/发现属主是root,而Nginx进程以www-data运行,必须chown授权。这个权限问题在CentOS和Ubuntu表现不同,务必实测。
5.4 “PDF导出中文乱码”终极解决方案
用Flying Saucer导出PDF时,中文显示方块,网上方案多是“加中文字体”,但实际要三步:
- 下载思源黑体(免费开源字体)到服务器:
sudo mkdir -p /usr/share/fonts/opentype/noto sudo wget -O /usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf sudo fc-cache -fv- 在HTML模板中声明字体:
<html> <head> <style> @font-face { font-family: "Noto Sans CJK SC"; src: url("file:///usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf"); } body { font-family: "Noto Sans CJK SC", sans-serif; } </style> </head> <body>...</body> </html>- 后端渲染时指定字体路径:
ITextRenderer renderer = new ITextRenderer(); renderer.getFontResolver().addFont("/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf", "UTF-8", true); renderer.setDocumentFromString(html);关键点:
file:///协议必须用三个斜杠,且路径是服务器绝对路径。fc-cache -fv刷新字体缓存,否则Flying Saucer找不到字体。
6. 毕业设计答辩与扩展建议
这个系统做完,只是完成了80%。剩下20%决定你能否拿优秀——答辩呈现与未来延展。
答辩时,老师最关注三点:业务理解深度、技术实现严谨性、问题解决能力。不要一上来就演示“我做了登录页”,而是先说:“我们调研了学校后勤处,发现失物招领最大痛点是信息不对称——捡到者不知道该交到哪,失主不知道物品在哪,管理员每天手动登记30+条信息。所以我们设计了‘智能归还点推荐’:用户发布失物时,系统根据楼宇GPS坐标,自动推荐最近的3个领取点,并在地图上标注。”然后演示地图组件+后台算法(经纬度计算距离),这比单纯展示CRUD高一个维度。
技术上,准备一个“我踩过的坑”清单。比如:“最初用localStorage存Token,但发现同设备多账号切换时Token冲突,后来改用HttpOnly Cookie+SameSite=Lax,既防XSS又兼容CSRF防护。”这种细节,老师一听就知道你真做过。
至于扩展,别只说“可以
本文还有配套的精品资源,点击获取