Browse Source

feat(nexo-model): 添加抖音配置和Dockerfile支持

- 新增Dockerfile用于模型服务容器化部署
- 添加DouYinConfig配置类支持抖音相关配置
- 更新DouYinUtils工具类添加路径参数支持
- 修改ABogus算法使用外部配置文件路径
- 更新Cookie配置为最新有效值
- 添加日志记录用于调试请求信息
- 新增websocket模块的日志配置文件
JX.Li 2 weeks ago
parent
commit
a07c18255b
22 changed files with 990 additions and 266 deletions
  1. 4 0
      nexo-api/nexo-api-module/pom.xml
  2. 8 6
      nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java
  3. 1 1
      nexo-api/nexo-api-system/src/main/java/com/nexo/system/api/domain/SysUser.java
  4. 42 0
      nexo-common/nexo-common-websocket/pom.xml
  5. 66 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/config/WebSocketConfig.java
  6. 26 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/config/properties/WebSocketProperties.java
  7. 29 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/constant/WebSocketConstants.java
  8. 27 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/dto/WebSocketMessageDto.java
  9. 126 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/handler/PlusWebSocketHandler.java
  10. 74 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/holder/WebSocketSessionHolder.java
  11. 58 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/interceptor/PlusWebSocketInterceptor.java
  12. 50 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/listener/WebSocketTopicListener.java
  13. 127 0
      nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/utils/WebSocketUtils.java
  14. 1 0
      nexo-common/nexo-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  15. 26 0
      nexo-common/nexo-common-websocket/src/main/resources/logback-plus.xml
  16. 1 0
      nexo-common/pom.xml
  17. 20 0
      nexo-example/nexo-model/Dockerfile
  18. 17 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/DouYinConfig.java
  19. 9 1
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java
  20. 254 254
      nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java
  21. 6 0
      nexo-example/nexo-stream-mq/pom.xml
  22. 18 4
      nexo-modules/nexo-job/src/main/java/com/nexo/job/service/SampleService.java

+ 4 - 0
nexo-api/nexo-api-module/pom.xml

@@ -26,6 +26,10 @@
             <groupId>com.nexo</groupId>
             <artifactId>nexo-common-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-context</artifactId>
+        </dependency>
 
     </dependencies>
 

File diff suppressed because it is too large
+ 8 - 6
nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java


+ 1 - 1
nexo-api/nexo-api-system/src/main/java/com/nexo/system/api/domain/SysUser.java

@@ -33,7 +33,7 @@ public class SysUser extends BaseEntity {
     /**
      * 用户ID
      */
-    @TableId(value = "user_id")
+    @TableId(value = "user_id", type = IdType.AUTO)
     private Long userId;
 
     /**

+ 42 - 0
nexo-common/nexo-common-websocket/pom.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.nexo</groupId>
+        <artifactId>nexo-common</artifactId>
+        <version>1.8.2</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nexo-common-websocket</artifactId>
+
+    <description>
+        nexo-common-websocket 模块
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-satoken</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-tomcat</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>

+ 66 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/config/WebSocketConfig.java

@@ -0,0 +1,66 @@
+package com.nexo.common.websocket.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.nexo.common.websocket.config.properties.WebSocketProperties;
+import com.nexo.common.websocket.handler.PlusWebSocketHandler;
+import com.nexo.common.websocket.interceptor.PlusWebSocketInterceptor;
+import com.nexo.common.websocket.listener.WebSocketTopicListener;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+/**
+ * WebSocket 配置
+ *
+ * @author zendwang
+ */
+@Slf4j
+@AutoConfiguration
+@ConditionalOnProperty(value = "websocket.enabled", havingValue = "true")
+@EnableConfigurationProperties(WebSocketProperties.class)
+@EnableWebSocket
+public class WebSocketConfig {
+
+    @Bean
+    public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor handshakeInterceptor,
+                                                   WebSocketHandler webSocketHandler, WebSocketProperties webSocketProperties) {
+        log.info("初始化WebSocket配置");
+        // 如果WebSocket的路径为空,则设置默认路径为 "/websocket"
+        if (StrUtil.isBlank(webSocketProperties.getPath())) {
+            webSocketProperties.setPath("/websocket");
+        }
+
+        // 如果允许跨域访问的地址为空,则设置为 "*",表示允许所有来源的跨域请求
+        if (StrUtil.isBlank(webSocketProperties.getAllowedOrigins())) {
+            webSocketProperties.setAllowedOrigins("*");
+        }
+
+        // 返回一个WebSocketConfigurer对象,用于配置WebSocket
+        return registry -> registry
+            // 添加WebSocket处理程序和拦截器到指定路径,设置允许的跨域来源
+            .addHandler(webSocketHandler, webSocketProperties.getPath())
+            .addInterceptors(handshakeInterceptor)
+            .setAllowedOrigins(webSocketProperties.getAllowedOrigins());
+    }
+
+    @Bean
+    public HandshakeInterceptor handshakeInterceptor() {
+        return new PlusWebSocketInterceptor();
+    }
+
+    @Bean
+    public WebSocketHandler webSocketHandler() {
+        return new PlusWebSocketHandler();
+    }
+
+    @Bean
+    public WebSocketTopicListener topicListener() {
+        return new WebSocketTopicListener();
+    }
+}

+ 26 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/config/properties/WebSocketProperties.java

@@ -0,0 +1,26 @@
+package com.nexo.common.websocket.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * WebSocket 配置项
+ *
+ * @author zendwang
+ */
+@ConfigurationProperties("websocket")
+@Data
+public class WebSocketProperties {
+
+    private Boolean enabled;
+
+    /**
+     * 路径
+     */
+    private String path;
+
+    /**
+     *  设置访问源地址
+     */
+    private String allowedOrigins;
+}

+ 29 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/constant/WebSocketConstants.java

@@ -0,0 +1,29 @@
+package com.nexo.common.websocket.constant;
+
+/**
+ * websocket的常量配置
+ *
+ * @author zendwang
+ */
+public interface WebSocketConstants {
+
+    /**
+     * websocketSession中的参数的key
+     */
+    String LOGIN_USER_KEY = "loginUser";
+
+    /**
+     * 订阅的频道
+     */
+    String WEB_SOCKET_TOPIC = "global:websocket";
+
+    /**
+     * 前端心跳检查的命令
+     */
+    String PING = "ping";
+
+    /**
+     * 服务端心跳恢复的字符串
+     */
+    String PONG = "pong";
+}

+ 27 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/dto/WebSocketMessageDto.java

@@ -0,0 +1,27 @@
+package com.nexo.common.websocket.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 消息的dto
+ *
+ * @author zendwang
+ */
+@Data
+public class WebSocketMessageDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 需要推送到的session key 列表
+     */
+    private List<Long> sessionKeys;
+
+    /**
+     * 需要发送的消息
+     */
+    private String message;
+}

