Explorar o código

act 基础互动服务抽象

wangqi49 hai 4 días
pai
achega
0fbce0ed79
Modificáronse 30 ficheiros con 1265 adicións e 51 borrados
  1. 54 0
      resources/database-sql/webchat-act.sql
  2. 27 2
      resources/nacos-yaml/webchat-act-service-dev.yaml
  3. 9 9
      webchat-act/pom.xml
  4. 34 0
      webchat-act/src/main/java/com/webchat/act/controller/ResourceBehaviorController.java
  5. 13 0
      webchat-act/src/main/java/com/webchat/act/repository/dao/ICommentDAO.java
  6. 37 0
      webchat-act/src/main/java/com/webchat/act/repository/dao/IResourceBehaviorDAO.java
  7. 28 0
      webchat-act/src/main/java/com/webchat/act/repository/entity/BaseEntity.java
  8. 105 0
      webchat-act/src/main/java/com/webchat/act/repository/entity/CommentEntity.java
  9. 63 0
      webchat-act/src/main/java/com/webchat/act/repository/entity/ResourceBehaviorEntity.java
  10. 36 0
      webchat-act/src/main/java/com/webchat/act/repository/entity/SimpleBaseEntity.java
  11. 317 0
      webchat-act/src/main/java/com/webchat/act/service/AbstractResourceBehaviorService.java
  12. 11 0
      webchat-act/src/main/java/com/webchat/act/service/CommentService.java
  13. 26 0
      webchat-act/src/main/java/com/webchat/act/service/OneWayResourceBehaviorService.java
  14. 47 0
      webchat-act/src/main/java/com/webchat/act/service/ResourceBehaviorFactory.java
  15. 53 0
      webchat-act/src/main/java/com/webchat/act/service/ResourceBehaviorInter.java
  16. 49 0
      webchat-act/src/main/java/com/webchat/act/service/TwoWayResourceBehaviorService.java
  17. 22 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/MomentController.java
  18. 21 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/service/MomentService.java
  19. 39 0
      webchat-common/src/main/java/com/webchat/common/constants/ResourceBehaviorConstants.java
  20. 24 5
      webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java
  21. 15 0
      webchat-common/src/main/java/com/webchat/common/service/RedisService.java
  22. 1 1
      webchat-domain/src/main/java/com/webchat/domain/vo/request/MomentSaveOrUpdateVO.java
  23. 25 0
      webchat-domain/src/main/java/com/webchat/domain/vo/request/act/ResourceBehaviorRequestVO.java
  24. 31 0
      webchat-remote/src/main/java/com/webchat/rmi/act/IResourceBehaviorClient.java
  25. 19 0
      webchat-remote/src/main/java/com/webchat/rmi/ugc/MomentClient.java
  26. 13 0
      webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentController.java
  27. 0 26
      webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentTimeLineController.java
  28. 6 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentTimeLineDAO.java
  29. 1 1
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentService.java
  30. 139 7
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentTimeLineService.java

+ 54 - 0
resources/database-sql/webchat-act.sql

