package com.webchat.pgc.service; import com.webchat.common.bean.APIPageResponseBean; import com.webchat.common.constants.WebConstant; import com.webchat.common.enums.ArticleStatusEnum; import com.webchat.common.enums.RedisKeyEnum; import com.webchat.common.enums.messagequeue.MessageQueueEnum; import com.webchat.common.service.RedisService; import com.webchat.common.service.messagequeue.producer.MessageQueueProducer; import com.webchat.common.util.JsonUtil; import com.webchat.domain.dto.queue.ArticleDelayMessageDTO; import com.webchat.domain.vo.request.publicaccount.SaveArticleRequestVO; import com.webchat.domain.vo.response.UserBaseResponseInfoVO; import com.webchat.domain.vo.response.mess.PublicAccountArticleMessageVO; import com.webchat.domain.vo.response.publicaccount.ArticleBaseResponseVO; import com.webchat.pgc.repository.dao.IArticleDAO; import com.webchat.pgc.repository.entity.ArticleEntity; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * 公众号文章服务 * */ @Service public class OfficialArticleService { @Autowired private IArticleDAO articleDAO; @Autowired private AccountService accountService; @Autowired private RedisService redisService; @Autowired private MessageQueueProducer messageQueueProducer; /** * 公众号推文 * * @param saveArticleRequest * @return */ public Long submit(SaveArticleRequestVO saveArticleRequest) { /** * 1. 持久化文化到数据库 */ ArticleEntity articleEntity = this.convert(saveArticleRequest); articleEntity = articleDAO.save(articleEntity); /** * 2. 缓存文章详情到redis */ this.refreshArticleRedisDetailCache(articleEntity); /** * 3. 推文:MQ ---> 延迟队列 + 普通列表 * 《实时推文》:用户未指定推送时间(默认当前时间) * 《延迟推送》:用户指定未来时间推送 */ final String publicAccount = saveArticleRequest.getPublicAccount(); final Long articleId = articleEntity.getId(); final Long pushTime = saveArticleRequest.getPlanPushTime(); this.doSubmitDelayQueue(publicAccount, articleId, pushTime); /** * 4. 公众号文章消息数据持久化 */ // TODO return articleId; } /** * 公众号文章推送计划加入延迟队列 * * @param publicAccount 公众号账号 * @param articleId 公众号文章 * @param pushTime 设定的推送时间 */ private void doSubmitDelayQueue(String publicAccount, Long articleId, Long pushTime) { ArticleDelayMessageDTO message = new ArticleDelayMessageDTO(); message.setArticleId(articleId); message.setPublicAccount(publicAccount); // 延迟推文时间(如果未设置发布时间,则立即发布) pushTime = pushTime == null ? System.currentTimeMillis() : pushTime; message.setTime(pushTime); // 提交队列:优先级队列(一级队列) messageQueueProducer.prioritySend(MessageQueueEnum.QUEUE_OFFICIAL_ARTICLE_PUSH_MESSAGE, message, pushTime); } /** * 刷新公众号文章redis缓存 * * @param articleId * @return */ private ArticleBaseResponseVO refreshArticleRedisDetailCache(Long articleId) { if (articleId == null) { return null; } ArticleEntity articleEntity = articleDAO.findById(articleId).orElse(null); return this.refreshArticleRedisDetailCache(articleEntity); } /** * 刷新公众号文章redis缓存 * * @param articleEntity * @return */ private ArticleBaseResponseVO refreshArticleRedisDetailCache(ArticleEntity articleEntity) { if (articleEntity == null) { return null; } String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleEntity.getId())); // 文章详情我们使用string类型来缓存,每个文章有自己的失效时间,避免缓存雪崩 ArticleBaseResponseVO articleBaseResponseVO = this.convert(articleEntity); redisService.set(cacheKey, JsonUtil.toJsonString(articleBaseResponseVO), RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime()); return articleBaseResponseVO; } /** * 缓存空值,防止缓存击穿 * * @param articleId */ private void refreshArticleNoneCache(Long articleId) { String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId)); // 文章详情我们使用string类型来缓存,每个文章有自己的失效时间,避免缓存雪崩 redisService.set(cacheKey, WebConstant.CACHE_NONE, RedisKeyEnum.ARTICLE_DETAIL_CACHE.getExpireTime()); } /** * 浏览文章 * * @param articleId * @return */ public ArticleBaseResponseVO viewArticle(Long articleId) { ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId); if (article == null) { return null; } // 设置公众号信息 article.setPublicAccountInfo(accountService.accountInfo(article.getPublicAccount())); return article; } /** * 查询消息列表所有文章信息 * * @param articleId * @return */ public PublicAccountArticleMessageVO getPublicAccountArticleMessage(Long articleId) { ArticleBaseResponseVO article = this.getArticleDetailFromCache(articleId); if (article == null) { return null; } PublicAccountArticleMessageVO publicAccountArticleMessage = new PublicAccountArticleMessageVO(); publicAccountArticleMessage.setTitle(article.getTitle()); publicAccountArticleMessage.setDescription(article.getDescription()); publicAccountArticleMessage.setCover(article.getCover()); publicAccountArticleMessage.setArticleId(articleId); publicAccountArticleMessage.setRedirectUrl(article.getRedirectUrl()); return publicAccountArticleMessage; } public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId, boolean needContent) { ArticleBaseResponseVO articleVO = this.getArticleDetailFromCache(articleId); if (articleVO == null) { return null; } if (!needContent) { articleVO.setContent(null); } return articleVO; } /** * 查询公众号文章详情 * * @param articleId * @return */ public ArticleBaseResponseVO getArticleDetailFromCache(Long articleId) { String cacheKey = RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(articleId)); String cache = redisService.get(cacheKey); // 这里可能存在击穿问题(比如:有人恶意那不存在的文章一致查询) // 文章缓存击穿解决办法:我们缓存一个空值 if (StringUtils.isNotBlank(cache)) { if (WebConstant.CACHE_NONE.equals(cache)) { // 文章不存在,直接返回null,不需要在查库 return null; } return JsonUtil.fromJson(cache, ArticleBaseResponseVO.class); } // 缓存不存在主动查询数据库,重新刷新缓存 ArticleBaseResponseVO articleBase = this.refreshArticleRedisDetailCache(articleId); if (articleBase == null) { // 数据库文章不存在,这里缓存空值,防止redis击穿 this.refreshArticleNoneCache(articleId); } return articleBase; } /** * 批量查询redis,获取文章详情缓存 * * 场景:公众号详情页,一次可能查询10篇文章 * @param articleIdList * @return */ public Map batchGetArticleDetailFromCache(List articleIdList) { if (CollectionUtils.isEmpty(articleIdList)) { return Collections.emptyMap(); } Map batchGetResult = new HashMap<>(); // 构造批量查询redis的缓存key List cacheKeys = articleIdList.stream().map( id -> RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(id))) .collect(Collectors.toList()); // 批量查询redis List caches = redisService.mget(cacheKeys); for (int i = 0; i < articleIdList.size(); i++) { Long articleId = articleIdList.get(i); String cache = caches.get(i); ArticleBaseResponseVO articleBaseResponseVO; if (StringUtils.isNotBlank(cache)) { articleBaseResponseVO = JsonUtil.fromJson(cache, ArticleBaseResponseVO.class); } else { articleBaseResponseVO = this.refreshArticleRedisDetailCache(articleId); } batchGetResult.put(articleId, articleBaseResponseVO); } return batchGetResult; } /** * 分页查询文章列表 * * @param pageNo * @param pageSize * @return */ public APIPageResponseBean> page(Integer pageNo, Integer pageSize) { Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id")); Page userEntities = articleDAO.findAll(pageable); List articles = new ArrayList<>(); if (userEntities != null && !CollectionUtils.isEmpty(userEntities.getContent())) { // 走缓存批量查询公众号信息 Set publicAccounts = userEntities.stream().map(ArticleEntity::getPublicAccount).collect(Collectors.toSet()); Map accounts = accountService.batchGet(publicAccounts); // 批量构造返回文章列表参数 articles = userEntities.getContent().stream().map(a -> { ArticleBaseResponseVO article = convert(a); // 列表一般不需要返回详情信息,减少网络数据包传输设置为null article.setContent(null); // 这里偷懒了,建议 article.setPublicAccountInfo(accounts.get(article.getPublicAccount())); return article; }).collect(Collectors.toList()); } return APIPageResponseBean.success(pageNo, pageSize, userEntities.getTotalElements(), articles); } private ArticleBaseResponseVO convert(ArticleEntity articleEntity) { ArticleBaseResponseVO articleBase = new ArticleBaseResponseVO(); BeanUtils.copyProperties(articleEntity, articleBase); if (articleEntity.getPlanPushDate() != null) { articleBase.setPlanPushTime(articleEntity.getPlanPushDate().getTime()); } if (articleEntity.getCreateDate() != null) { articleBase.setPublishTime(articleEntity.getCreateDate().getTime()); } return articleBase; } private ArticleEntity convert(SaveArticleRequestVO saveArticleRequest) { Long articleId = saveArticleRequest.getId(); String author = saveArticleRequest.getAuthor(); Date now = new Date(); ArticleEntity articleEntity; if (articleId != null) { articleEntity = articleDAO.findById(articleId).orElse(null); Assert.notNull(articleEntity, "文章更新失败: 文章不存在!"); Assert.isTrue(ObjectUtils.equals(articleEntity.getAuthor(), author), "没有更新权限!"); } else { articleEntity = new ArticleEntity(); articleEntity.setCreateDate(now); articleEntity.setCreateBy(author); articleEntity.setAuthor(author); articleEntity.setStatus(ArticleStatusEnum.WAIT_PUSH.getStatus()); } articleEntity.setRedirectUrl(saveArticleRequest.getRedirectUrl()); articleEntity.setStatus(articleEntity.getStatus()); articleEntity.setCover(saveArticleRequest.getCover()); articleEntity.setDescription(saveArticleRequest.getDescription()); articleEntity.setTitle(saveArticleRequest.getTitle()); articleEntity.setContent(saveArticleRequest.getContent()); articleEntity.setPublicAccount(saveArticleRequest.getPublicAccount()); articleEntity.setSigns(saveArticleRequest.getSigns()); Date planPushDate = new Date(); if (saveArticleRequest.getPlanPushTime() != null) { planPushDate = new Date(saveArticleRequest.getPlanPushTime()); } articleEntity.setPlanPushDate(planPushDate); return articleEntity; } }