OfficialArticleService.java 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. package com.webchat.pgc.service;
  2. import com.webchat.common.bean.APIPageResponseBean;
  3. import com.webchat.common.constants.WebConstant;
  4. import com.webchat.common.enums.ArticleStatusEnum;
  5. import com.webchat.common.enums.RedisKeyEnum;
  6. import com.webchat.common.enums.messagequeue.MessageQueueEnum;
  7. import com.webchat.common.service.RedisService;
  8. import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
  9. import com.webchat.common.util.JsonUtil;
  10. import com.webchat.domain.dto.queue.ArticleDelayMessageDTO;
  11. import com.webchat.domain.vo.request.publicaccount.SaveArticleRequestVO;
  12. import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
  13. import com.webchat.domain.vo.response.mess.PublicAccountArticleMessageVO;
  14. import com.webchat.domain.vo.response.publicaccount.ArticleBaseResponseVO;
  15. import com.webchat.pgc.repository.dao.IArticleDAO;
  16. import com.webchat.pgc.repository.entity.ArticleEntity;
  17. import org.apache.commons.lang3.ObjectUtils;
  18. import org.apache.commons.lang3.StringUtils;
  19. import org.springframework.beans.BeanUtils;
  20. import org.springframework.beans.factory.annotation.Autowired;
  21. import org.springframework.data.domain.Page;
  22. import org.springframework.data.domain.PageRequest;
  23. import org.springframework.data.domain.Pageable;
  24. import org.springframework.data.domain.Sort;
  25. import org.springframework.stereotype.Service;
  26. import org.springframework.util.Assert;
  27. import org.springframework.util.CollectionUtils;
  28. import java.util.ArrayList;
  29. import java.util.Collections;
  30. import java.util.Date;
  31. import java.util.HashMap;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.Set;
  35. import java.util.stream.Collectors;
  36. /**
  37. * 公众号文章服务
  38. *
  39. */
  40. @Service
  41. public class OfficialArticleService {
  42. @Autowired
  43. private IArticleDAO articleDAO;
  44. @Autowired
  45. private AccountService accountService;
  46. @Autowired
  47. private RedisService redisService;
  48. @Autowired
  49. private MessageQueueProducer<ArticleDelayMessageDTO, Long> messageQueueProducer;
  50. /**
  51. * 公众号推文
  52. *
  53. * @param saveArticleRequest
  54. * @return
  55. */
  56. public Long submit(SaveArticleRequestVO saveArticleRequest) {
  57. /**
  58. * 1. 持久化文化到数据库
  59. */
  60. ArticleEntity articleEntity = this.convert(saveArticleRequest);
  61. articleEntity = articleDAO.save(articleEntity);
  62. /**
  63. * 2. 缓存文章详情到redis
  64. */
  65. this.refreshArticleRedisDetailCache(articleEntity);
  66. /**
  67. * 3. 推文:MQ ---> 延迟队列 + 普通列表
  68. * 《实时推文》:用户未指定推送时间(默认当前时间)
  69. * 《延迟推送》:用户指定未来时间推送
  70. */
  71. final String publicAccount = saveArticleRequest.getPublicAccount();
  72. final Long articleId = articleEntity.getId();
  73. final Long pushTime = saveArticleRequest.getPlanPushTime();
  74. this.doSubmitDelayQueue(publicAccount, articleId, pushTime);
  75. /**
  76. * 4. 公众号文章消息数据持久化
  77. */
  78. // TODO
  79. return articleId;
  80. }
  81. /**
  82. * 公众号文章推送计划加入延迟队列
  83. *
  84. * @param publicAccount 公众号账号
  85. * @param articleId 公众号文章
  86. * @param pushTime 设定的推送时间
  87. */
  88. private void doSubmitDelayQueue(String publicAccount, Long articleId, Long pushTime) {
  89. ArticleDelayMessageDTO message = new ArticleDelayMessageDTO();
  90. message.setArticleId(articleId);
  91. message.setPublicAccount(publicAccount);
  92. // 延迟推文时间(如果未设置发布时间,则立即发布)
  93. pushTime = pushTime == null ? System.currentTimeMillis() : pushTime;
  94. message.setTime(pushTime);
  95. // 提交队列:优先级队列(一级队列)
  96. messageQueueProducer.prioritySend(MessageQueueEnum.QUEUE_OFFICIAL_ARTICLE_PUSH_MESSAGE, message, pushTime);
  97. }
  98. /**
  99. * 刷新公众号文章redis缓存
  100. *
  101. * @param articleId
  102. * @return
  103. */
  104. private ArticleBaseResponseVO refreshArticleRedisDetailCache(Long articleId) {
  105. if (articleId == null) {
  106. return null;
  107. }
  108. ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null);
  109. return this.refreshArticleRedisDetailCache(articleEntity);
  110. }
  111. /**
  112. * 刷新公众号文章redis缓存
  113. *
  114. * @param articleEntity
  115. * @return
  116. */
  117. private ArticleBaseResponseVO refreshArticleRedisDetailCache(ArticleEntity articleEntity) {
  118. if (articleEntity == null) {
  119. return null;
  120. }
  121. String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleEntity.getId()));
  122. // 文章详情我们使用string类型来缓存,每个文章有自己的失效时间,避免缓存雪崩
  123. ArticleBaseResponseVO articleBaseResponseVO = this.convert(articleEntity);
  124. redisService.set(cacheKey, JsonUtil.toJsonString(articleBaseResponseVO), RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime());
  125. return articleBaseResponseVO;
  126. }
  127. /**
  128. * 缓存空值,防止缓存击穿
  129. *
  130. * @param articleId
  131. */
  132. private void refreshArticleNoneCache(Long articleId) {
  133. String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
  134. // 文章详情我们使用string类型来缓存,每个文章有自己的失效时间,避免缓存雪崩
  135. redisService.set(cacheKey, WebConstant.CACHE_NONE, RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime());
  136. }
  137. /**
  138. * 浏览文章
  139. *
  140. * @param articleId
  141. * @return
  142. */
  143. public ArticleBaseResponseVO viewArticle(Long articleId) {
  144. ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId);
  145. if (article == null) {
  146. return null;
  147. }
  148. // 设置公众号信息
  149. article.setPublicAccountInfo(accountService.accountInfo(article.getPublicAccount()));
  150. return article;
  151. }
  152. /**
  153. * 查询消息列表所有文章信息
  154. *
  155. * @param articleId
  156. * @return
  157. */
  158. public PublicAccountArticleMessageVO getPublicAccountArticleMessage(Long articleId) {
  159. ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId);
  160. if (article == null) {
  161. return null;
  162. }
  163. PublicAccountArticleMessageVO publicAccountArticleMessage = new PublicAccountArticleMessageVO();
  164. publicAccountArticleMessage.setTitle(article.getTitle());
  165. publicAccountArticleMessage.setDescription(article.getDescription());
  166. publicAccountArticleMessage.setCover(article.getCover());
  167. publicAccountArticleMessage.setArticleId(articleId);
  168. publicAccountArticleMessage.setRedirectUrl(article.getRedirectUrl());
  169. return publicAccountArticleMessage;
  170. }
  171. public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId, boolean needContent) {
  172. ArticleBaseResponseVO articleVO = this.getArticleDetailFromCache(articleId);
  173. if (articleVO == null) {
  174. return null;
  175. }
  176. if (!needContent) {
  177. articleVO.setContent(null);
  178. }
  179. return articleVO;
  180. }
  181. /**
  182. * 查询公众号文章详情
  183. *
  184. * @param articleId
  185. * @return
  186. */
  187. public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId) {
  188. String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId));
  189. String cache = redisService.get(cacheKey);
  190. // 这里可能存在击穿问题(比如:有人恶意那不存在的文章一致查询)
  191. // 文章缓存击穿解决办法:我们缓存一个空值
  192. if (StringUtils.isNotBlank(cache)) {
  193. if (WebConstant.CACHE_NONE.equals(cache)) {
  194. // 文章不存在,直接返回null,不需要在查库
  195. return null;
  196. }
  197. return JsonUtil.fromJson(cache, ArticleBaseResponseVO.class);
  198. }
  199. // 缓存不存在主动查询数据库,重新刷新缓存
  200. ArticleBaseResponseVO articleBase = this.refreshArticleRedisDetailCache(articleId);
  201. if (articleBase == null) {
  202. // 数据库文章不存在,这里缓存空值,防止redis击穿
  203. this.refreshArticleNoneCache(articleId);
  204. }
  205. return articleBase;
  206. }
  207. /**
  208. * 批量查询redis,获取文章详情缓存
  209. *
  210. * 场景:公众号详情页,一次可能查询10篇文章
  211. * @param articleIdList
  212. * @return
  213. */
  214. public Map<Long, ArticleBaseResponseVO> batchGetArticleDetailFromCache(List<Long> articleIdList) {
  215. if (CollectionUtils.isEmpty(articleIdList)) {
  216. return Collections.emptyMap();
  217. }
  218. Map<Long, ArticleBaseResponseVO> batchGetResult = new HashMap<>();
  219. // 构造批量查询redis的缓存key
  220. List<String> cacheKeys = articleIdList.stream().map(
  221. id -> RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(id)))
  222. .collect(Collectors.toList());
  223. // 批量查询redis
  224. List<String> caches = redisService.mget(cacheKeys);
  225. for (int i = 0; i < articleIdList.size(); i++) {
  226. Long articleId = articleIdList.get(i);
  227. String cache = caches.get(i);
  228. ArticleBaseResponseVO articleBaseResponseVO;
  229. if (StringUtils.isNotBlank(cache)) {
  230. articleBaseResponseVO = JsonUtil.fromJson(cache, ArticleBaseResponseVO.class);
  231. } else {
  232. articleBaseResponseVO = this.refreshArticleRedisDetailCache(articleId);
  233. }
  234. batchGetResult.put(articleId, articleBaseResponseVO);
  235. }
  236. return batchGetResult;
  237. }
  238. /**
  239. * 分页查询文章列表
  240. *
  241. * @param pageNo
  242. * @param pageSize
  243. * @return
  244. */
  245. public APIPageResponseBean<List<ArticleBaseResponseVO>> page(Integer pageNo, Integer pageSize) {
  246. Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id"));
  247. Page<ArticleEntity> userEntities = articleDAO.findAll(pageable);
  248. List<ArticleBaseResponseVO> articles = new ArrayList<>();
  249. if (userEntities != null && !CollectionUtils.isEmpty(userEntities.getContent())) {
  250. // 走缓存批量查询公众号信息
  251. Set<String> publicAccounts = userEntities.stream().map(ArticleEntity::getPublicAccount).collect(Collectors.toSet());
  252. Map<String, UserBaseResponseInfoVO> accounts = accountService.batchGet(publicAccounts);
  253. // 批量构造返回文章列表参数
  254. articles = userEntities.getContent().stream().map(a -> {
  255. ArticleBaseResponseVO article = convert(a);
  256. // 列表一般不需要返回详情信息,减少网络数据包传输设置为null
  257. article.setContent(null);
  258. // 这里偷懒了,建议
  259. article.setPublicAccountInfo(accounts.get(article.getPublicAccount()));
  260. return article;
  261. }).collect(Collectors.toList());
  262. }
  263. return APIPageResponseBean.success(pageNo, pageSize, userEntities.getTotalElements(), articles);
  264. }
  265. private ArticleBaseResponseVO convert(ArticleEntity articleEntity) {
  266. ArticleBaseResponseVO articleBase = new ArticleBaseResponseVO();
  267. BeanUtils.copyProperties(articleEntity, articleBase);
  268. if (articleEntity.getPlanPushDate() != null) {
  269. articleBase.setPlanPushTime(articleEntity.getPlanPushDate().getTime());
  270. }
  271. if (articleEntity.getCreateDate() != null) {
  272. articleBase.setPublishTime(articleEntity.getCreateDate().getTime());
  273. }
  274. return articleBase;
  275. }
  276. private ArticleEntity convert(SaveArticleRequestVO saveArticleRequest) {
  277. Long articleId = saveArticleRequest.getId();
  278. String author = saveArticleRequest.getAuthor();
  279. Date now = new Date();
  280. ArticleEntity articleEntity;
  281. if (articleId != null) {
  282. articleEntity = articleDAO.findById(articleId).orElse(null);
  283. Assert.notNull(articleEntity, "文章更新失败: 文章不存在!");
  284. Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有更新权限!");
  285. } else {
  286. articleEntity = new ArticleEntity();
  287. articleEntity.setCreateDate(now);
  288. articleEntity.setCreateBy(author);
  289. articleEntity.setAuthor(author);
  290. articleEntity.setStatus(ArticleStatusEnum.WAIT_PUSH.getStatus());
  291. }
  292. articleEntity.setRedirectUrl(saveArticleRequest.getRedirectUrl());
  293. articleEntity.setStatus(articleEntity.getStatus());
  294. articleEntity.setCover(saveArticleRequest.getCover());
  295. articleEntity.setDescription(saveArticleRequest.getDescription());
  296. articleEntity.setTitle(saveArticleRequest.getTitle());
  297. articleEntity.setContent(saveArticleRequest.getContent());
  298. articleEntity.setPublicAccount(saveArticleRequest.getPublicAccount());
  299. articleEntity.setSigns(saveArticleRequest.getSigns());
  300. Date planPushDate = new Date();
  301. if (saveArticleRequest.getPlanPushTime() != null) {
  302. planPushDate = new Date(saveArticleRequest.getPlanPushTime());
  303. }
  304. articleEntity.setPlanPushDate(planPushDate);
  305. return articleEntity;
  306. }
  307. }