Selaa lähdekoodia

朋友圈Sharding-jdbc分表

wangqi49 1 viikko sitten
vanhempi
commit
9e4bf516dd
53 muutettua tiedostoa jossa 1751 lisäystä ja 16 poistoa
  1. 76 1
      resources/database-sql/webchat-ugc.sql
  2. 19 6
      resources/nacos-yaml/webchat-ugc-service-dev.yaml
  3. 28 0
      webchat-aigc/src/main/java/com/webchat/aigc/controller/LLMChatController.java
  4. 36 0
      webchat-aigc/src/main/java/com/webchat/aigc/llm/LlmChatService.java
  5. 41 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/controller/MomentController.java
  6. 32 0
      webchat-client-chat/src/main/java/com/webchat/client/chat/service/MomentService.java
  7. 37 0
      webchat-common/src/main/java/com/webchat/common/constants/MomentConstants.java
  8. 2 0
      webchat-common/src/main/java/com/webchat/common/enums/ClickEvent.java
  9. 2 0
      webchat-common/src/main/java/com/webchat/common/enums/PromptTemplateEnum.java
  10. 12 1
      webchat-common/src/main/java/com/webchat/common/enums/RedisKeyEnum.java
  11. 3 1
      webchat-common/src/main/java/com/webchat/common/enums/messagequeue/MessageQueueEnum.java
  12. 15 0
      webchat-domain/src/main/java/com/webchat/domain/dto/aigc/ChatCompletionMessageRequest.java
  13. 11 0
      webchat-domain/src/main/java/com/webchat/domain/dto/queue/MomentPublishMessageDTO.java
  14. 27 3
      webchat-domain/src/main/java/com/webchat/domain/vo/request/MomentSaveOrUpdateVO.java
  15. 14 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentDetailVO.java
  16. 13 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentLinkVO.java
  17. 15 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentMediaVO.java
  18. 35 0
      webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentVO.java
  19. 0 0
      webchat-gateway/src/main/resources/application.yml
  20. 18 0
      webchat-remote/src/main/java/com/webchat/rmi/aigc/LLMChatClient.java
  21. 21 0
      webchat-remote/src/main/java/com/webchat/rmi/ugc/MomentClient.java
  22. 9 3
      webchat-ugc/pom.xml
  23. 4 0
      webchat-ugc/src/main/java/com/webchat/ugc/WebchatUGCApplication.java
  24. 49 0
      webchat-ugc/src/main/java/com/webchat/ugc/config/shardingJdbc/CustomHashModShardingAlgorithm.java
  25. 26 0
      webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentController.java
  26. 26 0
      webchat-ugc/src/main/java/com/webchat/ugc/controller/MomentTimeLineController.java
  27. 56 0
      webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/consumer/redis/MomentPublishRedisMQConsumer.java
  28. 41 0
      webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/consumer/rocketmq/MomentPublishRocketMQConsumer.java
  29. 73 0
      webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/service/MomentPublishConsumeService.java
  30. 13 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentDAO.java
  31. 16 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentLinkDAO.java
  32. 20 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentMediaDAO.java
  33. 11 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentTimeLineDAO.java
  34. 92 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentEntity.java
  35. 44 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentLinkEntity.java
  36. 55 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentMediaEntity.java
  37. 31 0
      webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentTimeLineEntity.java
  38. 15 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/AccountService.java
  39. 22 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentImageHandler.java
  40. 22 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentIpAddressHandler.java
  41. 22 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentLinkHandler.java
  42. 16 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentPublishHandler.java
  43. 37 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentPublishHandlerChain.java
  44. 22 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentRefreshHandler.java
  45. 71 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentReviewHandler.java
  46. 22 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentVideoHandler.java
  47. 43 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentLinkService.java
  48. 36 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentMediaService.java
  49. 248 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentService.java
  50. 103 0
      webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentTimeLineService.java
  51. 1 1
      webchat-ugc/src/main/resources/application.yml
  52. 29 0
      webchat-ugc/src/main/resources/sharding-jdbc.yml
  53. 19 0
      webchat-ugc/src/main/resources/templates/ftl/MOMENT_REVIEW.ftl

+ 76 - 1
resources/database-sql/webchat-ugc.sql

