软件工程实践结对作业二

这个作业属于哪个课程 软件工程2021春软件工程W班(FZU)
结对学号 221801424 221801435
作业要求 作业链接
作业目标 论文查询网站,完成基础功能,并实现一到两个附加功能
参考文献 Flask官方文档 Angular官方文档

作业描述

基础功能
功能1:对已爬取的论文列表进行操作
功能2:分析已爬取到的论文信息,提取top10个热门领域或热门研究方向等统计分析效果展示
项目部署到云服务器上
附加功能
功能3:获取待爬取论文列表及论文信息爬取【附加分:15%】
扩展功能:扩展基础功能以外的功能【附加分:0%~15%】

Github仓库地址

代码规范链接

博客园链接(排版更清晰)
github代码规范链接

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(h) 实际耗时(h)
Planning 计划 2 2.5
Estimate 估计这个任务需要多少时间 0.5 0.4
Development 开发 4*24 4*24+12
Analysis 需求分析 (包括学习新技术) 2*24 2*24
Design Spec 生成设计文档 2 2.5
Design Review 设计复审 2 1.5
Coding Standard 代码规范 (为目前的开发制定合适的规范) 1 1.2
Design 具体设计 1 1.5
Coding 具体编码 2*24 2*24+12
Code Review 代码复审 2 2.5
Test 测试(自我测试,修改代码,提交修改) 1 1.5
Reporting 报告
Test Repor 测试报告 1 1
Size Measurement 计算工作量 1 1.5
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 1.5 2
合计 206 234.1

成品展示

网页链接

功能一:登录注册



因为本网站自带论文保存与收藏功能,用户可以根据自己的需要在用户列表收藏论文,并对列表进行删除和笔记等操作,而这些功能需要在用户登录的情况下才可以使用,因此,本gif专门展示一下登录注册和修改个人信息的一整个流程。

功能二:通过搜索框搜索论文,并添加到收藏列表




用户可以通过论文列表模块的搜索框模糊搜索论文的作者、标题、关键字,网站将根据搜索框输入内容查询论文并显示在列表中,用户可以选择自己感兴趣的论文并将其添加到“收藏列表”(当然前提是先登录成功),需要时还可以删除收藏列表里的论文。

功能三:趋势分析






用户可以通过“趋势分析->走势图”来查看近几年计算机视觉领域研究方向的热度走势图,其中:
折线图展示近几年三大顶会cvpr、iccv和eccv的投稿论文数量,为用户选择会议提供一些数据上的支持;
柱状图展示了三年里最热门的三个领域在三大顶会的论文投稿数量;
饼状图展示了本年度计算机视觉领域各个方向论文投稿数量的相对占比;
旭日图详细列出了本年度三大顶会论文投稿的热门方向,并给出了每个方向的投稿论文数量。

功能四:论文笔记



用户在“论文收藏”列表点击论文的“note”按钮之后,网站右边会弹出一个抽屉,用户可以在其中记录自己对于这篇论文的看法,例如这篇文章用了XX的方法,取得了XX的效果。

功能五:

用户可以通过直方图直观的看到本年最热的研究方向。

结对讨论过程描述

结对过程综述

拿到题目之后,我们首先做了功能的分析,确定了功能的模块可以大致分为论文搜索(爬取信息)、论文管理(论文列表以及文件导入)、趋势分析(包括走势图和热词统计)、用户信息和用户收藏,之后按照结对作业一的原型设计规划了大致的UI界面。因为cold之前有学过flask后端以及部署,所以后端框架确定使用Flask来进行开发,前端我们考虑了许久,为了能够加快开发速度,降低代码耦合,我们选择了具有组件化开发模式的Angular作为前端开发框架。因其组件化的特点,我们可以每人完成不同的组件,最后只需要在一个公共的组件上进行整合即可。之后,我们大约花费了3、4天的时间学习新的框架,剩余的时间用于编码和博客写作,期间我们互相参考了对方的代码,互相改bug,使得项目的推进变得顺利起来。

讨论过程截图

在项目开始时,我们讨论要如何处理助教给的论文数据,并和相应的数据库表对应,我们首先在网页上用jsonview工具展示json数据,分析数据格式,最后再编写python处理json文件,并将其结果导入到数据库当中。


在项目初期,讨论github如何协作使用,并一起学习了github的如何进行fetch,fork,和设置upstream,其中cold找到了一篇十分具有参考价值的博客

讨论功能块要如何划分,我们首先参考的是第一次结对作业,结合了助教给我们的意见之后,我们决定将首页的旭日图移动到趋势展示页面,做到一个页面只完成一个功能。我们也针对原型所不能表现出来的功能要如何实现做了细致的讨论,例如,用户笔记要如何实现,是用抽屉还是弹出一个输入框?最后我们还选择了ant design作为我们的前端UI辅助开发工具。

讨论前后端的接口如何统一,为了实现前后端分离,我们需要确定一套统一的接口api,用于在前端和后端传递数据。

设计实现过程

功能结构图

数据库设计图

