diff --git a/pom.xml b/pom.xml index f41bda7..389a35e 100644 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,12 @@ commonmark-ext-gfm-tables ${commonmark.version} + + + + org.springframework.boot + spring-boot-starter-mail + diff --git a/src/main/java/cc/bnblogs/common/DefaultImages.java b/src/main/java/cc/bnblogs/common/DefaultImages.java index 87e206d..bb172c0 100644 --- a/src/main/java/cc/bnblogs/common/DefaultImages.java +++ b/src/main/java/cc/bnblogs/common/DefaultImages.java @@ -6,6 +6,8 @@ import org.springframework.stereotype.Component; import org.thymeleaf.util.StringUtils; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * @author zfp@bnblogs.cc @@ -15,13 +17,30 @@ import java.util.List; @Data @ConfigurationProperties(prefix = "default-images") public class DefaultImages { + /** + * 默认文章封面 + */ private List images; + /** + * 默认评论头像 + */ + private List avatars; public String cover(String imgUrl) { if (StringUtils.isEmptyOrWhitespace(imgUrl)) { - // 返回一张随机图 + // 返回一张随机图作为封面 return images.get((int)(Math.random() * images.size())); } return imgUrl; } + + public String avatar(String mail) { + Pattern pattern = Pattern.compile("(\\d{5,10})@qq.com"); + Matcher matcher = pattern.matcher(mail); + if (matcher.find()) { + String qq = matcher.group(1); + return String.format("https://q1.qlogo.cn/g?b=qq&nk=%s&s=100", qq); + } + return avatars.get((int) (Math.random() * avatars.size())); + } } diff --git a/src/main/java/cc/bnblogs/common/MailHelper.java b/src/main/java/cc/bnblogs/common/MailHelper.java new file mode 100644 index 0000000..f0d781f --- /dev/null +++ b/src/main/java/cc/bnblogs/common/MailHelper.java @@ -0,0 +1,54 @@ +package cc.bnblogs.common; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.util.Date; + +/** + * @author zfp@bnblogs.cc + * @createTime: 2022/10/22 + */ +@Slf4j +@Component +public class MailHelper { + @Value("${spring.mail.username}") + private String from; + private final JavaMailSender javaMailSender; + + public MailHelper(JavaMailSender javaMailSender) { + this.javaMailSender = javaMailSender; + } + + /** + * 发送邮件 + * @param toSend 收件人(目标邮箱) + * @param subject 主题 + * @param text 内容 + */ + public void sendMail(String toSend, String subject, String text) { + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message); + try { + //设置发件时间 + helper.setSentDate(new Date()); + // 发件人(配置文件中的邮箱) + helper.setFrom(from); + //设置收件人 + helper.setTo(toSend); + //设置标签 + helper.setSubject(subject); + //设置内容 + helper.setText(text, true); + //发邮件 + javaMailSender.send(message); + } catch (MessagingException e) { + log.error(e.getMessage(), e); + } + } +} diff --git a/src/main/java/cc/bnblogs/common/PageHelper.java b/src/main/java/cc/bnblogs/common/PageHelper.java index 851e05d..7e39a11 100644 --- a/src/main/java/cc/bnblogs/common/PageHelper.java +++ b/src/main/java/cc/bnblogs/common/PageHelper.java @@ -15,6 +15,7 @@ import java.util.List; @Builder @AllArgsConstructor @NoArgsConstructor +// T: 返回的数据类型 public class PageHelper { // 数据列表 private List rows; diff --git a/src/main/java/cc/bnblogs/controller/IndexController.java b/src/main/java/cc/bnblogs/controller/IndexController.java index 01a0922..27b60b1 100644 --- a/src/main/java/cc/bnblogs/controller/IndexController.java +++ b/src/main/java/cc/bnblogs/controller/IndexController.java @@ -1,21 +1,18 @@ package cc.bnblogs.controller; -import cc.bnblogs.common.ArticleSearch; -import cc.bnblogs.common.LRUCache; -import cc.bnblogs.common.PageHelper; +import cc.bnblogs.common.*; import cc.bnblogs.config.WebConfig; +import cc.bnblogs.enums.ResultEnum; import cc.bnblogs.pojo.Article; import cc.bnblogs.pojo.Category; +import cc.bnblogs.pojo.Comment; import cc.bnblogs.pojo.Tag; import cc.bnblogs.service.*; import cc.bnblogs.utils.CookieUtil; -import org.springframework.http.HttpRequest; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; +import org.thymeleaf.util.StringUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -40,11 +37,15 @@ public class IndexController { private final LRUCache lruCache; + private final CommentService commentService; + + private final WebSite webSite; + public IndexController(FriendsService friendsService, NavigationService navigationService, TagService tagService, ArticleService articleService, BannerService bannerService, - CategoryService categoryService, LRUCache lruCache) { + CategoryService categoryService, LRUCache lruCache, CommentService commentService, WebSite webSite) { this.friendsService = friendsService; this.navigationService = navigationService; this.tagService = tagService; @@ -52,6 +53,8 @@ public class IndexController { this.bannerService = bannerService; this.categoryService = categoryService; this.lruCache = lruCache; + this.commentService = commentService; + this.webSite = webSite; } @@ -192,6 +195,44 @@ public class IndexController { } // todo 文章内容不存在时应该抛出异常 model.addAttribute("article",article); + PageHelper commentPage = commentService.show(id,1); + // 返回第一页评论 + model.addAttribute("commentPage",commentPage); return "detail"; } + + /** + * 提交评论 + * @param comment 文章评论 + * @return 操作结果 + */ + @PostMapping("/comment") + @ResponseBody + public Result comment(Comment comment) { + // 邮箱是否和管理员相同 + if (StringUtils.equals(comment.getEmail(),webSite.getMail())) { + return Result.error(ResultEnum.RESULT_MAIL_FAILED); + } + // 昵称不能和管理员重名 + if (StringUtils.equals(comment.getNickname(),webSite.getNickname())) { + return Result.error(ResultEnum.RESULT_COMMENT_NAME_FAILED); + } + commentService.save(comment); + return Result.success(); + } + + /** + * 评论局部刷新 + * @param id 评论id + * @param pageNumber 页号 + * @param model MVCmodel + * @return + */ + @GetMapping("/comment/{id}") + public String comments(@PathVariable Integer id, @RequestParam(required = false, defaultValue = "1") Integer pageNumber, Model model) { + pageNumber = Math.max(1, pageNumber); + PageHelper commentPage = commentService.show(id, pageNumber); + model.addAttribute("commentPage", commentPage); + return "detail::comments"; + } } diff --git a/src/main/java/cc/bnblogs/enums/ResultEnum.java b/src/main/java/cc/bnblogs/enums/ResultEnum.java index 7c27a73..dc17832 100644 --- a/src/main/java/cc/bnblogs/enums/ResultEnum.java +++ b/src/main/java/cc/bnblogs/enums/ResultEnum.java @@ -15,14 +15,17 @@ public enum ResultEnum { * 操作结果的枚举常量 */ RESULT_SUCCESS(200,"请求成功"), - RESULT_NOT_FOUND(404,"数据不存在"), RESULT_UPLOAD_FAIL(501,"文件上传出错"), + RESULT_MAIL_FAILED(503,"不能用管理员邮箱进行评论"), + + RESULT_COMMENT_NAME_FAILED(504,"不能与管理员重名"), RESULT_ERROR(500,"请求错误"); + private final Integer code; private final String message; } diff --git a/src/main/java/cc/bnblogs/mapper/CommentMapper.java b/src/main/java/cc/bnblogs/mapper/CommentMapper.java index c4b7ff0..300758e 100644 --- a/src/main/java/cc/bnblogs/mapper/CommentMapper.java +++ b/src/main/java/cc/bnblogs/mapper/CommentMapper.java @@ -2,8 +2,12 @@ package cc.bnblogs.mapper; import cc.bnblogs.pojo.Article; import cc.bnblogs.pojo.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + /** * @author zfp@bnblogs.cc * @createTime: 2022/10/17 @@ -15,4 +19,20 @@ public interface CommentMapper extends JpaRepository { * @return 评论个数 */ long countByArticle(Article article); + + /** + * 按照文章创建时间降序来分页 + * @param article 文章 + * @param pid 父评论的id + * @param pageable 分页 + * @return 该页的评论 + */ + Page findAllByArticleAndPidOrderByCreatedDesc(Article article, Integer pid,Pageable pageable); + + /** + * 根据父id获取所有子评论 + * @param pid 评论的父id + * @return 该父评论下的所有评论 + */ + List findAllByPidOrderByCreatedDesc(Integer pid); } diff --git a/src/main/java/cc/bnblogs/pojo/Comment.java b/src/main/java/cc/bnblogs/pojo/Comment.java index 8aa209f..c1fe0fb 100644 --- a/src/main/java/cc/bnblogs/pojo/Comment.java +++ b/src/main/java/cc/bnblogs/pojo/Comment.java @@ -8,6 +8,7 @@ import lombok.NoArgsConstructor; import javax.persistence.*; import java.io.Serializable; import java.util.Date; +import java.util.List; /** * @author zfp@bnblogs.cc @@ -43,5 +44,8 @@ public class Comment implements Serializable { // 评论和文章是多对一关系 @ManyToOne private Article article; + // 父评论的评论列表 + @Transient + private List children; } diff --git a/src/main/java/cc/bnblogs/service/ArticleService.java b/src/main/java/cc/bnblogs/service/ArticleService.java index dccd647..e7138c2 100644 --- a/src/main/java/cc/bnblogs/service/ArticleService.java +++ b/src/main/java/cc/bnblogs/service/ArticleService.java @@ -71,7 +71,12 @@ public class ArticleService { * @return id对应的文章对象 */ public Article detail(Integer id) { - return articleMapper.findById(id).orElse(null); + Article article = articleMapper.findById(id).orElse(null); + if (Objects.isNull(article)) { + return null; + } + article.setCommentCount(commentMapper.countByArticle(article)); + return article; } /** diff --git a/src/main/java/cc/bnblogs/service/CommentService.java b/src/main/java/cc/bnblogs/service/CommentService.java index 91559ed..c48c8ae 100644 --- a/src/main/java/cc/bnblogs/service/CommentService.java +++ b/src/main/java/cc/bnblogs/service/CommentService.java @@ -1,10 +1,22 @@ package cc.bnblogs.service; +import cc.bnblogs.common.MailHelper; +import cc.bnblogs.common.PageHelper; +import cc.bnblogs.common.WebSite; +import cc.bnblogs.mapper.ArticleMapper; import cc.bnblogs.mapper.CommentMapper; +import cc.bnblogs.pojo.Article; import cc.bnblogs.pojo.Comment; +import cc.bnblogs.utils.UpdateUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import java.util.Date; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; /** * @author zfp@bnblogs.cc @@ -15,12 +27,20 @@ public class CommentService { private final CommentMapper commentMapper; - public CommentService(CommentMapper commentMapper) { + private final MailHelper mailHelper; + private final WebSite webSite; + private final ArticleMapper articleMapper; + + public CommentService(CommentMapper commentMapper, MailHelper mailHelper, WebSite webSite, ArticleMapper articleMapper) { this.commentMapper = commentMapper; + this.mailHelper = mailHelper; + this.webSite = webSite; + this.articleMapper = articleMapper; } /** * 获取所有评论 + * * @return 评论列表 */ public List list() { @@ -29,6 +49,7 @@ public class CommentService { /** * 获取评论总数 + * * @return 评论总数 */ public long count() { @@ -37,6 +58,7 @@ public class CommentService { /** * 根据id查询评论对象 + * * @param id 评论id * @return id对应的评论对象 */ @@ -46,17 +68,106 @@ public class CommentService { /** * 保存评论 + * * @param comment 待保存的评论对象 */ public void save(Comment comment) { - commentMapper.save(comment); + // 如果是新评论 + if (Objects.isNull(comment.getId())) { + // 有人在你的网站评论了 + sendMailToWebsite(comment.getArticle().getId()); + comment.setCreated(new Date()); + comment.setView(false); + if (comment.getPid() != 0) { + // 有人回复了你的评论 + sendMailToComment(comment.getArticle().getId(), comment.getPid(), comment.getContent()); + // 展示它的父评论 + Comment parent = detail(comment.getPid()); + comment.setContent("@" + parent.getNickname() + ": " + comment.getContent()); + comment.setPid(findCommentPid(comment.getPid())); + } + commentMapper.save(comment); + } else { + Comment one = detail(comment.getId()); + UpdateUtil.copyNullProperties(comment, one); + commentMapper.save(one); + } + } + + /** + * 网站有人留言了发送一封邮件给站长 + * + * @param id 文章id + */ + @Async + public void sendMailToWebsite(Integer id) { + Article article = articleMapper.findById(id).orElse(null); + if (Objects.isNull(article)) { + return; + } + String content = "

\n" + " 你的文章 " + article.getTitle() + + "收到了新评论," + + "" + "点此" + "查看\n" + "

\n" + + "

\n" + "时间:\n" + new Date() + "

"; + mailHelper.sendMail(webSite.getMail(), webSite.getTitle() + "收到新评论", content); + } + + /** + * 异步方法: 有人回复了你的评论,需要给你发送一封邮件 + * + * @param id 文章id + * @param cid 被评论的评论id + * @param reply 回复内容 + */ + @Async + public void sendMailToComment(Integer id, Integer cid, String reply) { + Article article = articleMapper.findById(id).orElse(null); + if (Objects.isNull(article)) { + return; + } + Comment comment = commentMapper.findById(cid).orElse(null); + if (Objects.isNull(comment)) { + return; + } + String content = "

\n" + " 你在" + webSite.getTitle() + "" + article.getTitle() + "文章的评论收到了新回复,回复内容如下:\n" + "

\n" + "

\n" + reply + "

\n" + "

\n" + " 一一发件人:" + webSite.getTitle() + "

\n" + "

\n" + "时间:" + new Date() + "

\n" + "

\n" + " 此邮件是由" + webSite.getTitle() + "自动发送,请勿回复 \n" + "

"; + mailHelper.sendMail(comment.getEmail(), webSite.getTitle() + "收到新回复", content); + } + + /** + * 递归查找直到找到最原始评论 + * + * @param id 评论id + * @return 祖先评论的id + */ + private Integer findCommentPid(Integer id) { + Comment comment = detail(id); + // 直到找到某个评论的pid为0,因为他就是最原始的评论 + if (comment.getPid() == 0) { + return comment.getId(); + } + return findCommentPid(comment.getPid()); } /** * 根据id删除评论对象 + * * @param id 待删除评论的id */ public void delete(Integer id) { commentMapper.deleteById(id); } + + /** + * 返回文章的评论 + * + * @param id 文章id + * @param pageNumber 初始页号 + * @return 评论分页 + */ + public PageHelper show(Integer id, int pageNumber) { + // 每页显示5个评论 + Page page = commentMapper.findAllByArticleAndPidOrderByCreatedDesc(Article.builder().id(id).build(), 0, PageRequest.of(pageNumber - 1, 5)); + // 评论内容-当前页数-评论总数-总页数 + return PageHelper.builder().rows(page.getContent().stream().peek(x -> x.setChildren(commentMapper.findAllByPidOrderByCreatedDesc(x.getId()))).collect(Collectors.toList())).currentPage(pageNumber).total(page.getTotalElements()).totalPages(page.getTotalPages()).build(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8634cd..f80f902 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,6 +34,21 @@ spring: type: ehcache ehcache: config: classpath:/ehcache-spring.xml + mail: + # 默认的邮件编码为UTF-8 + default-encoding: UTF-8 + # 邮箱服务器 + host: smtp.qq.com + # 邮箱 + username: 1337425156@qq.com + # 密码 + password: yidyoatxwswwjgjj + # 端口 + port: 587 + # 其它属性,这里只开启debug输出错误信息 + properties: + debug: true + logging: file: @@ -81,3 +96,19 @@ default-images: - /static/image/3.jpg - /static/image/4.jpg - /static/image/5.jpg + avatars: + - /static/image/avatar/1.jpg + - /static/image/avatar/2.jpg + - /static/image/avatar/3.jpg + - /static/image/avatar/4.jpg + - /static/image/avatar/5.jpg + - /static/image/avatar/6.jpg + - /static/image/avatar/7.jpg + - /static/image/avatar/8.jpg + - /static/image/avatar/9.jpg + - /static/image/avatar/10.jpg + - /static/image/avatar/11.jpg + - /static/image/avatar/12.jpg + - /static/image/avatar/13.jpg + - /static/image/avatar/14.jpg + diff --git a/src/main/resources/static/image/avatar/1.jpg b/src/main/resources/static/image/avatar/1.jpg new file mode 100644 index 0000000..332cafa Binary files /dev/null and b/src/main/resources/static/image/avatar/1.jpg differ diff --git a/src/main/resources/static/image/avatar/10.jpg b/src/main/resources/static/image/avatar/10.jpg new file mode 100644 index 0000000..526fc4b Binary files /dev/null and b/src/main/resources/static/image/avatar/10.jpg differ diff --git a/src/main/resources/static/image/avatar/11.jpg b/src/main/resources/static/image/avatar/11.jpg new file mode 100644 index 0000000..460afb4 Binary files /dev/null and b/src/main/resources/static/image/avatar/11.jpg differ diff --git a/src/main/resources/static/image/avatar/12.jpg b/src/main/resources/static/image/avatar/12.jpg new file mode 100644 index 0000000..714dd63 Binary files /dev/null and b/src/main/resources/static/image/avatar/12.jpg differ diff --git a/src/main/resources/static/image/avatar/13.jpg b/src/main/resources/static/image/avatar/13.jpg new file mode 100644 index 0000000..6dcd460 Binary files /dev/null and b/src/main/resources/static/image/avatar/13.jpg differ diff --git a/src/main/resources/static/image/avatar/14.jpg b/src/main/resources/static/image/avatar/14.jpg new file mode 100644 index 0000000..0495fa2 Binary files /dev/null and b/src/main/resources/static/image/avatar/14.jpg differ diff --git a/src/main/resources/static/image/avatar/2.jpg b/src/main/resources/static/image/avatar/2.jpg new file mode 100644 index 0000000..a65aec8 Binary files /dev/null and b/src/main/resources/static/image/avatar/2.jpg differ diff --git a/src/main/resources/static/image/avatar/3.jpg b/src/main/resources/static/image/avatar/3.jpg new file mode 100644 index 0000000..c02e287 Binary files /dev/null and b/src/main/resources/static/image/avatar/3.jpg differ diff --git a/src/main/resources/static/image/avatar/4.jpg b/src/main/resources/static/image/avatar/4.jpg new file mode 100644 index 0000000..a805f74 Binary files /dev/null and b/src/main/resources/static/image/avatar/4.jpg differ diff --git a/src/main/resources/static/image/avatar/5.jpg b/src/main/resources/static/image/avatar/5.jpg new file mode 100644 index 0000000..233a1ec Binary files /dev/null and b/src/main/resources/static/image/avatar/5.jpg differ diff --git a/src/main/resources/static/image/avatar/6.jpg b/src/main/resources/static/image/avatar/6.jpg new file mode 100644 index 0000000..288a759 Binary files /dev/null and b/src/main/resources/static/image/avatar/6.jpg differ diff --git a/src/main/resources/static/image/avatar/7.jpg b/src/main/resources/static/image/avatar/7.jpg new file mode 100644 index 0000000..49c5bb0 Binary files /dev/null and b/src/main/resources/static/image/avatar/7.jpg differ diff --git a/src/main/resources/static/image/avatar/8.jpg b/src/main/resources/static/image/avatar/8.jpg new file mode 100644 index 0000000..1ad4131 Binary files /dev/null and b/src/main/resources/static/image/avatar/8.jpg differ diff --git a/src/main/resources/static/image/avatar/9.jpg b/src/main/resources/static/image/avatar/9.jpg new file mode 100644 index 0000000..828d6d8 Binary files /dev/null and b/src/main/resources/static/image/avatar/9.jpg differ diff --git a/src/main/resources/templates/common.html b/src/main/resources/templates/common.html index aafef98..0098b83 100644 --- a/src/main/resources/templates/common.html +++ b/src/main/resources/templates/common.html @@ -28,7 +28,7 @@ > - +
diff --git a/src/main/resources/templates/detail.html b/src/main/resources/templates/detail.html index c4b7907..a92f23f 100644 --- a/src/main/resources/templates/detail.html +++ b/src/main/resources/templates/detail.html @@ -66,13 +66,15 @@
-
+

@@ -136,102 +138,96 @@

+ +
+

评论 ( + 共条 + ) +

+
-

评论(6)

-
+ + + +
- +
- +
-
- +
+
- +
+
+ +
+
- -
-
    -
  • -
    - -
    -
    -

    xpboy 2022-06-03 18:32 回复

    -

    - 已经解决,问题是后台的的永久链接--重写功能,一定要开启成功,我是开启没有成功,但是可以使用,部分功能受限,如点赞、登录、注册等。我是windows - iis,添加web.config放到网站根目录就可以了,希望踩坑的朋友注意了。具体的web.config伪静态规则代码,可以联系我免费提供哦 - qq24985536,希望帮助到大家

    -
    -
      -
    • -
      - -
      -
      -

      VOODOO 2022-09-28 19:53 回复

      -

      - @xpboy -
      - 谢谢

      -
      -
    • -
    -
  • -
  • -
    - -
    -
    -

    anle 2022-06-03 18:32 回复

    -

    - 您好~我是俺没偷前端的运营,关注了您在分享的技术文章,觉得您的这套模板很棒,我们诚挚邀请您加入俺没偷前端CP主计划。完整福利和详细介绍请见:https://anlenotes.com/cp - 我们会给作者提供包括流量、创作分成等, 我们诚挚的邀请您并期待您的加入~

    -
    -
  • -
  • -
    - -
    -
    -

    王伟忘记使自己快乐 2022-06-03 18:32 回复

    -

    - 你好,请问一下,搭建的网页,在本地访问一点问题都没有,通过互联网域名访问,打开很慢,而且显示不正常,电脑手机都是一样的,是什么问题呢

    -
    +
    + +
    + +
    +
      +
    • 首页
    • +
    • +
    • -
    • -
      - -
      -
      -

      小布丁 2022-06-03 18:32 回复

      -

      - 为什么启用这个主题 后,评论用不了了,报错。

      -
      +
    • + 尾页
    • +
    +
    - -
@@ -292,7 +288,8 @@
- + + @@ -328,6 +325,8 @@ + +