先说个背景。做过课程设计或者带过教学项目的人应该都知道,学生作业管理这事儿看着不起眼,真正落地的时候特别磨人。收上来的文件格式千奇百怪,命名规范形同虚设,要么是“新建文档(2)(3)(最终版).docx”,要么是压缩包套压缩包,更别提老师那边还要一个个下载、批改、登记成绩,最后再手动汇总。这套“基于Python的学生作业管理系统”,本质上就是把这些重复劳动压缩成几个按钮的事。
这套系统的定位很明确:学生端可以看作业、提交作业、查成绩;教师端可以发布作业、管理学生、在线批改和打分;管理员端负责课程与账号的统筹管理。它不追求花哨的功能,核心目标只有一个——把作业管理的流程理顺。文章里我会把从需求分析、技术选型、数据库设计到核心功能实现的过程完整讲一遍,顺带把开发中踩过的坑和排查思路一并分享出来,适合正在做课程设计、毕业设计,或者想快速搭建一个Web项目练手的读者参考。
1. 项目设计与需求拆解
1.1 需求从哪来:先搞清楚谁在用、用在哪
开发之前我习惯先理一遍角色和使用场景。这个系统的使用场景非常典型:某高校的计算机课程,老师每周布置一次编程作业,学生提交代码文件和运行截图,老师需要逐一查看、评分、反馈。
在这个场景下,用户分为三类:
- 学生:查看已发布的作业列表,提交作业文件,查看批改结果和分数,修改已提交的内容。
- 教师:创建作业任务,设置截止时间,查看学生提交情况,下载作业文件,在线填写评语和分数,导出统计数据。
- 管理员:管理教师和学生账号,维护课程信息,处理账号异常。
这三类角色对应的权限不互通。学生不能看到其他学生的作业内容和分数,教师只能看到自己课程范围内的数据,管理员不参与具体的作业评审。所以权限设计是系统开发前的第一优先级。
1.2 功能模块划分:不贪多,但求闭环
我把系统拆成了四个功能域:
用户认证域:注册、登录、密码加密存储、会话管理。这个域是所有功能的前置条件,做不好后面全白搭。
作业管理域:教师的作业发布、截止时间设置、作业内容编辑;学生的作业列表查看、详情浏览、文件上传。
批改评分域:教师查看单个作业的全部提交记录,给每个学生打分、写评语;学生查看自己的评分和教师评语。
数据统计域:按班级、按作业维度统计提交率、平均分、分布区间,支持导出。这个模块看着简单,但实际上是对教师最有价值的部分。
一开始我也想过度设计,比如加上消息推送、在线聊天、自动查重这些功能。但冷静下来一想,任何一个功能都会引入额外复杂度。查重意味着要接外部接口或者自研相似度算法,消息推送意味着额外的服务依赖。我后来只保留了核心闭环,其余功能留到了扩展阶段。这个取舍很关键,正因为控制了范围,系统才能在短时间内完成开发和调试。
1.3 设计方案的取舍:B/S架构为什么更合适
有过几种方案可以考虑:
- C/S桌面程序(如PyQt + SQLite):开发简单,但老师要一个个安装客户端,学生也要安装,部署成本直接劝退。
- B/S Web应用(浏览器访问):老师只要发一个链接,学生打开浏览器就能用,完全不依赖客户端环境,这才是学校场景的正解。
所以最终选择B/S架构。后端用Python提供API服务,前端用HTML + CSS + JavaScript实现页面交互,数据库用MySQL存储结构化数据。整个系统部署在一台普通配置的服务器上就能跑,学生用手机、电脑、平板都能访问。
提示:B/S架构并不是技术最炫的方案,但它是教学场景中部署成本最低、触达用户最广的方案。选型首先要看使用者的真实环境,而不是技术栈的流行度。
2. 技术栈选择与环境准备
2.1 为什么选Flask
主流的Python Web框架就二选一:Django和Flask。Django功能全面,自带Admin后台、ORM、认证体系,开箱即用。但问题也出在“全面”上,框架把很多结构都定死了,初学者改起来容易迷路。Flask则轻量灵活,核心只负责路由和请求处理,其他能力靠扩展组件组合进去。
我最后选了Flask,原因是这套系统的业务逻辑并复杂,用Django有点杀鸡用牛刀,Flask能把每个环节的控制权交到我手里,遇到问题排查起来路径更短。而且Flask的上下文机制相对简单,接口和逻辑拆解更直观。
具体版本组合是这样的:
- Python 3.9+
- Flask 2.x
- PyMySQL(操作MySQL)
- Werkzeug(Flask自带的WSGI工具库,用于密码哈希和文件处理安全性高一点)
- 前端使用原生HTML + CSS + JavaScript,配合一个轻量的组件库快速搭界面
为什么不用Django REST Framework来写API?因为系统内部页面是服务端渲染为主,Ajax局部刷新为辅,严格来说并不需要一个重量级的API序列化层。原生写路由和响应,反而更容易被看懂、改明白。
2.2 环境搭建实操记录
如果你是新手,我建议按下面步骤把环境准备完整再开工:
- 安装Python 3.9以上版本,装完验证一下版本号。
- 创建虚拟环境,这一步务必做,避免依赖冲突。
- 安装依赖包。
依赖安装我用了一个requirements.txt来统一管理:
Flask==2.2.3 PyMySQL==1.0.2 cryptography==39.0.1安装命令:
pip install -r requirements.txtcryptography是为了支持MySQL8的caching_sha2_password认证方式,少了它连接数据库会报认证错误。这个问题后面排查章节我会详细讲。
数据库这边我用MySQL 8.0,创建独立数据库和账号,生产环境务必杜绝用root连接应用,这既是安全习惯,也是为了避免误操作。
CREATE DATABASE homework_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'hw_user'@'localhost' IDENTIFIED BY '这里填强密码'; GRANT ALL PRIVILEGES ON homework_system.* TO 'hw_user'@'localhost'; FLUSH PRIVILEGES;数据库字符集用utf8mb4,而不是老的utf8。原因后面也会提到,这跟中文乱码直接相关。
3. 数据库设计与核心表结构
3.1 表结构设计思路
先理一下实体关系:
- 一个用户有角色,角色分为学生、教师、管理员。
- 一个教师可以负责多门课程,一个学生可以选修多门课程,所以课程和用户是多对多关系。
- 一门课程下可以布置多个作业,一个作业下每个学生只能提交一次记录(但允许修改,覆盖式更新)。
- 一次提交对应一个评分记录(可空,教师未批改时为空)。
基于这个关系,我设计了五张核心表:用户表、课程表、课程选课关联表、作业表、提交记录表。
3.2 核心表字段详解
用户表(sys_user)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 用户ID |
| username | VARCHAR(50) 唯一 | 登录账号 |
| password_hash | VARCHAR(255) | 密码哈希值,绝对不存明文 |
| real_name | VARCHAR(50) | 姓名 |
| role | TINYINT | 1学生 2教师 3管理员 |
| created_at | DATETIME | 创建时间 |
密码这块多说一句。开发时我用的是werkzeug.security里自带的generate_password_hash函数,默认用pbkdf2算法加盐哈希。不用MD5和SHA1的原因很简单,这两种算法已经被证实可以快速碰撞,存用户密码相当于没锁门。
课程表(course)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 课程ID |
| course_name | VARCHAR(100) | 课程名称 |
| course_code | VARCHAR(30) | 课程编号 |
| teacher_id | INT | 负责教师ID |
| created_at | DATETIME | 创建时间 |
选课关联表(course_student)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 关联ID |
| course_id | INT | 课程ID |
| student_id | INT | 学生ID |
为什么需要关联表?一开始我的确考虑过在课程表里存一个“学生ID组”的文本字段,用逗号拼接。这个方案在数据量小的时候看着没什么问题,但只要涉及统计“某课程有多少学生”或者“某学生选了几门课”,就得做字符串切割,性能和可靠性都很差。第三范式的关系表虽然多了一次查询,但语义清楚、扩展方便,后续加退课功能也简单。
作业表(homework)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 作业ID |
| course_id | INT | 所属课程ID |
| title | VARCHAR(100) | 作业标题 |
| content | TEXT | 作业要求描述 |
| deadline | DATETIME | 截止时间 |
| created_at | DATETIME | 发布时间 |
这里注意一个细节:每天作业除了标题,必须有详细的作业要求描述,纯标题式的作业发布会导致学生反复问“怎么做”“格式是什么”。所以content字段我用TEXT类型,允许较长的富文本内容。
提交记录表(submission)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INT 主键自增 | 提交ID |
| homework_id | INT | 作业ID |
| student_id | INT | 学生ID |
| file_path | VARCHAR(255) | 存储路径 |
| original_filename | VARCHAR(255) | 原始文件名 |
| submit_time | DATETIME | 提交时间 |
| score | DECIMAL(5,2) | 分数,可空 |
| comment | TEXT | 教师评语 |
| status | TINYINT | 1已提交 2已批改 |
为了防止同一个学生对同一个作业产生多条记录,我对homework_id和student_id加了联合唯一约束。也就是说,同一个学生针对同一份作业最多只有一条提交记录,后来的提交会覆盖之前的,但file_path会重命名,保留历史文件到备份目录。
注意:数据库里学生ID和作业ID的外键约束我没做强约束(物理外键),而是用逻辑外键维护。主要原因是为了防止并发插入时外键锁带来的性能损耗,同时应用层做了足够的数据校验。中小型系统这样处理没问题,但如果并发量上来了,建议还是把物理外键加上,数据一致性更有保障。
4. 后端核心功能实现与难点拆解
4.1 登录认证与会话管理
用户登录后需要维持登录状态,我用Flask自带的session来管理登录态,配合@login_required装饰器保护受限页面。逻辑上是这样做的:
from flask import session, redirect, url_for from functools import wraps def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('auth.login', next=request.url)) return f(*args, **kwargs) return decorated_function def role_required(*roles): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('auth.login')) if session.get('role') not in roles: return "无权限访问", 403 return f(*args, **kwargs) return decorated_function return decorator使用方式很直接。比如教师发布作业的接口:
@app.route('/teacher/homework/create', methods=['GET', 'POST']) @login_required @role_required(2) def create_homework(): # 只有教师角色能够进入 ...角色控制通过装饰器统一收敛,页面路由拿到用户角色后还能进一步做细粒度的按钮级控制,比如学生页面不渲染评分表格。
session配置上需要注意一个安全点:Flask默认session是签名但不加密的cookie,内容用户是可读的。所以session里只存user_id和role,不要存密码、真实姓名这类敏感信息。会话有效期默认31天,对校园系统来说够用了,如果想改短一点,可以配置PERMANENT_SESSION_LIFETIME。
4.2 文件上传的坑与正确姿势
作业提交模块是文件上传的重灾区,踩坑主体。先看核心实现:
import os import uuid from datetime import datetime from werkzeug.utils import secure_filename ALLOWED_EXTENSIONS = {'py', 'zip', 'rar', 'doc', 'docx', 'pdf', 'txt', 'jpg', 'png'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/student/submit/<int:homework_id>', methods=['POST']) @login_required @role_required(1) def submit_homework(homework_id): file = request.files.get('homework_file') if not file or file.filename == '': return '请选择要上传的文件', 400 if not allowed_file(file.filename): return '不支持的文件类型', 400 # 安全处理文件名 original_name = secure_filename(file.filename) ext = original_name.rsplit('.', 1)[1].lower() # 用uuid重命名,防止路径穿越和重名覆盖 stored_name = uuid.uuid4().hex + '.' + ext upload_dir = os.path.join('uploads', str(homework_id), str(session.get('user_id'))) os.makedirs(upload_dir, exist_ok=True) save_path = os.path.join(upload_dir, stored_name) file.save(save_path) ...几个关键点:
第一,白名单校验扩展名。限制后缀只是第一道防线,实际上某些公共网关可以做文件头校验(比如检测ZIP的PK头、PNG的文件签名),但Web框架层先做到白名单限制最重要。
第二,重命名存储文件。很多教程喜欢直接用原始文件名存盘,这里就有两个隐患:一是中文文件名在不同操作系统的编码处理不一致;二是如果两个学生上传了同名文件,后传的会覆盖先传的。所以我用uuid.uuid4().hex生成一串随机字符串作为存储名,原始文件名单独存入数据库用于下载时还原显示。
第三,按作业ID和学生ID分级创建目录。上传目录结构是uploads/<homework_id>/<student_id>/<随机文件名>。这样管理批量文件时能快速定位,也便于后续做磁盘清理。
第四,限制上传大小。Flask默认不限制上传大小,一个学生传个5GB的视频会把磁盘塞爆。通过配置限制单文件最大100MB:
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024超过这个大小的请求会被中断并返回413错误,前端同步弹提示告知学生“文件过大,请压缩后再传”。
4.3 作业批改与评分联动
批改页面的核心是展示某个作业下所有学生的提交状态列表,教师点击“批改”按钮后进入详情页,看到学生提交的文件预览和下载链接,然后填写分数和评语。
分数需要做校验,0到100之间的数值,最多保留两位小数。提交时更新状态字段,从“已提交”改为“已批改”。
这里有一个业务细节容易遗漏:截止时间之后提交的作业要标记为迟交。我处理的逻辑是:提交时比对当前时间和deadline,晚于截止时间的,在状态里加一个标志位。这里不搞一刀切拒绝提交,因为有些老师允许迟交但会扣分。灵活一点,把判断交给老师,系统只负责如实标记。
批改评分的前端用一个简单的模态框完成,不跳转页面,Ajax提交之后局部刷新表格,体验比整页刷新好得多。
4.4 统计导出功能的实现
统计模块做了两个功能:作业提交率统计和成绩分布统计。
提交率统计是算出一个作业的已提交人数占选课总人数的百分比。SQL核心是:
SELECT hw.id, hw.title, COUNT(DISTINCT cs.student_id) AS total_students, COUNT(DISTINCT CASE WHEN sub.id IS NOT NULL THEN sub.student_id END) AS submitted_students FROM homework hw JOIN course_student cs ON hw.course_id = cs.course_id LEFT JOIN submission sub ON hw.id = sub.homework_id AND cs.student_id = sub.student_id WHERE hw.course_id = %s GROUP BY hw.id, hw.title这个SQL用COUNT(DISTINCT CASE WHEN ...)来避免多表连接后的笛卡尔积重复计数,是统计场景里很常见也容易写错的点。如果不加CASE WHEN,直接COUNT(DISTINCT sub.student_id)也可行,但CASE WHEN结构对后期加过滤条件更友好。
成绩分布则统计0-59、60-69、70-79、80-89、90-100五个区间的学生数量,用CASE WHEN配合SUM:
SELECT SUM(CASE WHEN score < 60 THEN 1 ELSE 0 END) AS fail_count, SUM(CASE WHEN score >= 60 AND score < 70 THEN 1 ELSE 0 END) AS pass_count, SUM(CASE WHEN score >= 70 AND score < 80 THEN 1 ELSE 0 END) AS good_count, SUM(CASE WHEN score >= 80 AND score < 90 THEN 1 ELSE 0 END) AS great_count, SUM(CASE WHEN score >= 90 THEN 1 ELSE 0 END) AS excellent_count FROM submission WHERE homework_id = %s导出功能我直接在后端生成CSV格式,用流式响应返回给前端下载,不需要额外引Excel操作库,实现成本低,教师拿到CSV后也能直接用办公软件打开。
import csv import io def export_scores(homework_id): data = get_submission_scores(homework_id) output = io.StringIO() writer = csv.writer(output) writer.writerow(['学号', '姓名', '分数', '提交时间', '状态']) for row in data: writer.writerow(row) response = make_response(output.getvalue()) response.headers['Content-Disposition'] = f'attachment; filename=scores_{homework_id}.csv' response.headers['Content-Type'] = 'text/csv; charset=utf-8-sig' return response注意一个细节:charset=utf-8-sig而不是utf-8。UTF-8-SIG带有BOM头,Excel打开CSV时中文才不乱码。这个坑很小,但不知道的人导出全是乱码,非常影响体验。
5. 开发与部署中踩过的坑
5.1 中文乱码问题
刚写完登录功能测试时,注册用户名填了中文,页面上显示的姓名直接变成问号。排查后发现两层问题:
第一层是数据库连接字符集没指定。PyMySQL连接MySQL时,如果URL里没带charset=utf8mb4,连接使用的是MySQL默认的latin1(旧版本),中文写入就变乱码。解法是连接配置必须写上:
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://hw_user:密码@localhost/homework_system?charset=utf8mb4'第二层是MySQL数据表本身的字符集。如果建表时没指定字符集,走的是MySQL实例默认值。我用的是UTF8MB4,这是MySQL8的常见默认配置,但你在自己环境上建库时一定要确认表字符集:
SHOW CREATE TABLE sys_user\G看到CHARSET=utf8mb4才算放心。
第三层是CSV导出字符集,上面已经提过了,要用utf-8-sig。
5.2 PyMySQL认证方式报错
MySQL8默认的认证插件是caching_sha2_password,而旧版PyMySQL只支持mysql_native_password,连接时直接报Authentication plugin 'caching_sha2_password' cannot be loaded。解法有两个:升级PyMySQL到1.0+并安装cryptography包;或者把MySQL用户认证改回旧方式。我推荐前者,因为后者需要改MySQL配置,安全性也差。
5.3 文件上传413错误处理
前面配置了最大上传100MB,但前端如果不做提示,学生上传超出大小的文件时只会看到“页面无法访问”的失败页面,非常不友好。我在前端Ajax请求里统一处理了413状态码:
axios.post(url, formData) .then(response => { /* 成功处理 */ }) .catch(error => { if (error.response && error.response.status === 413) { alert('文件过大,单个文件请控制在100MB以内'); } else { alert('提交失败,请重试'); } });养成一个习惯:后端每种异常状态码,前端都要有对应的用户可读提示。不然排查问题时你会收到大量无效的“老师,我传不了”反馈,却不知道具体是为什么。
5.4 路径安全:防目录穿越
一个很隐蔽的坑是文件下载接口。如果实现时直接拼接文件名参数:
@app.route('/download/<path:filename>') def download(filename): # 危险写法,存在路径穿越风险 return send_file(os.path.join(upload_base, filename))攻击者传入../../etc/passwd这样的路径,就可能读取服务器任意文件。修复方式:不要信任用户传来的路径参数,而是从数据库里查真实存储路径:
sub = get_submission_by_id(submission_id) if sub and current_user_has_permission(sub): return send_file(sub.file_path, download_name=sub.original_filename)因为sub.file_path是服务端生成并存入数据库的,不经过用户输入,安全系数大大提升。这一点是实战中我曾经马虎踩过的坑,现在写出来提醒大家。
5.5 误删文件的后悔药
有一次我在测试批量删除作业时,直接调了os.remove删文件,结果把一个还在被其他页面引用的文件删了。后来在所有删除操作前都加了一层“软删除”策略:数据库记录加一个deleted_at字段,文件暂不物理删除,只有确认过了保留期才定期清理。
这样设计实际上是一种常见的保险机制。文件删除是不可逆操作,任何系统都不应该立即物理删除,尤其是教学场景里,老师可能忽然想起某个学生的作业还有用。软删除可以随时恢复,服务器磁盘几十GB也足够容纳一段时间的冗余文件。
6. 文档、测试与项目交付心得
6.1 文档结构怎么组织
项目交付时除了源码和数据库,文档也是硬要求。这套系统的文档我拆成了三份。
需求说明书:写明系统背景、用户角色、功能清单、界面原型草图。这部分是给指导老师看的,重点在于让没看过系统的人也能快速理解“这个系统是干嘛的”。
数据库设计文档:包含ER图、表结构说明、关键字段设计理由。重点是让接手的人知道submission表里的score为什么可空,为什么联合唯一索引加在哪个字段上。
系统部署与使用说明:环境版本、初始化数据库步骤、启动命令、管理员账号初始化方式、常见问题排查。这部分是最容易被忽略的,但也是最有价值的。没有它,别人拿到源码也跑不起来。
文档写作有个小技巧:先写操作步骤,再画补充图,最后补解释。因为使用者第一步永远是想看到“怎么把它跑起来”,而不是听你从零讲架构。
6.2 功能测试与边界验证
开发完成之后,我按流程做了三轮测试。
第一轮,正常流程测试:注册学生和教师账号、创建课程、选课、发布作业、提交作业、批改评分、查看成绩。每一步按预期走通。
第二轮,异常流程测试:重复提交作业、提交空文件、截止时间后提交、上传超大文件、非本课程学生访问他人作业详情页。这一轮帮我修掉了一个越权bug:原本接口只校验了登录状态,没校验用户是否属于该课程,手写请求就能访问到其他班级的批改数据。
第三轮,多浏览器兼容测试。Chrome、Edge、Firefox分别过一遍,发现旧的某个浏览器内核里日期组件显示不对,最后统一改用原生input type="date",不再依赖第三方日期库。
提示:功能测试别只测“正常路径”,异常输入才是系统健壮性的试金石。时间、角色、权限、文件大小这些边界条件都要过一遍。
6.3 代码目录结构复盘
整个项目的目录结构最终是这样的:
homework_system/ ├── app.py # 应用入口 ├── config.py # 配置项(数据库、上传路径、密钥) ├── models/ │ ├── __init__.py │ ├── user.py # 用户模型 │ ├── course.py # 课程模型 │ ├── homework.py # 作业模型 │ └── submission.py # 提交模型 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录注册 │ ├── teacher.py # 教师端路由 │ └── student.py # 学生端路由 ├── templates/ │ ├── base.html │ ├── auth/ │ ├── teacher/ │ └── student/ ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── uploads/ # 作业文件存储目录 ├── requirements.txt └── README.mdFlask的蓝图Blueprint在这里帮了大忙。我没有把所有路由堆在一个app.py里,而是按角色拆成三个蓝图模块,每个模块只处理自己职责范围内的路由。这样做的好处是改老师端逻辑时不会误碰到学生端代码,调试时定位问题也更快。
7. 进阶扩展思路
这套系统跑通之后,可扩展的空间其实很大,我梳理几个实用方向:
自动批改扩展。对于编程作业,可以集成自动化测试脚本。学生提交代码后,系统自动运行预置的测试用例,生成通过率作为评分参考,教师只需要复评即可。这是一个很大的效率提升,但实现起来要小心:沙箱环境必须隔离,防止恶意代码攻击服务器。
消息通知扩展。接入企业微信或邮件API,作业发布和评分完成后自动通知学生。这个对实际使用体验提升非常明显,学生不会因为错过截止时间而交不了作业。
作业查重扩展。针对文本类作业,可以用文本相似度算法做初步筛查。Word类型的作业可以先提取文本,然后计算余弦相似度或者杰卡德系数。这部分不用追求银弹级准确率,能够标记疑似重复就够了,最终判断还是交给教师。
数据可视化扩展。成绩分布、班级对比、课程趋势等数据,可以接入图表库做成可视化看板。教师打开后台一眼就能看到哪个班提交率低、哪个模块得分率高。
这些扩展的优先级排序建议是:消息通知 > 自动批改(编程类课程) > 作业查重 > 可视化。通知能直接减少“忘交作业”的频率,是投入产出比最高的改进。
8. 一点实操后的体会
整套系统从零到可交付,前后大约花了两周业余时间。最耗时间的不是写代码,而是设计数据结构的时候反复权衡、以及后面排查那些看起来不大但很影响体验的细节问题。
我个人的体会是,像学生作业管理系统这种业务大家都很熟悉,反而是优势,因为需求不需要花时间去想象,流程就摆在眼前。关键是不要把系统做得太复杂,先跑通主流程,再考虑增强功能。
如果你正准备照着做一个类似的系统,我的建议是:把权限控制、文件安全和数据关系这三件事放在第一优先级,界面丑一点没关系,数据安全和权限正确才是系统的命根子。另外一个经验是,提前把常见坑的解决笔记记录下来,比如中文乱码的三种修复方式、MySQL8认证插件的问题、CSV导出加BOM头,这些零零碎碎的小知识最后都成了部署文档里最实用的部分。
把这套流程完整走一遍,你对Web开发的理解会比看十遍教程都深。毕竟,只有亲手踩过坑,才知道路该怎么修。