當我在寫一個評論通知功能的時候, 我在想些什麼? 原

最近忙完了公司的事情,在空閒時間,來更新一下自己的博客了。現在博客在我個人博客在自己的努力推廣下,終於有了一些訪問量(屈指可數),有一些朋友會回覆一些文章進行詢問和探討。
由於沒什麼時間,一直沒有完善評論功能,還必須每次登陸後臺才能知道有沒有新用戶的評論。 大部分時間都不能及時回覆,回覆的話,用戶如果不來瀏覽你的網頁,他也不知道,所以就想做一個郵件提醒,告訴用戶,有人回覆你的評論了,快來我博客看看

需求分析

  • 總結起來就兩個功能
  1. 用戶評論後,發送郵件通知博主
  2. 博主在後臺可以回覆對應的評論,並且如果評論人填了郵箱,發送通知到評論人

我們來細分一下這兩個功能,以及講一下具體的實現。大家可以想一想,如果是你,你會如何去實現這一簡單的功能,有不同的意見,歡迎大家進行交流和探討。

表修改

  1. 新增兩個字段, reply_idfrom_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;

郵件發送構思

  1. 基礎郵件發送模塊
  2. 使用模板來發送郵件,郵件是支持 html 格式的,雖然每個郵件服務商支持的標準不同,但是使用模板還是可以一定程度的美化郵件內容,讓用戶擁有更好的體驗。
  3. 發送郵件作爲一個提醒服務,大部分情況下不需要同步。發送郵件需要佔用一定的時間,而且服務器的網絡情況和郵件服務商的服務器不能確定,有一定機率發送失敗,這個時候需要保證正常的業務邏輯不受影響。

新增接口

  1. 新增回覆接口,博主在後臺進行回覆操作的時候調用,參數爲 回覆內容以及被回覆的評論id
  2. 修改原有前端調用的評論接口,將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
因爲使用的是前後端分離的架構,所以這個項目是可以獨立跑起來的,並且有相應的單元測試,可以進行一些接口的調試和驗證功能完整性。
博客系統沒有那麼多複雜的功能,整體架構較爲簡單。對整個項目的分包比較細,想着以後功能越來越多的時候,可以方便的拆分,服務化等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章