程序员

OnlineTest项目

主要路由:

  • 172.31.214.212:7000/signup—注册页面
  • 172.31.214.212:7000/signin—登陆页面
  • 172.31.214.212:7000/wait—等待页面
  • 172.31.214.212:7000/test—考试页面
  • 172.31.214.212:7000/userlist—用户列表页面
  • 172.31.214.212:7000/user/:id—用户详情页面,展示所有用户回答的问题,
    管理员可以打分。如果打完分就可以查看所有的总分
  • 172.31.214.212:7000/questionlist—问题列表页面(按课程分类)
  • 172.31.214.212:7000/class/:id—查看具体某一门课程的考试内容
  • 172.31.214.212:7000/question/:id—查看具体某一个问题的考试内容,并作答,
    如果已经回答,显示答案

项目用户

  • 管理员
    • 学号: 2014211500
    • 用户名: why
    • 密码:1234
  • 用户
    • 学号: 2014211400
    • 用户名:tom
    • 密码: a

默认注册的用户都是用户,即权限都是0,要手动在命令行更改用户权限


这几天要把原先的项目都放到自己的服务器上,但是发现暑假写的blog出现了问题,可以运行但是不能用之前写的路由访问网页。
于是找到原先的github网站,把最后一份成功的代码git clone下来,所以也就认识到,项目托管的重要性了。

1.项目托管

1.利用github托管工具,进行代码的托管。到github官网www.github.com注册一个账号.

2.新建一个代码仓库new Repository,选中自动生成一个README.md文档。
将这个网址拷贝下来.。

git clone 工具仓库的网址

3.这样就会在本地生成一个目录,目录下会有.git隐藏文件夹和一个生成的README.md文件.我们将.git文件夹拷贝到自己项目下。

4.然后每天只要三步就可以记录下自己每天实现的功能。

  1. git add .
  2. git commit -m “新增XXXX功能.”
  3. git push origin master.

每天提交一份,网速快的话,可能2分钟就搞定了。而且看着自己的commit次数,都有很大的满足感呢!!!这学期效率低下,甚至觉得写代码太痛苦,都可能是因为自己没有代码的积累呢~~~嘤嘤嘤(让我哭一会儿)。

2.数据库的操作。

之前对数据库有一种莫名的恐惧感,但是重温了Scott老师的幕课教程之后,发现其实数据库最常用的几种语句也是所有数据库最基本的指令

  • 增(new+save)!
  • 删(remove)!
  • 改(update+set)!
  • 查(find, findOne, findById)!

1.首先打开mongodb数据库,在命令行打入

mongo

出现下面的指令,就可以进行数据库的操作

MongoDb shell version:3.2.7
connection to: test
>

2.打开本地的数据库, onlineTest是数据库名

> use onlineTest
switch to db onlineTest---表名已经在使用onlineTest数据库

3.查找数据库的内容

> db.users.find()
下面显示的就是users表里面的每一个user数据
{ "_id" : ObjectId("5854ecdd225ecb0bf48a743c"), "ID" : "2014211500", "name" : "Bob", "password" : "$2a$10$Tl5KU92XlXzm.7XTpnMOvuj.2Iy3XXxW6wWM7ODvquUTqMBIru6QW", "meta" : { "createAt" : ISODate("2016-12-17T07:44:29.417Z"), "updateAt" : ISODate("2016-12-17T07:48:02.853Z") }, "answers" : [ ObjectId("5854edb2b1cb7517b806ceaf") ], "authority" : 1, "status" : "TESTING", "__v" : 1 }

4.查找单条数据

查找_id为ObjectId(....)的一条记录
>db.users.findOne({'_id': ObjectId("5854ecdd225ecb0bf48a743c")})

5.更新数据

将用户的权限设置为1, 如果未存在,那么回自动创建这个字段,如果已存在,那么会更新这个字段
>db.users.update({'_id': ObjectId("5854ecdd225ecb0bf48a743c")}, {$set: {authority: 1}})
显示一条匹配, 一条更新
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

6.删除数据

删除users表中的所有数据
>db.users.remove({})
显示删除了一个数据
WriteResult({ "nRemoved" : 1 })

数据库操作还有很多,基本的,常用的就是上面几条


新建一个classSchema

var mongoose = require('../db');
var Schema = mongoose.Schema;
//ObjectId是mongodb默认的主键,每生成一个字段,
数据库都会默认新建一个_id,它的类型就是ObjectId, 而且是唯一的
mongoose也是通过这个字段来查找每条记录
var ObjectId = Schema.Types.ObjectId;//关联文档的查询_id

//加盐
var bcrypt = require('bcrypt-nodejs');
//加盐参数,默认也是10
var SALT_WORK_FACTOR = 10;

