结对作业二

这个作业属于哪个课程 2021春软件工程实践 | W班 (福州大学)
这个作业要求在哪里 结对第二次作业——顶会热词统计的实现
结对学号 221801329|221801316
这个作业的目标 1. 学会web项目开发 2.感受结对编程中前后端分离开发 3. 学习前后端开发知识 4. 学会部署项目至云服务器
其他参考文献
Github仓库地址 PairProject

一、项目链接

项目地址:PaperSearcher

用户:ldy,密码:123456登录完一定要退出!

Github地址:点这儿

前后端代码规范:别点错了

二、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
• Estimate • 估计这个任务需要多少时间 20 15
Development 开发
• Analysis • 需求分析 (包括学习新技术) 60 120
• Design Spec • 生成设计文档 20 15
• Design Review • 设计复审 10 7
• Coding Standard • 代码规范 (为目前的开发制定合适的规范) 20 40
• Design • 具体设计 20 15
• Coding • 具体编码 2520 3240
• Code Review • 代码复审 30 15
• Test • 测试(自我测试,修改代码,提交修改) 90 120
Reporting 报告
• Test Repor • 测试报告 30 45
• Size Measurement • 计算工作量 10 6
• Postmortem & Process Improvement Plan • 事后总结, 并提出过程改进计划 15 10
合计 2845 3648

三、项目介绍

总体介绍

项目分为登录、主页两个页面

  • 未登录无法访问主页,并没有设计注册页面
  • 登录主页后,左侧边栏是主页、论文列表、论文分析、收藏夹
  • 主页右侧边栏为装饰效果,除收藏夹按钮登出按钮无其他交互

模块介绍

主页

简单展示论文、只是为了美观

正上方的搜索栏无论点击哪个选项卡都可以使用

论文列表

论文列表用于展现查询出来的论文信息,每个论文分为标题、作者、部分摘要、关键词来展示

标题的右上角为收藏按钮,用户可以点击自己喜欢的论文添加进收藏夹中方便查阅

下拉到底下为分页块,若查询到的论文数量不足分页只会显示一页;未查询到则显示“No Result”

论文分析

该板块由两张图表组成

上方的图表是涵括近五年来三大顶会热门话题所发表论文数,用户可以点击对应扇形区域展开图表查看详细数据

下方的图表涵括近五年来三大顶会热门关键词发表论文总数趋势,在该图标的上方为饼图,展现了某年10个关键词在总和中的比例,下方的曲线图则展示五年来发表论文数的趋势变化,鼠标悬停在某一结点,上方饼图则会对应显示该年论文比例

收藏夹

收藏夹是为了方便用户查看自己需要保存记录的论文而创立,考虑到用户不能直接对总论文列表进行删改操作,将删除论文弹性划分为取消收藏这一功能,与论文列表结构一致,但只能看到自己所收藏的论文

四、结对过程

YK:这给了数据以后看起来一两天就能做完,后端也就查询,整理格式,收藏,总的来说都是CRUD没啥东西的感觉,前端我写我就尽快写一下和原型长得差不多的吧
DY:ok,但是感觉一两天不够
YK:先试试看
YK:我查了一点资料,主页雏形差不多出来了,你可以给我接口了
DY:???太快了,再等一会(开始摸鱼)
小故事到这里结束!!!

我们两个人是前后端分离开发,有需要的时候向对方请求接口/数据验证/结果验证;
YK负责前端,也不会vue也不会js,都是从0手撸html和css,一边学一边找资料,实际上发现写的效率还是挺高的,就是没有及时和后端对接,让DY偷偷摸了几天的鱼
DY在随后的一两天是进行数据库设计,并对助教打包好的json数据进行解析,制定代码规范;在json解析的过程中遇到了很多坑,比如json末尾居然是分号关键词、摘要太长,摘要、作者是空的,于是我们决定把含有空字段的数据做了一定处理:摘要太长→字符串截取,做一定非空判断
因为我们是舍友,在宿舍讨论比较方便,转头就可以疯狂提问/要接口

图为在宿舍讨论

YK平时在实验室,就采用线上QQ联系来对接,遇到问题的时候我们会把具体出错的地方告诉对方

图为线上讨论


五、实现过程

最终我们是确定了如下结构

前端设计

分为登录、主页两个页面

登录

登录页面就与平时见到的差不多,没什么技术性在里头

主页

主页最基本的功能就是要获取论文信息、收藏夹、热点趋势等
想尝试一下选项卡式的布局,在左侧边栏设置了四个选项卡,其中的切换都是依靠js完成的(从后台获取数据添加到innerHTML中),直到团队作业我学了vue以后才知道全靠js是很不规范的一种方案

