简介:本资源是一套完整的本科毕业设计项目——Web云存储硬盘系统的设计与实现,面向计算机类专业学生(如软件工程、计科、信息安全、大数据等)及初入职场的开发人员,聚焦于前后端协同、文件管理、用户权限与Web存储架构等核心实践问题,适用于课程设计、大作业、毕设参考及立项演示。压缩包共125个文件,涵盖33份Word文档(含开题报告、论文终稿、登记表、学分申请表等全流程材料)、23份Visio系统设计图(含概念模型.cdb/.cdm、数据库逻辑结构等)、20张PNG界面与流程图、10份PDF技术说明与规范,以及SQL建库脚本、Vue前端页面、PDM数据库模型等关键工程文件,整体大小为127.02MB。已有114人学习下载,内容经实测可运行,包含从需求分析、系统设计、数据库建模到前后端实现的完整闭环,特别适合希望掌握全栈开发流程、理解云存储业务逻辑与工程文档规范的学习者深度研习。
1. 为什么一个“课设级”的 Web 云存储硬盘系统,反而成了验证工程能力的试金石?
你手头正赶着期末大作业,题目写着“Web 云存储硬盘系统的设计与实现”——听起来像套壳网盘,实则是个微型分布式系统的全链路沙盒。它不追求亿级用户,但必须在单机或三节点内跑通用户鉴权 → 文件分块上传 → 元数据持久化 → 断点续传 → 前端可视化管理这五关;稍有疏忽,上传卡死、列表乱码、删除不生效、多用户并发写入元数据错乱……全是课设答辩现场的“公开处刑”高发区。这不是在堆 HTML+Node.js 就能交差的玩具项目,而是用最小成本暴露你对 HTTP 协议边界、文件 I/O 阻塞模型、数据库事务隔离、前端 Blob 处理、以及前后端时钟同步等真实工程细节的理解深度。适合计算机专业大三及以上学生:已有基础 Web 开发经验,但尚未在生产环境处理过文件流、大文件上传、或元数据一致性问题。本文不讲“云原生”“对象存储架构”,只聚焦课设可落地、老师能验收、答辩能讲清的完整闭环。
2. 从零搭起服务端骨架:选型不是炫技,是为课设减负
课设时间紧、资源少、部署环境受限(常只能跑在本地或学校虚拟机),服务端选型必须满足三个硬约束:启动快、依赖少、调试直观、文档友好。Python Flask + SQLite 组合在此场景下反而是更优解——不是因为它“高级”,而是因为:
- Flask 启动只需
flask run,无构建步骤,改完代码 Ctrl+S 刷新即见效果; - 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_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(120), nullable=False) # 文件元数据表:关键字段是 file_id(唯一标识)、user_id(归属)、real_name(原始名)、stored_name(存储名)、size、upload_time class FileRecord(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) real_name = db.Column(db.String(255), nullable=False) # 用户看到的文件名 stored_name = db.Column(db.String(255), nullable=False) # 服务器存储的唯一文件名(防重名) size = db.Column(db.Integer, nullable=False) upload_time = db.Column(db.Integer, default=lambda: int(time.time())) # 初始化数据库(首次运行自动建表) with app.app_context(): db.create_all()这段代码做了三件关键事:
- 用
secure_filename()防止路径遍历攻击(如上传../../etc/passwd); stored_name不直接用real_name,而是生成唯一哈希名(后文详述),避免同名覆盖;upload_time存时间戳整数而非 datetime 对象,SQLite 原生支持INTEGER,省去类型转换烦恼,前端展示时再转成可读格式。
2.2 实现用户注册与登录接口(JWT 简化版)
课设不需复杂 OAuth,用轻量 JWT(JSON 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(username=username).first(): return jsonify({'error': '用户名已存在'}), 409 # 密码哈希:课设用简单 sha256(勿用于真实系统!) password_hash = hashlib.sha256(password.encode()).hexdigest() user = User(username=username, password_hash=password_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(username=username).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(hours=24) }, SECRET_KEY, algorithm='HS256') return jsonify({'token': token}), 200参数说明与踩坑点:
jwt.encode()的algorithm='HS256'必须显式指定,否则新版 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> <head><title>Web 云存储硬盘</title></head> <body> <h2>上传文件</h2> <input type="file" id="fileInput" /> <button onclick="uploadFile()">开始上传</button> <div id="progress">等待选择文件...</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(`上传第${i+1}块失败`); 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_ok=True) @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_ok=True) # 保存当前块:命名格式 chunk_0001 chunk_path = os.path.join(temp_dir, f"chunk_{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_ok=True) 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, f"chunk_{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_id=user_id, real_name=filename, stored_name=stored_name, size=actual_size, upload_time=int(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 # 分页参数:默认 page=1, per_page=20 page = request.args.get('page', 1, type=int) per_page = request.args.get('per_page', 20, type=int) # 查询该用户所有文件,按上传时间倒序 pagination = FileRecord.query.filter_by(user_id=user_id)\ .order_by(FileRecord.upload_time.desc())\ .paginate(page=page, per_page=per_page, error_out=False) 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()而非limit+offset?
Flask-SQLAlchemy 的paginate()自动处理边界情况(如page=1000时返回空列表而非报错),且error_out=False让课设更健壮——用户乱点页码不会导致 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?page=1&per_page=50', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; if (data.files.length === 0) { fileList.innerHTML = '<tr><td colspan="4">暂无文件</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 onclick="downloadFile(${file.id})">下载</button> <button onclick="deleteFile(${file.id})" style="color: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; filename="xxx",部分浏览器可能忽略该属性,此时靠后端响应头兜底;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(id=file_id, user_id=user_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_attachment=True, download_name=file_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(id=file_id, user_id=user_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_LENGTH=16MB,超过即 413 错误,但前端 Fetch 可能只报通用 Network Error。 - 解决:在
app.py顶部添加配置:app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB - 验证:用
curl -X POST -F "chunk=@test.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.token(Fetch 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)会被阻塞,表现为“列表加载转圈”。解决方案是启用 WAL(Write-Ahead Logging)模式,允许读写并发。
在app.py初始化数据库后,添加:
# 启用 WAL 模式,提升读写并发能力 with app.app_context(): db.create_all() # 执行 PRAGMA 启用 WAL db.session.execute(text("PRAGMA journal_mode=WAL")) 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应类似:
Flask==2.3.3 Flask-SQLAlchemy==3.0.5 PyJWT==2.8.0 Werkzeug==2.3.7答辩时这句话很加分:“老师,您只需要pip install -r requirements.txt,然后python app.py,所有功能都能立刻运行,包括
本文还有配套的精品资源,点击获取