@@ -0,0 +1,54 @@
+
+-- 资源行为记录表:点赞、收藏、分享、浏览、打赏
+CREATE TABLE webchat_act.`web_chat_resource_behavior` (
+                                                          `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                          `USER_ID` char(60) DEFAULT NULL COMMENT '用户ID',
+                                                          `RESOURCE_INDEX` bigint NOT NULL COMMENT '资源索引',
+                                                          `RESOURCE_TYPE` char(30) NOT NULL COMMENT '资源类型',
+                                                          `BEHAVIOR_TYPE` char(30) NOT NULL COMMENT '行为',
+                                                          `STATUS` tinyint(1) DEFAULT '0' COMMENT '状态',
+                                                          `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                                          `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+                                                          `VERSION` int DEFAULT '0' COMMENT '版本',
+                                                          PRIMARY KEY (`ID`),
+                                                          KEY `INDEX_STATUS` (`STATUS`),
+                                                          KEY `INDEX_USER_ID` (`USER_ID`),
+                                                          KEY `INDEX_RESOURCE` (`RESOURCE_TYPE`, `RESOURCE_INDEX`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='资源操作记录表';
+
+-- 评论表
+CREATE TABLE webchat_act.`web_chat_comment` (
+                                                `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                `RESOURCE_ID` bigint NOT NULL COMMENT '资源ID',
+                                                `RESOURCE_TYPE` char(50) NOT NULL COMMENT '资源类型',
+                                                `AUTHOR` char(60) NOT NULL COMMENT '作者',
+                                                `STATUS` char(20) NOT NULL COMMENT '状态',
+                                                `CONTENT` varchar(2000) DEFAULT NULL COMMENT '正文',
+                                                `IMAGES` varchar(1000) DEFAULT NULL COMMENT '图片',
+                                                `REPLY_ID` bigint DEFAULT NULL COMMENT '回复ID',
+                                                `PARENT_ID` bigint DEFAULT NULL COMMENT '父级评论ID',
+                                                `IS_TOP` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否置顶',
+                                                `LIKE_COUNT` bigint NOT NULL DEFAULT 0 COMMENT '点赞量',
+                                                `TOP_DATE` datetime DEFAULT NULL COMMENT '置顶时间',
+                                                `PUB_DATE` datetime DEFAULT NULL COMMENT '发布时间',
+                                                `IP` char(20) DEFAULT NULL COMMENT 'IP',
+                                                `IP_ADDRESS` char(40) DEFAULT NULL COMMENT 'IP归属地',
+                                                `CREATE_BY` char(100) DEFAULT NULL COMMENT '创建人',
+                                                `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                                `UPDATE_BY` char(100) DEFAULT NULL COMMENT '更新人',
+                                                `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+                                                `VERSION` int DEFAULT '0' COMMENT '版本',
+                                                PRIMARY KEY (`ID`),
+                                                KEY `INDEX_RESOURCE_TYPE_ID` (`RESOURCE_TYPE`, `RESOURCE_ID`),
+                                                KEY `INDEX_REPLY_ID` (`REPLY_ID`),
+                                                KEY `INDEX_PARENT_ID` (`PARENT_ID`),
+                                                KEY `INDEX_AUTHOR` (`AUTHOR`),
+                                                KEY `INDEX_STATUS` (`STATUS`),
+                                                KEY `INDEX_LIKE_COUNT` (`LIKE_COUNT`),
+                                                KEY `INDEX_IS_TOP_DATE` (`IS_TOP`, `TOP_DATE`),
+                                                KEY `INDEX_PUB_DATE` (`PUB_DATE`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='评论表';
+
+
+
+

+ 27 - 2
resources/nacos-yaml/webchat-act-service-dev.yaml

@@ -1,3 +1,28 @@
+#---------------------------------数据库配置----------------------------------#
 spring:
-  application:
-    name: webchat-act-service
+  datasource:
+    url: jdbc:mysql://127.0.0.1:3306/webchat_act?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=true
+    username: root
+    password: 12345678
+    driver-class-name: com.mysql.jdbc.Driver
+    hikari:
+      maximum-pool-size: 10
+  jpa:
+    show-sql: true
+  #---------------------------------redis----------------------------------#
+  data:
+    redis:
+      port: 6379
+      database: 6
+      jedis:
+        pool:
+          max-active: 100
+          max-wait: -1
+          min-idle: 10
+
+rocketmq:
+  name-server: 127.0.0.1:9876
+  consumer:
+    group: web_chat
+  producer:
+    group: web_chat

+ 9 - 9
webchat-act/pom.xml

@@ -23,16 +23,16 @@
     <dependencies>
 
         <!--引入mysql驱动-->
-<!--        <dependency>-->
-<!--            <groupId>mysql</groupId>-->
-<!--            <artifactId>mysql-connector-java</artifactId>-->
-<!--            <version>5.1.46</version>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>5.1.46</version>
+        </dependency>
         <!--使用JPA作为ORM框架 -->
-<!--        <dependency>-->
-<!--            <groupId>org.springframework.boot</groupId>-->
-<!--            <artifactId>spring-boot-starter-data-jpa</artifactId>-->
-<!--        </dependency>-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>com.webchat</groupId>

+ 34 - 0
webchat-act/src/main/java/com/webchat/act/controller/ResourceBehaviorController.java

@@ -0,0 +1,34 @@
+package com.webchat.act.controller;
+
+import com.webchat.act.service.ResourceBehaviorFactory;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.config.annotation.SafeClick;
+import com.webchat.domain.vo.request.act.ResourceBehaviorRequestVO;
+import com.webchat.rmi.act.IResourceBehaviorClient;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+public class ResourceBehaviorController implements IResourceBehaviorClient {
+
+
+    @Override
+    public APIResponseBean<Long> behavior(@RequestBody ResourceBehaviorRequestVO resourceBehaviorRequest) {
+
+        resourceBehaviorRequest.validateRequestParam();
+
+        // 根据具体互动行为类型走工厂模式获取具体的互动实现类,完成behavior操作
+        long count = ResourceBehaviorFactory.getService(resourceBehaviorRequest.getBehaviorType())
+                        .behavior(resourceBehaviorRequest.getUserId(),
+                                    resourceBehaviorRequest.getResourceType(),
+                                    resourceBehaviorRequest.getResourceId());
+        return APIResponseBeanUtil.success(count);
+    }
+
+    @Override
+    public APIResponseBean<Long> cancelBehavior(@RequestBody ResourceBehaviorRequestVO resourceBehaviorRequest) {
+        return null;
+    }
+}

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

@@ -0,0 +1,13 @@
+package com.webchat.act.repository.dao;
+
+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.stereotype.Repository;
+
+@Repository
+public interface ICommentDAO extends JpaSpecificationExecutor<CommentEntity>,
+                                     JpaRepository<CommentEntity, Long> {
+
+}

+ 37 - 0
webchat-act/src/main/java/com/webchat/act/repository/dao/IResourceBehaviorDAO.java

@@ -0,0 +1,37 @@
+package com.webchat.act.repository.dao;
+
+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 IResourceBehaviorDAO extends JpaSpecificationExecutor<ResourceBehaviorEntity>,
+                                              JpaRepository<ResourceBehaviorEntity, Long> {
+
+    ResourceBehaviorEntity findByUserIdAndBehaviorTypeAndResourceTypeAndResourceIndexAndStatusTrue(
+            String userId, String behaviorType, String resourceType, Long resourceIndex);
+
+
+    Long countByBehaviorTypeAndResourceTypeAndResourceIndexAndStatusTrue(
+            String behaviorType, String resourceType, Long resourceIndex);
+
+
+    @Query(value = "select rbe.resourceIndex, rbe.updateDate " +
+                   "from ResourceBehaviorEntity rbe " +
+                   "where rbe.userId = :userId and rbe.status = true and rbe.behaviorType = :behaviorType" +
+                   " and rbe.resourceType = :resourceType")
+    List<ResourceBehaviorEntity> findAllByUserBehaviorResource(
+            String userId, String behaviorType, String resourceType);
+
+    @Query(value = "select rbe.userId, rbe.updateDate " +
+            "from ResourceBehaviorEntity rbe " +
+            "where rbe.status = true and rbe.behaviorType = :behaviorType " +
+            " and rbe.resourceType = :resourceType and rbe.resourceIndex = :resourceId")
+    List<ResourceBehaviorEntity> findAllByResourceBehaviorUser(
+            Long resourceId, String behaviorType, String resourceType);
+
+}

+ 28 - 0
webchat-act/src/main/java/com/webchat/act/repository/entity/BaseEntity.java

@@ -0,0 +1,28 @@
+package com.webchat.act.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.PreUpdate;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 基础实体类
+ */
+@MappedSuperclass
+@Data
+public class BaseEntity extends SimpleBaseEntity implements Serializable {
+
+    @Column(name = "update_by")
+    private String updateBy;
+
+    @Column(name = "update_date")
+    private Date updateDate;
+
+    @PreUpdate
+    public void preUpdate() {
+        this.updateDate = new Date();
+    }
+}

+ 105 - 0
webchat-act/src/main/java/com/webchat/act/repository/entity/CommentEntity.java

@@ -0,0 +1,105 @@
+package com.webchat.act.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@Entity
+@Table(name = "web_chat_comment")
+public class CommentEntity extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 资源ID
+     */
+    @Column(name = "resource_id")
+    private Long resourceId;
+
+    /***
+     * 资源类型
+     */
+    @Column(name = "resource_type")
+    private String resourceType;
+
+    /***
+     * 评论正文
+     */
+    @Column(name = "content")
+    private String content;
+
+    /***
+     * 评论图片
+     */
+    @Column(name = "images")
+    private String images;
+
+    /***
+     * 回复ID
+     */
+    @Column(name = "reply_id")
+    private Long replyId;
+
+    /***
+     * 父级评论ID
+     */
+    @Column(name = "parent_id")
+    private Long parentId;
+
+    /***
+     * 作者
+     */
+    @Column(name = "author")
+    private String author;
+
+    /***
+     * 是否置顶
+     */
+    @Column(name = "is_top")
+    private Boolean isTop;
+
+    /***
+     * 点赞量
+     */
+    @Column(name = "like_count")
+    private Long likeCount;
+
+    /***
+     * 状态
+     */
+    @Column(name = "status")
+    private String status;
+
+    /***
+     * 置顶时间
+     */
+    @Column(name = "top_date")
+    private Date topDate;
+
+    /***
+     * 发布时间
+     */
+    @Column(name = "pub_date")
+    private Date pubDate;
+
+    /***
+     * IP
+     */
+    @Column(name = "ip")
+    private String ip;
+
+    /***
+     * IP ADDRESS
+     */
+    @Column(name = "ip_address")
+    private String ipAddress;
+}

+ 63 - 0
webchat-act/src/main/java/com/webchat/act/repository/entity/ResourceBehaviorEntity.java

@@ -0,0 +1,63 @@
+package com.webchat.act.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.PreUpdate;
+import jakarta.persistence.Table;
+import jakarta.persistence.Version;
+import lombok.Data;
+
+import java.util.Date;
+
+
+@Data
+@Entity
+@Table(name = "web_chat_resource_behavior")
+public class ResourceBehaviorEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    @Column(name = "USER_ID")
+    private String userId;
+
+    @Column(name = "RESOURCE_INDEX")
+    private Long resourceIndex;
+
+    @Column(name = "RESOURCE_TYPE")
+    private String resourceType;
+
+    @Column(name = "BEHAVIOR_TYPE")
+    private String behaviorType;
+
+    @Column(name = "status")
+    private Boolean status;
+
+    @Column(name = "update_date")
+    private Date updateDate;
+
+    @Column(name = "create_date")
+    private Date createDate;
+
+    @Version
+    @Column(name = "version")
+    private Integer version;
+
+    @PrePersist
+    public void prePersist() {
+        Date now = new Date();
+        if (this.createDate == null) {
+            this.createDate = now;
+        }
+    }
+
+    @PreUpdate
+    public void preUpdate() {
+        this.updateDate = new Date();
+    }
+}

