BLOG – 個人博文系統開發總結 三:批量博文匯入功能

BLOG – 個人博文系統開發總結 三:批量博文匯入功能

BLOG – 個人博文系統開發總結 三:批量博文匯入功能

自上一篇博文以來,網站在很多地方有了改進:

  1. 修改了網站的主要字型為Segoe UI Semibold
  2. 博主個人主頁博文列表每一項在右側顯示博文中的圖片(選取博文中的首張圖片,如果有的話)
  3. 將博文統計資訊用單獨的頁面展示,普通使用者也能檢視(非博主)
  4. 新增博主個人設定頁面,博主可修改使用者名稱,個人簡介,主頁佈局等
  5. 完成了博文批量匯入功能

GitHub:DuanJiaNing/BlogSystem

部分新功能截圖

主頁列表項新增圖片和博文統計資料檢視入口

這裡寫圖片描述

博文統計資料頁面

這裡寫圖片描述

博主個人設定 # 基礎設定

這裡寫圖片描述

博主個人設定 # 賬號 # 批量匯入博文

這裡寫圖片描述

批量博文匯入功能

好了,總結過後,進入本篇博文的主題,解析 批量博文匯入 功能。

流程是這樣的

  1. 上傳將字尾為.md的博文檔案打包的.zip壓縮檔案
  2. .zip存至臨時資料夾
  3. 讀取.zip並遍歷其中的.md檔案
  4. .md解析並呼叫新增博文服務新增博文
  5. 刪除臨時檔案
  6. 返回成功新增博文的標題

前端.zip檔案上傳程式碼

前端介面使用了 bootstrap 的進度條元件,進度條元件通過修改 css 中的 width 屬性來修改當前進度(百分比),如 width = 80%時進度條百分比如下:
這裡寫圖片描述

我設計了在 0% – 60% 的時間段內表示檔案上傳,當 ajax 請求返回時(這意味著後端處理已經結束),此時進度條快速從 60% 走到 100%(模擬解析過程耗時),表示解析完成,之後將批量匯入的博文標題顯示出來。

難點在於進度條的 0% – 60 % 這一過程,進度條在走時,檔案上傳和後端處理正在進行,但兩者沒有前後關係,即有可能進度條到 60% 但後端處理沒有結束,這種情況進度條會停止 60% 處,沒有影響,但如果後端處理結束返回但進度條沒走到 60% 處,此時就需要提醒進度條直接跳到 60%,另一種情況是,後端處理返回,但處理出錯,此時需要提醒進度條回到 0%,並顯示錯誤資訊。

簡化程式碼如下:

...
$('#processStatus').html('正在上傳...');
var stopSuc = false; // 用於後端處理提前返回時進行提醒
var stopFail = false; // 用於後端處理出錯時進行提醒
//進度條從 0% -> 60%,countDown 方法會每過 20 毫秒將值(從60開始)減一,並呼叫回撥方法
countDown(60, 20, function (count) {
if (stopFail) {
// 將進度設為 0%
$('#progressbar').css('width', '0%');
return true; // 返回 true 後倒計時會立即停止
}
if (stopSuc) {
// 將進度設為 60%
$('#progressbar').css('width', '60%');
return true;
}
// 正常倒計時(遞增進度)
$('#progressbar').css('width', (60 - count)   '%');
});
// zip 檔案
var data = new FormData();
data.append('zipFile', $('#zipFile').prop('files')[0]);
$.ajax({
url: '/blogger/'   bloggerId   '/blog/patch',
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false,
success: function (result) {
if (result.code === 0) { // 後端處理成功回撥
stopSuc = true;
// 60% -> 100% 處理時間
$('#processStatus').html('正在解析...');
countDown(40, 20, function (count) {
if (count === 0) { // 倒計時結束(進度 100%)
$('#progressbar').css('width', '100%');
// 將後端處理返回的成功匯入的博文標題顯示出來
handleImportSucc(result.data);
return true;
} else { 
$('#progressbar').css('width', (100 - count)   '%');
}
});
} else { // 後端處理失敗回撥
stopFail = true;
$('#processStatus').html('');
// 有可能進度條已經走到 60% 
$('#progressbar').css('width', '0%');
// 顯示後端處理出錯資訊
error(result.msg, 'blogImportErrorMsg', true, 3000);
}
}
});
...

