简介:一套基于Java Web实现的轻量级云盘系统,面向正在学习Java后端与Web开发的初学者、以及需要快速搭建在线存储演示项目的开发者。项目模仿百度网盘的核心交互,涵盖文件上传、下载、分享、删除、重命名等常用操作,并包含用户认证与权限控制,可直接作为课程设计或毕业设计的参考原型。压缩包内共两百零四个文件,整体大小约四点五五兆字节。主要文件类型包括五十个Java源码文件、六十五个class编译文件、十五个依赖库、二十五张界面图片、二十四个脚本、五个样式表、四个JSP页面,以及数据库脚本和构建配置;目录按照控制层、业务层、数据访问层、模型层等划分,结构清晰。目前已有三百四十六人学习下载。通过阅读源码可掌握Spring MVC请求处理、Servlet与JSP页面交互、数据库ORM操作、文件上传下载的实现思路;配合SQL脚本和配置文件,还能快速搭建运行环境,理解一个完整Java Web项目从前后端到部署的闭环。
1. 一个仿百度网盘的javaweb小型云盘系统,zip里装了什么
“基于javaweb的仿照百度网盘做的小型云盘系统”这个标题,在课程设计和技能训练里出现频率很高。它指的不是百度网盘那种海量存储的产品,而是一个用Java Web技术栈写出来的、支持用户注册登录、文件上传下载、个人目录隔离的轻量工程。这个zip包里装的是完整的javaweb项目,不是几个演示页面。如果你正在找javaweb项目完整案例,它的价值在于能让你跑通从页面点击到文件落盘的全过程,把Servlet、JSP、MySQL、Tomcat串成一条线。适合刚学完Java基础、想照着一个真实工程动手做一遍的人。跑通这个项目,你对HTTP请求怎么变成数据库记录、IO流怎么落盘,会比读十篇面经都有底。
2. javaweb技术选型与数据库设计:先别急着写代码
2.1 打开zip后先确认工程形态:Maven还是传统Web
拿到zip,不要急着双击解压然后直接导入IDEA。先解压,看根目录有没有pom.xml。有pom.xml就是Maven工程,没有就是传统Web工程,依赖jar包一般都在WEB-INF/lib目录里一起交付。这个区别直接影响后续的依赖导入方式。我见过不少人在这一步翻车:把Maven工程当普通工程导入,结果jar包一个没加载,Tomcat启动直接NoClassDefFoundError。所以第一步是明确构建方式。
传统javaweb工程的web目录一般长这样:WEB-INF/web.xml、WEB-INF/lib/*.jar、static或css/js目录,页面可能是JSP。如果是SSM,还会有spring-mvc.xml、mybatis-config.xml这些配置文件。Spring Boot工程则会有application.yml。这个zip既然标题写了javaweb,大概率是前两类。如果根目录有pom.xml,IDEA里直接Open as Project,选Maven,等依赖下完就能跑。如果只有src和web目录,用New Project from Existing Sources,把web目录指给IDEA的Web模块就行。
这里有个参数要养成习惯:Tomcat版本和JDK版本必须匹配。JDK8配Tomcat8.5或9,JDK11以上最好直接Tomcat9。zip里如果带了lib目录,注意jar包是不是按旧JDK编译的,出现java.lang.UnsupportedClassVersionError,说明编译JDK和运行JDK不一致,这种问题退JDK比换Tomcat更干脆。
我拿到zip后通常会先翻一遍lib目录。看到spring-webmvc和mybatis,就能按SSM框架去理解;如果只有servlet-api和jstl,那基本就是纯Servlet加JSP,连Spring都不用碰。第一种工程RequestMapping注解满天飞,第二种全靠web.xml里的servlet-mapping。搞清楚这个,你才知道该读哪段代码,后续改上传逻辑时也不至于对着一个Controller发呆。
2.2 数据库表设计:用户、文件、分享各一张
小型云盘的表不会太多,三张核心表够了:用户表存账号信息,文件表存文件元数据和存储路径,分享表存分享链接和提取码。文件表是整个系统的核心,字段可以这样设计:
CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, `salt` VARCHAR(32) NOT NULL, `total_storage` BIGINT DEFAULT 1073741824, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `file_info` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `parent_id` BIGINT DEFAULT 0, `file_name` VARCHAR(255) NOT NULL, `file_size` BIGINT NOT NULL, `file_hash` VARCHAR(64) NOT NULL, `store_path` VARCHAR(500) NOT NULL, `is_dir` TINYINT DEFAULT 0, `upload_time` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_parent (`user_id`, `parent_id`), INDEX idx_hash (`file_hash`, `file_size`) ); CREATE TABLE `share` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `file_id` BIGINT NOT NULL, `share_code` VARCHAR(16) NOT NULL, `expire_time` DATETIME, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_code (`share_code`) );逻辑说明:user.total_storage用来做配额,默认1GB,传文件前先算是否超配额。file_info.user_id把文件归属到用户,parent_id支持文件夹结构,file_hash是秒传判断的关键,store_path存服务器上的实际物理路径,永远不要把这条路经返回给前端。share_code是提取码,用户分享时生成一个短码。
参数说明:password存加盐哈希,不是明文,salt单独存盐值。file_size用BIGINT是因为文件可能超过2GB,INT上限约21亿,会溢出。file_hash用SHA256,64个字符,不建议用MD5,碰撞概率高。store_path用VARCHAR(500),要留够Linux路径的长度。file_hash和file_size建了联合索引,秒传查询才快。
主键和外键的问题再说一句。小型系统里很多人喜欢给所有表加外键约束,我一开始也这么干,后来发现删除父记录时总被外键限制卡住。文件表和用户表之间可以不建物理外键,只保留user_id这个逻辑关联。理由一是性能,每次插入都要检查外键;理由二是删除太灵活,物理外键会逼着你先删子记录再删父记录。课程设计里按逻辑关联做,老师反而觉得你懂设计。
2.3 文件存储策略:本地磁盘比数据库BLOB更靠谱
网盘系统的文件本体不建议进MySQL。用BLOB或LONGBLOB听着省事,但数据库文件会迅速膨胀,备份和迁移变成灾难。而且Java侧把BLOB读出再写回磁盘时,内存要承受整个文件的byte[]压力,上传几百MB文件JVM直接OOM。小型云盘最经济的做法是本地磁盘存储,用一个固定目录作为网盘的物理根路径。
常见做法是这样组织目录:/data/cloud/{userId}/{date}/{uuid}.bin。按用户分目录,删用户时直接删一个文件夹;按日期分目录,避免单目录文件过多。ext4文件系统下,单一目录文件数量超过几万后访问会变慢。开发环境我会直接写死一个目录,比如File dir = new File("E:/cloud");生产环境就改成/data/cloud。存储目录在properties里维护,不要在代码里散落一堆路径拼接。
为什么不建议第一版就用对象存储?因为这个项目本质是教学和训练向的。用OSS等于引入外部依赖,失去了自己处理文件流的意义。你把这个本地存储逻辑写坏了,再迁移到OSS就很容易理解SDK封装了什么。第一版老老实实写本地,把权限、重名、目录结构这几个问题想清楚,后面接MinIO还是OSS都是顺手的事。
磁盘空间的坑也要提前提一下。云盘跑一段时间后,E盘或/data分区会被文件塞满。代码里要有清理临时目录的逻辑,比如上传时用到的tmp目录。另外,如果上传的文件已存在,file_info里可能有多条记录指向同一个store_path,这是秒传场景的正常现象,磁盘占用不会增加,但数据库记录会增加,不做引用计数的话删除时要小心。
3. 核心功能实现:上传、秒传与下载的Servlet代码
3.1 上传接口:从Part到落盘
先写最核心的上传接口。如果是纯Servlet工程,用Servlet 3.0的Part接口来接收文件。注意@MultipartConfig注解必须加,否则Part获取不到内容。这里以SSM之外的最简形态为例:
@WebServlet("/api/upload") @MultipartConfig(location = "E:/cloud/tmp", maxFileSize = 1024L * 1024 * 1024, maxRequestSize = 1024L * 1024 * 1024 * 2) public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); HttpSession session = req.getSession(); Integer userId = (Integer) session.getAttribute("userId"); if (userId == null) { resp.sendError(401); return; } Part part = req.getPart("file"); String originalName = getFileName(part); String uuid = UUID.randomUUID().toString().replace("-", ""); String ext = originalName.contains(".") ? originalName.substring(originalName.lastIndexOf(".")) : ""; File dir = new File("E:/cloud/" + userId + "/" + new SimpleDateFormat("yyyyMMdd").format(new Date())); if (!dir.exists()) { dir.mkdirs(); } part.write(dir.getAbsolutePath() + "/" + uuid + ext); File stored = new File(dir, uuid + ext); long size = stored.length(); String hash = FileDigest.sha256(stored); // 这里把userId, originalName, size, hash, storePath插入file_info resp.getWriter().write("{\"status\":\"success\"}"); } private String getFileName(Part part) { String header = part.getHeader("content-disposition"); for (String token : header.split(";")) { if (token.trim().startsWith("filename=")) { return token.substring(token.indexOf("=") + 1).trim().replace("\"", ""); } } return "unknown"; } }逻辑说明:@MultipartConfig里的maxFileSize是单文件上限,maxRequestSize是整个请求上限。Part.write只能写绝对路径,所以先拼好目录再mkdirs。getFileName从Content-Disposition头里抠出原始文件名。form表单里文件控件的name必须是"file",否则getPart("file")会拿到null。
参数说明:maxFileSize=1G只是个上限,课程设计不需要开这么大,调成100M足够。location是上传临时目录,文件会先落临时再迁移,直接把location指到最终目录也行,但并发高时会有文件互相覆盖的隐患,建议保留临时目录概念。Windows和Linux的路径分隔符问题,这里用File类拼接,不要手写带反斜杠的路径。
3.2 秒传:先算哈希再查库
秒传是网盘系统的标志性功能,也是一个让老师和面试官眼前一亮的点。它的核心是哈希去重:前端在上传前先计算文件的SHA256,把hash和文件大小传给后端。后端查file_info表,如果存在同hash同size的记录,说明这个文件已经有人传过了,直接给当前用户复制一条file_info记录,不实际传文件。
String hash = req.getParameter("hash"); long size = Long.parseLong(req.getParameter("size")); FileInfo existing = fileDao.findByHashAndSize(hash, size); if (existing != null) { FileInfo copy = new FileInfo(); copy.setUserId(userId); copy.setFileName(originalName); copy.setFileSize(existing.getFileSize()); copy.setFileHash(hash); copy.setStorePath(existing.getStorePath()); fileDao.insert(copy); resp.getWriter().write("{\"status\":\"fast_success\"}"); return; } // 没有匹配记录,继续走完整上传逻辑说明:这个逻辑成立的前提是文件只读不写。一旦有人删除了自己的记录但没删磁盘文件,store_path还是有效的。这里有个坑:如果删除操作把磁盘文件也删了,秒传判断就会指向一个不存在的文件。所以删除要谨慎,要么做引用计数,要么判断还有多少条file_info记录指向同一个store_path,只有当记录数为0时才删磁盘文件。
前端配合也很简单。上传前先用FileReader读文件,计算SHA256,把hash和size用AJAX发到后端一个检查接口。后端返回fast_success就提示"秒传成功",否则走真实上传。Java后端计算SHA256的代码不复杂,用MessageDigest配合流式读取,不要一次性读整个文件。
需要注意的是file_hash和file_size的联合索引。数据量小无所谓,但上传记录到一万条以上后,没有索引的秒传查询会明显变慢,MySQL全表扫描的代价会直接体现在接口耗时上。
3.3 下载:流式输出别用byte[]
下载接口是另一个容易出问题的点。常见错误是File.readAllBytes()之后一次性write,几十MB文件就把内存吃掉了,Tomcat线程一多直接OOM。正确做法是定长缓冲,循环读写。
@WebServlet("/api/download") public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long fileId = Long.parseLong(req.getParameter("fileId")); FileInfo info = fileDao.findById(fileId); File file = new File(info.getStorePath()); if (!file.exists()) { resp.sendError(404); return; } resp.setContentType("application/octet-stream"); String encodedName = URLEncoder.encode(info.getFileName(), "UTF-8").replace("+", "%20"); resp.setHeader("Content-Disposition", "attachment; filename=\"" + encodedName + "\""); resp.setContentLengthLong(file.length()); try (InputStream in = new FileInputStream(file); OutputStream out = resp.getOutputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } } }逻辑说明:Content-Disposition用attachment触发浏览器下载。中文文件名不能直接放进header,要先URLEncoder编码,否则会乱码。setContentLengthLong告诉浏览器文件大小,配合下载工具可以显示进度和剩余时间。
参数说明:URLEncoder会把空格编码成+,HTTP头里的+会被当成空格,所以要用replace("+", "%20")修正。这是下载文件名乱码之外最常见的坑。另外,buffer大小用8KB只是起步值,用16KB或64KB在机械硬盘上性能差异不大,但SSD上大缓冲可以减少IO次数,我用32KB比较多。
3.4 用户目录隔离:权限判断不能省
上传和下载接口里都要拿session中的userId,在SQL或者存储路径上强制挂上用户维度。常见错误是只根据fileId查文件就返回,不校验这个fileId属于谁。这会造成越权:登录用户A,构造fileId就能下载用户名B的文件。解决办法是每个查询都加上AND user_id = ?条件。
目录隔离的物理实现其实很简单。每个用户的文件都放在以自己ID命名的文件夹下,上传时路径里带userId,下载时从file_info里查出的store_path已经包含了userId。这样即使文件被越权访问,只要store_path不泄露,风险也可控。但底线是接口层就得判断,不能指望目录名保密。
分块上传在纯Servlet工程里实现起来会复杂一些,需要前端把文件切成固定大小的块,逐块上传,后端等所有块到位后合并。这个功能对于课程设计不是必须项,但如果你想让系统更完整,可以放到后面扩展。合并代码不算多,用BufferedOutputStream逐个块按顺序写出即可,真正的复杂度在进度管理和断点续传上。
4. 把zip工程在IDEA里跑通:配置与部署全流程
4.1 环境:JDK8、MySQL8、Tomcat9一个都不能乱
开发小型 javaweb 项目,最稳的组合是JDK8 + Tomcat8.5 + MySQL5.7。MySQL8也能用,但驱动要换成mysql-connector-java 8.0.x,连接串需要加serverTimezone=Asia/Shanghai和allowPublicKeyRetrieval=true。很多人按照黑马javaweb笔记里的老配置去连MySQL8,一启动就报时区异常。
如果你用的zip里自带数据库脚本,先执行脚本建库。如果脚本里没有建库语句,就手动建一个cloud_disk库,把表结构脚本导进去。MySQL8用zip方式安装时还需要先初始化data目录,这个步骤和云盘工程本身无关,但配置不好会导致后续连接失败。建议数据库账号密码就设为root/123456,写在jdbc.properties里,不要图省事直接写死在Servlet代码中。
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cloud_disk?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456参数说明:characterEncoding=UTF-8解决中文乱码,useSSL=false避免MySQL8在本地连接时反复警告,allowPublicKeyRetrieval=true是MySQL8用caching_sha2_password认证时的必需项。不加serverTimezone会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,这种错误很容易让新手误以为是驱动问题。
4.2 idea运行javaweb项目配置:Artifacts和Deployment
把zip导入IDEA后,下一步是配置Tomcat。在Run/Debug Configurations里新增Tomcat Server > Local。注意IDEA不会自动识别web项目,这里必须手动确认Artifacts和Deployment。
很多人在这里卡住:明明写了正确的Servlet,访问题目却报404。原因通常是IDEA的Web Facet没有指向正确的web目录。打开Project Structure,找到Facets,确认Web Resource Directory是不是zip解压后的web目录。然后再看Artifacts,如果只有一个web:war exploded,说明OK;如果是空的,点Create from facets自动生成。
Deployment里的Application context,建议设置成/cloud,不要用根路径/。这样访问地址是http://localhost:8080/cloud,和登录页、静态资源路径都好区分。后续跳转代码里如果有/cloud/login.jsp这种硬编码路径,depoyment配置改掉后记得同步改代码,不然404会重新出现。
4.3 zip包导入的几种特殊状况
解压zip时如果报invalid zip archive: could not find eocd,说明文件下载不完整。EAZYcode不,EOCD是zip文件末尾的中央目录记录,缺少它无法解析整个压缩包,重新下载即可。还有一种情况是zip内部嵌套了一层目录,解压后是cloud-disk/cloud-disk/,导入时要选里面的那层,不是外层。
如果工程是Maven的,导入后IDEA会提示Auto Import,要选Enable。依赖下载过程中不要关IDEA,否则Maven库会留下损坏的lastUpdated文件,下次导入时很麻烦。右侧Maven面板如果出现红色波浪线,多半是仓库里缺包,先用mvn clean package验证能否整体编译过。这里我一般建议先打一次war包,再用war包部署到Tomcat。war能打出来,说明源码层面已经通了;war打不出来,说明依赖或编译配置有问题,再回头查。
4.4 启动顺序和最小验证
启动一套javaweb项目的顺序通常是:启动MySQL,确认能连上;启动Tomcat,观察控制台没有异常;然后访问http://localhost:8080/cloud。如果首页是JSP,第一次访问Tomcat会把它编译成Servlet,速度偏慢是正常的。
Tomcat控制台直接能看到启动日志和异常堆栈。常见错误分两类:一类是ClassNotFoundException,集中在依赖缺失;另一类是数据库连接失败,集中在驱动或连接串。数据库连不上时,先看jdbc.properties,再看MySQL服务有没有起来。Windows下用zip安装的MySQL,启动命令是net start mysql,忘了初始化的要先用mysqld --initialize-insecure初始化。
IDEA控制台里如果项目启动后自动弹出浏览器但显示404,先别怀疑代码逻辑,看Tomcat的Deployment里添加的Artifact是什么。有时候添加了war包而不是exploded目录,也会出现路径解析差异。运行一次之后,把URL里的端口和contextPath核对一遍,基本能定位。
5. 避坑指南:javaweb云盘最常见的5个翻车现场
5.1 上传文件超过一定大小就报错
现象:点击上传后浏览器报413 Request Entity Too Large,或者后端抛FileUploadException。明明MySQL表里的字段没问题,就是传不了大文件。
原因:Tomcat对POST请求体大小有限制,老版本默认2MB。如果没有确认@MultipartConfig注解生效,或者只设置maxFileSize没设置maxRequestSize,超过限制就会抛异常。另外如果用了SpringMVC,spring.servlet.multipart.max-request-size也要同步配置。
解决:在Servlet上显式加@MultipartConfig(maxFileSize = 104857600, maxRequestSize = 209715200),前端form加enctype="multipart/form-data"。如果走SpringMVC,在application.yml或xml里配好multipart参数。不要再依赖Tomcat的maxPostSize,那个参数在新版本里已经不太适用。
5.2 中文文件名下载乱码
现象:下载时文件名变成%E4%B8%AD%E6%96%87.pdf,或者直接显示乱码,在Chrome和Edge下表现还不一样。
原因:Content-Disposition的filename参数默认按ISO-8859-1解析,中文没有编码进去。直接放中文会导致浏览器解析失败。
解决:Java侧先用URLEncoder.encode(fileName, "UTF-8")把文件名转成百分号编码,再把空格+替换成%20。更兼容的做法是同时输出filename*参数,格式是filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf。两者都写上,老浏览器用filename,现代浏览器用filename*。
5.3 下载大文件导致JVM内存溢出
现象:Tomcat运行一段时间后,下载一个几百MB的文件就报OutOfMemoryError,重启就好,过一阵又出现。
原因:下载代码里用了IOUtils.toByteArray或File.readAllBytes,把整个文件读进内存再写给浏览器。一个请求占用几百MB内存,Tomcat默认堆也就1GB左右,并发一上来必崩。
解决:改成流式输出,byte[] buffer = new byte[8192]循环读取写入,代码在3.3里已经给出。另外把JVM堆适当调大,IDEA的Tomcat配置里有个VM options,设成-Xms256m -Xmx1024m比较稳妥。这个参数要改的是运行Tomcat的实例配置,不是写进代码。
5.4 秒传出现误判
现象:两个内容不同的文件,前端算的MD5一样,上传执行了秒传,下载下来发现打开是另一个文件的内容。
原因:MD5碰撞概率虽然不高,但确实存在被人为构造的可能性。更大的可能是指标太单薄,只hash没比大小,或者hash计算范围只取了文件的一部分。
解决:改用SHA256,并且把文件大小作为双重判断条件,也就是file_hash和file_size都要匹配。后端不能只信任前端传的hash,在高要求场景下要服务端重新计算,但那样会损失秒传的意义。折中方案是:秒传命中时不立即返回成功,而是异步对文件做一个抽查校验,比如比对头部1KB的hash。
5.5 数据库连接时区与公钥问题
现象:第一次连接MySQL8时,报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,或Public Key Retrieval is not allowed。
原因:MySQL8默认时区不是Java识别的格式;caching_sha2_password插件需要在非SSL连接下先获取公钥。
解决:连接串加serverTimezone=Asia/Shanghai和allowPublicKeyRetrieval=true,useSSL=false也要带上。如果仍然报时区错误,在MySQL里执行set global time_zone = '+8:00'。这个问题基本只在MySQL8出现,MySQL5.7几乎没有。
5.6 存储路径在Windows能跑,部署Linux就找不到文件
现象:本地开发一切正常,部署到服务器后下载文件报404,但数据库记录存在。
原因:某些代码里用\拼接路径,或者把Windows绝对路径存进了数据库。Linux下\不是路径分隔符,文件当然找不到。
解决:代码中统一用File.separator或直接使用正斜杠/,Java的File类在Windows下能自动兼容正斜杠。数据库store_path字段只存以/开头的相对风格路径,真正的前缀由配置文件指定。另外不要用System.getProperty行号之类的东西去拼路径,容易踩平台差异的坑。
6. 进阶技巧:用JMeter验证并发,再想想下一步往哪走
6.1 用JMeter做一次最小并发验证
系统跑通后,第一件事是用JMeter做一次并发测试,别急着加功能。打开JMeter,新建线程组,设20个线程,循环5次;添加HTTP请求,路径指向/api/upload,方式POST,文件上传用${__randomString(10, abcdefg, name)}.txt做文件名。这样能快速脚本化一批不同文件名的上传请求。跑完后重点看两个指标:Error%=0算及格,平均响应时间在几百毫秒内算正常。如果Error%过高,先看Tomcat控制台有没有OOM或锁等待。
6.2 还能往哪个方向改
这个系统最值得扩展的方向是分块上传和断点续传,再往后是异步转存和离线下载。分块只需要前端把文件切片,后端加一个合并接口,复杂度可控;断点续传则需要记录每个块的上传状态,用Redis存进度会让代码更清晰。存储层也可以做一次升级,把本地路径替换成MinIO或其他对象存储,接口层不变,秒传逻辑可以继续复用。还有分享链接的过期策略,目前是写死的,改成定时任务扫描share.expire_time会更接近真实产品。最后要检查的是日志,别只打System.out,至少用commons-logging留出标准记录格式,不然出了问题全靠瞎试。
做这套系统时我自己也栽过不少跟头。早期做秒传只算MD5,导致老师上传一个文件下载出来是另一个人的内容,脸都丢光了。后来把hash换成了SHA256加大小双重校验,才真正敢说这个功能能上台演示。还有个习惯是每跑通一个接口就做一次浏览器跨设备验证,上传下载、中文名、错乱路径这些场景是网盘系统的命门,提前踩一遍,答辩被追问时不慌。希望这些经验能帮到你,少走点我走过的弯路。
本文还有配套的精品资源,点击获取