☰
Spring Boot + Vue 实现个人云盘系统:文件上传、版本管理与分享链接
2026/10/18 18:52:14 网站建设 项目流程

简介:一套围绕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 不做任何业务判断,拿到参数直接丢给 service;service 里处理文件存储、权限校验、版本快照;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 指向自身表的主键,最终构成树状结构,对应前端左侧的文件夹树,根目录约定为 0;is_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 LambdaQueryWrapper<User>() .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 一串token,substring(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 Result<FileInfo> 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 接收文件流和父文件夹 id,parentId 默认 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 Result<String> 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 是新上传的 MultipartFile,userId 用于归属校验;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 目录 pause
@echo 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.port=8081临时改端口,或者在 application.yml 里永久改。

4.4 application.yml 配置和前端样式资源说明

环境变量相关的配置集中在 application.yml,关键节点如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/cloud_disk?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 200MB max-request-size: 500MB mybatis-plus: configuration: map-underscore-to-camel-case: true

逻辑说明:数据库连接串的 characterEncoding=utf8 保证读写中文不出乱码,serverTimezone 指定时区,否则 MySQL 8 会报连接时区错误。max-file-size 是单文件大小上限 200MB,max-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 'root'@'localhost',或者 Communications link failure。

原因:前者是 application.yml 里的数据库密码和本地 MySQL 不一致,后者是数据库服务没启动,或者 url 里的端口写错。这类错误还有一个隐蔽触发点:MySQL 8 的驱动要求 url 必须带 serverTimezone,不带会直接连接失败。

解决:先确认 MySQL 服务在运行,再用命令行mysql -uroot -p验证账号密码能否登录;然后把 application.yml 里的 username、password 改成你本机的实际值;如果你的 MySQL 是 8.x,确认 url 末尾有 serverTimezone=Asia/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 的查询逻辑,把「文件属于我」或「我被授权」合并成同一个查询条件。改造完可以进一步去想评论、协同编辑这些功能怎么基于授权表扩展权限判断。

说实话,每次我拿到这类毕业设计项目,都会先问自己一个问题:如果别人往我这个项目里传了一个和现有文件同名的文件,系统会怎样。答不上来才做版本管理和秒传,项目也就会从「能跑」变成「考虑清楚了」。希望你在这个项目上省下的时间,都能花在真正的理解上,少走我当年走过的弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询