论文列表、收藏夹

论文列表展现的是用户所可以看到的信息,我参考了百度搜索的布局样式,设计了白底蓝灰字色的样式,一篇论文对应一个block,看起来比较简洁美观,信息依靠模板样式展现,查询则是根据用户输入、选择字段来向后端发起不同路由的请求

论文分析

论文分析页面展现的是一些热点关键词的趋势变化,需要直截了当,旭日图能较好的看到全局的数量分布情况,其可交互性很强,用户可以直接选择自己需要了解的方向去查看,我很喜欢这样的设计,所以就用上了
另一个图是表达热点趋势变化的图,我和DY挑了很久才挑出这个饼图+曲线图的样式,它的可交互性也很强
图中所有数据都是向后端请求后所生成的,数据格式的规定是参考了echarts和highcharts的格式

交互弹窗

交互弹窗我是用了以前在西二后端考核时候接触到的swal,个人感觉他的风格比较简洁

后端设计

数据库设计

本次项目中,主要是对论文数据进行存储和操作。首先设计了一张论文表,里面存储论文的字段信息,如标题,摘要,链接等等。
由于一篇论文对应着多个关键词,多个作者,一个关键词或者作者也对应着多篇论文,它们之间存在着多对多的关系,因此还需设计两张论文-关键词、论文-作者的关联表。
我们还对项目进行了功能拓展,增加了用户模块和收藏夹模块,因此设计了用户表和用户-论文关联表,从而达到注册登录以及增加/移除收藏的功能。
具体表结构见如下ER图

代码设计

本次项目采用了MVC模式,使用了springboot框架,Controller层负责提供接口与前端交互,Service层负责业务逻辑处理,Dao层负责数据持久化,参与数据库的交互,Pojo下存放着实体类

  • 数据解析
    对文件进行读取,遍历每一个.json文件,利用JSONObject解析json,指定所需要的数据的key值,将得到的数据分装入实体类中,最终存入数据库。
    当然,本次不同会议的.json文件中key值不完全一致,既存在英文也存在中文,需要写分支对其按不同情况处理
  • 论文搜索
    与前端协商好交互的数据格式,根据传入的参数不同,实现不同搜索功能。如按关键词模糊查询,按作者模糊查询...实现方式主要是sql语句的编写
    在查询的时候前端提供偏移量和页面大小,后端根据两个参数查询指定的内容并且返回总条目数和数据,从而实现分页功能
  • 论文分析
    主要编写sql语句,对近五年的三大会议的Top10关键词进行统计。与前端确定所需要展示的图表并且分析图表中data数据的结构,按照结构将后端查询的数据整合,返回给前端。
    主要使用的是JSONObject和JSONArray,两者嵌套使用,可以灵活地向前端提供数据。
  • 用户模块和收藏夹模块
    该模块为我们本次项目的拓展功能,用户根据用户名和密码,实现简单的注册和登录,由于时间比较紧迫,没有考虑安全和加密等方面的功能。用户登录后在论文搜索的结果页面中,可以点击收藏按钮,对喜欢的论文进行收藏。
    前端将该论文的id和该用户的id传给后端,后端对其进行存储,建立用户与论文的管理。在收藏夹页面,联合查询论文表和用户-收藏表,可以查看该用户收藏的论文列表。

六、关键代码说明

前端

登录、登录状态检测、登出

//login.js
function login() {
    let username = document.getElementById('username').value;
    let password = document.getElementById('password').value;
    //采用axios来进行网络连接,操作比ajax方便
    instance.post('/login', {
            username: username,
            password: password
        })
        .then(res => {
            if (res.data.userId !== -1) {
                //采用session存储登录信息,为随后的判定登录做准备
                window.sessionStorage.setItem('username', username);
                window.sessionStorage.setItem('isLogin', true);
                window.sessionStorage.setItem('userId', res.data.userId);
                swal("登录成功!", "即将为您跳转至主页……", "success");
                window.setTimeout(3000);
                window.location.href = 'index.html';
            } else {
                swal("用户名或密码错误!", "请重新登录", "error")
            }
        })
}

//paperList.js
//获取session判断用户是否登录,若未登录则返回登录界面
let isLogin = window.sessionStorage.getItem('isLogin');
if (!isLogin) window.location.href = 'login.html';

//登出为清除session并跳转至登陆界面
function logout() {
    sessionStorage.clear();
    window.location.href = 'login.html';
}

论文列表实现

