Node.js在線考試系統(tǒng)
1. 系統(tǒng)結(jié)構(gòu)
系統(tǒng)考慮使用Nodejs和SocketIo實(shí)現(xiàn)服務(wù)器端邏輯,前端使用HTML5,數(shù)據(jù)庫使用MongoDB
2.數(shù)據(jù)結(jié)構(gòu)
用戶表
var userSchema = new Schema({
userId:String, //用戶ID
username: String, //用戶名稱
password: String, //賬戶密碼,初始化為a
status:String, //用戶狀態(tài),分為四個(gè)狀態(tài),在線landing,離線offline,提交完畢submited,考試中testing
identity:String, //用戶身份,分別為student和teacher
});
科目表
var subjectsSchema = new Schema({
course:String, //考試科目名稱
startTime:Date, //考試開始時(shí)間
lastTime:Number, //考試持續(xù)時(shí)間,單位為分
numOfQuestions:Number,//問題總數(shù)
questions:[{ //問題
index:Number, //當(dāng)前問題序號(hào)
question:String,//問題內(nèi)容
score:Number //問題分值
}]
});
答題表
var answersSchema = new Schema({
user:{type:Schema.Types.ObjectId, ref:'User' },//用戶表引用
subject:{type:Schema.Types.ObjectId,ref:'Subjects'},//科目表引用
questions:[{
index:Number,//當(dāng)前問題序號(hào)
content:String,//答案
score:Number //后期打分分值
}]
});
3.系統(tǒng)模塊設(shè)計(jì)
3.1前端
3.1.1學(xué)生模塊
- 繪制倒計(jì)時(shí)界面
* 提示學(xué)生還有多少時(shí)間開始考試
* 如果考試時(shí)間未到,學(xué)生點(diǎn)擊開始考試會(huì)彈出"未到考試時(shí)間"提示框
* 開始考試,學(xué)生點(diǎn)擊開始考試按鈕,進(jìn)入考試界面并且發(fā)送消息給服務(wù)器,服務(wù)器轉(zhuǎn)發(fā)給教師端,更新教師端當(dāng)前學(xué)生狀態(tài)

- 繪制答題界面
- 繪制題目下標(biāo)、題目標(biāo)題、題目內(nèi)容
- 繪制答題框
- 綁定上一題、提交答案、下一題三個(gè)按鈕點(diǎn)擊事件
-
存儲(chǔ)數(shù)據(jù)
答題界面.jpg - 提交答案
- 發(fā)送學(xué)生ID、題目序號(hào)、題目答案到服務(wù)器,服務(wù)器存入數(shù)據(jù)庫
3.1.2教師模塊
- 繪制學(xué)生狀態(tài)界面
* 將每一個(gè)學(xué)生的名字與學(xué)生ID作為一個(gè)單位,有序的排列在界面上
* 綁定點(diǎn)擊學(xué)生信息單元,進(jìn)入對(duì)應(yīng)學(xué)生的打分界面
* 通過socket實(shí)時(shí)更新學(xué)生狀態(tài)