+ 36 - 0
webchat-act/src/main/java/com/webchat/act/repository/entity/SimpleBaseEntity.java

@@ -0,0 +1,36 @@
+package com.webchat.act.repository.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.PrePersist;
+import jakarta.persistence.Version;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 基础实体类
+ */
+@MappedSuperclass
+@Data
+public class SimpleBaseEntity implements Serializable {
+
+    @Column(name = "create_by")
+    private String createBy;
+
+    @Column(name = "create_date")
+    private Date createDate;
+
+    @Version
+    @Column(name = "version")
+    private Integer version;
+
+    @PrePersist
+    public void prePersist() {
+        Date now = new Date();
+        if (this.createDate == null) {
+            this.createDate = now;
+        }
+    }
+}

+ 317 - 0
webchat-act/src/main/java/com/webchat/act/service/AbstractResourceBehaviorService.java

@@ -0,0 +1,317 @@
+package com.webchat.act.service;
+
+import com.google.common.collect.Lists;
+import com.webchat.act.repository.dao.IResourceBehaviorDAO;
+import com.webchat.act.repository.entity.ResourceBehaviorEntity;
+import com.webchat.common.constants.ResourceBehaviorConstants;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.util.DateUtils;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.DefaultTypedTuple;
+import org.springframework.data.redis.core.ZSetOperations;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public abstract class AbstractResourceBehaviorService implements ResourceBehaviorInter {
+
+    protected String behaviorType;
+    protected String behaviorTypeName;
+    public void setBehaviorType(String behaviorType) {
+        this.behaviorType = behaviorType;
+        this.behaviorTypeName = ResourceBehaviorConstants.getBehaviorTypeName(behaviorType);
+    }
+
+    @Autowired
+    IResourceBehaviorDAO resourceBehaviorDAO;
+    @Autowired
+    RedisService redisService;
+    @Autowired
+    RedissonClient redissonClient;
+
+    /**
+     * 实际互动行为,由具体实现类来实现
+     *
+     * @param userId
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    protected abstract boolean doBehavior(String userId, String resourceType, Long resourceId);
+
+    /**
+     * 实际取消互动行为,由具体实现类来实现
+     *
+     * @param userId
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    protected boolean doCancelBehavior(String userId, String resourceType, Long resourceId) {
+
+        throw new BusinessException("不支持的互动行为");
+    }
+
+    @Override
+    public long behavior(String userId, String resourceType, Long resourceId) {
+
+        Long behaviorCount = 0L;
+        /**
+         * 1. 数据持久化 DB
+         */
+        if (this.doBehavior(userId, resourceType, resourceId)) {
+
+            /**
+             * 2. 构建、刷新互动行为数据缓存
+             */
+            behaviorCount = this.refreshBehaviorCache(userId, resourceType, resourceId, false);
+            /**
+             * 3. 互动消息 TODO
+             */
+        }
+        return behaviorCount;
+    }
+
+    @Override
+    public long cancelBehavior(String userId, String resourceType, Long resourceId) {
+        Long behaviorCount = 0L;
+        /**
+         * 1. 数据持久化 DB
+         */
+        if (this.doCancelBehavior(userId, resourceType, resourceId)) {
+            /**
+             * 2. 构建、刷新互动行为数据缓存
+             */
+            behaviorCount = this.refreshBehaviorCache(userId, resourceType, resourceId, true);
+        }
+        return behaviorCount;
+    }
+
+
+    /**
+     * 批量查询资源的互动数量
+     *
+     * @param resourceType
+     * @param resourceIds
+     * @return
+     */
+    @Override
+    public Map<Long, Long> countBehavior(String resourceType, List<Long> resourceIds) {
+        Map<Long, Long> behaviorCountMap = new HashMap<>();
+        String behaviorCacheKey = resourceBehaviorCountCacheHashKey(resourceType);
+        List<String> resourceIdArr = resourceIds.stream().map(String::valueOf).collect(Collectors.toList());
+        List<String> caches = redisService.hmget(behaviorCacheKey, resourceIdArr);
+        for (int i = 0; i < resourceIds.size(); i++) {
+            Long resourceId = resourceIds.get(i);
+            String cache = caches.get(i);
+            Long count;
+            if (StringUtils.isNotBlank(cache)) {
+                count = Long.valueOf(cache);
+            } else {
+                // TODO 推荐大家遍历结束后批量刷新
+                count = this.initRefreshBehaviorCache(resourceType, resourceId);
+            }
+            behaviorCountMap.put(resourceId, count);
+        }
+        return behaviorCountMap;
+    }
+
+    @Override
+    public Map<Long, Boolean> isBehavior(String userId, String resourceType, List<Long> resourceIds) {
+
+        String cacheKey = this.userBehaviorResourceCacheKey(userId, resourceType);
+        if(!redisService.exists(cacheKey)) {
+            this.initUserBehaviorResourcesCache(userId, resourceType);
+        }
+        Set<String> resourceIdSet = resourceIds.stream().map(String::valueOf).collect(Collectors.toSet());
+        Map<String, Boolean> behaviorStatusMap = redisService.zContains(cacheKey, resourceIdSet);
+        return behaviorStatusMap.entrySet().stream()
+                .collect(Collectors.toMap(
+                        entry -> Long.parseLong(entry.getKey()),
+                        Map.Entry::getValue,
+                        (existing, replacement) -> existing
+                ));
+    }
+
+    public boolean isBehavior(String userId, String resourceType, Long resourceId) {
+        Map<Long, Boolean> isBehaviorMap = this.isBehavior(userId, resourceType, Lists.newArrayList(resourceId));
+        return ObjectUtils.equals(isBehaviorMap.get(resourceId), true);
+    }
+
+    private Long refreshBehaviorCache(String userId, String resourceType, Long resourceId, boolean isCancel) {
+
+        /**
+         * 1. 刷新资源互动总量
+         */
+        Long behaviorCount = this.refreshBehaviorCountCache(resourceType, resourceId, isCancel);
+        /**
+         * 2. 刷新用户维度操作过的资源列表缓存
+         */
+        this.refreshUserBehaviorResourcesCache(userId, resourceType, resourceId, isCancel);
+        /**
+         * 3. 刷新资源维度操作过的用户列表缓存
+         */
+        this.refreshResourceBehaviorUsersCache(userId, resourceType, resourceId, isCancel);
+        return behaviorCount;
+    }
+
+
+    /**
+     * 刷新用户维度,互动资料列表缓存
+     *
+     * 这里为什么使用Zset,而不是Set?
+     * 因为我们考虑到后续在”个人中心这一场景下,我们需要分页获取指定用户互动过的有序资源列表“
+     *
+     * @param userId
+     * @param resourceType
+     * @param resourceId
+     * @param isCancel
+     */
+    private void refreshUserBehaviorResourcesCache(String userId, String resourceType, Long resourceId, boolean isCancel) {
+        String cacheKey = this.userBehaviorResourceCacheKey(userId, resourceType);
+        if (!redisService.exists(cacheKey)) {
+            RLock lock = redissonClient.getLock(RedisKeyEnum.USER_BEHAVIOR_RESOURCE_CACHE_INIT_LOCK.getKey(userId));
+            try {
+                lock.lock();
+                // 双重检查锁,避免并发下重复查库刷新redis
+                if (!redisService.exists(cacheKey)) {
+                    this.initUserBehaviorResourcesCache(userId, resourceType);
+                }
+            } finally {
+                lock.unlock();
+            }
+            return;
+        }
+        if (isCancel) {
+            // 取消互动
+            redisService.zremove(cacheKey, String.valueOf(resourceId));
+            return;
+        }
+        redisService.zadd(cacheKey, String.valueOf(resourceId), DateUtils.getCurrentTimeMillis(),
+                RedisKeyEnum.USER_BEHAVIOR_RESOURCE_ZSET_CACHE.getExpireTime());
+    }
+
+    private void initUserBehaviorResourcesCache(String userId, String resourceType) {
+        List<ResourceBehaviorEntity> entities =
+                resourceBehaviorDAO.findAllByUserBehaviorResource(userId, behaviorType, resourceType);
+        if (CollectionUtils.isNotEmpty(entities)) {
+            Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
+            for (ResourceBehaviorEntity resourceBehavior : entities) {
+                String value = String.valueOf(resourceBehavior.getResourceIndex());
+                double score = resourceBehavior.getUpdateDate() != null ?
+                               resourceBehavior.getUpdateDate().getTime() : resourceBehavior.getCreateDate().getTime();
+                ZSetOperations.TypedTuple<String> tuple = new DefaultTypedTuple<>(value, score);
+                typedTupleSet.add(tuple);
+            }
+            String cacheKey = this.userBehaviorResourceCacheKey(userId, resourceType);
+            redisService.zadd(cacheKey, typedTupleSet, RedisKeyEnum.USER_BEHAVIOR_RESOURCE_ZSET_CACHE.getExpireTime());
+        }
+    }
+
+
+    private void initResourceBehaviorUsersCache(Long resourceId, String resourceType) {
+        List<ResourceBehaviorEntity> entities =
+                resourceBehaviorDAO.findAllByResourceBehaviorUser(resourceId, behaviorType, resourceType);
+        if (CollectionUtils.isNotEmpty(entities)) {
+            Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
+            for (ResourceBehaviorEntity resourceBehavior : entities) {
+                String value = String.valueOf(resourceBehavior.getUserId());
+                double score = resourceBehavior.getUpdateDate() != null ?
+                        resourceBehavior.getUpdateDate().getTime() : resourceBehavior.getCreateDate().getTime();
+                ZSetOperations.TypedTuple<String> tuple = new DefaultTypedTuple<>(value, score);
+                typedTupleSet.add(tuple);
+            }
+            String cacheKey = this.resourceBehaviorUserCacheKey(resourceId, resourceType);
+            redisService.zadd(cacheKey, typedTupleSet, RedisKeyEnum.RESOURCE_BEHAVIOR_USER_ZSET_CACHE.getExpireTime());
+        }
+    }
+
+
+    /**
+     *
+     * @param userId
+     * @param resourceType
+     * @param resourceId
+     * @param isCancel
+     */
+    private void refreshResourceBehaviorUsersCache(String userId, String resourceType, Long resourceId, boolean isCancel) {
+        String cacheKey = this.resourceBehaviorUserCacheKey(resourceId, resourceType);
+        if (!redisService.exists(cacheKey)) {
+            RLock lock = redissonClient.getLock(RedisKeyEnum.RESOURCE_BEHAVIOR_USER_CACHE_INIT_LOCK.getKey(
+                    resourceType, String.valueOf(resourceId)));
+            try {
+                lock.lock();
+                // 双重检查锁,避免并发下重复查库刷新redis
+                if (!redisService.exists(cacheKey)) {
+                    this.initResourceBehaviorUsersCache(resourceId, resourceType);
+                }
+            } finally {
+                lock.unlock();
+            }
+            return;
+        }
+        if (isCancel) {
+            // 取消互动
+            redisService.zremove(cacheKey, userId);
+            return;
+        }
+        redisService.zadd(cacheKey, userId, DateUtils.getCurrentTimeMillis(),
+                RedisKeyEnum.USER_BEHAVIOR_RESOURCE_ZSET_CACHE.getExpireTime());
+    }
+
+    /**
+     * 基于redis hash结构的原子+-特性实现计数器,记录资源互动最新量
+     *
+     * @param resourceType
+     * @param resourceId
+     * @param isCancel
+     * @return
+     */
+    private Long refreshBehaviorCountCache(String resourceType, Long resourceId, boolean isCancel) {
+        String cacheKey = this.resourceBehaviorCountCacheHashKey(resourceType);
+        Long behaviorCount;
+        if (isCancel) {
+            behaviorCount = redisService.hdecrex(cacheKey, String.valueOf(resourceId));
+        } else {
+            behaviorCount = redisService.hincrex(cacheKey, String.valueOf(resourceId));
+        }
+        redisService.expire(cacheKey, RedisKeyEnum.RESOURCE_BEHAVIOR_COUNT_CACHE.getExpireTime());
+        if (behaviorCount == 1 || behaviorCount == 0) {
+            // 说明:之前没有互动或者redis过期,都重新查库初始化一次redis缓存
+            behaviorCount = this.initRefreshBehaviorCache(resourceType, resourceId);
+        }
+        return behaviorCount;
+    }
+
+    private Long initRefreshBehaviorCache(String resourceType, Long resourceId) {
+        Long behaviorCount = resourceBehaviorDAO.countByBehaviorTypeAndResourceTypeAndResourceIndexAndStatusTrue(
+                behaviorType, resourceType, resourceId);
+        String countHashKey = this.resourceBehaviorCountCacheHashKey(resourceType);
+        redisService.hset(countHashKey, String.valueOf(resourceId), String.valueOf(behaviorCount),
+                RedisKeyEnum.RESOURCE_BEHAVIOR_COUNT_CACHE.getExpireTime());
+        return behaviorCount;
+    }
+
+    private String resourceBehaviorCountCacheHashKey(String resourceType) {
+        return RedisKeyEnum.RESOURCE_BEHAVIOR_COUNT_CACHE.getKey(this.behaviorType, resourceType);
+    }
+
+    private String userBehaviorResourceCacheKey(String userId, String resourceType) {
+        return RedisKeyEnum.USER_BEHAVIOR_RESOURCE_ZSET_CACHE.getKey(behaviorType, resourceType, userId);
+    }
+
+    private String resourceBehaviorUserCacheKey(Long resourceId, String resourceType) {
+        return RedisKeyEnum.USER_BEHAVIOR_RESOURCE_ZSET_CACHE.getKey(behaviorType, resourceType, String.valueOf(resourceId));
+    }
+}

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