//因为是论文列表和收藏夹用同一套代码,就封装成函数了
function setList(data, pageNum, type) {
    if (data.length === 0) {
        panel.innerHTML = panel.innerHTML + "<p style=\"text-align:center;color: rgb(127, 127, 127);\">No result</p>";
    } else {
        let list = data.paper;
        for (let k in list) {
            //定义一个临时变量方便操作
            let element = list[k].data;
            //有的摘要太多了,砍掉一些内容
            let abstractStr = element['abstractContent'].slice(0, 100) + "...";
            let authorStr = "";
            let keywordStr = "";
            //拼接作者信息,最多只显示5个
            for (let t in element.author) {
                authorStr += element.author[t] + ';';
                if (t >= 3) {
                    break;
                }
            }
            //去除最后的分号
            authorStr = authorStr.slice(0, -1);
            //拼接关键词信息,最多只显示5个
            for (let t in element.keywords) {
                keywordStr += element.keywords[t] + ';';
                if (t >= 3) {
                    break;
                }
            }
            //同样的去分号
            keywordStr = keywordStr.slice(0, -1);
            //标记是否收藏
            let sytle = "like";
            let src = '../img/gary-star.svg'
            //若该条论文被收藏,则收藏图标亮起
            if (list[k].isLike === 1) {
                src = '../img/orange-star.svg'
            }
            //写入页面
            panel.innerHTML = panel.innerHTML +
                "<div class=\"paper-list\" id=" +
                element.id +
                "><a href=" +
                element.link +
                " class=\"paper-title\">" +
                element.title +
                "</a>" +
                "<p class=\"paper-author\">" +
                authorStr +
                "</p> <p> <span class=\"paper-abstract-title\">[Abstract]</span>" +
                "<span class=\"paper-abstract-detial\">" +
                abstractStr +
                "</span></p>" +
                "<p><span class=\"paper-keyword\">[Keyword]</span>" +
                "<span class=\"paper-keyword-list\">" +
                keywordStr +
                "</span></p>" +
                "<img src=" + src + ' onclick=like(' +
                element.id + ')' + ' id=Like' +
                element.id + ' class=' + sytle + '>' +
                '</div>'

        }
        //分页部分
        initPagination(pageNum, Math.floor(data.total / 10) + 1, type);
    }
}

分页实现

