
简介本资源是一套完整的本科毕业设计项目——Web云存储硬盘系统的设计与实现面向计算机类专业学生如软件工程、计科、信息安全、大数据等及初入职场的开发人员聚焦于前后端协同、文件管理、用户权限与Web存储架构等核心实践问题适用于课程设计、大作业、毕设参考及立项演示。压缩包共125个文件涵盖33份Word文档含开题报告、论文终稿、登记表、学分申请表等全流程材料、23份Visio系统设计图含概念模型.cdb/.cdm、数据库逻辑结构等、20张PNG界面与流程图、10份PDF技术说明与规范以及SQL建库脚本、Vue前端页面、PDM数据库模型等关键工程文件整体大小为127.02MB。已有114人学习下载内容经实测可运行包含从需求分析、系统设计、数据库建模到前后端实现的完整闭环特别适合希望掌握全栈开发流程、理解云存储业务逻辑与工程文档规范的学习者深度研习。1. 为什么一个“课设级”的 Web 云存储硬盘系统反而成了验证工程能力的试金石你手头正赶着期末大作业题目写着“Web 云存储硬盘系统的设计与实现”——听起来像套壳网盘实则是个微型分布式系统的全链路沙盒。它不追求亿级用户但必须在单机或三节点内跑通用户鉴权 → 文件分块上传 → 元数据持久化 → 断点续传 → 前端可视化管理这五关稍有疏忽上传卡死、列表乱码、删除不生效、多用户并发写入元数据错乱……全是课设答辩现场的“公开处刑”高发区。这不是在堆 HTMLNode.js 就能交差的玩具项目而是用最小成本暴露你对 HTTP 协议边界、文件 I/O 阻塞模型、数据库事务隔离、前端 Blob 处理、以及前后端时钟同步等真实工程细节的理解深度。适合计算机专业大三及以上学生已有基础 Web 开发经验但尚未在生产环境处理过文件流、大文件上传、或元数据一致性问题。本文不讲“云原生”“对象存储架构”只聚焦课设可落地、老师能验收、答辩能讲清的完整闭环。2. 从零搭起服务端骨架选型不是炫技是为课设减负课设时间紧、资源少、部署环境受限常只能跑在本地或学校虚拟机服务端选型必须满足三个硬约束启动快、依赖少、调试直观、文档友好。Python Flask SQLite 组合在此场景下反而是更优解——不是因为它“高级”而是因为Flask 启动只需flask run无构建步骤改完代码 CtrlS 刷新即见效果SQLite 是单文件数据库无需安装服务、配置用户、开防火墙端口app.db丢进 Git 就是完整状态所有逻辑路由、上传处理、元数据操作可写在一个app.py里答辩时打开文件就能讲清楚数据流向。提示别被“云存储”字眼带偏去啃 MinIO 或 Ceph。课设阶段把“文件存哪”和“怎么找回来”这两件事做稳比模拟 AWS S3 接口重要十倍。2.1 初始化 Flask 服务与 SQLite 数据库结构先创建项目目录结构mkdir web-cloud-disk cd web-cloud-disk pip install flask flask-sqlalchemy werkzeug新建app.py定义核心数据模型注意字段命名直白避免课设中因表名歧义被质疑# app.py from flask import Flask, request, jsonify, send_from_directory, abort from flask_sqlalchemy import SQLAlchemy from werkzeug.utils import secure_filename import os import time import hashlib app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///app.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db SQLAlchemy(app) # 用户表课设级只需用户名密码哈希不存明文 class User(db.Model): id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(120), nullableFalse) # 文件元数据表关键字段是 file_id唯一标识、user_id归属、real_name原始名、stored_name存储名、size、upload_time class FileRecord(db.Model): id db.Column(db.Integer, primary_keyTrue) user_id db.Column(db.Integer, db.ForeignKey(user.id), nullableFalse) real_name db.Column(db.String(255), nullableFalse) # 用户看到的文件名 stored_name db.Column(db.String(255), nullableFalse) # 服务器存储的唯一文件名防重名 size db.Column(db.Integer, nullableFalse) upload_time db.Column(db.Integer, defaultlambda: int(time.time())) # 初始化数据库首次运行自动建表 with app.app_context(): db.create_all()这段代码做了三件关键事用secure_filename()防止路径遍历攻击如上传../../etc/passwdstored_name不直接用real_name而是生成唯一哈希名后文详述避免同名覆盖upload_time存时间戳整数而非 datetime 对象SQLite 原生支持INTEGER省去类型转换烦恼前端展示时再转成可读格式。2.2 实现用户注册与登录接口JWT 简化版课设不需复杂 OAuth用轻量 JWTJSON Web Token做会话管理即可。安装依赖pip install PyJWT在app.py中追加import jwt from datetime import datetime, timedelta # 密钥课设用固定字符串实际项目必须环境变量注入 SECRET_KEY web-cloud-disk-2024-course app.route(/api/register, methods[POST]) def register(): data request.get_json() username data.get(username) password data.get(password) if not username or not password: return jsonify({error: 用户名和密码不能为空}), 400 if User.query.filter_by(usernameusername).first(): return jsonify({error: 用户名已存在}), 409 # 密码哈希课设用简单 sha256勿用于真实系统 password_hash hashlib.sha256(password.encode()).hexdigest() user User(usernameusername, password_hashpassword_hash) db.session.add(user) db.session.commit() return jsonify({message: 注册成功}), 201 app.route(/api/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) user User.query.filter_by(usernameusername).first() if not user or user.password_hash ! hashlib.sha256(password.encode()).hexdigest(): return jsonify({error: 用户名或密码错误}), 401 # 生成 token有效期 24 小时 token jwt.encode({ user_id: user.id, exp: datetime.utcnow() timedelta(hours24) }, SECRET_KEY, algorithmHS256) return jsonify({token: token}), 200参数说明与踩坑点jwt.encode()的algorithmHS256必须显式指定否则新版 PyJWT 默认用HS256但旧版行为不一致导致本地测试正常、部署后 token 解析失败exp字段用datetime.utcnow()而非datetime.now()避免时区混乱SQLite 存储无时区信息统一用 UTC 时间戳最稳妥密码哈希仅用于课设演示绝对不可照搬至真实系统——真实场景必须用bcrypt或scrypt加盐哈希。3. 文件上传的核心攻坚分块、校验、断点续传一个都不能少课设里最容易翻车的环节用户上传一个 200MB 的视频页面卡死、进度条不动、刷新后文件消失。根本原因在于没处理好大文件上传的阻塞性、网络中断恢复、以及服务端内存溢出。解决方案是前端分块上传 后端合并校验。这不是炫技而是让课设具备基本可用性的底线。3.1 前端分块上传逻辑HTML JavaScript新建templates/index.html包含基础 UI 和上传脚本!-- templates/index.html -- !DOCTYPE html html headtitleWeb 云存储硬盘/title/head body h2上传文件/h2 input typefile idfileInput / button onclickuploadFile()开始上传/button div idprogress等待选择文件.../div script let file null; const CHUNK_SIZE 5 * 1024 * 1024; // 5MB 每块 document.getElementById(fileInput).onchange function(e) { file e.target.files[0]; document.getElementById(progress).innerText 已选择${file.name} (${(file.size/1024/1024).toFixed(2)} MB); }; async function uploadFile() { if (!file) return; const totalChunks Math.ceil(file.size / CHUNK_SIZE); let uploadedChunks 0; for (let i 0; i totalChunks; i) { const start i * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); const chunk file.slice(start, end); const formData new FormData(); formData.append(chunk, chunk); formData.append(filename, file.name); formData.append(chunk_index, i); formData.append(total_chunks, totalChunks); formData.append(file_size, file.size); try { const res await fetch(/api/upload, { method: POST, headers: { Authorization: Bearer ${localStorage.getItem(token)} }, body: formData }); if (!res.ok) throw new Error(上传第${i1}块失败); uploadedChunks; document.getElementById(progress).innerText 上传中${uploadedChunks}/${totalChunks} 块完成; } catch (err) { document.getElementById(progress).innerText 上传失败${err.message}; return; } } // 所有块上传完毕触发合并 await fetch(/api/merge, { method: POST, headers: { Authorization: Bearer ${localStorage.getItem(token)} }, body: JSON.stringify({ filename: file.name }) }); document.getElementById(progress).innerText ✅ 上传完成${file.name}; file null; } /script /body /html关键设计点说明CHUNK_SIZE 5MB是课设友好值太小如 1MB导致 HTTP 请求过多增加服务端压力太大如 50MB易因单次请求超时失败file.slice()是浏览器原生 API不加载整个文件到内存避免页面卡死localStorage.getItem(token)读取登录后存的 token实现无感鉴权。3.2 后端接收分块并暂存防重复、防覆盖在app.py中添加/api/upload接口import tempfile import shutil UPLOAD_TEMP_DIR temp_uploads # 创建临时目录课设级不考虑清理策略答辩前手动删 os.makedirs(UPLOAD_TEMP_DIR, exist_okTrue) app.route(/api/upload, methods[POST]) def upload_chunk(): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: 未授权}), 401 try: payload jwt.decode(token[7:], SECRET_KEY, algorithms[HS256]) user_id payload[user_id] except jwt.ExpiredSignatureError: return jsonify({error: 登录已过期}), 401 except jwt.InvalidTokenError: return jsonify({error: 无效令牌}), 401 if chunk not in request.files: return jsonify({error: 缺少文件块}), 400 chunk_file request.files[chunk] filename request.form.get(filename) chunk_index int(request.form.get(chunk_index)) total_chunks int(request.form.get(total_chunks)) file_size int(request.form.get(file_size)) # 为每个用户文件生成唯一临时目录避免并发冲突 safe_filename secure_filename(filename) temp_dir os.path.join(UPLOAD_TEMP_DIR, f{user_id}_{hashlib.md5(safe_filename.encode()).hexdigest()}) os.makedirs(temp_dir, exist_okTrue) # 保存当前块命名格式 chunk_0001 chunk_path os.path.join(temp_dir, fchunk_{str(chunk_index).zfill(4)}) chunk_file.save(chunk_path) # 记录已上传块数用文件数代替数据库课设简化 uploaded_count len([f for f in os.listdir(temp_dir) if f.startswith(chunk_)]) return jsonify({ uploaded_chunks: uploaded_count, total_chunks: total_chunks, status: received }), 200为什么不用数据库存分块状态课设阶段用文件系统记录更直观temp_uploads/123_abcd1234/chunk_0001这种路径一眼可知归属和进度答辩时ls -l temp_uploads/123_abcd1234/就能证明“块确实收到了”。数据库存状态虽严谨但增加事务复杂度且课设无高并发压力。3.3 合并分块并写入最终存储含完整性校验添加/api/merge接口完成最后一步app.route(/api/merge, methods[POST]) def merge_chunks(): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: 未授权}), 401 try: payload jwt.decode(token[7:], SECRET_KEY, algorithms[HS256]) user_id payload[user_id] except Exception: return jsonify({error: 无效令牌}), 401 data request.get_json() filename data.get(filename) if not filename: return jsonify({error: 缺少文件名}), 400 safe_filename secure_filename(filename) temp_dir os.path.join(UPLOAD_TEMP_DIR, f{user_id}_{hashlib.md5(safe_filename.encode()).hexdigest()}) if not os.path.isdir(temp_dir): return jsonify({error: 临时文件不存在请重新上传}), 400 # 检查所有块是否齐全 total_chunks len([f for f in os.listdir(temp_dir) if f.startswith(chunk_)]) expected_chunks max([int(f.split(_)[1]) for f in os.listdir(temp_dir) if f.startswith(chunk_)]) 1 if total_chunks ! expected_chunks: return jsonify({error: f分块不完整收到{total_chunks}块期望{expected_chunks}块}), 400 # 生成唯一存储名user_id 时间戳 原文件哈希防同名覆盖 file_hash hashlib.md5((str(user_id) filename str(int(time.time()))).encode()).hexdigest() stored_name f{user_id}_{file_hash}_{safe_filename} # 合并所有块到 uploads 目录 UPLOADS_DIR uploads os.makedirs(UPLOADS_DIR, exist_okTrue) final_path os.path.join(UPLOADS_DIR, stored_name) with open(final_path, wb) as f_out: for i in range(expected_chunks): chunk_path os.path.join(temp_dir, fchunk_{str(i).zfill(4)}) if os.path.exists(chunk_path): with open(chunk_path, rb) as f_in: shutil.copyfileobj(f_in, f_out) else: return jsonify({error: f缺失块 {i}}), 400 # 计算合并后文件大小校验用 actual_size os.path.getsize(final_path) # 写入元数据到数据库 file_record FileRecord( user_iduser_id, real_namefilename, stored_namestored_name, sizeactual_size, upload_timeint(time.time()) ) db.session.add(file_record) db.session.commit() # 清理临时目录课设可选答辩前手动删更安全 shutil.rmtree(temp_dir) return jsonify({ message: 合并成功, file_id: file_record.id, stored_name: stored_name, size: actual_size }), 200校验逻辑为何必要actual_size与前端传的file_size比对可发现传输截断如网络中断导致最后一块不完整合并时逐块检查是否存在避免因某块上传失败却误判为“全部完成”stored_name包含user_id和时间戳哈希确保即使两个用户同时上传同名文件也不会覆盖。4. 前端文件管理界面用纯静态页实现动态交互拒绝框架绑架课设答辩时老师最想看到的是“你能控制数据流向”而不是“你会配 Vue Router”。因此我们用原生 JavaScript Fetch API 实现文件列表、下载、删除功能所有逻辑写在static/main.js中不引入任何前端框架。这样做的好处是代码行数可控、调试直接Chrome 控制台打断点即停、答辩时讲一句“这里调用了/api/files接口获取 JSON”就能说清本质。4.1 获取用户文件列表带分页与排序在app.py中添加/api/files接口app.route(/api/files, methods[GET]) def list_files(): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: 未授权}), 401 try: payload jwt.decode(token[7:], SECRET_KEY, algorithms[HS256]) user_id payload[user_id] except Exception: return jsonify({error: 无效令牌}), 401 # 分页参数默认 page1, per_page20 page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 20, typeint) # 查询该用户所有文件按上传时间倒序 pagination FileRecord.query.filter_by(user_iduser_id)\ .order_by(FileRecord.upload_time.desc())\ .paginate(pagepage, per_pageper_page, error_outFalse) files [] for record in pagination.items: files.append({ id: record.id, real_name: record.real_name, size: record.size, upload_time: record.upload_time, formatted_time: datetime.fromtimestamp(record.upload_time).strftime(%Y-%m-%d %H:%M) }) return jsonify({ files: files, pagination: { page: page, per_page: per_page, total: pagination.total, pages: pagination.pages } })为什么用paginate()而非limitoffsetFlask-SQLAlchemy 的paginate()自动处理边界情况如page1000时返回空列表而非报错且error_outFalse让课设更健壮——用户乱点页码不会导致 500 错误。4.2 前端渲染与交互static/main.js新建static/main.js// static/main.js async function loadFiles() { const token localStorage.getItem(token); if (!token) { alert(请先登录); window.location.href /login; return; } try { const res await fetch(/api/files?page1per_page50, { headers: { Authorization: Bearer ${token} } }); const data await res.json(); const fileList document.getElementById(fileList); fileList.innerHTML ; if (data.files.length 0) { fileList.innerHTML trtd colspan4暂无文件/td/tr; return; } data.files.forEach(file { const row document.createElement(tr); row.innerHTML td${file.real_name}/td td${formatFileSize(file.size)}/td td${file.formatted_time}/td td button onclickdownloadFile(${file.id})下载/button button onclickdeleteFile(${file.id}) stylecolor:red删除/button /td ; fileList.appendChild(row); }); } catch (err) { console.error(err); alert(加载文件列表失败); } } function formatFileSize(bytes) { if (bytes 0) return 0 Bytes; const k 1024; const sizes [Bytes, KB, MB, GB]; const i Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) sizes[i]; } async function downloadFile(fileId) { const token localStorage.getItem(token); try { const res await fetch(/api/download/${fileId}, { headers: { Authorization: Bearer ${token} } }); if (!res.ok) throw new Error(下载失败); const blob await res.blob(); const url window.URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download download_${fileId}; // 实际文件名由后端 Content-Disposition 决定 document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (err) { alert(下载失败 err.message); } } async function deleteFile(fileId) { if (!confirm(确定要删除此文件)) return; const token localStorage.getItem(token); try { const res await fetch(/api/delete/${fileId}, { method: DELETE, headers: { Authorization: Bearer ${token} } }); if (!res.ok) throw new Error(删除失败); alert(删除成功); loadFiles(); // 刷新列表 } catch (err) { alert(删除失败 err.message); } }关键细节a.download属性在现代浏览器中有效但若后端未设置Content-Disposition: attachment; filenamexxx部分浏览器可能忽略该属性此时靠后端响应头兜底window.URL.createObjectURL(blob)是标准做法避免将大文件全部读入内存删除后调用loadFiles()刷新而非location.reload()体验更流畅。4.3 后端提供下载与删除接口继续在app.py中补全app.route(/api/download/int:file_id, methods[GET]) def download_file(file_id): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: 未授权}), 401 try: payload jwt.decode(token[7:], SECRET_KEY, algorithms[HS256]) user_id payload[user_id] except Exception: return jsonify({error: 无效令牌}), 401 file_record FileRecord.query.filter_by(idfile_id, user_iduser_id).first() if not file_record: return jsonify({error: 文件不存在或无权限}), 404 file_path os.path.join(uploads, file_record.stored_name) if not os.path.exists(file_path): return jsonify({error: 文件物理路径不存在}), 404 # 设置响应头提示浏览器下载 return send_from_directory(uploads, file_record.stored_name, as_attachmentTrue, download_namefile_record.real_name) app.route(/api/delete/int:file_id, methods[DELETE]) def delete_file(file_id): token request.headers.get(Authorization) if not token or not token.startswith(Bearer ): return jsonify({error: 未授权}), 401 try: payload jwt.decode(token[7:], SECRET_KEY, algorithms[HS256]) user_id payload[user_id] except Exception: return jsonify({error: 无效令牌}), 401 file_record FileRecord.query.filter_by(idfile_id, user_iduser_id).first() if not file_record: return jsonify({error: 文件不存在或无权限}), 404 # 删除物理文件 file_path os.path.join(uploads, file_record.stored_name) if os.path.exists(file_path): os.remove(file_path) # 删除数据库记录 db.session.delete(file_record) db.session.commit() return jsonify({message: 删除成功}), 200注意send_from_directory的安全限制它会自动校验file_path是否在uploads/目录下防止路径遍历攻击如file_id被篡改为../../../etc/passwd。这是 Flask 内置防护课设必须用不能自己拼接open()路径。5. 避坑指南课设答辩前必须亲手验证的 5 个血泪现场课设最怕的不是功能没做完而是答辩时当场复现一个低级错误。以下是我在多个课设辅导中高频遇到的 5 类“答辩翻车点”每一条都附带现象、根因和可立即执行的验证方法。请务必在提交前逐条自查。5.1 现象上传大文件50MB时前端报 “Network Error” 或 “Failed to fetch”原因Nginx/Apache 反向代理如有或 Flask 内置服务器默认限制了请求体大小。Flask 默认MAX_CONTENT_LENGTH16MB超过即 413 错误但前端 Fetch 可能只报通用 Network Error。解决在app.py顶部添加配置app.config[MAX_CONTENT_LENGTH] 500 * 1024 * 1024 # 500MB验证用curl -X POST -F chunktest.bin http://localhost:5000/api/upload上传一个 100MB 的二进制文件观察是否返回 200。5.2 现象多个用户同时上传同名文件后上传者覆盖先上传者的文件原因stored_name生成逻辑未包含user_id导致不同用户生成相同哈希名。例如stored_name hashlib.md5(filename.encode()).hexdigest()忽略了用户上下文。解决严格按本文 3.3 节stored_name必须拼接user_id和时间戳哈希stored_name f{user_id}_{file_hash}_{safe_filename}验证用两个不同账号分别上传report.pdf检查uploads/目录下生成的文件名是否以123_和456_开头。5.3 现象删除文件后数据库记录消失但uploads/目录下文件仍在原因os.remove(file_path)执行前未检查os.path.exists(file_path)或file_path拼接错误如漏掉uploads/前缀导致remove()报FileNotFoundError但被静默吞掉。解决在delete_file函数中删除前加日志print(f[DEBUG] Attempting to delete: {file_path}) # 课设调试用 if os.path.exists(file_path): os.remove(file_path) print(f[DEBUG] Deleted: {file_path}) else: print(f[DEBUG] File not found: {file_path})验证删除后手动ls uploads/确认文件是否真被移除。5.4 现象登录后前端localStorage.getItem(token)为空导致所有接口 401原因登录接口/api/login返回的token是字符串但前端未正确提取。常见错误是response.json().token写成response.tokenFetch API 必须.json()。解决前端登录成功后明确写入const data await res.json(); localStorage.setItem(token, data.token); // 确保 data.token 存在验证打开浏览器开发者工具 → Application → Local Storage确认tokenkey 存在且值为长字符串。5.5 现象文件列表中时间显示为1970-01-01或乱码原因数据库upload_time存的是INTEGER时间戳但前端尝试用new Date(record.upload_time)解析——JavaScript 的Date构造函数需要毫秒级时间戳而 Pythontime.time()返回秒级。解决后端返回时转成毫秒或前端乘以 1000// 正确写法 const date new Date(record.upload_time * 1000);验证在list_files接口中console.log(record.upload_time)看是否为 10 位数字如1717023456再检查前端new Date(1717023456 * 1000)是否输出正确日期。注意以上每一条我都曾亲眼见过学生在答辩 PPT 翻到“系统架构图”时老师突然说“你上传个 100MB 文件试试”然后全场寂静。课设的价值不在“看起来多炫”而在“每一个边界 case 都亲手踩过、修过、讲得清”。6. 让课设脱颖而出的 3 个实战技巧不增工作量只换思维方式课设评分隐性规则之一老师会下意识对比“这个学生是否理解技术背后的约束”。以下三个技巧都不需要额外写几百行代码但能让你在答辩时自然流露工程思维让老师觉得“这孩子真懂”。6.1 把“上传进度条”从“伪进度”升级为“真进度”只改 3 行前端当前前端用for循环顺序上传进度 已上传块数 / 总块数看似合理实则是“伪进度”——如果第 3 块上传慢进度会卡在 2/10 长达 10 秒用户体验差也暴露你没考虑异步并发。真正的优化是并发上传 3 块用 Promise.allSettled 管理进度。修改uploadFile()函数中循环部分替换原 for 循环// 替换原 for 循环支持并发上传最多 3 块同时进行 const uploadPromises []; for (let i 0; i totalChunks; i) { const start i * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); const chunk file.slice(start, end); const formData new FormData(); formData.append(chunk, chunk); formData.append(filename, file.name); formData.append(chunk_index, i); formData.append(total_chunks, totalChunks); formData.append(file_size, file.size); uploadPromises.push( fetch(/api/upload, { method: POST, headers: { Authorization: Bearer ${localStorage.getItem(token)} }, body: formData }).then(res { if (!res.ok) throw new Error(块${i}上传失败); return { index: i, status: success }; }).catch(err ({ index: i, status: error, message: err.message })) ); } // 并发执行实时更新进度 let uploadedCount 0; const progressInterval setInterval(() { const successCount uploadPromises.filter(p p.status success).length; document.getElementById(progress).innerText 上传中${successCount}/${totalChunks} 块完成; }, 300); await Promise.allSettled(uploadPromises); clearInterval(progressInterval);为什么这 3 行改动值得讲它展示了你理解“HTTP 请求是网络 I/O应并发而非串行”Promise.allSettled保证即使某块失败其他块仍继续符合真实网络环境进度条不再卡顿是用户可感知的体验提升——课设不是 Demo而是最小可行产品MVP。6.2 用 SQLite 的 WAL 模式解决“多用户同时上传导致数据库锁死”课设常被忽略的性能点SQLite 默认是DELETE模式当一个用户正在上传写入FileRecord另一个用户查列表读FileRecord会被阻塞表现为“列表加载转圈”。解决方案是启用 WALWrite-Ahead Logging模式允许读写并发。在app.py初始化数据库后添加# 启用 WAL 模式提升读写并发能力 with app.app_context(): db.create_all() # 执行 PRAGMA 启用 WAL db.session.execute(text(PRAGMA journal_modeWAL)) db.session.commit()需要from sqlalchemy import text。WAL 模式下写操作写入日志文件读操作仍从主数据库读互不阻塞。课设中 2~3 人并发操作就足够验证效果。6.3 在requirements.txt里锁定版本让老师一键复现课设交付物不只是代码更是可复现的环境。很多学生交的requirements.txt是flask结果老师装的是 Flask 3.x而你的代码用的是 2.x 的url_for语法直接报错。正确做法是pip freeze requirements.txt然后手动删掉无关包如pipsetuptools保留核心依赖。最终requirements.txt应类似Flask2.3.3 Flask-SQLAlchemy3.0.5 PyJWT2.8.0 Werkzeug2.3.7答辩时这句话很加分“老师您只需要pip install -r requirements.txt然后python app.py所有功能都能立刻运行包括本文还有配套的精品资源点击获取