文档章节

当我在写一个评论通知功能的时候, 我在想些什么?

DiamondFsd
 DiamondFsd
发布于 2017/04/01 19:55
字数 1585
阅读 44
收藏 1

最近忙完了公司的事情,在空闲时间,来更新一下自己的博客了。现在博客在我个人博客在自己的努力推广下,终于有了一些访问量(屈指可数),有一些朋友会回复一些文章进行询问和探讨。
由于没什么时间,一直没有完善评论功能,还必须每次登陆后台才能知道有没有新用户的评论。 大部分时间都不能及时回复,回复的话,用户如果不来浏览你的网页,他也不知道,所以就想做一个邮件提醒,告诉用户,有人回复你的评论了,快来我博客看看

需求分析

  • 总结起来就两个功能
  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
因为使用的是前后端分离的架构,所以这个项目是可以独立跑起来的,并且有相应的单元测试,可以进行一些接口的调试和验证功能完整性。
博客系统没有那么多复杂的功能,整体架构较为简单。对整个项目的分包比较细,想着以后功能越来越多的时候,可以方便的拆分,服务化等。

© 著作权归作者所有

DiamondFsd
粉丝 88
博文 18
码字总数 20427
作品 0
深圳
程序员
私信 提问
职场疑问:如何进行技术面试

笔者评论:本文涉及到的技术问题都是domino开发中的,本文只是以这个作为一个例子来说明问题,以哪种技术为例并不重要。 小A是一名Domino开发人员,转眼间已经一年多过去了。小A觉得自己技术...

zting科技
2017/01/10
0
0
模块与模块之间,如何进行对应的操作触发?

最近在写程序,是模块的方式。但发现个比较头疼的问题。由于都是以独立模块的方式开发,这样难免在一些功能上需要多个模块配合。比如评论和会员中心的关系吧。在设计时,除了会员接口是全部通...

水平凡
2013/09/11
121
0
你为什么得不到梦想中的开发工作

你可能对现在的工作不是很满意或者谈不上喜欢与不喜欢, 不然你也不会来看这篇文章。 很多开发者都梦想在一个这样的地方工作:工程师文化、极客、自由、对外宣称自己和Airbnb等硅谷公司一样。...

刘腾飞
2017/12/14
0
0
朋也社区 pybbs v5.1.0 更新,支持 websocket、i18n

上次发更新资讯结尾求助大佬帮忙写个dockerfile,然后Github用户 @zzzzbw 帮忙写了一个,再次感谢! 这次更新的内容如下: 1. 完善了redis的缓存,之前只实现了系统设置内容的缓存,这一版把...

朋也
01/24
765
5
一起撸个朋友圈吧 - 重构

项目地址:github.com/razerdp/Fri… 上篇链接:http://www.jianshu.com/p/deda1849a084 下篇链接:http://www.jianshu.com/p/8d24f9b7a63a 在我们这个项目开发进行到一半,因为某些问题,我...

WeiChaoFeng
2017/12/14
0
0

没有更多内容

加载失败,请刷新页面

加载更多

关于AsyncTask的onPostExcute方法是否会在Activity重建过程中调用的问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/XG1057415595/article/details/86774575 假设下面一种情况...

shzwork
今天
6
0
object 类中有哪些方法?

getClass(): 获取运行时类的对象 equals():判断其他对象是否与此对象相等 hashcode():返回该对象的哈希码值 toString():返回该对象的字符串表示 clone(): 创建并返此对象的一个副本 wait...

happywe
今天
6
0
Docker容器实战(七) - 容器中进程视野下的文件系统

前两文中,讲了Linux容器最基础的两种技术 Namespace 作用是“隔离”,它让应用进程只能看到该Namespace内的“世界” Cgroups 作用是“限制”,它给这个“世界”围上了一圈看不见的墙 这么一...

JavaEdge
今天
8
0
文件访问和共享的方法介绍

在上一篇文章中,你了解到文件有三个不同的权限集。拥有该文件的用户有一个集合,拥有该文件的组的成员有一个集合,然后最终一个集合适用于其他所有人。在长列表(ls -l)中这些权限使用符号...

老孟的Linux私房菜
今天
7
0
面试套路题目

作者:抱紧超越小姐姐 链接:https://www.nowcoder.com/discuss/309292?type=3 来源:牛客网 面试时候的潜台词 抱紧超越小姐姐 编辑于 2019-10-15 16:14:56APP内打开赞 3 | 收藏 4 | 回复24 ...

MtrS
今天
6
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部