123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 |
- 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<ArticleDelayMessageDTO, Long> 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<Long, ArticleBaseResponseVO> batchGetArticleDetailFromCache(List<Long> articleIdList) {
- if (CollectionUtils.isEmpty(articleIdList)) {
- return Collections.emptyMap();
- }
- Map<Long, ArticleBaseResponseVO> batchGetResult = new HashMap<>();
- // 构造批量查询redis的缓存key
- List<String> cacheKeys = articleIdList.stream().map(
- id -> RedisKeyEnum.ARTICLE_DETAIL_CACHE.getKey(String.valueOf(id)))
- .collect(Collectors.toList());
- // 批量查询redis
- List<String> 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<List<ArticleBaseResponseVO>> page(Integer pageNo, Integer pageSize) {
- Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "id"));
- Page<ArticleEntity> userEntities = articleDAO.findAll(pageable);
- List<ArticleBaseResponseVO> articles = new ArrayList<>();
- if (userEntities != null && !CollectionUtils.isEmpty(userEntities.getContent())) {
- // 走缓存批量查询公众号信息
- Set<String> publicAccounts = userEntities.stream().map(ArticleEntity::getPublicAccount).collect(Collectors.toSet());
- Map<String, UserBaseResponseInfoVO> 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;
- }
- }
|