前言
专科即将毕业,学校要求毕业设计,于是我就写了一个学习平台,叫做“苍穹学习平台”,不仅是应付毕业设计,之后我也会拿来自己用,这东西消耗了我快半个月的时间。最后查重也只有2%左右,这让我挺意外的,毕竟用了很多文字模版,可能是我这个类型的设计比较少见吧。
已在gitee开放旧版本的源代码(不包含数据库):https://gitee.com/ShanShanKo/a-learning-platform
以下是我写的毕业设计中对该平台的介绍:
在各行各业都深受信息化浪潮洗礼的大背景下,人的教育培训要求也随之发生深刻的信息化转变。传统的教学模式逐渐显得力不从心,难以满足当今社会快速发展的信息需求。因此,学习平台作为信息化教育的重要载体,其地位和作用日益凸显。
本文阐述的是一个基于B/S架构的学习平台实现,该系统基于Node.js,集成了学生老师会用到的常用功能,前端部分主要使用jquery和bootstrap实现,页面简洁,后端使用node.js和mysql实现。经过后期测试,前台页面响应迅速,后台在经过各种数据输入的测试后,仍然运行稳定,表现出良好的健壮性。
苍穹学习平台适用于轻量、任务导向的教学要求。无论是普通课程、项目实践课程还是其他形式的教学任务,不管是中学、大学课堂还是社会培训机构,平台都能提供有力的支持。老师可以通过灵活的课程安排和作业设计,轻松地实现教学目标,而学生也能在完成任务的过程中不断提升自己的能力和水平。
虽然说的好听,但是我知道这个平台更像是胡乱拼凑的东西,bug也不少,一半的时间都在改,毕竟之前没有做过这样规模的软件。下面就说说设计和具体实现:
设计与实现
首先需要决定技术栈,我的专科专业就是学的b/s架构,我以前参与html技能大赛和写网页app的底子也没有消退太多,于是前端果断的是前端三件套。但是我想学点新东西,于是增加了jquery和bootstrap,这两货我还从来没用过。至于后端,java我没有好好学,至于php,我虽然用它写过待办列表,但终究没有用太多次。所以我选择了nodejs,毕竟对js比较熟悉,也比较轻量。
然后进行需求和分析,教师和学生分别有什么需求:
学生需求
1、课程搜索功能
在搜索页面搜索课程,点击搜索之后,系统对输入做出相应的反映,搜索为模糊匹配,输入课程的任意一个字词即可匹配。
2、查看课程功能
在进入课程内容页后,能够看到图文并茂的课程资料,并且每个课程分为多个单元。
3、作业提交功能
在学习完课程后,学生可以提交作业文本,可以附带文件,并且根据实际情况可以多次提交。待老师批改完成后,可查看自己作业的评分和评语。
教师需求
1.教师登录功能
在前台页面登录以后,如果是教师身份,可以通过功能菜单进入教师管理后台。
2.作业批改功能
教师能够给学生的作业评分和评语。
3.课程编辑功能
教师能够创建、修改、删除课程,使用多种媒体展现课程内容。带给学生丰富的学习体验。
然后就决定有什么页面、数据库:
决定有哪些身份:
身份 |
权限 |
未登录访客 |
查看主页 |
学生 |
查看课程具体内容、提交作业并得到反馈、搜索课程 |
助教 |
学生的所有权限,查看批改各个课程的学生作业。 |
普通老师 |
助教的所有权限,增加、修改、删除课程内容 |
管理老师 |
普通老师的所有权限、修改主页的课程位、修改其他用户账号、查看操作记录。 |
在旧版本中,普通老师和管理老师的功能并没有实现。在新版本中,课程和单元增删改实现了,管理老师的部分实在没精力实现了,因为写的不规范,代码慢慢成为了一座屎山,后端还稍微好点,前台一看到一大堆dom和骚操作就头疼,领略到了vue的出现不是没有理由的。
功能实现——登录注册
在前台写好登录注册页面,代码像这样。
<div class="container">
<form class="form-horizontal" role="form">
<div class="form-group">
<label class="col-sm-2 control-label">用户名或邮箱</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="username" id="login_username" />
</div>
</div>
<div class="form-group">
<label
class="col-sm-2 control-label"
>密码</label
>
<div class="col-sm-10">
<input
type="password"
name="password"
class="form-control"
id="login_password"
/>
</div>
</div>
</form>
<div class="row">
<div class="col">
<div class="form-group eright">
<button type="submit" class="btn btn-primary login_btn">
登录
</button>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="form-group ebe">
<label class="link">忘记密码</label>
<label class="link regist_btn">注册账号</label>
</div>
</div>
</div>
</div>
此外,登录页在用户名和密码框为空时,弹出提示。注册页也一样,并且要正则表达式判断邮箱时候输入正确、判断两次输入密码的值是否相同。
后台受到请求时先判断是否缺失参数,如果缺失返回一个字符串“0”,我知道这样不规范,但是我懒。
如果正确,登录的控制器会用我事先写好的 tokenGen()函数生成token,这个函数又使用了jsonWebToken这个包,然后将token以及用户信息发送到客户端,客户端将信息存储到localstrage里面,在进入除了登录页和注册页以外的页面,都会携带token到 /entrydetection 这个接口进行验证,验证不通过会直接退出登录,token有效期21天。
注册页的实现,我想让你们看看旧版本的辣眼代码:
app.post("/regist_send", function (req, res) {
var reg = /^([\.a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/;
console.log(req.body);
if (
(req.body.username == "") |
(req.body.password == "") |
(req.body.email == "") |
!reg.test(req.body.email) |
!req.body.username
) {
res.send(0);
return;
}
let p = new Promise(function (resolve, reject) {
// 检测用户名重复
connection.query(
'select count(username) as c from user where username="' +
req.body.username +
'"',
function (error, results, fields) {
if (error) {
reject("0");
throw error;
}
if (results[0].c >= 1) {
reject("name_error");
j = "name_error";
} else {
resolve("success");
}
}
);
})
.then(async function (r) {
// 检测邮箱重复
// reject('email_error')
let sql =
"select count(email) as e from user where email='" +
req.body.email +
"'";
// where email="+"'"+req.body.email+"'"
async function asyncFunc() {
try {
let o = "";
await new Promise(function (resolve, reject) {
connection.query(sql, function (error, results, fields) {
if (error) {
console.log(error);
res.send({
success: false,
message: "database error",
error: error,
});
return;
}
if (results[0].e >= 1) {
o = "email_error";
// throw 'email_error'
} else {
}
resolve("");
});
});
if (o === "email_error") {
throw "email_error";
}
} catch (err) {
console.log(err);
throw err;
// 会输出 Some error
}
}
await asyncFunc();
console.log(":" + p);
return;
})
.then(function (r) {
//检查完成后插入
function create(ident){
let sql =
"insert into user (email,username,password,create_time,identity,level,head_portrait) values ('" +
req.body.email +
"','" +
req.body.username +
"','" +
req.body.password +
"','" +
util.getTime() +
"'," +
ident +
",0,'default.png')";
console.log(sql);
// sql="insert into user (email,username,password,create_time,identity,level,head_portrait) values ('shan@qq','shan','123','2023-01-01 00:00:01',1);"
connection.query(sql, function (error, results, fields) {
if (error) {
console.log(error);
res.send({ success: false, message: "database error", error: error });
return;
}
res.send("1");
return;
});
}
// 有使用次数限制的邀请码使用后减少次数
async function reduce_degree(id){
let sql="update invitation set degree = degree-1 WHERE id ="+id;
console.log(sql)
connection.query(sql, function (error, results, fields) {
if (error) {
}
console.log("有限邀请码使用")
})
}
new Promise(function (resolve, reject) {
if(req.body.invitation==""){
resolve(1)
}else{
connection.query("SELECT id,ident,degree from invitation where now()<period_validity and code='"+req.body.invitation+"' and(degree>=1 || degree<=-1)", function (error, results, fields) {
if (error) {
return;
}
console.log(results[0])
if(results[0]==null){
resolve(0);
}else{
if(results[0].degree<=-1){
resolve(results[0].ident);
}else if(results[0].degree>=1){
resolve(results[0].ident);
reduce_degree(results[0].id)
}
}
})
}
}).then(function(resu){
console.log("身份",resu)
if(resu<=0 || !resu){
console.log("无效邀请码")
res.send("invitcode_error")
}else{
// console.log("有效邀请码")
create(resu)
}
})
})
.catch((error) => {
console.log(error + "错误");
res.send(error);
});
});
功能实现——查看课程
之后的功能实现描述就直接copy我在毕业设计里写的了,并且代码太长太多也不放在这里了,感兴趣去文章开头的gitee仓库链接。
在进入对应课程的学习页面后,分为三个区域,左边加载出课程单元的目录,右边加载出课程文字内容,最下面如果课程有作业要求,作业区域会被加载出来。
左边的课程单元拦,如果有需要提交作业的单元但是没有提交作业或者评分低于D,那么下一个单元以及之后的单元都将被锁定,无法查看。如果是不需要提交作业的单元,只需查看此单元的内容就可解锁下一单元。右边的课程内容区域,如果课程附带视频就加载视频,放在内容区域最上方。课程文本是用markdown编写的,支持数学公式,点击某一节的课程单元项就要加载出对应单元的课程内容。
进入学习页面时,首先获取地址栏上的course_id、section参数,随后附带这些参数发送获取目录和获取章节内容的请求到/course_catalogs和/unit_content,如果地址栏上没有参数,将会跳转回主页。对于章节列表,获取到的json数组,使用for循环,逐一创建<li>元素然后添加到章节选择区域。如果其中done字段的值为null或者0,则将对应<li>元素的onclick事件修改,提示用户单元未解锁,并且在元素内添加一个小锁的图标。
对于课程内容,如果获得的json数组包括视频链接,就移除video的hide类,并修改video元素的src属性为视频地址。课程的文字内容先用第三方库marked.js将markdown文本渲染成html标签,然后再插入到文本区域中。如果存在数学公式,mathjx.js库将会渲染标签内“$”包括的内容。
功能实现——提交作业
如果某个课程的某个章节要求交作业,那么在课程学习页的最下方将会显示作业提交区,包括文本编辑器、文件上传栏、作业提交按钮和查看历史提交与历史提交区域。
文本编辑器可以输入所见即所得的markdown文本,如果文本为空,则不能提交作业。在提交后,文本编辑器里的内容将清空。文件上传栏点击就打开windows的文件选择器,至多上传十个文件。点击作业提交按钮后,作业文本内容和作业附件一并发送到服务器存储。点击“查看历史提交”,将展示历史提交的作业列表,并能提供上传过的附件下载,如果作业还未被评分,可以将其删除。如果作业已被老师评分,可以看到评分和评语。
Markdown文本编辑器使用的是Vditor.js库,单击文件上传时,如果文本内容为空,则不能提交。文件上传使用的是另一个接口,文件成功上传后将生成一个json数组,字段包括上传前的文件名和上传后的文件名,为防止中文名乱码,上传前文件名会被base64编码。这个json数组作为作业提交参数的一部分。提交作业的ajax请求的参数包括学生的id、作业文本内容、课程代码、单元码、附件列表。
这其中文件上传是折磨了我最久的问题,花了三天时间才搞定学生的作业提交附带文件功能。
进入vditor.js库介绍页时,发现这个库的作者也是我正在使用的软件思源笔记的作者,爱了。
功能实现——批改作业
当用户为老师身份时,点击头像下拉菜单的“老师页面”进入管理后台,默认的视图就是作业批改页面,作业分为已批改和未批改两类,以表格形式展示作业部分信息,单击表格中的一项弹出窗口,内有作业的全部文本、作业的附件下载链接、评分下拉菜单、评语框、提交按钮、关闭按钮、删除作业按钮。
进入老师页面后,首先要验证是否是老师身份,ajax请求接口/teacher_verify,如果为2及以上的数字,则继续以下的操作,否则返回主页。随后加载待批改作业的列表,接口为“/getWork”,方式为post。获得数据后用for循环生成html元素并添加对应的单击监听事件。当单击作业表格其中一项时,触发showCorrection()函数,弹出批改作业的窗口,作业文本用marked.js解析,jquery dom生成a链接并插入到file_links链接组里面。单击提交按钮后,转到workCorrection_submit()函数处理,发起ajax请求,更新作业表中的对应字段。
使用的框架和插件
前端:jquery、bootstrap4、marked、mathjx、vditor.js、bootstrap-icons
后端:express、multer、jsonwebtoken、moment、mysql
效果图
这里用的图床,站主别打我。。。。。。
新版本更新
- 后台接口改为模块化路由,更加规范整洁。
- 封装sql查询,改为连接池更加稳定。并且将所有字符串拼接改为参数化查询,更加安全。
- 增加课程和单元增删改查功能。
- 网站管理子页面增加暂停用户注册功能。
- 每个单元可以附带附件,老师可以在作业批改里添加附件。
- 新增帮助页面,可以在内发送公开建议和问题。
- 现在每个重要接口请求都会验证token内的用户信息身份,更加安全。
- 修复学习页面用户可以通过地址栏跳关获得内容和解锁后面无作业关卡的bug。
- 学生或老师删除作业时,服务器能将对应的作业附件删除,节省服务器空间。
不过就像之前说的,新版本我要留着自己用了。如果我能在9月之前制作出来一批课程内容,我会上线网站的。
参考文献
- 郑婷婷,黄杰晟, 响应式网页开发基础教程(jQuery+Bootstrap)[M]. 北京: 人民邮电出版社. 2019
- 周文洁, JavaScript与jQuery网页前端开发与设计 [M]. 北京: 清华大学出版社, 2018.
- Ethan Brown, Node与Express开发(第2版).吴滠栩 译 [M]. 人民邮电出版社, 2021.
- 蔚然 论教学系统中教学反馈的重要性 [J]. 教育现代化 ,2017,4(47):226-227.
- 丁笑舒 基于信息构建的国内外典型在线教育网站发展现状评价——以国内外MOOC学习平台为例 [J]. 信息化研究, 2017(04):6-12.
写完了,我去画画了。