+ 126 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/handler/PlusWebSocketHandler.java

@@ -0,0 +1,126 @@
+package com.nexo.common.websocket.handler;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.nexo.common.websocket.dto.WebSocketMessageDto;
+import com.nexo.common.websocket.holder.WebSocketSessionHolder;
+import com.nexo.common.websocket.utils.WebSocketUtils;
+import com.nexo.system.api.model.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.*;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import static com.nexo.common.websocket.constant.WebSocketConstants.LOGIN_USER_KEY;
+
+/**
+ * WebSocketHandler 实现类
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketHandler extends AbstractWebSocketHandler {
+
+    /**
+     * 连接成功后
+     */
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws IOException {
+        LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+        if (ObjectUtil.isNull(loginUser)) {
+            session.close(CloseStatus.BAD_DATA);
+            log.info("[connect] invalid token received. sessionId: {}", session.getId());
+            return;
+        }
+        WebSocketSessionHolder.addSession(loginUser.getUserId(), new ConcurrentWebSocketSessionDecorator(session, 10 * 1000, 64000));
+        log.info("[connect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
+    }
+
+    /**
+     * 处理接收到的文本消息
+     *
+     * @param session WebSocket会话
+     * @param message 接收到的文本消息
+     * @throws Exception 处理消息过程中可能抛出的异常
+     */
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+
+        log.info("[receive] sessionId: {},message: {}", session.getId(), message.getPayload());
+
+        // 从WebSocket会话中获取登录用户信息
+        LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+
+        // 创建WebSocket消息DTO对象
+        WebSocketMessageDto webSocketMessageDto = new WebSocketMessageDto();
+        webSocketMessageDto.setSessionKeys(Collections.singletonList(loginUser.getUserId()));
+        webSocketMessageDto.setMessage(message.getPayload());
+        WebSocketUtils.publishMessage(webSocketMessageDto);
+    }
+
+    /**
+     * 处理接收到的二进制消息
+     *
+     * @param session WebSocket会话
+     * @param message 接收到的二进制消息
+     * @throws Exception 处理消息过程中可能抛出的异常
+     */
+    @Override
+    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
+        super.handleBinaryMessage(session, message);
+    }
+
+    /**
+     * 处理接收到的Pong消息(心跳监测)
+     *
+     * @param session WebSocket会话
+     * @param message 接收到的Pong消息
+     * @throws Exception 处理消息过程中可能抛出的异常
+     */
+    @Override
+    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
+        WebSocketUtils.sendPongMessage(session);
+    }
+
+    /**
+     * 处理WebSocket传输错误
+     *
+     * @param session   WebSocket会话
+     * @param exception 发生的异常
+     * @throws Exception 处理过程中可能抛出的异常
+     */
+    @Override
+    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+        log.error("[transport error] sessionId: {} , exception:{}", session.getId(), exception.getMessage());
+    }
+
+    /**
+     * 在WebSocket连接关闭后执行清理操作
+     *
+     * @param session WebSocket会话
+     * @param status  关闭状态信息
+     */
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
+        LoginUser loginUser = (LoginUser) session.getAttributes().get(LOGIN_USER_KEY);
+        if (ObjectUtil.isNull(loginUser)) {
+            log.info("[disconnect] invalid token received. sessionId: {}", session.getId());
+            return;
+        }
+        WebSocketSessionHolder.removeSession(loginUser.getUserId());
+        log.info("[disconnect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
+    }
+
+    /**
+     * 指示处理程序是否支持接收部分消息
+     *
+     * @return 如果支持接收部分消息,则返回true;否则返回false
+     */
+    @Override
+    public boolean supportsPartialMessages() {
+        return false;
+    }
+
+}

+ 74 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/holder/WebSocketSessionHolder.java

@@ -0,0 +1,74 @@
+package com.nexo.common.websocket.holder;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocketSession 用于保存当前所有在线的会话信息
+ *
+ * @author zendwang
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketSessionHolder {
+
+    private static final Map<Long, WebSocketSession> USER_SESSION_MAP = new ConcurrentHashMap<>();
+
+    /**
+     * 将WebSocket会话添加到用户会话Map中
+     *
+     * @param sessionKey 会话键,用于检索会话
+     * @param session    要添加的WebSocket会话
+     */
+    public static void addSession(Long sessionKey, WebSocketSession session) {
+        removeSession(sessionKey);
+        USER_SESSION_MAP.put(sessionKey, session);
+    }
+
+    /**
+     * 从用户会话Map中移除指定会话键对应的WebSocket会话
+     *
+     * @param sessionKey 要移除的会话键
+     */
+    public static void removeSession(Long sessionKey) {
+        WebSocketSession session = USER_SESSION_MAP.remove(sessionKey);
+        try {
+            session.close(CloseStatus.BAD_DATA);
+        } catch (Exception ignored) {
+        }
+    }
+
+    /**
+     * 根据会话键从用户会话Map中获取WebSocket会话
+     *
+     * @param sessionKey 要获取的会话键
+     * @return 与给定会话键对应的WebSocket会话,如果不存在则返回null
+     */
+    public static WebSocketSession getSessions(Long sessionKey) {
+        return USER_SESSION_MAP.get(sessionKey);
+    }
+
+    /**
+     * 获取存储在用户会话Map中所有WebSocket会话的会话键集合
+     *
+     * @return 所有WebSocket会话的会话键集合
+     */
+    public static Set<Long> getSessionsAll() {
+        return USER_SESSION_MAP.keySet();
+    }
+
+    /**
+     * 检查给定的会话键是否存在于用户会话Map中
+     *
+     * @param sessionKey 要检查的会话键
+     * @return 如果存在对应的会话键,则返回true;否则返回false
+     */
+    public static Boolean existSession(Long sessionKey) {
+        return USER_SESSION_MAP.containsKey(sessionKey);
+    }
+}

+ 58 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/interceptor/PlusWebSocketInterceptor.java

