Selaa lähdekoodia

webchat-act服务抽象、实现+新增评论服务

wangqi49 3 päivää sitten
vanhempi
commit
e68ee32666

+ 83 - 0
webchat-act/src/main/java/com/webchat/act/controller/CommentController.java

@@ -0,0 +1,83 @@
+package com.webchat.act.controller;
+
+import com.webchat.act.service.CommentService;
+import com.webchat.common.bean.APIPageResponseBean;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.domain.vo.request.CommentSaveVO;
+import com.webchat.domain.vo.response.CommentResponseVO;
+import com.webchat.rmi.act.ICommentClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+
+@RestController
+public class CommentController implements ICommentClient {
+
+
+    @Autowired
+    private CommentService commentService;
+
+    /**
+     * 保存评论
+     * @param vo
+     * @return
+     */
+    @PostMapping("/addComment")
+    public APIResponseBean<Long> saveComment(@RequestBody CommentSaveVO vo) {
+        String authorId = SessionHelper.getCurrentUserId();
+        vo.setAuthorId(authorId);
+        Long commentId = commentService.saveComment(vo);
+        return APIResponseBeanUtil.success(commentId);
+    }
+
+    /**
+     * 删除评论
+     * @return
+     */
+    public APIResponseBean deleteComment(@PathVariable Long id) {
+        commentService.deleteComment(id, SessionHelper.getCurrentUserId());
+        return APIResponseBeanUtil.success();
+    }
+
+    public APIResponseBean<Long> topComment(@PathVariable Long commentId) {
+        commentService.topComment(commentId);
+        return APIResponseBeanUtil.success("OK");
+    }
+
+    public APIResponseBean<Long> cancelTopComment(@PathVariable Long commentId) {
+        commentService.cancelTopComment(commentId);
+        return APIResponseBeanUtil.success("OK");
+    }
+
+    /***
+     * 查询资源评论列表
+     * @param resourceId
+     * @param resourceType
+     * @param isHot
+     * @param isNew
+     * @param pageNo
+     * @param pageSize
+     * @return
+     */
+    public APIPageResponseBean<List<CommentResponseVO>> queryComments(@RequestParam(value = "resourceId", required = false) Long resourceId,
+                                                                      @RequestParam(value = "resourceType", required = false) String resourceType,
+                                                                      @RequestParam(value = "content", required = false) String content,
+                                                                      @RequestParam(value = "isHot", required = false, defaultValue = "false") Boolean isHot,
+                                                                      @RequestParam(value = "isNew", required = false, defaultValue = "false") Boolean isNew,
+                                                                      @RequestParam(value = "pageNo", required = false, defaultValue = "1") Integer pageNo,
+                                                                      @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
+
+        String currentUserId = SessionHelper.getCurrentUserId();
+        return commentService.queryCommentByCondition(currentUserId, content, resourceId, resourceType, pageNo, pageSize, isHot, isNew);
+    }
+
+}

+ 35 - 0
webchat-act/src/main/java/com/webchat/act/repository/dao/ICommentDAO.java

@@ -4,10 +4,45 @@ import com.webchat.act.repository.entity.CommentEntity;
 import com.webchat.act.repository.entity.ResourceBehaviorEntity;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
+
 @Repository
 public interface ICommentDAO extends JpaSpecificationExecutor<CommentEntity>,
                                      JpaRepository<CommentEntity, Long> {
 
+
+    CommentEntity findAllById(Long id);
+
+
+    @Query(value = "select * from coder_util_comment c where c.parent_id = ?1 and c.status = ?2 order by id asc"
+            + " limit ?3", nativeQuery = true)
+    List<CommentEntity> findAllByParentIdAndStatus(Long parentId, String status, Integer size);
+
+    /**
+     * 查询资源评论数量,这里指主楼数量
+     * @param resourceId
+     * @param resourceType
+     * @param status
+     * @return
+     */
+    Long countByResourceIdAndResourceTypeAndStatusAndParentIdIsNull(Long resourceId, String resourceType, String status);
+
+
+    /**
+     * 查询外显评论
+     *
+     * @param resourceType
+     * @param resourceId
+     * @param status
+     * @return
+     */
+    @Query(value = "select * from web_chat_comment c " +
+            "where c.resource_type = ?1 and c.resource_id = ?2 and c.status = ?3 " +
+            "order by id desc " +
+            "limit ?4",
+            nativeQuery = true)
+    List<CommentEntity> getOutSideCommentListLimit(String resourceType, Long resourceId, String status, Integer limit);
 }

