2026/10/18 18:52:22

Spring Boot + Vue 实现个人云盘系统:文件上传、版本管理与分享链接

Spring Boot + Vue 实现个人云盘系统:文件上传、版本管理与分享链接 简介一套围绕SpringBoot个人云盘管理系统的毕业设计资料适合计算机相关专业学生用于课程设计或毕业论文参考。系统完整实现了用户注册登录、多角色权限、文件上传下载与批量操作、树状文件夹管理、全文搜索、标签标记、分享链接与协同编辑、版本历史、数据加密备份等功能能够帮助读者快速理解SpringBoot后端接口设计、Vue前端交互以及MySQL数据建模的实际落地方式。资源包共782个文件总大小28.96MB主要包含Java后端源码、Vue组件页面、JavaScript逻辑、CSS样式、SVG图标、SQL初始化脚本以及HTML静态页面等覆盖了前后端分离项目的基础工程结构。包内附有一键安装、构建、运行脚本与mp4演示视频等辅助文件可降低环境搭建门槛便于对照源码逐模块学习通过阅读源码与运行演示可系统掌握个人云盘从数据表设计、接口开发到页面联调的全过程。目前已有90人学习下载适合作为个人云盘类毕业设计或课程作业的完整参考案例。1. 项目速览Spring Boot 写网盘不难难的是能一次跑通个人云盘管理系统这类选题在毕业设计和课程作业里出现的频率很高但大多数版本只做了上传下载点两下就到底了。这个项目除了常规的注册登录、文件夹管理、批量上传之外把版本管理和分享链接也做了进去文件每次更新都会自动留一个历史版本随时可以回滚分享链接带上有效期过期自动失效。后端是 Spring Boot MyBatis-Plus前端是 Vue Element UI开发时前后端分离部署时打成单个 jar提供三个 bat 脚本依次执行就能把环境拉起来。适合拿来当毕业论文的项目讲解素材也适合第一次接触 Spring Boot 全家桶的同学照着跑通一套完整流程。2. 整体架构与目录结构一个 jar 里塞下前后端的「伪分离」在双击 run.bat 之前先花二十分钟把骨架看清楚。这个项目给我最直接的感受是它采用了「开发时分离、部署时合并」的方案开发阶段前端单独起一个 Vue 服务后端只管 /api 接口部署时前端构建产物直接丢进后端的 static 目录最后两个进程合并成一个 jar。这种做法的好处是答辩现场只需要起一个进程不用解释 Node 环境也不会出现前端忘了启动导致白屏的尴尬。坏处是如果你第一次看 jar 包里的文件会觉得静态资源特别杂。2.1 技术选型Spring Boot MyBatis-Plus Vue 2 的组合方式后端用的是 Spring Boot 2.x 那套标准组合Web 层是 spring-boot-starter-web数据访问层用 MyBatis-Plus 替代了原生 MyBatis。选 MyBatis-Plus 的原因很实际这个项目的单表操作占了绝大多数用户表、文件表、版本表、分享表基本都是简单的增删改查用 LambdaQueryWrapper 写条件查询比写 XML 快得多而且自带分页插件文件列表的分页不用自己拼 SQL。前端是 Vue 2 Element UI页面组件主要用了 el-upload 做文件上传、el-table 做文件列表、el-tree 做左侧文件夹树。static 目录下的 element.min.css、bootstrap.min.css 就是这两个组件库编译后的产物。需要注意资源包里还出现了 homeworkPC.min.css、front-kaoshi-style.css 这两个文件名它们来自某个在线作业考试系统的前端模板是打包时被一并拷进 static 目录的历史遗留资源下文避坑章节会专门讲先记住别动它们。2.2 目录结构Controller、Service、Mapper 的分区方式cloud-disk/ ├── backend/ │ ├── src/main/java/com/example/clouddisk/ │ │ ├── controller/ # 接口层只做参数接收和结果封装 │ │ ├── service/ # 业务逻辑上传下载、版本管理都在这里 │ │ ├── mapper/ # MyBatis-Plus 的数据访问接口 │ │ ├── entity/ # 用户、文件、版本、分享链接的实体类 │ │ ├── config/ # 拦截器注册、跨域配置、MP 分页插件 │ │ └── common/ # 统一返回结果、异常处理、JWT 工具类 │ ├── src/main/resources/ │ │ ├── application.yml # 数据库连接、上传大小、端口配置 │ │ └── static/ # 前端打包产物Vue 的 dist 内容 │ └── pom.xml ├── frontend/ # Vue 源码目录可能被裁剪 ├── 1-install.bat # 安装后端依赖 ├── 2-build.bat # 打包后端 jar └── 3-run.bat # 启动项目这个分层是典型的 Spring Boot 单应用结构controller 不做任何业务判断拿到参数直接丢给 serviceservice 里处理文件存储、权限校验、版本快照mapper 只负责和数据库打交道。config 目录里比较重要的是拦截器注册登录校验就是在这里把自定义的 AuthInterceptor 加进 Spring MVC 的拦截器链。static 目录说明前端构建后的 index.html、css、js 已经全部打到了后端资源路径下所以直接访问 http://localhost:8080 看到的就是登录页而不是接口文档。2.3 数据表设计四张核心表怎么支撑整个网盘这个项目的核心表大致是四张用户表 t_user、文件信息表 t_file_info、版本表 t_file_version、分享表 t_share_link。文件标签如果单独存会再多一张 t_tag 或 t_file_tag。表名关键字段承担职责t_userid, username, password, role, create_time登录账号、管理员/普通用户角色区分t_file_infoid, user_id, parent_id, file_name, path, size, is_dir文件和文件夹的统一元数据t_file_versionid, file_id, version_no, path, modifier, modify_time每次覆盖前的旧文件快照t_share_linkid, file_id, share_code, expire_time, create_uid分享链接生成与过期校验t_file_info 里有几个字段值得留意。parent_id 指向自身表的主键最终构成树状结构对应前端左侧的文件夹树根目录约定为 0is_dir 用来区分这条记录是真实文件还是一个空文件夹因为空文件夹没有物理文件path 可以留空path 字段存的是文件在服务器磁盘上的绝对路径或相对路径下载时直接从这个路径读流。t_file_version 的 design 思路是「只存旧路径不复制新文件」文件被覆盖时先把当前 path 和版本号写进版本表再用新文件覆盖主表记录回滚时把旧路径写回主表即可。文件权限在普通用户维度上没有做得特别细主要通过 t_user.role 区分管理员和普通用户普通用户只能管理自己 user_id 下的文件管理员可以查看全局文件列表。如果你的二次开发需要做「指定人分享」可以在 t_share_link 上加一个 target_user_id 字段后面第六章会展开。3. 核心模块拆解从登录校验到文件回滚的完整实现这一章按调用链路往下走登录拿 token请求带着 token 过拦截器文件上传落盘下载做归属校验覆盖更新时自动保存版本。每一段代码都是可以直接抄进自己项目里的写法重点是理解每个参数在链路里承担的角色。3.1 登录与权限拦截token 怎么变成 userId登录接口的逻辑很直接根据用户名查用户用 BCrypt 校验密码通过后生成 token 返回给前端。前端把 token 存进 localStorage之后的每个请求都在 Authorization 头里带上它。Service public class UserServiceImpl implements UserService { Override public String login(String username, String password) { User user userMapper.selectOne( new LambdaQueryWrapperUser() .eq(User::getUsername, username)); if (user null || !BCrypt.checkpw(password, user.getPassword())) { throw new BizException(用户名或密码错误); } return JwtUtil.createToken(user.getId(), user.getRole()); } }逻辑说明先用 LambdaQueryWrapper 按用户名查用户查不到直接抛业务异常。BCrypt.checkpw 是常见做法密码表里存的不是明文而是 BCrypt 哈希值就算数据库泄露也没法直接看到密码。登录成功的返回值是 JwtUtil.createToken 生成的字符串里面压入了用户 id 和角色后续接口不需要再查数据库就能拿到当前用户身份。参数说明username 是登录账号password 是用户输入的原始密码和库里存的 BCrypt 密文比对createToken 的第一个参数是 Long 型 userId第二个是角色字符串token 默认有效期通常设置为 24 小时过期后前端会收到 401 状态码并跳回登录页。有了 token 之后每个受保护接口都要做身份解析这个动作统一放在拦截器里。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } Long userId JwtUtil.parseUserId(token.substring(7)); if (userId null) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } request.setAttribute(userId, userId); return true; } }逻辑说明preHandle 在 Controller 方法执行前跑先检查 Authorization 头是否存在并且前缀正确然后调用 JwtUtil 解析 token把 userId 放进 request 属性里。这样一来Controller 里只需要写一行(Long) request.getAttribute(userId)就能拿到当前用户不用在每个接口里重复解析 token。如果你要加「管理员接口」可以在拦截器里再解析 role 并判断权限或者配合 Spring Security 的 PreAuthorize 注解二选一即可。参数说明Authorization 头的标准格式是Bearer 一串tokensubstring(7) 截掉前缀部分401 状态码会触发前端 axios 的响应拦截器自动清理本地缓存并跳转登录页。这里没有把角色也放进 request是因为文件接口的归属校验在后面 Service 层做角色判断只在极少的管理接口用到。3.2 文件上传与下载MultipartFile 与流式输出上传下载是网盘的核心链路Controller 层只做参数接收和响应封装真正的落盘、路径管理、记录入库都在 Service 里。RestController RequestMapping(/api/file) public class FileController { // 构造器注入避免字段注入带来的循环依赖隐患 private final FileService fileService; public FileController(FileService fileService) { this.fileService fileService; } PostMapping(/upload) public ResultFileInfo upload(RequestParam(file) MultipartFile file, RequestParam(value parentId, defaultValue 0) Long parentId, HttpServletRequest request) throws IOException { Long userId (Long) request.getAttribute(userId); return Result.success(fileService.upload(file, userId, parentId)); } GetMapping(/download/{fileId}) public void download(PathVariable Long fileId, HttpServletResponse response, HttpServletRequest request) throws IOException { Long userId (Long) request.getAttribute(userId); FileInfo file fileService.findOwnFile(fileId, userId); response.setContentType(application/octet-stream); String fileName URLEncoder.encode(file.getFileName(), StandardCharsets.UTF_8.toString()); response.setHeader(Content-Disposition, attachment;filename fileName); fileService.writeFileToStream(file, response.getOutputStream()); } }逻辑说明upload 接口用 RequestParam 接收文件流和父文件夹 idparentId 默认 0 表示上传到根目录。download 接口先调用 findOwnFile 做归属校验确保用户只能下载自己 user_id 下的文件校验通过后设置响应头把文件字节流直接写到 response 输出流。这里的关键点是 URLEncoder.encode 对文件名做编码否则浏览器下载时会因为中文乱码显示出错。参数说明MultipartFile 是 Spring 封装的上传文件对象getOriginalFilename 取原始文件名getSize 取文件大小response.setContentType(application/octet-stream) 告诉浏览器这是二进制附件而不是可预览页面download 接口的返回类型是 void因为文件内容直接写进了 HttpServletResponse不需要再包装成 Result 对象。上传接口的返回 Result 里携带了新插入的文件记录前端拿它刷新当前目录列表。Service 层的上传实现值得细看它决定了文件在服务器磁盘上的物理分布。public FileInfo upload(MultipartFile file, Long userId, Long parentId) throws IOException { // 按用户分目录存储避免所有文件堆在同一个目录 String dir D:/cloud-disk-storage/ userId /; File dirFile new File(dir); if (!dirFile.exists()) { dirFile.mkdirs(); } String originalName file.getOriginalFilename(); // UUID 前缀防止不同用户上传同名文件互相覆盖 String storedName UUID.randomUUID().toString().replace(-, ) _ originalName; file.transferTo(new File(dir storedName)); FileInfo info new FileInfo(); info.setUserId(userId); info.setParentId(parentId); info.setFileName(originalName); info.setPath(dir storedName); info.setSize(file.getSize()); info.setVersion(1); info.setIsDir(false); fileMapper.insert(info); return info; } 逻辑说明先按用户 id 建目录每个用户的文件物理隔离这个设计直接影响后面权限控制的实现——就算是遍历服务器磁盘目录也天然分好了边界。文件名加 UUID 前缀是因为普通用户上传的 report.docx 可能和其他人的重名直接在磁盘上用原名保存会发生覆盖加随机前缀后物理文件名永远不会重复但浏览器下载时取的是 t_file_info.file_name用户感知不到这个前缀。 参数说明dir 是存储根路径这个项目用的是本地磁盘绝对路径换到 Linux 服务器上要改成 /data/cloud-disk/{userId}/storedName 是物理文件名originalName 是逻辑文件名两个字段分开存是网盘系统很关键的一步file.transferTo 是 MultipartFile 提供的落盘方法它内部处理了临时文件的转移比自己写 InputStream 再 FileOutputStream 更省事。 ### 3.3 批量上传与分享链接前端循环与后端校验 批量上传在后端不需要单独写接口前端用 Element UI 的 el-upload 组件设置 multiple 属性或者动手把 FileList 循环调同一个 /api/file/upload 接口。我一般建议前端控制并发数量比如同时最多发三个请求不然一次性几十个文件同时进来Tomcat 线程池会被打满浏览器也会因为连接数限制排队。这个项目里前端是多选文件后按顺序逐个上传进度条用 el-progress 展示代码逻辑上维护一个 pending 队列即可。 分享链接是网盘比较有亮点的功能。后端生成一个随机的短码保存到 t_share_link 表前端把完整地址拼出来给用户复制。 java PostMapping(/share) public ResultString createShare(RequestParam Long fileId, RequestParam(required false) Integer expireDays, HttpServletRequest request) { Long userId (Long) request.getAttribute(userId); String shareCode UUID.randomUUID().toString() .replace(-, ).substring(0, 10); ShareLink link new ShareLink(); link.setFileId(fileId); link.setShareCode(shareCode); link.setCreateUid(userId); // 不传有效期则默认 7 天 link.setExpireTime(expireDays null ? LocalDateTime.now().plusDays(7) : LocalDateTime.now().plusDays(expireDays)); shareLinkMapper.insert(link); return Result.success(/share/ shareCode); }逻辑说明分享链接的核心是生成一个不可预测的短码这里直接用 UUID 取前 10 位碰撞概率在单项目规模下可以忽略。expireDays 参数控制有效期为空时默认 7 天过期判断不依赖定时任务而是在访问时实时比对当前时间和 expire_time。短的分享码也方便 URL 传播比长 ID 更安全别人没法遍历接口猜出别人的分享链接。参数说明fileId 是要分享的文件主键前端从表格选中行里取expireDays 是可选参数Integer 类型前端可以做成下拉框选择 1 天、7 天、30 天返回值是一个相对路径前端再拼上自己的 baseURL 生成完整地址。访问端的校验逻辑更简单——按 shareCode 查表检查过期标记和 expire_time通过后返回 fileId再根据 fileId 查出文件输出流。3.4 文件版本管理覆盖前先存一份旧路径版本管理是这个项目区别于普通上传组件的核心点。实现套路不复杂每次覆盖更新前把当前文件记录的 path 和版本号复制到版本表然后把新文件路径写回主表同时主表版本号加一。Transactional(rollbackFor Exception.class) public void overwriteWithVersion(Long fileId, MultipartFile newFile, Long userId) throws IOException { // 1. 查出当前文件记录同时校验归属 FileInfo current fileMapper.selectById(fileId); if (current null || !current.getUserId().equals(userId)) { throw new BizException(文件不存在或无权操作); } // 2. 当前文件路径写入版本表作为可回滚快照 FileVersion version new FileVersion(); version.setFileId(current.getId()); version.setVersionNo(current.getVersion()); version.setPath(current.getPath()); version.setModifier(userId); version.setModifyTime(LocalDateTime.now()); versionMapper.insert(version); // 3. 新文件落盘主表路径更新 String newPath storeFile(newFile, userId); current.setPath(newPath); current.setSize(newFile.getSize()); current.setVersion(current.getVersion() 1); current.setUpdateTime(LocalDateTime.now()); fileMapper.updateById(current); }逻辑说明整个方法加了 Transactional版本表插入和主表更新必须在同一个事务里否则可能出现版本记录写了但主表没更新的脏状态。设计上的小巧思是版本表不存文件内容只存旧路径。文件内容本身就在磁盘上版本回滚就是把旧路径从版本表取出来写回主表几行代码完成。这样既不浪费磁盘复制也不会有两份内容不一致的问题。modified 记录了操作人回滚时可以知道这个版本是谁改的。参数说明fileId 是主表主键newFile 是新上传的 MultipartFileuserId 用于归属校验versionNo 取的是当前版本号而不是加一后的值含义是「这个版本对应第几次修改的快照」回滚接口就是把上面步骤反过来把版本表里的 path 更新为主表 path然后把版本号设置成该版本号。需要注意文件扩展名前端更新上传时一般会把新文件的原始名一起传进来主表的 file_name 是否同步更新取决于你是把它当成「内容修订」还是「另存为新版本」。4. 环境搭建与启动三个 bat 脚本从零跑到登录页这个项目的启动设计得比较省心根目录放三个批处理文件名字就是执行顺序1-install.bat 装依赖2-build.bat 打包3-run.bat 启动。但实际跑的过程中大多数人翻车都翻在环境准备上而不是项目本身。4.1 环境准备JDK 8、Maven、MySQL先确认本机有 JDK 1.8 或更高版本命令行执行 java -version 能输出版本号Maven 建议 3.6 以上执行 mvn -v 验证。数据库用 MySQL 5.7 或 8.0 都行项目启动前需要手工建一个库库名和 application.yml 里 spring.datasource.url 的路径一致比如CREATE DATABASE cloud_disk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;utf8mb4 几乎是网盘项目的必选项文件名里可能出现的 emoji 和特殊字符只有这个字符集能完整存下来。建库之后数据库脚本放在 resources 目录下的 sql 文件夹里用 Navicat 或命令行 source 执行一遍会生成前文说的那几张表。注意脚本执行时表创建顺序先建 t_user再建 t_file_info因为文件表的 user_id 有外键约束指向用户表顺序反了会报错。4.2 1-install.bat依赖安装脚本做了什么echo off chcp 65001 nul echo [1/3] 安装依赖首次执行可能需要数分钟... call mvn -f backend/pom.xml clean install -DskipTests echo 依赖安装完成 pause逻辑说明mvn clean install 会把后端模块编译、测试、打包后安装到本地 Maven 仓库。如果你之前在本机开发过其他 Spring Boot 项目这一步会快很多因为大部分依赖已经在本地仓库。chcp 65001 是把批处理控制台切到 UTF-8 代码页防止打印中文时乱码。首次执行时 Maven 会下载大量依赖慢的根源通常是中央仓库网络问题解决办法是修改 Maven 的 settings.xml把 mirror 指向国内镜像源。参数说明-DskipTests 跳过单元测试课程项目大多没有写测试类跳过可以避免 test 阶段找不到测试类而报错clean 会先删除 target 目录保证这不是一次增量构建。执行完这步backend 目录下会多个 target 文件夹里面是编译产物。4.3 2-build.bat 与 3-run.bat打包和启动echo off chcp 65001 nul echo [2/3] 打包项目... call mvn -f backend/pom.xml package -DskipTests echo 打包完成jar 位于 backend/target 目录 pauseecho off chcp 65001 nul echo [3/3] 启动项目请保持窗口开启... java -jar backend/target/cloud-disk-0.0.1-SNAPSHOT.jar pause逻辑说明package 会生成可执行 jar文件名一般是 artifactId 加版本号比如 cloud-disk-0.0.1-SNAPSHOT.jar。run.bat 用 java -jar 启动启动过程中只要看到 Started 开头的日志并且没有红色 ERROR就说明端口和数据库都正常了。此时浏览器打开 http://localhost:8080前端静态资源从 jar 内 static 目录直接提供服务。如果端口被占用可以在启动命令后追加--server.port8081临时改端口或者在 application.yml 里永久改。4.4 application.yml 配置和前端样式资源说明环境变量相关的配置集中在 application.yml关键节点如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/cloud_disk?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 200MB max-request-size: 500MB mybatis-plus: configuration: map-underscore-to-camel-case: true逻辑说明数据库连接串的 characterEncodingutf8 保证读写中文不出乱码serverTimezone 指定时区否则 MySQL 8 会报连接时区错误。max-file-size 是单文件大小上限 200MBmax-request-size 是单次请求总大小 500MB对应批量上传场景。map-underscore-to-camel-case 让数据库的 file_name 自动映射到实体的 fileName省去写 resultMap。资源包里那堆 CSS 文件的来历在首次看目录时最容易引起疑惑。element.min.css 和 bootstrap.min.css 是 Element UI 和 Bootstrap 组件库的正常产物但 homeworkPC.min.css、front-kaoshi-style.css 明显属于在线考试系统的前端模板多半是初始化项目时直接把某个模板的 static 目录整体拷了过来。它们不影响功能但占了 jar 体积如果强迫症犯了想清理务必先确认 index.html 里有没有引用没引用再删。5. 常见问题与避坑指南跑不起来先查这五个地方这个项目本身不复杂但环境和前后端交互的细节很容易让人翻车。下面五条都是我实际拆解和运行这类网盘项目时最常见的问题按现象到原因再到解决办法的顺序记录遇到类似报错直接对号入座。5.1 启动报错Access denied for user 或 Communications link failure现象run.bat 启动后控制台出现红色异常提示 Access denied for user rootlocalhost或者 Communications link failure。原因前者是 application.yml 里的数据库密码和本地 MySQL 不一致后者是数据库服务没启动或者 url 里的端口写错。这类错误还有一个隐蔽触发点MySQL 8 的驱动要求 url 必须带 serverTimezone不带会直接连接失败。解决先确认 MySQL 服务在运行再用命令行mysql -uroot -p验证账号密码能否登录然后把 application.yml 里的 username、password 改成你本机的实际值如果你的 MySQL 是 8.x确认 url 末尾有 serverTimezoneAsia/Shanghai。改完配置文件后重新执行 2-build.bat 再启动因为 jar 打进的是 resources 里的配置。5.2 上传文件报 413 或 500不是代码 bug是大小限制现象上传小文件没问题传一个 40MB 的压缩包直接报 413 Request Entity Too Large或者后端日志抛 MaxUploadSizeExceededException。原因Spring Boot 默认的上传限制是 1MB虽然 application.yml 里配了 200MB但如果你同时用了 Nginx 反向代理Nginx 默认 client_max_body_size 也是 1MB请求在到达 Tomcat 之前就被挡掉了。如果只改了 Spring 没改 Nginx上传超过 1MB 的文件永远是 413。解决后端配置前文那段 max-file-size 和 max-request-size如果有 Nginx在 server 块加client_max_body_size 200m;。改完 Nginx 要 reload 才生效。实际项目里我还习惯把 MultipartFile 的 transferTo 包一层 try-catch专门捕获 MaxUploadSizeExceededException 返回友好提示而不是抛一堆堆栈给前端。5.3 下载文件中文名乱码现象从网盘下载「项目报告.docx」浏览器提示保存的文件名变成一串_或者乱码用下载工具打开更是看不懂但文件内容本身正常。原因HTTP 头里的 Content-Disposition 属性用 ISO-8859-1 编码解析直接放中文文件名会被浏览器按错误编码解读。这是网盘系统非常经典的乱码来源和数据库字符集无关。解决按前文下载代码里的写法用 URLEncoder.encode 把文件名转成百分号编码再加到 Content-Disposition 里。关键点是要用StandardCharsets.UTF_8而不是系统默认字符集不然在中文 Windows 上会被转成 GBK 编码再 encode结果还是乱。改完记得重新打包。5.4 static 目录混入考试系统样式文件别顺手删现象打开项目发现 static 下有一堆 CSS比如 homeworkPC.min.css、front-kaoshi-style.css文件名和云盘业务完全无关有人为了「整洁」直接删掉结果页面样式错乱。原因这些文件是项目初始模板自带的静态资源来自一套在线作业考试系统的前端模板。index.html 或打包后的 HTML 里有link引用了其中某些资源删掉它们会导致页面失去部分样式表现为主页面能用但某些弹窗、表格样式塌了。解决动手裁剪前先在 static/index.html 或打包后的 index.html 里搜索 homeworkPC、kaoshi 这些关键字看哪些文件被实际引用。没有引用的可以删有引用的留下。严格来说这个问题不影响跑通但它在实操里确实迷惑了很多人血泪经验是「先查 index.html 再动 static 目录」。5.5 前端单独跑开发服务器时跨域报错现象没有直接访问 jar而是把前端目录单独npm run dev起在 8081 端口后端跑在 8080前端页面能打开但所有接口请求都报 CORS 错误登录都登不进去。原因开发模式下前端页面来自 localhost:8081接口请求发到 localhost:8080跨了端口就触发了浏览器的同源策略。部署模式下前端资源从 8080 端口直接提供不存在跨域问题所以很多人只测 jar 模式发现一切正常换到前端源码模式就翻车。解决在后端添加一个跨域配置类注册 CorsFilter 并允许所有来源或者更省事的做法是后端接口统一加CrossOrigin但项目接口多时还是全局过滤器治理比较干净。另一种思路是让前端开发服务器的 devServer 配置 proxy把 /api 开头的请求转发到 8080浏览器视角始终保持同源。两种方案选后者的项目更常见因为前者等于生产环境也把跨域放开安全上不够严谨。6. 二次开发改造从课程作业升级成能谈上线的东西把存储层跟本地磁盘解耦是最优先的改造项。现在文件全存在D:/cloud-disk-storage/下换一台机器数据就丢了谈不到上线。我一般会引入一个对象存储服务改造思路是Service 层不再直接操作 File 对象而是抽象出一个 StorageService 接口本地磁盘实现保留用于测试另一套实现接对象存储。上传时拿到 InputStream 直接 PUT 到存储桶路径字段存对象 key 而不是本地绝对路径。下载时也不再读本地文件而是生成一个带签名的临时 URL 重定向给浏览器。改动范围集中在 3.2 节的 upload 和 writeFileToStream 两个方法Controller 层完全不用动。第二个值得做的改造是文件秒传。原理是前端在上传前先计算文件 MD5通过一个接口传给后端查重。逻辑落地不复杂在 t_file_info 加一列 md5上传时先按md5 user_id查记录如果存在就直接返回已有的 fileId跳过磁盘写入响应时间从秒级降到几十毫秒。改造价值很直观答辩演示时拖一个大文件进去瞬间显示「上传成功」比任何 PPT 都可以更好地说明你的系统考虑过性能。第三个方向是权限模型扩展。当前用户只能访问自己的文件这个粒度在个人网盘里够用但如果你想把分享做成「指定协作人可见」还需要调整查询。我的习惯做法是新建 t_share_permission 表字段包括 file_id、target_user_id、permission enum然后修改 findOwnFile 的查询逻辑把「文件属于我」或「我被授权」合并成同一个查询条件。改造完可以进一步去想评论、协同编辑这些功能怎么基于授权表扩展权限判断。说实话每次我拿到这类毕业设计项目都会先问自己一个问题如果别人往我这个项目里传了一个和现有文件同名的文件系统会怎样。答不上来才做版本管理和秒传项目也就会从「能跑」变成「考虑清楚了」。希望你在这个项目上省下的时间都能花在真正的理解上少走我当年走过的弯路。希望帮到你。本文还有配套的精品资源点击获取