@@ -0,0 +1,58 @@
+package com.nexo.common.websocket.interceptor;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import com.nexo.common.satoken.utils.LoginHelper;
+import com.nexo.system.api.model.LoginUser;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+import static com.nexo.common.websocket.constant.WebSocketConstants.LOGIN_USER_KEY;
+
+/**
+ * WebSocket握手请求的拦截器
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class PlusWebSocketInterceptor implements HandshakeInterceptor {
+
+    /**
+     * WebSocket握手之前执行的前置处理方法
+     *
+     * @param request    WebSocket握手请求
+     * @param response   WebSocket握手响应
+     * @param wsHandler  WebSocket处理程序
+     * @param attributes 与WebSocket会话关联的属性
+     * @return 如果允许握手继续进行,则返回true;否则返回false
+     */
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
+        try {
+            LoginUser loginUser = LoginHelper.getLoginUser();
+            attributes.put(LOGIN_USER_KEY, loginUser);
+            return true;
+        } catch (NotLoginException e) {
+            log.error("WebSocket 认证失败'{}',无法访问系统资源", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * WebSocket握手成功后执行的后置处理方法
+     *
+     * @param request   WebSocket握手请求
+     * @param response  WebSocket握手响应
+     * @param wsHandler WebSocket处理程序
+     * @param exception 握手过程中可能出现的异常
+     */
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+        // 在这个方法中可以执行一些握手成功后的后续处理逻辑,比如记录日志或者其他操作
+    }
+
+}

+ 50 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/listener/WebSocketTopicListener.java

@@ -0,0 +1,50 @@
+package com.nexo.common.websocket.listener;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.extern.slf4j.Slf4j;
+import com.nexo.common.websocket.holder.WebSocketSessionHolder;
+import com.nexo.common.websocket.utils.WebSocketUtils;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.Ordered;
+
+/**
+ * WebSocket 主题订阅监听器
+ *
+ * @author zendwang
+ */
+@Slf4j
+public class WebSocketTopicListener implements ApplicationRunner, Ordered {
+
+    /**
+     * 在Spring Boot应用程序启动时初始化WebSocket主题订阅监听器
+     *
+     * @param args 应用程序参数
+     * @throws Exception 初始化过程中可能抛出的异常
+     */
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        // 订阅WebSocket消息
+        WebSocketUtils.subscribeMessage((message) -> {
+            log.info("WebSocket主题订阅收到消息session keys={} message={}", message.getSessionKeys(), message.getMessage());
+            // 如果key不为空就按照key发消息 如果为空就群发
+            if (CollUtil.isNotEmpty(message.getSessionKeys())) {
+                message.getSessionKeys().forEach(key -> {
+                    if (WebSocketSessionHolder.existSession(key)) {
+                        WebSocketUtils.sendMessage(key, message.getMessage());
+                    }
+                });
+            } else {
+                WebSocketSessionHolder.getSessionsAll().forEach(key -> {
+                    WebSocketUtils.sendMessage(key, message.getMessage());
+                });
+            }
+        });
+        log.info("初始化WebSocket主题订阅监听器成功");
+    }
+
+    @Override
+    public int getOrder() {
+        return -1;
+    }
+}

+ 127 - 0
nexo-common/nexo-common-websocket/src/main/java/com/nexo/common/websocket/utils/WebSocketUtils.java

@@ -0,0 +1,127 @@
+package com.nexo.common.websocket.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import com.nexo.common.redis.utils.RedisUtils;
+import com.nexo.common.websocket.dto.WebSocketMessageDto;
+import com.nexo.common.websocket.holder.WebSocketSessionHolder;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.PongMessage;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static com.nexo.common.websocket.constant.WebSocketConstants.WEB_SOCKET_TOPIC;
+
+/**
+ * WebSocket工具类
+ *
+ * @author zendwang
+ */
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class WebSocketUtils {
+
+    /**
+     * 向指定的WebSocket会话发送消息
+     *
+     * @param sessionKey 要发送消息的用户id
+     * @param message    要发送的消息内容
+     */
+    public static void sendMessage(Long sessionKey, String message) {
+        WebSocketSession session = WebSocketSessionHolder.getSessions(sessionKey);
+        sendMessage(session, message);
+    }
+
+    /**
+     * 订阅WebSocket消息主题,并提供一个消费者函数来处理接收到的消息
+     *
+     * @param consumer 处理WebSocket消息的消费者函数
+     */
+    public static void subscribeMessage(Consumer<WebSocketMessageDto> consumer) {
+        RedisUtils.subscribe(WEB_SOCKET_TOPIC, WebSocketMessageDto.class, consumer);
+    }
+
+    /**
+     * 发布WebSocket订阅消息
+     *
+     * @param webSocketMessage 要发布的WebSocket消息对象
+     */
+    public static void publishMessage(WebSocketMessageDto webSocketMessage) {
+        List<Long> unsentSessionKeys = new ArrayList<>();
+        // 当前服务内session,直接发送消息
+        for (Long sessionKey : webSocketMessage.getSessionKeys()) {
+            if (WebSocketSessionHolder.existSession(sessionKey)) {
+                WebSocketUtils.sendMessage(sessionKey, webSocketMessage.getMessage());
+                continue;
+            }
+            unsentSessionKeys.add(sessionKey);
+        }
+        // 不在当前服务内session,发布订阅消息
+        if (CollUtil.isNotEmpty(unsentSessionKeys)) {
+            WebSocketMessageDto broadcastMessage = new WebSocketMessageDto();
+            broadcastMessage.setMessage(webSocketMessage.getMessage());
+            broadcastMessage.setSessionKeys(unsentSessionKeys);
+            RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> {
+                log.info("WebSocket发送主题订阅消息topic:{} session keys:{} message:{}",
+                    WEB_SOCKET_TOPIC, unsentSessionKeys, webSocketMessage.getMessage());
+            });
+        }
+    }
+
+    /**
+     * 向所有的WebSocket会话发布订阅的消息(群发)
+     *
+     * @param message 要发布的消息内容
+     */
+    public static void publishAll(String message) {
+        WebSocketMessageDto broadcastMessage = new WebSocketMessageDto();
+        broadcastMessage.setMessage(message);
+        RedisUtils.publish(WEB_SOCKET_TOPIC, broadcastMessage, consumer -> {
+            log.info("WebSocket发送主题订阅消息topic:{} message:{}", WEB_SOCKET_TOPIC, message);
+        });
+    }
+
+    /**
+     * 向指定的WebSocket会话发送Pong消息
+     *
+     * @param session 要发送Pong消息的WebSocket会话
+     */
+    public static void sendPongMessage(WebSocketSession session) {
+        sendMessage(session, new PongMessage());
+    }
+
+    /**
+     * 向指定的WebSocket会话发送文本消息
+     *
+     * @param session WebSocket会话
+     * @param message 要发送的文本消息内容
+     */
+    public static void sendMessage(WebSocketSession session, String message) {
+        sendMessage(session, new TextMessage(message));
+    }
+
+    /**
+     * 向指定的WebSocket会话发送WebSocket消息对象
+     *
+     * @param session WebSocket会话
+     * @param message 要发送的WebSocket消息对象
+     */
+    private static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
+        if (session == null || !session.isOpen()) {
+            log.warn("[send] session会话已经关闭");
+        } else {
+            try {
+                session.sendMessage(message);
+            } catch (IOException e) {
+                log.error("[send] session({}) 发送消息({}) 异常", session, message, e);
+            }
+        }
+    }
+}