+ 113 - 0
webchat-act/src/main/java/com/webchat/act/service/AccountService.java

@@ -0,0 +1,113 @@
+package com.webchat.act.service;
+
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.rmi.user.UserServiceClient;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.Set;
+
+@Slf4j
+@Service
+public class AccountService {
+
+    @Autowired
+    private UserServiceClient userServiceClient;
+
+    /**
+     * 批量查询账号详情
+     *
+     * @param userIds
+     * @return
+     */
+    public Map<String, UserBaseResponseInfoVO> batchGet(Set<String> userIds) {
+        APIResponseBean<Map<String, UserBaseResponseInfoVO>> responseBean = userServiceClient.batchGet(userIds);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        // TODO 建议做都低策略(两级缓存 local cache + redis)
+        return null;
+    }
+
+
+    /**
+     * 获取账号详情,底层走Redis查询
+     * @param account
+     * @return
+     */
+    public UserBaseResponseInfoVO accountInfo(String account) {
+        if (StringUtils.isBlank(account)) {
+            return null;
+        }
+        APIResponseBean<UserBaseResponseInfoVO> responseBean = userServiceClient.userInfo(account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        log.error("RPC Invoke Error ====> UserServiceClient.userInfo error, account:{}, responseBean:{}",
+                   account, JsonUtil.toJsonString(responseBean));
+        return null;
+    }
+
+    /**
+     * 查询所有群成员
+     * @param account
+     * @return
+     */
+    public Set<String> getGroupUserIds(String account) {
+
+        return getAllSubscriberByAccount(account, AccountRelationTypeEnum.USER_GROUP);
+    }
+
+    /**
+     * 查询公众号所有订阅用户
+     *
+     * @param account
+     * @return
+     */
+    public Set<String> getOfficialUserIds(String account) {
+
+        return getAllSubscriberByAccount(account, AccountRelationTypeEnum.USER_OFFICIAL);
+    }
+
+    /**
+     * 获取群组下的群成员用户id集合
+     *
+     * @return
+     */
+    public Set<String> getAllSubscriberByAccount(String account, AccountRelationTypeEnum accountRelationType) {
+        /**
+         * TODO 建议: 对数据做降级
+         */
+        APIResponseBean<Set<String>> responseBean =
+                userServiceClient.getAllSubscriberByAccount(accountRelationType.getType(), account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        return null;
+    }
+
+    /**
+     * 判断userAccount是否订阅account
+     * @param userAccount
+     * @param account
+     * @param accountRelationType
+     * @return
+     */
+    public boolean isSubscribe(String userAccount, String account, AccountRelationTypeEnum accountRelationType) {
+        APIResponseBean<Boolean> responseBean =
+                userServiceClient.isSubscribe(accountRelationType.getType(), userAccount, account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return ObjectUtils.equals(responseBean.getData(), true);
+        }
+        return false;
+    }
+}

+ 427 - 0
webchat-act/src/main/java/com/webchat/act/service/CommentService.java

@@ -1,11 +1,438 @@
 package com.webchat.act.service;
 
 
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.webchat.act.repository.dao.ICommentDAO;
+import com.webchat.act.repository.entity.CommentEntity;
+import com.webchat.common.bean.APIPageResponseBean;
+import com.webchat.common.constants.ResourceBehaviorConstants;
+import com.webchat.common.enums.CommonStatusEnum;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.helper.SessionHelper;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.util.DateUtils;
+import com.webchat.common.util.IPAddressUtil;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.common.util.ThreadPoolExecutorUtil;
+import com.webchat.domain.vo.request.CommentSaveVO;
+import com.webchat.domain.vo.response.CommentOutResponseVO;
+import com.webchat.domain.vo.response.CommentResponseVO;
+import com.webchat.domain.vo.response.IpLocationResponseVO;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Predicate;
+import jakarta.persistence.criteria.Root;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+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.data.jpa.domain.Specification;
 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.stream.Collectors;
+
+
+@Slf4j
 @Service
 public class CommentService {
 
 
+    @Autowired
+    private ICommentDAO commentDAO;
+    @Autowired
+    private AccountService userService;
+    @Autowired
+    private RedisService redisService;
+
+    private static final int DEFAULT_REPLY_COMMENT_SIZE = 20;
+
+    /**
+     * 默认外显评论条数
+     */
+    private static final int DEFAULT_OUT_COMMENT_SIZE = 3;
+
+    /***
+     * 发布评论
+     */
+    public Long saveComment(CommentSaveVO commentSaveVO) {
+        Long replyId = commentSaveVO.getReplyId();
+        Long parentId = null;
+        if (replyId != null) {
+            CommentEntity replyComment = commentDAO.findAllById(replyId);
+            Assert.isTrue(replyComment != null, "回复评论不存在!");
+            if(replyComment.getParentId() != null) {
+                parentId = replyComment.getParentId();
+            } else {
+                parentId = replyComment.getId();
+            }
+        }
+        CommentEntity commentEntity = new CommentEntity();
+        commentEntity.setResourceId(commentSaveVO.getResourceId());
+        commentEntity.setResourceType(commentSaveVO.getResourceType());
+        commentEntity.setAuthor(commentSaveVO.getAuthorId());
+        String commentContent = commentSaveVO.getContent();
+        commentEntity.setContent(commentContent);
+        commentEntity.setImages(commentSaveVO.getImages());
+        commentEntity.setPubDate(new Date());
+        commentEntity.setReplyId(replyId);
+        commentEntity.setParentId(parentId);
+        commentEntity.setIsTop(false);
+        commentEntity.setLikeCount(0L);
+        commentEntity.setStatus(CommonStatusEnum.PUBLISHED.getStatus());
+
+        // 获取客户端IP
+        String ip = SessionHelper.getCurrentClientIP();
+        String ipAddress = null;
+        // 成功获取客户端IP,解析IP归属地
+        if (StringUtils.isNotBlank(ip)) {
+            IpLocationResponseVO ipAddressVO = IPAddressUtil.location(ip);
+            if (ipAddressVO != null && StringUtils.isNotBlank(ipAddressVO.getProvince())) {
+                // IP归属地取IP地址位置具体到省份
+                ipAddress = StringUtils.isBlank(ipAddressVO.getProvince()) ? "未知" : ipAddressVO.getProvince();
+            }
+        }
+        commentEntity.setIp(ip);
+        commentEntity.setIpAddress(ipAddress);
+        Long commentId = commentDAO.save(commentEntity).getId();
+        if (replyId == null) {
+            // 刷新评论数量
+            addCommentCount2Cache(commentSaveVO.getResourceType(), commentSaveVO.getResourceId());
+            // TODO 发送评论消息
+        } else {
+            // 发送评论回复消息
+        }
+        // 刷新外显评论缓存
+        ThreadPoolExecutorUtil.execute(() ->
+                refreshOutCommentCache(commentSaveVO.getResourceType(), commentSaveVO.getResourceId()));
+        return commentId;
+    }
+
+    public void deleteComment(Long commentId, String userId) {
+        CommentEntity commentEntity = commentDAO.findAllById(commentId);
+        commentEntity.setStatus(CommonStatusEnum.DELETED.getStatus());
+        commentEntity.setUpdateBy(userId);
+        commentDAO.save(commentEntity);
+        if (commentEntity.getReplyId() == null) {
+            // 刷新评论数量
+            reduceCommentCount2Cache(commentEntity.getResourceType(), commentEntity.getResourceId());
+        }
+        log.info("{} 删除了评论 {}!", userId, commentId);
+        // 刷新外显评论缓存
+        ThreadPoolExecutorUtil.execute(() ->
+                refreshOutCommentCache(commentEntity.getResourceType(), commentEntity.getResourceId()));
+    }
+
+    /***
+     * 批量查询外显评论
+     * @param resourceType
+     * @param resourceIdList
+     * @return
+     */
+    public Map<Long, List<CommentOutResponseVO>> batchGetOutCommentFromCache(String resourceType, List<Long> resourceIdList) {
+        Map<Long, List<CommentOutResponseVO>> bulkQueryResultMap = new HashMap<>();
+        if (CollectionUtils.isEmpty(resourceIdList)) {
+            return bulkQueryResultMap;
+        }
+        List<String> bulkKey = resourceIdList.stream()
+                .map(rid -> RedisKeyEnum.COMMENT_OUT_LIST_CACHE.getKey(resourceType, String.valueOf(rid)))
+                .collect(Collectors.toList());
+        List<String> bulkQueryResult = redisService.mget(bulkKey);
+        for (int i = 0; i < resourceIdList.size(); i++) {
+            String val = bulkQueryResult.get(i);
+            Long resourceId = resourceIdList.get(i);
+            List<CommentOutResponseVO> commentOutResponseVOList;
+            if (StringUtils.isBlank(val)) {
+                commentOutResponseVOList = refreshOutCommentCache(resourceType, resourceId);
+            } else {
+                commentOutResponseVOList = JsonUtil.fromJson(val, new TypeReference<List<CommentOutResponseVO>>() { } );
+            }
+            for (CommentOutResponseVO commentOutResponseVO : commentOutResponseVOList) {
+                String author = commentOutResponseVO.getAuthor();
+                String replyAuthor = commentOutResponseVO.getReplyAuthor();
+                if (StringUtils.isNotBlank(author)) {
+                    commentOutResponseVO.setAuthorInfo(userService.accountInfo(author));
+                }
+                if (StringUtils.isNotBlank(replyAuthor)) {
+                    commentOutResponseVO.setReplyAuthorInfo(userService.accountInfo(replyAuthor));
+                }
+            }
+            bulkQueryResultMap.put(resourceId, commentOutResponseVOList);
+        }
+        return bulkQueryResultMap;
+    }
+
+    /***
+     * 查询外显评论
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    public List<CommentOutResponseVO> getOutCommentFromCache(String resourceType, Long resourceId) {
+        String key = RedisKeyEnum.COMMENT_OUT_LIST_CACHE.getKey(resourceType, String.valueOf(resourceId));
+        String val = redisService.get(key);
+        List<CommentOutResponseVO> commentOutResponseVOList;
+        if (StringUtils.isBlank(val)) {
+            commentOutResponseVOList = refreshOutCommentCache(resourceType, resourceId);
+        } else {
+            commentOutResponseVOList = JsonUtil.fromJson(val, new TypeReference<List<CommentOutResponseVO>>() { } );
+        }
+        for (CommentOutResponseVO commentOutResponseVO : commentOutResponseVOList) {
+            String author = commentOutResponseVO.getAuthor();
+            String replyAuthor = commentOutResponseVO.getReplyAuthor();
+            if (StringUtils.isNotBlank(author)) {
+                commentOutResponseVO.setAuthorInfo(userService.accountInfo(author));
+            }
+            if (StringUtils.isNotBlank(replyAuthor)) {
+                commentOutResponseVO.setReplyAuthorInfo(userService.accountInfo(replyAuthor));
+            }
+        }
+        return commentOutResponseVOList;
+    }
+
+    /***
+     * 刷新外显评论
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    private List<CommentOutResponseVO> refreshOutCommentCache(String resourceType, Long resourceId) {
+        if (!ResourceBehaviorConstants.ResourceType.MOMENT.name().equals(resourceType)) {
+            return Collections.emptyList();
+        }
+        String key = RedisKeyEnum.COMMENT_OUT_LIST_CACHE.getKey(resourceType, String.valueOf(resourceId));
+        List<CommentEntity> commentEntities = commentDAO.getOutSideCommentListLimit(resourceType, resourceId,
+                CommonStatusEnum.PUBLISHED.getStatus(), DEFAULT_OUT_COMMENT_SIZE);
+        List<CommentOutResponseVO> commentOutResponseVOList = new ArrayList<>();
+        if (org.apache.commons.collections.CollectionUtils.isNotEmpty(commentEntities)) {
+            commentEntities.forEach(c -> {
+                CommentOutResponseVO commentOutResponseVO = new CommentOutResponseVO();
+                commentOutResponseVO.setId(c.getId());
+                String content = c.getContent() == null ? "" : c.getContent();
+                if (StringUtils.isNotBlank(c.getImages())) {
+                    for (String image : c.getImages().split(",")) {
+                        content += " [图片]";
+                    }
+                }
+                commentOutResponseVO.setContent(content);
+                commentOutResponseVO.setAuthor(c.getAuthor());
+                commentOutResponseVO.setIpAddress(c.getIpAddress());
+                if (c.getReplyId() != null) {
+                    CommentEntity replyComment = commentDAO.findAllById(c.getReplyId());
+                    commentOutResponseVO.setReplyAuthor(replyComment.getAuthor());
+                }
+                commentOutResponseVOList.add(commentOutResponseVO);
+            });
+        }
+        redisService.set(key, JsonUtil.toJsonString(commentOutResponseVOList), RedisKeyEnum.COMMENT_OUT_LIST_CACHE.getExpireTime());
+        return commentOutResponseVOList;
+    }
+
+    /***
+     * 刷新评论数量
+     * @param resourceType
+     * @param resourceId
+     */
+    public Long getCommentCountFromCache(Long resourceId, String resourceType) {
+        String resourceIdStr = String.valueOf(resourceId);
+        String key = RedisKeyEnum.COMMENT_COUNT_CACHE.getKey(resourceType);
+        String countCache = redisService.hget(key, resourceIdStr);
+        if (StringUtils.isNotBlank(countCache)) {
+            return Long.valueOf(countCache);
+        }
+        return refreshResourceCommentCountCache(resourceId, resourceType);
+    }
+
+    public Map<Long, Long> batchGetCommentCountFromCache(List<Long> resourceIdList, String resourceType) {
+        Map<Long, Long> resultMap = new HashMap<>();
+        String key = RedisKeyEnum.COMMENT_COUNT_CACHE.getKey(resourceType);
+        List<String> resourceIdKeys = resourceIdList.stream().map(String::valueOf).collect(Collectors.toList());
+        List<String> resourceVals = redisService.hmget(key, resourceIdKeys);
+        for (int i = 0; i < resourceIdList.size(); i++) {
+            Long id = resourceIdList.get(i);
+            String cache = resourceVals.get(i);
+            Long count = 0L;
+            if (StringUtils.isNotBlank(cache)) {
+                count = Long.valueOf(cache);
+            } else {
+                count = refreshResourceCommentCountCache(id, resourceType);
+            }
+            resultMap.put(id, count);
+        }
+        return resultMap;
+    }
+
+    /***
+     * 刷新评论数量
+     * @param resourceType
+     * @param resourceId
+     */
+    private void addCommentCount2Cache(String resourceType, Long resourceId) {
+        String resourceIdStr = String.valueOf(resourceId);
+        String key = RedisKeyEnum.COMMENT_COUNT_CACHE.getKey(resourceType);
+        Long count = redisService.hincrex(key, resourceIdStr);
+        if (count == 1) {
+            refreshResourceCommentCountCache(resourceId, resourceType);
+        }
+    }
+
+    /***
+     * 刷新评论数量
+     * @param resourceType
+     * @param resourceId
+     */
+    private void reduceCommentCount2Cache(String resourceType, Long resourceId) {
+        String resourceIdStr = String.valueOf(resourceId);
+        String key = RedisKeyEnum.COMMENT_COUNT_CACHE.getKey(resourceType);
+        Long count = redisService.hdecrex(key, resourceIdStr);
+        if (count == 0) {
+            refreshResourceCommentCountCache(resourceId, resourceType);
+        }
+    }
+
+    private Long refreshResourceCommentCountCache(Long resourceId, String resourceType) {
+        String key = RedisKeyEnum.COMMENT_COUNT_CACHE.getKey(resourceType);
+        Long count = getResourceCommentCountFromDB(resourceId, resourceType);
+        redisService.hset(key, String.valueOf(resourceId), String.valueOf(count), RedisKeyEnum.COMMENT_COUNT_CACHE.getExpireTime());
+        return count;
+    }
+
+    private Long getResourceCommentCountFromDB(Long resourceId, String resourceType) {
+        return commentDAO.countByResourceIdAndResourceTypeAndStatusAndParentIdIsNull(resourceId, resourceType, CommonStatusEnum.PUBLISHED.getStatus());
+    }
+
+    /***
+     * 评论置顶
+     * @param commentId
+     */
+    public void topComment(Long commentId) {
+        CommentEntity commentEntity = commentDAO.findAllById(commentId);
+        Assert.isTrue(commentEntity != null, "评论不存在");
+        commentEntity.setIsTop(true);
+        commentEntity.setTopDate(new Date());
+        commentDAO.save(commentEntity);
+    }
+
+    /***
+     * 取消评论置顶
+     * @param commentId
+     */
+    public void cancelTopComment(Long commentId) {
+        CommentEntity commentEntity = commentDAO.findAllById(commentId);
+        Assert.isTrue(commentEntity != null, "评论不存在");
+        commentEntity.setIsTop(false);
+        commentEntity.setTopDate(null);
+        commentDAO.save(commentEntity);
+    }
+
+    /***
+     * 按条件查询评论列表
+     * @return
+     */
+    public APIPageResponseBean<List<CommentResponseVO>> queryCommentByCondition(
+            String currentUserId,
+            String content, Long resourceId, String resourceType, Integer pageNo, Integer pageSize, boolean isHot,
+            boolean isNew) {
+        // 排序规则
+        Sort sort = buildSortCondition(isHot, isNew);
+        // 分页条件
+        Pageable pageable = PageRequest.of(pageNo - 1, pageSize, sort);
+        // 构建查询条件
+        Specification<CommentEntity> specification = buildCommentQuerySpecification(content, resourceId, resourceType);
+        // 查询评论
+        Page<CommentEntity> commentEntityPage = commentDAO.findAll(specification, pageable);
+
+        List<CommentResponseVO> commentResponseVOList = new ArrayList<>();
+        if (commentEntityPage != null && commentEntityPage.getTotalElements() > 0) {
+            for (CommentEntity commentEntity : commentEntityPage.getContent()) {
+                CommentResponseVO vo = convertCommentEntityToVo(commentEntity);
+                vo.setReplyToList(getCommentReplyCommentVoList(vo.getId()));
+                commentResponseVOList.add(vo);
+            }
+        }
+        return APIPageResponseBean.success(pageNo, pageSize, commentEntityPage.getTotalElements(), commentResponseVOList);
+    }
+
+    private Sort buildSortCondition(boolean isHot, boolean isNew) {
+        List<Sort.Order> orderList = new ArrayList<>();
+        orderList.add(new Sort.Order(Sort.Direction.DESC, "topDate"));
+        if (isNew) {
+            orderList.add(new Sort.Order(Sort.Direction.DESC, "id"));
+        }
+        if (isHot) {
+            orderList.add(new Sort.Order(Sort.Direction.DESC, "likeCount"));
+        }
+        return Sort.by(orderList);
+    }
+
+    private Specification<CommentEntity> buildCommentQuerySpecification(String content, Long resourceId,
+                                                                        String resourceType) {
+
+        Specification<CommentEntity> specification = new Specification<CommentEntity>() {
+            @Override
+            public Predicate toPredicate(Root<CommentEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
+                List<Predicate> predicates = new ArrayList<>();
+                predicates.add(criteriaBuilder.equal(root.get("status").as(String.class), "PUBLISHED"));
+                predicates.add(criteriaBuilder.isNull(root.get("parentId").as(Long.class)));
+                if (resourceId != null) {
+                    predicates.add(criteriaBuilder.equal(root.get("resourceId").as(Long.class), resourceId));
+                }
+                if (StringUtils.isNotBlank(resourceType)) {
+                    predicates.add(criteriaBuilder.equal(root.get("resourceType").as(String.class), resourceType));
+                }
+                if (StringUtils.isNotBlank(content)) {
+                    predicates.add(
+                            criteriaBuilder.or(
+                                    criteriaBuilder.like(root.get("content").as(String.class), "%" + content + "%")));
+                }
+                Predicate[] pre = new Predicate[predicates.size()];
+                criteriaQuery.where(predicates.toArray(pre));
+                return criteriaQuery.getRestriction();
+            }
+        };
+        return specification;
+    }
+
+    private List<CommentResponseVO> getCommentReplyCommentVoList(Long parentId) {
+        List<CommentResponseVO> commentResponseVOList = new ArrayList<>();
+        List<CommentEntity> replyComments =
+                commentDAO.findAllByParentIdAndStatus(parentId, CommonStatusEnum.PUBLISHED.getStatus(), DEFAULT_REPLY_COMMENT_SIZE);
+        if (CollectionUtils.isEmpty(replyComments)) {
+            return commentResponseVOList;
+        }
+        for (CommentEntity commentEntity : replyComments) {
+            commentResponseVOList.add(convertCommentEntityToVo(commentEntity));
+        }
+        return commentResponseVOList;
+    }
 
+    private CommentResponseVO convertCommentEntityToVo(CommentEntity commentEntity) {
+        CommentResponseVO commentResponseVO = new CommentResponseVO();
+        BeanUtils.copyProperties(commentEntity, commentResponseVO);
+        commentResponseVO.setPubDateStr(DateUtils.getDate2String(commentEntity.getPubDate()));
+        commentResponseVO.setAuthorInfo(userService.accountInfo(commentEntity.getAuthor()));
+        if (commentEntity.getReplyId() != null) {
+            CommentEntity replyEntity = commentDAO.findAllById(commentEntity.getReplyId());
+            if (replyEntity != null) {
+                CommentResponseVO replyVO = new CommentResponseVO();
+                BeanUtils.copyProperties(replyEntity, replyVO);
+                replyVO.setPubDateStr(DateUtils.getDate2String(replyEntity.getPubDate()));
+                replyVO.setAuthorInfo(userService.accountInfo(replyEntity.getAuthor()));
+                commentResponseVO.setReply(replyVO);
+            }
+        }
+        return commentResponseVO;
+    }
 }

+ 1 - 1
webchat-client-chat/src/main/java/com/webchat/client/chat/controller/MomentController.java

@@ -54,7 +54,7 @@ public class MomentController {
      * @param size
      * @return
      */
-    @GetMapping("/ugc-service/moment/timeline")
+    @GetMapping("/timeline")
     APIResponseBean<List<MomentDetailVO>> timeline(@RequestParam Long lastMomentId,
                                                    @RequestParam(value = "size", defaultValue = "10") Integer size) {
         String userId = SessionHelper.getCurrentUserId();

+ 1 - 1
webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java

@@ -152,7 +152,7 @@ public enum RedisKeyEnum {
     /**
      * 朋友圈动态列表
      */
-    MOMENT_TIME_LINE_CACHE("MOMENT_TIME_LINE_CACHE", 3 * 24 * 60 * 60L),
+    MOMENT_TIME_LINE_CACHE("MOMENT_TIME_LINE_CACHE", 30 * 24 * 60 * 60L),
 
     MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE("MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE", 5 * 60L),
 

+ 2 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/CommentOutResponseVO.java

@@ -18,6 +18,8 @@ public class CommentOutResponseVO {
 
     private String content;
 
+    private String ipAddress;
+
     private UserBaseResponseInfoVO authorInfo;
 
     private UserBaseResponseInfoVO replyAuthorInfo;

+ 1 - 1
webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentVO.java

@@ -29,7 +29,7 @@ public class MomentVO {
 
     private String ipAddress;
 
-    private Boolean includeImage;
+    private Boolean includeImages;
     private Boolean includeVideo;
     private Boolean includeLink;
 

+ 61 - 0
webchat-remote/src/main/java/com/webchat/rmi/act/ICommentClient.java

@@ -0,0 +1,61 @@
+package com.webchat.rmi.act;
+
+
+import com.webchat.common.bean.APIPageResponseBean;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.CommentSaveVO;
+import com.webchat.domain.vo.response.CommentResponseVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+@FeignClient(name = "webchat-act-service", contextId = "commentClient")
+public interface ICommentClient {
+
+    /**
+     * 保存评论
+     * @param vo
+     * @return
+     */
+    @PostMapping("/addComment")
+    APIResponseBean<Long> saveComment(@RequestBody CommentSaveVO vo);
+
+    /**
+     * 删除评论
+     * @return
+     */
+    @PostMapping("/act-service/comment/deleteComment/{id}")
+    APIResponseBean deleteComment(@PathVariable Long id);
+
+    @PostMapping("/act-service/comment/top/{commentId}")
+    APIResponseBean<Long> topComment(@PathVariable Long commentId);
+
+    @PostMapping("/act-service/comment/cancelTop/{commentId}")
+    APIResponseBean<Long> cancelTopComment(@PathVariable Long commentId);
+
+    /***
+     * 查询资源评论列表
+     * @param resourceId
+     * @param resourceType
+     * @param isHot
+     * @param isNew
+     * @param pageNo
+     * @param pageSize
+     * @return
+     */
+    @GetMapping("/act-service/comment/query")
+    APIPageResponseBean<List<CommentResponseVO>> queryComments(@RequestParam(value = "resourceId", required = false) Long resourceId,
+                                                               @RequestParam(value = "resourceType", required = false) String resourceType,
+                                                               @RequestParam(value = "content", required = false) String content,
+                                                               @RequestParam(value = "isHot", required = false, defaultValue = "false") Boolean isHot,
+                                                               @RequestParam(value = "isNew", required = false, defaultValue = "false") Boolean isNew,
+                                                               @RequestParam(value = "pageNo", required = false, defaultValue = "1") Integer pageNo,
+                                                               @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize);
+
+
+}

+ 2 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentMediaDAO.java

@@ -17,4 +17,6 @@ public interface IMomentMediaDAO extends JpaSpecificationExecutor<MomentMediaEnt
 
     List<MomentMediaEntity> findAllByMomentIdAndType(Long momentId, Integer type);
 
+    List<MomentMediaEntity> findAllByMomentId(Long momentId);
+
 }

+ 1 - 1
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentImageHandler.java

@@ -24,7 +24,7 @@ public class MomentImageHandler implements MomentPublishHandler {
 
     @Override
     public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
-        if (!ObjectUtils.equals(moment.getIncludeImage(), true)) {
+        if (!ObjectUtils.equals(moment.getIncludeImages(), true)) {
             chain.handle(moment, chain);
             return;
         }

+ 1 - 1
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentRefreshHandler.java

@@ -71,7 +71,7 @@ public class MomentRefreshHandler implements MomentPublishHandler {
         momentEntity.setStatus(MomentConstants.getStatusByReviewScore(moment.getReviewScore()).getStatus());
         momentDAO.save(momentEntity);
         // 刷新媒体资源数据
-        if (ObjectUtils.equals(moment.getIncludeImage(), true)) {
+        if (ObjectUtils.equals(moment.getIncludeImages(), true)) {
             Set<Long> resourceIds = moment.getImages().stream().map(MomentMediaVO::getId).collect(Collectors.toSet());
             List<MomentMediaEntity> images = momentMediaDAO.findAllById(resourceIds);
             Map<Long, MomentMediaVO> mediaVOMap = moment.getImages().stream().collect(Collectors.toMap(MomentMediaVO::getId, Function.identity()));

+ 27 - 3
webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentMediaService.java

@@ -1,14 +1,18 @@
 package com.webchat.ugc.service.moment;
 
 
+import com.webchat.common.constants.MomentConstants;
 import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
 import com.webchat.domain.vo.response.moment.MomentMediaVO;
 import com.webchat.ugc.repository.dao.IMomentMediaDAO;
 import com.webchat.ugc.repository.entity.MomentMediaEntity;
+import org.apache.commons.collections.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @Service
 public class MomentMediaService {
@@ -22,9 +26,29 @@ public class MomentMediaService {
     public boolean saveMomentMedia(Long momentId,
                                    MomentSaveOrUpdateVO momentSaveOrUpdate) {
 
-
-
-        return false;
+        List<MomentMediaEntity> medias = momentMediaDAO.findAllByMomentId(momentId);
+        if (CollectionUtils.isNotEmpty(medias)) {
+            momentMediaDAO.deleteAll(medias);
+        }
+        medias = new ArrayList<MomentMediaEntity>();
+        if (momentSaveOrUpdate.includeImage()) {
+            medias = momentSaveOrUpdate.getImages().stream().map(img -> {
+                MomentMediaEntity momentMediaEntity = new MomentMediaEntity();
+                momentMediaEntity.setMomentId(momentId);
+                momentMediaEntity.setResource(img);
+                momentMediaEntity.setType(MomentConstants.MediaType.IMAGE.getType());
+                return momentMediaEntity;
+            }).collect(Collectors.toList());
+        }
+        if (momentSaveOrUpdate.includeVideo()) {
+            MomentMediaEntity momentMediaEntity = new MomentMediaEntity();
+            momentMediaEntity.setMomentId(momentId);
+            momentMediaEntity.setType(MomentConstants.MediaType.VIDEO.getType());
+            momentMediaEntity.setResource(momentSaveOrUpdate.getVideo());
+            medias.add(momentMediaEntity);
+        }
+        momentMediaDAO.saveAll(medias);
+        return true;
     }
 
     public List<MomentMediaEntity> medias(Long momentId, int type) {

+ 6 - 6
webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentTimeLineService.java

@@ -48,10 +48,10 @@ public class MomentTimeLineService {
      */
     public void addTimeLine(String author, Long momentId, Long time, boolean writeFriends) {
         Set<String> friends = new HashSet<>();
-        friends.add(author);
         if (writeFriends) {
             friends = accountService.getAllSubscriberByAccount(author, AccountRelationTypeEnum.USER_USER);
         }
+        friends.add(author);
         if (CollectionUtils.isEmpty(friends)) {
             return;
         }
@@ -95,16 +95,16 @@ public class MomentTimeLineService {
         // 无热点缓存存在说明:我们MySQL无3天内热点数据,但是可能存在3天外的冷备数据
         if (!redisService.exists(noneCacheKey) && !redisService.exists(timeLineKey)) {
             // BY用户时间线查询,不存在并发场景,所以这里不需要加锁
-            this.refreshUserTimelineCache(timeLineKey);
+            this.refreshUserTimelineCache(userId);
         }
         Set<String> caches = redisService.zreverseRangeByScore(timeLineKey, lastMomentId - 1, 0, size);
-        if (caches.size() < size) {
+//        if (caches.size() < size) {
             // 说明redis缓存的数据不够显示
             // 1、本身用户朋友圈数据就是很少
             // 2、用户加载到中间页,redis3天内数据不够显示
             // TODO 当前页数据不足{size}条,重新查询冷备(MongoDB)
-            caches = this.queryMomentsByMongoDB(userId, lastMomentId, size);
-        }
+//            caches = this.queryMomentsByMongoDB(userId, lastMomentId, size);
+//        }
         if (CollectionUtils.isEmpty(caches)) {
             // mysql、redis、冷备都同时没查询到数据,说明数据已经加载完
             return Collections.emptyList();
@@ -195,7 +195,7 @@ public class MomentTimeLineService {
         keys.forEach(key -> {
             if (!redisService.exists(key)) {
                 // 主动刷新用户朋友圈时间线缓存(3天热点数据)
-                // TODO
+//                this.refreshUserTimelineCache(key);
             }
             redisService.zadd(key, String.valueOf(momentId), momentId,
                     RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getExpireTime());