
简介这是一份基于Spring Boot开发的相册管理系统完整项目面向Java后端入门及课设/毕设场景涵盖用户注册登录、相册维护、照片批量上传下载等核心功能可帮助理解分层开发与文件处理。项目采用Spring Boot MyBatis Plus MySQL通过CorsFilter处理跨域、Java 8 DateTime统一日期格式代码按mapper、service、controller分层便于研读。压缩包共96个文件含33个Java源码、33个class文件、18张jpg示例图、7个XML配置及2个YML配置大小4.24MB轻量易部署。附带README和application.yml便于快速启动与二次开发。已有363人学习下载适合需要完整参考项目或搭建类似系统的开发者。1. 这套 Spring Boot 相册管理系统源码包能跑通再谈改拿到这份“基于 Spring Boot 的相册管理系统.zip”先别急着解压看代码第一步应该确认它能不能在你自己的电脑上直接启动。这是一套典型的 Spring Boot 单体课程设计源码包后端用 Spring MVC 接收请求Thymeleaf 做页面渲染MyBatis 处理数据库操作MySQL 存数据图片走本地磁盘存储。整个项目没有 Docker、没有前后端分离、没有微服务那套复杂结构胜在链路短、好改、能跑。它能解决的具体问题是图片上传、相册分类、列表分页、关键字搜索外加一套最简单的登录拦截。适合两类人一类是准备交课程设计或毕业设计的学生另一类是想快速过一遍 Spring Boot 全流程的初级开发者。如果你期待的是分布式存储和高并发架构那这个包帮不上忙它最大的价值是让你在两个小时内把工程跑起来再逐行看懂每个环节。2. 项目骨架与技术选型先看清 Spring Boot 依赖再动手打开源码包第一件事我建议你先看pom.xml和application.yml不要一上来就点开 Controller。这个阶段花十五分钟能搞清楚整套系统的技术组合后面定位问题会少走很多弯路。2.1 依赖清单与选型为什么是 Thymeleaf 而不是前后端分离课程设计类的相册系统最常见的技术组合是 Spring Boot Thymeleaf MyBatis MySQL这套组合最大的优势是上手门槛低、调试直观。Controller 里返回一个视图名Thymeleaf 直接渲染 HTML不需要额外启动前端项目也不用处理跨域问题。对交课设的场景来说用一个 Spring Boot 内嵌的模板引擎比引入 Vue Axios 的分离架构要省事得多。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper-spring-boot-starter/artifactId version1.4.7/version /dependency /dependencies这套依赖配 Spring Boot 2.7.x 和 JDK 8 完全没问题。spring-boot-starter-web负责内嵌 Tomcat 和 Spring MVCspring-boot-starter-thymeleaf做服务端页面渲染mybatis-spring-boot-starter用 2.3.2 这个版本是因为它对 Spring Boot 2.x 的兼容性最稳太旧的版本会在启动时出现日志组件冲突。这里我提一个点如果你拿到的源码用的是 MyBatis-Plus而不是原生 MyBatis那mapper接口和 XML 文件放在同一个包下时必须在application.yml里额外配置mapper-locations否则启动后所有查询都会报Invalid bound statement (not found)这个后面避坑章节还会展开。对比前后端分离的方案Thymeleaf 这种服务端渲染的方式对课设项目有三个实际好处第一不用单独管理静态资源服务器的跨域配置第二页面上的数据由 Model 直接携带断点调试时一眼就能看到值第三打包产物就是一个可执行的 jar部署时不用再配置 Nginx 转发。代价是前后端代码耦合如果以后想改成接口给小程序用需要把页面部分重写。对这个项目来说这个代价完全可以接受。2.2 application.yml 配置数据源、上传路径与日志输出配置文件是这套系统的命脉之一。我一般拿到手会先改三处数据库连接、上传目录、日志输出级别。这三处配置错了后面所有功能都会跟着出错。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/album_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl file: upload-dir: D:/album_data/upload这里逐项说清楚参数含义。serverTimezoneAsia/Shanghai是给 MySQL 8 用的不加它启动时大概率报时区错误characterEncodingutf8保证写入数据库的中文不乱码。spring.thymeleaf.cache: false在开发期必须关掉否则改了 HTML 页面不刷新浏览器里永远是旧页面这算新手最常见的一个“玄学”问题。spring.servlet.multipart.max-file-size: 10MB控制单张图片大小max-request-size: 20MB控制一次请求的总大小这两个值按需调调太小上传稍大点的图片就被拒。mybatis.configuration.log-impl设为StdOutImpl之后控制台会实时打印 SQL排查问题时能直接看到语句和参数但生产环境记得去掉不然日志量很大。最后的file.upload-dir是自定义配置项我在后面写了个ConfigurationProperties来读取它。数据库部分需要有基础表结构。建表语句一般放在src/main/resources下的sql目录或者你直接用 Navicat 执行也行。CREATE DATABASE IF NOT EXISTS album_system DEFAULT CHARACTER SET utf8mb4; USE album_system; CREATE TABLE album ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 相册名称, description VARCHAR(255) COMMENT 相册描述, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ); CREATE TABLE photo ( id INT PRIMARY KEY AUTO_INCREMENT, album_id INT NOT NULL COMMENT 所属相册ID, url VARCHAR(255) NOT NULL COMMENT 图片访问路径, original_name VARCHAR(255) COMMENT 原始文件名, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 上传时间 );两张表的关系很简单一个相册对应多张照片photo.album_id关联album.id。为什么用utf8mb4而不是utf8因为utf8mb4能存 emoji 表情和生僻字而且兼容性更好现在的新表我基本都默认utf8mb4。url字段存的是相对路径比如/uploadUrl/xxxx.jpg这个路径要和后端的静态资源映射规则对应这部分在第 3 章会详细说。3. 图片上传与访问链路从 MultipartFile 落盘到静态映射相册管理系统的核心功能是图片上传。这一章我会把上传接口的完整实现、文件落盘逻辑、以及浏览器如何访问这张图片讲透。这里也是项目里最容易翻车的地方值得花时间看仔细。3.1 上传接口MultipartFile 接收与 file.transferTo 的落盘逻辑上传接口在PhotoController里核心逻辑是接收前端传来的MultipartFile把图片写入磁盘再把访问路径存进photo表。下面是简化后的完整实现。PostMapping(/upload) public String upload(RequestParam(file) MultipartFile file, RequestParam(albumId) Integer albumId, HttpSession session) { // 防止用户绕过登录页直接调接口 if (session.getAttribute(loginUser) null) { return redirect:/login; } if (file.isEmpty()) { throw new RuntimeException(上传文件不能为空); } // 取原始文件名的后缀用于生成新文件名 String originalName file.getOriginalFilename(); String suffix ; if (originalName ! null originalName.contains(.)) { suffix originalName.substring(originalName.lastIndexOf(.)); } // 用 UUID 拼后缀生成新文件名避免重名覆盖和中文文件名乱码 String newName UUID.randomUUID().toString().replace(-, ) suffix; // 从自定义配置读取磁盘目录D:/album_data/upload String uploadDir fileUploadProperties.getDir(); File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } try { file.transferTo(new File(uploadDir / newName)); } catch (IOException e) { throw new RuntimeException(文件保存失败, e); } // 数据库保存相对路径页面通过 /uploadUrl/ 映射访问 photoService.savePhoto(albumId, /uploadUrl/ newName, originalName); return redirect:/album/detail?albumId albumId; }代码逻辑是先做登录校验再判空然后生成唯一文件名创建目标目录调用transferTo落盘最后写库。这里有几个关键参数值得注意。file.transferTo()是 Spring 封装的方法它要求目标文件所在目录必须已经存在很多人的报错就是java.nio.file.NoSuchFileException原因是用了相对路径如upload/xxx.jpg而 IDEA 中运行时的当前工作目录和命令行java -jar运行时的目录不一样目录一找不到文件就写不进去。所以我建议用配置项写死一个绝对路径比如D:/album_data/upload这样开发和生产环境行为一致。文件名生成用 UUID 而不是直接用原始文件名是因为原始文件名可能有中文、空格、特殊字符拼进 URL 会出现编码问题而且同名文件会互相覆盖。这个做法有一个副作用数据库的original_name字段把原始文件名存了下来方便列表页展示“拍摄自 xxx”之类的信息。new File(uploadDir / newName)这里别有空格Windows 下路径分隔符用反斜杠没问题Linux 服务器上也兼容因为 Java 的File会自己处理分隔符。如果你想在这个基础上扩展把本地存储替换成 MinIO 对象存储接口层改动非常小只需把transferTo换成minioClient.putObject其余参数校验和数据库记录逻辑可以原样保留。先跑通本地再考虑上对象存储这个顺序更稳。3.2 静态资源映射访问不到图片时排查这三处图片落盘成功不代表浏览器能访问到要让/uploadUrl/xxx.jpg这个 URL 对应到磁盘上的D:/album_data/upload/xxx.jpg必须配置静态资源映射。这一步漏了你上传的图片会在页面上一片红叉。Configuration public class WebMvcConfig implements WebMvcConfigurer { private final FileUploadProperties fileUploadProperties; public WebMvcConfig(FileUploadProperties fileUploadProperties) { this.fileUploadProperties fileUploadProperties; } Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploadUrl/**) .addResourceLocations(file: fileUploadProperties.getDir() /); } }这段配置的核心是addResourceHandler和addResourceLocations两个参数。addResourceHandler(/uploadUrl/**)定义的是浏览器访问时的 URL 前缀addResourceLocations(file: dir /)定义的是文件在磁盘上的真实位置。注意这里必须写file:前缀表示从本地文件系统读取否则 Spring 会把它当作 classpath 资源处理。结尾的斜杠也不能省D:/album_data/upload和D:/album_data/upload/在资源映射里含义不同少了斜杠有些 Spring Boot 版本会匹配不到文件。用ConfigurationProperties读取自定义配置项的部分也很简单直接绑定file.upload-dir的值。Component ConfigurationProperties(prefix file) public class FileUploadProperties { private String uploadDir; public String getDir() { return uploadDir; } public void setUploadDir(String uploadDir) { this.uploadDir uploadDir; } }遇到图片 404 的情况时排查顺序我一般是三连问。第一WebMvcConfig有没有被 Spring 扫描到确认这个类是否在启动类的子包下第二addResourceLocations里的路径结尾有没有斜杠前缀有没有file:第三数据库photo.url里存的值是不是以/uploadUrl/开头。这三处对齐了图片基本就能正常显示。4. 相册列表分页与关键字搜索Controller 层参数怎么设相册列表页是整个系统的门面这里涉及两个高频技术点分页和模糊搜索。很多人在这个模块栽过跟头要么翻页后数据不变要么搜索关键字后 SQL 报错。这一章把参数设计和 SQL 写法拆开讲。4.1 PageHelper 分页startPage 的参数与 PageInfo 的边界分页我建议直接用 PageHelper 插件它是国内课设项目里最主流的方案一行代码就能完成分页查询不用手写LIMIT。核心逻辑在AlbumController里。GetMapping(/album/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, String keyword, Model model) { // 关键点pageNum 从 1 开始pageSize 是每页条数 PageHelper.startPage(pageNum, pageSize); // 紧接着的这条查询会被自动拦截并添加 LIMIT ListAlbum albumList albumService.search(keyword); // PageInfo 里封装了总条数、总页数等翻页数据 PageInfoAlbum pageInfo new PageInfo(albumList); model.addAttribute(pageInfo, pageInfo); return album/list; }PageHelper.startPage(pageNum, pageSize)的参数设计有一个重要边界pageNum从 1 开始也就是说第一页是pageNum1不是 0这和 MyBatis 里手写LIMIT 0, 10的逻辑不同。如果你之前写过原生 SQL 分页很容易在这个地方把页码搞混导致第一页显示第二页的数据。pageSize是每页显示多少条课设项目一般设 10 条数据量大时可以调整为 15 或 20。这里最需要注意的场景是startPage只对紧接着执行的第一条select查询生效。如果你在startPage之后先执行了别的查询比如查一个相册数量做统计分页就会作用到那条统计 SQL 上列表页数据就会莫名其妙地少了或者重复。正确做法是让startPage后面紧跟需要分页的那条查询语句中间不要插入任何其他数据库操作。PageInfo这个类会自动把查询结果包装起来里面提供total、pageNum、pages等属性前端直接用 Thymeleaf 就能渲染页码按钮。常见的参数对应关系我整理成了一张表方便你对照确认。参数名含义取值范围与说明pageNum当前页码从 1 开始前端传 0 或负数时 PageHelper 会自动当第 1 页处理pageSize每页条数建议 10~20 条太大页面渲染会慢total总记录数由 PageHelper 自动执行 count 查询得到pages总页数计算规则(total pageSize - 1) / pageSizenavigatePages页码列表长度默认 8控制底部页码按钮显示几个翻页后数据不变的常见原因是前端表单重新提交了搜索条件但页码参数丢失了。这个要靠 Thymeleaf 模板在生成分页链接时把keyword一并带上否则从第二页开始搜索条件就失效了。4.2 模糊搜索与时间排序SQL 拼接里的三个隐蔽坑相册列表页通常会有一个搜索框按相册名称模糊匹配。这个功能在AlbumMapper.xml里实现SQL 不复杂但三个隐蔽坑值得单独说。select idsearch resultTypecom.example.album.entity.Album SELECT id, name, description, create_time FROM album where if testkeyword ! null and keyword ! AND name LIKE CONCAT(%, #{keyword}, %) /if /where ORDER BY create_time DESC /select第一个坑是占位符的选择。LIKE CONCAT(%, #{keyword}, %)用#{}预编译传进去的值会被当作参数处理不会改变 SQL 结构但如果写成%${keyword}%${}是字符串替换用户输入% OR 11 --之类的关键字会直接拼进 SQL造成注入风险。一律用#{}。第二个坑是LIKE和%的匹配关系。CONCAT(%, #{keyword}, %)表示包含匹配任何位置包含关键字都算命中。但如果你希望按前缀匹配也就是搜索“旅”能出来“旅行相册”但搜索“旅行相册”不出来“我的旅行相册”这种那要把 SQL 改成CONCAT(#{keyword}, %)。至于后缀匹配实际场景很少用到就不展开了。第三个坑是ORDER BY create_time DESC在数据量大了之后性能会明显下降。create_time字段如果没加索引MySQL 要先把全表数据查出来再排序相册数据上千条时感觉不明显上万条时会明显卡顿。解决方案是在建表语句里给create_time加一个索引ALTER TABLE album ADD INDEX idx_create_time (create_time);加了索引之后排序走索引扫描速度会快不少。另外搜索场景最好也给name字段加一个普通索引LIKE %keyword%这种写法虽然用不上索引但至少能让执行计划更稳定。控制器里接收keyword参数时还要做一层防空处理。如果用户没输入关键字就提交搜索Controller 侧拿到的keyword是null此时if判定为 falseSQL 退化为全表查询这个逻辑是对的。但如果前端传了一个空字符串keyword ! 判定为 false同样不会拼LIKE。所以 XML 里的if判断要同时考虑null和空串两种情况这也是很多人在搜索栏偶尔好使、偶尔不好使的原因。5. 相册系统常见问题排查五条踩坑记录这一章我整理了接手 Spring Boot 相册项目时最常见的五个问题每一条都是血泪经验。你照着顺序排查能省下大量去网上搜“Spring Boot 图片上传 404”这类问题的时间。5.1 图片上传成功但访问 404现象接口返回成功数据库里也有记录但浏览器访问/uploadUrl/xxx.jpg直接 404页面图片位置是个红叉。原因绝大多数情况是静态资源映射没生效。要么是WebMvcConfig没有被 Spring Boot 扫描到要么是addResourceLocations的路径少了结尾斜杠要么是数据库里存的 URL 是upload/xxx.jpg而不是/uploadUrl/xxx.jpg和后端资源映射的 handler 对不上。解决按三个位置依次核对。首先确认WebMvcConfig这个类放在启动类所在包或其子包下否则 Spring 不会扫描它其次检查addResourceLocations(file: dir /)结尾的斜杠这里少一个斜杠就可能匹配失败最后打开数据库看一眼photo.url字段里存的实际值如果存的是相对路径把存储逻辑改成存/uploadUrl/开头的路径。5.2 图片上传报 FileSizeLimitExceededException现象上传一张稍微大点的图片控制台抛MaxUploadSizeExceededException或FileSizeLimitExceededException页面直接 500。原因spring.servlet.multipart.max-file-size默认只有 1MB课设项目里手机拍的照片动辄几兆很容易超限。很多人只改了前端的accept限制忽略了后端配置。解决在application.yml里把上限调大单文件 10MB、单次请求 20MB 一般够用。注意max-file-size和max-request-size是两个独立参数max-file-size限制单个文件max-request-size限制一次请求里所有文件的总大小两个都要改。5.3 Thymeleaf 页面修改后浏览器一直显示旧页面现象改了templates/album/list.html里的文字或布局浏览器刷新好几次都不生效看起来像代码没改。原因Thymeleaf 默认开启模板缓存运行时会把解析过的模板存起来文件改了但它不知道。这个问题在开发期出现的频率极高尤其是不知道有这个配置的人会反复重启应用白白浪费时间。解决开发环境在application.yml里加spring.thymeleaf.cache: false保存后刷新页面即可生效。如果用 IDEA 还不行执行一次Build - Rebuild Project重新生成 target 目录。如果改了页面但控制台没报错也没变化多半就是这个问题不是代码逻辑的锅。5.4 分页第二页开始数据重复或总数不对现象第一页显示正常点击第二页后数据和第一页一模一样或者PageInfo.total的数字明显错误比如总共有 25 条数据但 total 只显示 10。原因PageHelper.startPage()只管紧接着的第一条select查询。如果你在startPage后先执行了其他查询比如查一下相册总数、查一下用户信息分页就会作用到错误的 SQL 上。另一个常见原因是pageNum参数没有正确绑定前端传的是字符串Controller 用Integer接收时报类型转换异常但异常被吞掉了导致每次都查第一页。解决代码里确保startPage后面紧跟那条需要分页的查询中间不要插入任何其他数据库操作。Controller 方法参数用RequestParam(defaultValue 1) Integer pageNum强制转换类型前端分页链接里也要带上pageNum和pageSize两个参数。拿不准时可以打开mybatis.configuration.log-impl: StdOutImpl控制台会打印出 PageHelper 自动拼接的LIMIT语句一眼就能看出来分页作用到哪条查询上了。5.5 打包成 jar 后上传的图片找不到了现象本地 IDEA 里运行一切正常执行mvn spring-boot:run或者java -jar启动后上传图片偶尔报错或者上传成功但访问时 404重启后图片全没了。原因开发时用了相对路径作为上传目录IDEA 的工作目录固定所以没问题。但java -jar启动时工作目录依启动位置而定Spring Boot 内嵌 Tomcat 的临时目录也可能变化相对路径就失效了。更隐蔽的是如果上传目录落在了 jar 包内部重启后临时文件被清理图片就“消失”了。解决把上传目录改成外部绝对路径在application.yml里用配置项file.upload-dir指定同时保证WebMvcConfig里的addResourceLocations也指向同样的目录。生产环境最好直接用/data/album/upload这种 Linux 路径。从那以后我每次部署 Spring Boot 项目都强制先检查一遍所有磁盘写入路径是不是绝对路径再决定要不要动代码。希望帮到你。本文还有配套的精品资源点击获取