🎨 文章发布/更新/删除后续事件执行过程解耦

This commit is contained in:
ronger 2022-08-20 19:11:30 +08:00
parent 97375a3dbd
commit a019078802
12 changed files with 226 additions and 101 deletions

View File

@ -97,4 +97,9 @@ forest[ˈfôrəst]n.森林)是一款现代化的知识社区项目,使
## 鸣谢 ## 鸣谢
- 感谢 `JetBrains` 对本项目的帮助,为作者提供了开源许可版 `JetBrains` 全家桶 - 感谢 `JetBrains` 对本项目的帮助,为作者提供了开源许可版 `JetBrains` 全家桶
![JetBrains](src/main/resources/static/jb_beam.svg) ![JetBrains](src/main/resources/static/jb_beam.svg)
## ⭐ Star 历史
[![Stargazers over time](https://starchart.cc/rymcu/forest.svg)](https://starchart.cc/rymcu/forest)

View File

@ -0,0 +1,66 @@
package com.rymcu.forest.handler;
import com.rymcu.forest.core.constant.NotificationConstant;
import com.rymcu.forest.handler.event.ArticleDeleteEvent;
import com.rymcu.forest.handler.event.ArticleEvent;
import com.rymcu.forest.lucene.service.LuceneService;
import com.rymcu.forest.util.NotificationUtils;
import com.rymcu.forest.wx.mp.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* Created on 2022/8/16 20:42.
*
* @author ronger
* @email ronger-x@outlook.com
*/
@Slf4j
@Component
public class ArticleHandler {
@Resource
private LuceneService luceneService;
@EventListener
@Async
public void processArticlePostEvent(ArticleEvent articleEvent) throws InterruptedException {
Thread.sleep(1000);
log.info(String.format("执行文章发布相关事件:[%s]", JsonUtils.toJson(articleEvent)));
// 发送系统通知
if (articleEvent.getNotification()) {
NotificationUtils.sendAnnouncement(articleEvent.getIdArticle(), NotificationConstant.Article, articleEvent.getArticleTitle());
} else {
// 发送关注通知
StringBuilder dataSummary = new StringBuilder();
if (articleEvent.getIsUpdate()) {
dataSummary.append(articleEvent.getNickname()).append("更新了文章: ").append(articleEvent.getArticleTitle());
NotificationUtils.sendArticlePush(articleEvent.getIdArticle(), NotificationConstant.UpdateArticle, dataSummary.toString(), articleEvent.getArticleAuthorId());
} else {
dataSummary.append(articleEvent.getNickname()).append("发布了文章: ").append(articleEvent.getArticleTitle());
NotificationUtils.sendArticlePush(articleEvent.getIdArticle(), NotificationConstant.PostArticle, dataSummary.toString(), articleEvent.getArticleAuthorId());
}
}
// 草稿不更新索引
if (articleEvent.getIsUpdate()) {
log.info("更新文章索引id={}", articleEvent.getIdArticle());
luceneService.updateArticle(articleEvent.getIdArticle());
} else {
log.info("写入文章索引id={}", articleEvent.getIdArticle());
luceneService.writeArticle(articleEvent.getIdArticle());
}
log.info("执行完成文章发布相关事件...id={}", articleEvent.getIdArticle());
}
@EventListener
@Async
public void processArticleDeleteEvent(ArticleDeleteEvent articleDeleteEvent) throws InterruptedException {
Thread.sleep(1000);
log.info(String.format("执行文章删除相关事件:[%s]", JsonUtils.toJson(articleDeleteEvent)));
luceneService.deleteArticle(articleDeleteEvent.getIdArticle());
log.info("执行完成文章删除相关事件...id={}", articleDeleteEvent.getIdArticle());
}
}

View File

@ -0,0 +1,18 @@
package com.rymcu.forest.handler.event;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Created on 2022/8/20 18:51.
*
* @author ronger
* @email ronger-x@outlook.com
*/
@Data
@AllArgsConstructor
public class ArticleDeleteEvent {
private Long idArticle;
}

View File

@ -0,0 +1,27 @@
package com.rymcu.forest.handler.event;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Created on 2022/8/16 20:56.
*
* @author ronger
* @email ronger-x@outlook.com
*/
@Data
@AllArgsConstructor
public class ArticleEvent {
private Long idArticle;
private String articleTitle;
private Boolean isUpdate;
private Boolean notification;
private String nickname;
private Long articleAuthorId;
}

View File

@ -66,7 +66,7 @@ public class LuceneServiceImpl implements LuceneService {
int totalCount = list.size(); int totalCount = list.size();
int perThreadCount = 3000; int perThreadCount = 3000;
// 加1避免线程池的参数为0 // 加1避免线程池的参数为0
int threadCount = totalCount / perThreadCount + (totalCount % perThreadCount == 0 ? 0 : 1) + 1; int threadCount = totalCount / perThreadCount + (totalCount % perThreadCount == 0 ? 0 : 1);
ExecutorService pool = Executors.newFixedThreadPool(threadCount); ExecutorService pool = Executors.newFixedThreadPool(threadCount);
CountDownLatch countDownLatch1 = new CountDownLatch(1); CountDownLatch countDownLatch1 = new CountDownLatch(1);
CountDownLatch countDownLatch2 = new CountDownLatch(threadCount); CountDownLatch countDownLatch2 = new CountDownLatch(threadCount);

View File

@ -56,7 +56,7 @@ public class PortfolioLuceneServiceImpl implements PortfolioLuceneService {
int totalCount = list.size(); int totalCount = list.size();
int perThreadCount = 3000; int perThreadCount = 3000;
// 加1避免线程池的参数为0 // 加1避免线程池的参数为0
int threadCount = totalCount / perThreadCount + (totalCount % perThreadCount == 0 ? 0 : 1) + 1; int threadCount = totalCount / perThreadCount + (totalCount % perThreadCount == 0 ? 0 : 1);
ExecutorService pool = Executors.newFixedThreadPool(threadCount); ExecutorService pool = Executors.newFixedThreadPool(threadCount);
CountDownLatch countDownLatch1 = new CountDownLatch(1); CountDownLatch countDownLatch1 = new CountDownLatch(1);
CountDownLatch countDownLatch2 = new CountDownLatch(threadCount); CountDownLatch countDownLatch2 = new CountDownLatch(threadCount);

View File

@ -1,7 +1,6 @@
package com.rymcu.forest.lucene.util; package com.rymcu.forest.lucene.util;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.rymcu.forest.lucene.model.ArticleLucene; import com.rymcu.forest.lucene.model.ArticleLucene;
import org.apache.lucene.document.Document; import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field; import org.apache.lucene.document.Field;
@ -12,6 +11,7 @@ import org.apache.lucene.index.Term;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* 文章索引更新工具类 * 文章索引更新工具类
@ -20,68 +20,85 @@ import java.util.Arrays;
*/ */
public class ArticleIndexUtil { public class ArticleIndexUtil {
/** lucene索引保存目录 */ /**
private static final String PATH = * lucene索引保存目录
System.getProperty("user.dir") + StrUtil.SLASH + LucenePath.ARTICLE_INDEX_PATH; */
private static final String PATH =
System.getProperty("user.dir") + LucenePath.ARTICLE_INDEX_PATH;
private static final String WINDOW_PATH = /**
System.getProperty("user.dir") + StrUtil.BACKSLASH + "lucene\\index\\article"; * 删除所有运行中保存的索引
*/
/** 删除所有运行中保存的索引 */ public static void deleteAllIndex() {
public static void deleteAllIndex() { if (FileUtil.exist(LucenePath.ARTICLE_INCREMENT_INDEX_PATH)) {
if (FileUtil.exist(LucenePath.ARTICLE_INCREMENT_INDEX_PATH)) { FileUtil.del(LucenePath.ARTICLE_INCREMENT_INDEX_PATH);
FileUtil.del(LucenePath.ARTICLE_INCREMENT_INDEX_PATH); }
} }
}
public static void addIndex(ArticleLucene t) { public static void addIndex(ArticleLucene t) {
creatIndex(t); creatIndex(t);
}
public static void updateIndex(ArticleLucene t) {
deleteIndex(t.getIdArticle());
creatIndex(t);
}
/**
* 增加或创建单个索引
*
* @param t
* @throws Exception
*/
private static synchronized void creatIndex(ArticleLucene t) {
System.out.println("创建单个索引");
IndexWriter writer;
try {
writer = IndexUtil.getIndexWriter(LucenePath.ARTICLE_INCREMENT_INDEX_PATH, false);
Document doc = new Document();
doc.add(new StringField("id", t.getIdArticle() + "", Field.Store.YES));
doc.add(new TextField("title", t.getArticleTitle(), Field.Store.YES));
doc.add(new TextField("summary", t.getArticleContent(), Field.Store.YES));
writer.addDocument(doc);
writer.close();
} catch (IOException e) {
e.printStackTrace();
} }
}
/** 删除单个索引 */ public static void updateIndex(ArticleLucene t) {
public static synchronized void deleteIndex(Long id) { deleteIndex(t.getIdArticle());
Arrays.stream(FileUtil.ls(PATH)) creatIndex(t);
.forEach( }
each -> {
if (each.isDirectory()) { /**
IndexWriter writer; * 增加或创建单个索引
try { *
writer = IndexUtil.getIndexWriter(each.getAbsolutePath(), false); * @param t
writer.deleteDocuments(new Term("id", String.valueOf(id))); * @throws Exception
writer.forceMergeDeletes(); // 强制删除 */
writer.commit(); private static void creatIndex(ArticleLucene t) {
writer.close(); System.out.printf("创建单个索引");
} catch (IOException e) { IndexWriter writer;
e.printStackTrace(); ReentrantLock reentrantLock = new ReentrantLock();
} reentrantLock.lock();
} try {
}); boolean create = true;
} if (FileUtil.exist(LucenePath.ARTICLE_INCREMENT_INDEX_PATH)) {
create = false;
}
writer = IndexUtil.getIndexWriter(LucenePath.ARTICLE_INCREMENT_INDEX_PATH, create);
Document doc = new Document();
doc.add(new StringField("id", t.getIdArticle() + "", Field.Store.YES));
doc.add(new TextField("title", t.getArticleTitle(), Field.Store.YES));
doc.add(new TextField("summary", t.getArticleContent(), Field.Store.YES));
writer.addDocument(doc);
writer.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
/**
* 删除单个索引
*/
public static void deleteIndex(Long id) {
Arrays.stream(FileUtil.ls(PATH))
.forEach(
each -> {
if (each.isDirectory()) {
IndexWriter writer;
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try {
writer = IndexUtil.getIndexWriter(each.getAbsolutePath(), false);
writer.deleteDocuments(new Term("id", String.valueOf(id)));
writer.forceMerge(1);
// 强制删除
writer.forceMergeDeletes();
writer.commit();
writer.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
});
}
} }

View File

@ -8,7 +8,7 @@ package com.rymcu.forest.lucene.util;
public final class LucenePath { public final class LucenePath {
/** lucene 目录 */ /** lucene 目录 */
public static final String INDEX_PATH = "lucene/index"; public static final String INDEX_PATH = "/lucene/index";
/** 文章 lucene 目录 */ /** 文章 lucene 目录 */
public static final String ARTICLE_INDEX_PATH = INDEX_PATH + "/article"; public static final String ARTICLE_INDEX_PATH = INDEX_PATH + "/article";

View File

@ -1,8 +1,8 @@
package com.rymcu.forest.service.impl; package com.rymcu.forest.service.impl;
import com.rymcu.forest.core.constant.NotificationConstant; import com.rymcu.forest.core.constant.NotificationConstant;
import com.rymcu.forest.core.exception.ContentNotExistException;
import com.rymcu.forest.core.exception.BusinessException; import com.rymcu.forest.core.exception.BusinessException;
import com.rymcu.forest.core.exception.ContentNotExistException;
import com.rymcu.forest.core.exception.UltraViresException; import com.rymcu.forest.core.exception.UltraViresException;
import com.rymcu.forest.core.service.AbstractService; import com.rymcu.forest.core.service.AbstractService;
import com.rymcu.forest.dto.*; import com.rymcu.forest.dto.*;
@ -10,26 +10,33 @@ import com.rymcu.forest.entity.Article;
import com.rymcu.forest.entity.ArticleContent; import com.rymcu.forest.entity.ArticleContent;
import com.rymcu.forest.entity.Tag; import com.rymcu.forest.entity.Tag;
import com.rymcu.forest.entity.User; import com.rymcu.forest.entity.User;
import com.rymcu.forest.lucene.service.LuceneService; import com.rymcu.forest.handler.event.ArticleDeleteEvent;
import com.rymcu.forest.handler.event.ArticleEvent;
import com.rymcu.forest.mapper.ArticleMapper; import com.rymcu.forest.mapper.ArticleMapper;
import com.rymcu.forest.service.ArticleService; import com.rymcu.forest.service.ArticleService;
import com.rymcu.forest.service.NotificationService; import com.rymcu.forest.service.NotificationService;
import com.rymcu.forest.service.TagService; import com.rymcu.forest.service.TagService;
import com.rymcu.forest.service.UserService; import com.rymcu.forest.service.UserService;
import com.rymcu.forest.util.*; import com.rymcu.forest.util.Html2TextUtil;
import com.rymcu.forest.util.UserUtils;
import com.rymcu.forest.util.Utils;
import com.rymcu.forest.util.XssUtils;
import com.rymcu.forest.web.api.exception.BaseApiException; import com.rymcu.forest.web.api.exception.BaseApiException;
import com.rymcu.forest.web.api.exception.ErrorCode; import com.rymcu.forest.web.api.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.text.StringEscapeUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Condition; import tk.mybatis.mapper.entity.Condition;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.*; import java.util.Date;
import java.util.List;
import java.util.Objects;
/** /**
* @author ronger * @author ronger
@ -45,9 +52,9 @@ public class ArticleServiceImpl extends AbstractService<Article> implements Arti
@Resource @Resource
private UserService userService; private UserService userService;
@Resource @Resource
private LuceneService luceneService;
@Resource
private NotificationService notificationService; private NotificationService notificationService;
@Resource
private ApplicationEventPublisher publisher;
@Value("${resource.domain}") @Value("${resource.domain}")
private String domain; private String domain;
@ -141,39 +148,16 @@ public class ArticleServiceImpl extends AbstractService<Article> implements Arti
articleMapper.updateArticleContent(newArticle.getIdArticle(), articleContent, articleContentHtml); articleMapper.updateArticleContent(newArticle.getIdArticle(), articleContent, articleContentHtml);
} }
Long newArticleId = newArticle.getIdArticle(); Long newArticleId = newArticle.getIdArticle();
// 发送相关通知 // 更新文章链接
if (DEFAULT_STATUS.equals(newArticle.getArticleStatus())) { if (DEFAULT_STATUS.equals(newArticle.getArticleStatus())) {
// 发送系统通知 // 文章
if (notification) {
NotificationUtils.sendAnnouncement(newArticleId, NotificationConstant.Article, newArticle.getArticleTitle());
} else {
// 发送关注通知
StringBuilder dataSummary = new StringBuilder();
if (isUpdate) {
dataSummary.append(user.getNickname()).append("更新了文章: ").append(newArticle.getArticleTitle());
NotificationUtils.sendArticlePush(newArticleId, NotificationConstant.UpdateArticle, dataSummary.toString(), newArticle.getArticleAuthorId());
} else {
dataSummary.append(user.getNickname()).append("发布了文章: ").append(newArticle.getArticleTitle());
NotificationUtils.sendArticlePush(newArticleId, NotificationConstant.PostArticle, dataSummary.toString(), newArticle.getArticleAuthorId());
}
}
// 草稿不更新索引
if (isUpdate) {
log.info("更新文章索引id={}", newArticleId);
luceneService.updateArticle(newArticleId);
} else {
log.info("写入文章索引id={}", newArticleId);
luceneService.writeArticle(newArticleId);
}
// 更新文章链接
newArticle.setArticlePermalink(domain + "/article/" + newArticleId); newArticle.setArticlePermalink(domain + "/article/" + newArticleId);
newArticle.setArticleLink("/article/" + newArticleId); newArticle.setArticleLink("/article/" + newArticleId);
} else { } else {
// 更新文章链接 // 草稿
newArticle.setArticlePermalink(domain + "/draft/" + newArticleId); newArticle.setArticlePermalink(domain + "/draft/" + newArticleId);
newArticle.setArticleLink("/draft/" + newArticleId); newArticle.setArticleLink("/draft/" + newArticleId);
} }
tagService.saveTagArticle(newArticle, articleContentHtml, user.getIdUser());
if (StringUtils.isNotBlank(articleContentHtml)) { if (StringUtils.isNotBlank(articleContentHtml)) {
String previewContent = Html2TextUtil.getContent(articleContentHtml); String previewContent = Html2TextUtil.getContent(articleContentHtml);
@ -183,6 +167,12 @@ public class ArticleServiceImpl extends AbstractService<Article> implements Arti
newArticle.setArticlePreviewContent(previewContent); newArticle.setArticlePreviewContent(previewContent);
} }
articleMapper.updateByPrimaryKeySelective(newArticle); articleMapper.updateByPrimaryKeySelective(newArticle);
// 更新标签
tagService.saveTagArticle(newArticle, articleContentHtml, user.getIdUser());
if (DEFAULT_STATUS.equals(newArticle.getArticleStatus())) {
// 文章发布事件
publisher.publishEvent(new ArticleEvent(newArticleId, newArticle.getArticleTitle(), isUpdate, notification, user.getNickname(), newArticle.getArticleAuthorId()));
}
return newArticleId; return newArticleId;
} }
@ -195,7 +185,9 @@ public class ArticleServiceImpl extends AbstractService<Article> implements Arti
deleteLinkedData(id); deleteLinkedData(id);
// 删除文章 // 删除文章
int result = articleMapper.deleteByPrimaryKey(id); int result = articleMapper.deleteByPrimaryKey(id);
luceneService.deleteArticle(id); if (result > 0) {
publisher.publishEvent(new ArticleDeleteEvent(id));
}
return result; return result;
} else { } else {
throw new BusinessException("已有评论的文章不允许删除!"); throw new BusinessException("已有评论的文章不允许删除!");

View File

@ -35,7 +35,7 @@
select art.id, art.article_title, content.article_content_html as article_content select art.id, art.article_title, content.article_content_html as article_content
from forest_article art from forest_article art
join forest_article_content content on art.id = content.id_article join forest_article_content content on art.id = content.id_article
where article_status = 0; where article_status = 0
</select> </select>
<select id="getArticlesByIds" resultMap="DTOResultMap"> <select id="getArticlesByIds" resultMap="DTOResultMap">
@ -61,6 +61,6 @@
from forest_article art from forest_article art
join forest_article_content content on art.id = content.id_article join forest_article_content content on art.id = content.id_article
where article_status = 0 where article_status = 0
and id = #{id}; and id = #{id}
</select> </select>
</mapper> </mapper>

View File

@ -39,6 +39,6 @@
<select id="getById" resultMap="BaseResultMap"> <select id="getById" resultMap="BaseResultMap">
SELECT id, portfolio_title, portfolio_description SELECT id, portfolio_title, portfolio_description
FROM forest_portfolio FROM forest_portfolio
where id = #{id}; where id = #{id}
</select> </select>
</mapper> </mapper>

View File

@ -37,6 +37,6 @@
<select id="getById" resultMap="BaseResultMap"> <select id="getById" resultMap="BaseResultMap">
SELECT id, nickname, signature SELECT id, nickname, signature
FROM forest_user FROM forest_user
where id = #{id}; where id = #{id}
</select> </select>
</mapper> </mapper>