User对应用户表,User_article对应用户收藏的文章表,与User表为1对多关系;Note表对应用户收藏文章的笔记,与User表为1对多关系,User_article 为1对1关系;Meeting_article为论文数据集所对应的表。

设计过程

主页

进入主页时我们为用户提供一个简洁的搜索输入框,输入框的旁边设置了下拉菜单用来选择是按标题、关键词、还是作者来搜索,其中下拉菜单的值可以用angular的ngModel动态绑定,在用户输入查询字符串并点击Search按钮之后,前端用angular自带的HttpClient向后端发送请求,后端利用mysql的%符号来进行模糊搜索,最后返回给前端符合查询条件的文章列表。考虑到符合条件的论文列表较多,前端最后用分页来展示所有数据(前端在请求后端数据时,还需要传入页的号码和页的大小,这个也可以通过angular的动态数据绑定做到)。

趋势分析

这里我们直接利用开源的图表显示框架echarts做数据渲染,在用户进入该模块时,前端向后端请求符合echarts格式的json数据,后端返回数据后由前端对数据进行组装(即设置echarts所需要的option对象),最后显示在页面上即可。

用户收藏列表

添加操作,只需要在搜索返回的论文列表上提供一个star按钮,并设置点击事件,当用户点击论文的star按钮之后,发送用户id和论文id到后端,后端将收藏信息存入数据库。当用户访问收藏列表时,
前端传用户的id给后端,后端返回用户收藏的论文列表。

论文笔记

这里我们使用了ant design自带的NgDrawerComponent,在用户点击"note"按钮时,前端发送文章id和用户id给后端,后端接受后返回过去用户在该篇论文上做的笔记,前端弹出抽屉之后将过去的笔记先显示在抽屉的textarea中,等用户编辑完毕,点击主页面,抽屉收回,前端获取新的笔记内容,并将其发送给后端,后端保存进数据库。

代码说明

前端实现侧边栏路由跳转

