结对第二次作业
这个作业属于哪个课程 | 2021春软件工程实践S班 |
---|---|
这个作业要求在哪里 | 结对第二次作业 |
结对学号 | 221801314 &221801321 |
这个作业的目标 | 编程实现顶会热词统计、撰写博客 |
其他参考文献 | CSDN、简书、博客园... |
Github仓库地址
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 60 |
• Estimate | • 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 2020 | 2350 |
• Analysis | • 需求分析 (包括学习新技术) | 720 | 900 |
• Design Spec | • 生成设计文档 | 30 | 60 |
• Design Review | • 设计复审 | 30 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 60 | 50 |
• Design | • 具体设计 | 180 | 200 |
• Coding | • 具体编码 | 850 | 960 |
• Code Review | • 代码复审 | 30 | 50 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 100 |
Reporting | 报告 | 120 | 160 |
• Test Repor | • 测试报告 | 60 | 80 |
• Size Measurement | • 计算工作量 | 30 | 30 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 50 |
合计 | 2200 | 2570 |
项目访问链接
成品展示
主界面
主要由页头、轮播图、搜索框、(论文列表)、页脚组成。
页头下拉栏
轮播图
搜索功能
在搜索框中输入关键词,可以搜索出相关文章(包含标题、作者、论文来源、发表时间、摘要以及关键词),点击论文标题还可跳转至原文连接。
搜索结果分页展示
页脚上方放置页数条,可点击进行切换页数查看不同论文。
高级搜索
在搜索框旁的高级搜索按钮可展开筛选论文的发表年份或者是论文来源。
热词分析界面
Top10
展示热度前十的关键词,并可以点击柱形进行相关关键词搜索
-
Top10关键词相关查询
热点图
展示09-16年的部分关键词热度变化
结对讨论过程描述
炜:
刚开始拿到题目的时候我们对于分工讨论了很久,因为我们对于前后端的工作都不太熟悉,一开始曾想过两个一起做前端,之后再做后端,但是这个方案存在许多问题,于是我们之后进行了前后端的分工。对于数据库的设计,是由海翔进行设计后两人一起讨论,前端的界面设计由我大致根据前一次的原型设计进行改造,并且和海翔沟通界面设计,两人共同敲定需求与功能,并分开进行前后端相关的开发。在查找资料的时候,特别是前后端联调阶段,我们经常碰见交互的错误码,我们两人一同查询资料,并讨论可能产生bug的地方,最后逐步解决bug。
海翔:
拿到题目后,我与炜哥积极沟通,确认了彼此分工,炜哥使用Vue框架写前端,我使用Spring boot 写后端。之后我选择去B站、CSDN等平台寻找Spring boot相关教程,遇到问题积极沟通。及时与前端沟通,确定前端所需数据和接口。同时学习jar包部署至服务器的相关操作。
设计实现过程
前端
选择使用vue框架,进行组件化的开发,并采用axios进行前后端交互,同时引入echarts图表进行数据可视化操作
使用流程图
后端
选择使用Spring Boot框架,使用分层思想,分为controller(前后端交互)、entity(实体类)、mapper(用于写接口和.xml数据库配置文件)、service(实现业务逻辑层)、untils(工具类)、config(放swagger配置等)层,整合Mybatis,简便了数据库操作。之后根据前端所需数据,编码每一层操作,实现相关接口。
其中abstracts、keyword、authors字端长度设为5000,以便存得下数据,id为自动递增
代码说明
代码规范
前端
发送get请求查询title
获取 标题、当前页数、年份、会议 等查询条件,合并成url,向后端发送get请求获取数据,再接收查询到的论文列表papers。
关键代码
//get请求查询title
queryPaper() {
var page = this.page - 1;
var newUrl = "";
newUrl += "?address=";
newUrl += page;
newUrl += "&source=";
newUrl += this.meeting;
newUrl += "&title=";
newUrl += this.title;
newUrl += "&years=";
newUrl += this.year;
window.location.href = newUrl;
var myUrl = "/paper/title";
myUrl += newUrl;
console.log(myUrl);
this.ctx.$http.get(myUrl).then((data) => {
this.papers = data;
console.log(this.papers);
// window.location.reload();
});
},
发送get请求查询图表数据
查询数据库图表数据,放进echart
关键代码
const querySearch = () => {
ctx.$http.get("/word/top", {}).then((data) => { //get请求获取数据
console.log(data);
for (let i = 0; i < data.length; i++) { //存入数据
let array = new Array();
array.push(data[i].keyword);
array.push(data[i].num);
myArray.push(array);
}
//需要获取到element,所以是onMounted的Hook
let myChart = echarts.init(document.getElementById("top10"));
// 绘制图表
myChart.setOption({
dataset: [
{
// dimensions: ["name", "age", "profession", "score", "date"],
dimensions: ["keyword", "num"],
source: myArray, //存入数据
},
});
myChart.on("click", function (params) {
..
});
window.onresize = function () {
//自适应大小
myChart.resize();
};
});
};
父组件向子组件传参
将查询到的论文,通过父组件向子组件传参供子组件使用
使用v-bind绑定数据对象,v-for遍历对象数组,逐个传参到子组件中。
子组件中使用props接收父组件传参。
关键代码
//父组件
<template>
<Paper :paper="item" v-for="item in papers" /> //v-bind绑定论文列表对象
</template>
<script>
data() {
return {
papers: [],
};
},
methods: {
//get请求查询title
queryPaper() {
....
this.ctx.$http.get(myUrl).then((data) => {
this.papers = data; //传入查询结果
console.log(this.papers);
// window.location.reload();
});
},
</script>
//子组件
<template>
<div>
<div>
<a :href="item.link"> //v-bind绑定属性
{{ item.title }} //模板语法
</a>
</div>
...
<div>
<h2>摘要:</h2>
<div>
<span>{{ item.abstracts }}</span> //模板语法
</div>
<h3>关键词:</h3>
<div>
<span>{{ item.keyword }}</span> //模板语法
</div>
</div>
</div>
</template>
<script>
props: ["paper"], //接收参数
data() {
return {
item: this.paper, //将父组件传来的参数重命名,防止误修改产生警告
};
},
</script>
子组件向父组件传参
各个搜索子组件传参给父组件,所有搜索条件联合搜索。
关键代码
//部分子组件
<script>
export default defineComponent({
setup(props, {emit}) {
const year = ref("");
const meeting = ref("");
function getCond() {
emit("year",year.value);
emit("meeting", meeting.value);
}
return {
year,
meeting,
getCond,
};
},
});
</script>
//父组件
<script>
export default defineComponent({
data() {
return {
title: "",
year: "",
meeting: "",
page: 1,
};
},
methods: {
//获取输入框组件title
getTitle(val) {
console.log(val);
this.title = val;
},
//获取高级搜索组件条件
//年份
getYear(val) {
console.log(val);
this.year = val;
},
//会议
getMeeting(val) {
console.log(val);
this.meeting = val;
},
//获取分页组件当前页面
getPage(val) {
if (val === undefined) {
val = 1;
}
console.log(val);
var title = this.getQueryString("title");
var meeting = this.getQueryString("meeting");
var year = this.getQueryString("years");
var myUrl = this.setUrl(val, title, year, meeting);
this.page = val;
window.location.href = myUrl;
},
},
});
</script>
后端
组合查询的实现
根据文章标题/会议/年份查找论文,利用Mybatis的
关键代码
<select id="selectTitle" resultMap="Paper" >
select * from papers where 1=1
<if test="title != null and title != ''">
and title like CONCAT('%', #{title}, '%')
</if>
<if test="source != null and source != ''">
and source = #{source}
</if>
<if test="years != null and years != ''">
and years = #{years}
</if>
limit #{address}, 5
</select>
接口层
int createPaper(Paper paper);
List<Paper> queryAll();
List<Paper> selectTitle(String title,String source, String years, Integer address);
List<Paper> selectSource(String source);
List<Paper> selectYears(String years);
List<Paper> selectKeyword(String keyword, Integer address);
List<Paper> selectKeywordYear(String years, String keyword);
TOP10热词的统计
遍历从数据库读出的Paper类,用Map存储Key和Value,然后使用Map自带的排序函数根据Value排序,并取前10个。
关键代码
wordsMap = new HashMap<>();
Map<String,Integer> result = new LinkedHashMap<>();
List<Keyword> keywordList = new LinkedList<>();
for(Paper paper : papers)
{
for (String keyword : paper.getKeyword().split(","))
{
if (keyword.length() < 3)
{
continue;
}
if (wordsMap.containsKey(keyword))
{
wordsMap.put(keyword, wordsMap.get(keyword) + 1);
}
else
{
wordsMap.put(keyword, 1);
}
}
}
wordsMap.entrySet().stream().sorted(Map.Entry.<String, Integer> comparingByValue().reversed()).limit(10)
.forEachOrdered(x->result.put(x.getKey(),x.getValue()));
根据年份查询关键词热度
接收前端传来的相关信息,把结果封装为一个新的类返回给前端。
关键代码
public List<YearWord> selectKeywordYear(@RequestBody WordYear wordYear)
{
String []years = wordYear.getYears();
String []keyword = wordYear.getKeyword();
List<YearWord> yearWords = new LinkedList<>();
for (String word : keyword)
{
YearWord yearWord = new YearWord();
yearWord.setKeyword(word);
int [] num = new int[years.length];
for (int i = 0; i < years.length; i++)
{
num[i] = paperService.selectKeywordYear(years[i], word).size();
}
yearWord.setNum(num);
yearWords.add(yearWord);
}
return yearWords;
}
解决前端测试接口CORS
设置过滤器。
关键代码
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin","*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PATCH, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
chain.doFilter(req, res);
}
}
Swagger相关配置
使用swagger进行Api测试,书写相关配置文档。
关键代码
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("yangyu")
.enable(true)
.select()
.apis(RequestHandlerSelectors.basePackage("com.yangyu.esearch.controller"))
.build();
}
//配置Swagger信息
private ApiInfo apiInfo(){
Contact contact = new Contact("yangyu","https://www.cnblogs.com/yangyu-huang/",
"1849588816@qq.com");
return new ApiInfo(
"yangyu的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.cnblogs.com/yangyu-huang/",
contact,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList<>()
);
}
}
心路历程与收获
炜:作业发布前看往届的作业,以为只要两人写一个类似后端处理的程序,没想到是要两人共同实现前后端一个完整的系统(虽然功能较简单)。然后我这次负责的是前端,但是也只是刚入门的阶段(经过github实训后也成长了一些),但对于一些具体的流程还不是特别清楚。但随着和海翔的讨论,确定需求,商量解决方案,原本空荡的vue框架也逐渐变得丰满起来,界面也逐渐美观起来。在这次过程中,我也对vue框架更加熟悉,能够搭建一个丰富的网页,但对于vue的生命周期,以及vue2,3之间的一些特性和改动以及axios异步请求的方面还是需要进一步学习深入。总而言之,这次的结对作业,让我的学习能力提升了不少,也熟练了关于vue的使用,收获颇丰。
海翔: 在看到此次结对作业要对上一次结对作业的实现时,我一开始是比较担心自己能不能成功实现这个项目,怕拖累队友。因为我对框架是属于听过,但是没用过的状态。于是我第一时间去B站、CSDN寻找相关的教程,在一步步的摸索中,成功的搭建了Spring Boot框架,学会了使用yml文件进行配置,学会了整合Mybatis,学会了分层编程的思想(controller层、entity层、mapper层......),学会了前后端分离编程,发现Spring Boot框架确实很好用,收获颇多。
队友评价
炜to海翔:海翔是一个非常认真,可靠的队友,能够快速地上手使用后端框架以及一些在线接口部署的方案,让我在进行前端设计的时候能够放心的进行相关的接口的调试,还愿意积极和我交流有关接口的设计以及传输数据的具体设计,有时候碰到请求错误的时候我们也积极讨论研究可能存在的问题,每次都能顺利地排除错误,顺利,稳步地推进项目,甚至有时候请求超时的时候,海翔也和我认真地分析原因,最后顺利找到问题,让我们网页的响应速度成倍提升,我们对于这样的改进都非常开心。非常期待之后与海翔编程的机会。
海翔to炜:炜是一个有责任心,专业能力极强、态度认真、擅长学习新知识的队友。在商量好编程方式为前后端分离后,他负责前端,我负责后端。他热情学习前端Vue框架,实现了一个美观、交互性好的前端界面。并且及时与我交流沟通,分析并解决遇到的困难,及时告诉我前端所需的数据,让我能及时根据前端需求修改Controller层的Api。结对合作使得彼此之间能够及时地交流遇到的问题,及时地解决问题,大大提高了开发的效率。期待与炜的下一次合作。