简介:一套面向Java开发者的Spring Boot相册管理系统完整源码,适合正在学习Spring Boot整合MyBatis Plus、文件上传下载及跨域处理的初学者或课程设计人群。项目聚焦相册与照片管理场景,涵盖用户注册登录、相册创建删除、照片上传下载等核心功能,并配有CorsFilter跨域配置、Java 8日期处理与CommonsMultipartFile文件处理工具类。整个压缩包共96个文件,包含33个Java源码、33个编译后的class文件、7个XML配置、2个YML配置、1个README文档及18张示例图片,整体仅4.24MB,结构清晰便于对照阅读。已有364人学习下载,适合用作用户管理、文件存储与相册业务开发的参考模板,也可直接作为毕业设计或项目实训的改造基础。
1. 一套能直接跑的 Spring Boot 相册管理系统:从源码到图片预览
很多人在找 Spring Boot 课设项目时,下载下来不是缺数据库脚本,就是启动直接报错。这套相册管理系统是我拆过的比较规整的一套:用户注册登录、创建相册、上传照片、列表预览、删除、按月份存储,全链路都是通的。它解决的是“拿到源码后怎么在两小时内跑起来”的问题,也顺手把 Spring Boot 文件上传、静态资源映射和 MyBatis Plus 常用操作一次覆盖掉。如果你是第一次接触 Spring Boot,建议按第三章的步骤走,同时把第四章的坑提前看一遍;如果你已经有基础,直接跳到第五章看缩略图和批量导出的思路,这部分对你扩展功能会有帮助。
2. 功能拆解:Spring Boot 相册管理的技术栈、表设计与存储选型
2.1 技术栈选型:为什么是 Spring Boot + MyBatis Plus + Thymeleaf
相册管理系统这种业务,最合适的组合就是 Spring Boot + MyBatis Plus + Thymeleaf + MySQL。Spring Boot 2.x 目前仍然是课设和毕设里最稳妥的选择,自动配置齐全,网上能查到的踩坑记录也最多。MyBatis Plus 的优势是单表 CRUD 完全不用写 XML,继承 BaseMapper 后 insert、selectList、deleteById 这些操作直接可用,省掉大量重复代码。Thymeleaf 做服务端渲染,对新手非常友好,不用额外搭 Node 环境和处理跨域问题,Controller 里塞 Model 就能把数据带到页面。
这套组合的另一层考虑是运行成本低。前后端分离的项目虽然界面更现代,但需要额外开前端服务和后端服务,部署时还要处理 Nginx 或者跨域配置,对只做课设展示和答辩演示来说反而增加负担。Thymeleaf 的方案是打包成一个 jar,JDK 8 环境一条命令就能跑,演示的时候也不用担心前端端口没启动导致白屏。
需要提醒的是:如果你本机装的是 JDK 17,就别用 Spring Boot 2.3 这种老版本,因为老版本对 JDK 17 支持不好,启动时会报 IllegalStateException。我一般建议用 Spring Boot 2.7 搭配 JDK 8 或 JDK 11,这是目前最没门槛的版本组合。如果非要用 JDK 17,就把 Spring Boot 升到 3.x,但 MyBatis Plus 也要跟着用 3.5.3 以上的版本,否则会有兼容问题。
2.2 数据库设计:user、album、photo 三张表撑起完整业务
相册系统的表结构不需要设计得很复杂,三张表就能覆盖全部流程:用户表 user、相册表 album、照片表 photo。用户和相册是一对多,相册和照片是一对多。下面是初始化脚本的核心部分:
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `create_time` datetime DEFAULT NULL COMMENT '注册时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `album` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '所属用户', `name` varchar(100) NOT NULL COMMENT '相册名称', `description` varchar(255) DEFAULT NULL COMMENT '相册描述', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `photo` ( `id` bigint NOT NULL AUTO_INCREMENT, `album_id` bigint NOT NULL COMMENT '所属相册', `user_id` bigint NOT NULL COMMENT '上传用户', `original_name` varchar(255) NOT NULL COMMENT '原始文件名', `file_path` varchar(255) NOT NULL COMMENT '存储路径', `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)', `upload_time` datetime DEFAULT NULL COMMENT '上传时间', PRIMARY KEY (`id`), KEY `idx_album_id` (`album_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个有意为之的设计。photo 表里冗余了 user_id,目的是做“用户最近上传照片”这类查询时,不用拿 album 表和 user 表做 join,直接按 user_id 过滤即可。upload_time 用了 datetime 而不是 timestamp,因为 MySQL 8 的 timestamp 带有时区转换,如果 JDBC 连接串里的 serverTimezone 配置不一致,查出来的时间可能差八个小时。这个细节很隐蔽,很多人头一天跑得好好的,第二天突然发现时间不对,多半就是这个原因。
建表脚本里没有设置任何外键约束,这也是故意的。MyBatis Plus 做单表操作时,外键约束反而会带来不必要的麻烦,比如删除用户前必须先删相册,否则报约束错误。逻辑删除由 Service 层控制,比数据库外键更灵活,这也是这套项目里值得学习的处理方式。
2.3 存储方案:本地磁盘保存文件,数据库只记录路径
图片文件的存储方式直接决定项目后续能不能扩容。这套系统默认使用的是本地磁盘存储,数据库 photo 表里只保存文件访问路径,不保存二进制数据。这么做的好处是数据库体积不会迅速膨胀,备份时只需要备份数据和上传目录两个部分,读取图片时由 Spring Boot 的静态资源映射直接指向目录,不需要把图片读进内存再输出。
对应的配置在 application.yml 里:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB mvc: static-path-pattern: /** resources: static-locations: classpath:/static/,file:${photo.upload-dir} photo: upload-dir: D:/upload/photostatic-locations 里的 file:${photo.upload-dir} 很关键,它把 URL 中 /upload 开头的请求直接映射到本地磁盘目录。比如你访问 http://localhost:8080/upload/202405/123.jpg,实际上读取的是 D:/upload/photo/202405/123.jpg。max-file-size 限制单个文件不超过 10MB,max-request-size 限制一次请求中所有文件总大小不超过 20MB,做批量上传时这两个值经常要一起调大,只调一个会踩到下一个坑。
上传接口的 Controller 实现如下:
@PostMapping("/upload") public Result upload(@RequestParam("file") MultipartFile file, @RequestParam("albumId") Long albumId, HttpServletRequest request) { if (file.isEmpty()) { return Result.error("文件为空"); } // 用时间戳加随机数拼文件名,解决中文名和重名问题 String ext = StringUtils.getFilenameExtension(file.getOriginalFilename()); String filename = System.currentTimeMillis() + "_" + RandomStringUtils.randomAlphanumeric(4) + "." + ext; // 按月分目录,避免单目录文件过多 String dateDir = new SimpleDateFormat("yyyyMM").format(new Date()); File dir = new File(uploadDir + "/" + dateDir); if (!dir.exists()) { dir.mkdirs(); } File dest = new File(dir, filename); try { file.transferTo(dest); String relativePath = "/upload/" + dateDir + "/" + filename; photoService.save(photo); return Result.success(relativePath); } catch (IOException e) { log.error("上传失败", e); return Result.error("上传失败"); } }注意 transferTo 之前必须先 mkdirs,否则文件不存在会抛 FileNotFoundException。文件名用当前时间戳加四位随机字母,基本可以避免重名冲突,也比纯 UUID 短一些,URL 看起来更干净。按月份分目录给后续做归档和清理留了余地,比如写一个定时任务把三个月前的照片迁移到备份磁盘,就可以按目录层级直接处理。
2.4 登录拦截与权限控制:拦截器比注解更直观
相册系统里不是所有页面都允许未登录用户访问,比如创建相册和上传照片必须要登录。这个项目里用的是 Spring Boot 拦截器,比在每个 Controller 方法上加注解更直观,也方便统一处理未登录跳转。
拦截器代码如下:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { // 未登录跳转到登录页 response.sendRedirect("/login"); return false; } return true; } }注册拦截器时,要注意放行静态资源,否则页面的 CSS 和图片也会被拦住:
@Override public void addInterceptors(InterceptorRegistry registry) { // 放行登录页、登录接口和上传目录的静态资源 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/api/user/login", "/upload/**"); }这里最容易错的是 /upload/** 的排除路径。如果忘了加,上传好的图片在页面上都无法显示,因为图片请求会被拦截器重定向到登录页,浏览器里看到的就是一张裂开的图。这也是为什么我把这个点单拎出来讲,它的排查过程往往很耗时间。
3. 把压缩包变成可访问的系统:环境准备、启动步骤与接口验证
3.1 导入项目和配置文件修改要点
拿到压缩包后,先确认目录结构。一个合格的 Spring Boot 工程应该有 pom.xml、src/main/java、src/main/resources、sql 文件夹。用 IDEA 打开时选择 pom.xml 作为 Maven 项目,等待依赖下载完成。如果之前装过老版本依赖,建议点一下 Maven 面板里的刷新按钮,确保所有 jar 包都重新解析。
需要修改的配置文件集中在 src/main/resources/application.yml。数据库连接部分是最重要的,直接复制容易翻车,因为每个人的 MySQL 密码不一样。参考配置如下:
spring: datasource: url: jdbc:mysql://localhost:3306/album_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver如果你的本机 MySQL 版本是 5.7,驱动的 8.0.x 版本也可以正常连接,因为驱动向下兼容。但如果 password 这一行是空值,就必须写 password: 后面留空,不能把整行删掉,否则 Spring 的自动配置会尝试用默认值,产生奇怪的认证错误。
上传路径的配置项 photo.upload-dir 也要改成你自己机器上的绝对路径,比如 Linux 下是 /opt/album/upload,Windows 下是 D:/album/upload。路径里不要带中文,Windows 下尽量避免 C 盘系统目录,因为权限问题经常导致文件写入失败。
3.2 数据库初始化与项目启动
在 MySQL 中执行 sql 目录下的 init.sql。这个脚本会先创建 album_system 数据库,然后建表并插入一个测试账号。执行完成之后,在 IDEA 里运行主类,看到这段日志就是启动成功:
Tomcat started on port(s): 8080 (http) with context path ''访问 http://localhost:8080/ 会跳转到登录页。如果是有 BCrypt 加密的账号,登录密码需要从脚本里找,一般是 admin/admin123 或者根据注册接口生成的记录。如果脚本里插入的是明文密码,那你需要确认项目里有没有加 BCryptPasswordEncoder,否则对比密码时会一直失败。
3.3 用 Postman 走一遍核心接口
页面功能验证通过后,建议再走一遍接口,确认后端逻辑没被页面掩盖问题。打开 Postman,按下面的顺序建请求:
- POST http://localhost:8080/api/user/login,Body 选择 x-www-form-urlencoded,传 username 和 password,返回结果里应包含用户 ID。
- POST http://localhost:8080/api/album/create,传 name 和 description,返回相册 ID。
- POST http://localhost:8080/api/photo/upload,Body 选择 form-data,添加 albumId 字段和 file 文件字段。
- GET http://localhost:8080/api/photo/list?albumId=1,返回该相册下照片列表。
上传接口返回的 relativePath 可以拼成完整 URL 直接访问图片。如果返回 404,先查配置里的 static-locations 和实际目录是否对齐。我一般在服务器上会用 curl 测试图片 URL,看返回状态码是 200 还是 302,302 多半是被拦截器重定向了,200 才是真的读到了图片文件。
3.4 前端页面和服务端配置的对照清单
这个项目的前端页面是 Thymeleaf 模板,目录在 src/main/resources/templates/ 下。页面里的静态资源路径,如 CSS、JS,都是以 / 开头。这里有一份我整理的最小配置对照表,启动前逐项检查一遍,能省掉大半启动后的排查时间:
| 检查项 | 预期值 | 出现问题的表现 |
|---|---|---|
| server.port | 8080 或自定义 | 端口被占用时启动报 BindException |
| context-path | 留空 | 设置成 /demo 后所有页面路径要多一层前缀 |
| datasource url | 包含 serverTimezone | 时间差八小时 |
| multipart max-file-size | 10MB 或更大 | 上传大文件报 MaxUploadSizeExceededException |
| static-locations | 包含 file:${photo.upload-dir} | 图片 404 |
| thymeleaf cache | false | 改页面不生效(开发时建议 false) |
开发阶段务必把 spring.thymeleaf.cache 设置为 false,否则你改了 HTML 后浏览器还是旧页面,必须重启服务,很影响效率。这个配置在生产环境再改成 true 也不迟。
4. 避坑指南:相册管理项目里最常见的五个问题
4.1 启动报“Field uploadDir cannot be found”
现象:项目启动时抛 BeanCreationException,提示 uploadDir 属性找不到,或者在 @Value 注入的地方直接报空指针。
原因:配置类里的 @Value("${photo.upload-dir}") 读取不到配置项,可能是 application.yml 里没有这一项,也可能是配置类的位置没有被 Spring 扫描到。还有种情况是把配置写在 application.properties 里,但项目加载的是 yml,两者没对应上。
解决:检查配置类是否在 Spring Boot 主类所在包的子包下,然后打开 application.yml 看 photo 和 upload-dir 的层级关系。注意 yml 里冒号后面必须有一个空格,写成 photo: upload-dir 而不是 photo: upload-dir 这种格式。我一般会在配置类里临时输出一下这个值,确认读取结果,再往下排查。
4.2 上传接口返回 404,文件没有写入任何目录
现象:Postman 调用上传接口,返回 404,后台日志里没有异常,只有一条路径匹配记录。
原因:404 分两种情况,要么是 Controller 的 @RequestMapping 路径没写对,要么是 Spring MVC 没有识别到 MultipartResolver。第二种情况在 Spring Boot 里其实很少发生,因为自动配置会创建标准 MultipartResolver,但如果 pom.xml 里误加了别人的依赖,把自动配置覆盖了,就会出现上传接口 404。
解决:先用 GET 请求访问上传接口路径,如果返回 405 说明路径存在但方法不对,返回 404 说明路径没匹配上。确认 Controller 类上有 @RestController 或 @Controller 注解,且没有把方法写成 private。如果依赖有冲突,检查 pom.xml 里有没有引入 spring-boot-starter-web 两遍的情况,删掉多余的依赖。
4.3 上传成功但图片无法预览,浏览器显示裂图
现象:数据库 photo 表能看到记录,文件也保存在本地目录,但浏览器访问图片 URL 返回 404。
原因:这是静态资源映射最常见的问题。比如文件保存到了 D:/upload/photo/202405/xxx.jpg,数据库里存的路径是 /upload/202405/xxx.jpg,但 application.yml 里的 upload-dir 配置成了 D:/upload/photo/202405,导致最终拼接出的路径是 D:/upload/photo/202405/202405/xxx.jpg,文件自然找不到。
解决:对比上传接口里保存的 relativePath 和 upload-dir 之间的拼接关系。如果接口里写的是 dateDir + filename,那么 upload-dir 就不要带日期目录。另外检查 URL 是否有中文编码问题,上传时用时间戳加随机数的方式可以完全避开中文文件名,这是最省事的做法。
4.4 列表页时间字段显示为空或时间相差八小时
现象:数据库表里 create_time 有值,但页面表格显示空白,或者查出来的时间比实际时间多八个小时。
原因:空白通常是 MyBatis Plus 没有把数据库的下划线字段映射到实体类的驼峰字段。比如数据库列是 create_time,实体属性是 createTime,默认的 map-underscore-to-camel-case 配置没打开时,属性就匹配不上。相差八小时则是 JDBC 连接串里的 serverTimezone 没有配置,或者配置成了 UTC。
解决:在 application.yml 里开启下划线转驼峰:
mybatis-plus: configuration: map-underscore-to-camel-case: true同时把数据库连接串改成 serverTimezone=Asia/Shanghai。这两个配置都改完后,重启项目再看时间。如果数据库里的时间本身就是错的,那需要检查建表语句里有没有设置 DEFAULT CURRENT_TIMESTAMP,建议统一用 insert 时由 Java 端填充,不要依赖数据库默认值。
4.5 修改端口后页面能打开,但所有接口请求失败
现象:把 server.port 改成 8081,登录页正常显示,但点击登录后没有任何反应,控制台也没有网络请求。
原因:页面里的 JavaScript 把接口地址写死了,比如 $.post("http://localhost:8080/login"),端口改了以后浏览器还在请求 8080,自然不会成功。这种问题在带前端页面的课设项目里非常常见,因为开发时只改了后端端口,忘了前端代码里也有地址。
解决:打开 templates 目录下的 login.html、album.html、photo.html,搜索所有 http:// 开头的字符串,全部改成以 /api 开头的相对路径,这样端口由浏览器自动带入当前页面地址。如果必须要拼完整地址,用 Thymeleaf 取端口:
<script th:inline="javascript"> var baseUrl = '/' + window.location.host + '/api'; </script>从那以后我入手任何项目,第一件事就是全局搜索 8080 这个字符串,看看有多少写死的地方,能在启动前改完的就绝不等页面跑挂了再回头补。
5. 进阶玩法:给相册系统加缩略图缓存和批量下载
基础功能跑通之后,下一步往往是应对演示时的性能问题。相册列表页如果一次性展示几十张原图,比如手机拍的一张照片就有 8MB,浏览器加载会非常慢,答辩时现场网络差一点直接白屏。我的做法是给项目加上缩略图生成,用 Thumbnailator 这个轻量库,首次访问时生成缩略图,之后直接读缓存文件,把图片体积压缩到原来的十分之一甚至更小。
先加依赖,然后在 PhotoService 里写一个生成方法:
public void generateThumbnail(String sourcePath, String thumbPath) { File source = new File(sourcePath); File dest = new File(thumbPath); if (!dest.exists()) { Thumbnails.of(source) .size(600, 600) .outputFormat("jpg") .toFile(dest); } }调用时机放在照片上传成功之后,直接调用一次生成,列表页读取的 URL 指向 thumb 目录,原图只在大图查看时访问。size(600, 600) 表示宽度和高度都限制在 600 以内,等比缩放,不会拉伸变形。
批量下载也是演示时常用的功能,本质是把相册下所有照片打包成一个 zip 文件输出给浏览器。关键点是 zip 包内的文件名不能带绝对路径,否则对方解压时会出现多层无用目录。我会用 UUID 前缀加原始文件名:
ZipOutputStream zos = new ZipOutputStream(response.getOutputStream()); for (Photo photo : photoList) { String entryName = UUID.randomUUID().toString().substring(0, 8) + "_" + photo.getOriginalName(); zos.putNextEntry(new ZipEntry(entryName)); Files.copy(Paths.get(photo.getFilePath()), zos); zos.closeEntry(); } zos.finish();这个方案对课设和中小规模场景足够用,真正的生产环境会有磁盘 IO 和内存压力,但相册管理系统的定位本身就不需要扛住超大规模并发。从那以后我每次拿到这类项目,都会先确认静态资源映射和文件上传路径是不是外置配置,而不是散落在业务代码里。你要改业务、加功能、做性能优化,都从这两条线入手最安全。希望这份拆解能帮你把相册管理系统顺利跑起来,也让你在 Spring Boot 的文件上传和资源映射这条路上少走一些我当年走过的弯路。
本文还有配套的精品资源,点击获取