這個作業屬於哪個課程 | 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態度非常積極,執行力強,總是能在我松懈的時候提醒我要跟緊進度,希望在接下來的階段中能夠增強溝通交流,更加契合。