//分页框部分实现
function initPagination(currentPage, totalPage, type) {
    console.log(totalPage)
    panel = document.getElementById('main-panel');
    let start;
    let end;
    //将页数控制在8页以内
    if (totalPage < 8) {
        start = 1;
        end = totalPage;
    } else {
        start = currentPage - 4;
        end = currentPage + 3;
        if (start < 1) {
            start = 1;
            end = start + 7;
        }
        if (end > totalPage) {
            end = totalPage;
            start = end - 7;
        }
    }
    //添加分页栏
    let str = '<nav aria-label="Page navigation">' +
        '<ul class="pagination">';
    //判断是列表还是收藏夹,在li中添加不同函数
    if (type === 'like') {
        for (let i = start; i <= end; i++) {
            if (currentPage == i - 1) {
                var li = "<li class=\"active\"><a onclick=getLikeList(" + (i - 1) + ")>" + i + "</a></li>";
            } else {
                var li = "<li><a onclick=getLikeList(" + (i - 1) + ")>" + i + "</a></li>";
            }
            str += li;
        }
    } else if (type === 'list') {
        for (let i = start; i <= end; i++) {
            if (currentPage == i - 1) {
                var li = "<li class=\"active\"><a onclick=getPaperList(" + (i - 1) + ")>" + i + "</a></li>";
            } else {
                var li = "<li><a onclick=getPaperList(" + (i - 1) + ")>" + i + "</a></li>";
            }
            str += li;
        }
    }
    str += '</ul></nav>'
    panel.innerHTML = panel.innerHTML + str;

收藏实现

function like(data) {
    //根据id获取元素
    let ID = 'Like' + data;
    let star = document.getElementById(ID);
    let src = star.getAttribute('src');
    let router = '';
    //根据收藏图标判断收藏/取消收藏路由
    if (src === '../img/gary-star.svg') {
        router = '/addLike'
    } else {
        router = '/deleteLike'
    }
    //请求部分
    instance.get(router, { params: { userId: sessionStorage.getItem('userId'), paperId: data } })
        .then(res => {
            if (router == '/addLike') {
                swal("收藏成功!", "点击继续", 'success')
            } else {
                swal("取消收藏成功!", "点击继续", 'success')
            }
            //请求结束以后需要修改按钮状态
            star.setAttribute('src', (src == '../img/gary-star.svg') ? '../img/orange-star.svg' : '../img/gary-star.svg');
        })
}

后端

service层关键代码——旭日图实现

@Override
    public List<JSONObject> queryTop10ByYear() {
        String []meets=new String[]{"CVPR","ECCV","ICCV"};
        Integer []years=new Integer[]{2016,2017,2018,2019,2020};
        List<JSONObject> data=new ArrayList<>();
        //0级数据
        Map<String,String> param0=new HashMap<>();
        JSONObject jsonObject0 =new JSONObject();
        jsonObject0.put("id","0.0");
        jsonObject0.put("parent","");
        jsonObject0.put("name","顶会五年总计");
        data.add(jsonObject0);
        //一级数据
        for(int i=0;i<3;i++){
           JSONObject jsonObject1=new JSONObject();
           jsonObject1.put("id","1."+i);
           jsonObject1.put("parent","0.0");
           jsonObject1.put("name",meets[i]);
           data.add(jsonObject1);
        }
        //二级数据
        int k=0;
        for(int i=0;i<3;i++){
            for(int j=0;j<5;j++){
                JSONObject jsonObject2=new JSONObject();
                jsonObject2.put("id","2."+k);
                jsonObject2.put("parent","0.0");
                jsonObject2.put("parent","1."+i);
                jsonObject2.put("name",String.valueOf(years[j]));
                data.add(jsonObject2);
                k++;
            }
        }
        //三级数据
        k=0;
        int n=0;
        for(int i=0;i<3;i++){
            for(int j=0;j<5;j++){
                //获得第i个会议,第j年的前10关键词及其数量
                List<Keyword> keywordMapList=paperMapper.queryTop10ByYear(years[j],meets[i]);
                //如果查询不到记录
                if(keywordMapList.size()==0){
                    for(int m=0;m<10;m++){
                        JSONObject jsonObject3=new JSONObject();
                        jsonObject3.put("id","3."+n);
                        jsonObject3.put("parent","2."+k);
                        jsonObject3.put("name", "nothing");
                        jsonObject3.put("value",1);
                        data.add(jsonObject3);
                        n++;
                    }

                }else{
                    //查询得到记录
                    for (Keyword keyword : keywordMapList) {
                        JSONObject jsonObject3=new JSONObject();
                        jsonObject3.put("id","3."+n);
                        jsonObject3.put("parent","2."+k);
                        jsonObject3.put("name", keyword.getName());
                        jsonObject3.put("value",keyword.getCount());
                        data.add(jsonObject3);
                        n++;
                    }
                }

                k++;
            }
        }
        return data;
 public List<Paper> cvprJsonParse() {
        List<Paper> paperList=new ArrayList<>();
        String dir=System.getProperty("user.dir");
        System.out.println(dir);
        File file=new File(dir+"c/main/resources/论文数据/1");
        if(file.exists()){
            File []child=file.listFiles();
            for(int i=0;i<child.length;i++){
                Paper paper=new Paper();
                String json=jsonRead(child[i]);
                json=json.replace(";","");
                JSONObject jsonObject=JSONObject.parseObject(json);
                String title=jsonObject.getString("title");
                String abstractContent=jsonObject.getString("abstract");
                if (abstractContent==null)abstractContent="暂无";
                if(abstractContent.length()>=150){
                    abstractContent=abstractContent.substring(0,150);
                }
                String link=jsonObject.getString("doiLink");
                String meet="CVPR";
                Integer year=Integer.valueOf(jsonObject.getString("publicationYear"));
                List<String> keywordList=new ArrayList<>();
                JSONArray keywords= jsonObject.getJSONArray("keywords");
                if(keywords!=null){
                    for(int j=0;j<keywords.size();j++){
                        JSONObject keyword=keywords.getJSONObject(j);
                        JSONArray jsonArray=keyword.getJSONArray("kwd");
                        for(int k=0;k<jsonArray.size();k++){
                            keywordList.add(jsonArray.getString(k));
                        }
                    }
                }
                else {
                    keywordList.add(" ");
                }
                List<String> authorList=new ArrayList<>();
                JSONArray authors=jsonObject.getJSONArray("authors");
                if(authors!=null){
                    for(int j=0;j<authors.size();j++){
                        JSONObject author=authors.getJSONObject(j);
                        authorList.add(author.getString("name"));
                    }
                }
                else{
                    authorList.add(" ");
                }
                paper.setTitle(title);
                paper.setAbstractContent(abstractContent);
                paper.setAuthor(authorList);
                paper.setKeywords(keywordList);
                paper.setLink(link);
                paper.setMeet(meet);
                paper.setYear(year);
                paperList.add(paper);
                System.out.println(paper.toString());
            }
        }
        else{
            System.out.println("文件不存在");
        }
        return paperList;
    }

Dao层主要代码

    <select id="queryPaper" resultType="com.fzu.pojo.Paper">
        select id,title,abstract_content as "abstractContent",meet,`year`,link from paper_search.paper limit #{start},#{rows}
    </select>
    <select id="countAll" resultType="java.lang.Integer">
        select distinct count(*) from paper_search.paper
    </select>
    <select id="queryKeywords" resultType="java.lang.String">
        select keyword from paper_search.paper_keyword where paper_id=#{paperId}
    </select>
    <select id="queryAuthors" resultType="java.lang.String">
        select author from paper_search.paper_author where paper_id=#{paperId}
    </select>
    <select id="queryPaperByKeyword" resultType="com.fzu.pojo.Paper">
        select distinct a.id,a.title,a.abstract_content as "abstractContent",a.meet,a.year,a.link from paper a,paper_keyword b
        where b.keyword like '%${keyword}%' and a.id=b.paper_id limit #{start},#{rows}
    </select>
    <select id="queryByTitle" resultType="com.fzu.pojo.Paper">
        select distinct a.id,a.title,a.abstract_content as "abstractContent",a.meet,a.year,a.link from paper a
        where a.title like '%${title}%' limit #{start},#{rows}
    </select>
    <select id="queryPaperByAuthor" resultType="com.fzu.pojo.Paper">
        select distinct a.id,a.title,a.abstract_content as "abstractContent",a.meet,a.year,a.link from paper a,paper_author b
        where b.author like '%${author}%' and a.id=b.paper_id limit #{start},#{rows}
    </select>
    <select id="countAllByKeyword" resultType="java.lang.Integer">
        select distinct count(*) from paper a,paper_keyword b
        where b.keyword like '%${keyword}%' and a.id=b.paper_id
    </select>
    <select id="queryTop10ByYear" resultType="com.fzu.pojo.Keyword">
        select keyword as name,count(*) as `count`  from(select b.keyword from paper a,paper_keyword b where
        a.id=b.paper_id and a.year=#{year} and a.meet =#{meet}) as tmp group by keyword order by count(*) desc limit 10
    </select>

七、心路历程和收获

221801329(LYK)的收获

不用框架写前端还是挺吃力的,也只能用用课内知识去还原自己的原型,自己原型写的多牛逼,实现的时候就哭的有多惨,这次我没有遇到很大的问题,主要是和后端交互的时候需要规划好返回数据的结构,不然每次调试起来都要花费很多时间

221801316(LDY)的收获

本次结对使我对springboot有了进一步的学习,也对MVC模式有了更深刻的理解。在这次项目中,我使用了之前未使用过的MyBatis框架,相比于之前的项目中直接使用的JDBC,代码量减少了不少,并且它提供了数据映射功能,支持对象与数据库字段的关系映射,方便了不少。
本次结对所需要实现的功能其实并不复杂,整体的逻辑思路还算清晰,但是许多细节的地方却仍然需要注意。在完成了整个项目后,我也对本次编程过程进行了回顾和思考,其实大部分的时间并不是花在代码的编写,而是花在代码bug的修改,可能是某一处无意的变量名写错,或者是
特殊情况如null时应该进行的处理,这些都是我们在专注的同时也可能忽略的东西,希望自己以后编程能够更加严谨,并且要养成每个功能模块完成后进行单元测试的习惯。除此之外对我还认识到了项目架构和需求分析的重要性,在实践的过程中,由于前期对收藏夹功能保持着非必选的态度,因此
设计时没有考虑到用户模块,在多数接口定义及实现完成之后,才想到需要添加用户和收藏夹的功能,进而引发对已有的接口进行修改。总的来说,本次项目使我认识到了自己仍存在着许多不足,虽然基本的功能都能轻松实现,但是对功能结构的规划和代码的优化,还有很大进步的空间。
希望接下来的阶段里,自己能够学习更多技术,积累更多的经验,提高自己编程的效率!

八、队友互评

221801329(LYK)对221801316(LDY)的评价

DY认真的时候效率还是很高的,就是摸鱼的时候是真的摸,前期我主页列表都写好了他一个接口还没放出来,把我急死了,不过后期两个人一起认真写确实能很快解决很多问题,下次还会和他合作,不过我想写后端了,这次是因为两个人都是后端我选了前端QWQ

221801316(LDY)对221801329(LYK)的评价

YK态度非常积极,执行力强,总是能在我松懈的时候提醒我要跟紧进度,希望在接下来的阶段中能够增强沟通交流,更加契合。

posted @ 2021-03-31 17:57  YukioLee  阅读(157)  评论(10编辑  收藏  举报