工具:Node.js + MongoDB + Socket.IO
完成進(jìn)度
教師端:
- 學(xué)生的添加刪除等操作
- 考題和分?jǐn)?shù)的添加刪除編輯修改等操作
- 在查看考試情況頁面顯示所有考生的姓名和學(xué)號痹束,以及狀態(tài)信息(紅色:已登錄椿争;灰色:未登錄;藍(lán)色:考試中最域;綠色:已交卷)
- 點擊已提交的考生對象逗扒,進(jìn)入該考生的閱卷界面咆贬,顯示該考生提交考卷和答案的信息肄满,并且可以批閱該考卷的得分;
- 實時顯示考生的狀態(tài)
學(xué)生端:
- 考生登入系統(tǒng)后智政,若時間未到,顯示倒計時箱蝠,點擊題號彈出警告框续捂;若時間到,可進(jìn)行答題
- 點擊題號后進(jìn)入答題狀態(tài)宦搬,同時教師端會實時顯示考生的狀態(tài)牙瓢;
- 點擊其他題號答題自動保存;
- 考試時間到自動提交等间校;
預(yù)覽
1. 數(shù)據(jù)結(jié)構(gòu)定義
1.用戶表
用戶學(xué)號
userId
矾克,姓名username
, 密碼password
憔足, 類型category
(學(xué)生/老師)胁附, 狀態(tài)status
(初始,登錄滓彰,答題控妻,提交)
var userSchema = new Schema({
userId: String,
username: String,
password: String,
category: String, //分類-學(xué)生
status: String, //狀態(tài)
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
2.答題表
題目內(nèi)容
content
,分?jǐn)?shù)score
var questionSchema = new Schema({
content: String,
score: Number,
meta: {
updateAt: {type:Date, default: Date.now()},
createAt: {type:Date, default: Date.now()}
}
});
3. 學(xué)生答題內(nèi)容表
學(xué)生ID
userId
揭绑,問題IDquestionId
弓候,回答內(nèi)容answerCtn
, 批閱后得到的分?jǐn)?shù)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 學(xué)生管理
- 學(xué)生列表:查看已添加的學(xué)生學(xué)號和姓名
- 添加學(xué)生:添加新學(xué)生
2.2 題目管理
- 查看題目列表:點擊題號顯示保存的題目內(nèi)容和分?jǐn)?shù)他匪,點擊文本框修改內(nèi)容
- 添加題目:添加新題目和分?jǐn)?shù)
2.3 考試情況
- 學(xué)生考試狀態(tài):
- 實時查看學(xué)生的各種狀態(tài)信息(紅色:已登錄菇存;灰色:未登錄;藍(lán)色:考試中邦蜜;綠色:已交卷)
- 可點擊已交卷的學(xué)生塊依鸥,進(jìn)行對該學(xué)生的閱卷操作
- 學(xué)生考試成績:查看學(xué)生的考試成績信息
3. 學(xué)生端模塊分解
3.1 倒計時模塊
- 未到達(dá)開考時間顯示 “距離考試開始” 的倒計時;
- 到達(dá)開考時間顯示 “距離考試結(jié)束” 的倒計時畦徘,直到考試結(jié)束倒計時停止毕籽;
3.2 答題模塊
- 若考試時間未到點擊題號抬闯,彈出警告框(考試時間未到);
- 考試時間到學(xué)生點擊題號進(jìn)入答題狀態(tài)关筒,教師端更新學(xué)生狀態(tài)溶握;
- 考試未結(jié)束考生點擊提交或者考試時間到,考生轉(zhuǎn)換成提交狀態(tài)蒸播,教師端更新學(xué)生狀態(tài)睡榆,提交狀態(tài)的考生無法繼續(xù)答題;
4. 模塊代碼分析
4.1 登錄檢測
用戶類型分為考生和教師袍榆,在登錄時檢測用戶的類型胀屿,如果是教師則登入教師端頁面,如果是考生則進(jìn)入考生頁面包雀。
// result為登錄成功返回的用戶信息
if (result.data.category === "TEACHER") {
location.href = "/p/index";
} else {
location.href = "/p/indexStudent";
}
4.2 添加學(xué)生(添加題目方法類似)
//postData()為之后的post提供函數(shù)
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);
}
//返回結(jié)果
function cbAddStudent(result) {
if (result.code == 99) {
alert(result.msg);
} else {
alert("添加成功宿崭!");
location.href = '/p/index';
}
}
4.3 閱卷(查看考題信息和學(xué)生答題模塊方法類似)
進(jìn)入頁面通過POST從數(shù)據(jù)庫獲得題目列表,渲染出題號列表才写,每個題號給予一個
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);
//顯示題目內(nèi)容和學(xué)生答題內(nèi)容
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獲取題目內(nèi)容,返回結(jié)果放到指定div內(nèi)
$("#questionContent").text(result.content);
//獲取題目內(nèi)容
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獲取學(xué)生答題內(nèi)容讹堤,返回的結(jié)果放到指定div內(nèi)
$("#answerCtn").text(result.answerCtn);
//獲得學(xué)生答案
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("該學(xué)生沒有完成該題目");
}
}
4.4 考生端倒計時
- 將教師設(shè)定的開考時間和結(jié)束時間分別與當(dāng)前時間比較,得到相差的時間差毫秒
seconds
厨疙。對seconds
進(jìn)行處理得到格式化的字符串表示時間洲守。
- 若當(dāng)前時間小于開考時間,顯示距離考試開始倒計時沾凄,到時間
seconds <= 0
梗醇,進(jìn)入答題倒計時,顯示距離考試結(jié)束倒計時撒蟀,直到seconds <= 0
婴削,停止倒計時并自動提交考卷,考生轉(zhuǎn)換成提交狀態(tài)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);
}
//考試結(jié)束倒計時
function showExamTime() {
var timer = setInterval(function() {
var time = getTimeDifference(2016, 10, 24, 23, 59);
$('#time-title').text("距離考試結(jié)束");
$('#time-ctn').text(time.hour + " : " + time.minute + " : " + time.second);
if(time.remain <= 0) {
status = END;
doUpdate(SUBMIT);
clearInterval(timer);
}
}, 1000);
}
5. 將考生端的考生狀態(tài)實時更新到教師端
- 考生登錄系統(tǒng)發(fā)送帶有用戶ID
userId
和用戶類型category
的login
消息給服務(wù)器唉俗,服務(wù)器保存該用戶(user[userId] = socket
),接著判斷該用戶是否為教師配椭,若是則保存teacherId
虫溜;最后在數(shù)據(jù)庫中將該用戶狀態(tài)更新為登錄LOGIN
狀態(tài),向教師端發(fā)送reload
消息股缸,教師端接收到后重新post獲取學(xué)生狀態(tài);
- 開考時間到衡楞,考生處于可考試狀態(tài)
WAIT
,考生點擊題號轉(zhuǎn)換成答題狀態(tài)EXAM
敦姻,post到數(shù)據(jù)庫更新狀態(tài)EXAM
瘾境,同時向服務(wù)器發(fā)送狀態(tài)轉(zhuǎn)換消息update status
歧杏,服務(wù)器接收到后向教師端發(fā)送reload
消息`; - 考生點擊提交按鈕,數(shù)據(jù)庫更新狀態(tài)
SUBMIT
迷守,同時向服務(wù)器發(fā)送狀態(tài)轉(zhuǎn)換消息update status
犬绒,服務(wù)器接收到后向教師端發(fā)送reload
消息`;
考生端
//socket初始化
function socketInit() {
var data = {
userId: userId,
userCategory: userCategory
};
socket.emit("login", data);
status = LOGIN;
}
function showQuestion(e) {
//如果為開考狀態(tài)且用戶不處于提交狀態(tài)
if(status == START && userStatus != SUBMIT) {
getQuestionCtn(); //獲取題目內(nèi)容
getAnswerOne(); //獲取保存的答題內(nèi)容
doUpdate(EXAM); //轉(zhuǎn)換為答題狀態(tài)
} else if(userStatus == SUBMIT) {
alert("你已提交答卷,請等候老師批閱兑凿。");
} else if(status != START) {
alert("考試時間未到凯力!");
}
}
//獲取題目列表
function getQuestionCtn() {}
function cbShowQuestionCtn(result) {}
//保存答題內(nèi)容
function doSaveAnswer() {}
//獲取答題保存的內(nèi)容
function getAnswerOne() {}
function cbShowAnswer(result) {}
//數(shù)據(jù)庫更新用戶狀態(tài)SUBMIT,向教師端發(fā)送reload消息
function doUpdate(status) {
socket.emit("update status");
}
//轉(zhuǎn)換成SUBMIT狀態(tài)
function cbUpdateStatus(result) {
userStatus = SUBMIT;
}
服務(wù)器
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;
}
//向老師的客戶端發(fā)送重新加載命令
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];
});
});