package com.tanpu.community.manager; import com.alibaba.fastjson.JSON; import com.fasterxml.jackson.core.type.TypeReference; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.tanpu.biz.common.enums.RelTypeEnum; import com.tanpu.biz.common.enums.community.CollectionTypeEnum; import com.tanpu.biz.common.enums.community.ReportTypeEnum; import com.tanpu.common.api.CommonResp; import com.tanpu.common.constant.ErrorCodeConstant; import com.tanpu.common.enums.fund.ProductTypeEnum; import com.tanpu.common.exception.BizException; import com.tanpu.common.util.JsonUtil; import com.tanpu.community.api.CommunityConstant; import com.tanpu.community.api.beans.qo.ESThemeQo; import com.tanpu.community.api.beans.qo.FormerThemeQo; import com.tanpu.community.api.beans.qo.ThemeContentQo; import com.tanpu.community.api.beans.qo.ThemeQo; import com.tanpu.community.api.beans.req.homepage.QueryRecordThemeReq; import com.tanpu.community.api.beans.req.theme.CollectThemeReq; import com.tanpu.community.api.beans.req.theme.CreateThemeReq; import com.tanpu.community.api.beans.req.theme.ForwardThemeReq; import com.tanpu.community.api.beans.req.theme.LikeThemeReq; import com.tanpu.community.api.beans.req.theme.ReportThemeReq; import com.tanpu.community.api.beans.req.theme.SynchroThemeReq; import com.tanpu.community.api.beans.req.theme.ThemeContentReq; import com.tanpu.community.api.beans.req.theme.ThemeListReq; import com.tanpu.community.api.beans.resp.CreateThemeResp; import com.tanpu.community.api.beans.resp.ThemeFullSearchResp; import com.tanpu.community.api.beans.resp.ThemeListResp; import com.tanpu.community.api.beans.vo.ImagesDTO; import com.tanpu.community.api.beans.vo.feign.fatools.UserInfoResp; import com.tanpu.community.api.beans.vo.feign.newsfeed.NewsFeedResReq; import com.tanpu.community.api.beans.vo.feign.newsfeed.NewsFeedSave4NewCommReq; import com.tanpu.community.api.constants.BizConstant; import com.tanpu.community.api.enums.BlockTypeEnum; import com.tanpu.community.api.enums.DeleteTagEnum; import com.tanpu.community.api.enums.NotificationTypeEnum; import com.tanpu.community.api.enums.OperationTypeEnum; import com.tanpu.community.api.enums.ThemeListTypeEnum; import com.tanpu.community.api.enums.ThemeTypeEnum; import com.tanpu.community.cache.RedisCache; import com.tanpu.community.dao.entity.community.BlackListEntity; import com.tanpu.community.dao.entity.community.CollectionEntity; import com.tanpu.community.dao.entity.community.CommentEntity; import com.tanpu.community.dao.entity.community.ThemeAttachmentEntity; import com.tanpu.community.dao.entity.community.ThemeEntity; import com.tanpu.community.feign.community.FeignClientForCommunity; import com.tanpu.community.feign.fatools.FeignClientForFatools; import com.tanpu.community.service.*; import com.tanpu.community.service.base.ESService; import com.tanpu.community.service.quartz.TopicReportService; import com.tanpu.community.util.BizUtils; import com.tanpu.community.util.ConvertUtil; import com.tanpu.community.util.OtherUtil; import com.tanpu.community.util.RankUtils; import com.tanpu.community.util.TencentcloudUtils; import com.tanpu.community.util.TimeUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.File; import java.io.IOException; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static com.tanpu.community.api.constants.RedisKeyConstant.*; @Slf4j @Service public class ThemeManager { @Value("${tmpfile.dir:/data/tmp/}") private String tmpDir; @Resource private ThemeService themeService; @Resource private CollectionService collectionService; @Resource private CommentService commentService; @Resource private FollowRelService followRelService; @Resource private BlackListService blackListService; @Resource private ThemeAttachmentService themeAttachmentService; @Resource private BatchFeignCallService batchFeignCallService; @Resource private VisitLogService visitLogService; @Resource private ReportLogService reportLogService; @Resource private RankService rankService; @Resource private ESService esService; @Resource private FeignClientForFatools feignClientForFatools; @Resource private RedisCache redisCache; @Resource private RecommendService recommendService; @Resource private RestTemplate restTemplate; @Resource private NotificationService notificationService; @Resource private ThemeTextCheckService themeTextCheckService; @Resource private TopicService topicService; @Autowired private TopicReportService topicReportService; @PostConstruct public void init() throws IOException { File f = new File(tmpDir); log.info("create directory {}", tmpDir); if (!f.exists()) { FileUtils.forceMkdir(f); } } // 专栏 @Autowired private FeignClientForCommunity feignClientForCommunity; public ThemeFullSearchResp themeFullSearch(String keyword, Integer pageNo, Integer pageSize, String ident, String userId) { List<String> excludeIds; if (pageNo > 1) { String l = redisCache.get("themeFullSearch_" + ident); excludeIds = StringUtils.isBlank(l) ? new ArrayList<>() : JsonUtil.toBean(l, new TypeReference<List<String>>() { }); } else { excludeIds = new ArrayList<>(); } Integer from = (pageNo - 1) * pageSize; ThemeFullSearchResp resp = new ThemeFullSearchResp(); String[] keywords = StringUtils.split(keyword, " "); // 按时间倒叙查询 List<ESThemeQo> esIds = esService.queryThemeIdByContentAndTitle(keywords, from, pageSize * 5); if (esIds.isEmpty()) { return resp; } // 排除已经展示过的id List<String> filterEsIds = esIds.stream().map(ESThemeQo::getThemeId).filter(tId -> { return !excludeIds.contains(tId); }).limit(pageSize).collect(Collectors.toList()); resp.themes = convertEntityToQo(themeService.queryByThemeIds(filterEsIds), userId); resp.themes.sort(new Comparator<ThemeQo>() { @Override public int compare(ThemeQo o1, ThemeQo o2) { return o2.createTime.compareTo(o1.createTime); } }); // 截取关键词出现的那一部分段落 for (ThemeQo theme : resp.themes) { theme.briefContent4FullSearch = BizUtils.getThemeContent(keywords, theme); } excludeIds.addAll(resp.themes.stream().map(ThemeQo::getThemeId).collect(Collectors.toList())); redisCache.put("themeFullSearch_" + ident, excludeIds, 60 * 60 * 6); return resp; } /** * 发表主题(修改) */ @Transactional public CommonResp<CreateThemeResp> publishTheme(CreateThemeReq req, String userId) { // 校验参数 checkAttachment(req.getContent()); // 文本查重,编辑不查 if (StringUtils.isBlank(req.getEditThemeId()) && themeTextCheckService.checkDuplicate(ConvertUtil.convertThemeText(JsonUtil.toJson(req.getContent())))) { return CommonResp.error(ErrorCodeConstant.THEME_TEXT_DUPLICATE.getCode(), ErrorCodeConstant.THEME_TEXT_DUPLICATE.getMsg()); } // 保存主题表 ThemeEntity themeEntity = new ThemeEntity(); BeanUtils.copyProperties(req, themeEntity); themeEntity.setAuthorId(userId); // 腾讯云敏感词校验 checkContent(req); themeEntity.setContent(JsonUtil.toJson(req.getContent())); if (StringUtils.isBlank(req.getEditThemeId())) { // 新建 themeService.insertTheme(themeEntity); } else { // 修改 themeService.update(themeEntity, req.getEditThemeId()); themeEntity.setThemeId(req.getEditThemeId()); this.evictThemeCache(req.getEditThemeId()); } // 保存附件表 List<ThemeAttachmentEntity> themeAttachments = ConvertUtil.themeReqToAttachmentList(req, themeEntity.getThemeId()); if (StringUtils.isNotEmpty(req.getEditThemeId())) { // 修改需要刪除 themeAttachmentService.deleteByThemeId(req.getEditThemeId()); } themeAttachmentService.insertList(themeAttachments); ESThemeQo esThemeQo = ConvertUtil.convert(themeEntity); try { esService.insertOrUpdateTheme(esThemeQo); } catch (Exception e) { log.error("error in save theme to ES. themeId:{}, error:{}", themeEntity.getThemeId(), ExceptionUtils.getStackTrace(e)); } themeTextCheckService.insert(esThemeQo.getTextContent(), themeEntity.getThemeId(), userId, themeEntity.getThemeType(), req.getEditThemeId()); redisCache.evict(StringUtils.joinWith("_", CACHE_THEME_ID, themeEntity.getThemeId())); CreateThemeResp themeResp = CreateThemeResp.builder().themeId(themeEntity.getThemeId()).build(); // 同步到专栏 if (1 == req.getSyncToNewComm()) { CommonResp response = synchronizeToNewsFeed(req, themeEntity.getThemeId(), userId); if (response.isNotSuccess()) { if ("8001".equals(response.getCode()) || ErrorCodeConstant.THEME_SYNCHRONIZE_FAILED.getCode().equals(response.getCode())) { // 内容受限,不回滚发布 return CommonResp.error(ErrorCodeConstant.THEME_SYNCHRONIZE_FAILED.getCode(), "发布成功,同步失败:" + response.getMsg(), themeResp); } else { // 其他回滚异常 throw new BizException(ErrorCodeConstant.THEME_PUBLISH_FAILED.getCode() , "调用专栏同步异常:" + response.getMsg()); } } } if (StringUtils.isNotBlank(req.topicId)) { topicReportService.reportTopicOnTime(req.topicId); } return CommonResp.success(themeResp); } private CommonResp synchronizeToNewsFeed(CreateThemeReq req, String themeId, String userId) { if (!ThemeTypeEnum.DISCUSSION.getCode().equals(req.getThemeType())) { // 只有讨论类型才能同步专栏 throw new BizException("长文类型无法同步专栏"); } NewsFeedSave4NewCommReq newsFeedReq = new NewsFeedSave4NewCommReq(); newsFeedReq.setNewsFeedId(themeId); newsFeedReq.setUserId(userId); ArrayList<NewsFeedResReq> feedList = new ArrayList<>(); for (ThemeContentReq themeContentReq : req.getContent()) { // 文字内容添加到content if (RelTypeEnum.TEXT.type.equals(themeContentReq.getType())) { newsFeedReq.setContent(themeContentReq.getValue()); } else if (RelTypeEnum.MULTIPLE_IMAGE.type.equals(themeContentReq.getType())) { List<ImagesDTO> imgList = themeContentReq.getImgList(); imgList.forEach(img -> { feedList.add(convertImg(img, userId)); }); } else if (RelTypeEnum.OFFLINE_ACTIVITY.type.equals(themeContentReq.getType())) { // throw new BizException("线下活动暂时无法同步到专栏"); return CommonResp.error(ErrorCodeConstant.THEME_SYNCHRONIZE_FAILED.getCode(), "线下活动无法同步"); } else { //其他类型的附件 feedList.add(NewsFeedResReq.builder().relType(Integer.parseInt(themeContentReq.getType())) .relId(themeContentReq.getValue()) .productType(themeContentReq.getProductType()) .remark(themeContentReq.getRemark()) .build()); } } newsFeedReq.setNewsFeedResList(feedList); return feignClientForCommunity.saveNewsFeed4NewComm(newsFeedReq); } /** * 转存图片到老接口 * * @param img * @param userId * @return */ private NewsFeedResReq convertImg(ImagesDTO img, String userId) { String imgUrl = img.getRemark(); String[] arr = StringUtils.split(imgUrl, "."); String suffix = arr[arr.length - 1]; String fileName = tmpDir + imgUrl.substring(imgUrl.lastIndexOf('/') + 1); ResponseEntity<byte[]> resp = restTemplate.getForEntity(img.getRemark(), byte[].class); byte[] rst = resp.getBody(); File f = new File(fileName); try { FileUtils.writeByteArrayToFile(f, rst); // 调用对方接口 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.set("uid", userId); MultiValueMap<String, Object> params = new LinkedMultiValueMap<String, Object>(); HashMap<String, Object> item = new HashMap<>(); item.put("filetype", suffix); item.put("refid", img.getRelId()); item.put("mode", 0); item.put("userId", userId); FileSystemResource br = new FileSystemResource(f); params.add("file", br); params.add("item", item); HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<MultiValueMap<String, Object>>(params, headers); // tp-community-svc ResponseEntity<String> response = restTemplate.exchange(CommunityConstant.OLD_FILE_UPLOAD_URL, HttpMethod.POST, requestEntity, String.class); log.info("new-community uploadThemePic returns {}", JSON.toJSONString(response)); if (StringUtils.isBlank(response.getBody())) { throw new RuntimeException("response body is blank"); } CommonResp<LinkedHashMap<String, String>> responseBody = JsonUtil.toBean(response.getBody(), CommonResp.class); if (!responseBody.isSuccess()) { throw new RuntimeException("reponse is not success"); } HashMap<String, String> data = responseBody.getData(); return NewsFeedResReq.builder().relId(data.get("fileId")) .relType(Integer.parseInt(RelTypeEnum.IMAGE_FILE.type)) .remark(data.get("fileurl")).build(); } catch (Exception e) { log.error("error in handleSyncImg for imgUrl: {}", img.getRemark(), e); throw new RuntimeException(e); } finally { try { FileUtils.forceDelete(f); } catch (Exception e) { // do nothing } } } /** * 参数校验 * * @param themeAttachments */ private void checkAttachment(List<ThemeContentReq> themeAttachments) { if (CollectionUtils.isEmpty(themeAttachments)) { throw new BizException(ErrorCodeConstant.ILLEGAL_ARGEMENT.getCode(), "正文内容不能为空"); } for (ThemeContentReq content : themeAttachments) { if (content.getType() == null) { throw new BizException(ErrorCodeConstant.ILLEGAL_ARGEMENT.getCode(), "主题内容ThemeContentReq缺少类型"); } Set<String> types = Arrays.stream(RelTypeEnum.values()).map(o -> o.type).collect(Collectors.toSet()); if (!types.contains(content.getType())) { throw new BizException(ErrorCodeConstant.ILLEGAL_ARGEMENT.getCode(), "主题内容ThemeContentReq类型错误"); } if (content.getType().equals(RelTypeEnum.FUND.type)) { if (content.getProductType() == null) { throw new BizException(ErrorCodeConstant.ILLEGAL_ARGEMENT.getCode(), "附件产品FUND缺少类型"); } if (content.getProductType() == ProductTypeEnum.CUSTOMER_IMPORT.type) { throw new BizException(ErrorCodeConstant.LIMIT_CONTENT.getCode(), "圈子暂不支持私有基金"); } if (content.getProductType() == ProductTypeEnum.NOT_NET_PRODUCT.type) { throw new BizException(ErrorCodeConstant.LIMIT_CONTENT.getCode(), "圈子暂不支持无净值私有基金"); } } } } // 转发主题 public CreateThemeResp forward(ForwardThemeReq req, String userId) { // 校验 themeService.checkForwardSpecialPermission(req.getFormerThemeId()); ThemeEntity themeEntity = ThemeEntity.builder() .content(JsonUtil.toJson(req.getContent())) .topicId(req.getTopicId()) .formerThemeId(req.getFormerThemeId()) .authorId(userId) .themeType(ThemeTypeEnum.FORWARD.getCode()) .build(); if (StringUtils.isBlank(req.getEditThemeId()) || req.getEditThemeId().equals(req.getFormerThemeId())) { // 新建 themeService.insertTheme(themeEntity); // 消息通知 ThemeEntity formerTheme = themeService.queryByThemeId(req.getFormerThemeId()); if (formerTheme != null) { notificationService.insertForward(userId, formerTheme.getAuthorId(), req.getFormerThemeId(), req.getTopicId(), req.getContent().get(0).getValue(), themeEntity.getThemeId()); notificationService.putNotifyCache(formerTheme.getAuthorId(), userId, NotificationTypeEnum.FORWARD); } } else { // 修改 themeService.update(themeEntity, req.getEditThemeId()); themeEntity.setThemeId(req.getEditThemeId()); this.evictThemeCache(req.getEditThemeId()); } // 转发同步评论并消息通知 if (req.getSyncComment() == BizConstant.SyncCommentType.SYNC_COMMENT) { String commentId = commentService.forwardSyncComment(req, userId); ThemeEntity formerTheme = themeService.queryByThemeId(req.getFormerThemeId()); if (formerTheme != null) { notificationService.insert(userId, formerTheme.getAuthorId(), NotificationTypeEnum.COMMENT, commentId, req.getContent().get(0).getValue()); notificationService.putNotifyCache(formerTheme.getAuthorId(), userId, NotificationTypeEnum.COMMENT); } } try { esService.insertOrUpdateTheme(ConvertUtil.convert(themeEntity)); } catch (Exception e) { log.error("error in save theme to ES. themeId:{}, error:{}", themeEntity.getThemeId(), ExceptionUtils.getStackTrace(e)); } //失效缓存 redisCache.evict(StringUtils.joinWith("_", THEME_FORWARD_COUNT, themeEntity.getThemeId())); return CreateThemeResp.builder().themeId(themeEntity.getThemeId()).build(); } /** * 查询主题列表:推荐/关注/热门/最新 */ public ThemeListResp queryList(ThemeListReq req, String userId) { List<String> excludeIds = new ArrayList<>(); LocalDateTime firstThemeTime = LocalDateTime.now(); if (req.page.pageNumber > 1) { String l = redisCache.get("queryThemes_" + req.ident); if (!StringUtils.isBlank(l)) { excludeIds = JsonUtil.toBean(l, new TypeReference<List<String>>() { }); firstThemeTime = themeService.queryByThemeIdIgnoreDelete(excludeIds.get(0)).getCreateTime(); } } Integer pageStart = (req.page.pageNumber - 1) * req.page.pageSize; Integer pageSize = req.page.pageSize; List<ThemeEntity> themes = new ArrayList<>(); // 推荐:由最新,最热和python推荐三个部分组成,默认比例为6,3,1,可在配置文件中配置 if (ThemeListTypeEnum.RECOMMEND.getCode().equals(req.getType())) { // 需要筛掉用户访问过详情的 & 最近出现在列表页过的. List<String> visitedIds = StringUtils.isEmpty(req.getUserId()) ? Lists.newArrayListWithCapacity(0) : visitLogService.queryUserRecentVisited(req.getUserId()); List<String> excludes = ListUtils.union(excludeIds, visitedIds); // 计算推荐列表 List<String> recmdIds = recommendService.getRecommendThemes(pageStart, pageSize, req.getUserId(), excludes, firstThemeTime); // 加载第一页时,为防止首页显示空列表,从推荐池中再捞出已看过帖子 if (req.page.pageNumber <= 3 && recmdIds.size() < pageSize) { List<String> reSearchIds = ListUtils.union(excludeIds, recmdIds); recmdIds.addAll(recommendService.getRecommendThemes(pageStart, pageSize, req.getUserId(), reSearchIds, firstThemeTime)); } themes = themeService.queryByThemeIds(recmdIds); // 权限控制,筛选出当前用户有权限的话题 Set<String> userPermitTopics = topicService.getUserPermitTopics(userId); // 排序并去重 themes = RankUtils.sortThemeEntityByIds(themes, recmdIds, userPermitTopics).stream().limit(pageSize).collect(Collectors.toList()); } else if (ThemeListTypeEnum.FOLLOW.getCode().equals(req.getType())) { if (StringUtils.isEmpty(userId)) { // 未登录情况下返回空数组 themes = Lists.newArrayListWithCapacity(0); } else { // 根据关注列表查询,按时间倒序 List<String> fansList = followRelService.queryIdolsByFansId(req.getUserId()); fansList.add(userId); // 保证fansList不为空 // 权限控制,筛选出当前用户关注的话题 Set<String> userFollowTopics = topicService.getUserFollowTopics(userId); // 查库 themes = themeService.queryByUserIdsCreateDesc(fansList, pageStart, pageSize, userFollowTopics); if (CollectionUtils.isEmpty(excludeIds) && !themes.isEmpty()) { // 说明是从头开始刷,则直接把最新的lastId放到redis中,保留一个月 Long lastId = themes.stream().map(ThemeEntity::getId).max(Long::compareTo).get(); redisCache.put(CACHE_IDOL_THEME_LAST_ID + req.getUserId(), lastId, 60 * 60 * 24 * 7 * 4); // visitLogService.addPageView(userId, userId, VisitTypeEnum.FOLLOW_THEME_VIEW); } } } else if (ThemeListTypeEnum.TOPIC_HOT.getCode().equals(req.getType())) { // 根据话题查询热门 if (StringUtils.isEmpty(req.getTopicId())) { throw new BizException("TopicId为空"); } // 话题下的置顶 List<ThemeEntity> topThemes = themeService.queryTopByTopic(req.getTopicId()); excludeIds.addAll(topThemes.stream().map(ThemeEntity::getThemeId).collect(Collectors.toList())); List<String> rankThemeIds = rankService.getRankThemeListByTopic(req.getTopicId(), excludeIds); rankThemeIds = BizUtils.subList(rankThemeIds, pageStart, pageSize); themes = themeService.queryByThemeIds(rankThemeIds); themes = RankUtils.sortThemeEntityByIds(themes, rankThemeIds); // 置顶 if (pageStart == 0) { topThemes.addAll(themes); themes = topThemes; } } else if (ThemeListTypeEnum.TOPIC_LATEST.getCode().equals(req.getType())) { //根据话题查询最新 if (StringUtils.isEmpty(req.getTopicId())) { throw new BizException("TopicId为空"); } themes = themeService.queryNewestByTopic(req.topicId, pageStart, pageSize, excludeIds); } ThemeListResp resp = new ThemeListResp(); resp.themes = convertEntityToQo(themes, req.getUserId()); // 最新3条评论 // todo 测试性能 commentService.queryRecentComments(resp.themes); // 讨论区添加是否管理员 if (ThemeListTypeEnum.TOPIC_LATEST.getCode().equals(req.getType()) || ThemeListTypeEnum.TOPIC_HOT.getCode().equals(req.getType())) { topicService.checkManager(req.getTopicId(), resp.themes); } // 非讨论区热门擦除顶置状态 if (!ThemeListTypeEnum.TOPIC_HOT.getCode().equals(req.getType())) { resp.themes.stream().forEach(ThemeQo::evictTop); } // 保存缓存、记录已浏览 excludeIds.addAll(resp.themes.stream().map(ThemeQo::getThemeId).collect(Collectors.toList())); redisCache.put("queryThemes_" + req.ident, excludeIds, 60 * 60 * 6); //组装详情 return resp; } // 主题Entity转QO,组装所有信息 private List<ThemeQo> convertEntityToQo(List<ThemeEntity> themeEntities, String userId) { //Entity转Qo List<ThemeQo> themeQos = ConvertUtil.themeEntitiesToDTOs(themeEntities); // 批量查询附件detail batchFeignCallService.getAttachDetailByBatch(themeQos); // 转赞评 batchBuildThemeCountInfo(themeQos); // 转发对象 for (ThemeQo themeQO : themeQos) { buildThemeForwardObj(themeQO); } // 和用户相关信息 if (StringUtils.isNotEmpty(userId)) { buildThemeExtraInfoByUser(userId, themeQos); } return themeQos; } // 转发对象 private void buildThemeForwardObj(ThemeQo themeQo) { String themeId = themeQo.getThemeId(); // 封装转发对象 FormerThemeQo former = redisCache.getObject(StringUtils.joinWith("_", CACHE_FORWARD_THEME_ID, themeQo.getFormerThemeId()), 60, () -> this.getFormerTheme(themeQo.getFormerThemeId()), FormerThemeQo.class); themeQo.setFormerTheme(former); } // 单个查询 点赞、收藏、转发数 private void buildThemeCountInfo(ThemeQo themeQo) { String themeId = themeQo.getThemeId(); // 点赞,收藏,转发 Integer likeCount = redisCache.getObject(StringUtils.joinWith("_", THEME_LIKE_COUNT, themeId), 60 * 60 * 24, () -> collectionService.getCountByTypeAndId(themeId, CollectionTypeEnum.LIKE_THEME), Integer.class); Integer commentCount = redisCache.getObject(StringUtils.joinWith("_", THEME_COMMENT_COUNT, themeId), 60 * 60 * 24, () -> commentService.getCommentCountByThemeId(themeId), Integer.class); Integer forwardCount = redisCache.getObject(StringUtils.joinWith("_", THEME_FORWARD_COUNT, themeId), 60 * 60 * 24, () -> themeService.getForwardCountById(themeId), Integer.class); themeQo.setCommentCount(commentCount); themeQo.setLikeCount(likeCount); themeQo.setForwardCount(forwardCount); } // 批量-点赞、收藏、转发数 private void batchBuildThemeCountInfo(List<ThemeQo> themeQos) { List<String> themeIds = themeQos.stream().map(ThemeQo::getThemeId).collect(Collectors.toList()); // 点赞,收藏,转发 Map<String, Integer> likeCountMap = collectionService.getCountMapByType(themeIds, CollectionTypeEnum.LIKE_THEME); Map<String, Integer> commentCountMap = commentService.getCountMapByThemeIds(themeIds); Map<String, Integer> forwardCountMap = themeService.getForwardCountMap(themeIds); themeQos.stream().forEach(o -> { o.setCommentCount(commentCountMap.getOrDefault(o.getThemeId(), 0)); o.setLikeCount(likeCountMap.getOrDefault(o.getThemeId(), 0)); o.setForwardCount(forwardCountMap.getOrDefault(o.getThemeId(), 0)); }); } // 组装和当前用户相关信息(单个查询) private void buildThemeExtraInfoByUser(String userId, ThemeQo themeQo) { String themeId = themeQo.getThemeId(); // 是否关注作者 themeQo.setFollow(followRelService.checkFollow(themeQo.getAuthorId(), userId)); // 是否点赞 CollectionEntity likeEntity = collectionService.queryCollection(themeId, userId, CollectionTypeEnum.LIKE_THEME); themeQo.setHasLiked(likeEntity != null); // 是否转发 themeQo.setHasForward(themeService.judgeForwardByUser(themeId, userId)); // 是否收藏 CollectionEntity collectionEntity = collectionService.queryCollection(themeId, userId, CollectionTypeEnum.COLLECT_THEME); themeQo.setHasCollect(collectionEntity != null); } // 组装和当前用户相关信息(批量查询) private void buildThemeExtraInfoByUser(String userId, List<ThemeQo> themeQos) { // 批量查询 List<String> themeIds = themeQos.stream().map(ThemeQo::getThemeId).collect(Collectors.toList()); Set<String> idolSet = new HashSet<>(followRelService.queryIdolsByFansId(userId)); Set<String> likeSet = collectionService.getTargets(themeIds, userId, CollectionTypeEnum.LIKE_THEME); Set<String> bookSet = collectionService.getTargets(themeIds, userId, CollectionTypeEnum.COLLECT_THEME); Set<String> forwardUsers = themeService.getForwardUsers(themeIds); // 从set中查找 for (ThemeQo themeQo : themeQos) { themeQo.setFollow(idolSet.contains(themeQo.getAuthorId())); themeQo.setHasLiked(likeSet.contains(themeQo.getThemeId())); themeQo.setHasCollect(bookSet.contains(themeQo.getThemeId())); themeQo.setHasForward(forwardUsers.contains(userId)); } } /** * 返回用户发布、回复、点赞、收藏的主题列表 * * @param req 查询用户 * @param userId 当前用户 * @return */ public List<ThemeQo> queryThemesByUser(QueryRecordThemeReq req, String userId) { List<ThemeEntity> themeEntities = Collections.emptyList(); // 权限控制,筛选出当前用户有权限的话题 Set<String> userPermitTopics = topicService.getUserPermitTopics(userId); switch (req.getRecordType()) { case 1://发布 themeEntities = themeService.queryThemesByUserIdCreateDesc(req.getUserId(), req.getLastId(), req.getPageSize(), userPermitTopics); break; case 2://回复 List<ThemeQo> commentThemeList = getCommentThemeQos(req, userId); return commentThemeList; case 3://点赞 List<String> likeThemeIds = collectionService.getListByUser(req.getUserId(), CollectionTypeEnum.LIKE_THEME, req.getLastId(), req.getPageSize()); themeEntities = themeService.queryByThemeIds(likeThemeIds, req.getLastId(), req.getPageSize(), userPermitTopics); themeEntities = RankUtils.sortThemeEntityByIds(themeEntities, likeThemeIds); break; case 4://收藏 List<String> collectThemeIds = collectionService.getListByUser(req.getUserId(), CollectionTypeEnum.COLLECT_THEME, req.getLastId(), req.getPageSize()); themeEntities = themeService.queryByThemeIds(collectThemeIds, req.getLastId(), req.getPageSize(), userPermitTopics); themeEntities = RankUtils.sortThemeEntityByIds(themeEntities, collectThemeIds); break; } List<ThemeQo> themeQos = convertEntityToQo(themeEntities, userId); if (StringUtils.equals(userId, req.getUserId())) { //如果用户是查询自己的帖子,需要实时查询用户自己的个人信息,防止数据不一致(非收藏类型) CommonResp<UserInfoResp> userInfoNewCommonResp = feignClientForFatools.queryUserInfoNew(userId); if (userInfoNewCommonResp.isNotSuccess()) { throw new BizException("内部接口调用失败"); } UserInfoResp user = userInfoNewCommonResp.getData(); themeQos.forEach(o -> { if (o.getAuthorId().equals(userId)) { reBuildAuthorInfo(o, user); } }); redisCache.put(StringUtils.joinWith("_", CACHE_FEIGN_USER_INFO, userId), user, 60); } commentService.queryRecentComments(themeQos); return themeQos; } // 查询正文 public CommonResp<ThemeQo> getThemeDetail(String themeId, String userId) { ThemeEntity themeEntity = themeService.queryByThemeIdIgnoreDelete(themeId); if (themeEntity == null) { throw new BizException("找不到帖子id:" + themeId); } // 校验主题权限 if (StringUtils.isNotBlank(themeEntity.getTopicId()) && !topicService.checkPermission(themeEntity.getTopicId(), userId)) { return CommonResp.error(ErrorCodeConstant.TOPIC_PERMISSION_ABORT.getCode(), topicService.getPermissionToast(themeEntity.getTopicId())); } if (themeEntity.getDeleteTag().equals(DeleteTagEnum.DELETED.getCode())) { return CommonResp.error(ErrorCodeConstant.UNREACHABLE, null); } ThemeQo themeQo = ConvertUtil.themeEntityToQo(themeEntity); //附件 batchFeignCallService.getAttachDetail(themeQo); //转发、收藏、点赞 buildThemeForwardObj(themeQo); buildThemeCountInfo(themeQo); // 添加用户相关信息 if (StringUtils.isNotEmpty(userId)) { buildThemeExtraInfoByUser(userId, themeQo); } // 是否管理员 topicService.checkManager(themeQo.getTopicId(), themeQo); return CommonResp.success(themeQo); } // 点赞/取消点赞 public void like(LikeThemeReq req, String userId) { if (OperationTypeEnum.CONFIRM.getCode().equals(req.getType())) { if (collectionService.saveOrUpdate(req.getThemeId(), userId, CollectionTypeEnum.LIKE_THEME)) { ThemeEntity themeEntity = themeService.queryByThemeId(req.getThemeId()); // 消息通知 notificationService.insertLike(userId, themeEntity.getAuthorId(), req.getThemeId()); notificationService.putNotifyCache(themeEntity.getAuthorId(), userId, NotificationTypeEnum.LIKE); } } else if (OperationTypeEnum.CANCEL.getCode().equals(req.getType())) { collectionService.delete(req.getThemeId(), userId, CollectionTypeEnum.LIKE_THEME); } //失效缓存 redisCache.evict(StringUtils.joinWith("_", THEME_LIKE_COUNT, req.getThemeId())); } //收藏/取消收藏 public void collect(CollectThemeReq req, String userId) { if (OperationTypeEnum.CONFIRM.getCode().equals(req.getType())) { collectionService.saveOrUpdate(req.getThemeId(), userId, CollectionTypeEnum.COLLECT_THEME); } else if (OperationTypeEnum.CANCEL.getCode().equals(req.getType())) { collectionService.delete(req.getThemeId(), userId, CollectionTypeEnum.COLLECT_THEME); } } //举报主题 @Transactional public void report(ReportThemeReq req, String userId) { //更改举报状态 themeService.updateReportStatus(req.getThemeId()); //写入举报记录表 ThemeEntity themeEntity = themeService.queryByThemeId(req.getThemeId()); reportLogService.insert(ReportTypeEnum.THEME, userId, req.getThemeId(), themeEntity.getAuthorId(), req.getReason()); } //关注用户是否有更新 public Integer getFollowUpdateCount(String userId) { String lastIdStr = redisCache.get(CACHE_IDOL_THEME_LAST_ID + userId); Long lastId = StringUtils.isBlank(lastIdStr) ? 0L : Long.parseLong(lastIdStr); List<String> fansList = followRelService.queryIdolsByFansId(userId); return themeService.queryCountFromLastId(fansList, lastId); } // 屏蔽(用户) public void blockUser(String blockUser, String userId) { BlackListEntity selectOne = blackListService.selectOne(blockUser, userId, BlockTypeEnum.USER.getCode()); if (selectOne == null) { blackListService.addBlock(blockUser, userId, BlockTypeEnum.USER); } } //返回被转发主题 private FormerThemeQo getFormerTheme(String formerThemeId) { if (StringUtils.isNotBlank(formerThemeId)) { ThemeQo formerTheme = ConvertUtil.themeEntityToQo(themeService.queryByThemeId(formerThemeId)); if (formerTheme != null) { batchFeignCallService.getAttachDetail(formerTheme); FormerThemeQo f = ConvertUtil.themeQo2FormerThemeQo(formerTheme); return f; } } return null; } // 逻辑删除主题,校验用户 public void delete(String themeId, String userId) { themeService.deleteById(themeId, userId); themeTextCheckService.deleteByThemeId(themeId); this.evictThemeCache(themeId); } // 从专栏同步 public void convertFromNewsFeed(SynchroThemeReq req2) { String userId = req2.getUserId(); CreateThemeReq req = new CreateThemeReq(); BeanUtils.copyProperties(req2, req); req.setTopicId(""); req.setTitle(""); // 校验参数 checkAttachment(req.getContent()); // 权限校验 // liveRelayCheck(userId, req.getContent()); // 保存主题表 ThemeEntity themeEntity = new ThemeEntity(); BeanUtils.copyProperties(req, themeEntity); themeEntity.setAuthorId(userId); themeEntity.setContent(JsonUtil.toJson(req.getContent())); themeEntity.setThemeId(CommunityConstant.THEME_PREFIX + req2.getThemeId()); themeService.insertTheme(themeEntity); // 保存附件表 List<ThemeAttachmentEntity> themeAttachments = ConvertUtil.themeReqToAttachmentList(req, themeEntity.getThemeId()); themeAttachmentService.insertList(themeAttachments); try { esService.insertOrUpdateTheme(ConvertUtil.convert(themeEntity)); } catch (Exception e) { log.error("error in save theme to ES. themeId:{}, error:{}", themeEntity.getThemeId(), ExceptionUtils.getStackTrace(e)); } } /** * 腾讯云-内容检测 * * @param req */ private void checkContent(CreateThemeReq req) { if (ThemeTypeEnum.LONG_TEXT.getCode().equals(req.getThemeType()) && req.getTitle().length() > 50) { throw new IllegalArgumentException("长文标题不能超过50字"); } StringBuilder sb = new StringBuilder(); for (ThemeContentReq themeContentReq : req.getContent()) { if (RelTypeEnum.TEXT.type.equals(themeContentReq.getType())) { sb.append(themeContentReq.getValue()); } } String content = sb.toString(); // 腾讯云接口最多支持5000文字校验 // 检查内容是否涉黄违法 String b = ""; while (content.length() > 5000) { b = TencentcloudUtils.textModeration(content.substring(0, 5000)); if (StringUtils.isNotBlank(b)) { throw new BizException(ErrorCodeConstant.CONTENT_ILLEGAL.getCode(), "疑似违规词汇:" + b); } content = content.substring(5000); } b = TencentcloudUtils.textModeration(content); if (StringUtils.isNotBlank(b)) { throw new BizException(ErrorCodeConstant.CONTENT_ILLEGAL.getCode(), "疑似违规词汇:" + b); } } /** * 直播类型做转播检查 * * @param userId * @param contents */ private void liveRelayCheck(String userId, List<ThemeContentReq> contents) { for (ThemeContentReq content : contents) { if (content != null && content.getType().equals(RelTypeEnum.LIVE.type)) { CommonResp<Set<String>> notRelayResp = feignClientForFatools.getNotRelaySet(userId, Sets.newHashSet(content.getValue())); if (!notRelayResp.isSuccess()) { throw new BizException("转播失败"); } if (CollectionUtils.isNotEmpty(notRelayResp.getData())) { throw new BizException("9999", "很抱歉!您需要购买或报名成功后才可以添加这个直播哦~"); } } } } /** * 查询用户评论过的主题,并封装成转发主题结构 * * @param req * @param userId 当前用户 * @return */ private List<ThemeQo> getCommentThemeQos(QueryRecordThemeReq req, String userId) { List<ThemeQo> commentThemeList = new ArrayList<>(); List<ThemeEntity> themeEntities; // 评论列表 List<CommentEntity> commentEntities = commentService.queryCommentsByUserId(req.getUserId(), req.getLastId(), req.getPageSize()); // 当前用户信息 UserInfoResp userInfo = redisCache.getObject(StringUtils.joinWith("_", CACHE_FEIGN_USER_INFO, req.getUserId()), 60, () -> this.getUserInfo(req.getUserId()), UserInfoResp.class); Set<String> replyThemeIds = commentEntities.stream().map(CommentEntity::getThemeId).collect(Collectors.toSet()); if (CollectionUtils.isEmpty(replyThemeIds)) { return commentThemeList; } themeEntities = themeService.queryByThemeIds(new ArrayList<>(replyThemeIds)); List<ThemeQo> themeQos = convertEntityToQo(themeEntities, userId); // 组装附件 batchFeignCallService.getAttachDetailByBatch(themeQos); // 主题列表 Map<String, ThemeQo> themeMap = themeQos.stream() .collect(Collectors.toMap(ThemeQo::getThemeId, o -> o)); // 主题+评论封装转发对象 for (CommentEntity commentEntity : commentEntities) { String themeId = commentEntity.getThemeId(); //评论内容包装到ThemeContentQo里 ThemeContentQo commentContent = ThemeContentQo.builder() .type(RelTypeEnum.TEXT.type) .value(OtherUtil.blockPhoneAndEmail(commentEntity.getContent())) .build(); ThemeQo commentThemeQo = ThemeQo.builder() .authorId(userInfo.getUserId()) .nickName(userInfo.getNickName()) .userImg(userInfo.getHeadImageUrl()) .userType(userInfo.getUserType()) .levelGrade(userInfo.getLevelGrade()) .userInvestorType(userInfo.getUserInvestorType()) .belongUserOrgId(userInfo.getBelongUserOrgId()) .belongUserOrgName(userInfo.getBelongUserOrgName()) .content(Collections.singletonList(commentContent)) .commentId(commentEntity.getCommentId()) .themeType(ThemeTypeEnum.RES_COMMENT.getCode()) // 回复列表不需要关注按钮 // .follow(followRelService.checkFollow(userInfo.getUserId(), userId)) .upToNowTime(TimeUtils.calUpToNowTime(commentEntity.getCreateTime())) .build(); //原主题包装到formerThemeQo中 ThemeQo themeQo = themeMap.get(themeId); if (themeQo != null) { FormerThemeQo f = ConvertUtil.themeQo2FormerThemeQo(themeQo); commentThemeQo.setFormerTheme(f); } commentThemeList.add(commentThemeQo); } return commentThemeList; } private UserInfoResp getUserInfo(String authorId) { CommonResp<UserInfoResp> userInfoNewCommonResp = feignClientForFatools.queryUserInfoNew(authorId); if (userInfoNewCommonResp.isNotSuccess()) { throw new BizException("内部接口调用失败"); } return userInfoNewCommonResp.getData(); } private void reBuildAuthorInfo(ThemeQo themeQo, UserInfoResp userInfo) { themeQo.setNickName(userInfo.getNickName()); themeQo.setUserImg(userInfo.getHeadImageUrl()); themeQo.setUserIntroduction(userInfo.getIntroduction()); //认证标签相关 themeQo.setUserType(userInfo.getUserType()); themeQo.setLevelGrade(userInfo.getLevelGrade()); themeQo.setUserInvestorType(userInfo.getUserInvestorType()); themeQo.setBelongUserOrgId(userInfo.getBelongUserOrgId()); themeQo.setBelongUserOrgName(userInfo.getBelongUserOrgName()); //工作室相关 themeQo.setWorkshopName(userInfo.getWorkshopName()); themeQo.setWorkshopStatus(userInfo.getWorkshopStatus()); themeQo.setWorkshopIntroduction(userInfo.getWorkshopIntroduction()); } private void evictThemeCache(String themeId) { redisCache.evict(StringUtils.joinWith("_", CACHE_FORWARD_THEME_ID, themeId)); redisCache.evict(StringUtils.joinWith("_", CACHE_THEME_ID, themeId)); } /** * 查重初始化 */ @Transactional public void initThemeTextCheck() { List<ThemeEntity> themeEntities = themeService.queryLatestThemes(30); List<ThemeQo> themeQos = ConvertUtil.themeEntitiesToDTOs(themeEntities); for (ThemeQo themeQo : themeQos) { List<ThemeContentQo> content = themeQo.getContent(); for (ThemeContentQo themeContentQo : content) { if (themeContentQo.getType().equals(RelTypeEnum.TEXT.type)) { if (StringUtils.isNotBlank(themeContentQo.getValue()) && themeContentQo.getValue().length() > 50) themeTextCheckService.insertInit(themeContentQo.getValue(), themeQo.getThemeId(), themeQo.getAuthorId(), TimeUtils.getDateTimeOfTimestamp(themeQo.getCreateTime()), themeQo.getThemeType()); } } } } }