+ 1 - 0
nexo-common/nexo-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+com.nexo.common.websocket.config.WebSocketConfig

+ 26 - 0
nexo-common/nexo-common-websocket/src/main/resources/logback-plus.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="logs/${project.artifactId}" />
+   <!-- 日志输出格式 -->
+    <property name="console.log.pattern"
+              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36}%n) - %msg%n"/>
+
+    <!-- 控制台输出 -->
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${console.log.pattern}</pattern>
+            <charset>utf-8</charset>
+        </encoder>
+    </appender>
+
+    <include resource="logback-common.xml" />
+
+    <!-- 开启 skywalking 日志收集 -->
+    <include resource="logback-skylog.xml" />
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="console" />
+    </root>
+</configuration>

+ 1 - 0
nexo-common/pom.xml

@@ -36,6 +36,7 @@
         <module>nexo-common-prometheus</module>
         <module>nexo-common-translation</module>
         <module>nexo-common-encrypt</module>
+        <module>nexo-common-websocket</module>
     </modules>
 
     <artifactId>nexo-common</artifactId>

+ 20 - 0
nexo-example/nexo-model/Dockerfile

@@ -0,0 +1,20 @@
+FROM anapsix/alpine-java:8_server-jre_unlimited
+
+MAINTAINER Lion Li
+
+RUN mkdir -p /nexo/model/logs  \
+    /nexo/model/temp  \
+    /nexo/skywalking/agent
+
+WORKDIR /nexo/model
+
+ENV SERVER_PORT=9402
+
+EXPOSE ${SERVER_PORT}
+
+ENTRYPOINT ["java", \
+            "-Djava.security.egd=file:/dev/./urandom", \
+            "-Dserver.port=${SERVER_PORT}", \
+            "-Dfile.encoding=UTF-8", \
+            "-Duser.timezone=Asia/Shanghai", \
+            "-jar", "app.jar"]

+ 17 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/DouYinConfig.java

@@ -0,0 +1,17 @@
+package com.nexo.model.douyin.domain;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "douyin")
+public class DouYinConfig {
+
+    private String configPath;
+
+    private String cookies;
+}

+ 9 - 1
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java

@@ -7,6 +7,7 @@ import com.nexo.common.core.utils.OkHttpClientUtils;
 import com.nexo.common.core.utils.StringUtils;
 import com.nexo.common.mybatis.core.page.PageQuery;
 import com.nexo.common.mybatis.core.page.TableDataInfo;
+import com.nexo.common.redis.utils.RedisUtils;
 import com.nexo.common.satoken.utils.LoginHelper;
 import com.nexo.model.douyin.domain.UpdataStatusBo;
 import com.nexo.model.douyin.mapper.NexoDouyinUserInfoMapper;
@@ -15,6 +16,7 @@ import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 import com.nexo.module.api.douyin.utils.DouYinUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -37,6 +39,9 @@ public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService
 
     private final NexoDouyinUserInfoMapper baseMapper;
 
+    @Value("${spring.profiles.active}")
+    private String activeProfile;
+
     /**
      * 查询抖音用户信息
      */
@@ -111,7 +116,10 @@ public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService
             }
             return true;
         }
-        item = DouYinUtils.getUserInfo(sec_user_id);
+        log.info("activeProfile:{}", activeProfile);
+        log.info("douyin_config_dev:{}", RedisUtils.getCacheMapValue("sys_config", "douyin_config_dev").toString());
+        log.info("douyin_config_pord:{}", RedisUtils.getCacheMapValue("sys_config", "douyin_config_pord").toString());
+        item = DouYinUtils.getUserInfo(sec_user_id, !activeProfile.equals("prod") ? RedisUtils.getCacheMapValue("sys_config", "douyin_config_dev") : RedisUtils.getCacheMapValue("sys_config", "douyin_config_pord"));
         item.setDeptId(LoginHelper.getDeptId());
         Boolean flag = baseMapper.insert(item) > 0;
         return flag;

+ 254 - 254
nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java

