团队作业总结(byPB16061082耿子钢):
本次项目做的是一个作业管理系统,包括教师端和学生端,主要功能简单来说为教师通过教师端来上传学生名单,发布作业,批改作业,管理作业这样一个平台,学生通过学生端来看自己被布置的作业,提交作业这几个功能。
我们项目后端是用nodejs+mysql来做的,后端主要负责:前后端的交互,mysql数据库的设计,数据库的增删改查,前端的动态渲染,数据的管理还有一些其他的补充功能。接下来我分部分介绍我们的项目。
前后端的交互:
一。首先配置好proxytable,来实现代理和跨域,这样的话一个后端的路由就可以处理来自两个url的请求了:
1 dev: { 2 env: require('./dev.env'), 3 host:'localhost', 4 port: 8080, 5 autoOpenBrowser: true, 6 assetsSubDirectory: 'static', 7 assetsPublicPath: '/', 8 proxyTable: { 9 '/api/' : { 10 target:'http://localhost:3000/', 11 changeOrigin: true, 12 } 13 }, 14 cssSourceMap: false 15 },
二。写好后端接受的代码:这样的话后端接收到post请求可以进行处理:
1 const userApi = require('./api/userApi'); 2 const fs = require('fs'); 3 const path = require('path'); 4 const bodyParser = require('body-parser'); 5 const cookieParser = require('cookie-parser') 6 const cors = require('cors'); 7 const express = require('express'); 8 const app = express(); 9 10 app.use(cors({ 11 origin:['http://localhost:8080'], 12 methods:['GET','POST'], 13 alloweHeaders:['Conten-Type', 'Authorization'] 14 })); 15 16 app.set('port', (process.env.port || 3000)) 17 app.use(bodyParser.json()) 18 app.use(bodyParser.urlencoded({extended: false})) 19 app.use(cookieParser()) 20 21 app.use(userApi) 22 23 app.listen(app.get('port'), function () { 24 console.log('Visit http://localhost:' + app.get('port')) 25 }) 26 27 router.post('/api/login', function (req, res) { 28 let userName = req.body.username, 29 password = req.body.password, 30 resBody = {state: ''} 31 if (userName !== undefined && userName.length > 0) { 32 conn.query("SELECT * FROM users WHERE ?", {name: userName}, function (err, doc) { 33 if (err) { 34 resBody.state = '账号不存在'; 35 res.send(resBody); 36 } else { 37 if (doc.length === 0) { 38 resBody.state = '账号不存在'; 39 res.send(resBody); 40 } else { 41 console.log(doc); 42 resBody.state = '登录成功'; 43 res.send(resBody); 44 } 45 } 46 }) 47 } 48 else { 49 resBody = {state: '请输入用户名'} 50 res.send(resBody) 51 } 52 });
MYSQL数据库的设计:
最终的数据库:5个表:users-class-student homework_single-homework_file
users:主要记录了用户在注册时的一些信息,一些比较特殊的地方如下解释:
①老师学生以is_stu这一项来区分
②enable在点开注册邮件激活可以登录。
class:主要记录一个课程,包含了以下信息:课程名称,任课老师,上课时间,课程的学生,课程的公告。
主要用于老师的第一个界面--teacherclass界面的动态渲染。
student:看到这里可能有人要问,明明users已经记录了所有的信息,为什么还要有student这个table,我解释一下:
student对于我们的项目是至关重要的,虽然他只比users多了一个信息:class,也就是这个学生属于的班级,但是有了这个很多事情可以简化。
比如说如果一个学生属于多个课程,那么就有多个student对应于这个学生。但如果多个users的话,就不合理了,因为注册时有检查一个学号,一个邮箱只能有一次注册。
这个表可以说很有用了,在加载学生的作业界面时思路就是,先查到学生所在的所有班级,在查到这些班级的所有作业,显示出来。
homework_single:这个table主要记录了老师布置的一次作业。它的主要信息有:作业名称,截止日期,归属于哪个班级,作业要求,上交数量,没交人数,布置时间等等。归属于哪个班级,记录的是归属班级的id,因为id是独一无二的。
homework_file:记录的是学生提交的单次作业,主要记录了作业路径,学生姓名,上交时间,作业标题,是否已经批改。分数与评语。
感想:
①数据库的设计是个特别特别关键的步骤。一招不慎满盘皆输。设计一个好的数据库可以让自己的代码更简洁,思路更完美。如果设计不好的话就各种想办法,有时候就走进了死胡同。
②我们这个项目的主要特点是层次特别多,也比较凌乱。因为一个同学属于一个班级也属于一次作业,一次作业也属于一个班级。所以,数据库尽量信息要丰富,不能单纯为了追求数据库的简洁,而无意中增加了查询数据库的层次。
③如果需要一个表中保留一个信息来在另一个表中查询数据的话,ex:在student里面保存他的class的一个标志信息,用来从class表中查更多信息,我建议保存id,因为只有id是独一无二的,其他不管学号啊,邮箱啊,都不靠谱。
数据库的增删改查+前端的动态渲染:
经典的程序:选取了动态渲染老师的作业界面来展示:
前端:
1 created() { 2 this.$http.post('api/gethws', { 3 }).then((response) => { 4 let name = response.body.name; 5 let deadline = response.body.deadline; 6 let id = response.body.id; 7 let announcement = response.body.announcement; 8 let nohand = 0; 9 let hand = 0; 10 let nodata = []; 11 let data = []; 12 let final = id[id.length-1]; 13 for (let i = 0; i < id.length; i++) { 14 this.$http.post('api/getidh', { 15 id: id[i], 16 }).then((response) => { 17 var obj ={}; 18 let body1 = response.data; 19 let temp1 = body1.data; 20 if(response.body.state === "未交作业"){ 21 obj.name2 = name[i]; 22 obj.demand = announcement[i]; 23 obj.deadline = deadline[i]; 24 nodata[nohand] = obj; 25 nohand++; 26 } 27 else if(response.body.state === "已交作业"){ 28 obj.name = name[i]; 29 obj.title = temp1[0].hwname; 30 if(temp1[0].already === 0) obj.scores = '未评分'; 31 else obj.scores = temp1[0].grade; 32 if(temp1[0].already === 0|temp1[0].comment ===' ') obj.comment = '没有评语'; 33 else obj.comment = temp1[0].comment; 34 data[hand] =obj; 35 hand++; 36 } 37 if(hand+nohand === id.length){ 38 this.scoresTable = data; 39 this.undoTable = nodata; 40 } 41 }, (response) => { 42 console.log(response) 43 } 44 ) 45 } 46 }, (response) => { 47 console.log(response) 48 }) 49 },
后端:
1 router.post('/api/posthws', function (req, res) { 2 var cookies = req.headers.cookie; 3 req.cookies = Object.create(null); 4 req.cookies = cookie.parse(cookies); 5 6 let xuehao = req.cookies.xuehao; 7 let homeworkid = req.cookies.hwids; 8 let title = req.body.title; 9 let remark = req.body.remark; 10 let content = req.body.content; 11 let student = req.cookies.userName; 12 let handin_time = sd.format(new Date(), 'YYYY-MM-DD HH:mm:ss'); 13 let already = 0; 14 let resBody = {state:'hhh'}; 15 16 let sql_1 = {sql:'select * from homework_file Where homeworkid=? AND student=?'}; 17 let param_1 = [homeworkid,xuehao]; 18 19 conn.query(sql_1, param_1, function(err, doc){ 20 if(err){ 21 resBody.state = 'error:交互失败'; 22 return res.send(resBody); 23 }else{ 24 if (doc.length !== 0) { 25 resBody.state = 'error:已经存在'; 26 return res.send(resBody); 27 } 28 let sql_2 = {sql:'insert into homework_file (filepath,student,hwname,handin_time,homeworkid,xuehao,already) VALUES (?, ?, ?, ?, ?, ?, ?)'}; 29 let param_2 = [content,student,title,handin_time,homeworkid,xuehao,already]; 30 31 conn.query(sql_2, param_2, function(err){ 32 if(err){ 33 resBody.state = 'error:提交失败'; 34 return res.send(resBody); 35 }else { 36 resBody.state = 'success'; 37 return res.send(resBody); 38 } 39 }) 40 } 41 }) 42 });
感想:
①not null最好要慎用,因为你insert一个数据的时候,如果要求notnull,而你没加这一项,就会报错。
②在查询数据比较两个数据是否一样时,一定要注意数据库中数据的类型:比如说int与varchar肯定不一样,输出类型会看到一个是num,一个是string,在这个上面踩得坑特别多。更坑的是char和varchar都不一样。
③删除一个东西的时候,要把和他相关的都删干净。比如说删除一个班级,就要把homework_single老师在这个班级下面布置的所有作业都删掉,也要把学生homework_file在这个班级下的也删掉,还有student中属于这个班级的。
④改数据库觉得一次只能改一个column。还有就是一定要注意数据库的命名,因为所有命名有前端发送时的命名,有后端接收后的命名,还有数据库的命名,很容易混淆。一个不好的命名可能会浪费你一下午的时间。
其他一些附加功能:
发邮件的功能+一键提醒的功能:
1 router.post('/api/sendmail', function (req, res) { 2 let temp = req.body.temp1; 3 console.log(temp); 4 resBody = {state: ''} 5 for(i = 0;i<temp.length;i++){ 6 email = temp[i].email; 7 sendmail(email); 8 } 9 resBody.state = '发送成功'; 10 return res.send(resBody); 11 }) 12 13 14 var sendmail = function(email) { 15 var transporter = nodemailer.createTransport({ 16 service: 'qq', 17 auth: { 18 user: '@qq.com', 19 pass: 'uuevruvffnrcbg' 20 } 21 }); 22 var mailOptions = { 23 from: '@qq.com', // 发送者 24 to: email, 25 subject: '提醒作业', // 标题 26 text: '同学,您有还未提交的作业,别玩了,请赶紧去交作业"' 27 }; 28 transporter.sendMail(mailOptions, function (err, info) { 29 if (err) { 30 console.log(err) 31 return; 32 } 33 }); 34 }
总结与体悟:
①nodejs的异步回调:
主线程发出一个异步请求,相应的工作线程接受请求并告知主线程已收到(异步函数返回);主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定的动作(调用回调函数)。
工作线程在异步操作完成后通知主线程的过程:工作线程将消息放到消息队列,主线程通过事件循环过程去取信息。实际上,主线程只会从消息队列里面取信息,执行信息,再取信息,再执行。当消息队列为空时,就会等待消息队列变成非空。只有当前消息执行完成后,才会去取下一个信息,这就是事件循环机制。
事件驱动程序:当server接收到请求,就把它关闭然后处理,然后去服务下一个请求。当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
这样做的优点是不用多线程就可以极大地提高工作效率, 但是有一个问题是,只要变量在回调函数外面,他的值都不会被这个函数改变,这样就跟直觉相反,也跳入了很多这种坑,经历了几次undefined之后,才彻底理解了这种机制。
还有一个缺点是,外部的计数器对于回调函数来说会失效,经过反复琢磨,还想过把异步操作换成同步,后来想明白加一个函数内部的计数器就可以很好地解决这个问题,这也算是一个经验吧。
②一个大的项目,需要有很好的全局意识,需要考虑到的东西太多了,这就需要你的工程能力和清晰的思路了。
一般大的项目都是牵一发而动全身,这就要求我们尽可能把他们之间的联系变得简单化,模块化,这样才不会有太大的绝望。
这个项目中,原本以为能用学生的学号做唯一的标识,但是后来才发现这个是极其幼稚的,导致了代码的第一次重构。
③从前期的团队作业进度中,可以明显感觉到技术掌握的不够把进度拖了又拖,后来发现不能等到都学会然后用,要边学边用,边踩坑中成长。
后来又发现团队管理真的特别重要,如果管理得好,多个人的效率远远超过一个人,如果管理的不好,一个人的效率也会远远超过多个人。
做任何事都不能靠别人,尤其在大学,不要幻想着老师还会教给你什么知识,什么技术,只有自己去学,去黑暗中摸索,才能会用一些东西。
现在做网页的状态,是把零散的细碎的点滴的知识搭建成一个大项目(目前来说,对大项目整体构架仍然没有一个很明确地认识),这坑,必须要自己踩了。
④对项目整体没有一个清晰的认识,经常会带来很多负面的情绪,严重拖累项目进度,会让人丧失动力。
对技术本身没有清楚的认识,只是会使用它,这样的学习我觉得是悲剧的,致使经常遇到这种情况:
遇到问题,google一下,按照他的一步一步去做,结果自己做和他上面应该有的不一样,这时候有两种方案。
第一,自己想想,想不明白这个什么,然后瞎搞,搞坏了就坏了,转到第二种方案;搞好了,就有一种劫后余生的欣喜,但是回头想想自己高兴什么,这个不是随便一个人都会的吗?然后跳出这个循环。
第二,找个看起来更靠谱的从头再一步一步去做,同样面临着循环回去的可能,也就是自己做的和他上面的不一样。
这么循环着循环着,一天就过去了,就很绝望。
到最后发觉,
自己为什么又在随便找一个人就可以做的按照教程一步一步走这个过程中浪费了一天呢?
这一天又收获了什么呢?可能google的能力又进步了一点?可能对问题有了更加深入一点的了解?
⑤虽然这个过程很艰苦,但是我想着,一定做什么事情,既然选择了,不管成功失败,都要有一个结果,都要有东西可以展示出来,这才是最重要的。所有事情都要追求一个结果,成功的或者失败的。