在线考试系统之模块分析

工具:Node.js + MongoDB + Socket.IO

完成进度
教师端:
  1. 学生的添加删除等操作
  2. 考题和分数的添加删除编辑修改等操作
  3. 在查看考试情况页面显示所有考生的姓名和学号,以及状态信息(红色:已登录;灰色:未登录;蓝色:考试中;绿色:已交卷)
  4. 点击已提交的考生对象,进入该考生的阅卷界面,显示该考生提交考卷和答案的信息,并且可以批阅该考卷的得分;
  5. 实时显示考生的状态
学生端:
  1. 考生登入系统后,若时间未到,显示倒计时,点击题号弹出警告框;若时间到,可进行答题
  2. 点击题号后进入答题状态,同时教师端会实时显示考生的状态;
  3. 点击其他题号答题自动保存;
  4. 考试时间到自动提交等;
预览
教师端实时显示考生状态
教师端界面
学生端界面

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. 学生答题内容表

学生IDuserId,问题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 考试情况
  • 学生考试状态:
  1. 实时查看学生的各种状态信息(红色:已登录;灰色:未登录;蓝色:考试中;绿色:已交卷)
  2. 可点击已交卷的学生块,进行对该学生的阅卷操作
  • 学生考试成绩:查看学生的考试成绩信息

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 考生端倒计时
  1. 将教师设定的开考时间和结束时间分别与当前时间比较,得到相差的时间差毫秒seconds。对seconds进行处理得到格式化的字符串表示时间。
  1. 若当前时间小于开考时间,显示距离考试开始倒计时,到时间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. 将考生端的考生状态实时更新到教师端

  1. 考生登录系统发送带有用户IDuserId和用户类型categorylogin 消息给服务器,服务器保存该用户(user[userId] = socket),接着判断该用户是否为教师,若是则保存teacherId;最后在数据库中将该用户状态更新为登录LOGIN状态,向教师端发送reload消息,教师端接收到后重新post获取学生状态;
  1. 开考时间到,考生处于可考试状态WAIT,考生点击题号转换成答题状态EXAM,post到数据库更新状态EXAM,同时向服务器发送状态转换消息update status,服务器接收到后向教师端发送reload消息`;
  2. 考生点击提交按钮,数据库更新状态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];
  });
});

Github参考代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容