结对作业二
作业信息
这个作业属于哪个课程 | 2021春软件工程实践W班 (福州大学) |
---|---|
这个作业要求在哪里 | 结对作业二 |
结对学号 | 221801428 221801402 |
这个作业的目标 | 关于论文顶会热词统计的实现 1、对已爬取的论文列表进行操作 (1)可对论文列表进行删除 (2)可对论文列表进行查询详细信息 2、分析已爬取到的论文信息,提取top10个热门领域或热门研究方向 (1)形成如关键词图谱之类直观的查看方式,点击某个关键词可展现相关的论文 (2)可对多年间、不同顶会的热词呈现热度走势对比,以动图的形式呈现 |
其他参考文献 |
目录
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 50 |
• Estimate | • 估计这个任务需要多少时间 | 2700 | 3050 |
Development | 开发 | 480 | 525 |
• Analysis(Pair) | • 需求分析(结对,包括学习新技术) | 360 | 380 |
• Design Spec | • 生成设计文档 | 30 | 45 |
• Design Review | • 设计复审 | 5 | 15 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 15 |
• Design | • 具体设计 | 30 | 45 |
• Coding | • 具体编码 | 1295 | 1405 |
• Code Review | • 代码复审 | 120 | 150 |
• Test | • 测试(测试,修改代码,提交修改) | 240 | 270 |
Reporting | 报告 | 30 | 30 |
• Test Repor | • 测试报告 | 30 | 45 |
• Size Measurement | • 计算工作量 | 10 | 15 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 | 2700 | 3050 |
Github仓库地址
代码规范链接
部署云服务器访问链接
账号提供
用户名 : yzx
密码 : 123456
项目地址
成品展示
首页展示
文件导入论文题目
登录注册页面
搜索结果列表
搜索自动补全
关键词列表显示
用户搜索记录
用户收藏
数据统计页面
结对讨论过程
刚拿到题目的时候,我就想到了要用前后端分离的模式进行设计,于是我便和润寒同学商量谁做前端,谁做后端,润寒同学似乎对后端较为感兴趣,所以经过商讨过后,我决定我来使用AugularJS作为前端框架使用,而润寒同学则使用Spring boot作为后端框架,前后端之间通过json格式传输数据。
项目初期,润寒同学遇到了Maven导入依赖包的问题,于是我们进行了简易讨论,最后成功导入后端所需要的依赖。
在实现通过文件批量导入文件题目的过程中,我们对如何实现文件的读取,以及后端该构造什么样的数据给前端等方面进行了沟通。
此外我们还一起指定了接受文件格式的要求细则,为后来的具体实现提供了宝贵的规则
关于后端读取文件的函数的实现,一定程度上保证了程序输出的正确性,为前端的开发工作提供了支持。
通过展示网页结果验证工程的可行性与正确性
描述设计实现过程
前端
前端主要使用了Angular来处理页面逻辑,使用html+css+Antd对页面UI进行设计,在数据图表的显示方面则使用了highchart组件。
前端分为三个模块,分别为启动模块app,该模块用于启动项目,没什么好说。auth模块,用于处理认证相关的页面和逻辑、core模块,项目的核心模块,包含了项目主要的功能。
并且为了将业务代码与逻辑代码相分离,我还添加了三个Service类用于封装各个模块中会使用到的操作,dataservice主要用于数据的处理,coreservice主要用于核心模块下组件与后端进行交互,authservice主要用于登录注册功能的请求过程。
后端
后端主要使用了Spring Boot + Jpa完成核心功能,包括控制器,服务,工具类的编写以及与数据库的交互等。并且使用了各种领域对象封装前后端数据的传输问题,将数据封装在自定义的领域对象中进行传送能极大地方便前后端数据的处理问题,也能减小json数据出错的情况。
我还使用了redis数据库来保存用户的搜索记录,使用spring-redis与redis进行交互,并在用户点击搜索的时候更新redis中保存的用户搜索记录。
使用了三个服务模块:AuthService用于处理用户的认证功能。DataService用于处理数据统计的功能。RedisService则封装了与redis交互的细节,提供给控制层更新,获取,清空redis数据的调用接口。
代码说明
1、登录逻辑的判断,以及功能实现
userLogin() {
// 暂时先在这里判断用户输入是否为空,服务器端仍要判断。
if (this.userForm.username == undefined || this.userForm.password == undefined ||
this.userForm.username!.trim() === ""
||
this.userForm.password!.trim() === ""
) {
this.message.error("用户名或密码不得为空!")
} else {
// 执行登录功能
this.authService.login(this.userForm)
.then(auth => {
switch (auth.statusCode) {
// 登录成功!
case 200:
//保存用户信息
this.authService.setLocalStorage(auth);
// 导航到主页面
this.router.navigate(['/core/home']);
break;
// 用户名不存在
case 201:
this.message.error(auth.errMsg);
this.userForm.password = "";
break;
default:
break;
}
})
}
}
2、元素动画定义
// 输入框动画
trigger('inputTrigger', [
state('in', style({
opacity: 1, transform: 'translateX(0) scale(1)'
})),
state('inputEnter', style({
'border-radius': '0px'
})),
state('inputLeave', style({
'border-radius': '10px'
})),
transition("* => in", [
style({ opacity: 0, transform: 'translateX(-100%) scale(0)' }),
animate(500)
]),
transition("inputEnter => inputLeave", animate('.5s ease-out')),
transition("inputLeave => inputEnter", animate('.5s ease-in'))
])
3、首页处理用户上传文件的回调函数,将用户输入的论文题目依次显示在弹出的对话框中
/**
* 论文题目批量导入
* @param info
*/
handleChange(info: NzUploadChangeParam): void {
if (info.file.status === 'uploading'){
// 按钮进入加载
this.toggleBtnLoading(true);
}
if (info.file.status === 'done') {
// 先判断数据格式是否符合要求
let responseData = info.file.response.result;
if (responseData.code !== 200){
this.msg.error(responseData.msg);
this.toggleBtnLoading(false);
}
else{
// 获得返回的总数据,['title1','title2',..]
this.totalData = Object.assign([],this.totalData,responseData.data)
// 上传成功后显示对话框
this.toggleVisible(true)
// 上传按钮加载中
this.toggleBtnLoading(true);
// 第一页显示的数据
this.uploadContent =Object.assign([],
this.coreService.getFirstPageData(this.totalData.length,this.pageSize,this.totalData));
}
} else if (info.file.status === 'error') {
this.msg.error('上传失败,请重试!');
this.toggleBtnLoading(false);
}
}
4、使用RxJs发布响应式流用于时刻监听搜索框的输入事件,并进行回调处理。该方式用于实现输入自动补全功能。
ngAfterViewInit():void{
this.inputSubscription = fromEvent(this.searchInput?.nativeElement,'input')
.pipe(
debounceTime(200),
distinctUntilChanged(),
pluck('target','value')
).subscribe((value:any) => {
this.changeOps(value);
})
}
changeOps(value : string){
if (this.searchType == 'title'){
this.filteredOptions = this.titleoptions.filter(option => {
return option.toLowerCase().startsWith(value.toLowerCase());
});
}
else{
this.filteredOptions = this.kwdoptions.filter(option => option.toLowerCase().startsWith(value.toLowerCase()));
}
}
ngOnDestory(){
if (this.inputSubscription != null){
this.inputSubscription.unsubscribe();
}![](https://img2020.cnblogs.com/blog/2290755/202103/2290755-20210331143810089-565518350.png)
}
5、分页处理逻辑。我选择在后端一次性请求数据,在前端进行分页,这样能减少发送http请求的次数,也能减少后端处理分页的压力,减小了报错的风险
// 封装前端页数处理
getFirstPageData( totalNum : number , pageSize : number , totalData : any){
let result = totalNum / pageSize;
// 计算共有多少页
let totalPage = result > Math.floor(result) ? Math.floor(result)+1 : Math.floor(result);
//第一页显示条数
let firstPageSize:number = totalPage == 1 ? totalNum : pageSize;
// 第一页显示的数据
return Object.assign([],totalData.slice(0,firstPageSize));
}
getPageChangedData(totalNum : number , pageSize : number , totalData : any,pageIndex : number){
let result = totalNum / pageSize;
let totalPage = result > Math.floor(result) ? Math.floor(result)+1 : Math.floor(result);
// 当前是最后一页
if (pageIndex == totalPage){
let lastTotal = (totalPage - 1) * pageSize;
console.log(lastTotal)
return totalData.slice(lastTotal);
}
// 当前不是最后一页
else{
let start = (pageIndex-1) * pageSize;
let end = start + pageSize;
return totalData.slice(start , end);
}
}
6、后端处理文件上传的逻辑,根据文件类型,选择不同的解析方法
/**
* 处理上传文件的controller
* @param file
* @return
*/
@PostMapping
public ResponseEntity<Map<String,Object>> uploadFile(@RequestParam("file") MultipartFile file){
String filename = "";
ReadInformation result = new ReadInformation();
try {
// 生成随机文件名
filename = generateRandomFilename(file.getOriginalFilename());
/**
* windows下的文件保存路径
*/
// 获取当前项目的路径
//String projectPath = System.getProperty("user.dir");
// String filePath = projectPath+"\\src\\main\\resources\\static\\files\\"+filename;
/**
* Linux下的文件保存路径
*/
String filePath = "/usr/local/upload/"+filename;
File dest = new File(filePath);
// 保存文件
file.transferTo(dest);
System.out.println("file saved");
if (filename.split("\\.")[1].trim().equals("txt")){
result = readerUtils.readUploadedTxtFile(filePath);
}else{
result = readerUtils.readUploadedExcelFile(filePath);
}
//读取文件
} catch (Exception e) {
e.printStackTrace();
}
HashMap<String, Object> responseMap = new HashMap<>();
responseMap.put("result",result);
return ResponseEntity.ok(responseMap);
}
7、后端实现了许多工具类,这些工具类主要负责读取文件内容,包装成java类型对象,或者转换对象类型,计算单词词频,生成验证用户信息要用的token,以及解析token获得当前登录的用户。
解析Excel文件的辅助函数
/**
* 读取excel文件
*/
public ReadInformation readUploadedExcelFile(String inputFilePath) throws IOException {
FileInputStream fileInput = new FileInputStream(inputFilePath);
XSSFWorkbook wb = new XSSFWorkbook(fileInput);
XSSFSheet sheet = wb.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
int realRowNum=0;
ReadInformation info=new ReadInformation();
String str="";
List<String>list=new ArrayList<String>();
if ((lastRowNum+1)==0){
info.setMsg("EXCEL表格内容为空");
info.setData(null);
info.setCode(201);
}
else{
for (int i = 0; i <= lastRowNum; i++) {
XSSFRow row = sheet.getRow(i);
XSSFCell cell = row.getCell(0);
cell.setCellType(Cell.CELL_TYPE_STRING);
str=cell.getStringCellValue();
if (!str.replaceAll(" ","").equals("")&&str!=null){
list.add(str);
realRowNum++;
}
}
if(realRowNum>10){
info.setCode(202);
info.setData(null);
info.setMsg("EXCEL表格内容大于10行");
}
else if (realRowNum==0){
info.setMsg("EXCEL表格内容为空");
info.setData(null);
info.setCode(201);
}
else {
info.setCode(200);
info.setData(list);
info.setMsg("成功");
}
}
wb.close();
fileInput.close();
return info;
}
使用Jwt包,生成token,以及解析token获得User对象,以此保护API接口,防止恶意调用
public static String generateToken(User user){
JwtBuilder jwtBuilder =
Jwts.builder()
.setSubject(user.getUsername())
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, SECRET_KEY);
// 获取token字符串
return jwtBuilder.compact();
}
public User parseToken(String token){
try{
Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
String un = claims.getSubject();
return userRepository.findUserByUsername(un);
}catch (Exception e){
return null;
}
}
8、使用redis保存用户的搜索记录,每次用户点击搜索时,就将该搜索记录加入list集或提到最前显示。
将所有对redis的操作包装为一个类,对外提供redis服务。
/**
* 处理用户搜索记录的更新,一次最多显示5条记录
*/
public void updateUserSearchHistory(String username, String keyword) {
// 获得操作对象
ListOperations<String, String> listOperations = redisTemplate.opsForList();
// 用户搜索历史列表
List<String> userHistory = listOperations.range(username, 0, -1);
// 搜索记录提到最左
if (userHistory.contains(keyword)) {
// 从右往左删除第一个
listOperations.remove(username, -1, keyword);
listOperations.leftPush(username, keyword);
}
// 否则表示是新的记录
else {
// 若size<5,直接push
if (userHistory.size() < MAX_HISTORY_NUM) listOperations.leftPush(username, keyword);
else {
listOperations.rightPop(username);
listOperations.leftPush(username, keyword);
}
}
}
结对总结
结对心路历程
润寒:因为以前很少有共同编程的机会,所以这次结对作业开始不论是在原型的设计上还是实际的编程上首先都是充满着未知感,脑中零碎的知识并不足以完成自己所需要做的工作。在开始作业时,因为后端所需框架为springboot,然而自己在之前所知相关知识点很少,只能花时间去学习,但在有其他课程的情况下差不多学了个3,4天后,不但自己的进程停留在原地,渴望速成的我关于框架的知识也没学透彻,那天截止学到的是集成mybatis,但照视频所学使用的是逆向工程,结果我连在配置文件中自定义一个查询方法都还没学会,这让我产生了迷茫和挫败,毕竟所花时间无法为这次的作业作出贡献。这无疑已经在拖结对队友的后腿了,好在后来朕炫给了我一些自己差不多能实现的功能的任务,之后又悉心辅导我相关编程,迷茫感才逐渐消失,我才能有机会稍微对这作业有了贡献。这时感受到类似结对,团队作业绝不能闭门造车,学习过程中产生问题一定得及时向队友反应,否则自己车没造出来,还拖了队友的后腿。以及以后得加强自己的框架学习,功能代码编写学习。希望以后共同编程作业时能有更好的知识储备去应对
朕炫:和润寒同学一样,我在项目作业开始前也在担心该如何与结对队友相合作,相互沟通,对于我来说,虽然做过了几个小项目,但第一次和队友一起编码实现还是充满着坎坷与未知。然而,幸运的是,润寒是一个非常不错的合作伙伴,在项目开始阶段润寒给了我很多的建议和点子,对于我不太理解的题目要求,润寒也能细心和我解释,这让我在开始便有了大致的方向。开始作业后,我选择使用了之前学习的Angular框架来进行前端的编码实现,这个框架我才刚学没几天,这么快就投入实践中说实话我是很担心的,我也比较担心润寒刚开始学spring boot会有些曲折,这些因素都有可能导致我们的作业无法按时完成或者完成度不高,于是我们脚踏实地,一步一个脚印的进行编码设计,边学边做,不懂的时候我们互相帮助,积极查找解决问题的办法。为了加快开发速度,我又去向Antd框架寻求帮助,向来不喜欢看文档的我在这次实践中不得不钻研文档,查明如何使用组件,但也因此我学会了如何看api文档,以及如何去使用。未来也许我更应该加强这方面的能力。
结对收获
221801428杨朕炫:这次结对对我来说算蛮有意义的,因为我自学Angular刚不久,正好有一个项目能让我练练手,并且这一次作业真正让我体验到了前后端如何交互,如何交流,如何对接,以前我总是找不到队友,只能自己一个人做前后端,及其忙碌,也少了许多乐趣。然而这次和润寒同学一起合作,我感受到轻松许多,也逐渐感受到团队开发的乐趣和意义所在。作为一个对后端也略知一二的人,这次作业中我也对后端的具体工作模式有了一个很大程度上的了解,并且对后端的一些热门技术有了一些体验。在参与文件读取返回数据格式的时候,我感受到了结对交流过程中所需的严谨,任何一方单独蛮干都不能解决问题,只有积极,尽可能的多沟通,才能一起解决困难,提高开发效率。同时,这次结对作业中我也学会了如何部署项目到服务器上,不仅让我重新学起来Linux的命令语言,也对前后端分离模式下,前端和后端项目的简单部署方式,总之,这次结对作业虽然遇到不少坎坷,但我也受益良多。
221801402普润寒:略微领会到了优秀队友带来的优秀的学习思路,能够更好地与队友接触交流。粗浅地接触学习到了SpringBoot框架部分内容(例如配置文件的使用,集成jsp,逆向工程,集成mybatis。虽然所学都不足以支持我对这次作业做出更多的贡献,但我相信今后关于这方面的持续学习能够有它一定的意义)。以及明白了与他人共同编程时凭着自己主观意识去学习和听取优秀队友的意见,前者很可能会使得事倍功半,客观分析自己的实力能做些什么而不是一条路走到黑,这样只会拖累工作进度,要优先采取后者让做啥就做啥,毕竟有更高编程经验的人对项目的开发有着更胜深的理解。同时自己也加深了根据所需要求更改思路及代码的理解。对自己的团结协作能力也有一定的提升。
结对评价
221801428杨朕炫 => 221801402普润寒 : 润寒同学很好沟通,很好交流,有很多时候我的要求没有说清楚,但润寒也有积极的询问我要什么格式的数据,有时数据格式要做变化,润寒也很主动的进行修改,并且给到前端的数据都有较高的正确性,润寒强大的数据处理能力极大地简化了前后端交互的过程,也提高了开发效率。在开发过程中,他一直是我值得信赖的伙伴,耐心的听我的要求,也经常提出有代表性,有创新性的建议,在我止步于一些困难无法前进的时候,他总能和我一起解决。有这样的队友是我的荣幸。
221801402普润寒 => 221801428杨朕炫 : 假设如果把朕炫换成我自己,我已经不敢想象这次作业在截止时期到来时自己交的文件夹是不是一个空文件夹。我觉得朕炫实力很强,各方面的知识储备都强我太多太多,以及他有着强大的执行能力。我的感觉是我可能还在休息,朕炫已经利用这段时间把某一功能模块已经解决了。他有着优秀的领导力,在开始作业前他就给了我所要执行任务的方向,奈何自己学习能力较弱在一定程度上影响到了这次我们结对作业的进度。但是随后他也能根据队友的情况灵活应变,有他作为队友,作为领导者真的再合适不过。