最近忙完了公司的事情,在空閒時間,來更新一下自己的博客了。現在博客在我個人博客在自己的努力推廣下,終於有了一些訪問量(屈指可數),有一些朋友會回覆一些文章進行詢問和探討。
由於沒什麼時間,一直沒有完善評論功能,還必須每次登陸後臺才能知道有沒有新用戶的評論。 大部分時間都不能及時回覆,回覆的話,用戶如果不來瀏覽你的網頁,他也不知道,所以就想做一個郵件提醒,告訴用戶,有人回覆你的評論了,快來我博客看看。
需求分析
- 總結起來就兩個功能
- 用戶評論後,發送郵件通知博主
- 博主在後臺可以回覆對應的評論,並且如果評論人填了郵箱,發送通知到評論人
我們來細分一下這兩個功能,以及講一下具體的實現。大家可以想一想,如果是你,你會如何去實現這一簡單的功能,有不同的意見,歡迎大家進行交流和探討。
表修改
- 新增兩個字段,
reply_id
和from_author
reply_id
用於記錄是回覆的哪個評論,
from_author
作爲boolean類型,用於表示是否是博主做出的評論。(用於以後前端加特效,duan duan duan~)
CREATE TABLE `c_comment` (
`id` varchar(40) NOT NULL,
`article_id` varchar(40) NOT NULL,
`nickname` varchar(50) NOT NULL,
`email` varchar(50) NOT NULL,
`content` varchar(512) DEFAULT NULL,
`state` int(11) NOT NULL DEFAULT '1' COMMENT '0: 刪除\n1: 正常\n',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`ip` varchar(40) NOT NULL,
`reply_id` varchar(40) DEFAULT NULL,
`from_author` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`),
KEY `fk_c_comment_c_article1_idx` (`article_id`),
KEY `fk_c_comment_c_comment1_idx` (`reply_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
郵件發送構思
- 基礎郵件發送模塊
- 使用模板來發送郵件,郵件是支持 html 格式的,雖然每個郵件服務商支持的標準不同,但是使用模板還是可以一定程度的美化郵件內容,讓用戶擁有更好的體驗。
- 發送郵件作爲一個提醒服務,大部分情況下不需要同步。發送郵件需要佔用一定的時間,而且服務器的網絡情況和郵件服務商的服務器不能確定,有一定機率發送失敗,這個時候需要保證正常的業務邏輯不受影響。
新增接口
- 新增回覆接口,博主在後臺進行回覆操作的時候調用,參數爲 回覆內容以及被回覆的評論id
- 修改原有前端調用的評論接口,將
from_author
設置爲false
渲染模板
寫了,TemplateRenderUtil工具類,提供 render
方法,基礎的是render(String temp, Map<String,String>)
這個方法,其他所有方法都是對這個方法的重載。
Map<String,String> tempData = new HashMap<>();
tempData.put("nickname", "testnickname");
String renderResult = TemplateRenderUtil.render("<p> nickname: {{nickname}}</p>", tempData);
System.out.println(renderResult);
// 輸出 <p> nickname: testnickname</p>
package diamond.cms.server.utils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TemplateRenderUtil {
public static String renderResource(String resourcePath, Object value) throws IOException {
InputStream input = TemplateRenderUtil.class.getResourceAsStream(resourcePath);
return render(input, value);
}
/**
* @see diamond.cms.server.utils.TemplateRenderUtil#render(String, Map)
* @param file
* @param value
* @return
* @throws IOException
*/
public static String render(File file, Object value) throws IOException {
return render(new FileInputStream(file), value);
}
/**
* @see diamond.cms.server.utils.TemplateRenderUtil#render(String, Map)
* @param in
* @param value
* @return
* @throws IOException
*/
public static String render(InputStream in, Object value) throws IOException {
BufferedReader re = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuffer fileContent = new StringBuffer();
while((line = re.readLine()) != null) {
fileContent.append(line);
}
re.close();
return render(fileContent.toString(), value);
}
/**
* @see diamond.cms.server.utils.TemplateRenderUtil#render(String, Map)
* @param temp
* @param value
* @return
*/
public static String render(String temp, Object value) {
if (value instanceof Map) {
Map<?,?> map = (Map<?, ?>) value;
Map<String,String> stringMap = new HashMap<>();
map.entrySet().forEach(entry -> {
String key = entry.getKey() == null ? null : entry.getKey().toString();
String mapValue = entry.getValue() == null ? null : entry.getValue().toString();
stringMap.put(key, mapValue);
});
return render(temp, stringMap);
}
Map<String, String> map = new HashMap<>();
Arrays.asList(value.getClass().getMethods()).stream().filter(m -> {
return m.getName().startsWith("get") && m.getParameterCount() == 0;
}).forEach(method -> {
String name = method.getName().substring(3);
String fieldName = name.substring(0, 1).toLowerCase() + name.substring(1);
String stringResult = null;
try {
Object result = method.invoke(value);
stringResult = (result == null ? null : result.toString());
} catch (Exception e) {
}
map.put(fieldName, stringResult);
});
return render(temp, map);
}
/**
* render template string
* @param temp like "my name is {{name}}"
* @param data <Map> {"name": "diamond"}
* @return "my name is diamond
*/
public static String render(String temp, Map<String, String> data) {
Pattern pattern = Pattern.compile("\\{\\{[\\w]{0,}\\}\\}");
Matcher m = pattern.matcher(temp);
while (m.find()) {
String mp = m.group();
String key = mp.substring(2).substring(0, mp.length() - 4);
String value = data.get(key);
temp = temp.replace(mp, value == null ? "" : value);
}
return temp;
}
}
郵件通知切面
根據兩個接口方法做不同切點,異步執行模板渲染,發送郵件等邏輯。
普通用戶評論,發送郵件通知管理員,使用通知管理員模板。
管理員回覆用戶,如果評論者有郵箱,發送郵件通知用戶,使用通知用戶模板。
異步就直接用 java8 的 CompletableFuture.runAsync
來完成,簡單粗暴。
避免不可預知情況,捕獲了異常並且輸出到錯誤日誌裏面去,方便排查。
package diamond.cms.server.mvc.aspect;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Resource;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import diamond.cms.server.model.Comment;
import diamond.cms.server.model.User;
import diamond.cms.server.services.ArticleService;
import diamond.cms.server.services.CommentService;
import diamond.cms.server.services.EmailSendService;
import diamond.cms.server.services.UserService;
import diamond.cms.server.utils.TemplateRenderUtil;
import diamond.cms.server.utils.ValidateUtils;
@Component
@Aspect
public class CommentEmailNoticeAspect{
public static String COMMENT_NOTICE_TEMP = "/email-template/CommentNoticeTemplate.html";
public static String REPLY_NOTICE_TEMP = "/email-template/ReplyCommentNoticeTemplate.html";
@Resource
UserService userService;
@Resource
EmailSendService emailSendService;
@Resource
ArticleService articleService;
@Resource
CommentService commentService;
Logger log = LoggerFactory.getLogger(getClass());
@AfterReturning(returning="comment", pointcut="execution(* diamond.cms.server.mvc.controllers.CommentController.saveComment(..))")
public void after(Comment comment) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
User admin = userService.findAdmin();
if (admin != null) {
String artTitle = articleService.getTitle(comment.getArticleId());
comment.setArticleTitle(artTitle);
try {
String emailContent = TemplateRenderUtil.renderResource(COMMENT_NOTICE_TEMP, comment);
emailSendService.sendEmail(admin.getUsername(), "Blog Comment Notice", emailContent, "comment-notice-" + comment.getId());
} catch (IOException e) {
log.error("template render error, send email after comment faild", e);
}
}
} catch(Exception e) {
log.error("send comment notice email faild", e);
}
}
});
}
@AfterReturning(returning="comment", pointcut="execution(* diamond.cms.server.mvc.controllers.CommentController.replyComment(..))")
public void afterReply(Comment comment) {
CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
try {
Comment byReplyComment = commentService.get(comment.getReplyId());
String toEmail = byReplyComment.getEmail();
if (ValidateUtils.isEmail(toEmail)) {
String articleTitle = articleService.getTitle(comment.getArticleId());
comment.setArticleTitle(articleTitle);
try {
String emailContent = TemplateRenderUtil.renderResource(REPLY_NOTICE_TEMP, comment);
emailSendService.sendEmail(toEmail, "Comment Reply Notice", emailContent, "comment-reply-" + comment.getId());
} catch (IOException e) {
log.error("template render error, send email reply comment faild", e);
}
}
} catch (Exception e) {
log.error("send comment reply email faild", e);
}
}
});
}
}
總結
每個人實現功能的想法都大不相同,希望我的這篇文章可以在你的工作,學習中帶來一定的幫助。更多的源碼細節可以看本博客的源碼
後臺接口源碼: github-cms-admin-end
因爲使用的是前後端分離的架構,所以這個項目是可以獨立跑起來的,並且有相應的單元測試,可以進行一些接口的調試和驗證功能完整性。
博客系統沒有那麼多複雜的功能,整體架構較爲簡單。對整個項目的分包比較細,想着以後功能越來越多的時候,可以方便的拆分,服務化等。