後端檔案接收和處理程式碼

Controller層方法
這裡寫圖片描述

Service 層方法

 @Override
public List<BlogTitleIdDTO> insertBlogPatch(MultipartFile file, int bloggerId) {
// 儲存到臨時檔案
StringBuilder b = new StringBuilder();
// 從 bloggerProperties.getPatchImportBlogTempPath() 中獲得臨時資料夾路徑
File dir = new File(bloggerProperties.getPatchImportBlogTempPath());
if (!dir.exists() || dir.isFile()) {
if (!dir.mkdirs())
return null;
}
// 構建臨時檔案全路徑
String fullPath = b.append(dir.getAbsolutePath())
.append(File.separator)
.append("temp-")
.append(bloggerId)
.append("-")
.append(System.currentTimeMillis())
.append("-")
.append(file.getOriginalFilename())
.toString();
// 儲存臨時檔案
FileUtils.saveFileTo(file, fullPath);
// 用於將讀取到的 md 格式檔案解析為 html 格式
final Parser parser = Parser.builder().build();
final HtmlRenderer renderer = HtmlRenderer.builder().build();
// 解析博文
List<BlogTitleIdDTO> result = new ArrayList<>();
ZipFile zipFile = null;
try {
zipFile = new ZipFile(fullPath);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
// 變數 zip 中的 md 檔案
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
BufferedInputStream stream = new BufferedInputStream(zipFile.getInputStream(entry));
InputStreamReader reader = new InputStreamReader(stream, Charset.forName("UTF-8"));
// 解析 md 檔案,存入資料庫,並返回博文標題和記錄在資料庫中的id
BlogTitleIdDTO node = analysisAndInsertMdFile(parser, renderer, entry, reader, bloggerId);
if (node != null)
result.add(node);
}
} catch (IOException e) {
throw new InternalIOException(e);
} finally {
if (zipFile != null) try {
zipFile.close();
// 刪除臨時檔案
File tempFile = new File(fullPath);
if (tempFile.exists() && tempFile.isFile())
tempFile.delete();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return result;
}

Parser 和 HtmlRenderer 是 flexmark 框架中的類,flexmark 可以將 markdown 轉為 Html。

參考文章:Java 實現 markdown轉Html
maven:com.vladsch.flexmark

md 檔案解析和新增博文


// 解析 md 檔案讀取字元流,新增記錄到資料庫
private BlogTitleIdDTO analysisAndInsertMdFile(Parser parser, HtmlRenderer renderer, ZipEntry entry, InputStreamReader reader, int bloggerId) throws IOException {
if (!entry.getName().endsWith(".md")) return null;
// 檔名作為標題
String title = entry.getName().replace(".md", "");
StringBuilder b = new StringBuilder((int) entry.getSize());
int len = 0;
char[] buff = new char[1024];
while ((len = reader.read(buff)) > 0) {
b.append(buff, 0, len);
}
// reader.close();
// zip 檔案關閉由 insertBlogPatch 方法 finally 塊中的 zipFile.close() 統一關閉。
// 博文內容
String mdContent = b.toString();
// 對應的 html 內容
Document document = parser.parse(mdContent);
String htmlContent = renderer.render(document);
// 摘要
String firReg = htmlContent.replaceAll("<.*?>", ""); // 避免 subString 使有遺留的 html 標籤,這樣前端顯示時可能會出錯
String tmpStr = firReg.length() > 500 ? firReg.substring(0, 500) : firReg;
String aftReg = tmpStr.replaceAll("\\n", "");
String summary = aftReg.length() > 200 ? aftReg.substring(0, 200) : aftReg;
// 呼叫博文新增方法:插入資料到資料庫,建立 Lucene 索引 ...
int id = insertBlog(bloggerId, null, null, PUBLIC, title, htmlContent, mdContent, summary, null, false);
if (id < 0) return null;
BlogTitleIdDTO node = new BlogTitleIdDTO();
node.setTitle(title);
node.setId(id);
return node;
}

思路通過註釋能夠很好的說明,也就不再使用過多語言描述。

Controller 層程式碼原始檔:BloggerBlogController.java
Service 層程式碼原始檔:BloggerBlogServiceImpl.java