- 繪制打分界面
- 主要調(diào)用學(xué)生模塊的繪制答題界面函數(shù)
- 添加打分欄
-
更改提交答案按鈕為打分按鈕=
改成績界面
- 存儲(chǔ)數(shù)據(jù)
3.1.3客戶端socket模塊
- 發(fā)送數(shù)據(jù)模塊
* 向服務(wù)器提交學(xué)生考試答案(學(xué)生端)
* 向服務(wù)器提交教師所改學(xué)生答卷題目分?jǐn)?shù)(教師端)
* 向服務(wù)器提交開始考試信息(學(xué)生端)
* 向服務(wù)器提交結(jié)束考試信息(學(xué)生端)
- 接受數(shù)據(jù)模塊
- 接受來自服務(wù)器發(fā)送的確認(rèn)登錄消息(學(xué)生端、教師端)
- 接受來自服務(wù)器發(fā)送的狀態(tài)改變消息(教師端)
3.2后臺(tái)
3.2.1數(shù)據(jù)庫操作模塊
- 向登錄的學(xué)生發(fā)送數(shù)據(jù)(考試開始時(shí)間,考試內(nèi)容)
- 向登錄的教師發(fā)送數(shù)據(jù)(學(xué)生列表,學(xué)生考試答案)
- 更改用戶狀態(tài)(在線、進(jìn)行考試、結(jié)束考試、離線)
- 存儲(chǔ)學(xué)生考卷答案
- 存儲(chǔ)學(xué)生考卷題目得分
3.2.2服務(wù)器socket模塊
- 接收來自客戶端所提交的學(xué)生考試答案,并調(diào)用數(shù)據(jù)庫模塊存儲(chǔ)學(xué)生考卷答案
- 接收來自客戶端提交的教師所改學(xué)生答卷題目分?jǐn)?shù),并調(diào)用數(shù)據(jù)庫模塊存儲(chǔ)學(xué)生考卷得分
。。。。。。
服務(wù)器socket模塊實(shí)現(xiàn)的功能與客戶端socket模塊相對(duì)應(yīng)
4.功能分析
學(xué)生模塊與教師模塊中的界面繪制之前已有文章提及,這里略去
學(xué)生模塊的本地?cái)?shù)據(jù)存儲(chǔ)功能
學(xué)生登錄->
后臺(tái)路由發(fā)送數(shù)據(jù)data->
router.get('/student', function(req, res, next) {
dbHelper.findUsrInfo(req, function (success, data) {
//console.log(data.friends);
//data.identify身份,data.subject考試題目,data.answer考試答案
res.render('student', {
scriptData:JSON.stringify(data),
data:data
});
})
});
因?yàn)榉?wù)器應(yīng)用express模塊,data數(shù)據(jù)用于渲染前端頁面,但無法被js文件獲取到,所以需要經(jīng)過處理,data數(shù)據(jù)被轉(zhuǎn)換為json格式,并存入window.scriptData->
<script>
window.scriptData = JSON.stringify({{{scriptData}}});
window.scriptData=eval("("+window.scriptData+")");//轉(zhuǎn)換為json對(duì)象
</script>
學(xué)生成功登錄后,進(jìn)入準(zhǔn)備開始考試界面,運(yùn)行modalBox.init()函數(shù),并將modalBox對(duì)象并賦值給STUDENT,default存儲(chǔ)本地?cái)?shù)據(jù),window.scriptData.subject.questions即為服務(wù)器所發(fā)考試相關(guān)數(shù)據(jù)中的題目對(duì)象->
var STUDENT;
new modal_student();
var modal_student = function () {
var modalBox = {
default:{
questions:[],//題目對(duì)象
answers:[],//學(xué)生所寫答案
index:0//當(dāng)前題目編號(hào),
},
init:function () {
ptrThis = this;
ptrThis.setQuestions();
},
setQuestions:function () {
ptrThis.default.questions = window.scriptData.subject.questions;
console.log(ptrThis.default.questions);
},
}
STUDENT = modalBox;
return modalBox.init();
}
今后學(xué)生切換題目時(shí)只需要從本地獲取數(shù)據(jù),而不需要多次與服務(wù)器進(jìn)行請(qǐng)求
教師模塊的本地?cái)?shù)據(jù)存儲(chǔ)功能
教師登錄->
后臺(tái)發(fā)送數(shù)據(jù)data->
router.get('/teacher', function(req, res, next) {
dbHelper.findStudentsInfo(req, function (success, data) {
//console.log(data.friends);
//data.students學(xué)生信息,data.subject課程信息
res.render('teacher', {
students:data.students,
scriptData:JSON.stringify(data)
});
})
});
與上同->
<script>
window.scriptData = JSON.stringify({{{scriptData}}});
window.scriptData=eval("("+window.scriptData+")");//轉(zhuǎn)換為json對(duì)象
</script>
教師成功登錄后,進(jìn)入瀏覽學(xué)生名單界面,運(yùn)行modalBox.init()函數(shù),并將modalBox對(duì)象并賦值給TEACHER->
var TEACHER;
new modal_teacher();
var modal_teacher = function () {
var ptrThis;
var modalBox = {
default:{
studentIndex:0,//當(dāng)前正在進(jìn)行批改的學(xué)生的下標(biāo)
studentsList:[]
},
init:function () {
ptrThis = this;
ptrThis.default.studentsList = window.scriptData.students;
}
}
TEACHER = modalBox;
return modalBox.init();
}
studentsList存儲(chǔ)學(xué)生信息列表,包含學(xué)生姓名、學(xué)號(hào)、狀態(tài),以及學(xué)生每道題目的答案
studentsList格式
var studentList = [{
user:{
username:String,//學(xué)生姓名
userId:String,//學(xué)生ID
status:String,//學(xué)生狀態(tài)
},
questions:[{
content:String,//答案
index:Number//當(dāng)前題目下標(biāo)
}]
}]
學(xué)生交卷
modalBox.bindEnd()綁定了結(jié)束考試button的點(diǎn)擊事件->
bindEnd:function () {
var end = document.getElementById("end");
end.addEventListener("click",function () {
ptrThis.saveAnswer();//將當(dāng)前答案保存到本地文件
socketFun.end();//調(diào)用客戶端socket模塊的end()函數(shù)
});
},
點(diǎn)擊事件觸發(fā),調(diào)用客戶端socket模塊的end()函數(shù)->
var socket = io.connect('http://localhost:3000');
var X = window.scriptData; //截取服務(wù)器發(fā)送過來的數(shù)據(jù)
var socketFun = {
//省略
end:function () {
var obj = {
userId:X.userId,
indentify:X.identify
}
obj ["answers"] = STUDENT.default.answers;
if(X.status=="submited"){
alert("你已經(jīng)提交過答案!");
}else {
socket.emit("end", obj );
}
}
}
客戶端socket模塊將學(xué)生學(xué)號(hào)userId、學(xué)生身份identify、學(xué)生答案answers發(fā)送到服務(wù)器->
socket.on("end",function (result) {
User.update({userId:result.userId},{status:"submited"}).exec(function (err,doc) {//更新數(shù)據(jù)庫中的學(xué)生狀態(tài)為submited(提交答案)
User.findOne({userId:result.userId}).exec(function (err,doc) {//根據(jù)學(xué)生ID查詢?cè)摋l學(xué)生記錄
var change = {
studentId:result.userId,
status:"submited"
};
if(teacherOnline){//如果教師在線,則向其發(fā)送學(xué)生狀態(tài)改變的消息
teacher.emit("statusChange",change);
}
Answers.findOne({user:doc._id}).exec(function (err,answer) {//答題表根據(jù)學(xué)生記錄的_id查詢到對(duì)應(yīng)記錄,更新答案
answer.questions = result.answers;
answer.save(function (err) {})
})
});
})
}),
教師模塊通過socket實(shí)時(shí)更新學(xué)生狀態(tài)
客戶端socket模塊接收到來自服務(wù)器的學(xué)生狀態(tài)改變信息->
var socket = io.connect('http://localhost:3000');
socket.on("statusChange",function (change) {
TEACHER.changeStatus(change);
})
調(diào)用教師模塊的changeStatus函數(shù),以change為參數(shù)->
changeStatus:function (change) {
console.log(ptrThis.default.studentsList);
for(var i = 0;i<ptrThis.default.studentsList.length;++i){//遍歷當(dāng)前學(xué)生列表
console.log(ptrThis.default.studentsList[i].user.userId+"與"+change.studentId);
if(ptrThis.default.studentsList[i].user.userId==change.studentId){//重新設(shè)置status
ptrThis.default.studentsList[i].user.status = change.status;
var studentInfo = document.getElementsByClassName("studentInfo");
studentInfo[i].setAttribute("class","studentInfo "+change.status);//修改界面上學(xué)生列表中對(duì)應(yīng)的學(xué)生的狀態(tài)(改變顏色以提示教師有學(xué)生狀態(tài)改變)
break;
}
}
},
5.具體代碼見
github下的onlineTest項(xiàng)目

