基于Spring Boot的个人网盘系统:分片上传与秒传实战 之前一直有人问我网上的个人网盘管理系统源码那么多到底该怎么选、怎么改、怎么跑起来。正好我这段时间把一个基于 Spring Boot 的个人网盘管理系统从数据库设计到文件上传下载完整重构了一遍把源码、数据库脚本和项目文档都整理好了这里直接把核心思路和实操过程写出来。这个系统解决的是最典型的需求把自己散落在各处的文件集中管理支持目录分类、分片上传、秒传、分享链接、回收站后台还能看存储占用。整套东西用的是 Spring Boot MyBatis-Plus MySQL Redis 的组合前后端分离接口文档也一并配好了。适合刚学完 Spring Boot 想做点拿得出手的项目的人也适合公司内部做轻量级文件共享的场景直接拿源码改一改就能用。1. 项目整体设计与模块拆分1.1 功能需求盘点与边界划分个人网盘管理和百度网盘那种动辄几十人的团队协作产品不一样它的核心是把文件管清楚、把文件传得动。所以我在梳理需求时没有盲目去堆功能而是先列了一个最小可用功能清单用户注册登录、目录树管理、文件上传下载、文件重命名和移动、文件夹打包下载、分享链接生成与取消、回收站恢复与清空、最近上传列表、存储空间统计。看起来功能不少但每条都能对应用户的真实动作没有一个是摆设。边界划分上我把系统分成了两大块用户空间和文件操作。用户空间解决谁在用文件操作解决文件怎么流转。两者之间通过一个简单的业务逻辑层串起来避免了写代码时把校验逻辑和文件IO混在一起。这个划分在后续维护里帮了大忙比如后来要加分享码功能我只动了分享模块和多了一张表核心文件上传逻辑一行没改。另外要注意的是个人网盘和对象存储产品有个本质区别它通常不追求极致的高并发更多时候是单个用户在操作自己的文件。所以设计上我刻意做了取舍没有引入复杂的消息队列也没有做分布式锁保持一个 Spring Boot 单体应用就能扛住日常使用这对新手来说也友好得多。1.2 技术选型为什么是 Spring Boot 全家桶技术选型这块我用的组合是 Spring Boot 2.7.x MyBatis-Plus 3.5.x MySQL 8.0 Redis MinIO。有人会问文件存储为什么不直接用本地磁盘非要引入 MinIO我的想法是本地磁盘在单机部署时确实够用但一旦你想迁移到云服务器、或者后面打算做多副本容灾MinIO 这类 S3 兼容存储能让你少改很多代码。而且 MinIO 的 SDK 封装得挺好上传下载也就多几行配置收益远大于成本。Spring Boot 版本这里要特别提醒一句不要一上来就选 3.x 最新版。3.x 基于 Jakarta EE很多老教程和老依赖都默认是 javax 命名空间你照着教程写很可能因为一个 import 报错卡半天。我用的 2.7.x 属于 2.x 系列的稳定尾版资料多、坑少、兼容性最好等项目跑熟了再考虑升级也不迟。MyBatis-Plus 的优势不用多讲代码生成器把实体、Mapper、Service 一次性生成单表 CRUD 几乎不用手写 SQL。唯一要注意的是网盘这种业务里有很多查父节点下的子文件列表这种递归查询MyBatis-Plus 的 QueryWrapper 能做单层查询但涉及多级目录统计时我还是选择手写 SQL性能和对代码的可控性都好很多。Redis 在这里承担了三个职责存登录 Token、做上传时的临时状态记录、缓存分享链接的访问信息。至于数据库MySQL 8.0 的 JSON 字段我用在了文件扩展信息上存文件的分辨率、时长、缩略图地址这类结构不固定的数据比硬拆成几十个字段要灵活。1.3 目录结构与源码组织方式源码组织方式上我采用的是常见的分层架构Controller 管接口、Service 管业务、Mapper 管数据库、Entity 映射表结构。除此之外我单独加了一个storage包专门对接文件存储这样以后换存储方案只需要动这个包和配置类。com.example.disk ├── controller // 接口层只做参数校验和结果封装 ├── service // 业务逻辑层事务边界在这里 │ └── impl ├── mapper // MyBatis-Plus Mapper 接口 ├── entity // 数据库实体 ├── dto // 前端交互的传输对象 ├── vo // 视图对象比如文件列表的树形结构 ├── storage // 文件存储抽象层本地磁盘和 MinIO 都在这实现 ├── config // 配置类跨域、拦截器、Redis 序列化 ├── common // 统一返回结果、异常处理、常量定义 └── utils // 工具类比如文件类型识别、大小格式化这样的组织方式有几个明显的好处。首先是 Controller 层很薄核心逻辑都在 Service 里接口层面的改动不影响业务其次是storage抽象层把文件 IO 隔离出去了后面我测试过切换本地存储和 MinIO 只改了一个配置项最后是common包里的统一返回结构和异常处理前端对接时只需约定一种格式省掉大量联调扯皮。1.4 前后端接口约定与统一返回格式前后端分离的项目接口约定混乱是最大的隐形炸弹。我踩过一次坑不同接口返回成功时有的带 data有的不带前端代码里全是if(res.data.data)这种三层判断。这次重构时我强制性统一了返回结构。{ code: 200, message: success, data: {} }code统一用 HTTP 语义200 是成功400 是参数错误401 是未登录403 是没权限500 是服务端异常。data类型不限可以是对象也可以是数组。为了方便前端处理我还把分页信息也塞进了data统一是records、total、pageSize、pageNum四个字段。这套约定一旦定下来前端只需要封装一个request.js所有接口的调用姿势就完全一致了。文件上传接口我单独做了说明因为它不走 JSON 格式而是multipart/form-data。上传时的参数有两个一个是分片序号一个是分片总大小。前端在分片上传时每次请求都会带上这些元数据后端据此决定是写临时目录还是触发合并逻辑。这种元数据和二进制分离的思路在网盘项目里非常关键后面我会专门讲分片上传的细节。2. 数据库设计与表结构详解2.1 用户表与文件表的核心字段设计数据库是网盘系统的地基表结构设计得好不好直接决定后面写代码的时候是行云流水还是到处打补丁。我最终落地的表一共有 8 张用户表、文件信息表、目录信息表、用户文件关系表、分享表、回收站表、上传分片记录表、操作日志表。这里重点讲用户表和文件信息表。用户表设计得比较简单核心字段是用户名、密码、昵称、头像、存储空间上限、已用空间。密码我用的是 BCrypt 加密注册时调用BCryptPasswordEncoder.encode()登录时用matches()校验。这里有个容易被忽略的问题不要在数据库里明文存密码就算你自己部署用也不行因为日志一旦打印了 SQL密码就全暴露了。文件信息表是整个系统的核心字段如下字段名类型说明idbigint主键file_namevarchar(255)原始文件名file_pathvarchar(500)存储路径相对路径file_sizebigint文件大小字节file_typevarchar(20)文件类型image/video/doc等ext_namevarchar(10)扩展名storage_keyvarchar(255)存储服务里的唯一标识uploader_idbigint上传者IDmd5varchar(32)文件MD5用于秒传statustinyint1正常 0回收站create_timedatetime上传时间storage_key这列是我后来重构时才加上的。之前直接用文件名做存储标识结果遇到两个同名文件直接覆盖了。改成唯一标识后文件名可以随便改存储层不用动。md5字段就是为秒传设计的前端选择文件时先计算 MD5请求后端检查是否存在存在就直接复用原文件记录不存在才走上传流程。2.2 目录信息表与用户文件关系表目录信息表我采用的是经典的父子结构字段是 id、parent_id、dir_name、user_id、is_deleted。查询某个用户完整目录树时一条 SQL 查出来然后在内存里组树比反复递归查数据库高效得多因为个人网盘的目录数量通常不会超过几千条内存组树足够了。用户文件关系表的设计值得多说两句。一开始我天真地把文件直接挂在用户表下面后来发现同一个文件被分享给别人后接收方应该有自己的文件记录如果共用一条记录就会出现一个人删了别人也删了的尴尬。解决方案是用一张关系表来解耦file_id 指向文件信息表user_id 指向用户再带上用户自定义的文件名和目录位置。这样同一个物理文件可以被多个用户引用删除时只是删关系不影响物理文件直到引用计数归零才真正删除存储上的数据。2.3 分享表与回收站表的业务设计分享表的字段包括 id、share_token、file_id、share_user_id、share_code、expire_time、visitor_count、status。share_token是分享链接的唯一标识我用 UUID 生成形如http://112.74.56.10:8080/s/{token}。share_code是提取码可以留空表示免提取码。visitor_count记录访问次数后端在每次分享校验通过后给它加一。回收站表设计上我没有做成独立的物理表而是复用文件信息表和目录信息表的status字段加一个delete_time。删除文件时把 status 置为 0同时记录删除时间超过 30 天由定时任务真正清理。这样设计的好处是恢复操作非常快直接改 status 就行不用搬运数据。坏处是文件信息表会积累一些历史脏数据所以我在定时清理时需要连同用户文件关系表一起处理。2.4 分片记录表与 Redis 缓存的关系上传分片记录表一开始没有做是我在实现分片上传时现加的。字段是 upload_id、user_id、file_md5、chunk_index、chunk_size、upload_status。但实际上我并没有把分片进度存进 MySQL而是放在了 Redis 里用upload_id作为 key。MySQL 里这张表唯一的作用是断电恢复时做持久化兜底正常流程根本不会查它。有人会问为什么不直接全放 Redis因为 Redis 有持久化策略如果设置的是 RDB 快照模式极端情况下会丢最近几秒的数据。分片合并一旦少了某一片整个文件就废了。所以我的策略是Redis 存热数据负责高频的状态查询和更新MySQL 只做异步落盘每完成一个分片就更新一次 upload_status。这套双写机制看起来冗余实际运行下来非常稳。3. 文件上传下载与秒传的核心实现3.1 分片上传与断点续传的完整流程分片上传是网盘系统里最值得认真写的功能没有之一。它的核心思想是前端把一个几百 MB 的文件切成若干片一片一片传给后端后端把所有片收齐后合并成完整文件。这样做好处有三个一是避免了单次请求过大导致网关超时或内存溢出二是网络中断时只需要重传失败的片不用整文件重来三是可以显示上传进度提升用户体验。具体流程我分成了三步。第一步前端计算文件 MD5调用后端的预上传接口传文件 MD5 和文件名。后端检查 MD5 是否已存在如果存在直接返回一个标记前端收到这个标记就知道是秒传直接跳过上传如果是新文件后端生成一个upload_id返回给前端并初始化 Redis 状态。第二步前端把文件按固定大小分片比如每片 5MB逐片调用上传接口请求体里带上upload_id、chunk_index、total_chunks。后端每收到一片先写临时文件再更新 Redis 里的分片位图。第三步所有分片传完后前端调用合并接口后端检查分片完整性合并文件计算最终 MD5写入文件信息表。分片合并的代码核心是文件流合并工具类。这里要注意两个细节一是合并时必须以正确的顺序写入我通常在分片文件名的末尾加上序号比如xxx.part0、xxx.part1然后按序号排序逐个读入二是合并结束后一定要记得删临时分片文件否则临时目录会被撑爆这个问题我在测试环境真实遇到过。3.2 秒传功能的实现原理与边界情况秒传的原理一句话就能说清楚相同内容的文件只存一份靠文件 MD5 判断内容是否相同。用户选择文件后前端调用\/file\/presign接口传的就是 MD5。后端拿到 MD5 去文件信息表找找到了就直接建立用户文件关系把materialized标志设为 true整个过程完全不传文件内容。不过秒传有几个边界情况必须处理。最典型的是MD5 一致但文件大小不同。理论上 MD5 碰撞的概率极低但大小不相同一定是不同文件所以判断条件必须是 MD5 和 size 同时匹配少一个都不行。另外一点是如果文件已经在回收站里秒传命中的应该是正常区而不是回收区这需要 SQL 里加上 status 1 的条件。我踩过这个坑第一次做时没加条件导致用户删除文件后重新上传秒传直接给了个回收站里的引用结果文件就神秘消失了。3.3 上传接口的代码实现与参数校验上传接口的 Controller 代码逻辑并不复杂但参数校验和异常处理不能省。在 Spring Boot 中上传的最大文件大小默认是 1MB需要在 application.yml 里显式配置spring: servlet: multipart: max-file-size: 200MB max-request-size: 5GB我把max-request-size设置得很大因为分片请求理论上是不受限的5GB 的判断主要是防止异常请求打爆内存。真正限制上传大小的逻辑在业务层根据用户的存储空间剩余量来动态判断不允许超额上传。Service 层处理分片上传时我用了一段简化的逻辑来展示核心思路public void uploadChunk(UploadChunkDTO dto, MultipartFile file) throws IOException { // 从 Redis 中获取上传状态 UploadStatus status uploadStatusMapper.getFromRedis(dto.getUploadId()); if (status null) { throw new BizException(上传会话已过期请重新预上传); } // 校验分片序号合法性不能跳过已有分片也不能重复提交 int received status.getReceivedChunks(); if (dto.getChunkIndex() ! received) { throw new BizException(分片序号错误请从第 received 片续传); } // 分片写入临时目录 File chunkFile new File(tmpDir dto.getUploadId() .part dto.getChunkIndex()); file.transferTo(chunkFile); // 更新 Redis 状态 status.setReceivedChunks(received 1); if (received 1 dto.getTotalChunks()) { status.setCompleted(true); } uploadStatusMapper.saveToRedis(status); }这里做过一次严格的分片序号校验后前端并发上传多个分片时会存在问题因为校验要求分片严格按顺序提交并发场景下会互相阻塞。所以我在实际实现里放宽了策略允许乱序提交但每次提交时更新位图位图存的是每片对应的文件大小合并时按位图顺序读文件。这样既支持并发又能保证完整性。3.4 下载、预览与文件夹打包的实现下载实现相对简单核心是ResponseEntitybyte[]处理注意设置Content-Disposition响应头用attachment;filename*UTF-8xxx的写法来兼容中文文件名。大文件下载时不能一次性把整个文件读进内存会 OOM。必须用流式下载我写了一个downloadFile方法返回ResponseEntityInputStreamResource底层流由线程池的异步任务持续输出前端拿到的是一个流式响应。文件夹打包下载我把临时压缩包放在一个约定好的目录打包完成后生成一个下载地址这个地址有效期为 5 分钟。打包工具我用的 Java 原生的ZipOutputStream不用第三方库逻辑稳定。要注意的是如果文件夹里嵌套很多层递归时需要记录目录层级否则解压出来的全是平铺文件。文件预览则根据fileType判断图片和视频直接走 Nginx 静态文件访问PDF 和文本文件用前端 PDF 阅读器和代码高亮组件Office 文件我提供的方案是转 PDF 预览这一步在文档里写清楚即可不一定非要在服务端实现。4. 安全鉴权与系统配置4.1 Token 鉴权方案为什么我选 JWT 加 Redis 双层方案个人网盘的鉴权我采用的是 JWT 生成 Token但 Token 状态存 Redis 的方案。具体流程是用户登录成功后后端生成一个 JWT把userId和expireAt写进去然后把这个 Token 存一份到 Rediskey 是token:{userId}value 是 JWT 串过期时间和 JWT 保持一致。用户每次请求拦截器解析 JWT 验证签名再查一下 Redis 确认这个 Token 还没被吊销。为什么这么设计纯 JWT 的问题在于无法主动让 Token 失效——用户退出登录服务端没有任何状态可改只要 Token 没过期就永远有效。纯 Redis Token 的问题则是每次请求都要查一次缓存接口响应会有额外耗时。JWT 加 Redis 的组合我用 Redis 的 key 来判断是否被拉黑逻辑上只有两个分支key 存在且值和 JWT 一致放行key 不存在或值不一致拒绝重新走登录。这样既保留了 JWT 无状态验证的轻量又具备了服务端控制会话的能力。4.2 拦截器里的权限校验细节拦截器配置有两个细节值得提醒。第一是白名单必须清晰/user/login、/user/register、/s/、/static/**这些不需要登录的路径要放行否则前端初始渲染都会直接 401。第二是 CORS 配置和拦截器容易冲突如果拦截器先拦截了预检请求OPTIONS 类型前端跨域就直接废了。我的处理是在拦截器里对所有 OPTIONS 请求直接放行真正的 CORS 配置交给 Spring 的 CorsFilter 处理。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new AuthException(未登录); } Long userId authService.verifyAndGetUserId(token); request.setAttribute(userId, userId); return true; }request.setAttribute(userId, userId)这个操作非常实用后续所有 Service 方法里直接UserContext.get()拿当前用户 ID不用每个接口都传 userId 参数代码清爽很多。我建了一个UserContext的 ThreadLocal 工具类来封装这个逻辑请求结束时在afterCompletion中清理避免线程池复用导致的用户串号。4.3 全局异常处理与统一错误返回全局异常处理是 Spring Boot 项目里最容易偷懒的地方。很多项目只在 Controller 里手动 try-catch返回结构乱七八糟。我用RestControllerAdvice做了统一处理把异常分成两类来处理业务异常和系统异常。业务异常比如文件名重复、存储空间不足这些情况前端需要展示友好提示我定义了BizException异常类抛出时带 code 和 message全局处理器捕获后返回 400 加业务码。系统异常比如 SQL 执行错误、空指针统一返回服务器开小差了这种提示然后在 log 里打印完整的堆栈信息。这里要强调不要把数据库原始错误信息返回给前端第一是信息安全问题暴露表结构第二是用户体验问题用户根本看不懂那些英文异常。我在日志里打完整异常接口返回的只是统一兜底文案这是大厂的基本规范。4.4 配置文件分离与环境隔离配置文件这块我一直强调环境隔离否则生产环境排查问题会非常痛苦。我用 Spring Boot 的多 Profile 机制拆成了三个文件application-dev.yml连本地数据库application-test.yml连测试服务器application-prod.yml连正式数据库。启动时通过参数指定java -jar disk-server.jar --spring.profiles.activeprod每个环境文件的差异点主要是数据库连接串、Redis 地址、文件存储路径、日志级别。特别注意生产环境的日志级别我设置的是com.example.disk: warn线上业务日志和普通日志分开排查问题时不会被海量的 debug 日志淹没。数据库密码务必用环境变量注入不要硬编码在配置文件中即使仓库是私有仓库也该养成这个习惯。5. 实操踩坑与问题排查实录5.1 前端上传大文件频繁断连的问题排查第一个要讲的问题是前端上传大文件时频繁断连。症状是文件 50MB 以上时浏览器请求经常卡死然后连接被重置。排查过程我记录了完整的链路。前端用的 axios默认超时时间是 60 秒但上传大文件时单个请求很难在 60 秒内完成于是被 axios 主动断掉。这个问题排查顺序是前端超时配置 - 后端接口响应时间 - Nginx 的 proxy timeout。最终发现 Nginx 默认的proxy_read_timeout是 60 秒加上前端也是一样双重超时叠加。解决方法是在 Nginx 的 server 块里针对上传接口做单独配置location /file/ { proxy_read_timeout 600s; proxy_send_timeout 600s; proxy_request_buffering off; }proxy_request_buffering off是为了让 Nginx 不在内存里缓冲整个请求体而是边收边传这对大文件上传非常重要。前端 axios 的超时时间也要调到 15 分钟以上或者直接针对上传请求单独设 timeout不要用全局默认值。5.2 合并分片时文件损坏的问题定位第二个问题很隐蔽分片全部上传成功合并后的文件却是损坏的。排查时我一开始怀疑是并发写入分片导致的文件错位但检查后发现我的写入逻辑是按序号写入独立的partN文件并不会互相覆盖。后来我把分片文件按顺序拼接后对比原始文件的字节数发现字节数对不上。最终定位到问题出在分片序号的传递上。前端在分片时chunkIndex是从 0 开始的而后端从 Redis 里读取的receivedChunks初始值是 1两边的起始状态不一致导致第一片被当成重复分片丢弃整个文件缺失了开头。修复方法很简单把 Redis 里receivedChunks的初始值改为 0同时在前端预上传接口的返回值里把后端当前已接收的分片序号告诉前端前端从正确的位置继续传。这样即使上传中断续传也能从断点位置继续而不是从 0 开始。5.3 存储路径策略避免文件名冲突和路径过长存储路径策略是网盘系统的一个隐蔽深坑。最开始我用的是日期/原始文件名这种人类可读的路径比如2025/01/12/项目方案.docx。结果出现了两个问题第一不同用户上传了同名文件直接互相覆盖了第二文件名里有特殊字符时操作系统层面会报错。后来我改成彻底的随机目录加随机文件名{storageKey}而这个 storageKey 是 UUID 加 MD5 前缀的组合。文件名和目录结构全部用随机字符串彻底避免冲突。文件元数据全部放数据库访问时再构建 URL 映射底层存储文件和用户看到的文件名没关系。这种方式牺牲了一点直接去服务器文件夹里找文件的便利但换来的是绝对安全的存储层。分享文件时后端生成一个短期有效的访问地址而不是直接暴露底层路径。个人网盘系统如果考虑多机部署这个策略也更灵活文件可以分布在多台存储节点上数据库里记录节点编号即可。5.4 常见问题速查与排查思路汇总现象可能原因排查优先级解决方法上传时报 413Nginx client_max_body_size 过小1调整到 200M 以上上传时报 500日志有 MaxUploadSizeExceededExceptionSpring 配置未生效2检查 multipart 配置是否在正确文件中下载中文文件名乱码Content-Disposition 编码问题3使用filename*UTF-8格式预览图片 404静态资源映射未配置或权限拦截4配置 WebMvcConfigurer 的 addResourceHandlers登录后接口全部 401前端请求未携带 Authorization 请求头5检查请求拦截器中 headers 设置Redis 连接断开后上传全部失败Redis 未做高可用6增加连接池和重试机制排查问题的时候我有一个习惯先看日志里的异常堆栈第一行再往前翻 3 到 5 条日志基本能定位是配置问题、代码问题还是环境问题。很多新手一遇到问题就怀疑自己代码写错了其实最常见的反而是配置问题尤其是 Nginx 和 Spring 的 multipart 配置冲突我见过不止一次。5.5 定时清理任务的实现细节回收站的文件 30 天后要真正从存储中删除我用了 Spring Boot 自带的Scheduled定时任务每天凌晨 3 点执行清理。清理流程是查询回收站表中状态为 0、且删除时间超过 30 天的记录先删用户文件关系再删文件信息记录最后调用存储层删除物理文件。顺序不能错先删关系再删元数据最后删物理文件这样即使中途失败最多残留一个无引用的物理文件不会出现记录还在但文件已经没了的严重问题。定时任务我用了一个简单的分页循环每次处理 2000 条记录防止一次删除太多导致数据库锁表。还要注意清理动作应该跳过仍处于分享状态的文件否则外部访问直接 404。这个判断逻辑是检查分享表里是否有关联该文件、且未过期的分享记录。5.6 分享链接的安全控制与访问限流分享链接是网盘系统一不小心就会出问题的点。我做的安全控制有三层第一层是分享码默认开启 4 位提取码用户可以关闭第二层是过期时间默认 7 天到期后分享自动失效第三层是访问限流同一个分享 token 在 10 分钟内访问超过 60 次就直接拒绝。限流的实现用的是 Redis 的计数器简单可靠Long count redisTemplate.opsForValue().increment(share: token); if (count ! null count 1) { redisTemplate.expire(share: token, 10, TimeUnit.MINUTES); } if (count ! null count 60) { throw new BizException(访问过于频繁请稍后重试); }这个限流不追求精确但它能挡住大多数恶意刷量保护分享文件的下载带宽。如果有人把分享链接发到群里流量瞬间涨起来限流才会生效。网盘系统没有做更复杂的 IP 维度限流是考虑到网盘大多在公司内网或家庭网络访问IP 固定且少没必要自找麻烦。6. 补充经验与项目扩展建议6.1 磁盘空间管理不能只依赖操作系统个人网盘运行久了磁盘空间会被吃光尤其是日志文件和临时分片目录。我做了一个磁盘监控的定时任务每小时检查一下分片临时目录的大小超过 2GB 就触发清理。这个数值可以根据磁盘大小调整但思路很实际流式大文件的临时区域一定要设置一个硬上限主动清理远比被动等系统崩溃好。还有一个容易被忽略的点日志轮转。Spring Boot 默认的 logback 配置如果没写 RollingPolicy日志文件会无限增长。我的配置是每天滚动一个日志文件保留 7 天超过 7 天的自动删。这个配置用 logback-spring.xml 实现非常简单但很多人会忘记导致服务器 D 盘被日志占满。6.2 数据库备份策略与恢复演练数据库备份我采用的是每天凌晨全量备份加每 30 分钟 binlog 增量的方式。全量备份用mysqldump备份文件按日期命名保留最近 14 天。增量备份依赖 MySQL 的 binlog恢复时先恢复最近一次全量备份再按时间点回放 binlog。我特别想说的是备份做了不测试等于没做。很多人配完定时备份任务就觉得万事大吉结果真正要恢复时发现备份文件是损坏的、或者 mysqldump 命令执行时报错。我养成的习惯是每个季度做一次恢复演练从备份文件里拉一个测试库启动应用连上去验证数据完整性和服务可用性。这种演练虽然耗时但真正出事的时候能救命。6.3 后续功能扩展的三个方向这个系统如果继续往下做我有三个明确的扩展方向。第一是支持多存储节点的接入把存储抽象层完全推向 S3 协议MinIO、阿里云 OSS、腾讯云 COS 都可以作为后端存储数据库里给文件表加上storage_node字段。第二是增加极速上传体验前端预上传时可以把文件的 MD5 计算放到 Web Worker 里避免 UI 卡顿。第三是如果想让分享功能更像知名网盘产品可以增加加密分享文件夹的能力核心逻辑是分享表加一个is_folder_share字段配合目录树表的递归标记来实现。6.4 项目文档的整理思路这个项目的文档我写了三部分部署文档、接口文档、开发文档。部署文档里写清楚环境要求、配置文件修改项、启动命令和常见问题。接口文档我用的是 Apifox可以自动同步接口定义也有在线文档分享的功能。开发文档主要是讲清楚代码结构和核心流程业务初期我自己看后来新同事接手也靠它。文档这件事我多说一句写文档的最直接受益人是未来的自己。项目搁置半年再回来看如果没有文档你要花大量时间去翻代码才想起来当年的设计意图。我的习惯是完成一个模块就顺手写一段文档不追求篇幅把决策原因和坑记下来就够了。我之前写过一版用原生 Servlet 实现的网盘代码能跑但扩展性极差加一个分享功能要改五六个文件。这次用 Spring Boot 重构后最大的体会是框架约束下的分层设计让你在动手写第一行代码前就想清楚数据怎么流、模块之间的边界在哪里。个人网盘这个项目看起来简单但真要做得顺手从数据库设计到存储选型再到分片上传的细节和小文件合并策略每个环节都值得好好打磨。如果你也想做一个类似项目建议先从最小功能跑通再逐步加功能而不是照着大而全的产品清单去堆代码那样维护成本会让你怀疑人生。