基于Express实现Web端树莓派云课堂习题上传功能

序言

一个在线教育平台,习题上传功能是不可缺少的。这里基于Nodejs的Express框架,接入MongoDB数据库,实现习题的创建,作答和统计,并使用Echart展示数据。

前期设计

流程设计

一个简单的习题功能,包括了教师的习题创建、学生的习题作答和教师的习题统计数据查看的三部分功能。

页面设计

教师课程页面的习题创建和查看入口

教师创建新习题页面,包括选择题和填空题

教师习题数据统计页面,使用柱状图进行数据的展示,并且可以查看具体的学生名单

学生的习题作答界面

前端脚本

习题上传脚本

主要需要动态修改题目数量、填入题目内容与答案、将填入好的习题数据发送至服务端的功能

$(document).ready(() => {
    // 添加选择题
    $("#addChoice").click(function() { // 点击增加选择题
        $("#choice").append('\
        // 选择题的具体HTML代码
        ');

		// jQ动态创建的DOM元素,需要通过父元素指定来绑定事件
        $("#choice").on("click", ".delete", function() { 
            $(this).parent().remove();
        });

    });
});

$(document).ready(() => {
    $("#addCompletion").click(function() { // 点击增加填空题
        $("#completion").append('\
       // 填空题的具体HTML码
        ')
    })

	// 同样绑定移除填空题的事件
    $("#completion").on("click", ".delete", function() {
        $(this).parent().remove();
    });
})

$(document).ready(() => {
    var choice = [];
    var completion = [];
    $("#submit").click(() => {
        $("#choice >li").each(function() {
            // 利用jQ选择题,获得填入的选择题信息
        });

        $("#completion >li").each(function() {
            // 利用jQ选择题,获得填入的填空题信息
        });
        
        // 将习题数据Post发出
        $.post("process_upload", {test: JSON.stringify(test)}, (data) => {
            // ...
        }, "json");
    })
})

习题作答脚本

只需要利用jQ Ajax功能获得题目并展示,以及将作答数据Post回服务端

$(document).ready(() => {
    $.get("/tests?id=" + id, (data) => { // 获得JSON格式的试题数据
        data.choice.forEach((item, index, array) => {
            $("#choice").append('\
           		// 选择题的具体HTML代码
            ')
        });
        data.completion.forEach((item, index, array) => {
            $("#completion").append('\
                // 填空题的具体HTML代码
            ')
        });
    }, "json");
});

$(document).ready(() => {
    $("#submit").click(() => {
        // 利用jQ获得作答的数据,并且Post给服务端
    });
});

习题数据查看脚本

$(document).ready(() => {
    $.get("/tests?id=" + id, (data) => {
        // 同样获得习题数据并展示
    }, "json");
});

// 绘图
function plant(choiceArr, completionArr) {
    choiceArr.forEach((element, index, array) => {
        // 对每道选择题数据进行绘图
    });

    completionArr.forEach((element, index, array) => {
         // 对每道填空题数据进行绘图
    });
}

后端处理

习题数据

每一份习题在数据库中存储的形式:

_idclassnamenamechoicecompletion
习题的唯一id(自动生成)习题发布的班级习题名所有选择题(数列)所有填空题(数列)

每一道选择题的存储形式:

stem A B C D answer answers
题干 A选项内容 B选项内容 C选项内容 D选项内容 标准答案 选择四个选项的学生名单

每一道填空题的存储形式:

stem answer answers
题干标准答案 回答正确学生名单、回答错误学生名单以及错误答案

路由配置

添加了一个tests.js路由文件,用来处理与试题相关的请求。
由于试题数据需要登录才能查看,并且需要分身份发送不同的内容,
几乎所有的响应都包含对是否已在session中登录和对身份是否符合的判断(在以下代码示例中略),
如果不符合,跳转至登录界面或者403界面。

// 直接返回试题原数据
router.get('/', function(req, res) {
    db.find("test", {"_id": ObjectId(req.query.id)}, (data) => {
        // 调用自己编写的调用数据库的函数,获得试题信息并返回
        // 如果身份是学生,只截取
    });
})

// 试题查看页面
router.get('/paper', function(req, res) {
    db.find("test", {"_id": ObjectId(req.query.id)}, (data) => {
        // 判断习题是否存在
        // 习题若存在,如果session的identity是教师,发送教师的页面;如果是学生发送学生的页面
    });
})

// 试题页面
router.get('/add', function(req, res) {
    // 如果session的identity是教师,发送创建试题页面
})

// 上传新试题
router.post('/process_upload', function(req, res) {
   	// 将获得的新习题数据存入数据库
})

// 提交完成的试题
router.post('/process_submit', function(req, res) {
    // 从数据库中取出相应的习题数据,将学生的作答插入数据中,再更新数据库
})

// 读取习题列表
router.get('/list', function(req, res) {
    // 获得所有习题的列表
})

结语

这样便完成了一个基础的习题上传功能,在接下来还需要做功能的添加和优化,例如

  • 增加题型
  • 设置试题允许作答的起止时间、得分分布等
  • 增加教师对学生作答内容的反馈功能
  • 增加教师对习题的删除、锁定、修改等等动能
  • 增加学生对习题的结果回查、排名查看
  • 在从数据库取原作答情况,和更新新作答情况之间给数据库加Pessimistic Lock,以免学生同时提交作答导致数据丢失
  • 模块化从试题数据库取试题所有数据、仅取题目、仅取学生作答情况和得分、仅取某一个学生的所有作答等功能
  • 以及其他功能和优化

树莓派局域网登录功能实现:Express+jQuery+MongoDB

序言

为了在树莓派上架设教学系统,这里以Express为框架,MongoDB作为数据库,利用了部分jQuery语法,实现了局域网内的注册、登录和找回密码服务。

前期设计

流程设计

因为是局域网登录,没有使用邮箱验证等联网方式注册账户,而是通过班级的注册码进行验证;同时找回密码的方式是由管理员或教师申请账号安全码。

页面设计

设计上使用的简洁的浮窗式界面,实现上使用了一个html文件,以及两个css文件,分别管理页面的表单样式、其他页面布局样式。二者分离开来,有助于表单样式的复用,将来在其他页面上使用到表单,可以调用同一个css文件。

数据库存储格式

  • MongoDB作为非关系型数据库(NoSQL)的一种,相比于关系型数据库,其格式更加灵活,对简单CRUD操作效率更高。
  • 同时MongoDB基于文档,以优化的二进制json(bson)格式存储数据,支持了更多数据类型,提高了运行效率,更有分片集群等功能,适合高并发海量数据的存储。
  • 在这个例子中,需要进行的操作比较简单,没有事务的需求,因此使用MongoDB作为数据库

学生、教师和管理员的信息分别存在三个集合中,共有的信息基本如下:

学(工)号用户名密码权限等级账号安全码
(字符串)(字符串)(字符串)(整型)(字符串数组)

注册码存在以下集合中:

班级权限注册码
(字符串)(整型)(字符串)

以及存储班级信息的集合

前端脚本

页面切换

脚本第一部分用来在不同的页面之间切换,使用原生的操作方式,利用document.getElementById().style等方法脚本化css,实现对元素的隐藏和展示,达到切换页面的效果

function show_form(value) {
//...
//展示登录框...
}
function to_login() {
	document.getElementById('login-form-box').style.display = 'inline';
    document.getElementById('register-form-box').style.display = 'none';
    document.getElementById('forget-form-box').style.display = 'none';
    document.getElementById('find-form-box').style.display = 'none';
//切换到登录界面
}
function to_register() {
//...
//切换到注册界面
}
function to_forget() {
//...
//切换到找回密码界面
}
function to_find() {
//...
//切换到找回密码成功界面
}

jQ请求和响应

脚本第二部分用来向服务器端发送请求,实现注册、登录、找回密码的功能。这部分使用了jQ语法,利用$.post()函数,大大简化了请求操作

$(document).ready(function() {

    //注册功能
    $("#register-button").click(function() {
    	//...
    	//向后端发送POST请求
    	//发送用户名、密码、注册码和身份(教师、学生)
    	//接受是否注册成功的消息
    });

    //登录功能
    $("#login-button").click(function() {
    	//...
        //向后端发送POST请求
    	//发送用户名、密码和身份(教师、学生)
    	//接受是否登录成功的消息
    }); 

    //找回密码功能
    $('#forget-button').click(function() {
    	//...
        //向后端发送POST请求
    	//发送账户安全码和身份(教师、学生)
    	//接受是否验证成功的消息
        //如果验证成功,跳转到重设密码界面
    });

    //找回成功并设置新密码
    $("#find-button").click(function() {
    	//...
        //向后端发送POST请求
    	//发送账户安全码,新密码和身份(教师、学生)
    	//接受是否重设密码成功的消息
        //如果重设密码成功,跳转到登录界面
});

后端处理

连接MongoDB数据库

首先需要mongodb模块

npm install mongodb --save

然后写一个脚本mongodb.js,整合连接数据库的方法,简化调用

const MongoClient = require("mongodb").MongoClient;//引入模块
const dbname = 'piclass';//数据库名
const url = 'mongodb://localhost:27017/' + dbname;//数据库所在的url

//合并插入单个和多个文档的方法,同时提前写好实际不会发生变化的变量,简化数据库的调用
function insertSome(collection, document, callback) { //只传入集合名,文档和回调函数
    if (!Array.isArray(document)) { 
    	//调用连接Mongodb数据库的方法
        MongoClient.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }, function(err, db) {
            if (err) throw err;
            var dbo = db.db(dbname);
            dbo.collection(collection).insertOne(document, function(err, res) {
                if (err) throw err;
                console.log("插入单个文档成功");
                db.close();
                if (callback) callback(res);
            });
        });
     } else {
     	//...
        //insertMany()方法
     }
}

function find(collection, query, callback) {
	//...
    //简化find方法查找元素
}

function find_select(collection, query, skip, limit, callback) {
	//...
    //包含限制个数、跳过个数的find方法
}

function updateSome(collection, query, update, justOne, callback) {
	//...
    //更新一条或者所有符合要求的文档的某个数据
}

function deleteSome(collection, query, justOne, callback) {
	//...
    //删除符合要求的文档
}

function sort(collection, isAscend, callback) {
	//...
    //文档排序
}

module.exports.insertSome = insertSome;
module.exports.find = find;
module.exports.find_select = find_select;
module.exports.updateSome = updateSome;
module.exports.deleteSome = deleteSome;
module.exports.sort = sort;
module.exports.clear = function(collection, callback) { 
    deleteSome(collection, {}, false); 
    if (callback) callback();
} //清空集合

配置路由

发送至express后台的请求由aqq.js分发到各个路由文件中,这里将登录界面的所有请求都由index.js默认路由来响应。

var express = require('express'); //引入express模块
var db = require('../myscripts/mongodb.js'); //引入操作数据库的自定义模块
var router = express.Router(); //调用路由

/* GET home page. */ //根页面直接使用登录静态页面进行响应
router.get('/', function(req, res, next) {
  res.sendfile( __dirname.slice(0, -6) + "public/" + "login.html");
});

/* Login Page */ // /login也同样使用这个静态页面
router.all('/login', function(req, res, next) {
  res.sendfile( __dirname.slice(0, -6) + "public/" + "login.html");
});

/* User Rsgister */ //响应注册请求
router.post('/process_register', function(req, res, next) {
  //...
  //获得用户名、密码、注册码和身份
  //1、根据身份进入不同的数据库集合
  //2、判断用户名是否已存在
  //3、判断注册码是否有效
  //4、存储用户数据
  //5、传回客户端操作是否成功等信息
})

/* User Login */ //响应登录请求
router.post('/process_login', function(req, res, next) {
  //...
  //同样地,查询用户名和密码是否存在,并且反馈信息
})

/* Forget Password */ //响应找回密码请求
router.post('/process_forget', function(req, res, next) {
  //...
  //查询账号安全码是否有效,并且反馈信息
})

/* Find Password */ //响应重设密码请求
router.post('/process_find', function(req, res, next) {
  //...
  //再次验证安全码,同时将新的密码存入数据库中,并且反馈信息
})

结语

利用这些功能,可以简单搭建一个注册、登录和找回密码功能的web应用。要继续完善,还需要在后面的开发中利用cookie或者session storage等本地存储方案,实现登录状态的保持;增加管理页面,实现高权限账户对低权限账户的管理和注册码、账号安全码的分发;以及对代码进行优化等等。