@@ -0,0 +1,11 @@
+package com.webchat.act.service;
+
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class CommentService {
+
+
+
+}

+ 26 - 0
webchat-act/src/main/java/com/webchat/act/service/OneWayResourceBehaviorService.java

@@ -0,0 +1,26 @@
+package com.webchat.act.service;
+
+
+import com.webchat.act.repository.entity.ResourceBehaviorEntity;
+import com.webchat.common.constants.ResourceBehaviorConstants;
+import org.springframework.stereotype.Service;
+
+@Service
+public class OneWayResourceBehaviorService extends AbstractResourceBehaviorService {
+
+
+
+    @Override
+    protected boolean doBehavior(String userId, String resourceType, Long resourceId) {
+
+        ResourceBehaviorEntity resourceBehavior = new ResourceBehaviorEntity();
+
+        resourceBehavior.setUserId(userId);
+        resourceBehavior.setBehaviorType(super.behaviorType);
+        resourceBehavior.setResourceType(resourceType);
+        resourceBehavior.setResourceIndex(resourceId);
+        resourceBehavior.setStatus(true);
+        super.resourceBehaviorDAO.save(resourceBehavior);
+        return true;
+    }
+}

+ 47 - 0
webchat-act/src/main/java/com/webchat/act/service/ResourceBehaviorFactory.java