@@ -51,4 +51,79 @@ CREATE TABLE webchat_ugc.`web_chat_red_packet_record` (
       PRIMARY KEY (`ID`),
       KEY `INDEX_RED_PACKET_ID` (`red_packet_id`),
       KEY `INDEX_USER_ID` (`user_id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包拆分记录明细表';
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='红包拆分记录明细表';
+
+
+-- webchat朋友圈动态核心数据表
+CREATE TABLE webchat_ugc.`web_chat_moment` (
+       `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+       `author` char(100) NOT NULL COMMENT '动态作者 ',
+       `content` varchar(300) DEFAULT NULL COMMENT '正文(纯文本)',
+       `status` int(4) NOT NULL DEFAULT 1 COMMENT '状态',
+       `include_images` tinyint(1) DEFAULT 0 COMMENT '是否包含图片,冗余字段',
+       `include_video` tinyint(1) DEFAULT 0 COMMENT '是否包含图片,冗余字段',
+       `include_link` tinyint(1) DEFAULT 0 COMMENT '是否包含连接,冗余字段',
+       `ip_address` varchar(100) DEFAULT NULL COMMENT 'IP归属地',
+       `CREATE_DATE` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+       `UPDATE_BY` char(100) DEFAULT NULL COMMENT '更新人',
+       `UPDATE_DATE` datetime DEFAULT NULL COMMENT '更新时间',
+       PRIMARY KEY (`ID`),
+       KEY `INDEX_AUTHOR_STATUS` (`author`, `status`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='webchat朋友圈动态核心数据表';
+
+-- webchat朋友圈动态媒体资源表
+CREATE TABLE webchat_ugc.`web_chat_moment_media` (
+                                                     `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                     `moment_id` bigint unsigned NOT NULL COMMENT '动态ID',
+                                                     `type` int(4) NOT NULL COMMENT '资源类型 1图片 2视频',
+                                                     `resource` varchar(300) NOT NULL COMMENT '资源地址,对应OS存储资源URL',
+                                                     `size` bigint unsigned DEFAULT 0 COMMENT '资源大小',
+                                                     `width` int(6) DEFAULT 0 COMMENT '资源宽度',
+                                                     `height` int(6) DEFAULT 0 COMMENT '资源高度',
+                                                     PRIMARY KEY (`ID`),
+                                                     KEY `INDEX_MOMENT_ID_TYPE` (`moment_id`, `type`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='webchat朋友圈动态媒体资源表';
+
+-- webchat朋友圈动态分享链接表
+CREATE TABLE webchat_ugc.`web_chat_moment_link` (
+
+                                                    `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                    `moment_id` bigint unsigned NOT NULL COMMENT '动态ID',
+                                                    `resource` varchar(300) NOT NULL COMMENT '分享到朋友圈的url网络资源链接',
+                                                    `title` varchar(100) DEFAULT NULL COMMENT '链接解析标题',
+                                                    `cover` varchar(300) DEFAULT NULL COMMENT '链接解析封面图,webchat OS地址',
+                                                    PRIMARY KEY (`ID`),
+                                                    UNIQUE KEY `INDEX_MOMENT_ID` (`moment_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='webchat朋友圈动态分享链接表';
+
+
+-- 朋友圈动态时间线
+CREATE TABLE webchat_ugc.`web_chat_moment_timeline_0`(
+                                                         `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                         `user_id` char(100) NOT NULL COMMENT '谁的时间线',
+                                                         `moment_id` bigint unsigned NOT NULL COMMENT '动态id',
+                                                         `time_line` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间线时间点',
+                                                         PRIMARY KEY (`ID`),
+                                                         KEY `INDEX_USER_ID_TIME_LINE` (`user_id`, `time_line`),
+                                                         KEY `INDEX_MOMENT_ID` (`moment_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='webchat朋友圈时间线';
+
+CREATE TABLE webchat_ugc.`web_chat_moment_timeline_1`(
+                                                         `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                         `user_id` char(100) NOT NULL COMMENT '谁的时间线',
+                                                         `moment_id` bigint unsigned NOT NULL COMMENT '动态id',
+                                                         `time_line` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间线时间点',
+                                                         PRIMARY KEY (`ID`),
+                                                         KEY `INDEX_USER_ID_TIME_LINE` (`user_id`, `time_line`),
+                                                         KEY `INDEX_MOMENT_ID` (`moment_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='webchat朋友圈时间线';
+
+CREATE TABLE webchat_ugc.`web_chat_moment_timeline_2`(
+                                                         `ID` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID',
+                                                         `user_id` char(100) NOT NULL COMMENT '谁的时间线',
+                                                         `moment_id` bigint unsigned NOT NULL COMMENT '动态id',
+                                                         `time_line` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间线时间点',
+                                                         PRIMARY KEY (`ID`),
+                                                         KEY `INDEX_USER_ID_TIME_LINE` (`user_id`, `time_line`),
+                                                         KEY `INDEX_MOMENT_ID` (`moment_id`)
+) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='webchat朋友圈时间线';

+ 19 - 6
resources/nacos-yaml/webchat-ugc-service-dev.yaml

@@ -1,12 +1,9 @@
+
 #---------------------------------数据库配置----------------------------------#
 spring:
   datasource:
-    url: jdbc:mysql://127.0.0.1:3306/webchat_ugc?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: 50
+    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
+    url: jdbc:shardingsphere:classpath:sharding-jdbc.yml
   jpa:
     show-sql: true
   data:
@@ -27,3 +24,19 @@ rocketmq:
     group: web_chat
   producer:
     group: web_chat
+
+pay:
+  config:
+    app-id: 4
+    access-key: ak-fc014fcb2e3147b191c6a99af8454609
+    secret-key: gHTpgluRkz1HAOKuw3H5vSn4/A6kosoHKh4AFe2UNMk=
+
+message-card:
+  account:
+    payment: S_51a7fbf50155c4b08c55fbbcfc5911db
+  template:
+    red-packet-send: 284361050017501184
+    red-packet-open: 456
+
+red-packet:
+  open-with: LOCK

+ 28 - 0
webchat-aigc/src/main/java/com/webchat/aigc/controller/LLMChatController.java

@@ -0,0 +1,28 @@
+package com.webchat.aigc.controller;
+
+import com.webchat.aigc.llm.LlmChatService;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.domain.dto.aigc.ChatCompletionMessageRequest;
+import com.webchat.rmi.aigc.LLMChatClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RefreshScope
+@RestController
+public class LLMChatController implements LLMChatClient {
+
+
+    @Autowired
+    private LlmChatService llmChatService;
+
+    @Override
+    public APIResponseBean<String> chat(@RequestBody ChatCompletionMessageRequest chatCompletionMessage) {
+
+        String content = llmChatService.chat(chatCompletionMessage);
+        return APIResponseBeanUtil.success(content);
+    }
+}

+ 36 - 0
webchat-aigc/src/main/java/com/webchat/aigc/llm/LlmChatService.java

@@ -0,0 +1,36 @@
+package com.webchat.aigc.llm;
+
+
+import com.webchat.domain.dto.aigc.ChatCompletionMessageRequest;
+import com.webchat.domain.vo.llm.ChatCompletionMessage;
+import com.webchat.domain.vo.llm.ChatCompletionResponse;
+import com.webchat.domain.vo.llm.ChatMessageRole;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+public class LlmChatService {
+
+
+    @Value("${llm.config.model:deepseek}")
+    private String modal;
+
+
+    public String chat(ChatCompletionMessageRequest chatCompletionMessageRequest) {
+
+        final List<ChatCompletionMessage> messageList = Arrays.asList(
+                new ChatCompletionMessage(ChatMessageRole.USER.value(),
+                        chatCompletionMessageRequest.getPrompt()));
+        AbstractLLMChatService abstractLLMChatService = LLMServiceFactory.getLLMService(modal);
+        try {
+            ChatCompletionResponse chatCompletionResponse = abstractLLMChatService.chat(messageList);
+            return chatCompletionResponse.getChoices().get(0).getMessage().getContent();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

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

@@ -0,0 +1,41 @@
+package com.webchat.client.chat.controller;
+
+
+import com.webchat.client.chat.service.MomentService;
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+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 org.springframework.beans.factory.annotation.Autowired;
+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.RestController;
+
+@RestController
+@RequestMapping("/client-service/moment")
+public class MomentController {
+
+
+    @Autowired
+    private MomentService momentService;
+
+    /**
+     * 发布动态
+     *
+     * @return
+     */
+    @SafeClick(event = ClickEvent.PUBLISH_MOMENT, time = 2000, message = "动态发布中,请勿重复操作")
+    @PostMapping("/publish")
+    public APIResponseBean<Long> publish(@RequestBody MomentSaveOrUpdateVO momentSaveOrUpdate) {
+
+        momentSaveOrUpdate.validateRequestParam();
+
+        String author = SessionHelper.getCurrentUserId();
+        momentSaveOrUpdate.setAuthor(author);
+        Long momentId = momentService.publish(momentSaveOrUpdate);
+        return APIResponseBeanUtil.success(momentId);
+    }
+}

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

@@ -0,0 +1,32 @@
+package com.webchat.client.chat.service;
+
+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.rmi.ugc.MomentClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MomentService {
+
+
+    @Autowired
+    private MomentClient momentClient;
+
+    /**
+     * 发布动态
+     * @param momentSaveOrUpdateVO
+     * @return
+     */
+    public Long publish(MomentSaveOrUpdateVO momentSaveOrUpdateVO) {
+
+        // RPC 请求UGC微服务实现朋友圈动态发布核心流程
+        APIResponseBean<Long> responseBean = momentClient.publish(momentSaveOrUpdateVO);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        throw new BusinessException(responseBean.getMsg());
+    }
+}

+ 37 - 0
webchat-common/src/main/java/com/webchat/common/constants/MomentConstants.java

@@ -0,0 +1,37 @@
+package com.webchat.common.constants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+public class MomentConstants {
+
+
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum MomentStatusEnum {
+
+        NEW(1, "新建动态"),
+        REVIEW(2, "内容审核中..."),
+        PUBLISHED(3, "已发布"),
+        REJECT(4, "审核被拒绝");
+
+        private int status;
+        private String statusName;
+    }
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum MediaType {
+
+        IMAGE(1, "图片"),
+
+        VIDEO(2, "视频");
+
+        private int type;
+        private String typeName;
+    }
+
+}

+ 2 - 0
webchat-common/src/main/java/com/webchat/common/enums/ClickEvent.java

@@ -31,6 +31,8 @@ public enum ClickEvent {
 
     CREATE_GROUP("创建群聊"),
 
+    PUBLISH_MOMENT("朋友圈动态发布"),
+
     ;
 
     private String actionName;

+ 2 - 0
webchat-common/src/main/java/com/webchat/common/enums/PromptTemplateEnum.java

@@ -20,6 +20,8 @@ public enum PromptTemplateEnum {
     ROBOT_FC("/ftl/ROBOT_FC.ftl", "大模型意图识别"),
 
     RAG("/ftl/RAG.ftl", "公众号文章RAG问答"),
+
+    MOMENT_REVIEW("/ftl/MOMENT_REVIEW.ftl", "朋友圈动态内容审核"),
     ;
 
     private String path;

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

@@ -157,7 +157,7 @@ public enum RedisKeyEnum {
     /**
      * 朋友圈动态列表
      */
-    MOMENT_TIME_LINE_CACHE("MOMENT_TIME_LINE_CACHE", -1L),
+    MOMENT_TIME_LINE_CACHE("MOMENT_TIME_LINE_CACHE", 3 * 24 * 60 * 60L),
 
     /**
      * 外显评论
@@ -341,6 +341,17 @@ public enum RedisKeyEnum {
      * 红包金额预生成金额队列
      */
     QUEUE_RED_PACKET_AMOUNT("QUEUE_RED_PACKET_AMOUNT",  25 * 60 * 60L),
+
+    /**
+     * 朋友圈动态基础详情缓存
+     */
+    MOMENT_BASE_CACHE("MOMENT_BASE_CACHE", 3 * 24 * 60 * 60L),
+
+    /**
+     * 动态详情缓存刷新分布式锁
+     */
+    MOMENT_CACHE_REFRESH_LOCK("MOMENT_CACHE_REFRESH_LOCK",  10L),
+
     ;
 
 

+ 3 - 1
webchat-common/src/main/java/com/webchat/common/enums/messagequeue/MessageQueueEnum.java

@@ -19,7 +19,9 @@ public enum MessageQueueEnum {
 
     QUEUE_CHAT_VIDEO_P2P("queue_chat_video_p2p", "P2P(一对一)音视频聊天信令消息队列"),
 
-    QUEUE_CHAT_VIDEO_MESH("queue_chat_video_mesh", "基于Mesh模式的多人音视频聊天信令消息队列");
+    QUEUE_CHAT_VIDEO_MESH("queue_chat_video_mesh", "基于Mesh模式的多人音视频聊天信令消息队列"),
+
+    QUEUE_MOMENT_PUBLISH("queue_moment_publish", "朋友圈动态发布消息队列");
 
     private String queue;
 

+ 15 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/aigc/ChatCompletionMessageRequest.java

@@ -0,0 +1,15 @@
+package com.webchat.domain.dto.aigc;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ChatCompletionMessageRequest {
+
+
+    private String prompt;
+}

+ 11 - 0
webchat-domain/src/main/java/com/webchat/domain/dto/queue/MomentPublishMessageDTO.java

@@ -0,0 +1,11 @@
+package com.webchat.domain.dto.queue;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.Data;
+
+@Data
+public class MomentPublishMessageDTO extends BaseQueueDTO {
+
+
+    private MomentVO moment;
+}

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

@@ -1,6 +1,8 @@
 package com.webchat.domain.vo.request;
 
 import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.util.CollectionUtils;
 
 import java.util.List;
 
@@ -24,7 +26,7 @@ public class MomentSaveOrUpdateVO {
      */
     private List<String> images;
 
-    /***
+    /**
      * 视频
      */
     private String video;
@@ -34,8 +36,30 @@ public class MomentSaveOrUpdateVO {
      */
     private String author;
 
-    /***
+    /**
+     * 链接分享
+     */
+    private String link;
+
+    /**
      * 客户端
      */
-    private String client;
+    private String clientIp;
+
+
+    public void validateRequestParam() {
+        // TODO
+    }
+
+    public boolean includeImage() {
+        return !CollectionUtils.isEmpty(images);
+    }
+
+    public boolean includeVideo() {
+        return StringUtils.isNotBlank(video);
+    }
+
+    public boolean includeLink() {
+        return StringUtils.isNotBlank(link);
+    }
 }

+ 14 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentDetailVO.java

@@ -0,0 +1,14 @@
+package com.webchat.domain.vo.response.moment;
+
+import lombok.Data;
+
+@Data
+public class MomentDetailVO extends MomentVO {
+
+    private Long likeCount;
+
+    private Boolean isLike;
+
+    private Long commentCount;
+
+}

+ 13 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentLinkVO.java

@@ -0,0 +1,13 @@
+package com.webchat.domain.vo.response.moment;
+
+import lombok.Data;
+
+@Data
+public class MomentLinkVO {
+
+    private String resource;
+
+    private String title;
+
+    private String cover;
+}

+ 15 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/response/moment/MomentMediaVO.java

@@ -0,0 +1,15 @@
+package com.webchat.domain.vo.response.moment;
+
+import lombok.Data;
+
+@Data
+public class MomentMediaVO {
+
+    private String resource;
+
+    private Integer width;
+
+    private Integer height;
+
+    private long size;
+}

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

@@ -0,0 +1,35 @@
+package com.webchat.domain.vo.response.moment;
+
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class MomentVO {
+
+    private Long id;
+
+    private String author;
+
+    /**
+     * 作者
+     */
+    private UserBaseResponseInfoVO authorInfo;
+
+    private String content;
+
+    private List<MomentMediaVO> images;
+
+    private MomentMediaVO video;
+
+    private MomentLinkVO link;
+
+    private String ipAddress;
+
+    private Boolean includeImage;
+    private Boolean includeVideo;
+    private Boolean includeLink;
+
+    private long publishTime;
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
webchat-gateway/src/main/resources/application.yml


+ 18 - 0
webchat-remote/src/main/java/com/webchat/rmi/aigc/LLMChatClient.java

@@ -0,0 +1,18 @@
+package com.webchat.rmi.aigc;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.dto.aigc.ChatCompletionMessageRequest;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+@FeignClient(name = "webchat-aigc-service", contextId = "llmChatClient")
+public interface LLMChatClient {
+
+
+
+    @PostMapping("/aigc-service/llm/chat")
+    APIResponseBean<String> chat(@RequestBody ChatCompletionMessageRequest chatCompletionMessage);
+
+
+}

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

@@ -0,0 +1,21 @@
+package com.webchat.rmi.ugc;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+
+@FeignClient(name = "webchat-ugc-service", contextId = "momentClient")
+public interface MomentClient {
+
+    /**
+     * 动态发布API
+     *
+     * @param momentSaveOrUpdate
+     * @return
+     */
+    @PostMapping("/ugc-service/moment/publish")
+    APIResponseBean<Long> publish(@RequestBody MomentSaveOrUpdateVO momentSaveOrUpdate);
+}

+ 9 - 3
webchat-ugc/pom.xml

@@ -26,9 +26,9 @@
         </dependency>
         <!--引入mysql驱动-->
         <dependency>
-            <groupId>mysql</groupId>
-            <artifactId>mysql-connector-java</artifactId>
-            <version>5.1.46</version>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <version>8.0.33</version>
         </dependency>
         <!--使用JPA作为ORM框架 -->
         <dependency>
@@ -37,6 +37,12 @@
         </dependency>
 
         <dependency>
+            <groupId>org.apache.shardingsphere</groupId>
+            <artifactId>shardingsphere-jdbc</artifactId>
+            <version>5.5.2</version>
+        </dependency>
+
+        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>

+ 4 - 0
webchat-ugc/src/main/java/com/webchat/ugc/WebchatUGCApplication.java

@@ -1,10 +1,13 @@
 package com.webchat.ugc;
 
 import com.webchat.common.util.SpringContextUtil;
+import com.webchat.ugc.messaegqueue.consumer.redis.MomentPublishRedisMQConsumer;
 import com.webchat.ugc.messaegqueue.consumer.redis.PersistentMessageRedisMQConsumer;
 import com.webchat.ugc.messaegqueue.consumer.redis.RefreshChattingRedisMQConsumer;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.context.annotation.ComponentScan;
@@ -24,6 +27,7 @@ public class WebchatUGCApplication {
          */
         SpringContextUtil.getBean(RefreshChattingRedisMQConsumer.class).initBean();
         SpringContextUtil.getBean(PersistentMessageRedisMQConsumer.class).initBean();
+        SpringContextUtil.getBean(MomentPublishRedisMQConsumer.class).initBean();
     }
 
 }

+ 49 - 0
webchat-ugc/src/main/java/com/webchat/ugc/config/shardingJdbc/CustomHashModShardingAlgorithm.java

@@ -0,0 +1,49 @@
+package com.webchat.ugc.config.shardingJdbc;
+
+import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
+import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
+import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+public class CustomHashModShardingAlgorithm implements StandardShardingAlgorithm<String> {
+
+    private Properties props;
+
+    @Override
+    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
+        if (!shardingValue.getLogicTableName().equals("web_chat_moment_timeline")) {
+            return shardingValue.getLogicTableName(); // 非目标表直接返回原表
+        }
+
+        String user_id = shardingValue.getValue();
+        int hash = user_id.hashCode();
+        int mod = hash % Integer.parseInt(props.getProperty("sharding-count"));
+        // 处理负值
+        int index = (mod & 0x7FFFFFFF) % Integer.parseInt(props.getProperty("sharding-count"));
+        String suffix = "_" + index;
+        return availableTargetNames.stream()
+                .filter(table -> table.endsWith(suffix))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("分片路由失败"));
+    }
+
+    @Override
+    public Collection<String> doSharding(Collection<String> collection,
+                                         RangeShardingValue<String> rangeShardingValue) {
+        return List.of();
+    }
+
+    @Override
+    public void init(Properties props) {
+        this.props = props;
+    }
+
+    @Override
+    public String getType() {
+        return "CUSTOM_HASH_MOD";
+    }
+}

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

@@ -0,0 +1,26 @@
+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.rmi.ugc.MomentClient;
+import com.webchat.ugc.service.moment.MomentService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+public class MomentController implements MomentClient {
+
+
+    @Autowired
+    private MomentService momentService;
+
+    @Override
+    public APIResponseBean<Long> publish(@RequestBody MomentSaveOrUpdateVO momentSaveOrUpdate) {
+
+        Long momentId = momentService.publish(momentSaveOrUpdate);
+        return APIResponseBeanUtil.success(momentId);
+    }
+}

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

@@ -0,0 +1,26 @@
+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();
+    }
+}

+ 56 - 0
webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/consumer/redis/MomentPublishRedisMQConsumer.java

@@ -0,0 +1,56 @@
+package com.webchat.ugc.messaegqueue.consumer.redis;
+
+import com.webchat.common.enums.messagequeue.MessageQueueEnum;
+import com.webchat.common.service.messagequeue.consumer.AbstractRedisQueueConsumer;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.queue.MomentPublishMessageDTO;
+import com.webchat.ugc.messaegqueue.service.MomentPublishConsumeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+@Component
+@Lazy(value = false)
+@Slf4j
+public class MomentPublishRedisMQConsumer extends AbstractRedisQueueConsumer<MomentPublishMessageDTO> {
+
+
+    @Autowired
+    private MomentPublishConsumeService momentPublishConsumeService;
+
+    public void initBean() {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                MomentPublishRedisMQConsumer.this.schedule();
+            }
+        }).start();
+    }
+
+    @Override
+    protected MomentPublishMessageDTO convert(String s) {
+
+        return JsonUtil.fromJson(s, MomentPublishMessageDTO.class);
+    }
+
+    @Override
+    protected MessageQueueEnum getMessageQueue() {
+
+        return MessageQueueEnum.QUEUE_MOMENT_PUBLISH;
+    }
+
+    @Override
+    protected void receive(MomentPublishMessageDTO data) {
+
+        /**
+         * 动态发布后置实现流程
+         */
+        momentPublishConsumeService.consume(data.getMoment());
+    }
+
+    @Override
+    protected void error(MomentPublishMessageDTO data, Exception ex) {
+        // TODO
+    }
+}

+ 41 - 0
webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/consumer/rocketmq/MomentPublishRocketMQConsumer.java

@@ -0,0 +1,41 @@
+package com.webchat.ugc.messaegqueue.consumer.rocketmq;
+
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.queue.MomentPublishMessageDTO;
+import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
+import com.webchat.ugc.messaegqueue.service.MomentPublishConsumeService;
+import com.webchat.ugc.messaegqueue.service.PersistentMessageService;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+
+/**
+ * 消息持久化RocketMQ队列
+ *
+ */
+@Component
+@RocketMQMessageListener(consumerGroup = "web_chat", topic = "queue_moment_publish")
+public class MomentPublishRocketMQConsumer implements RocketMQListener<String> {
+
+
+    @Autowired
+    private MomentPublishConsumeService momentPublishConsumeService;
+
+    /**
+     * 消息持久化
+     * @param message
+     */
+    @Override
+    public void onMessage(String message) {
+        System.out.println("Received message: " + message);
+        MomentPublishMessageDTO data = JsonUtil.fromJson(message, MomentPublishMessageDTO.class);
+        if (data == null) {
+            return;
+        }
+        // TODO
+        momentPublishConsumeService.consume(data.getMoment());
+    }
+}

+ 73 - 0
webchat-ugc/src/main/java/com/webchat/ugc/messaegqueue/service/MomentPublishConsumeService.java

@@ -0,0 +1,73 @@
+package com.webchat.ugc.messaegqueue.service;
+
+
+import com.google.common.collect.Lists;
+import com.webchat.domain.dto.queue.MomentPublishMessageDTO;
+import com.webchat.domain.vo.response.moment.MomentVO;
+import com.webchat.ugc.service.AccountService;
+import com.webchat.ugc.service.chain.MomentImageHandler;
+import com.webchat.ugc.service.chain.MomentIpAddressHandler;
+import com.webchat.ugc.service.chain.MomentLinkHandler;
+import com.webchat.ugc.service.chain.MomentPublishHandlerChain;
+import com.webchat.ugc.service.chain.MomentRefreshHandler;
+import com.webchat.ugc.service.chain.MomentReviewHandler;
+import com.webchat.ugc.service.chain.MomentVideoHandler;
+import com.webchat.ugc.service.moment.MomentTimeLineService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MomentPublishConsumeService {
+
+    @Autowired
+    private MomentImageHandler momentImageHandler;
+    @Autowired
+    private MomentVideoHandler momentVideoHandler;
+    @Autowired
+    private MomentLinkHandler momentLinkHandler;
+    @Autowired
+    private MomentReviewHandler momentReviewHandler;
+    @Autowired
+    private MomentRefreshHandler momentRefreshHandler;
+    @Autowired
+    private MomentIpAddressHandler momentIpAddressHandler;
+    @Autowired
+    private AccountService accountService;
+    @Autowired
+    private MomentTimeLineService momentTimeLineService;
+
+    /**
+     * 动态发布后置处理
+     * @param dto
+     */
+    public void consume(MomentVO dto) {
+
+        /**
+         * 后置动态数据处理链(应用责任链设计模式):
+         *
+         * 1. 图片媒体资源信息提取
+         * 2. 视频媒体资源信息提取
+         * 3. 连接解析
+         * 4. ip归属地解析
+         * 5. 大模型内容审核
+         * 6. 刷新/修改动态状态
+         * ……
+         */
+        /**
+         * 动态发布后置处理任务链
+         */
+        MomentPublishHandlerChain chain = new MomentPublishHandlerChain();
+        chain.addHandler(Lists.newArrayList(momentImageHandler,
+                                            momentVideoHandler,
+                                            momentLinkHandler,
+                                            momentIpAddressHandler,
+                                            momentReviewHandler,
+                                            momentRefreshHandler)).handle(dto, chain);
+
+        /**
+         * 写扩散(把当前动态写入到所有粉丝时间线DB、Redis)
+         */
+        momentTimeLineService.addTimeLine(dto.getAuthor(), dto.getId(), dto.getPublishTime());
+    }
+
+}

+ 13 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentDAO.java

@@ -0,0 +1,13 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.MomentEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface IMomentDAO  extends JpaSpecificationExecutor<MomentEntity>,
+        JpaRepository<MomentEntity, Long> {
+
+}

+ 16 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/dao/IMomentLinkDAO.java

@@ -0,0 +1,16 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.MomentLinkEntity;
+import com.webchat.ugc.repository.entity.RedPacketEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+
+@Repository
+public interface IMomentLinkDAO extends JpaSpecificationExecutor<MomentLinkEntity>,
+        JpaRepository<MomentLinkEntity, Long> {
+
+
+    MomentLinkEntity findAllByMomentId(Long momentId);
+}

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

@@ -0,0 +1,20 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.MomentMediaEntity;
+import com.webchat.ugc.repository.entity.RedPacketEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+
+@Repository
+public interface IMomentMediaDAO extends JpaSpecificationExecutor<MomentMediaEntity>,
+        JpaRepository<MomentMediaEntity, Long> {
+
+
+
+    List<MomentMediaEntity> findAllByMomentIdAndType(Long momentId, Integer type);
+
+}

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

@@ -0,0 +1,11 @@
+package com.webchat.ugc.repository.dao;
+
+import com.webchat.ugc.repository.entity.MomentTimeLineEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface IMomentTimeLineDAO extends JpaSpecificationExecutor<MomentTimeLineEntity>,
+                                            JpaRepository<MomentTimeLineEntity, Long> {
+}

+ 92 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentEntity.java

@@ -0,0 +1,92 @@
+package com.webchat.ugc.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_moment")
+public class MomentEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 动态作者
+     */
+    @Column(name = "author", nullable = false, length = 100)
+    private String author;
+
+    /**
+     * 正文(纯文本)
+     */
+    @Column(name = "content", length = 300)
+    private String content;
+
+    /**
+     * 状态(1-正常)
+     */
+    @Column(name = "status", nullable = false)
+    private int status = 1;
+
+    /**
+     * 是否包含图片(true-包含)
+     */
+    @Column(name = "include_images")
+    private boolean includeImages;
+
+    /**
+     * 是否包含视频(true-包含)
+     */
+    @Column(name = "include_video")
+    private boolean includeVideo;
+
+    /**
+     * 是否包含链接(true-包含)
+     */
+    @Column(name = "include_link")
+    private boolean includeLink;
+
+    /**
+     * IP归属地
+     */
+    @Column(name = "ip_address", length = 100)
+    private String ipAddress;
+
+    @Column(name = "create_date")
+    private Date createDate;
+
+    @Column(name = "update_by")
+    private String updateBy;
+
+    @Column(name = "update_date")
+    private Date updateDate;
+
+    @PreUpdate
+    public void preUpdate() {
+        this.updateDate = new Date();
+    }
+
+    @PrePersist
+    public void prePersist() {
+        Date now = new Date();
+        if (this.createDate == null) {
+            this.createDate = now;
+        }
+    }
+}

+ 44 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentLinkEntity.java

@@ -0,0 +1,44 @@
+package com.webchat.ugc.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;
+
+// 朋友圈动态分享链接Entity
+@Data
+@Entity
+@Table(name = "web_chat_moment_link")
+public class MomentLinkEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 动态ID
+     */
+    @Column(name = "moment_id", nullable = false)
+    private Long momentId;
+
+    /**
+     * 分享链接URL
+     */
+    @Column(name = "resource", nullable = false, length = 300)
+    private String resource;
+
+    /**
+     * 链接标题
+     */
+    @Column(name = "title", length = 100)
+    private String title;
+
+    /**
+     * 封面图地址
+     */
+    @Column(name = "cover", length = 300)
+    private String cover;
+}

+ 55 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentMediaEntity.java

@@ -0,0 +1,55 @@
+package com.webchat.ugc.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;
+
+@Data
+@Entity
+@Table(name = "web_chat_moment_media")
+public class MomentMediaEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    /**
+     * 动态ID
+     */
+    @Column(name = "moment_id", nullable = false)
+    private Long momentId;
+
+    /**
+     * 资源类型(1-图片 2-视频)
+     */
+    @Column(name = "type", nullable = false)
+    private int type;
+
+    /**
+     * 资源地址
+     */
+    @Column(name = "resource", nullable = false, length = 300)
+    private String resource;
+
+    /**
+     * 资源大小(字节)
+     */
+    @Column(name = "size")
+    private long size = 0;
+
+    /**
+     * 资源宽度(像素)
+     */
+    @Column(name = "width")
+    private int width = 0;
+
+    /**
+     * 资源高度(像素)
+     */
+    @Column(name = "height")
+    private int height = 0;
+}

+ 31 - 0
webchat-ugc/src/main/java/com/webchat/ugc/repository/entity/MomentTimeLineEntity.java

@@ -0,0 +1,31 @@
+package com.webchat.ugc.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_moment_timeline")
+public class MomentTimeLineEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    protected Long id;
+
+    @Column(name = "user_id")
+    private String userId;
+
+    @Column(name = "moment_id")
+    private Long momentId;
+
+    @Column(name = "time_line")
+    private Date timeLine;
+}

+ 15 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/AccountService.java

@@ -83,6 +83,21 @@ public class AccountService {
     }
 
     /**
+     * 获取群组下的群成员用户id集合
+     *
+     * @return
+     */
+    public Set<String> getAllSubscriberByAccount(String account, AccountRelationTypeEnum accountRelationType) {
+        APIResponseBean<Set<String>> responseBean =
+                userServiceClient.getAllSubscriberByAccount(accountRelationType.getType(), account);
+        if (APIResponseBeanUtil.isOk(responseBean)) {
+            return responseBean.getData();
+        }
+        return null;
+    }
+
+
+    /**
      * 判断userAccount是否订阅account
      *
      * @param userAccount

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

@@ -0,0 +1,22 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MomentImageHandler implements MomentPublishHandler {
+
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+
+
+
+
+        chain.handle(moment, chain);
+    }
+}

+ 22 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentIpAddressHandler.java

@@ -0,0 +1,22 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MomentIpAddressHandler implements MomentPublishHandler {
+
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+
+        
+
+
+        chain.handle(moment, chain);
+    }
+}

+ 22 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentLinkHandler.java

@@ -0,0 +1,22 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MomentLinkHandler implements MomentPublishHandler {
+
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+
+        
+
+
+        chain.handle(moment, chain);
+    }
+}

+ 16 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentPublishHandler.java

@@ -0,0 +1,16 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+
+public interface MomentPublishHandler {
+
+
+    /**
+     * 朋友圈动态发布后置处理
+     *
+     * @param moment
+     * @param chain
+     */
+    void handle(MomentVO moment, MomentPublishHandlerChain chain);
+
+}

+ 37 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentPublishHandlerChain.java

@@ -0,0 +1,37 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MomentPublishHandlerChain {
+
+    /**
+     * 管理动态后置处理任务责任链
+     */
+    public List<MomentPublishHandler> handlers = new ArrayList<>();
+
+    private int execIndex = 0;
+
+    public MomentPublishHandlerChain addHandler(MomentPublishHandler handler) {
+        this.handlers.add(handler);
+        return this;
+    }
+
+    public MomentPublishHandlerChain addHandler(List<MomentPublishHandler> handlers) {
+        this.handlers.addAll(handlers);
+        return this;
+    }
+
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+        if (execIndex == handlers.size()) {
+            // 跳出责任链任务
+            return;
+        }
+        MomentPublishHandler handler = handlers.get(execIndex++);
+        handler.handle(moment, chain);
+    }
+
+}

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

@@ -0,0 +1,22 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MomentRefreshHandler implements MomentPublishHandler {
+
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+
+        
+
+
+        chain.handle(moment, chain);
+    }
+}

+ 71 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentReviewHandler.java

@@ -0,0 +1,71 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.common.bean.APIResponseBean;
+import com.webchat.common.bean.APIResponseBeanUtil;
+import com.webchat.common.enums.PromptTemplateEnum;
+import com.webchat.common.service.FreeMarkEngineService;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.aigc.ChatCompletionMessageRequest;
+import com.webchat.domain.vo.response.moment.MomentVO;
+import com.webchat.rmi.aigc.LLMChatClient;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+@Slf4j
+@Component
+public class MomentReviewHandler implements MomentPublishHandler {
+
+
+    @Autowired
+    private FreeMarkEngineService freeMarkEngineService;
+
+    @Autowired
+    private LLMChatClient llmChatClient;
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+        // 这里我们仅支持对正文内容的审核
+        String content = moment.getContent();
+        if (StringUtils.isBlank(content)) {
+            return;
+        }
+
+        String templateName = PromptTemplateEnum.MOMENT_REVIEW.getPath();
+        Map<String, Object> vars = new HashMap<>();
+        vars.put("author", "程序员七七");
+        vars.put("input", content);
+        String prompt;
+        try {
+            prompt = freeMarkEngineService.getContentByTemplate(templateName, vars);
+        } catch (Exception e) {
+            log.error("[朋友圈内容审核异常] ===> prompt模版引擎模版渲染失败!vars:{}",
+                    JsonUtil.toJsonString(vars), e);
+            return;
+        }
+        ChatCompletionMessageRequest chatCompletionMessage = new ChatCompletionMessageRequest(prompt);
+        APIResponseBean<String> reviewResponse = llmChatClient.chat(chatCompletionMessage);
+        Integer reviewScore = null;
+        if (APIResponseBeanUtil.isOk(reviewResponse)) {
+            String response = reviewResponse.getData();
+            reviewScore = StringUtils.isNoneBlank(response) ? Integer.valueOf(response) : null;
+        } else {
+            log.error("[朋友圈内容审核异常] ===> LLM Chat Error!prompt:{}", prompt);
+            return;
+        }
+
+        log.info("[朋友圈内容审核结果] ===> prompt:{}, 内容质量分:{}", prompt, reviewScore);
+
+        if (reviewScore != null && reviewScore < 10) {
+            // 异常内容
+
+        }
+        chain.handle(moment, chain);
+    }
+}

+ 22 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/chain/MomentVideoHandler.java

@@ -0,0 +1,22 @@
+package com.webchat.ugc.service.chain;
+
+import com.webchat.domain.vo.response.moment.MomentVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+
+@Slf4j
+@Component
+public class MomentVideoHandler implements MomentPublishHandler {
+
+
+    @Override
+    public void handle(MomentVO moment, MomentPublishHandlerChain chain) {
+
+
+        
+
+
+        chain.handle(moment, chain);
+    }
+}

+ 43 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentLinkService.java

@@ -0,0 +1,43 @@
+package com.webchat.ugc.service.moment;
+
+
+import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.ugc.repository.dao.IMomentLinkDAO;
+import com.webchat.ugc.repository.dao.IMomentMediaDAO;
+import com.webchat.ugc.repository.entity.MomentLinkEntity;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MomentLinkService {
+
+
+    @Autowired
+    private IMomentLinkDAO momentLinkDAO;
+
+
+
+    public boolean saveMomentLink(Long momentId,
+                                  MomentSaveOrUpdateVO momentSaveOrUpdate) {
+        if (!momentSaveOrUpdate.includeLink()) {
+            return true;
+        }
+        String link = momentSaveOrUpdate.getLink();
+        MomentLinkEntity momentLink = momentLinkDAO.findById(momentId).orElse(null);
+        if (momentLink != null) {
+            momentLink.setResource(link);
+        } else {
+            momentLink = new MomentLinkEntity();
+            momentLink.setMomentId(momentId);
+            momentLink.setResource(link);
+        }
+        momentLinkDAO.save(momentLink);
+        return true;
+    }
+
+
+    public MomentLinkEntity getLink(Long momentId) {
+        return momentLinkDAO.findAllByMomentId(momentId);
+    }
+}

+ 36 - 0
webchat-ugc/src/main/java/com/webchat/ugc/service/moment/MomentMediaService.java

@@ -0,0 +1,36 @@
+package com.webchat.ugc.service.moment;
+
+
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class MomentMediaService {
+
+
+    @Autowired
+    private IMomentMediaDAO momentMediaDAO;
+
+
+
+    public boolean saveMomentMedia(Long momentId,
+                                   MomentSaveOrUpdateVO momentSaveOrUpdate) {
+
+
+
+        return false;
+    }
+
+    public List<MomentMediaEntity> medias(Long momentId, int type) {
+
+        return momentMediaDAO.findAllByMomentIdAndType(momentId, type);
+    }
+
+
+}

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

@@ -0,0 +1,248 @@
+package com.webchat.ugc.service.moment;
+
+
+import com.webchat.common.constants.MomentConstants;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.enums.messagequeue.MessageQueueEnum;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.common.service.RedisService;
+import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.domain.dto.queue.MomentPublishMessageDTO;
+import com.webchat.domain.vo.request.MomentSaveOrUpdateVO;
+import com.webchat.domain.vo.response.moment.MomentDetailVO;
+import com.webchat.domain.vo.response.moment.MomentLinkVO;
+import com.webchat.domain.vo.response.moment.MomentMediaVO;
+import com.webchat.domain.vo.response.moment.MomentVO;
+import com.webchat.ugc.repository.dao.IMomentDAO;
+import com.webchat.ugc.repository.entity.MomentEntity;
+import com.webchat.ugc.repository.entity.MomentLinkEntity;
+import com.webchat.ugc.repository.entity.MomentMediaEntity;
+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.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+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.List;
+import java.util.stream.Collectors;
+
+@Service
+public class MomentService {
+
+
+    @Autowired
+    private IMomentDAO momentDAO;
+    @Autowired
+    private MomentMediaService momentMediaService;
+    @Autowired
+    private MomentLinkService momentLinkService;
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private MessageQueueProducer<MomentPublishMessageDTO, Long> messageQueueProducer;
+    @Autowired
+    private RedissonClient redissonClient;
+
+
+    /**
+     * 朋友圈动态发布
+     *
+     * @param momentSaveOrUpdate
+     * @return
+     */
+    @Transactional
+    public Long publish(MomentSaveOrUpdateVO momentSaveOrUpdate) {
+
+        /**
+         * 1. 持久化
+         * 1.1 朋友圈动态数据持久化
+         * 1.2 动态资源持久化
+         * 1.3 链接数据持久化
+         */
+        MomentEntity moment = this.convert(momentSaveOrUpdate);
+        moment = momentDAO.save(moment);
+        Long momentId = moment.getId();
+        // 持久化图片、视频媒体资源数据
+        momentMediaService.saveMomentMedia(momentId, momentSaveOrUpdate);
+        // 持久化朋友圈动态连接数据
+        momentLinkService.saveMomentLink(momentId, momentSaveOrUpdate);
+        /**
+         * 2. 动态详情缓存(redis)
+         */
+        MomentVO momentVO = this.refreshMomentCache(momentId);
+        /**
+         * 3. mq
+         * 3.1
+         * 图片、视频媒体资源信息提取
+         * 连接解析
+         * ip归属地解析
+         * 3.2 大模型内容审核
+         * 3.3 刷新/修改动态状态
+         * 3.4 写扩散(把当前动态写入到所有粉丝时间线DB、Redis)
+         */
+        MomentPublishMessageDTO messageDTO = new MomentPublishMessageDTO();
+        messageDTO.setMoment(momentVO);
+        messageQueueProducer.send(MessageQueueEnum.QUEUE_MOMENT_PUBLISH, messageDTO);
+        return null;
+    }
+
+
+    /**
+     * 获取单条动态详情
+     *
+     * @param momentId
+     * @return
+     */
+    public MomentVO getMomentFromCache(Long momentId) {
+
+        String cacheKey = this.momentBaseCacheKey(momentId);
+        String cache = redisService.get(cacheKey);
+        if (StringUtils.isNotBlank(cache)) {
+            return JsonUtil.fromJson(cache, MomentVO.class);
+        }
+        // 主动刷新
+        String lockKey = RedisKeyEnum.MOMENT_CACHE_REFRESH_LOCK.getKey(String.valueOf(momentId));
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            lock.lock();
+            // 双重检查
+            if (StringUtils.isNotBlank(cache = redisService.get(cacheKey))) {
+                return JsonUtil.fromJson(cache, MomentVO.class);
+            }
+            return this.refreshMomentCache(momentId);
+        } catch (Exception e) {
+            throw new BusinessException("数据加载失败");
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * 批量查询动态详情
+     *
+     * @param momentIds
+     * @return
+     */
+    public List<MomentVO> batchGetMomentFromCache(List<Long> momentIds) {
+
+        if (CollectionUtils.isEmpty(momentIds)) {
+            return Collections.emptyList();
+        }
+
+        List<MomentVO> moments = new ArrayList<>();
+
+        List<String> keys = momentIds.stream().map(this::momentBaseCacheKey).collect(Collectors.toList());
+        List<String> caches = redisService.mget(keys);
+        for (int i = 0; i < momentIds.size(); i++) {
+            Long momentId = momentIds.get(i);
+            String cache = caches.get(i);
+            if (StringUtils.isNotBlank(cache)) {
+                moments.add(JsonUtil.fromJson(cache, MomentVO.class));
+            } else {
+                // TODO 这里可以优化,不建议在循环中查库刷新缓存,可能存在性能瓶颈
+                // 建议:这里批量收集一批缓存失败的ID,最后统一批量刷新
+                moments.add(this.refreshMomentCache(momentId));
+            }
+        }
+        return moments;
+    }
+
+
+    public MomentVO refreshMomentCache(Long momentId) {
+
+        MomentEntity moment = momentDAO.findById(momentId).orElse(null);
+        if (moment == null) {
+            return null;
+        }
+        List<MomentMediaEntity> medias = null;
+
+        if (moment.isIncludeImages()) {
+            medias = momentMediaService.medias(momentId, MomentConstants.MediaType.IMAGE.getType());
+        }
+        if (moment.isIncludeVideo()) {
+            medias = momentMediaService.medias(momentId, MomentConstants.MediaType.VIDEO.getType());
+        }
+        MomentLinkEntity link = null;
+        if (moment.isIncludeLink()) {
+            link = momentLinkService.getLink(momentId);
+        }
+        return this.refreshMomentCache(moment, medias, link);
+    }
+
+
+    private MomentVO refreshMomentCache(MomentEntity moment,
+                                        List<MomentMediaEntity> momentMedias,
+                                        MomentLinkEntity linkEntity) {
+        if (moment == null) {
+            return null;
+        }
+        MomentVO momentVO = this.convertVo(moment, momentMedias, linkEntity);
+        String momentCacheKey = this.momentBaseCacheKey(moment.getId());
+        redisService.set(momentCacheKey, JsonUtil.toJsonString(momentVO),
+                RedisKeyEnum.MOMENT_BASE_CACHE.getExpireTime());
+        return momentVO;
+    }
+
+    private MomentVO convertVo(MomentEntity moment,
+                               List<MomentMediaEntity> momentMedias,
+                               MomentLinkEntity linkEntity) {
+
+        MomentVO momentVO = new MomentVO();
+        BeanUtils.copyProperties(moment, momentVO);
+        momentVO.setPublishTime(moment.getCreateDate().getTime());
+        if (moment.isIncludeImages()) {
+            List<MomentMediaVO> imagesVo = momentMedias.stream().map(img -> {
+                MomentMediaVO image = new MomentMediaVO();
+                BeanUtils.copyProperties(img, image);
+                return image;
+            }).collect(Collectors.toList());
+            momentVO.setImages(imagesVo);
+        }
+        if (moment.isIncludeVideo()) {
+            MomentMediaEntity video = momentMedias.get(0);
+            MomentMediaVO videoVo = new MomentMediaVO();
+            BeanUtils.copyProperties(video, videoVo);
+            momentVO.setVideo(videoVo);
+        }
+        if (moment.isIncludeLink()) {
+            MomentLinkVO linkVO = new MomentLinkVO();
+            BeanUtils.copyProperties(linkEntity, linkVO);
+            momentVO.setLink(linkVO);
+        }
+        return momentVO;
+    }
+
+    private String momentBaseCacheKey(Long momentId) {
+
+        return RedisKeyEnum.MOMENT_BASE_CACHE.getKey(String.valueOf(momentId));
+    }
+
+    private MomentEntity convert(MomentSaveOrUpdateVO momentSaveOrUpdate) {
+        MomentEntity moment;
+        Long momentId = momentSaveOrUpdate.getId();
+        if (momentId != null) {
+            moment = momentDAO.findById(momentSaveOrUpdate.getId()).orElse(null);
+            Assert.notNull(moment, "动态不存在");
+            Assert.isTrue(ObjectUtils.equals(momentSaveOrUpdate.getAuthor(), moment.getAuthor()), "无操作权限");
+        } else {
+            moment = new MomentEntity();
+            moment.setStatus(MomentConstants.MomentStatusEnum.NEW.getStatus());
+            moment.setAuthor(momentSaveOrUpdate.getAuthor());
+            moment.setCreateDate(new Date());
+        }
+        moment.setContent(momentSaveOrUpdate.getContent());
+        moment.setIncludeImages(momentSaveOrUpdate.includeImage());
+        moment.setIncludeVideo(momentSaveOrUpdate.includeVideo());
+        moment.setIncludeLink(momentSaveOrUpdate.includeLink());
+        return moment;
+    }
+}

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

@@ -0,0 +1,103 @@
+package com.webchat.ugc.service.moment;
+
+import com.webchat.common.enums.AccountRelationTypeEnum;
+import com.webchat.common.enums.RedisKeyEnum;
+import com.webchat.common.service.RedisService;
+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.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+
+@Service
+public class MomentTimeLineService {
+
+
+    @Autowired
+    private IMomentTimeLineDAO momentTimeLineDAO;
+    @Autowired
+    private RedisService redisService;
+    @Autowired
+    private AccountService accountService;
+
+
+    /**
+     * TODO  数据一致性保证
+     *
+     * @param author
+     * @param momentId
+     * @param time
+     */
+    public void addTimeLine(String author, Long momentId, Long time) {
+
+        Set<String> friends = accountService.getAllSubscriberByAccount(author, AccountRelationTypeEnum.USER_USER);
+        if (CollectionUtils.isEmpty(friends)) {
+            return;
+        }
+
+        // TODO BY策略过滤不需要写入的用户
+
+        Date publishDate = new Date(time);
+        // 持计划话朋友圈数据
+        List<MomentTimeLineEntity> momentTimeLineEntities = friends.stream().map(u -> {
+            MomentTimeLineEntity momentTimeLineEntity = new MomentTimeLineEntity();
+            momentTimeLineEntity.setUserId(u);
+            momentTimeLineEntity.setMomentId(momentId);
+            momentTimeLineEntity.setTimeLine(publishDate);
+            return momentTimeLineEntity;
+        }).collect(Collectors.toList());
+        this.saveMomentTimeLine(momentTimeLineEntities);
+
+        // 写时间线缓存
+        this.saveMomentTimeLineCache(friends, momentId, time);
+    }
+
+    private void saveMomentTimeLineCache(Set<String> friends, Long momentId, Long time) {
+
+        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()));
+    }
+
+    private void saveMomentTimeLine(List<MomentTimeLineEntity> momentTimeLineEntities) {
+        if (CollectionUtils.isEmpty(momentTimeLineEntities)) {
+            return;
+        }
+        momentTimeLineDAO.saveAll(momentTimeLineEntities);
+    }
+
+
+    public void save(String userId) {
+        MomentTimeLineEntity entity = new MomentTimeLineEntity();
+
+        entity.setUserId(userId);
+        entity.setMomentId(1L);
+        entity.setTimeLine(new Date());
+        momentTimeLineDAO.save(entity);
+    }
+
+
+//    public static void main(String[] args) {
+//        String userId = "U_1bb8a10f2d2a45a4b075c20016f10cb8";
+//        int[] idxArr = new int[3];
+//        for (int i = 0; i < 1000; i++) {
+//
+//            userId += i;
+//            int idx = Math.abs(userId.hashCode()) % 3;
+//            idxArr[idx] = idxArr[idx] + 1;
+//        }
+//        System.out.println("0:" + idxArr[0]);
+//        System.out.println("1:" + idxArr[1]);
+//        System.out.println("2:" + idxArr[2]);
+//    }
+
+}

+ 1 - 1
webchat-ugc/src/main/resources/application.yml

@@ -1 +1 @@
-server:
  port: 8017
+server:
  port: 8017

+ 29 - 0
webchat-ugc/src/main/resources/sharding-jdbc.yml

@@ -0,0 +1,29 @@
+# 数据源配置:支持多数据源(即支持分库)
+dataSources:
+  ds_0:
+    driverClassName: com.mysql.cj.jdbc.Driver
+    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
+    jdbcUrl: jdbc:mysql://127.0.0.1:3306/webchat_ugc?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=true
+    username: root
+    password: 12345678
+# 分库分表规则配置
+rules:
+  - !SINGLE
+    defaultDataSource: ds_0
+    tables:
+      - ds_0.*  # 声明 ds_0 下所有表为单表
+  - !SHARDING
+    tables:
+      web_chat_moment_timeline:
+        actualDataNodes: ds_0.web_chat_moment_timeline_${0..2}
+        tableStrategy:
+          standard:
+            shardingColumn: user_id
+            shardingAlgorithmName: timeline_table_hash_mod
+    shardingAlgorithms:
+      timeline_table_hash_mod:
+        type: CLASS_BASED
+        props:
+          strategy: STANDARD
+          algorithmClassName: com.webchat.ugc.config.shardingJdbc.CustomHashModShardingAlgorithm
+          sharding-count: 3

+ 19 - 0
webchat-ugc/src/main/resources/templates/ftl/MOMENT_REVIEW.ftl

@@ -0,0 +1,19 @@
+# 任务简介
+你是朋友圈动态内容审核员,任务是识别用户发帖内容是否涉黄涉证、言语辱骂等不文明内容。
+审核结果通过评分0 ~ 100来体现,分数越高植绒越优质,分数越低大会概率越高。
+
+#参考内容
+- 比如涉黄涉证直接0分
+- 编程技术分享权重可以高一点,80以上
+- 负向内容不能超过50分
+
+#任务限制
+直接返回数字评分结果
+
+# 输出格式
+88
+
+# 用户输入
+${author} : ${input}
+
+# 输出:

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä