ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Spring Boot个人云盘系统实战:分片上传、JWT与文件管理设计

Spring Boot个人云盘系统实战:分片上传、JWT与文件管理设计 最近做了一套基于Spring Boot的网盘类管理系统核心功能就是个人云盘。像这种Spring Boot 前端JS的Title其实是很典型的Java Web练手项目网上也有各种各样的教程但绝大多数都是跑通就完事真正像工程一样考虑细节的很少。这篇文章就按我实际开发这套个人云盘管理系统的经验把这个题目的技术架构、数据库、核心功能、中间踩的坑从头梳理一遍给想拿这个题目做毕业设计或者练手的同学一个可以直接照抄的参考。整个系统功能覆盖得很完整用户注册登录、文件上传下载、文件夹管理、分享链接、回收站恢复、图片和文本在线预览。别看这些功能听起来简单真正做起来文件目录层级、分片上传、逻辑删除、权限校验这些模块单独拎出来都能写很多文章。所以我这里会把重点放在整体设计和最容易踩坑的实现细节上而不是贴几百行代码充数。1. 项目整体设计与技术选型思路1.1 核心需求拆解个人云盘听起来简单但真正拆开需求以后会发现很多东西如果不提前理清楚开发到后面很容易自己把自己绕晕。我把需求分成四块。第一块是用户相关。登录、注册、退出、获取当前登录用户信息。这个系统定位是个人云盘所以权限模型不需要做多角色只要认准当前登录的用户是谁就行。所有文件数据都归属于某个用户IDA用户看不到B用户的文件。第二块是文件管理。这是核心中的核心包括上传文件、下载文件、新建文件夹、重命名、删除、移动、文件列表展示、回收站恢复。其中文件列表的树形展示或者叫目录结构是很多新手容易卡住的地方。因为文件系统是有层级关系的不是平铺的列表。第三块是分享功能。文件可以生成一个分享链接加上提取码发给别人对方在浏览器里打开链接输入提取码后能看到文件详情或者直接下载文件。分享链接有过期时间过期后自动失效。第四块是其他辅助功能比如文件图标展示、图片预览、文本预览、磁盘使用情况统计之类的。这属于加分项但实际开发时也要考虑进去因为页面展示的时候得根据文件类型做不同的标识。需求拆解完以后最好不要急着写代码先设计一下数据表。数据表设计得好后面编码会很顺畅数据表设计得有问题后面会不断返工。所以我把数据库设计放在开发之前一步。1.2 为什么选Spring Boot 前端JS这个技术组合在毕业设计和各类实战项目里出现频率非常高。后端用Spring Boot核心原因在于它把Spring家族的配置简化到了不需要XML的程度内嵌Tomcat容器一个main方法就能启动服务。相比传统的SSHSpring Struts Hibernate或者SSMSpring Spring MVC MyBatis方案省去大量配置时间让开发者能把精力集中在业务逻辑上。另外Spring Boot有非常成熟的生态后面我们用到Web上传、拦截器、配置文件多环境切换都有现成方案。前端选择JavaScript方向具体我用了Vue框架加Element UI组件库。Vue的写法比原生JS更清晰数据驱动视图文件列表、上传弹窗这些交互用组件化开发会舒服很多。当然如果说题目里只写了基于JS用原生JS照样可以做只是开发速度慢一些。我的建议是如果自己有Vue基础就用Vue如果完全不会框架用原生JS配合jQuery也能完成只要确保和后端交互是标准HTTP接口即可。前后端分离以后开发的时候启动两个服务前端开发服务器和后端Java进程。遇到跨域问题不要怕后面我会专门讲CORS怎么配。重要的是不要在前端JS里写死绝对路径所有环境相关配置放到全局配置里统一管理。1.3 系统架构设计整个系统的架构并不复杂思路如下。前端页面通过浏览器发起HTTP请求请求走到后端的Controller层。Controller负责接收参数、校验参数、调用Service再返回统一格式的JSON给前端。Service层处理具体业务逻辑比如文件上传时判断大小、生成存储路径、更新数据库记录。数据访问用MyBatis Plus内置的BaseMapper常规的增删改查不需要手写SQL只有递归查询目录、多表统计这种特殊场景需要自定义XML。工程内的包结构大概是controller控制层service业务层mapper数据访问层entity数据库实体dto前端出入参封装config配置类utils工具类另外为了统一返回消息我写了一个单独的结果封装类比如返回码200表示成功500表示业务异常。所有Controller接口都返回这个封装前端根据返回码统一处理错误提示这样不需要每个接口单独写异常逻辑。提示前期架构可以简单但结果封装和异常处理务必要统一。前期偷懒只返回裸数据后期加了复杂页面之后前端会被各种非标准返回搞得焦头烂额。2. 数据库设计与核心表结构2.1 用户表设计用户表我用了这张结构字段名id、username、password、nickname、avatar、create_time、status。id是自增主键。username要加唯一索引避免注册时重名。password存BCrypt加密后的密文绝对禁止明文。avatar可以先存一个字符串路径指向服务器上的头像图片链接也可以不做头像功能完全省掉。status用于禁用账户1表示正常0表示禁用。注册接口的逻辑很简单先检查用户名是否已存在存在则提示用户名已被注册不存在则用BCrypt工具类加密密码写入。登录接口查询用户名和密码匹配匹配成功生成JWT令牌返回给前端。这里要注意用户密码的密文长度一般60位在数据库里字段长度不要建短了我遇到过有人把密码字段设置成varchar(32)结果BCrypt密文根本存不下。2.2 文件信息表设计文件表是整个系统的核心字段比较多。我用的核心字段是id文件IDfile_name文件名file_size文件大小单位字节file_type文件类型比如image/png、application/pdffile_path存储的相对路径不含根目录前缀user_id所属用户IDparent_id父目录ID0表示根目录folder_flag是否文件夹1是0否del_flag逻辑删除标记0正常1已删除create_time创建时间update_time更新时间这里有一个关键点很多初学者喜欢把文件的实际存储路径直接存一个绝对路径比如C:\server\data\user1\abc.txt。这个在本地跑没问题但是部署到服务器上服务器环境和本地不一致很可能路径就失效了。正确做法是存相对路径比如user/1/2025/03/abc.txt在启动配置里设置一个根目录结合起来才是完整物理路径。我做的系统就是上传后先根据用户ID生成一个目录用户ID然后把文件写到根目录下的用户ID目录里数据库中只存用户ID/文件名的相对路径。第二个关键点是文件夹本身也要在文件表里占一条记录。folder_flag1表示这条记录是一个文件夹记录没有实际物理文件file_path可以为空但有parent_id用于确定归属。这样文件夹树和管理操作全都是同一张表不需要额外的目录表。2.3 分享表设计分享功能只需要一张表。字段包括id分享IDshare_token随机生成的短链接标志用UUID去掉横线的形式user_id分享者IDfile_id被分享的文件IDextract_code提取码4到6位随机数字expire_time过期时间status状态1有效0失效每次生成分享时后端生成share_token和extract_code把文件ID和分享者ID写进去。其他用户访问分享链接时后端通过share_token拿到分享记录判断是否过期。然后前端页面上输入提取码比对成功后返回文件信息文件信息如果是文件夹则返回该文件夹下的文件列表如果是一个普通文件则可以直接触发下载。这里要注意分享文件夹的场景在提取码校验通过后访问文件夹下的子文件时还需要校验子文件的归属是否确实在这个分享文件夹下不然容易被越权访问。2.4 回收站与逻辑删除设计回收站我不建议单独建表原因很简单文件表已经足够承载这个逻辑了。在文件表里加一个del_flag字段删除文件时只是把del_flag改成1同时更新时间置为当前时间。回收站页面查询时条件加上del_flag1就能把删除过的文件捞出来。点击恢复时把del_flag改回0同时可能需要检查父ID是否还存在防止父目录已经被彻底删除导致无法挂载。彻底删除时才真正删除数据库记录并调用文件工具类删除物理文件。这个逻辑对比单独建回收站表要清爽很多也便于后面加回收站保留天数这样的扩展需求。注意逻辑删除容易出现的一个坑是查询文件列表时忘了加del_flag0条件导致回收站里的文件出现在正常列表里。如果使用MyBatis Plus可以给文件表实体加TableLogic注解做全局逻辑删除这样内置的selectList方法会自动追加del_flag0条件省心很多。3. 核心功能模块的实现细节3.1 用户认证与JWT权限控制登录认证这块我选择了JWT。原因是项目一旦前后端分离服务端如果还用Session就会面临跨域共享Session、跨服务部署Session不一致的问题。JWT的思路是登录成功后把用户ID和过期时间声明进一个加密的Token前端保存Token之后每个请求在Header里带上。后端写一个拦截器统一解析效率高也不需要额外的Redis存储。JWT工具类我封装了两个方法一个是生成Token一个是解析Token。生成Token时把用户ID作为claim放进去同时设置过期时间为24小时。拦截器在每个请求里读取Header的Authorization字段去掉Bearer前缀后解析。如果解析成功就把用户ID放入ThreadLocal如果解析失败则返回401错误。登录注册等少数公开接口需要放行。拦截器注册的时候要特别注意放行顺序。比如用Spring Boot的注册方式拦截器对所有路径生效需要在addPathPatterns时设置放行路径列表。我在实际开发中就把文件下载接口忘记放行结果前端跳转浏览器的下载链接全部被拦截排查了很久才发现问题是拦截器把所有下载请求拦截了不仅没有返回文件流还返回了JSON错误浏览器直接下载了一个json错误文件。这个经验值得记住。3.2 文件上传与分片秒传实现文件上传是最容易出问题的模块。常规上传后端接口就是接收MultipartFile参数获取文件名和大小然后把文件写到磁盘上再保存一条记录到数据库。但真实业务里大文件上传如果不做处理用户传几个GB的文件基本必失败不是连接超时就是内存溢出。我的方案是分片上传。前端把文件切片每片大小5MB然后循环调用上传接口上传接口接收文件分片以及一个分片编号还有一个上传ID用来标识同一个文件的分片集合。每个分片作为临时文件存到临时目录全部上传完成后前端调用合并接口后端把所有分片按编号拼接成一个完整文件。这样即使传到一半网络断了也能重新上传未完成的分片而不需要重头再传。这里核心代码如下Controller接收分片时用到MultipartFile和RequestParamPostMapping(/upload/chunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(uploadId) String uploadId, RequestParam(chunkIndex) int chunkIndex) { String tempDir storageConfig.getRootPath() /tmp/ uploadId; File dir new File(tempDir); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(dir, String.valueOf(chunkIndex))); return Result.ok(); }秒传功能就是另一个思路了。上传前前端用JavaScript读取文件的MD5值调用后端接口查询该文件是否已经存在于共用文件库中。如果存在直接给当前用户生成一条文件记录不再写入实际文件。我做的个人云盘系统里没有做全局去重库但可以先在用户自己的文件目录里查询如果同MD5文件已存在则直接建立引用关系避免重复存储。实际开发时推荐加一个独立的文件指纹表字段包括MD5、文件大小、物理路径、被引用次数这样才能支撑真正的秒传。分片上传的合并操作要小心不要把所有分片内容一次性读入内存应该用InputStream循环读取每个分片文件然后按顺序写入OutputStream。合并完成之后再删除临时分片文件。这个过程中还要注意处理并发上传同一个上传ID的情况避免合并时文件被占用。3.3 文件下载与在线预览文件下载接口的核心是返回文件流。我用了Spring的ResponseEntity 读取文件为Resource设置Content-Type和Content-Disposition。这里的坑主要在中英文文件名。中文文件名如果直接放到Content-Disposition里不同浏览器展示效果不一样最稳妥的做法是用URLEncoder编码然后前端或者浏览器会自动解码。另外自己拼接HTTP响应头时要注意header不能包含换行符否则会在安全扫描中被判为注入漏洞。在线预览的话图片和文本最简单。图片直接通过接口返回原始二进制前端用一个组件接收blob流来展示。文本文件读取时要注意编码UTF-8文件正常GBK文件会出现乱码。我的处理方式是先通过工具读取文件的前几百个字节尝试判断编码然后按对应编码读取内容再转成UTF-8返回给前端。这样预览体验会好很多。PDF和Office文件预览要复杂一些需要第三方组件转码或者使用在线预览服务依赖环境我在毕设项目中就没有做复杂的在线编辑只展示了图片、文本、视频音频这几种常见的。3.4 文件管理接口设计文件管理包括根目录列表、进入子目录、新建文件夹、重命名、移动文件、删除文件。接口设计其实都围绕文件表。根目录列表就是查询user_id当前用户ID AND parent_id0 AND del_flag0的所有文件。进入子目录就是查询parent_id某个ID的文件列表。新建文件夹就插入一条folder_flag1的记录。重命名很简单直接更新file_name字段。移动文件就是修改parent_id。删除文件则是逻辑删除。文件夹的删除是特殊的因为它是递归结构。后端删除文件夹时不能只删当前记录需要先把文件夹下所有子文件找出来。我的处理办法是用一个递归方法先查询当前文件夹的所有直接子节点对每个子节点如果是文件夹则递归调用如果是文件则删除当前记录最后再删除当前文件夹记录。在逻辑删除模式下也可以只删除当前文件夹记录然后让子文件的信息在文件夹树中悬挂但这种状态恢复时很容易出错还是老老实实递归处理。前端展示文件树时我建议不要一次性把所有层级的文件夹全部查出来。可以按需加载点击文件夹时请求一次子文件接口。这样接口简单数据量也小。如果确实需要展示整棵树那就后端递归组装树形JSON前端用Vue的递归组件渲染。3.5 分享链接生成与回收站逻辑分享链接我在实现时用了两个随机值。一个是share_token作为链接的唯一标识直接放在URL里另一个是extract_code作为输入提取码用户访问分享页面时必须输入。分享者可以选择分享有效天数比如1天、7天、永久。对应后端记录过期时间。校验时先比对share_token是否存在然后比对提取码最后判断是否过期。分享链接的生成逻辑很简单核心就是创建一条分享记录public Share createShare(Long userId, Long fileId, Integer expireDays) { Share share new Share(); share.setUserId(userId); share.setFileId(fileId); share.setShareToken(UUID.randomUUID().toString().replace(-, )); share.setExtractCode(RandomUtil.randomNumbers(4)); if (expireDays ! null expireDays 0) { share.setExpireTime(LocalDateTime.now().plusDays(expireDays)); } share.setStatus(1); shareMapper.insert(share); return share; }回收站逻辑我在文件表里用del_flag实现文件表里有一个delete_time字段方便定时清理超过保留期的记录。回收站页面复用文件列表接口只不过查询条件换成了del_flag1并且按删除时间倒序。恢复操作和删除正好相反将del_flag重置为0。彻底删除时后台会同时调用物理文件删除方法。如果文件在多个地方被引用比如秒传引用物理文件删除还需要处理引用计数不能直接删否则可能导致其他用户文件缺失。我的系统没有做跨用户引用所以没有这个顾虑但做扩展时要注意。4. 实操过程中的关键配置与调试4.1 核心依赖与配置文件这个项目的pom.xml里依赖不需要很多主要包括spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter、mysql-connector-java、jjwt、hutool-all、lombok。hutool这个工具包帮了很大忙生成随机数、计算文件MD5、日期工具都用它省下大量重复代码。application.yml这里没有特别难的东西但有几个配置容易出错。第一个是数据源url数据库名要和本地建的一致时区必须加serverTimezoneAsia/Shanghai否则JDBC连接会报时区异常。第二个是MyBatis Plus的mapper-locations配置如果XML文件没放到默认classpath下需要手动指定。第三个是文件上传大小我设置了max-file-size: 512MBmax-request-size: 1GB。第四个是自定义存储路径Spring Boot里使用ConfigurationProperties读取比如cloud.storage.root-path: ./data。这里的相对路径要小心启动时的工作目录不同会指向不同位置建议部署时使用绝对路径。我贴上最核心的配置片段可以直接改用自己的spring: servlet: multipart: max-file-size: 512MB max-request-size: 1GB datasource: url: jdbc:mysql://localhost:3306/cloud_disk?serverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl cloud: storage: root-path: ./data4.2 CORS跨域问题前后端分离后跨域问题基本是必踩的。如果前端项目跑在http://localhost:8080后端接口在http://localhost:8081这时候前端直接调后端接口浏览器就会因为跨域拦截。解决方式是在后端写一个配置类实现WebMvcConfigurer在addCorsMappings方法里配置允许的源、方法、请求头。我当时的经验是允许源可以直接用allowedOriginPatterns(*)开发环境这么配最省事。allowedHeaders需要把Authorization放进去否则带了Token的请求会被浏览器预检拦下来。配置类写起来也不复杂直接贴出来Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }另外如果自己实现了拦截器跨域配置和拦截器之间还有顺序问题。前端的预检请求是OPTIONS方法拦截器如果拦了OPTIONS请求就会导致预检失败。解决办法是在拦截器里面判断请求方法如果为OPTIONS则直接放行。4.3 上传大小限制的完整配置Spring Boot对文件上传的限制来自两个地方。一是Spring MVC的配置spring.servlet.multipart.max-file-size和max-request-size默认只有1MB和10MB不修改的话传大文件马上报FileSizeLimitExceededException。二是Tomcat服务器本身的连接器限制如果超过这个限制可能返回Connection reset前端看起来像是连接被服务器断开。我在做分片上传时因为每一片只有几MB主要是把分片大小调好整体上传接口的max-file-size还不能设太小否则即使分片没超过限制后端临时文件处理也可能不被允许。综合下来我直接把单文件上传限制改成512MB请求总大小改成1GB基本能满足大多数使用场景。这里补充一个容易忽略的知识点Spring Boot 2.x以上版本中如果使用了自定义的MultipartConfigElement配置类它的优先级会覆盖配置文件中的spring.servlet.multipart配置。我建议不要同时用两种配置只保留yml配置就好否则出了问题排查时会很困惑。4.4 日志配置与接口调试调试阶段强烈建议把日志级别调到DEBUG至少在开发环境要调。MyBatis Plus会在控制台打印SQL日志你可以看到执行的每条SQL对于排查数据有没有查对很有帮助。另外可以开启Spring Boot的web日志查看请求和响应的基本状态。我习惯把项目里所有Controller接口的出入参在AOP里打一条日志打印请求地址、请求参数、耗时这个做法在联调时省了无数口舌。前端调试我推荐用浏览器开发者工具配合Postman。遇到上传类接口异常先看请求体里file字段名称和后端MultipartFile参数名是否一致不一致的话后端拿到的file就是null。遇到下载接口异常直接用Postman请求下载地址看返回的Content-Type和Content-Disposition是不是正确。如果返回的是JSON错误则说明请求被拦截器或者异常处理接管了。5. 常见问题与避坑指南5.1 中文文件名乱码问题这个几乎是所有网盘类项目都会碰上的问题。表现是前端上传中文文件后端保存到磁盘后文件名变成乱码下载时设置的文件名也是乱码。原因有两层一是HTTP传输时编码不是UTF-8二是Content-Disposition头里不能直接放中文。解决办法是前端在FormData里正常传文件名后端获取文件名后到输出响应时用URLEncoder.encode(fileName, UTF-8)重新编码。如果保存到磁盘的文件名乱码检查一下服务器操作系统字符集和Tomcat的URIEncoding配置通常设成UTF-8就能解决。我写下载接口的时候对响应头做了一定处理关键在于文件名要编码而不是直接拼接String encodedFileName URLEncoder.encode(fileName, UTF-8); response.setHeader(Content-Disposition, attachment; filename\ encodedFileName \);5.2 跨域配置正确但请求依然失败遇到过一次很迷惑的问题跨域配置看起来没问题但前端带Authorization的请求依然失败。后来发现是拦截器把OPTIONS预检请求拦了。浏览器在发起复杂请求时会先发一个OPTIONS请求如果后端返回的不是成功响应真实请求就不会发出。解决办法很简单拦截器里对OPTIONS请求直接返回成功。这个坑我之前提过一次但值得再强调因为排查时很容易忽略拦截器的存在。5.3 文件路径与部署环境不兼容本地写的是绝对路径比如某个盘符下的目录部署到Linux服务器上直接找不到路径。这是因为操作系统的路径分隔符和磁盘挂载方式完全不同。规范做法是数据库存相对路径根目录通过配置文件指定。我在制作这个系统时就把存储根目录放到配置文件里所有物理文件写入时获取根目录再拼相对路径这样换机器只需要改一个配置项。另外在Java里拼接文件路径时不要手写斜杠统一用Paths.get或者File.separator否则在不同操作系统上会出现斜杠方向不一致的问题。5.4 回收站恢复后文件夹结构错乱恢复一个处于回收站的文件夹时如果原本的父目录已经被彻底删除了会出现一条记录指向一个不存在的父目录。前端打开这个文件夹就会查不到数据。解决办法是恢复时先检查父目录是否存在如果不存在则把parent_id置为0也就是回到用户根目录。顺带还要检查重命名冲突如果目标位置已经有同名文件恢复时自动在文件名后面加一个后缀。5.5 文件流下载时无法终止连接下载大文件时如果用户点击了取消后端的输出流可能还在写入最终导致链路占用很久。如果并发下载的人多连接数会飙升。我给下载接口的ResponseEntity设置缓冲区并且前端取消时浏览器会关闭连接后端需要在写入时处理IOException遇到异常就及时退出循环不要继续写。另外可以考虑用Servlet的异步输出做高并发优化但这个系统作为个人云盘并发量很小不用过度设计。这套个人云盘管理系统做下来我个人体感最难的其实不是写代码而是把文件操作的逻辑想清楚尤其是目录层级、回收站、分享这些和“文件关系”相关的功能。一旦数据表设计合理后端的每个接口都很直白如果一开始就草草建表后面补字段、改逻辑会非常痛苦。最后分享一个实用小技巧在开发时可以提前在本地准备一些不同类型的测试文件比如大文件、图片、中文文件名、无扩展名文件上传下载和预览每个环节都测一遍很多问题是在这些边界文件上会暴露出来。希望这篇文章能帮到准备做类似项目的朋友。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表