@@ -0,0 +1,47 @@
+package com.webchat.act.service;
+
+
+import com.webchat.common.constants.ResourceBehaviorConstants;
+import com.webchat.common.util.SpringContextUtil;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class ResourceBehaviorFactory implements InitializingBean , ApplicationContextAware {
+
+    private ApplicationContext applicationContext;
+    private static final Map<String, AbstractResourceBehaviorService> serviceMap = new HashMap<>();
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        this.initServiceMap();
+    }
+
+    private void initServiceMap() {
+
+        serviceMap.put(ResourceBehaviorConstants.BehaviorType.LIKE.name(), applicationContext.getBean(TwoWayResourceBehaviorService.class));
+        serviceMap.put(ResourceBehaviorConstants.BehaviorType.COLLECT.name(), applicationContext.getBean(TwoWayResourceBehaviorService.class));
+        serviceMap.put(ResourceBehaviorConstants.BehaviorType.SHARE.name(), applicationContext.getBean(OneWayResourceBehaviorService.class));
+        serviceMap.put(ResourceBehaviorConstants.BehaviorType.VIEW.name(), applicationContext.getBean(OneWayResourceBehaviorService.class));
+    }
+
+    public static AbstractResourceBehaviorService getService(String behaviorType) {
+
+        AbstractResourceBehaviorService abstractResourceBehaviorService = serviceMap.get(behaviorType);
+        Assert.notNull(abstractResourceBehaviorService, "不支持的互动行为");
+        abstractResourceBehaviorService.setBehaviorType(behaviorType);
+        return abstractResourceBehaviorService;
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+    }
+}

+ 53 - 0
webchat-act/src/main/java/com/webchat/act/service/ResourceBehaviorInter.java

@@ -0,0 +1,53 @@
+package com.webchat.act.service;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ResourceBehaviorInter {
+
+
+    /**
+     * 互动操作
+     *
+     * @param userId
+     * @param behaviorType
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    long behavior(String userId, String resourceType, Long resourceId);
+
+
+    /**
+     * 取消互动操作
+     *
+     * @param userId
+     * @param behaviorType
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    long cancelBehavior(String userId, String resourceType, Long resourceId);
+
+    /**
+     * 批量查询资源互动量
+     *
+     * @param behaviorType
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    Map<Long, Long> countBehavior(String resourceType, List<Long> resourceId);
+
+
+    /**
+     * 判断某个用户是否操作过
+     *
+     * @param userId
+     * @param behaviorType
+     * @param resourceType
+     * @param resourceId
+     * @return
+     */
+    Map<Long, Boolean> isBehavior(String userId, String resourceType, List<Long> resourceId);
+}

+ 49 - 0
webchat-act/src/main/java/com/webchat/act/service/TwoWayResourceBehaviorService.java

@@ -0,0 +1,49 @@
+package com.webchat.act.service;
+
+
+import com.webchat.act.repository.entity.ResourceBehaviorEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+
+/**
+ * 双向(支持取消操作)互动服务
+ */
+@Service
+public class TwoWayResourceBehaviorService extends AbstractResourceBehaviorService {
+
+
+    private static final String BEHAVIOR_ERROR_MSG = "%s失败";
+    private static final String CANCEL_BEHAVIOR_ERROR_MSG = "取消%s失败";
+
+    @Override
+    protected boolean doBehavior(String userId, String resourceType, Long resourceId) {
+
+        boolean isBehavior = this.isBehavior(userId, resourceType, resourceId);
+        Assert.isTrue(!isBehavior, String.format(BEHAVIOR_ERROR_MSG, super.behaviorTypeName));
+        ResourceBehaviorEntity resourceBehavior = new ResourceBehaviorEntity();
+        resourceBehavior.setUserId(userId);
+        resourceBehavior.setBehaviorType(super.behaviorType);
+        resourceBehavior.setResourceType(resourceType);
+        resourceBehavior.setResourceIndex(resourceId);
+        resourceBehavior.setStatus(true);
+        super.resourceBehaviorDAO.save(resourceBehavior);
+        return false;
+    }
+
+    @Override
+    protected boolean doCancelBehavior(String userId, String resourceType, Long resourceId) {
+
+        boolean isBehavior = this.isBehavior(userId, resourceType, resourceId);
+        Assert.isTrue(isBehavior, String.format(CANCEL_BEHAVIOR_ERROR_MSG, super.behaviorTypeName));
+
+        ResourceBehaviorEntity resourceBehavior =
+                resourceBehaviorDAO.findByUserIdAndBehaviorTypeAndResourceTypeAndResourceIndexAndStatusTrue(
+                userId, super.behaviorType, resourceType, resourceId);
+        Assert.notNull(resourceBehavior, String.format(CANCEL_BEHAVIOR_ERROR_MSG, super.behaviorTypeName));
+        // 互动已取消
+        resourceBehavior.setStatus(false);
+        super.resourceBehaviorDAO.save(resourceBehavior);
+        return true;
+    }
+}

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