var userSchema = new Schema({
    //唯一的学号,类型是String
    ID: {unique: true, type: String},//学号
    name: {unique: true, type: String},
    password: String,
    //学生状态,默认是OFFLINE离线
    status: {type: String, default: "OFFLINE" },//状态 OFFLINE| ONLINE | TESTING | FINISHED
    authority: {type: Number, default: 0}, //0-学生,1-教师
    //这个学生所有的回答
    answers: [{type: ObjectId, ref: 'Answer'}],
    totalScore: Number,//总分
    meta: {
        updateAt: {
            type:Date,
            default: Date.now()
        },
        createAt: {
            type:Date,
            default: Date.now()
        }
    }
});

//保存
userSchema.pre('save', function (next) {
    var user = this;
    if(this.isNew) {
        this.meta.createAt = this.meta.updateAt = Date.now();
    }else{
        this.meta.updateAt = Date.now();
    }
    //给密码加盐,用hash之后的密文来代替原先的明文密码
    bcrypt.genSalt(SALT_WORK_FACTOR,function(err,salt){
        if(err) return next(err);
        bcrypt.hash(user.password, salt, null, function(err,hash) {
            if(err) return next(err);
            user.password = hash;
            next();
        })
    });
});

//实例方法
//由于hash是不可逆的,因此只能再次将输入的密码加盐hash,来查看两者是否匹配(学了一学期的密码学还是蛮有用的)
userSchema.methods = {
    comparePassword: function(_password, cb){
        bcrypt.compare(_password, this.password, function(err, isMatch){
           if(err){
               return cb(err);
           }else {
               cb(null, isMatch);
           }
        })
    }
};

//静态方法
userSchema.statics = {
    //按照时间先后,查询所有的问题
    fetch: function(cb){
        return this
            .find({})
            .sort('meta.updateAt')
            .exec(cb)
    },
    //根据id查询单条数据
    findById: function(id, cb){
        return this
            .findOne({_id: id})
            .exec(cb)
    }
};
将Class作为一个模型导出
module.exports = mongoose.model('User', userSchema);

3.前段和后台交互

将页面的表单传递到后台数据
首先允许bodyParser这个中间件,用来获取从页面提交过来的表单数据,默认是false,我们要将它修改为true

app.use(bodyParser.urlencoded({ extended: true }));
  • form 表单数据传递方式的POST,传递的路由是/user/signup
  • 传递过来的数据就是name字段,name="user[ID]",在后台数据库获取的就是user.ID
  • form-control来控制字段,type='submit'的按钮来提交数据
注册

后台页面获取数据

router.post('/user/signup', function(req, res){
  //从表单传递过来的user数据
  var _user = req.body.user;
  //用传递过来的数据,来新建一个User记录
  var user = new User(_user);
  //初始化user的状态是OFFLINE
  //user.status = "OFFLINE";
  user.save(function(err, user){
    if(err){
      console.log(err);
    }
    //重定向到登陆界面
    res.redirect('/signin');
  });
});

登陆的方式和注册的方式其实是一样的,但是登陆需要找到对应的字段来判断用户名,密码等等是否匹配

router.post('/user/signin', function(req,res){
  var _user = req.body.user;
  //var io = global.io; 

  //通过ID来找到唯一的数据
  User.findOne({'ID': _user.ID}, function(err,user){
    if(err){
      console.log(err);
    }
    if(!user){//没有这个用户
      return res.redirect('/signup');//注册界面
    }
    //调用user的密码匹配方法来判断密码是否匹配
    user.comparePassword(_user.password, function(err, isMatch){
      if(err){
        console.log(err);
      }
      if(isMatch){
        user.status = 'ONLINE';
        //更新用户的状态为ONLINE
        User.update({'_id': user._id }, {$set: {status: "ONLINE"}}, function(err){
          if(err){
            console.log(err);
          }
          //将登陆的用户存储到一个session本地变量里面,这样就可以不同的路,通过访问session来得到当前登录的用户
          req.session.user = user;
          // io.emit('online', user);
          );
          //用不同的权限来跳转到不同的界面
          if(user.authority>0){
            return res.redirect('/admin/userlist');//全部用户界面
          }else {
            return res.redirect('/wait');//全部界面
          }
        });
      }
      else {
        // 用户密码不匹配,跳转到登陆界面,重新输入密码
        return res.redirect('/signin');
      }
    })
  })
});

上面提到了一个session来存储当前登陆的用户

//引入express-session,来引入session
var session = require('express-session');

//加入session支持,顺序很重要,踩过的坑要牢牢记住,要放到router之前
app.use(session({
  name:'onlineTest',
  maxAge: 30 * 1000,
  secret: 'web-test-secret-key',
  resave: false,
  saveUninitialized: true
}));

// 数据持久化
var mongoStore = require('connect-mongo')(session);

在saveUninitialized字段后面增加一个store对象
store: new mongoStore({
  url: dbUrl,
   collection: 'sessions'
})

登陆权限认证

