工具:Node.js + MongoDB + Socket.IO
完成进度
教师端:
- 学生的添加删除等操作
- 考题和分数的添加删除编辑修改等操作
- 在查看考试情况页面显示所有考生的姓名和学号,以及状态信息(红色:已登录;灰色:未登录;蓝色:考试中;绿色:已交卷)
- 点击已提交的考生对象,进入该考生的阅卷界面,显示该考生提交考卷和答案的信息,并且可以批阅该考卷的得分;
- 实时显示考生的状态
学生端:
- 考生登入系统后,若时间未到,显示倒计时,点击题号弹出警告框;若时间到,可进行答题
- 点击题号后进入答题状态,同时教师端会实时显示考生的状态;
- 点击其他题号答题自动保存;
- 考试时间到自动提交等;
预览
1. 数据结构定义
1.用户表
用户学号
userId
,姓名username
, 密码password
, 类型category
(学生/老师), 状态status
(初始,登录,答题,提交)
var userSchema = new Schema({
userId: String,
username: String,
password: String,
category: String, //分类-学生
status: String, //状态
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
2.答题表
题目内容
content
,分数score
var questionSchema = new Schema({
content: String,
score: Number,
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
3. 学生答题内容表
学生ID
userId
,问题IDquestionId
,回答内容answerCtn
, 批阅后得到的分数score
var answerSchema = new Schema({
userId: {type: ObjectId, ref: 'User'},
questionId: {type: ObjectId, ref: 'Question'},
answerCtn: String,
score: Number,
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
2. 教师端模块分解
2.1 学生管理
- 学生列表:查看已添加的学生学号和姓名
- 添加学生:添加新学生
2.2 题目管理
- 查看题目列表:点击题号显示保存的题目内容和分数,点击文本框修改内容
- 添加题目:添加新题目和分数
2.3 考试情况
- 学生考试状态:
- 实时查看学生的各种状态信息(红色:已登录;灰色:未登录;蓝色:考试中;绿色:已交卷)
- 可点击已交卷的学生块,进行对该学生的阅卷操作
- 学生考试成绩:查看学生的考试成绩信息
3. 学生端模块分解
3.1 倒计时模块
- 未到达开考时间显示 “距离考试开始” 的倒计时;
- 到达开考时间显示 “距离考试结束” 的倒计时,直到考试结束倒计时停止;
3.2 答题模块
- 若考试时间未到点击题号,弹出警告框(考试时间未到);
- 考试时间到学生点击题号进入答题状态,教师端更新学生状态;
- 考试未结束考生点击提交或者考试时间到,考生转换成提交状态,教师端更新学生状态,提交状态的考生无法继续答题;
4. 模块代码分析
4.1 登录检测
用户类型分为考生和教师,在登录时检测用户的类型,如果是教师则登入教师端页面,如果是考生则进入考生页面。
// result为登录成功返回的用户信息
if (result.data.category === "TEACHER") {
location.href = "/p/index";
} else {
location.href = "/p/indexStudent";
}
4.2 添加学生(添加题目方法类似)
//postData()为之后的post提供函数
function postData(url, data, cb) {
var promise = $.ajax({
type: "post",
url: url,
dataType: "json",
contentType: "application/json",
data:data
});
promise.done(cb);
}
//传递JSON
function doAddStudent() {
var jsonData = JSON.stringify({
'usrId': usrId,
'pwd': pwd,
'username': username
});
postData(urlAddStudent, jsonData, cbAddStudent);
}
//返回结果
function cbAddStudent(result) {
if (result.code == 99) {
alert(result.msg);
} else {
alert("添加成功!");
location.href = '/p/index';
}
}
4.3 阅卷(查看考题信息和学生答题模块方法类似)
进入页面通过POST从数据库获得题目列表,渲染出题号列表,每个题号给予一个
data-id
post获取题目列表,通过$.format(QUESTION_LIST, list[i]._id, i+1);
渲染每一个题号,添加到(".item-number"
里;
QUESTION_LIST模板
var QUESTION_LIST = "<div class='question-item' data-toggle='select' data-id='{0}'>{1}</div>";
//获取题目列表
function getQuestionList() {
var jsonData = JSON.stringify({});
postData(urlGetQuestionList, jsonData, cbQuestionList);
}
function cbQuestionList(result) {
var list = result.results;
for(var i = 0; i < list.length; i++) {
var html = $.format(QUESTION_LIST, list[i]._id, i+1);
$(".item-number").append(html);
}
}
点击题号获得data-id
$("body").on("click", "[data-toggle='select']", showContent);
//显示题目内容和学生答题内容
function showContent(e) {
$(".answer-wrap").removeClass("hide");
e.preventDefault();
var $this = $(this);
questionId = $this.data('id');
$("#question-head").text("第" + $(this).text() + "题");
getQuestionCtn();
getAnswerOne();
if(questionId != 0) {
saveMark();
}
}
post获取题目内容,返回结果放到指定div内
$("#questionContent").text(result.content);
//获取题目内容
function getQuestionCtn() {
var jsonData = JSON.stringify({
"_id": questionId
});
postData(urlGetQuestionCtn, jsonData, cbShowQuestionCtn);
}
function cbShowQuestionCtn(result) {
$("#que-score").text("分值:" + result.score);
$("#questionContent").text(result.content);
}
post获取学生答题内容,返回的结果放到指定div内
$("#answerCtn").text(result.answerCtn);
//获得学生答案
function getAnswerOne() {
var jsonData = JSON.stringify({
"userId": studentId,
"questionId": questionId
});
postData(urlGetAnswerOne, jsonData, cbShowAnswer);
}
function cbShowAnswer(result) {
if(result != "99") {
$("#give-score").val(result.score);
$("#answerCtn").text(result.answerCtn);
} else {
$("#answerCtn").text("该学生没有完成该题目");
}
}
4.4 考生端倒计时
- 将教师设定的开考时间和结束时间分别与当前时间比较,得到相差的时间差毫秒
seconds
。对seconds
进行处理得到格式化的字符串表示时间。
- 若当前时间小于开考时间,显示距离考试开始倒计时,到时间
seconds <= 0
,进入答题倒计时,显示距离考试结束倒计时,直到seconds <= 0
,停止倒计时并自动提交考卷,考生转换成提交状态SUBMIT
;
function getTimeDifference(y, n, M, h, m) {
var now = new Date();
var startTime = new Date(y, n, M, h, m);
var timeDifference = startTime.getTime() - now.getTime();
var second = parseInt(timeDifference / 1000);
var time = {
remain: second,
second: (second < 60) ? second : second % 60,
hour: parseInt(second / 3600),
minute: parseInt((second - parseInt(second / 3600) * 3600) / 60)
};
return time;
}
//考试开始时间
function timeBefore() {
var timer = setInterval(function() {
var time = getTimeDifference(2016, 10, 24, 18, 56);
$('#time-title').text("距离考试开始");
$('#time-ctn').text(time.hour + " : " + time.minute + " : " + time.second);
if(time.remain <= 0) {
status = START;
showExamTime();
clearInterval(timer);
}
}, 1000);
}
//考试结束倒计时
function showExamTime() {
var timer = setInterval(function() {
var time = getTimeDifference(2016, 10, 24, 23, 59);
$('#time-title').text("距离考试结束");
$('#time-ctn').text(time.hour + " : " + time.minute + " : " + time.second);
if(time.remain <= 0) {
status = END;
doUpdate(SUBMIT);
clearInterval(timer);
}
}, 1000);
}
5. 将考生端的考生状态实时更新到教师端
- 考生登录系统发送带有用户ID
userId
和用户类型category
的login
消息给服务器,服务器保存该用户(user[userId] = socket
),接着判断该用户是否为教师,若是则保存teacherId
;最后在数据库中将该用户状态更新为登录LOGIN
状态,向教师端发送reload
消息,教师端接收到后重新post获取学生状态;
- 开考时间到,考生处于可考试状态
WAIT
,考生点击题号转换成答题状态EXAM
,post到数据库更新状态EXAM
,同时向服务器发送状态转换消息update status
,服务器接收到后向教师端发送reload
消息`; - 考生点击提交按钮,数据库更新状态
SUBMIT
,同时向服务器发送状态转换消息update status
,服务器接收到后向教师端发送reload
消息`;
考生端
//socket初始化
function socketInit() {
var data = {
userId: userId,
userCategory: userCategory
};
socket.emit("login", data);
status = LOGIN;
}
function showQuestion(e) {
//如果为开考状态且用户不处于提交状态
if(status == START && userStatus != SUBMIT) {
getQuestionCtn(); //获取题目内容
getAnswerOne(); //获取保存的答题内容
doUpdate(EXAM); //转换为答题状态
} else if(userStatus == SUBMIT) {
alert("你已提交答卷,请等候老师批阅。");
} else if(status != START) {
alert("考试时间未到!");
}
}
//获取题目列表
function getQuestionCtn() {}
function cbShowQuestionCtn(result) {}
//保存答题内容
function doSaveAnswer() {}
//获取答题保存的内容
function getAnswerOne() {}
function cbShowAnswer(result) {}
//数据库更新用户状态SUBMIT,向教师端发送reload消息
function doUpdate(status) {
socket.emit("update status");
}
//转换成SUBMIT状态
function cbUpdateStatus(result) {
userStatus = SUBMIT;
}
服务器
io.on('connection', function(socket){
//用户登录
socket.on('login', function (data) {
socket.name = data.userId;
user[data.userId] = socket;
var data2 = {
userId: socket.name,
status: "LOGIN"
};
dbHelper.updateStatus(data2, function (success, doc) {});
//用户类型-老师
if(data.userCategory === "TEACHER") {
teacherId = data.userId;
}
//向老师的客户端发送重新加载命令
if(teacherId !== 0) {
user[teacherId].emit("reload");
}
});
socket.on('update status', function () {
if(teacherId !== 0) {
user[teacherId].emit("reload");
}
});
//用户退出
socket.on('disconnect', function () {
var data = {
userId: socket.name,
status: "INIT"
};
dbHelper.updateStatus(data, function (success, doc) {});
if(socket.name === teacherId) {
teacherId = 0;
} else if(teacherId !== 0){
user[teacherId].emit("reload");
}
delete user[socket.name];
});
});