@@ -8,12 +8,17 @@ import com.webchat.common.config.annotation.SafeClick;
 import com.webchat.common.enums.ClickEvent;
 import com.webchat.common.helper.SessionHelper;
 import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 @RestController
 @RequestMapping("/client-service/moment")
 public class MomentController {
@@ -35,7 +40,24 @@ public class MomentController {
 
         String author = SessionHelper.getCurrentUserId();
         momentSaveOrUpdate.setAuthor(author);
+        // 客户端ip
+        String clientIp = SessionHelper.getCurrentClientIP();
+        momentSaveOrUpdate.setIp(clientIp);
         Long momentId = momentService.publish(momentSaveOrUpdate);
         return APIResponseBeanUtil.success(momentId);
     }
+
+    /**
+     * 朋友圈动态列表加载
+     *
+     * @param lastMomentId
+     * @param size
+     * @return
+     */
+    @GetMapping("/ugc-service/moment/timeline")
+    APIResponseBean<List<MomentDetailVO>> timeline(@RequestParam Long lastMomentId,
+                                                   @RequestParam(value = "size", defaultValue = "10") Integer size) {
+        String userId = SessionHelper.getCurrentUserId();
+        return APIResponseBeanUtil.success(momentService.timeline(userId, lastMomentId, size));
+    }
 }

+ 21 - 0
webchat-client-chat/src/main/java/com/webchat/client/chat/service/MomentService.java

@@ -4,10 +4,14 @@ import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.common.exception.BusinessException;
 import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
 import com.webchat.rmi.ugc.MomentClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
+import java.util.List;
+
 @Service
 public class MomentService {
 
@@ -29,4 +33,21 @@ public class MomentService {
         }
         throw new BusinessException(responseBean.getMsg());
     }
+
+    /**
+     * 朋友圈动态列表加载
+     *
+     * @param userId
+     * @param lastMomentId
+     * @param size
+     * @return
+     */
+    public List<MomentDetailVO> timeline(String userId, Long lastMomentId, int size) {
+
+        APIResponseBean<List<MomentDetailVO>> responseBean = momentClient.timeline(userId, lastMomentId, size);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        return Collections.emptyList();
+    }
 }

+ 39 - 0
webchat-common/src/main/java/com/webchat/common/constants/ResourceBehaviorConstants.java

@@ -0,0 +1,39 @@
+package com.webchat.common.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+public class ResourceBehaviorConstants {
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum ResourceType {
+
+        MOMENT("朋友圈动态"),
+        ARTICLE("公众号文章"),
+        SHOP_SKU("商城商品");
+        private String typeName;
+    }
+
+    @Getter
+    @AllArgsConstructor
+    public enum BehaviorType {
+
+        LIKE("点赞"),
+        SHARE("分享"),
+        VIEW("浏览"),
+        COLLECT("收藏");
+
+        private String typeName;
+    }
+
+    public static String getBehaviorTypeName(String behaviorTypeCode) {
+        for (BehaviorType behaviorType : BehaviorType.values()) {
+            if (behaviorType.name().equals(behaviorTypeCode)) {
+                return behaviorType.getTypeName();
+            }
+        }
+        return "";
+    }
+}

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

