简介本资源是一套面向计算机专业本科生的毕业设计级Spring Boot后端项目聚焦数字相册管理场景解决照片分类、用户协作与内容安全等实际需求适合作为课程大作业、毕设选题或Java全栈开发入门实践。压缩包共83个文件含77个Java核心业务类涵盖Controller、Service、Mapper及实体层、2个Markdown说明文档含部署与模块说明、1个application.yml配置文件、1个SQL建表脚本及1个.gitignore整体仅82KB结构精简、依赖明确便于快速导入与二次开发。已有193人学习下载资源配套CSDN演示博客清晰呈现登录注册、照片/相册集管理、草稿箱、通讯录、分享圈、公告与用户权限等八大功能模块的实现逻辑与接口设计代码规范、分层合理可直接用于功能验证、技术对标或教学案例拆解。1. 毕业设计选 SpringBoot 做相册后端真不是图省事它能扛住 500 张高清图并发上传、自动缩略、按标签秒搜还能无缝对接 Vue 前端——适合没接触过真实 Web 项目但会写 Java 的本科生你手头有一堆旅行照、毕业合影、课程设计截图想做个能登录、能分类、能打标签、能批量下载的个人相册网站别急着翻 GitHub 找“完整源码”先看清现实很多所谓“毕设相册系统”后端用 Servlet 硬写连文件上传都卡在MultipartFile转File就报错有的强行套 SpringCloud结果光 Eureka 依赖就占掉 80MB 内存部署到学生机直接 OOM。而 SpringBoot 的价值恰恰藏在那些被忽略的细节里——比如它默认集成的spring-boot-starter-web已经把 Tomcat 8.5、Jackson 2.13、Spring MVC 最佳实践全配好了比如ConfigurationProperties能三行代码把application.yml里的upload.max-size50MB绑定到 Java Bean再比如spring-boot-devtools让你改完 Controller 不用重启CtrlS 后浏览器 F5 就看到新接口返回。这不是“框架炫技”是帮你把本该花在调依赖、配路径、修编码的 70% 时间省下来专注做三件事怎么存图更安全不暴露物理路径、怎么查图更快避免全表扫描、怎么让导师点开就夸“结构清晰”。本文不讲 SpringBoot 是什么只带你从零搭一个能跑通、能演示、能答辩、能改出新功能的相册后端——所有命令可复制粘贴所有配置有参数说明所有坑都标了血泪经验。2. 用 SpringBoot 3.2.6 MyBatis-Plus 4.3.2 搭建相册后端最小可行骨架从创建项目到启动成功只要 4 分钟2.1 创建项目用官方脚手架比 IDEA 内置向导更稳避开 JDK 17 兼容性玄学别用 IDEA 的 “New Project → Spring Initializr”它默认勾选Spring Web却漏掉Lombok和Validation等你写完实体类才发现Data报红、NotBlank不生效。直接访问 https://start.spring.io 注意是官网不是第三方镜像按以下配置勾选Project: MavenLanguage: JavaSpring Boot:3.2.6必须选这个版本3.3.x 对 MySQL 8.0 驱动有兼容问题3.1.x 缺少Transactional的timeout新参数Dependencies:Spring Web核心 MVCSpring Data JDBC轻量级数据库操作比 JPA 更可控MyBatis Framework不要选 MyBatis-Plus Starter先手动引入避免版本冲突Spring Boot DevTools开发期热重载Lombok简化 getter/setterValidation参数校验Spring Configuration ProcessorYAML 提示支持提示生成 ZIP 后解压用 IDEAOpen方式导入非 Import否则 Maven 依赖可能识别失败。首次加载时IDEA 右下角弹出 “Maven projects need to be imported” —— 点击 “Enable Auto-Import”否则后续pom.xml修改不会自动同步。2.2 替换 MyBatis-Plus 并配置多数据源为什么不用原生 MyBatis因为相册要存图和存元数据必须分库原生 MyBatis 写个分页要手写LIMIT #{offset}, #{limit}而相册列表页必须支持“每页 20 张图按上传时间倒序”手动写 SQL 容易漏ORDER BY create_time DESC导致排序错乱。MyBatis-Plus 的PageT对象能一行代码搞定// PhotoController.java GetMapping(/list) public ResultPagePhoto list(RequestParam(defaultValue 1) int current, RequestParam(defaultValue 20) int size) { PagePhoto page new Page(current, size); return Result.success(photoService.page(page)); }但 Spring Initializr 生成的pom.xml里 MyBatis-Plus 版本是3.5.5与 SpringBoot 3.2.6 不兼容会报NoSuchMethodError: com.baomidou.mybatisplus.core.toolkit.StringUtils.isBlank。必须手动替换为4.3.2!-- pom.xml -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version4.3.2/version /dependency注意mybatis-plus-spring-boot3-starter是专为 SpringBoot 3.x 设计的 startermybatis-plus-boot-starter是旧版混用必翻车。替换后右键pom.xml→ “Maven → Reload project”等待依赖下载完成。2.3 初始化数据库用 H2 内存库快速验证但必须提前规划 MySQL 迁移路径毕设答辩现场不能靠H2数据库撑场面导师问“上线怎么部署”你就懵了但开发初期用 H2 能跳过 MySQL 安装、用户授权、字符集设置三座大山。在application.yml中配置# application.yml spring: datasource: url: jdbc:h2:mem:photo_db;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console sql: init: mode: always schema-locations: classpath:schema-h2.sql >-- src/main/resources/schema-h2.sql CREATE TABLE IF NOT EXISTS photo ( id BIGINT AUTO_INCREMENT PRIMARY KEY, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名如 IMG_20230501_123456.jpg, stored_path VARCHAR(512) NOT NULL COMMENT 服务器存储路径如 /uploads/2023/05/IMG_20230501_123456.jpg, thumbnail_path VARCHAR(512) COMMENT 缩略图路径如 /thumbnails/2023/05/IMG_20230501_123456_thumb.jpg, width INT NOT NULL COMMENT 图片宽度像素, height INT NOT NULL COMMENT 图片高度像素, size_kb INT NOT NULL COMMENT 文件大小 KB, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, tags VARCHAR(512) COMMENT 逗号分隔标签如 毕业,校园,晴天 ); CREATE INDEX idx_tags ON photo(tags); -- 标签搜索必须加索引参数说明DB_CLOSE_DELAY-1防止 H2 在连接关闭时清空内存库schema-locations指向建表语句>curl -X GET http://localhost:8080/list?current1size5 -H Content-Type: application/json预期返回{code:200,msg:success,data:{records:[],total:0,size:5,current:1,pages:0}}逻辑说明Result是自定义响应包装类code200表示业务成功非 HTTP 状态码data.records为空数组证明 MyBatis-Plus 分页查询已通。如果返回404检查RestController是否漏写RequestMapping(/photo)如果返回500且日志有Invalid value for Enum说明Photo实体类中用了enum但没配EnumValue。3. 相册核心功能落地文件上传、缩略图生成、标签搜索三步写出导师点头的代码3.1 文件上传用MultipartFile接收前端二进制流但必须限制单文件 50MB、总请求 100MB前端 Vue 用axios.post(/upload, formData)传文件后端不能直接RequestParam(file) MultipartFile file就完事——没加校验的上传接口是服务器黑洞。application.yml必须显式配置# application.yml spring: servlet: context-path: /api web: resources: static-locations: classpath:/static/,file:./uploads/ # 关键上传大小限制 http: multipart: max-file-size: 50MB max-request-size: 100MB参数说明max-file-size控制单个文件上限防止用户上传 2GB 视频max-request-size控制整个 HTTP 请求体大小防恶意构造超长tags字段static-locations中file:./uploads/指定物理存储目录classpath:/static/用于存放 Vue 打包后的index.html后续整合用。PhotoController.java中的上传方法PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file, RequestParam(value tags, required false) String tags) { // 1. 校验文件类型只允许 JPG/PNG String contentType file.getContentType(); if (!image/jpeg.equals(contentType) !image/png.equals(contentType)) { return Result.fail(仅支持 JPG/PNG 格式); } // 2. 生成唯一文件名防重名覆盖 String originalName file.getOriginalFilename(); String extension originalName.substring(originalName.lastIndexOf(.)); String storedName UUID.randomUUID().toString() extension; // 3. 构建存储路径按年月分目录避免单目录文件过多 String datePath LocalDate.now().format(DateTimeFormatter.ofPattern(yyyy/MM)); String storedPath /uploads/ datePath / storedName; // 4. 保存文件到磁盘 try { File uploadDir new File(./uploads/ datePath); if (!uploadDir.exists()) uploadDir.mkdirs(); file.transferTo(new File(./uploads/ datePath / storedName)); } catch (IOException e) { log.error(文件保存失败, e); return Result.fail(服务器保存失败); } // 5. 保存元数据到数据库 Photo photo new Photo(); photo.setFileName(originalName); photo.setStoredPath(storedPath); photo.setSizeKb((int) (file.getSize() / 1024)); photo.setTags(tags); // ... 设置宽高见 3.2 节 photoService.save(photo); return Result.success(storedPath); }逻辑说明UUID.randomUUID()保证文件名全局唯一LocalDate.now().format(...)生成2023/05目录避免./uploads/下堆积数万文件导致ls卡死file.transferTo()是安全写法比file.getBytes()加FileOutputStream少 3 行代码且内存占用低。3.2 生成缩略图用 Thumbnailator 库一行代码压缩但必须处理 EXIF 朝向导致的旋转异常用户手机拍的照片常带 EXIF 信息Orientation6表示顺时针旋转 90°但Thumbnailator默认不读取 EXIF直接压缩会导致缩略图歪着。引入依赖!-- pom.xml -- dependency groupIdnet.coobird/groupId artifactIdthumbnailator/artifactId version0.4.20/version /dependency生成缩略图的工具方法PhotoService.javaprivate void generateThumbnail(String originalPath, String thumbnailPath) { try { // 1. 先读取原始图片并自动修正朝向 BufferedImage sourceImage ImageIO.read(new File(originalPath)); BufferedImage correctedImage correctOrientation(sourceImage, originalPath); // 2. 用 Thumbnailator 压缩保持宽高比最大边 300px Thumbnails.of(correctedImage) .size(300, 300) .outputQuality(0.85) // 压缩质量0.85 是画质和体积平衡点 .toFile(new File(thumbnailPath)); } catch (Exception e) { log.error(缩略图生成失败: {}, originalPath, e); } } // 修正 EXIF 朝向的核心方法 private BufferedImage correctOrientation(BufferedImage image, String imagePath) throws IOException { try (InputStream is new FileInputStream(imagePath)) { // 使用 metadata-extractor 读取 EXIF需额外引入依赖 Metadata metadata ImageMetadataReader.readMetadata(is); Directory exif metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); if (exif ! null exif.containsTag(ExifSubIFDDirectory.TAG_ORIENTATION)) { int orientation exif.getInt(ExifSubIFDDirectory.TAG_ORIENTATION); return rotateImageByOrientation(image, orientation); } } catch (Exception ignored) {} return image; }参数说明size(300, 300)表示“最长边 300px”不是固定宽高否则人像变方块outputQuality0.85是实测值——0.9 以上缩略图体积大 40%0.7 以下细节丢失严重correctOrientation方法需引入com.drew:metadata-extractor:2.17.0依赖否则无法读 EXIF。3.3 标签搜索用FIND_IN_SET实现模糊匹配但必须解决中文标签分词不准的痛点用户输入 “校园” 想搜出tags毕业,校园,晴天和tags校园生活,社团活动的照片。如果直接WHERE tags LIKE %校园%会误匹配tags毕业校园本意是“毕业”和“校园”两个标签。正确做法是把tags字段设计为逗号分隔字符串并用 MySQL 的FIND_IN_SET// PhotoMapper.java Select(SELECT * FROM photo WHERE FIND_IN_SET(#{keyword}, REPLACE(tags, , ,)) 0 ORDER BY upload_time DESC) ListPhoto selectByTag(Param(keyword) String keyword);注意REPLACE(tags, , ,)处理用户输入全角逗号中文输入法常见否则FIND_IN_SET(校园, 毕业校园)返回 0。但FIND_IN_SET无法利用索引大数据量会慢。优化方案是建一张关联表photo_tagphoto_id, tag_name用JOIN查询——毕设阶段用FIND_IN_SET足够答辩时可答“后续可优化为多对多关系”。4. 避坑毕业设计最常翻车的 4 个场景每一条都是我删库重来的血泪经验4.1 现象上传大文件20MB时接口超时前端显示 “Network Error”原因Nginx 或 Tomcat 默认超时时间只有 30 秒而 SpringBoot 的max-file-size只控制校验不控制传输耗时。解决在application.yml中增加 Tomcat 超时配置server: tomcat: connection-timeout: 600000 # 10 分钟如果用 Nginx 反向代理如宝塔面板在nginx.conf的location /api/块内加proxy_read_timeout 600; proxy_send_timeout 600;血泪经验曾因漏配 Nginx 超时用户上传 30MB 视频时Nginx 先断开连接SpringBoot 日志却显示 “上传成功”实际文件只写了一半。4.2 现象Linux 服务器上图片路径./uploads/2023/05/xxx.jpg404但 Windows 本地正常原因file:./uploads/在 Linux 下解析为/root/your-project/./uploads/而 Tomcat 默认不提供file:协议的静态资源服务。解决在application.yml中将static-locations改为绝对路径spring: web: resources: static-locations: classpath:/static/,file:/home/wwwroot/photo/uploads/启动前确保/home/wwwroot/photo/uploads/目录存在且 Java 进程有写权限mkdir -p /home/wwwroot/photo/uploads chown -R www:www /home/wwwroot/photo/血泪经验宝塔部署时www用户是默认运行用户用root启动 SpringBoot 会导致文件属主混乱后续缩略图生成失败。4.3 现象Vue 前端通过axios.get(/api/list)报 CORS 错误但 Postman 调用正常原因Vue 开发服务器npm run serve端口是8080SpringBoot 是8081浏览器同源策略拦截跨域请求。解决在 SpringBoot 中添加CrossOrigin注解到PhotoController类上RestController RequestMapping(/photo) CrossOrigin(origins http://localhost:8080) // 仅开发期允许 Vue 本地服务 public class PhotoController { ... }生产环境Vue 打包后放入static目录无需此配置因同域访问。血泪经验千万别在CrossOrigin(origins *)答辩时导师会问“生产环境是否允许任意域名调用”答不出就扣分。4.4 现象MySQL 中tags字段存了毕业,校园,晴天但搜索 “毕业” 返回空搜索 “毕业,” 却有结果原因FIND_IN_SET(毕业, 毕业,校园,晴天)正确但FIND_IN_SET(毕业, 毕业, 校园, 晴天)带空格失败因FIND_IN_SET不忽略空格。解决在INSERT时统一清理空格photo.setTags(tags.replaceAll(\\s, ).replaceAll(, ,)); // 去空格、转半角逗号或在 SQL 中用TRIMSELECT * FROM photo WHERE FIND_IN_SET(#{keyword}, TRIM(BOTH , FROM REPLACE(REPLACE(tags, , ,), , ))) 0血泪经验曾因前端传参带空格导致 200 张照片的标签搜索全部失效debug 到凌晨三点才发现tags字段里存的是毕业, 校园。5. Vue 前端整合与生产部署把dist打包文件放进static目录用一个 JAR 包跑通全栈5.1 Vue 项目打包必须关掉publicPath的默认/否则图片路径全错Vue CLI 默认vue.config.js中publicPath: /打包后index.html里的img src/uploads/2023/05/xxx.jpg会被浏览器请求http://localhost:8080/uploads/...但 SpringBoot 的静态资源映射是file:./uploads/路径不匹配。必须改为相对路径// vue.config.js module.exports { publicPath: ./, // 关键改为相对路径 outputDir: dist, assetsDir: static }执行npm run build后dist目录结构为dist/ ├── index.html ├── static/ │ ├── css/ │ └── js/ └── uploads/ // 注意这个 uploads 是空的实际图片由 SpringBoot 上传生成逻辑说明publicPath: ./让所有资源引用变成相对路径img src./uploads/2023/05/xxx.jpg浏览器会基于当前 HTML 路径拼接而index.html在 SpringBoot 的static目录下所以最终请求http://localhost:8080/uploads/2023/05/xxx.jpg正好命中file:./uploads/映射。5.2 整合步骤把dist复制到src/main/resources/static/删掉dist/uploads目录SpringBoot 启动时classpath:/static/是第一个静态资源位置file:./uploads/是第二个。因此dist/index.html→src/main/resources/static/index.htmldist/static/→src/main/resources/static/static/必须删除dist/uploads目录它只是占位真实图片由后端上传生成提示用命令行操作更可靠避免 IDE 缓存rm -rf src/main/resources/static/* cp -r dist/* src/main/resources/static/ rm -rf src/main/resources/static/uploads5.3 生产部署用mvn clean package打 JARnohup java -jar启动别碰 Docker学生机如腾讯云学生机内存仅 1GBDocker 启动一个容器至少吃 200MB留给 SpringBoot 的只剩 800MB上传大图时极易 OOM。直接 JAR 部署更轻量# 打包跳过测试节省时间 mvn clean package -Dmaven.test.skiptrue # 启动后台运行日志输出到 out.log nohup java -Xms512m -Xmx800m -jar target/photo-system-0.0.1-SNAPSHOT.jar out.log 21 # 查看进程 ps -ef | grep photo-system参数说明-Xms512m设初始堆内存 512MB-Xmx800m设最大堆内存 800MB避免频繁 GCnohup保证 SSH 断开后进程不退出 out.log 21合并标准输出和错误输出到文件方便查日志。5.4 验证全链路从浏览器打开首页上传一张图搜索标签下载原图——四步闭环打开首页访问http://你的服务器IP:8080看到 Vue 页面不是 Whitelabel Error Page上传图片点击上传按钮选一张 JPG观察 Network 面板POST /api/photo/upload返回 200Response是/uploads/2023/05/xxx.jpg搜索标签在搜索框输入 “校园”点击搜索GET /api/photo/search?keyword校园返回 JSON 中data.records有数据下载原图点击某张图的 “下载” 按钮浏览器弹出下载对话框文件名是原始名称如IMG_20230501.jpg进阶技巧在PhotoController的下载方法中用ResponseEntityResource设置Content-Disposition头确保下载文件名是原始名而非路径GetMapping(/download/{id}) public ResponseEntityResource download(PathVariable Long id) { Photo photo photoService.getById(id); Path path Paths.get(. photo.getStoredPath()); Resource resource new UrlResource(path); String header attachment; filename\ photo.getFileName() \; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, header) .body(resource); }这样导师点击下载得到的就是IMG_20230501.jpg不是20230501123456.jpg细节决定答辩印象分。6. 答辩加分项三个让导师眼前一亮的轻量级改造每个都不超过 20 行代码6.1 给相册加水印用 Graphics2D 在图片右下角打“个人相册”文字不依赖 ImageMagick导师看到“系统有水印功能”会觉得你考虑了版权保护。不用装外部软件Java 自带Graphics2D就能实现private void addWatermark(String imagePath) { try { BufferedImage image ImageIO.read(new File(imagePath)); Graphics2D g image.createGraphics(); g.setColor(new Color(255, 255, 255, 100)); // 半透明白色 g.setFont(new Font(微软雅黑, Font.BOLD, 30)); String watermark 个人相册; FontMetrics metrics g.getFontMetrics(); int x image.getWidth() - metrics.stringWidth(watermark) - 20; int y image.getHeight() - 20; g.drawString(watermark, x, y); g.dispose(); ImageIO.write(image, jpg, new File(imagePath)); } catch (IOException e) { log.error(水印添加失败, e); } }关键点new Color(255,255,255,100)的第四个参数100是 alpha 通道0~255值越小越透明100 是实测最佳值——太透明看不见太实心遮挡主体。调用时机放在upload方法保存文件后、生成缩略图前这样原图和缩略图都有水印。6.2 实现软删除按 Delete 键不真删而是设is_deleted1避免答辩时误删导师照片毕设系统没有回收站是硬伤。在Photo实体类中加字段TableLogic // MyBatis-Plus 软删除注解 private Integer isDeleted; // 0未删除1已删除application.yml中配置mybatis-plus: global-config: db-config: logic-delete-field: isDeleted logic-delete-value: 1 logic-not-delete-value: 0逻辑说明加了TableLogic后所有photoService.removeById(id)会自动转成UPDATE photo SET is_deleted1 WHERE id? AND is_deleted0所有photoService.list()会自动加AND is_deleted0条件。答辩时演示“删除照片”导师刷新页面看不到但你打开 H2 控制台执行SELECT * FROM photo WHERE is_deleted1就能展示数据还在体现工程思维。6.3 导出照片清单用 Apache POI 生成 Excel包含文件名、尺寸、上传时间、标签导师可能说“能导出所有照片信息吗” 一行代码生成 ExcelGetMapping(/export) public void export(HttpServletResponse response) throws IOException { ListPhoto photos photoService.list(); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(照片清单); String[] headers {文件名, 尺寸(KB), 上传时间, 标签}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum 1; for (Photo p : photos) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(p.getFileName()); row.createCell(1).setCellValue(p.getSizeKb()); row.createCell(2).setCellValue(p.getUploadTime().toString()); row.createCell(3).setCellValue(p.getTags()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamephoto_list.xlsx); workbook.write(response.getOutputStream()); }依赖org.apache.poi:poi-ooxml:5.2.4。注意response.getOutputStream()不能和ResponseBody共存所以方法返回void用HttpServletResponse直接写流。导出的 Excel 打开即见整齐表格比纯 JSON 高级得多。我带过 12 届毕业设计见过太多同学卡在“功能都写了但连不上前端”“答辩时接口 404”“导师问‘怎么保证图片不被外链盗用’答不上来”。这篇笔记里每一个命令、每一行配置、每一个避坑点都来自真实踩过的坑——不是理论推演是删库重来三次后记下的教训。SpringBoot 做相册后端核心就三件事上传别超限、缩略图别歪、搜索别漏字。把这三件事做扎实比堆十个花哨但不稳定的“AI 标签推荐”功能更能拿高分。希望帮到你。本文还有配套的精品资源点击获取