signin.isSignined
/*登陆验证中间件*/
'use strict';
module.exports = {
    isSignined: function (req, res, next) {
        if(typeof(req.session.user) != 'undefined') {
            return next();
        }else{
            res.redirect('/signin');
        }
    }
};

用户权限认证

authority.isAuthenticated
'use strict';
/*权限验证中间件*/
module.exports = {
    isAuthenticated: function (req, res, next) {
        if(req.session.user.authority>0) {
            return next();
        }else{
            console.log('只有管理员能进入');
            res.redirect('/test');
        }
    }
};

渲染用户列表界面

从数据库取出所有的数据,显示在页面上
路由是/userlist, 对应的姐main是admin文件夹下的userlist.hbs界面
User.fetch()从数据库取出所有的users,取出来的users是一个对象数组,每一个条都是个用户对象。
router.get('/userlist', function(req, res){
  User.fetch(function(err, users){
    if(err){
      console.log(err);
    }
    res.render('admin/userlist', {    //渲染test界面
      user: req.session.user,
      title: '用户列表页',
      users: users
    });
  })
});

在页面上渲染出来使用handlebars,

  • {{each users}}每一条user数据{{/each}}
  • {{if user}}
    只能判断数据是不是存在,这个是缺陷,但是可以自定义helper来使用
    {{/if}}
{{#each users}}
   {{#if user}}
       学号{{ID}}
       姓名{{name}}
       状态{{status}}
       权限{{authority}}
       详细信息
   {{else}}
        用户不存在
   {{/if}}
{{/each}}

得到每一个user的详细信息

router.get('/user/:id', function(req, res) {
  //通过路由获取_id
  var id = req.params.id;
  var totalScore = 0;
  User
      .findOne({_id: id}) 
      //通过populate的方式,找到user下的answers数组里面的每一个answer对应的数据,最后出来的answers是一个对象数组,限制每一页的长度是5条记录
      .populate({path: 'answers', options: {limit:5}})
      .exec(function(err, user){
        //通过遍历answers数组,来计算最后的总分
        for(var i=0;i

那么我们是怎么获取到user下面的answer数据的呢,这就需要我们在回答问题的时候,将答案插入到user的answers数组下面

router.post('/answer', function(req,res){
  var id = req.body.answer._id;
  var answerObj = req.body.answer;//回答对象
  var questionId = answerObj.question;//问题对象
  var userId = req.session.user._id;//作答的用户
  var classId = req.session.class._id;//作答的题目属于哪一门科目
  var _answer;
  _answer = new Answer(answerObj);
  _answer.save(function(err, answer){
      if(err){
        console.log(err);
      }
      console.log(answer);//打印存储好的问题对象
      //找到当前作答的用户
      User.findById(userId, function(err, userObj){
        //将存好的问题的_id存储到answers数组liam
        userObj.answers.push(answer._id);
        //再将数据保存
        userObj.save(function(err, userObj){
          //渲染到对应科目下的问题页面
          res.redirect('/class/'+classId);
        })
      })
  })
});

以此类推,我们需要实现一个Class课程表
class表下有一个questions数组

className: String,
questions: [{type: ObjectId, ref: 'Question'}],

然后按照课程表,产生Question表

uestionId: String,//题号
questionCtn: String,//题目信息
score: String,//对应分数
correctCtn: String, //正确答案
user: {type: ObjectId, ref: 'User'},//和用户双向绑定

然后再按照Answer表,Answer表要有问题和用户两个外键

question: {type: ObjectId, ref: 'Question'},
user: {type: ObjectId, ref: 'User'},

修改用户权限,可以在命令行输入
db.users.find({'_id': Object(_id)}, {$set: {authority: 2}})

用户有四个状态,每次在页面跳转之前都要先更新一下数据库
登出将用户状态设置为OFFLINE,再跳转到登陆界面

router.get('/logout', function(req, res){
  User.update(
      {'_id': req.session.user._id},
      {$set: {status: 'OFFLINE'}},
      function(err){
        console.log("req.session.user");
        console.log(req.session.user);
        delete req.session.user;
        res.redirect('/signin');
      });
});

等待界面是WIATING状态

router.get('/wait', function(req, res){
  User.update(
      {'_id': req.session.user._id},
      {$set: {status: 'WAITING'}},
      function(err){
        if(err){
          console.log(err);
        }
        console.log("req.session.user");
        console.log(req.session.user);
        res.render('wait', {
          user: req.session.user
        });
      });
});

考试界面是TESTING状态

router.get('/test', function(req, res){
    Class
        .find({})
        .populate({path: 'questions', options: {limit:5}})
        .exec(function(err, classes){
          if(req.session.user){
            User.update({'_id': req.session.user._id}, {$set: {status: "TESTING"}}, function(err){
              res.render('index', {
                title: '首页',
                classes: classes,
                user: req.session.user
              });
            })
          }
        });
});

考试完成之后是FINISHED状态

通过Socket来实现用户状态实时更新
github地址