const routes: Routes = [
  { path: 'welcome', loadChildren: () => import('./pages/pageSearch/welcome.module').then(m => m.WelcomeModule) },
  { path: 'page-trend/tendency',component:TendencyComponent},
  { path: 'page-trend/top-trend',component:TopTrendComponent},
  { path: 'article-manage/article-import',component:ArticleImportComponent},
  { path: 'article-manage/article-list',component:ArticleListComponent},
  { path: 'search/home',component:SearchComponentComponent},
  { path: 'user/info',component:UserInfoComponent},
  { path: 'user/star',component:UserStarComponent},
  { path: 'user/login',component:LoginComponent},
  { path: 'user/register',component:RegisterComponent},
  { path: '', pathMatch: 'full', redirectTo: 'search/home' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

前端实现请求公用服务

import { Injectable } from '@angular/core';
import { promise } from 'selenium-webdriver';
import { HttpClient,HttpHeaders} from '@angular/common/http';
@Injectable({
  providedIn: 'root'
})
export class RequestService {

  private headers:any=new HttpHeaders({'Content-Type':'application/json'})
  constructor(public http:HttpClient) { }

  getData(api:any,body:any){
    return new Promise((solve)=>{
      this.http.post(api,body,this.headers).subscribe((data)=>{
          solve(data);
      });
    });
  }
}

前端实现存储公用服务

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor() { }

  set(key:any,value:any){
    localStorage.setItem(key,JSON.stringify(value))
  }

  get(key:any){
    let temp:any=localStorage.getItem(key)

    return JSON.parse(temp)
  }
  remove(key:any){
    localStorage.removeItem(key)
  }
}

上传文件后端处理

 @auth.route("/upload_file",methods=["GET","POST"])
#@token_check_required
def upload_file():
    try:
        data=request.files.get('file')
        # print(data.read())
        filename = secure_filename(data.filename)
        # 生成文件保存路径
        save_path = r"F:\寒假作业2\结队作业2\Team_work\static\userupload\%s"%filename
        # 保存文件
        data.save(save_path)
        List=[]
        with open(save_path,'r')as f:
            List=f.read().split(';')
            f.close()

        return responseBody(data={'keyList':List})
    except Exception as e:
        print(e)
        return responseError(Responses.PARAMETERS_ERROR)

文章实体类

class Article:
    meeting = ['CVPR', 'ECCV', 'ICCV']

    def __init__(self):
        self.title = "暂无"
        self.time = "xxxx-xx-xx"
        self.meeting_name = "暂无会议名称"
        self.auth_name = "暂无作者"
        self.abstract = "暂无摘要"
        self.keyword = "暂无关键词"
        self.address = "暂无论文地址"
        self.pretime="2000"

    def set_title(self, title):
        self.title = title

    def set_time(self, time):
        if(list(time)[0]!='2'):
            time=self.pretime
        self.time=time+"-01-01"
        self.time = datetime.datetime.strptime(self.time, '%Y-%m-%d')
        self.pretime=time

    def set_meeting_name(self, meeting_index):
        self.meeting_name = self.meeting[meeting_index]

    def set_auth_name(self, auth_name):
        self.auth_name = auth_name

    def set_abstract(self, abstract):
        self.abstract = abstract

    def set_keyword(self, keyword):
        self.keyword = keyword

    def set_address(self, address):
        self.address = address


#判断是否存在键
def judge_in(key,Json):
    if(key in Json):
        return True
    else:
        return False

处理文件函数

def get_response_dict(status, message):
    return {
        "status": status,
        "message": message
    }


class Responses:
    OPERATION_SUCCESS = get_response_dict(200,"操作成功")
    # 获取目录信息失败
    SEARCH_CATS_ERROR = get_response_dict(1001, "获取目录信息失败")
    # 未找到班级
    NO_CLASS_FOUND = get_response_dict(1002, "未找到班级!")
    # 未找到token信息
    NO_TOKEN = get_response_dict(1003, "为找到token信息!")
    INVALID_TOKEN = get_response_dict(1004, "无效的token!")
    TOKEN_EXPRIRED = get_response_dict(1005, "您的会话已过期!")
    NO_USER_FOUND = get_response_dict(1006, "该用户不存在!")
    INCORRECT_PASSWORD = get_response_dict(1007, "密码错误!")
    PARAMETERS_ERROR = get_response_dict(1008, "参数错误!")
    NOT_SAME_PASSWORD = get_response_dict(1009,"两次密码不一致!")
    AUTHORIZATION_ERROR = get_response_dict(1010,"权限不足!")
    NO_RECORD_FOUND = get_response_dict(1011,"未找到记录!")
    EXIST_ACCOUNT=get_response_dict(1012,"账号已存在!")
    EXIST_NAME=get_response_dict(1013,"用户名已存在")
    SAVE_FILE_FAIL=get_response_dict(1014,"保存文件失败")
    EXIST_STAR=get_response_dict(1015,"该论文已收藏")

工具方法

# 返回格式
def responseBody(status=200, data=None, message=""):
    return jsonify(status=status, data=data, message=message)


def responseError(info_dict):
    return jsonify(status=info_dict.get("status"),
                   message=info_dict.get("message"))


def responseSuccess(info_dict):
    return jsonify(status=info_dict.get("status"),
                   message=info_dict.get("message"))


def token_check_required(func):
    @wraps(func)
    def wrap_function():
        try:
            token = session.get("token")
            if token is None:
                return responseError(Responses.NO_TOKEN)
            user_id = validate_token(token)
            session["user_id"] = user_id
            func()
        except BadSignature:
            return responseError(Responses.INVALID_TOKEN)
        except SignatureExpired:
            return responseError(Responses.TOKEN_EXPRIRED)

    return wrap_function


def generate_token(user_id, expire=60 * 60):
    serializer = TimedJSONWebSignatureSerializer(expires_in=expire, secret_key=secret_key)
    return serializer.dumps(user_id)


def validate_token(token):
    serializer = TimedJSONWebSignatureSerializer(secret_key=secret_key)
    user_id = serializer.loads(token)
    return user_id

心路历程和收获

心路历程和收获

fino(221801435)的心路历程

刚开始看到这个题目搭配上这个截止日期的时候,我的第一反应是这肯定是做不完的(虽然后面延长了截止日期)。但是之后经过了一定的需求分析,划分出个别功能模块,我发现其实有一些功能的写法都是类似的(比如搜索页面列表和用户收藏列表,又比如对于数据的请求和数据的展示),之后我们利用框架来开发,由于我的队友对于flask后端和部署已经非常熟练了,我们前端用的又是基于组件低耦合的Angular,所以后面的开发过程还算顺利,而且我也对这种组件化开发模式有了更深入的了解,期间还复习了一下echarts的用法。

cold(221801424)的心路历程

拿到项目要求是比较震惊的,要在短期内学习技术并实现项目是一件比较难的事情(后面有加长时间)。首先我们对题目要求进行了分析,并将其划分成几个功能块,划分之后感觉思路会清晰了许多。讨论后,后端用flask进行编写并且我和fino一起学习前端框架angular进行前端的设计,在这期间我们也遇到过angular模块导入失败、echarts数据渲染失败等问题,但是两个人可以共同思考,相互弥补,这使得解决bug比自己编写时候更加的迅速。

队友评价

fino(221801435)对cold的评价

cold拥有丰富的项目经验,特别是后端开发和部署的知识,在这次项目上帮了大忙。他的编程思路也是我值得学习的,例如模块之间如何搭配,前后端的消息发送如何处理。有时我写的有错误的地方,他也能及时发现并指出我的错误,这使得我的开发效率高了很多。

cold 对 fino 的评价

fino队友是一名非常可靠优秀的队友,与他一起学习技术学到了很多。他在课程学习上是年段前列,他的学习方法让我受益匪浅,在问题解决上能够给我一些特别的思路并往往能够取得不错的解决效果,编写代码效率很高,有条不紊。搭建前端模块时候也能做到分模块化,让项目结构更加清楚简洁,这是我以前比较忽略的,感谢队友!

posted @ 2021-03-31 22:51  Fino-  阅读(177)  评论(6编辑  收藏  举报