@@ -139,11 +139,6 @@ public enum RedisKeyEnum {
      */
     RESOURCE_BEHAVIOR_USERS_CACHE("RESOURCE_BEHAVIOR_USERS_CACHE", -1L),
 
-    /**
-     * 资源行为计数缓存
-     */
-    RESOURCE_BEHAVIOR_COUNT_CACHE("RESOURCE_BEHAVIOR_COUNT_CACHE", -1L),
-
     /***
      * 用户点赞的资源列表
      */
@@ -159,6 +154,8 @@ public enum RedisKeyEnum {
      */
     MOMENT_TIME_LINE_CACHE("MOMENT_TIME_LINE_CACHE", 3 * 24 * 60 * 60L),
 
+    MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE("MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE", 5 * 60L),
+
     /**
      * 外显评论
      */
@@ -352,6 +349,28 @@ public enum RedisKeyEnum {
      */
     MOMENT_CACHE_REFRESH_LOCK("MOMENT_CACHE_REFRESH_LOCK",  10L),
 
+    RESOURCE_BEHAVIOR_COUNT_CACHE("RESOURCE_BEHAVIOR_COUNT_CACHE",  3 * 24 * 60 * 60L),
+
+    /**
+     * 用户互动资料列表缓存
+     */
+    USER_BEHAVIOR_RESOURCE_ZSET_CACHE("USER_BEHAVIOR_RESOURCE_ZSET_CACHE", 3 * 24 * 60 * 60L),
+
+    /**
+     * 主动刷新用户互动资源列表数据,分布式锁
+     */
+    USER_BEHAVIOR_RESOURCE_CACHE_INIT_LOCK("USER_BEHAVIOR_RESOURCE_CACHE_INIT_LOCK", 60L),
+
+    /**
+     * 资源被互动的用户列表缓存
+     */
+    RESOURCE_BEHAVIOR_USER_ZSET_CACHE("RESOURCE_BEHAVIOR_USER_ZSET_CACHE", 3 * 24 * 60 * 60L),
+
+    /**
+     * 资源被互动的用户列表缓存刷新分布式锁
+     */
+    RESOURCE_BEHAVIOR_USER_CACHE_INIT_LOCK("RESOURCE_BEHAVIOR_USER_CACHE_INIT_LOCK", 60L),
+
     ;
 
 

+ 15 - 0
webchat-common/src/main/java/com/webchat/common/service/RedisService.java

@@ -343,6 +343,17 @@ public class RedisService {
         return false;
     }
 
+    public Map<String, Boolean> zContains(String key, Set<String> values) {
+        if (StringUtils.isBlank(key) || CollectionUtils.isEmpty(values)) {
+            return Collections.emptyMap();
+        }
+        Map<String, Boolean> containMap = new HashMap<>();
+        values.forEach(val -> {
+            containMap.put(val, this.zContains(key, val));
+        });
+        return containMap;
+    }
+
 
     /**
      * @param key
@@ -1053,6 +1064,10 @@ public class RedisService {
         redisTemplate.delete(key);
     }
 
+    public void removeAll(final Collection<String> keys) {
+        redisTemplate.delete(keys);
+    }
+
     /**
      * 判断缓存中是否有对应的value
      *

+ 1 - 1
webchat-domain/src/main/java/com/webchat/domain/vo/request/MomentSaveOrUpdateVO.java

@@ -44,7 +44,7 @@ public class MomentSaveOrUpdateVO {
     /**
      * 客户端
      */
-    private String clientIp;
+    private String ip;
 
 
     public void validateRequestParam() {

+ 25 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/request/act/ResourceBehaviorRequestVO.java

@@ -0,0 +1,25 @@
+package com.webchat.domain.vo.request.act;
+
+
+import lombok.Data;
+import org.springframework.util.Assert;
+
+@Data
+public class ResourceBehaviorRequestVO {
+
+    private String userId;
+
+    private String behaviorType;
+
+    private String resourceType;
+
+    private Long resourceId;
+
+
+    public void validateRequestParam() {
+        Assert.notNull(userId, "操作人为空");
+        Assert.notNull(behaviorType, "互动行为为空");
+        Assert.notNull(resourceType, "资源类型为空");
+        Assert.notNull(resourceId, "资源ID为空");
+    }
+}

+ 31 - 0
webchat-remote/src/main/java/com/webchat/rmi/act/IResourceBehaviorClient.java

@@ -0,0 +1,31 @@
+package com.webchat.rmi.act;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.act.ResourceBehaviorRequestVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = "webchat-act-service", contextId = "resourceBehaviorClient")
+public interface IResourceBehaviorClient {
+
+
+    /**
+     * 资源基础互动操作
+     *
+     * @param resourceBehaviorRequest
+     * @return
+     */
+    @PostMapping("/act-service/resource/behavior")
+    APIResponseBean<Long> behavior(@RequestBody ResourceBehaviorRequestVO resourceBehaviorRequest);
+
+    /**
+     * 取消资源基础互动操作
+     *
+     * @param resourceBehaviorRequest
+     * @return
+     */
+    @PostMapping("/act-service/resource/behavior/cancel")
+    APIResponseBean<Long> cancelBehavior(@RequestBody ResourceBehaviorRequestVO resourceBehaviorRequest);
+
+}

+ 19 - 0
webchat-remote/src/main/java/com/webchat/rmi/ugc/MomentClient.java

@@ -2,9 +2,14 @@ package com.webchat.rmi.ugc;
 
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
 import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
 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-ugc-service", contextId = "momentClient")
@@ -18,4 +23,18 @@ public interface MomentClient {
      */
     @PostMapping("/ugc-service/moment/publish")
     APIResponseBean<Long> publish(@RequestBody MomentSaveOrUpdateVO momentSaveOrUpdate);
+
+
+    /**
+     * 朋友圈动态数据瀑布流翻页加载
+     *
+     * @param userId
+     * @param lastMomentId
+     * @param size
+     * @return
+     */
+    @GetMapping("/ugc-service/moment/timeline")
+    APIResponseBean<List<MomentDetailVO>> timeline(@RequestParam String userId,
+                                                   @RequestParam Long lastMomentId,
+                                                   @RequestParam Integer size);
 }

+ 13 - 0
webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentController.java

@@ -3,12 +3,16 @@ package com.webchat.ugc.controller;
 import com.webchat.common.bean.APIResponseBean;
 import com.webchat.common.bean.APIResponseBeanUtil;
 import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
 import com.webchat.rmi.ugc.MomentClient;
 import com.webchat.ugc.service.moment.MomentService;
+import com.webchat.ugc.service.moment.MomentTimeLineService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 
 @RestController
 public class MomentController implements MomentClient {
@@ -17,10 +21,19 @@ public class MomentController implements MomentClient {
     @Autowired
     private MomentService momentService;
 
+    @Autowired
+    private MomentTimeLineService momentTimeLineService;
+
     @Override
     public APIResponseBean<Long> publish(@RequestBody MomentSaveOrUpdateVO momentSaveOrUpdate) {
 
         Long momentId = momentService.publish(momentSaveOrUpdate);
         return APIResponseBeanUtil.success(momentId);
     }
+
+    @Override
+    public APIResponseBean<List<MomentDetailVO>> timeline(String userId, Long lastMomentId, Integer size) {
+        List<MomentDetailVO> timeline = momentTimeLineService.timeline(userId, lastMomentId, size);
+        return APIResponseBeanUtil.success(timeline);
+    }
 }

+ 0 - 26
webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentTimeLineController.java

@@ -1,26 +0,0 @@
-package com.webchat.ugc.controller;
-
-
-import com.webchat.common.bean.APIResponseBean;
-import com.webchat.common.bean.APIResponseBeanUtil;
-import com.webchat.ugc.service.moment.MomentTimeLineService;
-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.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/ugc-service/moment/timeline")
-public class MomentTimeLineController {
-
-
-    @Autowired
-    private MomentTimeLineService momentTimeLineService;
-
-    @GetMapping("/save/{userId}")
-    public APIResponseBean<Boolean> save(@PathVariable String userId) {
-        momentTimeLineService.save(userId);
-        return APIResponseBeanUtil.success();
-    }
-}

+ 6 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentTimeLineDAO.java

@@ -5,7 +5,13 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.stereotype.Repository;
 
+import java.util.Date;
+import java.util.List;
+
 @Repository
 public interface IMomentTimeLineDAO extends JpaSpecificationExecutor<MomentTimeLineEntity>,
                                             JpaRepository<MomentTimeLineEntity, Long> {
+
+
+    List<MomentTimeLineEntity> findAllByUserIdAndTimeLineAfter(String userId, Date afterDate);
 }

+ 1 - 1
webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentService.java

@@ -237,7 +237,7 @@ public class MomentService {
             moment = new MomentEntity();
             moment.setStatus(MomentConstants.MomentStatusEnum.NEW.getStatus());
             moment.setAuthor(momentSaveOrUpdate.getAuthor());
-            moment.setIp(momentSaveOrUpdate.getClientIp());
+            moment.setIp(momentSaveOrUpdate.getIp());
             moment.setCreateDate(new Date());
         }
         moment.setContent(momentSaveOrUpdate.getContent());

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

@@ -1,18 +1,28 @@
 package com.webchat.ugc.service.moment;
 
+import com.webchat.common.constants.WebConstant;
 import com.webchat.common.enums.AccountRelationTypeEnum;
 import com.webchat.common.enums.RedisKeyEnum;
 import com.webchat.common.service.RedisService;
+import com.webchat.common.util.DateUtils;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
+import com.webchat.domain.vo.response.moment.MomentVO;
 import com.webchat.ugc.repository.dao.IMomentTimeLineDAO;
 import com.webchat.ugc.repository.entity.MomentTimeLineEntity;
 import com.webchat.ugc.service.AccountService;
 import org.apache.commons.collections.CollectionUtils;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.DefaultTypedTuple;
+import org.springframework.data.redis.core.ZSetOperations;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -20,14 +30,14 @@ import java.util.stream.Collectors;
 @Service
 public class MomentTimeLineService {
 
-
     @Autowired
     private IMomentTimeLineDAO momentTimeLineDAO;
     @Autowired
     private RedisService redisService;
     @Autowired
     private AccountService accountService;
-
+    @Autowired
+    private MomentService momentService;
 
     /**
      * TODO  数据一致性保证
@@ -48,6 +58,9 @@ public class MomentTimeLineService {
 
         // TODO BY策略过滤不需要写入的用户
 
+        /**
+         * 写扩散:双写 MySQL + 冷备
+         */
         Date publishDate = new Date(time);
         // 持计划话朋友圈数据
         List<MomentTimeLineEntity> momentTimeLineEntities = friends.stream().map(u -> {
@@ -59,17 +72,136 @@ public class MomentTimeLineService {
         }).collect(Collectors.toList());
         this.saveMomentTimeLine(momentTimeLineEntities);
 
+        // TODO 这里需要写冷备
+
         // 写时间线缓存
-        this.saveMomentTimeLineCache(friends, momentId, time);
+        this.saveMomentTimeLineCache(friends, momentId);
+    }
+
+
+    /**
+     * 朋友圈数据加载
+     *
+     * @param userId
+     * @param lastMomentId
+     * @param size
+     * @return
+     */
+    public List<MomentDetailVO> timeline(String userId, Long lastMomentId, int size) {
+
+        String timeLineKey = this.userTimelineCacheKey(userId);
+        String noneCacheKey = this.userTimelineNoneHotCacheKey(userId);
+        // 为什么这里需要noneCacheKey,原因为保证短时间只查询一次库
+        // 无热点缓存存在说明:我们MySQL无3天内热点数据,但是可能存在3天外的冷备数据
+        if (!redisService.exists(noneCacheKey) && !redisService.exists(timeLineKey)) {
+            // BY用户时间线查询,不存在并发场景,所以这里不需要加锁
+            this.refreshUserTimelineCache(timeLineKey);
+        }
+        Set<String> caches = redisService.zreverseRangeByScore(timeLineKey, lastMomentId - 1, 0, size);
+        if (caches.size() < size) {
+            // 说明redis缓存的数据不够显示
+            // 1、本身用户朋友圈数据就是很少
+            // 2、用户加载到中间页,redis3天内数据不够显示
+            // TODO 当前页数据不足{size}条,重新查询冷备(MongoDB)
+            caches = this.queryMomentsByMongoDB(userId, lastMomentId, size);
+        }
+        if (CollectionUtils.isEmpty(caches)) {
+            // mysql、redis、冷备都同时没查询到数据,说明数据已经加载完
+            return Collections.emptyList();
+        }
+        List<Long> momentIds = caches.stream().map(Long::parseLong).collect(Collectors.toList());
+        List<MomentVO> momentVOList = momentService.batchGetMomentFromCache(momentIds);
+        Set<String> authorIds = momentVOList.stream().map(MomentVO::getAuthor).collect(Collectors.toSet());
+        Map<String, UserBaseResponseInfoVO> authorInfoMap = accountService.batchGet(authorIds);
+
+        // TODO RPC 请求webchat-act互动服务获取用户资源互动状态、资源互动数据
+
+        return momentVOList.stream().map(m -> {
+            MomentDetailVO momentDetail = new MomentDetailVO();
+            BeanUtils.copyProperties(m, momentDetail);
+            momentDetail.setAuthorInfo(authorInfoMap.get(m.getAuthor()));
+            // TODO RPC 请求webchat-act 基础互动服务
+            momentDetail.setLikeCount(0L);
+            momentDetail.setIsLike(false);
+            momentDetail.setCommentCount(0L);
+            return momentDetail;
+        }).collect(Collectors.toList());
+    }
+
+    private Set<String> queryMomentsByMongoDB(String userId, Long lastMomentId, int size) {
+        // TODO
+        return null;
+    }
+
+    private String userTimelineCacheKey(String userId) {
+
+        return RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getKey(userId);
     }
 
-    private void saveMomentTimeLineCache(Set<String> friends, Long momentId, Long time) {
+    private String userTimelineNoneHotCacheKey(String userId) {
+
+        return RedisKeyEnum.MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE.getKey(userId);
+    }
+
+    /**
+     * 删除用户无热点数据标识
+     *
+     */
+    private void delUserTimelineNoneHotCache(Set<String> userIds) {
+
+        List<String> noneCacheKeys = userIds.stream().map(this::userTimelineNoneHotCacheKey).collect(Collectors.toList());
+        redisService.removeAll(noneCacheKeys);
+    }
+
+    /**
+     * 刷新用户朋友圈时间线缓存
+     *
+     * @param userId
+     */
+    private void refreshUserTimelineCache(String userId) {
+
+        // 计算3天前时间
+        Date afterDate = DateUtils.getBeforeNDayDate(3);
+        List<MomentTimeLineEntity> momentTimeLineEntities = momentTimeLineDAO.findAllByUserIdAndTimeLineAfter(userId, afterDate);
+        if (CollectionUtils.isNotEmpty(momentTimeLineEntities)) {
+            // 刷新时间线redis
+            Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
+            for (MomentTimeLineEntity momentTimeLine : momentTimeLineEntities) {
+                String value = momentTimeLine.getMomentId().toString();
+                double score = momentTimeLine.getMomentId();
+                ZSetOperations.TypedTuple<String> tuple = new DefaultTypedTuple<>(value, score);
+                typedTupleSet.add(tuple);
+            }
+            String timeLineKey = this.userTimelineCacheKey(userId);
+            redisService.zadd(timeLineKey, typedTupleSet, RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getExpireTime());
+        } else {
+            String noneHotKey = RedisKeyEnum.MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE.getKey(userId);
+            redisService.set(noneHotKey, WebConstant.CACHE_NONE, RedisKeyEnum.MOMENT_TIME_LINE_NONE_HOT_DATE_CACHE.getExpireTime());
+        }
+    }
+
+    /**
+     * 写入用户朋友圈时间线缓存
+     * 注意:这里redis sorted set中的score我们没有使用动态发布时间戳/写入时间线的时间戳,
+     * 主要是避免并发场景下score相同导致用户端分页查询可能出现重复数据、丢数据问题。所以我们使用唯一动态id作为,同时保证顺序性。
+     *
+     * @param friends
+     * @param momentId
+     */
+    private void saveMomentTimeLineCache(Set<String> friends, Long momentId) {
 
         List<String> keys = friends.stream().map( u ->
                 RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getKey(u)).collect(Collectors.toList());
-        keys.forEach(key ->
-            redisService.zadd(key, String.valueOf(momentId), time,
-                         RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getExpireTime()));
+        keys.forEach(key -> {
+            if (!redisService.exists(key)) {
+                // 主动刷新用户朋友圈时间线缓存(3天热点数据)
+                // TODO
+            }
+            redisService.zadd(key, String.valueOf(momentId), momentId,
+                    RedisKeyEnum.MOMENT_TIME_LINE_CACHE.getExpireTime());
+        });
+        // 删除用户无热点数据缓存
+        this.delUserTimelineNoneHotCache(friends);
     }
 
     private void saveMomentTimeLine(List<MomentTimeLineEntity> momentTimeLineEntities) {