做一个学生作业管理系统,听起来好像是个老生常谈的课程设计题,但真要把Python后端、Vue前端、MySQL数据库这三块完整串起来,做到功能闭环、代码能跑、文档齐全,里面的细节远比想象中多。这个项目我前前后后从功能拆解走到最终部署,踩了不少坑,也沉淀了不少可复用的套路。这篇文章不是贴一段代码就完事,而是把整个系统的设计思路、实际开发顺序、核心实现代码、联调部署经验全部捋一遍——如果你正好在筹备课设、毕设,或者想做一个真正能用的作业管理小系统,这篇内容可以做你的完整参考。
1. 项目概述与需求拆解
1.1 这个系统到底在解决什么问题
先说说做作业管理系统的真实需求背景。学校里的作业收发场景,大部分时候是老师在群里发个文档,学生写完拍了照提交,课代表汇总成Excel,老师再一份份下载批阅。这个过程至少有这么几个痛点:作业通知容易被聊天记录淹没,提交格式五花八门,催收要挨个私聊,成绩统计靠手工录入效率低还容易错。
“学生作业管理系统”解决的问题就是把这些线下流程搬到系统里,教师端能布置作业、设置截止时间、在线批改打分;学生端能查看作业列表、提交文件、查看成绩与评语;管理员还能管理用户和基础数据。技术栈上我选的是Python + Vue + MySQL,源码和数据库脚本外加接口文档一起交付,既能演示完整业务闭环,也符合目前主流的课设和毕设技术组合。
提示:这个项目的完整度足够支撑课程设计、毕业设计的演示答辩,甚至小范围改造后用于培训机构、企业内部培训班的作业管理。
1.2 为什么是 Python + Vue 这套组合
选型这件事,很多同学问我为什么不是Java + LayUI,也不是PHP,更不是纯前后端不分离。我综合考量了三方面:
- Python生态非常适合这种业务逻辑清晰的系统。Flask框架轻量,路由写起来直观,配合SQLAlchemy操作MySQL非常顺手,尤其适合快速开发和教学演示。而且Python社区关于文件上传、用户认证、Excel导出的现成方案最多,遇到问题基本搜索就能解决。
- Vue对前端交互的支撑很到位。作业管理涉及列表渲染、表单提交、状态切换这些常见场景,Vue的双向绑定和组件复用让代码量大幅减少。Element UI组件库让界面在短时间内就能做得规整,不至于像传统模板渲染那样页面结构混乱。
- 前后端分离的架构更接近真实企业项目。前端用axios调后端接口,后端只负责返回JSON数据,两者通过API契约联调——这是现在主流开发模式,写在简历上比单体JSP项目有说服力得多。
我自己在实际开发时会再叠加一个理由:Python和Vue的调试体验好。Python端可以用Flask的debug模式看接口异常堆栈,Vue端浏览器Vue Devtools插件直接查看组件数据和Vuex状态,定位问题比黑盒式开发快太多。这套组合看着朴素,但胜在稳定、可控、资料多,对单人或小组开发非常友好。
2. 数据库设计与核心模型
2.1 从业务反推数据表设计
数据库是整个系统能不能跑通的基座。作业管理系统看着功能简单,表拆不好后期改起来特别痛苦。我这里不走教科书式的“先画ER图再建表”流程,而是从实际业务操作反推需要哪些表、每个表要存什么。
先梳理角色和核心动作:学生要浏览作业、提交作业、查看成绩;老师要布置作业、批改作业、查看提交情况;系统要记录用户、作业、提交记录、成绩这四类核心数据。所以最基础的表是用户表(user)、作业表(homework)、提交表(submission)。
这还没完,等你仔细想“老师布置作业时能不能附参考文档”“学生能否多次提交覆盖文件”“批改时要不要写评语”这些细节,表结构就得继续迭代。我的建议是先把核心四张表建出来,再根据功能清单逐步补字段,比一开始追求一步到位要稳妥。
2.2 四张核心数据表的结构定义
我的数据库名是student_homework_db,字符集用utf8mb4,排序规则用utf8mb4_general_ci。下面是一份可以直接执行的建表SQL,四个核心表都包含在内:
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(255) NOT NULL COMMENT '密码(加密存储)', `role` tinyint(1) NOT NULL DEFAULT '2' COMMENT '角色:1管理员 2学生 3教师', `real_name` varchar(50) DEFAULT NULL COMMENT '姓名', `class_name` varchar(50) DEFAULT NULL COMMENT '班级', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;-- 作业表 CREATE TABLE `homework` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL COMMENT '作业标题', `content` text COMMENT '作业内容描述', `attachment` varchar(255) DEFAULT NULL COMMENT '教师附件路径', `deadline` datetime NOT NULL COMMENT '截止时间', `teacher_id` int(11) NOT NULL COMMENT '布置教师ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;-- 提交表 CREATE TABLE `submission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `homework_id` int(11) NOT NULL COMMENT '作业ID', `student_id` int(11) NOT NULL COMMENT '学生ID', `content` text COMMENT '学生提交说明', `file_url` varchar(255) DEFAULT NULL COMMENT '提交文件路径', `submit_time` datetime DEFAULT CURRENT_TIMESTAMP, `score` int(11) DEFAULT NULL COMMENT '成绩', `comment` varchar(255) DEFAULT NULL COMMENT '教师评语', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0未提交 1已提交 2已批改', PRIMARY KEY (`id`), UNIQUE KEY `uk_homework_student` (`homework_id`, `student_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;三个表加一个管理员中间表,基本覆盖了系统全部数据需求。submission表的唯一键uk_homework_student非常关键,它保证了一个学生对同一份作业只能有一条提交记录,后续“再次提交”就变成更新这条记录,而不是新增记录,避免数据冗余。
2.3 数据库设计的几个避坑细节
表结构敲定之前,有几个细节我反复调试过,先帮你避雷:
第一个坑:时间字段用datetime还是timestamp。学生端要显示“距截止还有X天”,后端要判断“是否超时”,如果用timestamp,时区问题在部署到不同环境的服务器时容易出乱子。统一用datetime,Python端全部用datetime.datetime.now()写入,配合MySQL的CURRENT_TIMESTAMP默认值最省心。
第二个坑:密码字段长度。如果用户密码用哈希存储(我用的werkzeug库的generate_password_hash),生成结果接近60个字符。密码字段varchar长度如果只写50,注册新用户时直接报Data too long。稳妥起见密码字段直接给255。
第三个坑:状态字段用整数枚举,不要用字符串。status字段我用0/1/2表示未提交/已提交/已批改,前端再映射成对应的中文标签。用整数参与排序和统计都方便,字符串看着直观但在代码里比较时容易因为大小写不一致出bug。
第四个坑:文件路径只存相对路径。作业附件、提交文件都不要在数据库里存完整绝对路径,更不要存base64内容。数据库只存类似uploads/2025/homework/3/xxx.docx的相对路径,文件实体保存在服务器磁盘目录。这样数据库体积可控,后续迁移服务器也只要整体拷贝上传目录。
提示:设计文档里一定要附上表关系说明——一个教师可布置多份作业,一份作业可被多个学生提交,一份提交记录唯一对应一份作业和一个学生,这是一对多和多对一的标准关系。
3. 后端实现:Python接口与业务逻辑
3.1 后端项目结构与初始化
后端我用的Flask + SQLAlchemy + PyMySQL的组合。为什么不选Django?因为Django对这类小项目来说太重,自带Admin和ORM虽然强大,但课设演示时面试官问“你项目里的模块怎么分的”,Flask自己手写的蓝本结构反而更好解释。
项目的后端目录结构如下:
backend/ ├── app.py # 应用入口,注册蓝图 ├── config.py # 配置文件(数据库连接、上传路径等) ├── models.py # SQLAlchemy模型定义 ├── utils/ │ ├── jwt_token.py # JWT令牌生成与校验 │ └── response.py # 统一返回格式封装 ├── api/ │ ├── auth.py # 登录注册接口 │ ├── homework.py # 作业相关接口(布置/列表/详情) │ ├── submission.py # 提交相关接口(提交/批改) │ └── upload.py # 文件上传接口 └── uploads/ # 上传文件存放目录入口文件app.py的写法比较固定,我直接贴关键部分:
from flask import Flask from flask_cors import CORS from api.auth import auth_bp from api.homework import homework_bp from api.submission import submission_bp app = Flask(__name__) app.config.from_object('config.Config') CORS(app) # 允许前端跨域调用 app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(homework_bp, url_prefix='/api/homework') app.register_blueprint(submission_bp, url_prefix='/api/submission') if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)配置文件里最核心的就是数据库连接串和上传相关参数:
class Config: SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:123456@localhost:3306/student_homework_db?charset=utf8mb4' SQLALCHEMY_TRACK_MODIFICATIONS = False UPLOAD_FOLDER = './uploads/' MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 限制上传文件最大16MB JWT_SECRET_KEY = 'your-secret-key-change-in-production'注意连接串里我加了charset=utf8mb4,这个参数不加,中文写入MySQL容易出现Incorrect string value乱码错误,属于新手最容易漏掉的一环。
3.2 用户认证与权限控制
作业管理系统的认证逻辑不复杂:登录成功后后端返回JWT令牌,前端把令牌存到localStorage,后续请求在请求头带上Authorization: Bearer <token>,后端写一个装饰器统一校验。
JWT工具类我用的是pyjwt库,核心代码就两个函数:
import jwt import datetime JWT_SECRET = 'your-secret-key' def generate_token(user_id, role): payload = { 'user_id': user_id, 'role': role, 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=24) } return jwt.encode(payload, JWT_SECRET, algorithm='HS256') def verify_token(token): try: payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256']) return payload # 返回 {'user_id': xx, 'role': xx} except jwt.ExpiredSignatureError: return None # token过期 except jwt.InvalidTokenError: return None # token无效实现权限控制的装饰器,核心逻辑是“取token -> 解析用户身份 -> 判断角色是否满足要求 -> 放行或拒绝”:
from functools import wraps from flask import request, jsonify def login_required(f): @wraps(f) def wrapper(*args, **kwargs): auth_header = request.headers.get('Authorization') if not auth_header or not auth_header.startswith('Bearer '): return jsonify({'code': 401, 'msg': '未登录或token缺失'}), 401 token = auth_header.split(' ')[1] payload = verify_token(token) if not payload: return jsonify({'code': 401, 'msg': '登录已过期,请重新登录'}), 401 request.user_id = payload['user_id'] request.user_role = payload['role'] return f(*args, **kwargs) return wrapper def teacher_required(f): @wraps(f) def wrapper(*args, **kwargs): if getattr(request, 'user_role', None) not in (1, 3): return jsonify({'code': 403, 'msg': '仅教师或管理员可操作'}), 403 return f(*args, **kwargs) return wrapper这里有一个关于JWT的要点:JWT是无状态的,服务端不存Session,所以token的有效期就是设置24小时。对于课设来说够用,但如果要做得更严谨,token刷新机制可以加入,不过那不在这篇文章的范围内。
3.3 作业提交与文件上传
文件上传是整个系统里最“实战”的部分,也是答辩时容易被追问细节的模块。学生提交作业,前端用FormData传文件字段,后端接收后做三件事:安全校验、重命名保存、写入数据库。
核心实现如下:
import os import uuid from flask import request, jsonify from werkzeug.utils import secure_filename ALLOWED_EXTENSIONS = {'doc', 'docx', 'pdf', 'zip', 'rar', 'jpg', 'png', 'txt'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app_upload.route('/upload', methods=['POST']) @login_required def upload_file(): file = request.files.get('file') if not file or file.filename == '': return jsonify({'code': 400, 'msg': '未选择文件'}), 400 if not allowed_file(file.filename): return jsonify({'code': 400, 'msg': '不支持的文件类型'}), 400 # 用uuid重命名文件,保留原扩展名,避免文件名冲突和中文文件名问题 ext = file.filename.rsplit('.', 1)[1].lower() new_filename = str(uuid.uuid4()).replace('-', '') + '.' + ext save_dir = os.path.join(app.config['UPLOAD_FOLDER'], str(request.user_id)) os.makedirs(save_dir, exist_ok=True) save_path = os.path.join(save_dir, new_filename) file.save(save_path) relative_path = f'uploads/{request.user_id}/{new_filename}' return jsonify({'code': 0, 'data': {'file_url': relative_path}, 'msg': '上传成功'})学生提交作业的接口,则是先检查是否已存在提交记录,存在就更新文件路径和内容,不存在就新建记录:
@app_submission.route('/submit', methods=['POST']) @login_required def submit_homework(): homework_id = request.json.get('homework_id') content = request.json.get('content', '') file_url = request.json.get('file_url', '') # 检查作业是否存在 homework = Homework.query.get(homework_id) if not homework: return jsonify({'code': 404, 'msg': '作业不存在'}), 404 # 检查是否已过截止时间 if datetime.datetime.now() > homework.deadline: return jsonify({'code': 400, 'msg': '已过截止时间,无法提交'}), 400 submission = Submission.query.filter_by( homework_id=homework_id, student_id=request.user_id ).first() if submission: # 覆盖旧文件(如果上传了新文件) submission.content = content submission.file_url = file_url submission.submit_time = datetime.datetime.now() else: submission = Submission( homework_id=homework_id, student_id=request.user_id, content=content, file_url=file_url, submit_time=datetime.datetime.now() ) db.session.add(submission) db.session.commit() return jsonify({'code': 0, 'msg': '提交成功'})教师批改接口和提交接口类似,核心就是更新score、comment和status字段。整个后端没有高超的技术,但每一块都是业务实际需要的,接口设计思路是:先验证“用户有没有权限”,再验证“数据合不合法”,然后才做业务操作。
4. 前端实现:Vue页面与交互
4.1 前端工程结构与路由设计
前端我用的Vue 2 + Vue Router + Vuex + Element UI + Axios的经典组合。Vue 2的资料最多,Element UI的组件样式也更能满足师生对“像个管理系统”的视觉期待。
前端项目目录结构如下:
frontend/ ├── public/ ├── src/ │ ├── api/ │ │ ├── request.js # axios封装 │ │ ├── auth.js # 登录接口 │ │ ├── homework.js # 作业接口 │ │ └── submission.js # 提交接口 │ ├── router/ │ │ └── index.js # 路由配置 │ ├── store/ │ │ └── index.js # Vuex状态管理 │ ├── views/ │ │ ├── Login.vue # 登录页 │ │ ├── Dashboard.vue # 首页看板 │ │ ├── HomeworkList.vue # 作业列表 │ │ ├── HomeworkDetail.vue # 作业详情与提交 │ │ ├── TeacherHomework.vue # 教师布置/管理作业 │ │ └── GradeManage.vue # 教师批改/成绩管理 │ ├── App.vue │ └── main.js路由需要做的一件事是“登录守卫”,未登录用户访问任何页面都跳转登录页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else if (to.path === '/login' && token) { next('/') } else { next() } })4.2 核心页面实现:作业列表与提交
作业列表页是学生端的主界面,要展示的字段包括作业标题、截止时间、当前状态(未提交/已提交/已批改)、是否逾期。状态判断如果全放前端做,逻辑会很绕,我是在后端返回数据时就直接算好状态,前端只管渲染。
后端返回的作业列表,每一条数据结构长这样:
{ "id": 3, "title": "第三次作业:Vue组件通信", "content": "实现父子组件通信的三种方式", "deadline": "2025-03-20 23:59:59", "status": 1, "score": null, "submit_time": "2025-03-18 10:30:00", "file_url": "uploads/5/ab12cd34ef5678.docx" }前端作业列表的模板核心部分:
<el-card v-for="item in homeworkList" :key="item.id"> <div class="homework-header"> <span class="title">{{ item.title }}</span> <el-tag :type="statusTagMap[item.status]">{{ statusTextMap[item.status] }}</el-tag> </div> <p class="content">{{ item.content }}</p> <div class="footer"> <span>截止时间:{{ item.deadline }}</span> <el-button type="primary" size="small" @click="goDetail(item.id)">查看详情</el-button> </div> </el-card>提交作业的逻辑在作业详情页里,学生可以填写文字说明、上传文件。这里我建议做一层“是否已过截止时间”的按钮禁用:后端已经做了权限校验,但前端提前禁用按钮,用户体验会好很多,不至于提交时被后端拦一下才知道超时。
![作业详情页示意]
提交组件示例代码:
<el-form> <el-form-item label="提交说明"> <el-input type="textarea" v-model="submitContent" placeholder="可以在这里写作业思路或者想对老师说的话"></el-input> </el-form-item> <el-form-item label="上传文件"> <el-upload :auto-upload="false" :on-change="handleFileChange" :limit="1"> <el-button size="small">选择文件</el-button> </el-upload> </el-form-item> <el-button type="primary" :disabled="isExpired" @click="handleSubmit">提交作业</el-button> </el-form>这里有个细节值得展开:el-upload组件如果直接配置action属性会导致组件自动发请求,不走我们自己的上传流程。最省心的做法是把auto-upload设为false,用on-change钩子拿到文件对象,再由axios用FormData上传,这样上传进度、错误处理都能统一收口,避免和Element UI默认行为打架。
4.3 axios请求封装与跨域配置
axios封装是前端工程化的第一步,我把所有请求统一拦截,做到三个统一:统一注入token、统一处理错误码、统一解析返回结构。核心代码看这儿:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API || '/api', timeout: 10000 }) // 请求拦截器:注入token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { Message.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default service开发环境下的跨域问题,我在Vue项目的vue.config.js里配置了代理:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } }配置代理后,前端请求的baseURL就直接写/api,开发联调阶段浏览器发出的请求经过webpack-dev-server转发到后端5000端口,完美绕过跨域限制。生产部署时nginx做同样的反向代理即可,后面部署章节会再讲。
5. 联调、打包与部署
5.1 前后端联调的关键点
前后端分离开发的坑,大部分不在单端,而在联调。我自己踩过三个典型的联调问题,先列出来提醒你:
字段命名不一致。后端习惯用下划线风格file_url,前端如果直接item.fileUrl取数,结果就是undefined。我这里约定统一用后端返回的下划线字段,前端不做过多的驼峰转换,取数的时候保持原样,省去转换层。
时间格式的处理。MySQL datetime通过Flask返回JSON后,格式是"2025-03-20T23:59:59"这种ISO格式(SQLAlchemy默认序列化行为),前端直接展示会带个T,不美观。我在后端序列化时统一处理成%Y-%m-%d %H:%M:%S格式,前端拿到就直接展示或交给dayjs做计算,不重复处理格式。
状态码语义不统一。我曾经把业务错误用HTTP状态码表达,比如“超时提交”返回400,“未登录”返回401,结果axios拦截器一看到非2xx就弹错误提示,前端自己的分支判断反而拿不到body里更详细的msg。后来约定:HTTP状态码只保留用于401这种真正需要特殊处理的场景,其他所有业务错误统一返回200 +code: 非0,前端代码更统一,也更好维护。
5.2 从开发到可交付的打包部署方案
课设和毕设场景,部署要求一般是“能跑起来”就行,但用户体验不能太拉胯。我给两套方案,按需选择。
方案一:最简本地部署。后端python app.py启动,前端npm run serve启动,浏览器访问前端地址。这套方案适合开发调试、现场答辩。前提是本机装了Python和Node环境,MySQL服务在跑、数据库已导入。
方案二:生产模式部署。前端执行npm run build生成dist静态文件,后端Flask配置静态文件目录指向前端dist目录,采用“前后端同源部署”:
from flask import send_from_directory # 在app.py中添加:生产环境由Flask托管前端静态文件 @app.route('/') def index(): return send_from_directory('../frontend/dist', 'index.html') @app.route('/<path:path>') def static_files(path): return send_from_directory('../frontend/dist', path)但要注意,Vue Router如果启用history模式,刷新页面会向服务端请求某个前端路由路径(比如/homework/3),后端必须做一行兜底处理——凡是不以/api开头的路径都返回index.html:
@app.errorhandler(404) def not_found(e): if request.path.startswith('/api'): return jsonify({'code': 404, 'msg': '接口不存在'}), 404 return send_from_directory('../frontend/dist', 'index.html')不写这行兜底,history模式下刷新页面就是404白屏。这个坑很多人要踩到。如果不想处理,最简单的方式是前端路由用hash模式,URL带上#符号,刷新时不会向服务端请求真实路径。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
每次做完一个项目,我都会把开发和调试中遇到的高频问题整理成一张速查表。这次的作业管理系统,最常见的问题集中在下面这几个:
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 前端登录接口403 | 登录时密码哈希验证出错,或token密钥不一致 | 确认注册和登录都用同一个check_password_hash,JWT_SECRET保持一致 |
| 上传文件后数据库路径正常但前端打不开 | 前端拼接路径少了/api前缀,或Flask没配静态目录 | 确认文件访问URL与静态资源映射一致,最好统一走上传接口返回完整URL |
| 中文数据写入MySQL报Incorrect string value | 连接串缺charset=utf8mb4 | 数据库连接URL增加?charset=utf8mb4,建库时确认字符集为utf8mb4 |
| 提交作业显示“已过截止时间”但本地时间还没到 | 服务器时区不是东八区 | 后端设置os.environ['TZ'] = 'Asia/Shanghai',或Windows下用time.localtime()生成时间 |
| Vue页面空白控制台报触发Vue警告 | 组件注册名和导入名不一致,或路由懒加载路径写错 | 清浏览器缓存和依赖缓存,检查main.js注册组件名 |
| npm run dev启动报端口占用 | 8080端口被占用 | 两种处理,释放端口占用或者vue.config.js修改devServer端口 |
6.2 几个特别容易忽略的坑
再分享三个我实际开发中最容易忽略、但又特别影响体验的细节。
第一个坑:文件上传后,旧文件没清理。学生重新提交作业时,上传了新文件,我本人在第一版代码里只更新了提交记录的file_url,旧文件一直留在服务器磁盘上。时间长了uploads目录全是垃圾文件。修正的方法很简单,在更新file_url之前把原文件从磁盘删掉,用os.path.exists判断再加os.remove删除。
第二个坑:Element UI表格数据量稍大就卡。一个班级50个学生、一学期20次作业,交作业列表就有1000行。如果用普通el-table不加分页,前端渲染加DOM操作会明显卡顿。我最后在成绩管理页面加了后端分页和关键词搜索,接口接受page、size、keyword三个参数,前端切换页码时重新请求数据,性能问题直接消失。
第三个坑:测试账号的初始化。数据库脚本里我预置了几个测试账号:管理员admin/123456、教师teacher01/123456、学生student01/123456,同时预置了3条作业和若干提交记录。没有预置数据的新手,登录系统后只有一个空壳框架,演示效果非常差。文档里一定要写清楚初始账号,答辩演示前先确认数据都在。
提示:如果要在多台电脑之间同步数据库,MD5文件校验不如直接使用导出的SQL脚本,字段、约束、预置数据一步到位。发布版本时在文档中附上
数据库初始化脚本.sql,接收方只需执行一次,便能获得系统的全部初始数据。
最后说点实际的
这个项目做完之后,我最深刻的体会是:课设、毕设乃至小型实战项目,真正拉开差距的往往不是用了多高深的技术,而是“需求拆得够不够细、边界想得够不够全、模块之间配不配合”。作业管理系统的核心逻辑不复杂,但把文件上传、权限控制、状态流转、前后端联调这些常见环节全部走通一遍,你对Web开发的整体理解会有一个明显的提升。
如果你打算在此基础上扩展,我建议优先做几个方向:一是增加“班级”实体,让作业可以按班级布置而不是全校统一;二是加入截止后的迟交标记,成绩统计页面增加导出Excel功能,这两块对教务场景的实用性提升非常明显,也是答辩时很亮眼的增值点。
最后再分享一个小技巧:写接口文档不要拖到最后一次性补,每写完一个接口就顺手记录到Markdown文件里,包含请求方法、路径、参数、返回示例。等整个项目写完,文档基本也就成型了,不至于最后对着代码反推文档累到崩溃。项目有源码、有数据库脚本、有文档,再配合这篇实操笔记,把它跑起来完全可以不踩我踩过的那些坑。