这个作业属于哪个课程 | 2021春软工实践|W班级(福州大学) |
---|---|
这个作业要求在哪里 | 结对第二次作业——顶会热词统计的实现 |
结对学号 | 221801418&221801409 |
这个作业的目标 | 根据上次结对作业一的原型来实现论文查找系统 |
其他参考文献 |
git仓库链接和代码规范链接
PSP表格
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
计划 | ||
• 估计这个任务需要多少时间 | 10 | 10 |
开发 | ||
• 需求理解 | 30 | 43 |
• 生成设计文档 | 25 | 20 |
• 学习相关知识 | 600 | 573 |
• 代码编写 | 1200 | 1432 |
• 服务器部署 | 180 | 212 |
报告 | ||
• 计算工作量 | 10 | 10 |
• 事后总结, 并提出过程改进计划 | 20 | 15 |
• 博客编写 | 60 | 78 |
合计 | 2135 | 2180 |
成品展示
云服务器项目地址
原型链接
用户的登陆和注册界面
用户可以在这里进行简单的登陆或注册
搜索界面(论文列表界面)
点击搜索框会出现当前最热门的十个关键词搜索建议
用户可以选择推荐词或者自己手动输入搜索词,输入完成后在左边的下拉框中可以选择根据论文标题、关键词、摘要进行搜索。
点击左边的小箭头按钮可以显示论文的详细信息。
在页面的最下面有分页器,用户可以一页一页点击跳转也可以直接输入页码进行跳转。同时点击收藏按钮可以把用户感兴趣的论文加入用户自己的收藏夹。
用户的收藏夹界面
在这里用户可以对自己已经收藏的论文进行查看操作,同时也可以把不感兴趣的论文从收藏夹中删除(只从收藏夹中删除,并不会影响到搜索界面用到的数据表),也包含了已收藏论文范围内的搜索功能。
热词分析界面
热词分析界面先用一个柱状图展示了过去几年三个峰会的热词总体情况
点击某个峰会的柱子,会显示出一个折线图以反应该峰会过去几年的热词数量变化趋势
再点击某个折线图中的拐点,会显示出该峰会该年份的热度排行前十的单词。
点击饼状图的某个单词,会自动跳转到以该单词作为关键词搜索的搜索界面。
结对讨论过程描述
这是当时在设计用户收藏夹功能的实现时的讨论,原本想的是弄成一个嵌套的表,但是发现好像不行,于是决定给每个用户创建一个单独的表用于存放用户收藏的论文信息
汉杰在上周六团队作业的第二天还要早起补考,之后还一起做结对,真的非常辛苦,好在最后完成了
两人发现搜索功能存在bug并一起对其进行了维护
两人发现部分页面的css文件没法被正确引入并一起解决了问题
杰の称赞
部署完服务器,十分开心
相片
设计实现过程
使用到的框架
后端主要采用了tp5框架,前端用到了vue和echart。
数据库结构
考虑到要求中用户可以对论文条目进行删除操作,因此引入登陆注册功能,每个用户单独维护自己的表,只在自己的表中进行添加和删除操作,避免用户共享同一张表出现的冲突。因此,本项目所使用的数据库中含有三类型的表,首先是paper表,该表储存了基础的paper数据,该数据使用了助教提供的从各大论文网站爬下来的论文数据。第二类为user表,该表中储存了用户的用户名、账号、密码。第三类为每个用户专属的论文列表,该表的结构与paper表完全一致,表明为paper_该表所属的用户账号,作为收藏夹的数据源使用。
用户的注册
采用表单收集用户填入的数据并提交到后台PHP,在后台register方法中通过查询user表寻找有没有账号相同的条目,有则提示账号已存在并返回注册界面,否则将数据添加进user表,显示注册成功并跳转至登陆界面。当然,用户名、账号、密码都不能为空才能提交,否则会提示“用户、账号名或密码不能为空”并返回注册界面。
由于在user表中,用户账号作为主键,因此在用户注册成功的同时,在数据库中新建命名为paper_$account(用户账号变量)的表,该表的结构与paper表完全一致,通过将用户主键(账号)添加在表名中,登陆后通过session跨页传递用户账号名实现用户只操作绑定自己的表。
用户的登录
同样用表单收集用户填入的数据并提交到后台PHP,通过login方法查询user表中账号相同的条目,没有则显示账号不存在。如果存在该条目,则进行密码的比对,密码正确则利用session储存用户账号以识别登陆的用户,然后跳转至论文列表主界面,否则提示“密码不正确”并返回登陆界面。判断所填数据不能为空的部分与注册一样。
用session跨页面传递该登录用户的账号,然后根据这个账号来选择数据库中属于该用户的表来操作,登出就是清空session回到登录界面。
用户收藏夹
最开始是设想用一个paper来存放所有的论文信息,然后用一个user表来放用户的信息(包括账号密码和收藏的论文),但是在实现的过程中,发现只用一个表来存放用户的所有信息是有一定的难度的。于是最终改为了数据库结构中所述每个用户对应一张专属paper表(收藏表)的形式。
每个用户的收藏夹数据源都是专属于该用户的收藏表。
与收藏夹相关的功能,除了基础的显示论文列表和搜索功能以外,有论文列表页面“通过点击收藏按钮将用户需要的论文条目加入收藏夹”以及收藏夹页面“通过点击删除按钮将用户不需要的论文条目从收藏夹中删除”两个功能。两者都通过ajax向后端PHP文件发出请求,用cookie储存含有所点击当前论文条目的信息List,将用户点击某个收藏或删除按钮所对应的论文条目的所有信息传递至后端PHP,在后端通过传递过来的用户账号变量和论文信息,对当前用户专属的收藏夹表进行数据库操作。
论文分析中的图表
主要使用了ehcart,非常方便,在option中可以设置图表的类型,其中的数据等等,这次任务中主要是创建了柱状图折线图和扇形图。同时给图表中的元素添加点击事件,实现图表的出现和页面跳转。
论文搜索界面
主要使用了vue来制作表格和搜索框等,表格用了el-table,数据放在tableData中根据搜索框内的内容自动返回。搜索框用的是el-autocomplete,随着搜索框内字符的变化,表格内的内容也会跟着变化(前提是已经选好前面的根据什么搜索,这个选择用的是el-select)。从论文分析界面跳到论文搜索界面用的url传递数据,给扇形图中的元素添加点击事件,使得分析界面能带着用户点击的关键词跳转到搜索界面,搜索界面从url中读取传递来的关键词,让tableData根据这个关键词返回关键词搜索的结果。
服务器部署
本项目的部署使用阿里云服务器,由于阿里云有云翼计划(学生优惠),仅需86元就能购入时长一年的阿里云服务器。并通过宝塔和Xftp进行服务器的设置和项目在服务器上的部署。
服务器地址为:
项目所使用端口为:81
另,由于财力有限,购入的服务器为1M服务器,经过测试,由于论文列表有至少一万两千条的条目,因此以这个网速每次进入论文列表读取论文信息会有相当一段时间的读取时间,还请耐心等待。实测通过加钱将服务器提升到10M可以有效解决这个问题。测试时6个小时花了4块钱,由于财力有限,而且由于不知道助教什么时候会进行检查,所以也没有24小时提速的打算。如果实在太卡,影响了审查,可以通知我们现场升级一下。
功能结构图
代码说明
public function register(){
if(request()->isPost()){
$name=input('post.name');
$account=input('post.account');
$password=input('post.password');
if($account!=""&&$password!=""&&$name!=""){
$db=Db('user');
$data=[
'name'=>$name,
'account'=>$account,
'password'=>$password
];
//检查账号是否已经注册
$check_username=$db
->where('account', $data['account'])
->find();
//如果已经注册返回信息 该用户已存在
if ($check_username) {
return $this->error("该用户已存在");
}
if ($db->insert($data) ) {
$sql = "
CREATE TABLE IF NOT EXISTS `paper_$account`(
`title` longtext NOT NULL,
`abstract` longtext,
`typeandyear` varchar(255) DEFAULT NULL,
`keyword` longtext,
`releasetime` varchar(255) DEFAULT NULL,
`link` varchar(255) NOT NULL,
PRIMARY KEY (`link`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8";
Db::execute($sql);
//注册成功返回到index模块下的index控制器下的index方法
return $this->success('注册成功','LoginInterface/create');
}else{
//注册失败返回注册页面错误码 500
return $this->error('500','index');
}
}else{
return $this->error("用户名、账号或密码不能为空");
}
}
}
注册界面的实现,除了收集表单的数据和各种验证以外,对于本项目而言最核心的功能在于,注册成功的时候根据用户的账户名新建一张以paper_用户账号为表名、结构与paper表完全相同的收藏夹表。
public function login(){
if(request()->isPost()){
$account=input('post.account');
$password=input('post.password');
if($account!=""&&$password!=""){
$db=Db('user');
$data=[
'account'=>$account,
'password'=>$password
];
$user_info=$db
->where('account', $data['account'])
->find();
if(empty($user_info)){
$this->error("该用户不存在");
}
else{
if($password!=$user_info['password']){
return $this->error("密码错误");
}
else{
Session("account",$account);
$this->success("登录成功",'SearchInterface/create');
}
}
}else{
return $this->error("账号或密码不能为空");
}
}
}
登录功能的实现代码,除了收集表单的数据和各种验证以外,对于本项目而言最核心的功能在于,在用户登录成功后,用session储存该登录用户的用户名后再进行页面的跳转。通过session将用户名进行跨页面传递实现用户的识别。
handleFavorite(index,row){
var list={
"title":row.title,
"abstract":row.abstract,
"typeandyear":row.typeandyear,
"keyword":row.keyword,
"releasetime":row.releasetime,
"link":row.link
}
$.cookie("favoriteList",JSON.stringify(list));
$.ajax({
tpye:"POST",
contentType: "application/x-www-form-urlencoded",
url:'/index/SearchInterface/favoritePaper',
data:{},
success:function(result){
console.log("传输成功");
},
error:function(msg){
console.log("传输失败")
}
})
}
后台PHP接收数据
……
$value=session('account');//接收用户的用户名,用于识别用户
……
$listString = $_COOKIE["favoriteList"];
$list = json_decode($listString,true);
……
$data=[//接收传递过来的论文信息
'title'=>$list["title"],
'abstract'=>$list["abstract"],
'typeandyear'=>$list["typeandyear"],
'keyword'=>$list["keyword"],
'releasetime'=>$list["releasetime"],
'link'=>$list["link"]
];
论文列表界面的收藏功能的实现代码,通过ajax向后台PHP发起请求,用cookie储存用户所选条目的论文信息并传递至后台php,便于后台php进行数据库操作。
收藏夹界面的删除功能同样使用该方法传递数据,与收藏功能的区别在于传递数据的目标路径不同,以及后台的数据库操作不同。
smallerChart.on('click',function(sech){
window.location.href = "/PairProject/221801409&221801418/public/index/SearchInterface?sech="+sech.name;
})
这段代码是我们论文分析界面传值给搜索界面的方法,使用Chart.on('click',function(obj){})方法可以给echart图表中的元素添加点击事件,其中obj会所点击的这个元素相关的值,obj.name能返回出点击元素的"name"值(在柱状图和折线图中是横坐标,扇形图中是点击区域代表的东西)。同时用window.location.href="..."来实现页面的跳转和传值(通过url的方法,下面有说)。
function getString(str) {
var reg = new RegExp('(^|&)' + str + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}
配合这段代码用于获得网页末尾的值,当网页地址末尾是"...search_interface?sech=computer vision"时使用getString(sech)就能得到computer vision,把这个获取到的值交给表格,让表格直接根据获取到的值返回出搜索结果。
var mainChart = echarts.init(document.getElementById('main'));
var optionMain = {
title: {
text: '各会议热词情况表'
},
tooltip: {},
legend: {
data: ['热度']
},
xAxis: {
data: ["CVPR", "ECCV", "ICCV"]
},
yAxis: {},
series: [{
name: '热度',
type: 'bar',
data: [3212, 323, 4121]
}]
};
mainChart.setOption(optionMain, true);
mainChart.on('click', function (params) {
......
})
这段代码是echart生成表格的方法,ehcarts.init(document.getElementById('main'))用来绑定html中的div(图表将显示的区域),option用于填写表格中的信息,title表示图表的标题,tooltip表示当鼠标放在图表元素上时会有内容的提示,legend表示数据的现实意义,xAxis表示横坐标,yAxis表示纵坐标,series表示图表的类型以及具体的数据。后面的Chart.setOption(option,true)方法则是用上面的option来生成表格,Chart.on('click', function (params) {})是给图表中的内容添加点击事件的方法。
<div style="width:50%;margin:15px auto">
<el-input placeholder="请输入搜索内容" v-model="search" class="input-with-select" >
<el-select slot="prepend" placeholder="请选择" v-model="select" style="width: 130px">
<el-option label="按标题搜索" value="1"></el-option>
<el-option label="按关键词搜索" value="2"></el-option>
<el-option label="按摘要搜索" value="3"></el-option>
</el-select>
</el-input>
</div>
这段代码是搜索框的html代码,把el-input(输入框)和el-select(下拉框)放在同一个div之中,palaceholder表示框中默认的数值,v-model能将输入框和下拉框与等号后面的变量绑定,当输入框或下拉框中的值发生变化时,网页可以根据变量值的变化来获得变化的数据。
<template>
<el-table
:data="tableData.slice((currentPage-1)*pageSize,currentPage*pageSize)" border
style="width: 90%;margin: 15px auto;">
<el-table-column type="expand">
<template slot-scope="props">
<el-form label-position="left" inline class="demo-table-expand" >
<el-form-item label="论文标题">
<span>{{ props.row.title }}</span>
</el-form-item>
<el-form-item label="论文地址">
<span>{{ props.row.link }}</span>
</el-form-item>
<el-form-item label="关键词">
<span>{{ props.row.keyword }}</span>
</el-form-item>
<el-form-item label="发布日期">
<span>{{ props.row.releasetime }}</span>
</el-form-item>
<el-form-item label="摘要">
<span>{{ props.row.abstract }}</span>
</el-form-item>
</el-form>
</template>
</el-table-column>
<el-table-column
min-width="5"
align="center"
label="标题"
prop="title">
</el-table-column>
<el-table-column
min-width="5"
align="center"
label="关键词"
prop="keyword">
</el-table-column>
<el-table-column
min-width="5"
align="center"
label="摘要"
show-overflow-tooltip
prop="abstract">
</el-table-column>
<el-table-column label="操作" min-width="3" align="center">
<template slot-scope="scope">
<el-button
size="mini"
@click.native="buttonHandler">查看</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
style="text-align: center;"
@current-change="pageChange"
:page-size="pageSize"
layout="total, prev, pager, next, jumper"
:total="tableData.length">
</el-pagination>
</template>
这段代码是生成表格的html代码,用到了vue框架,其中el-table表示的是表格,data="tableData.slice((currentPage-1)pageSize,currentPagepageSize)"表示的是表格中的数据来自tableData,但是会通过slice函数截成几个部分以此来进行分页,el-form-item表示的是表格中每项带的小项(就是成品展示中点击表格左边小箭头出现的内容),用el-table-column添加表头,@click="..."是el-button添加事件的方式,下面的el-pagination用于实现分页器,这部分让用户可以自由地进行页面跳转。
computed: {
tableData() {
const search = this.search
const select = this.select
if (select == "1") {
// filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
// 注意: filter() 不会对空数组进行检测。
// 注意: filter() 不会改变原始数组。
return this.db.filter(data => {
// some() 方法用于检测数组中的元素是否满足指定条件;
// some() 方法会依次执行数组的每个元素:
// 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测;
// 如果没有满足条件的元素,则返回false。
// 注意: some() 不会对空数组进行检测。
// 注意: some() 不会改变原始数组。
return Object.keys(data).some(i => {
// indexOf() 返回某个指定的字符在某个字符串中首次出现的位置,如果没有找到就返回-1;
// 该方法对大小写敏感!所以之前需要toLowerCase()方法将所有查询到内容变为小写。
return String(data.title).toLowerCase().indexOf(search) > -1
})
})
}
else if (select == "2") {
return this.db.filter(data => {
return Object.keys(data).some(i => {
return String(data.keyword).toLowerCase().indexOf(search) > -1
})
})
}
else if (select == "3") {
return this.db.filter(data => {
return Object.keys(data).some(i => {
return String(data.abstract).toLowerCase().indexOf(search) > -1
})
})
}
return this.db
}
}
这段代码实现了表格中数据能根据搜索框内的内容进行实时变化,computed中的值表示当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值,根据这点,能实现实时变化。然后tableData就是我们表格的数据来源,在这会对select进行一次判断,既判断当前是根据标题、关键词还是摘要进行搜索,条件语句里面的是在网络上学习到的模糊搜索的语句,能根据不同的搜索条件返回不同的模糊搜索结果。
心路历程和收获
221801418林子坤:
这次作业还好是助教和老师们延长了截止日期,在这个作业期间,我和汉杰各有一门需要补考的科目需要复习,能给我们共同结对编程的时间其实并不很多,虽然是由于我们自己的原因导致的,但好在助教和老师延长了截止日期让我们的时间能更多一些。这次实践又让我学到了很多新的知识,包括tp5框架、echart制作表格和vue多样多功能的美观样式结构。通过这次实践我充分意识到路比脚长,做这种带多功能的页面其实只有上个学期的web实训有做过,而且当时基本上是完全跟着视频学着做,其实有很多不明白的地方(包括php语言其实当时也是一点也不会),现在的这次作业虽然比起之前我能稍微有一些经验,但刚开始的时候还是非常懵,不知道要使用什么框架,不知道当初原型设计时制定的界面要如何很好的复原。好在这是一次结对作业,而且汉杰也是一名可靠的队友。通过合理的分工,让我的目标更加明确,从一开始的一团乱码到具体的每个页面、每个功能的实现,让我明白了遇到大问题于其抱怨,不如先认真地思考分析,把一个大问题拆分成一个个简单的小问题。Better to run than curse the road!
221801409洪汉杰:
非常凑巧的是此次的结对作业和上次的一日作业都是web相关的内容,并且我补考的内容正是web,这段时间我一边复习web的知识一边动手进行实际的网站设计和代码的编写,让我对web相关内容的掌握程度上升了一大截,至少我认为如果是现在的我,参加上学期web的期末考试一定能做到游刃有余。此次作业的编写不但让我复习并掌握了web的基础知识,还促使我去了解和学习了例如thinkphp框架、ajax、vue等在上学期的web课程中没有安排学习的内容,并且让我对于数据库相关编程有了更深刻的认识,还学习了将项目部署到服务器上的技术。我向来习惯将一个大任务分割成许多小任务分段完成,但是我往往在分划完区块后就直接投入去做了,只是抱着“大概就是这样”的心情直接开始做,而没有将具体的做法想的非常清楚之后再动手,这也是我的老毛病了。在这次的结对编程中,突出体现的就是关于用户和收藏表的对应问题,我第一反应是给用户设置一个ID,然后把ID作为表的属性保存,一眼看上去十分合理,但是子坤听了之后提问:“将id作为表属性保存的话,一个用户的收藏夹表每一条信息的用户ID不都一样吗?那有啥用。”这是一眼就能看出的错误,但是因为我没细想所以没发现。在他提问之后,我才开始认真仔细地思考,最终提出了以paper_account作为表名的解决方法。只是我一个人的话肯定就那么做下去然后遇到困难再推倒重来了,正因为是结对编程,有认真的队友,才使我做到思虑周全之后再开始行动。
评价结对队友
221801418林子坤对221801409洪汉杰:
汉杰是一位十分可靠的队友,上进心强,学习能力优秀,能够按时完成每个阶段的计划,在这次结对的过程中我们各司其职,对于不确定的地方我们也能很快地讨论出合理的解决方案。这次结对过程顺利,分工自然,我和汉杰也很有默契,总体来说结对的体验非常的好。
2218014109洪汉杰对221801418林子坤:
子坤作为队友相当可靠,在最初的功能总结和划分的讨论中,他能够认真仔细地倾听我的想法并指出其中的不合理之处,并且提供他的想法,和谐有效地推进讨论的进程。实现阶段,他能够将任务保质保量地完成,在我或者是他遇到某些技术困难的时候,会积极地发起或者参与讨论,并发表意见,两人一起寻找解决的办法。项目完成后,积极地进行测试并且提出修缮意见。在我看来,他是一个性格上积极、可靠、认真,技术上也相当有能力的队友。