@@ -1,254 +1,254 @@
-package com.nexo.model.douyin;
-
-import com.alibaba.fastjson.JSONArray;
-import com.alibaba.fastjson.JSONObject;
-import com.nexo.common.core.utils.OkHttpClientUtils;
-import com.nexo.common.core.utils.StringUtils;
-import com.nexo.module.api.douyin.utils.DouYinUtils;
-import lombok.extern.slf4j.Slf4j;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.util.HashMap;
-
-@Slf4j
-@SpringBootTest
-public class 抖音测试工具类 {
-
-    @Test
-    public void 单个作品解析() {
-        String 抖音视频链接 = "1.23 cAG:/ :0pm 12/29 A@G.IV p身材教程来啦~ # 剪辑教程  https://v.douyin.com/wssqLCJ9O4c/ 复制此链接,打开Dou音搜索,直接观看视频!";
-        String 抖音链接 = "5.66 :6pm 01/27 h@o.Qk mDh:/ 手机全屏壁纸。# 性感 # 完美身材  https://v.douyin.com/qDWqlDX8ouQ/ 复制此链接,打开Dou音搜索,直接观看视频!";
-        String string = DouYinUtils.getRegexString(抖音链接, "[a-zA-z]+://[^\\s]*", 0);
-        HashMap<String, String> hashMap = new HashMap<>();
-        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-        String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
-        String string1 = StringUtils.extractMiddleText(done, "\"", "\"");
-        String string2 = OkHttpClientUtils.doGetFollowRedirects(string1, hashMap);
-        String right = StringUtils.right(string2, "window._ROUTER_DATA = ");
-        String left = StringUtils.left(right, "</script>");
-        JSONObject jsonObject = JSONObject.parseObject(DouYinUtils.decodeUnicode(left));
-        JSONObject data = jsonObject.getJSONObject("loaderData").getJSONObject("video_(id)/page");
-        data = data.getJSONObject("videoInfoRes").getJSONArray("item_list").getJSONObject(0);
-        log.info("{}", data);
-        log.info("===================== 发布者 ========================");
-        String sec_uid = data.getJSONObject("author").getString("sec_uid");
-        log.info("sec_uid:{}", sec_uid);
-        String short_id = data.getJSONObject("author").getString("short_id");
-        log.info("short_id:{}", short_id);
-        String nickname = data.getJSONObject("author").getString("nickname");
-        log.info("nickname:{}", nickname);
-        String signature = data.getJSONObject("author").getString("signature");
-        log.info("signature:{}", signature);
-
-        log.info("===================== 信息 ========================");
-        if (data.getInteger("aweme_type") == 4) {
-            String play_addr = data.getJSONObject("video").getJSONObject("play_addr").getJSONArray("url_list").getString(0);
-            log.info("play_addr:{}", play_addr.replaceAll("playwm", "play"));
-            String cover = data.getJSONObject("video").getJSONObject("cover").getJSONArray("url_list").getString(0);
-            log.info("cover:{}", cover);
-        } else if (data.getInteger("aweme_type") == 2) {
-            for (int i = 0; i < data.getJSONArray("images").size(); i++) {
-                String imgs = data.getJSONArray("images").getJSONObject(i).getJSONArray("url_list").getString(0);
-                log.info("img[{}]:{}", i + 1, imgs);
-            }
-        }
-
-        String desc = data.getString("desc");
-        log.info("desc:{}", desc);
-        Integer comment_count = data.getJSONObject("statistics").getInteger("comment_count");
-        Integer share_count = data.getJSONObject("statistics").getInteger("share_count");
-        Integer digg_count = data.getJSONObject("statistics").getInteger("digg_count");
-        Integer collect_count = data.getJSONObject("statistics").getInteger("collect_count");
-        log.info("comment_count:{}", comment_count);
-        log.info("share_count:{}", share_count);
-        log.info("digg_count:{}", digg_count);
-        log.info("collect_count:{}", collect_count);
-    }
-
-    @Test
-    public void 获取用户作品列表() {
-        String 分享连接 = "0- 长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/iz6uY8RfrJ8/ 7@5.com :2pm";
-        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
-        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
-        String mid = null;
-        try {
-            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            throw new RuntimeException(e);
-        }
-        String maxcursor = "";
-        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 1);
-        log.info("sec_user_id:{}", sec_user_id);
-        Integer count = 0;
-        while (true) {
-            String params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=" + sec_user_id + "&max_cursor=" + maxcursor + "&locate_item_id=7388761148507639094&locate_query=false&show_live_replay_strategy=1&need_time_list=" + "0" + "&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&update_version_code=170400&pc_client_type=1&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1832&screen_height=314&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=126.0.0.0&browser_online=true&engine_name=Blink&engine_version=126.0.0.0&os_name=Android&os_version=6.0&cpu_core_num=12&device_memory=8&platform=Android&downlink=10&effective_type=4g&round_trip_time=150&webid=7347601222205687359&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&msToken=MIOLSj1Hic1rlNKxvhty424gjUhagKo0ti6PK1s9uFfs0keS0miQ0metd3ZljkX0KjkQU_3nmJZm8tuYhMjIC1jntmfmuTRAfcZEbz1UzWZVqMDrHgg%3D";
-            String aBogus = DouYinUtils.getABogus(params);
-            String url = "https://www.douyin.com/aweme/v1/web/aweme/post/?" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e";
-            HashMap<String, String> header = DouYinUtils.getHeader();
-            String reqStr = OkHttpClientUtils.doGet(url, null, header);
-            // 解析json
-            JSONObject reqJson = JSONObject.parseObject(reqStr);
-            maxcursor = reqJson.getString("max_cursor");
-            JSONArray awemeList = reqJson.getJSONArray("aweme_list");
-            count += awemeList.size();
-            log.info("maxcursor:{}", maxcursor);
-            log.info("aweme_list:{}", awemeList.size());
-            if (reqJson.getInteger("has_more") != 1) {
-                break;
-            }
-        }
-        log.info("count:{}", count);
-
-    }
-
-    @Test
-    public void 获取用户信息() {
-        String 分享连接 = "长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/";
-        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
-        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
-        String mid = null;
-        try {
-            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
-        } catch (UnsupportedEncodingException e) {
-            throw new RuntimeException(e);
-        }
-        log.info("mid:{}", mid);
-        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
-        sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
-        log.info("sec_user_id:{}", sec_user_id);
-        String params = "?device_platform=webapp&aid=6383&channel=channel_pc_web&publish_video_strategy_type=2&source=channel_pc_web&sec_user_id=" + sec_user_id + "&personal_center_strategy=1&profile_other_record_enable=1&land_to=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=0&support_dash=1&cpu_core_num=24&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=148.0.0.0&browser_online=true&engine_name=Blink&engine_version=148.0.0.0&os_name=Windows&os_version=10&device_memory=32&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7639313678160397875";
-        String aBogus = DouYinUtils.getABogus(params);
-        String url = "https://www.douyin.com/aweme/v1/web/user/profile/other/" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe&fp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe";
-        log.info("url:{}", url);
-        HashMap<String, String> header = DouYinUtils.getHeader();
-        String reqStr = OkHttpClientUtils.doGet(url, null, header);
-        // 解析json
-        JSONObject reqJson = JSONObject.parseObject(reqStr);
-        log.info("reqJson:{}", reqJson);
-
-    }
-
-    @Test
-    public void 获取抖音热榜() {
-        String url = "https://so-landing.douyin.com/aweme/v1/hot/search/list/?aid=581610&detail_list=1&board_type=0&board_sub_type=&need_board_tab=true&need_covid_tab=false&version_code=32.3.0 ";
-        String string = OkHttpClientUtils.doGet(url);
-        JSONObject jsonObject = JSONObject.parseObject(string);
-        JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("word_list");
-        for (int i = 0; i < jsonArray.size(); i++) {
-            JSONObject wordJson = jsonArray.getJSONObject(i);
-            if (wordJson.containsKey("group_id")) {
-                String url2 = null;
-                try {
-                    url2 = "https://so.douyin.com/s?is_from_mobile_home=1&search_entrance=aweme&enter_method=hot_list_page&innerWidth=430&innerHeight=932&reloadNavStart=1778841683198&is_no_width_reload=0&keyword=" + URLEncoder.encode(wordJson.getString("word"), "UTF-8") + "&gid=" + wordJson.getString("group_id");
-                } catch (UnsupportedEncodingException e) {
-                    throw new RuntimeException(e);
-                }
-                HashMap<String, String> hashMap = new HashMap<>();
-                hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-
-                String string2 = OkHttpClientUtils.doGet(url2, null, hashMap);
-                String substring = string2.substring(string2.indexOf("let data = {\"business_data\":") + "let data = {\"business_data\":".length(), string2.indexOf(",\"render_info\""));
-                JSONArray parsed = JSONObject.parseArray(substring);
-                JSONObject object = new JSONObject();
-                object.put("group_id", wordJson.getString("group_id"));
-                object.put("word", wordJson.getString("word"));
-                JSONArray array = new JSONArray();
-                for (int i1 = 0; i1 < parsed.size(); i1++) {
-                    String string1 = parsed.getJSONObject(i1).getJSONObject("data").getString("provider_doc_id_str");
-                    if (parsed.getJSONObject(i1).getInteger("type") == 1) {
-                        array.add("https://www.douyin.com/video/" + string1);
-                    }
-                }
-                object.put("list", array);
-                log.info("{}", object);
-            }
-
-        }
-
-    }
-
-    @Test
-    public void 获取直播视频流() {
-        String url = "https://live.douyin.com/660321581729";
-        HashMap<String, String> hashMap = new HashMap<>();
-        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-        String done = OkHttpClientUtils.doGet(url, null, hashMap);
-        String roomId = DouYinUtils.getRegexString(done, "roomId[^\\d]*(\\d{15,})", 1);
-        if (StringUtils.isEmpty(roomId)) {
-            throw new RuntimeException("未找到房间号");
-        }
-        String url2 = "https://webcast.amemv.com/webcast/room/reflow/info/?type_id=0&live_id=1&room_id=" + roomId + "&sec_user_id=&app_id=1128";
-        String string = OkHttpClientUtils.doGet(url2, null, hashMap);
-        JSONObject data = JSONObject.parseObject(string);
-        JSONObject stream = data.getJSONObject("data").getJSONObject("room").getJSONObject("stream_url");
-        JSONObject resolutionName = stream.getJSONObject("resolution_name");
-        log.info("========================== flv ==========================");
-        JSONObject flv_pull_url = stream.getJSONObject("flv_pull_url");
-        flv_pull_url.forEach((key, value) -> {
-            String resolution = resolutionName.getString(key);
-            log.info("{}:{}", resolution, value);
-        });
-
-        log.info("========================== hls ==========================");
-        JSONObject hls_pull_url_map = stream.getJSONObject("hls_pull_url_map");
-        hls_pull_url_map.forEach((key, value) -> {
-            String resolution = resolutionName.getString(key);
-            log.info("{}:{}", resolution, value);
-        });
-
-        log.info("========================== 主播信息 ==========================");
-
-        JSONObject owner = data.getJSONObject("data").getJSONObject("room").getJSONObject("owner");
-        String avatar = owner.getJSONObject("avatar_large").getJSONArray("url_list").getString(0);
-        log.info("头像:{}", avatar);
-        log.info("昵称:{}", owner.getString("nickname"));
-        log.info("签名:{}", owner.getString("signature"));
-        Integer following_count = owner.getJSONObject("follow_info").getInteger("following_count");
-        log.info("关注数:{}", following_count);
-        Integer follower_count = owner.getJSONObject("follow_info").getInteger("follower_count");
-        log.info("粉丝数:{}", follower_count);
-        log.info("SECUID:{}", owner.getString("sec_uid"));
-        log.info("抖音号:{}", owner.getString("display_id"));
-
-
-    }
-
-    @Test
-    public void 获取抖音弹幕() {
-        String liveId = "561090451047";
-        try {
-
-            // 设置工作目录
-            String workDir = "C:\\Users\\Administrator\\Downloads\\DouyinLiveWebFetcher-main";
-
-            // 构建命令
-            String command = "cmd /c start cmd /k \"python main.py --live_id=" + liveId + "\"";
-
-            System.out.println("执行命令: " + command);
-
-            // 使用 ProcessBuilder 执行命令
-            ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", command);
-            processBuilder.directory(new java.io.File(workDir));
-            // 合并错误流和输出流
-            processBuilder.redirectErrorStream(true);
-
-            // 启动进程
-            Process process = processBuilder.start();
-            // 等待进程自然结束
-            int exitCode = process.waitFor();
-            System.out.println("进程退出码: " + exitCode);
-
-
-        } catch (IOException e) {
-            e.printStackTrace();
-        } catch (InterruptedException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-}
+//package com.nexo.model.douyin;
+//
+//import com.alibaba.fastjson.JSONArray;
+//import com.alibaba.fastjson.JSONObject;
+//import com.nexo.common.core.utils.OkHttpClientUtils;
+//import com.nexo.common.core.utils.StringUtils;
+//import com.nexo.module.api.douyin.utils.DouYinUtils;
+//import lombok.extern.slf4j.Slf4j;
+//import org.junit.jupiter.api.Test;
+//import org.springframework.boot.test.context.SpringBootTest;
+//
+//import java.io.IOException;
+//import java.io.UnsupportedEncodingException;
+//import java.net.URLDecoder;
+//import java.net.URLEncoder;
+//import java.util.HashMap;
+//
+//@Slf4j
+//@SpringBootTest
+//public class 抖音测试工具类 {
+//
+//    @Test
+//    public void 单个作品解析() {
+//        String 抖音视频链接 = "1.23 cAG:/ :0pm 12/29 A@G.IV p身材教程来啦~ # 剪辑教程  https://v.douyin.com/wssqLCJ9O4c/ 复制此链接,打开Dou音搜索,直接观看视频!";
+//        String 抖音链接 = "5.66 :6pm 01/27 h@o.Qk mDh:/ 手机全屏壁纸。# 性感 # 完美身材  https://v.douyin.com/qDWqlDX8ouQ/ 复制此链接,打开Dou音搜索,直接观看视频!";
+//        String string = DouYinUtils.getRegexString(抖音链接, "[a-zA-z]+://[^\\s]*", 0);
+//        HashMap<String, String> hashMap = new HashMap<>();
+//        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+//        String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
+//        String string1 = StringUtils.extractMiddleText(done, "\"", "\"");
+//        String string2 = OkHttpClientUtils.doGetFollowRedirects(string1, hashMap);
+//        String right = StringUtils.right(string2, "window._ROUTER_DATA = ");
+//        String left = StringUtils.left(right, "</script>");
+//        JSONObject jsonObject = JSONObject.parseObject(DouYinUtils.decodeUnicode(left));
+//        JSONObject data = jsonObject.getJSONObject("loaderData").getJSONObject("video_(id)/page");
+//        data = data.getJSONObject("videoInfoRes").getJSONArray("item_list").getJSONObject(0);
+//        log.info("{}", data);
+//        log.info("===================== 发布者 ========================");
+//        String sec_uid = data.getJSONObject("author").getString("sec_uid");
+//        log.info("sec_uid:{}", sec_uid);
+//        String short_id = data.getJSONObject("author").getString("short_id");
+//        log.info("short_id:{}", short_id);
+//        String nickname = data.getJSONObject("author").getString("nickname");
+//        log.info("nickname:{}", nickname);
+//        String signature = data.getJSONObject("author").getString("signature");
+//        log.info("signature:{}", signature);
+//
+//        log.info("===================== 信息 ========================");
+//        if (data.getInteger("aweme_type") == 4) {
+//            String play_addr = data.getJSONObject("video").getJSONObject("play_addr").getJSONArray("url_list").getString(0);
+//            log.info("play_addr:{}", play_addr.replaceAll("playwm", "play"));
+//            String cover = data.getJSONObject("video").getJSONObject("cover").getJSONArray("url_list").getString(0);
+//            log.info("cover:{}", cover);
+//        } else if (data.getInteger("aweme_type") == 2) {
+//            for (int i = 0; i < data.getJSONArray("images").size(); i++) {
+//                String imgs = data.getJSONArray("images").getJSONObject(i).getJSONArray("url_list").getString(0);
+//                log.info("img[{}]:{}", i + 1, imgs);
+//            }
+//        }
+//
+//        String desc = data.getString("desc");
+//        log.info("desc:{}", desc);
+//        Integer comment_count = data.getJSONObject("statistics").getInteger("comment_count");
+//        Integer share_count = data.getJSONObject("statistics").getInteger("share_count");
+//        Integer digg_count = data.getJSONObject("statistics").getInteger("digg_count");
+//        Integer collect_count = data.getJSONObject("statistics").getInteger("collect_count");
+//        log.info("comment_count:{}", comment_count);
+//        log.info("share_count:{}", share_count);
+//        log.info("digg_count:{}", digg_count);
+//        log.info("collect_count:{}", collect_count);
+//    }
+//
+//    @Test
+//    public void 获取用户作品列表() {
+//        String 分享连接 = "0- 长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/iz6uY8RfrJ8/ 7@5.com :2pm";
+//        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
+//        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
+//        String mid = null;
+//        try {
+//            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
+//        } catch (UnsupportedEncodingException e) {
+//            throw new RuntimeException(e);
+//        }
+//        String maxcursor = "";
+//        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 1);
+//        log.info("sec_user_id:{}", sec_user_id);
+//        Integer count = 0;
+//        while (true) {
+//            String params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=" + sec_user_id + "&max_cursor=" + maxcursor + "&locate_item_id=7388761148507639094&locate_query=false&show_live_replay_strategy=1&need_time_list=" + "0" + "&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&update_version_code=170400&pc_client_type=1&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1832&screen_height=314&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=126.0.0.0&browser_online=true&engine_name=Blink&engine_version=126.0.0.0&os_name=Android&os_version=6.0&cpu_core_num=12&device_memory=8&platform=Android&downlink=10&effective_type=4g&round_trip_time=150&webid=7347601222205687359&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&msToken=MIOLSj1Hic1rlNKxvhty424gjUhagKo0ti6PK1s9uFfs0keS0miQ0metd3ZljkX0KjkQU_3nmJZm8tuYhMjIC1jntmfmuTRAfcZEbz1UzWZVqMDrHgg%3D";
+//            String aBogus = DouYinUtils.getABogus(params);
+//            String url = "https://www.douyin.com/aweme/v1/web/aweme/post/?" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e";
+//            HashMap<String, String> header = DouYinUtils.getHeader();
+//            String reqStr = OkHttpClientUtils.doGet(url, null, header);
+//            // 解析json
+//            JSONObject reqJson = JSONObject.parseObject(reqStr);
+//            maxcursor = reqJson.getString("max_cursor");
+//            JSONArray awemeList = reqJson.getJSONArray("aweme_list");
+//            count += awemeList.size();
+//            log.info("maxcursor:{}", maxcursor);
+//            log.info("aweme_list:{}", awemeList.size());
+//            if (reqJson.getInteger("has_more") != 1) {
+//                break;
+//            }
+//        }
+//        log.info("count:{}", count);
+//
+//    }
+//
+//    @Test
+//    public void 获取用户信息() {
+//        String 分享连接 = "长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/";
+//        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
+//        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
+//        String mid = null;
+//        try {
+//            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
+//        } catch (UnsupportedEncodingException e) {
+//            throw new RuntimeException(e);
+//        }
+//        log.info("mid:{}", mid);
+//        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
+//        sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
+//        log.info("sec_user_id:{}", sec_user_id);
+//        String params = "?device_platform=webapp&aid=6383&channel=channel_pc_web&publish_video_strategy_type=2&source=channel_pc_web&sec_user_id=" + sec_user_id + "&personal_center_strategy=1&profile_other_record_enable=1&land_to=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=0&support_dash=1&cpu_core_num=24&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=148.0.0.0&browser_online=true&engine_name=Blink&engine_version=148.0.0.0&os_name=Windows&os_version=10&device_memory=32&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7639313678160397875";
+//        String aBogus = DouYinUtils.getABogus(params);
+//        String url = "https://www.douyin.com/aweme/v1/web/user/profile/other/" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe&fp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe";
+//        log.info("url:{}", url);
+//        HashMap<String, String> header = DouYinUtils.getHeader();
+//        String reqStr = OkHttpClientUtils.doGet(url, null, header);
+//        // 解析json
+//        JSONObject reqJson = JSONObject.parseObject(reqStr);
+//        log.info("reqJson:{}", reqJson);
+//
+//    }
+//
+//    @Test
+//    public void 获取抖音热榜() {
+//        String url = "https://so-landing.douyin.com/aweme/v1/hot/search/list/?aid=581610&detail_list=1&board_type=0&board_sub_type=&need_board_tab=true&need_covid_tab=false&version_code=32.3.0 ";
+//        String string = OkHttpClientUtils.doGet(url);
+//        JSONObject jsonObject = JSONObject.parseObject(string);
+//        JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("word_list");
+//        for (int i = 0; i < jsonArray.size(); i++) {
+//            JSONObject wordJson = jsonArray.getJSONObject(i);
+//            if (wordJson.containsKey("group_id")) {
+//                String url2 = null;
+//                try {
+//                    url2 = "https://so.douyin.com/s?is_from_mobile_home=1&search_entrance=aweme&enter_method=hot_list_page&innerWidth=430&innerHeight=932&reloadNavStart=1778841683198&is_no_width_reload=0&keyword=" + URLEncoder.encode(wordJson.getString("word"), "UTF-8") + "&gid=" + wordJson.getString("group_id");
+//                } catch (UnsupportedEncodingException e) {
+//                    throw new RuntimeException(e);
+//                }
+//                HashMap<String, String> hashMap = new HashMap<>();
+//                hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+//
+//                String string2 = OkHttpClientUtils.doGet(url2, null, hashMap);
+//                String substring = string2.substring(string2.indexOf("let data = {\"business_data\":") + "let data = {\"business_data\":".length(), string2.indexOf(",\"render_info\""));
+//                JSONArray parsed = JSONObject.parseArray(substring);
+//                JSONObject object = new JSONObject();
+//                object.put("group_id", wordJson.getString("group_id"));
+//                object.put("word", wordJson.getString("word"));
+//                JSONArray array = new JSONArray();
+//                for (int i1 = 0; i1 < parsed.size(); i1++) {
+//                    String string1 = parsed.getJSONObject(i1).getJSONObject("data").getString("provider_doc_id_str");
+//                    if (parsed.getJSONObject(i1).getInteger("type") == 1) {
+//                        array.add("https://www.douyin.com/video/" + string1);
+//                    }
+//                }
+//                object.put("list", array);
+//                log.info("{}", object);
+//            }
+//
+//        }
+//
+//    }
+//
+//    @Test
+//    public void 获取直播视频流() {
+//        String url = "https://live.douyin.com/660321581729";
+//        HashMap<String, String> hashMap = new HashMap<>();
+//        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+//        String done = OkHttpClientUtils.doGet(url, null, hashMap);
+//        String roomId = DouYinUtils.getRegexString(done, "roomId[^\\d]*(\\d{15,})", 1);
+//        if (StringUtils.isEmpty(roomId)) {
+//            throw new RuntimeException("未找到房间号");
+//        }
+//        String url2 = "https://webcast.amemv.com/webcast/room/reflow/info/?type_id=0&live_id=1&room_id=" + roomId + "&sec_user_id=&app_id=1128";
+//        String string = OkHttpClientUtils.doGet(url2, null, hashMap);
+//        JSONObject data = JSONObject.parseObject(string);
+//        JSONObject stream = data.getJSONObject("data").getJSONObject("room").getJSONObject("stream_url");
+//        JSONObject resolutionName = stream.getJSONObject("resolution_name");
+//        log.info("========================== flv ==========================");
+//        JSONObject flv_pull_url = stream.getJSONObject("flv_pull_url");
+//        flv_pull_url.forEach((key, value) -> {
+//            String resolution = resolutionName.getString(key);
+//            log.info("{}:{}", resolution, value);
+//        });
+//
+//        log.info("========================== hls ==========================");
+//        JSONObject hls_pull_url_map = stream.getJSONObject("hls_pull_url_map");
+//        hls_pull_url_map.forEach((key, value) -> {
+//            String resolution = resolutionName.getString(key);
+//            log.info("{}:{}", resolution, value);
+//        });
+//
+//        log.info("========================== 主播信息 ==========================");
+//
+//        JSONObject owner = data.getJSONObject("data").getJSONObject("room").getJSONObject("owner");
+//        String avatar = owner.getJSONObject("avatar_large").getJSONArray("url_list").getString(0);
+//        log.info("头像:{}", avatar);
+//        log.info("昵称:{}", owner.getString("nickname"));
+//        log.info("签名:{}", owner.getString("signature"));
+//        Integer following_count = owner.getJSONObject("follow_info").getInteger("following_count");
+//        log.info("关注数:{}", following_count);
+//        Integer follower_count = owner.getJSONObject("follow_info").getInteger("follower_count");
+//        log.info("粉丝数:{}", follower_count);
+//        log.info("SECUID:{}", owner.getString("sec_uid"));
+//        log.info("抖音号:{}", owner.getString("display_id"));
+//
+//
+//    }
+//
+//    @Test
+//    public void 获取抖音弹幕() {
+//        String liveId = "561090451047";
+//        try {
+//
+//            // 设置工作目录
+//            String workDir = "C:\\Users\\Administrator\\Downloads\\DouyinLiveWebFetcher-main";
+//
+//            // 构建命令
+//            String command = "cmd /c start cmd /k \"python main.py --live_id=" + liveId + "\"";
+//
+//            System.out.println("执行命令: " + command);
+//
+//            // 使用 ProcessBuilder 执行命令
+//            ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", command);
+//            processBuilder.directory(new java.io.File(workDir));
+//            // 合并错误流和输出流
+//            processBuilder.redirectErrorStream(true);
+//
+//            // 启动进程
+//            Process process = processBuilder.start();
+//            // 等待进程自然结束
+//            int exitCode = process.waitFor();
+//            System.out.println("进程退出码: " + exitCode);
+//
+//
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        } catch (InterruptedException e) {
+//            throw new RuntimeException(e);
+//        }
+//    }
+//
+//}

+ 6 - 0
nexo-example/nexo-stream-mq/pom.xml

@@ -39,6 +39,12 @@
             <artifactId>nexo-common-sentinel</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-websocket</artifactId>
+            <version>1.8.2</version>
+        </dependency>
+
         <dependency>
             <groupId>com.nexo</groupId>
             <artifactId>nexo-common-security</artifactId>

+ 18 - 4
nexo-modules/nexo-job/src/main/java/com/nexo/job/service/SampleService.java

@@ -1,11 +1,14 @@
 package com.nexo.job.service;
 
+import com.nexo.common.redis.utils.RedisUtils;
 import com.nexo.job.mapper.NexoDouyinUserInfoJobMapper;
 import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 import com.nexo.module.api.douyin.utils.DouYinUtils;
 import com.xxl.job.core.context.XxlJobHelper;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.ConfigurableEnvironment;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -30,6 +33,10 @@ import java.util.stream.Collectors;
 @Service
 public class SampleService {
 
+    @Value("${spring.profiles.active}")
+    private String activeProfile;
+
+
     @Resource
     private NexoDouyinUserInfoJobMapper nexoDouyinUserInfoMapper;
     /**
@@ -99,13 +106,11 @@ public class SampleService {
         for (int i = 0; i < updateInfoList.size(); i++) {
             final NexoDouyinUserInfo info = updateInfoList.get(i);
             final int index = i + 1;
-
             executor.submit(() -> {
                 try {
                     long time1 = new Date().getTime();
                     XxlJobHelper.log("开始处理第{}条数据:{} - {} - {}", index, info.getUserId(), info.getNickname(), info.getSecUid());
-
-                    NexoDouyinUserInfo userInfo = DouYinUtils.getUserInfo(info.getSecUid());
+                    NexoDouyinUserInfo userInfo = DouYinUtils.getUserInfo(info.getSecUid(), !activeProfile.equals("prod") ? RedisUtils.getCacheMapValue("sys_config", "douyin_config_dev") : RedisUtils.getCacheMapValue("sys_config", "douyin_config_pord"));
                     List<Long> longs = nexoDouyinUserInfoMapper.getUpdateInfoList(info.getSecUid()).stream().map(item -> item.getId()).collect(Collectors.toList());
 
                     XxlJobHelper.log("user_id {} 昵称 {} 需要更新 {} 条用户记录", info.getUserId(), info.getNickname(), longs.size());
@@ -152,6 +157,15 @@ public class SampleService {
             log.error("任务被中断", e);
         }
     }
-
+    @Resource
+    private ConfigurableEnvironment configurableEnvironment;
+
+    @XxlJob("test")
+    public void test() throws Exception {
+        XxlJobHelper.log("开始执行任务");
+        String profile = configurableEnvironment.getProperty("spring.profiles.active");
+        log.info("activeProfile:{}", profile);
+        XxlJobHelper.log("任务执行完成");
+    }
 
 }

Some files were not shown because too many files changed in this diff