wuwenyi 2 недель назад
Родитель
Сommit
60b218619d
96 измененных файлов с 4627 добавлено и 1 удалено
  1. 29 0
      pom.xml
  2. 31 0
      src/main/java/com/qqflow/engine/common/assemble/CommonAssembler.java
  3. 47 0
      src/main/java/com/qqflow/engine/common/config/LoggingInterceptor.java
  4. 21 0
      src/main/java/com/qqflow/engine/common/config/RestTemplateConfig.java
  5. 14 0
      src/main/java/com/qqflow/engine/common/enums/StringIEnum.java
  6. 31 0
      src/main/java/com/qqflow/engine/common/po/BaseModel.java
  7. 20 0
      src/main/java/com/qqflow/engine/common/po/BaseUpdateModel.java
  8. 176 0
      src/main/java/com/qqflow/engine/common/service/RedissonService.java
  9. 70 0
      src/main/java/com/qqflow/engine/common/util/Assert.java
  10. 546 0
      src/main/java/com/qqflow/engine/common/util/JsonUtil.java
  11. 24 0
      src/main/java/com/qqflow/engine/common/util/XmlUtil.java
  12. 124 0
      src/main/java/com/qqflow/engine/common/util/id/AesUtil.java
  13. 36 0
      src/main/java/com/qqflow/engine/common/util/id/CodeUtil.java
  14. 140 0
      src/main/java/com/qqflow/engine/common/util/id/GenIntegerUtil.java
  15. 109 0
      src/main/java/com/qqflow/engine/common/util/id/GenLong15ForTokenUtil.java
  16. 116 0
      src/main/java/com/qqflow/engine/common/util/id/GenLong15Util.java
  17. 116 0
      src/main/java/com/qqflow/engine/common/util/id/GenLong19Util.java
  18. 48 0
      src/main/java/com/qqflow/engine/common/util/id/IdUtil.java
  19. 23 0
      src/main/java/com/qqflow/engine/common/util/id/TokenUtil.java
  20. 15 0
      src/main/java/com/qqflow/engine/common/util/id/TraceidUtil.java
  21. 31 0
      src/main/java/com/qqflow/engine/config/OpenApiConfig.java
  22. 219 0
      src/main/java/com/qqflow/engine/domain/ewechat/api/EnterpriseWechatApi.java
  23. 35 0
      src/main/java/com/qqflow/engine/domain/ewechat/assemble/EnterpriseWechatAssembler.java
  24. 71 0
      src/main/java/com/qqflow/engine/domain/ewechat/assemble/EnterpriseWechatSessionAssembler.java
  25. 88 0
      src/main/java/com/qqflow/engine/domain/ewechat/component/EnterpriseWechatEventHandler.java
  26. 70 0
      src/main/java/com/qqflow/engine/domain/ewechat/component/EnterpriseWechatValidator.java
  27. 18 0
      src/main/java/com/qqflow/engine/domain/ewechat/config/EnterpriseWechatConfig.java
  28. 24 0
      src/main/java/com/qqflow/engine/domain/ewechat/config/EnterpriseWechatProperties.java
  29. 96 0
      src/main/java/com/qqflow/engine/domain/ewechat/controller/EnterpriseTestController.java
  30. 58 0
      src/main/java/com/qqflow/engine/domain/ewechat/controller/EnterpriseWechatCallbackController.java
  31. 16 0
      src/main/java/com/qqflow/engine/domain/ewechat/dto/EnterpriseCorpDto.java
  32. 19 0
      src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatInfoType.java
  33. 19 0
      src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatMsgEventType.java
  34. 21 0
      src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatMsgType.java
  35. 27 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatCorpMapper.java
  36. 27 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatCorpMapper.xml
  37. 14 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMapper.java
  38. 19 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMapper.xml
  39. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMsgMapper.java
  40. 24 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMsgMapper.xml
  41. 13 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSuiteMapper.java
  42. 20 0
      src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSuiteMapper.xml
  43. 21 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/EnterpriseWechatEvent.java
  44. 21 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatAuthNotice.java
  45. 25 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatCallbackReq.java
  46. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatCallbackXmlReq.java
  47. 24 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatDecryptBody.java
  48. 35 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatMsgDecryptBody.java
  49. 15 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatSuiteTicket.java
  50. 27 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatAddCsReq.java
  51. 33 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatChangeSessionReq.java
  52. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatCsUrlReq.java
  53. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSendMsgReq.java
  54. 39 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSendOnEventMsgReq.java
  55. 23 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSessionReq.java
  56. 34 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSuiteAccessTokenReq.java
  57. 37 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSyncMsgReq.java
  58. 41 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/msg/EnterpriseWechatSendImgMsgReq.java
  59. 40 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/msg/EnterpriseWechatSendTextMsgReq.java
  60. 21 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatAccessTokenRsp.java
  61. 19 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatAddCsRsp.java
  62. 16 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatBaseRsp.java
  63. 21 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatChangeSessionRsp.java
  64. 18 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatCsUrlRsp.java
  65. 52 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatPermanentCodeRsp.java
  66. 14 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSendMsgRsp.java
  67. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSessionRsp.java
  68. 22 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSuiteAccessTokenRsp.java
  69. 31 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSyncMsgRsp.java
  70. 24 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatUploadRsp.java
  71. 18 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatEventMsg.java
  72. 25 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatFileMsg.java
  73. 25 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatImageMsg.java
  74. 46 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatMsg.java
  75. 26 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatTextMsg.java
  76. 26 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatEnterSessionMsgEvent.java
  77. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatFailMsgEvent.java
  78. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatMsgEvent.java
  79. 26 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatServicerStatusMsgEvent.java
  80. 22 0
      src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatUserRecallMsgEvent.java
  81. 64 0
      src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatCorp.java
  82. 29 0
      src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSession.java
  83. 55 0
      src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSessionMsg.java
  84. 34 0
      src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSuite.java
  85. 13 0
      src/main/java/com/qqflow/engine/domain/ewechat/service/EnterpriseWechatCallbackService.java
  86. 14 0
      src/main/java/com/qqflow/engine/domain/ewechat/service/EnterpriseWechatService.java
  87. 45 0
      src/main/java/com/qqflow/engine/domain/ewechat/service/impl/EnterpriseWechatCallbackServiceImpl.java
  88. 47 0
      src/main/java/com/qqflow/engine/domain/ewechat/service/impl/EnterpriseWechatServiceImpl.java
  89. 28 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/EnterpriseWechatUtil.java
  90. 59 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/AesException.java
  91. 26 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/ByteGroup.java
  92. 66 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/JsonParse.java
  93. 67 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/PKCS7Encoder.java
  94. 61 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/SHA1.java
  95. 284 0
      src/main/java/com/qqflow/engine/domain/ewechat/util/aes/WXBizJsonMsgCrypt.java
  96. 7 1
      src/main/resources/application-test.yml

+ 29 - 0
pom.xml

@@ -38,6 +38,16 @@
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>3.27.2</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+        <dependency>
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-boot-starter</artifactId>
             <version>${mybatis-plus.version}</version>
@@ -105,6 +115,25 @@
             <artifactId>poi-ooxml</artifactId>
             <version>5.2.5</version>
         </dependency>
+
+        <!-- JSON解析 -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-xml</artifactId>
+        </dependency>
+        <!-- Jackson JSR310 时间模块 -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <plugins>

+ 31 - 0
src/main/java/com/qqflow/engine/common/assemble/CommonAssembler.java

@@ -0,0 +1,31 @@
+package com.qqflow.engine.common.assemble;
+
+import com.qqflow.engine.common.po.BaseModel;
+import com.qqflow.engine.common.po.BaseUpdateModel;
+import com.qqflow.engine.common.util.id.IdUtil;
+import lombok.SneakyThrows;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author WuWenYi
+ * @since 2024/7/3
+ */
+public abstract class CommonAssembler {
+    @SneakyThrows
+    public <T extends BaseUpdateModel> T assembleCommon(Class<T> clazz) {
+        T t = clazz.newInstance();
+        t.setId(IdUtil.genId());
+        t.setCreatedTime(LocalDateTime.now());
+        t.setUpdatedTime(LocalDateTime.now());
+        return t;
+    }
+
+    @SneakyThrows
+    public <T extends BaseModel> T assembleCommonModel(Class<T> clazz) {
+        T t = clazz.newInstance();
+        t.setId(IdUtil.genId());
+        t.setCreatedTime(LocalDateTime.now());
+        return t;
+    }
+}

+ 47 - 0
src/main/java/com/qqflow/engine/common/config/LoggingInterceptor.java

@@ -0,0 +1,47 @@
+package com.qqflow.engine.common.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Slf4j
+public class LoggingInterceptor implements ClientHttpRequestInterceptor {
+
+    @Override
+    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
+            ClientHttpRequestExecution execution) throws IOException {
+        // 记录请求日志
+        logRequest(request, body);
+
+        // 执行请求
+        ClientHttpResponse response = execution.execute(request, body);
+
+        // 记录响应日志
+        logResponse(response);
+
+        return response;
+    }
+
+    private void logRequest(HttpRequest request, byte[] body) {
+        log.info("URI: {}, Method: {}, Headers: {}", request.getURI(), request.getMethod(), request.getHeaders());
+        log.info("Body: {}", new String(body, StandardCharsets.UTF_8));
+    }
+
+    private void logResponse(ClientHttpResponse response) throws IOException {
+        byte[] bytes = new byte[4096];
+        int read = response.getBody().read(bytes);
+        if (read > 0) {
+            String logInfo = new String(bytes, 0, read, StandardCharsets.UTF_8);
+            log.info("status:{}, Body: {}", response.getStatusCode(), logInfo);
+        }
+    }
+}

+ 21 - 0
src/main/java/com/qqflow/engine/common/config/RestTemplateConfig.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.common.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.BufferingClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author WuWenYi
+ * @since 2025/4/24
+ */
+@Configuration
+public class RestTemplateConfig {
+    @Bean
+    public RestTemplate restTemplate() {
+        RestTemplate restTemplate = new RestTemplate(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
+        restTemplate.getInterceptors().add(new LoggingInterceptor());
+        return restTemplate;
+    }
+}

+ 14 - 0
src/main/java/com/qqflow/engine/common/enums/StringIEnum.java

@@ -0,0 +1,14 @@
+package com.qqflow.engine.common.enums;
+
+import com.baomidou.mybatisplus.annotation.IEnum;
+
+/**
+ * 用于Mybatis-plus entity的实体类枚举映射
+ * @author WuWenYi
+ * @since 2024/6/5
+ */
+public interface StringIEnum extends IEnum<String> {
+    default String getValue() {
+        return toString();
+    }
+}

+ 31 - 0
src/main/java/com/qqflow/engine/common/po/BaseModel.java

@@ -0,0 +1,31 @@
+package com.qqflow.engine.common.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * @author WuWenYi
+ * @since 2024/4/23
+ */
+@Getter
+@Setter
+public abstract class BaseModel implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.INPUT)
+    private Long id;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "created_time")
+    private LocalDateTime createdTime;
+
+}

+ 20 - 0
src/main/java/com/qqflow/engine/common/po/BaseUpdateModel.java

@@ -0,0 +1,20 @@
+package com.qqflow.engine.common.po;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * @author WuWenYi
+ * @since 2024/4/23
+ */
+@Getter
+@Setter
+public abstract class BaseUpdateModel extends BaseModel {
+
+    @TableField(value = "updated_time")
+    private LocalDateTime updatedTime;
+}

+ 176 - 0
src/main/java/com/qqflow/engine/common/service/RedissonService.java

@@ -0,0 +1,176 @@
+package com.qqflow.engine.common.service;
+
+import org.redisson.api.*;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class RedissonService {
+
+    private final RedissonClient redissonClient;
+
+    public RedissonService(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
+
+    // ==================== 分布式锁 ====================
+
+    /** 获取锁(阻塞直到获取成功) */
+    public RLock getLock(String key) {
+        return redissonClient.getLock(key);
+    }
+
+    /** 尝试获取锁,默认等待 10 秒,持有 30 秒 */
+    public boolean tryLock(String key) {
+        try {
+            return redissonClient.getLock(key).tryLock(10, 30, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return false;
+        }
+    }
+
+    /** 尝试获取锁 */
+    public boolean tryLock(String key, long waitTime, long leaseTime, TimeUnit unit) {
+        try {
+            return redissonClient.getLock(key).tryLock(waitTime, leaseTime, unit);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            return false;
+        }
+    }
+
+    /** 释放锁 */
+    public void unlock(String key) {
+        RLock lock = redissonClient.getLock(key);
+        if (lock.isHeldByCurrentThread()) {
+            lock.unlock();
+        }
+    }
+
+    public void unlock(RLock lock) {
+        if (lock != null && lock.isHeldByCurrentThread()) {
+            lock.unlock();
+        }
+    }
+
+    /** 是否被锁 */
+    public boolean isLocked(String key) {
+        return redissonClient.getLock(key).isLocked();
+    }
+
+    // ==================== 通用 Key-Value ====================
+
+    public <T> void set(String key, T value) {
+        redissonClient.getBucket(key).set(value);
+    }
+
+    public <T> void set(String key, T value, long ttl, TimeUnit unit) {
+        redissonClient.getBucket(key).set(value, ttl, unit);
+    }
+
+    public <T> void set(String key, T value, Duration ttl) {
+        redissonClient.getBucket(key).set(value, ttl);
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T get(String key) {
+        RBucket<T> bucket = redissonClient.getBucket(key);
+        return bucket.get();
+    }
+
+    public boolean delete(String key) {
+        return redissonClient.getBucket(key).delete();
+    }
+
+    public boolean exists(String key) {
+        return redissonClient.getBucket(key).isExists();
+    }
+
+    /** 设置过期时间 */
+    public boolean expire(String key, long ttl, TimeUnit unit) {
+        return redissonClient.getBucket(key).expire(ttl, unit);
+    }
+
+    // ==================== 原子操作 ====================
+
+    public RAtomicLong getAtomicLong(String key) {
+        return redissonClient.getAtomicLong(key);
+    }
+
+    public long increment(String key) {
+        return redissonClient.getAtomicLong(key).incrementAndGet();
+    }
+
+    public long decrement(String key) {
+        return redissonClient.getAtomicLong(key).decrementAndGet();
+    }
+
+    // ==================== Map 操作 ====================
+
+    public <K, V> Map<K, V> getMap(String key) {
+        return redissonClient.<K, V>getMap(key);
+    }
+
+    public <K, V> void putMapValue(String key, K field, V value) {
+        redissonClient.<K, V>getMap(key).put(field, value);
+    }
+
+    public <K, V> V getMapValue(String key, K field) {
+        return redissonClient.<K, V>getMap(key).get(field);
+    }
+
+    // ==================== 限流器 ====================
+
+    public RRateLimiter getRateLimiter(String key) {
+        return redissonClient.getRateLimiter(key);
+    }
+
+    /** 设置限流速率并获取令牌 */
+    public boolean tryAcquireRateLimiter(String key, long rate, long rateInterval, RateIntervalUnit unit) {
+        RRateLimiter limiter = redissonClient.getRateLimiter(key);
+        limiter.trySetRate(RateType.OVERALL, rate, rateInterval, unit);
+        return limiter.tryAcquire();
+    }
+
+    // ==================== 队列操作 ====================
+
+    /** 获取阻塞队列 */
+    public <T> RBlockingQueue<T> getBlockingQueue(String key) {
+        return redissonClient.getBlockingQueue(key);
+    }
+
+    /** 入队 */
+    public <T> boolean offer(String key, T value) {
+        return redissonClient.<T>getQueue(key).offer(value);
+    }
+
+    /** 出队 */
+    public <T> T poll(String key) {
+        return redissonClient.<T>getQueue(key).poll();
+    }
+
+    // ==================== Set 操作 ====================
+
+    public boolean sAdd(String key, String value) {
+        return redissonClient.<String>getSet(key).add(value);
+    }
+
+    public boolean sContains(String key, String value) {
+        return redissonClient.<String>getSet(key).contains(value);
+    }
+
+    public Collection<String> sMembers(String key) {
+        return redissonClient.<String>getSet(key).readAll();
+    }
+
+    // ==================== 底层客户端 ====================
+
+    public RedissonClient getClient() {
+        return redissonClient;
+    }
+}

+ 70 - 0
src/main/java/com/qqflow/engine/common/util/Assert.java

@@ -0,0 +1,70 @@
+package com.qqflow.engine.common.util;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.common.exception.BusinessException;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 业务断言工具类
+ * @author WuWenYi
+ * @since 2023/11/30
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class Assert {
+    public static void isTure(boolean condition, String s) {
+        if (!condition) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void isFalse(boolean condition, String s) {
+        if (condition) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void notNull(Object target, String s) {
+        if (Objects.isNull(target)) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void isNull(Object target, String s) {
+        if (Objects.nonNull(target)) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void notEmpty(Collection<?> collection, String s) {
+        if (collection == null || collection.isEmpty()) {
+            throw new BusinessException(s);
+        }
+    }
+
+
+    public static void notBlank(String target, String s) {
+        if (StringUtils.isBlank(target)) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void equals(Object target, Object target2, String s) {
+        if (!Objects.equals(target, target2)) {
+            throw new BusinessException(s);
+        }
+    }
+
+    public static void notEquals(Object target, Object target2, String s) {
+        if (Objects.equals(target, target2)) {
+            throw new BusinessException(s);
+        }
+    }
+
+}
+

+ 546 - 0
src/main/java/com/qqflow/engine/common/util/JsonUtil.java

@@ -0,0 +1,546 @@
+package com.qqflow.engine.common.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.qqflow.engine.common.exception.BusinessException;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author chenanlian
+ */
+public class JsonUtil {
+
+    private static Logger log = LoggerFactory.getLogger(JsonUtil.class);
+
+    private static volatile ObjectMapper objectMapper;
+
+
+    /**
+     * 获取单例ObjectMapper
+     *
+     * @return
+     */
+    public static ObjectMapper getObjectMapper() {
+        if (objectMapper == null) {
+            synchronized (JsonUtil.class) {
+                if (objectMapper == null) {
+                    objectMapper = new ObjectMapper();
+                    objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
+                    objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
+                    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+                    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+                }
+            }
+        }
+        return objectMapper;
+    }
+
+    /**
+     * 对象转json字符串
+     *
+     * @param object
+     * @return
+     */
+    public static String entity2Json(Object object) {
+        try {
+            return getObjectMapper().writeValueAsString(object);
+        } catch (JsonProcessingException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将json形式字符串转换为java实体类
+     */
+    public static <T> T str2Entity(String jsonStr, TypeReference<T> clazz) {
+        if (StringUtils.isBlank(jsonStr)) {
+            return null;
+        }
+        T readValue = null;
+        try {
+            readValue = getObjectMapper().readValue(jsonStr, clazz);
+        } catch (JsonParseException e) {
+            log.error(e.getMessage(), e);
+        } catch (JsonMappingException e) {
+            log.error(e.getMessage(), e);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+        return readValue;
+    }
+
+    /**
+     * 将json形式字符串转换为java实体类
+     */
+    public static <T> T str2Entity(String jsonStr, Class<T> clazz) {
+        T readValue = null;
+        try {
+            readValue = getObjectMapper().readValue(jsonStr, clazz);
+        } catch (JsonParseException e) {
+            log.error(e.getMessage(), e);
+        } catch (JsonMappingException e) {
+            log.error(e.getMessage(), e);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        }
+        return readValue;
+    }
+
+
+    public static <T, T1> T str2Entity(String jsonStr, Class<T> clazz, Class<T1> clazz2) {
+        T readValue = null;
+        try {
+            JavaType javaType1 = JsonUtil.getObjectMapper().getTypeFactory().constructParametricType(clazz, clazz2);
+            readValue = JsonUtil.getObjectMapper().readValue(jsonStr, javaType1);
+        } catch (JsonParseException e) {
+            log.error(e.getMessage(), e);
+        } catch (JsonMappingException e) {
+            log.error(e.getMessage(), e);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+        return readValue;
+    }
+
+
+    public static <T, T1> T str2Entity(String jsonStr, String dateFormat, Class<T> clazz, Class<T1> clazz2) {
+        T readValue = null;
+        try {
+            ObjectMapper mapper = getObjectMapper();
+            if (dateFormat != null) {
+                mapper = new ObjectMapper();
+                mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);
+                mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+                mapper.setDateFormat(new SimpleDateFormat(dateFormat));
+            }
+
+            JavaType javaType1 = mapper.getTypeFactory().constructParametricType(clazz, clazz2);
+            readValue = mapper.readValue(jsonStr, javaType1);
+        } catch (JsonParseException e) {
+            log.error(e.getMessage(), e);
+        } catch (JsonMappingException e) {
+            log.error(e.getMessage(), e);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+        }
+        return readValue;
+    }
+
+    public static <T> T str2Entity(String jsonStr, Class<?>... classes) {
+        T readValue = null;
+        try {
+            ObjectMapper mapper = getObjectMapper();
+            TypeFactory typeFactory = mapper.getTypeFactory();
+            if (classes.length < 2) {
+                throw new BusinessException("参数长度必须大于1");
+            }
+            JavaType javaType = typeFactory.constructType(classes[classes.length - 1]);
+            for (int i = classes.length - 1; i >= 1; i--) {
+                if (i - 2 >= 0 && classes[i - 2].equals(Map.class)) {
+                    javaType = typeFactory.constructParametricType(HashMap.class, typeFactory.constructType(classes[i - 1]), javaType);
+                    i--;
+                } else {
+                    javaType = typeFactory.constructParametricType(classes[i - 1], javaType);
+                }
+            }
+            readValue = mapper.readValue(jsonStr, javaType);
+        } catch (JsonProcessingException e) {
+            log.error(e.getMessage(), e);
+        }
+        return readValue;
+    }
+
+    /**
+     * jsonNode 转实体
+     *
+     * @param jsonNode
+     * @param clazz
+     * @param <T>
+     * @return
+     */
+    public static <T> T jsonNode2Entity(JsonNode jsonNode, Class<T> clazz) {
+        T readValue = getObjectMapper().convertValue(jsonNode, clazz);
+        return readValue;
+    }
+
+    /**
+     * 获取泛型的Collection Type
+     *
+     * @param collectionClass 泛型的Collection
+     * @param elementClasses  元素类
+     * @return JavaType Java类型
+     * @since 1.0
+     */
+    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
+        return getObjectMapper().getTypeFactory().constructParametricType(collectionClass, elementClasses);
+    }
+
+    /**
+     * json字符串转集合类
+     *
+     * @param jsonString
+     * @param collectionClass
+     * @param elementClasses
+     * @return
+     */
+    public static Object str2Collection(String jsonString, Class<?> collectionClass, Class<?>... elementClasses) {
+        JavaType javaType = getCollectionType(collectionClass, elementClasses);
+        try {
+            return getObjectMapper().readValue(jsonString, javaType);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+    }
+
+    public static Object jsonNode2Collection(JsonNode jsonString, Class<?> collectionClass, Class<?>... elementClasses) {
+        JavaType javaType = getCollectionType(collectionClass, elementClasses);
+        try {
+            return getObjectMapper().convertValue(jsonString, javaType);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * jsonNode 转字符串并按字母排序
+     *
+     * @param node
+     * @return
+     */
+    public static String jsonNode2SortStr(final JsonNode node) {
+        Object obj;
+        String json = null;
+        try {
+            obj = objectMapper.treeToValue(node, Object.class);
+            json = objectMapper.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            log.error(e.getMessage(), e);
+        }
+        return json;
+    }
+
+    /**
+     * 字符串转JsonNode
+     *
+     * @param str
+     * @return
+     */
+    public static JsonNode str2JsonNode(String str) {
+        try {
+            return getObjectMapper().readTree(str);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+
+    }
+
+    public static JsonNode entity2JsonNode(Object entity) {
+        return objectMapper.convertValue(entity, JsonNode.class);
+    }
+
+
+    public static JsonNode map2JsonNode(Map<String, String> map) {
+
+        ObjectNode jsonNodes = getObjectMapper().createObjectNode();
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            jsonNodes.put(entry.getKey(), entry.getValue());
+        }
+        return jsonNodes;
+    }
+
+    /**
+     * JsonNode转Map,只取第一层,值转为字符串
+     *
+     * @param jsonNode
+     * @return
+     */
+    public static Map<String, String> jsonNode2MapStr(JsonNode jsonNode) {
+        Map<String, String> map = new HashMap<>();
+        if (jsonNode.isObject()) {
+            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
+            while (it.hasNext()) {
+                Map.Entry<String, JsonNode> entry = it.next();
+                map.put(entry.getKey(), entry.getValue().asText(null));
+            }
+        }
+        return map;
+    }
+
+    /**
+     * JsonNode转Map,只取第一层
+     *
+     * @param jsonNode
+     * @return
+     */
+    public static Map<String, Object> jsonNode2Map(JsonNode jsonNode) {
+        Map<String, Object> map = new HashMap<>();
+        if (jsonNode.isObject()) {
+            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
+            while (it.hasNext()) {
+                Map.Entry<String, JsonNode> entry = it.next();
+                map.put(entry.getKey(), entry.getValue());
+            }
+        }
+        return map;
+    }
+
+
+    /**
+     * 字符串转JsonNode
+     *
+     * @param str
+     * @return
+     */
+    public static JsonNode str2Node(String str) {
+        if (StringUtils.isBlank(str)) {
+            return null;
+        }
+        try {
+            return getObjectMapper().readTree(str);
+        } catch (IOException e) {
+            log.error(e.getMessage(), e);
+            return null;
+        }
+
+    }
+
+    public static void jsonLeaf(JsonNode node) {
+        if (node.isValueNode()) {
+            System.out.println(node.toString());
+            return;
+        }
+
+        if (node.isObject()) {
+            Iterator<Map.Entry<String, JsonNode>> it = node.fields();
+            while (it.hasNext()) {
+                Map.Entry<String, JsonNode> entry = it.next();
+                jsonLeaf(entry.getValue());
+            }
+        }
+
+        if (node.isArray()) {
+            Iterator<JsonNode> it = node.iterator();
+            while (it.hasNext()) {
+                jsonLeaf(it.next());
+            }
+        }
+    }
+
+
+    public static List<String> getStrList(JsonNode jsonNode) {
+        List<String> listll = new ArrayList<>();
+        if (jsonNode.isArray()) {
+            for (JsonNode objNode : jsonNode) {
+                String value = objNode.asText();
+                listll.add(value);
+            }
+        }
+        return listll;
+    }
+
+    /**
+     * 将数据源jsonNode 按模板jsonNode输出sql语句列表
+     *
+     * @param data     数据来源
+     * @param template json 模板
+     * @return sql 语句列表
+     */
+    public static List<String> transform(JsonNode data, JsonNode template) {
+        List<String> sqlList = new ArrayList<>();
+        Iterator<String> tableIterator = template.fieldNames();
+        while (tableIterator.hasNext()) {
+            /*表*/
+            String tableName = tableIterator.next();
+            JsonNode tableNode = template.get(tableName);
+            Iterator<String> fieldIterator = tableNode.fieldNames();
+
+            StringBuilder sb = new StringBuilder();
+            StringBuilder sbv = new StringBuilder();
+            sb.append("INSERT INTO ");
+            sb.append(tableName);
+            sb.append(" (");
+            sbv.append(" VALUES (");
+
+            while (fieldIterator.hasNext()) {
+                /*字段*/
+                String fieldName = fieldIterator.next();
+                String fieldPath = tableNode.get(fieldName).asText();
+                JsonNode fileValue = findValueByPath(data, fieldPath);
+                /*去除没有值或取值错误的节点*/
+                if (null != fileValue && !fileValue.equals("")) {
+                    sb.append(fieldName);
+                    sb.append(",");
+                    sbv.append(fileValue);
+                    sbv.append(",");
+                }
+            }
+            sb.deleteCharAt(sb.length() - 1);
+            sbv.deleteCharAt(sbv.length() - 1);
+            sb.append(")");
+            sbv.append(")");
+            sb.append(sbv);
+
+            sqlList.add(sb.toString());
+        }
+        return sqlList;
+    }
+
+
+    /**
+     * 根据路径获取值
+     *
+     * @param jsonNode
+     * @param valuePath 节点路径
+     * @return
+     */
+    public static JsonNode findValueByPath(JsonNode jsonNode, String valuePath) {
+        String[] pathMutil = valuePath.split("\\.");
+        JsonNode jsonTem = jsonNode;
+        for (int i = 0; i < pathMutil.length; i++) {
+            jsonTem = jsonTem.path(pathMutil[i]);
+        }
+        return jsonTem;
+    }
+
+
+    /**
+     * 重组json
+     *
+     * @param jsonNode
+     * @param nodePath
+     * @return
+     */
+    public static JsonNode jsonNodeTransform(JsonNode jsonNode, List<String> nodePath) {
+        JsonNodeFactory factory = new JsonNodeFactory(false);
+        ObjectNode objectNode = factory.objectNode();
+        for (String path : nodePath) {
+            String[] pathMutil = path.split("\\.");
+            if (pathMutil.length > 1) {
+                JsonNode jsonTem = jsonNode;
+                for (int i = 0; i < pathMutil.length; i++) {
+                    jsonTem = jsonTem.path(pathMutil[i]);
+                }
+                objectNode.put(path, jsonTem);
+            } else {
+                JsonNode jsonNodeChild = jsonNode.path(path);
+                objectNode.put(path, jsonNodeChild);
+            }
+        }
+        return objectNode;
+    }
+
+    /**
+     * 获取字符串值
+     *
+     * @param jsonNode
+     * @param key
+     * @return
+     */
+    public static String getString(JsonNode jsonNode, String key) {
+        JsonNode value = jsonNode.get(key);
+        if (value == null) {
+            return null;
+        } else {
+            return value.asText().trim();
+        }
+    }
+
+    public static Integer getInt(JsonNode jsonNode, String key) {
+        JsonNode value = jsonNode.get(key);
+        if (value == null) {
+            return null;
+        } else {
+            return value.asInt();
+        }
+    }
+
+
+    /**
+     * 实体对象转成Map
+     *
+     * @param obj 实体对象
+     * @return
+     */
+    public static Map<String, Object> object2Map(Object obj) {
+        Map<String, Object> map = new HashMap<String, Object>();
+        if (obj == null) {
+            return map;
+        }
+        Class clazz = obj.getClass();
+        Field[] fields = clazz.getDeclaredFields();
+        try {
+            for (Field field : fields) {
+                field.setAccessible(true);
+                map.put(field.getName(), field.get(obj));
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return map;
+    }
+
+    /**
+     * Map转成实体对象
+     *
+     * @param map   map实体对象包含属性
+     * @param clazz 实体对象类型
+     * @return
+     */
+    public static <T> T map2Object(Map<String, Object> map, Class<T> clazz) {
+        if (map == null) {
+            return null;
+        }
+        T obj = null;
+        try {
+            obj = clazz.newInstance();
+
+            Field[] fields = obj.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                int mod = field.getModifiers();
+                if (Modifier.isStatic(mod) || Modifier.isFinal(mod)) {
+                    continue;
+                }
+                field.setAccessible(true);
+                String filedTypeName = field.getType().getName();
+                if (filedTypeName.equalsIgnoreCase("java.util.date")) {
+                    String datetimestamp = String.valueOf(map.get(field.getName()));
+                    if (datetimestamp.equalsIgnoreCase("null")) {
+                        field.set(obj, null);
+                    } else {
+                        field.set(obj, new Date(Long.parseLong(datetimestamp)));
+                    }
+                } else {
+                    field.set(obj, map.get(field.getName()));
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return obj;
+    }
+
+
+
+}
+

+ 24 - 0
src/main/java/com/qqflow/engine/common/util/XmlUtil.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.common.util;
+
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/17
+ */
+public final class XmlUtil {
+    private static final XmlMapper XML_MAPPER = new XmlMapper();
+
+    static {
+        XML_MAPPER.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    }
+
+    public static <T> T fromXml(String xml, Class<T> clazz) {
+        try {
+            return XML_MAPPER.readValue(xml, clazz);
+        } catch (Exception e) {
+            throw new RuntimeException("XML 解析失败", e);
+        }
+    }
+
+}

+ 124 - 0
src/main/java/com/qqflow/engine/common/util/id/AesUtil.java

@@ -0,0 +1,124 @@
+package com.qqflow.engine.common.util.id;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.*;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 跟mysql内置aes加解密方法一致
+ * <p>
+ * mysql中aes加解密函数使用示例
+ * 加密:select HEX(AES_ENCRYPT('138000138000', 'key'))
+ * 解密:select AES_DECRYPT(UNHEX('189443C6FB32431CEEAD702638A619BD'), 'key')
+ *
+ * 批量更新
+ *
+ * 批量从row_data字段加密到encrypt字段:
+ * UPDATE codec a set a.encrypt =HEX(AES_ENCRYPT(a.row_data, 'key'))
+ *
+ * 批量从encrypt字段解密到decrypt字段:U
+ * PDATE codec a set a.decrypt =AES_DECRYPT(UNHEX(a.encrypt), 'key')
+ *
+ * @author chenanlian
+ */
+
+public class AesUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(AesUtil.class);
+
+
+    /**
+     * 加密
+     *
+     * @param data 数据
+     * @param key  秘钥
+     * @return
+     */
+    public static String encrypt(String data, String key) {
+        try {
+            SecretKey secretKey = generateMySQLAESKey(key, "ASCII");
+            Cipher cipher = Cipher.getInstance("AES");
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+            byte[] cleartext = data.getBytes("UTF-8");
+            byte[] ciphertextBytes = cipher.doFinal(cleartext);
+            return new String(Hex.encodeHex(ciphertextBytes));
+
+        } catch (UnsupportedEncodingException e) {
+            log.error(e.getMessage(), e);
+        } catch (NoSuchAlgorithmException e) {
+            log.error(e.getMessage(), e);
+        } catch (NoSuchPaddingException e) {
+            log.error(e.getMessage(), e);
+        } catch (InvalidKeyException e) {
+            log.error(e.getMessage(), e);
+        } catch (IllegalBlockSizeException e) {
+            log.error(e.getMessage(), e);
+        } catch (BadPaddingException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /**
+     * 解密
+     *
+     * @param data
+     * @param key
+     * @return
+     */
+    public static String decrypt(String data, String key) {
+        try {
+            SecretKey secretKey = generateMySQLAESKey(key, "ASCII");
+            Cipher cipher = Cipher.getInstance("AES");
+            cipher.init(Cipher.DECRYPT_MODE, secretKey);
+            byte[] cleartext = Hex.decodeHex(data.toCharArray());
+            byte[] ciphertextBytes = cipher.doFinal(cleartext);
+            return new String(ciphertextBytes, "UTF-8");
+
+        } catch (UnsupportedEncodingException e) {
+            log.error(e.getMessage(), e);
+        } catch (NoSuchAlgorithmException e) {
+            log.error(e.getMessage(), e);
+        } catch (NoSuchPaddingException e) {
+            log.error(e.getMessage(), e);
+        } catch (InvalidKeyException e) {
+            log.error(e.getMessage(), e);
+        } catch (IllegalBlockSizeException e) {
+            log.error(e.getMessage(), e);
+        } catch (BadPaddingException e) {
+            log.error(e.getMessage(), e);
+        } catch (DecoderException e) {
+            log.error(e.getMessage(), e);
+        }
+        return null;
+    }
+
+
+    public static SecretKeySpec generateMySQLAESKey(final String key, final String encoding) throws UnsupportedEncodingException {
+
+        final byte[] finalKey = new byte[16];
+        int i = 0;
+        for (byte b : key.getBytes(encoding)) {
+            finalKey[i++ % 16] ^= b;
+        }
+        return new SecretKeySpec(finalKey, "AES");
+
+    }
+
+    public static void main(String[] args) {
+        String abc = "138000138000";
+        String aeskey = "ppmoney";
+        String a1 = encrypt(abc, aeskey);
+        System.out.println(a1.toUpperCase());
+        System.out.println(decrypt(a1, aeskey));
+    }
+
+
+}

+ 36 - 0
src/main/java/com/qqflow/engine/common/util/id/CodeUtil.java

@@ -0,0 +1,36 @@
+package com.qqflow.engine.common.util.id;
+
+
+import java.util.Random;
+
+public class CodeUtil {
+
+    public static String gen6() {
+        String[] keyItemArr = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
+        String key = "";
+        int keyLength = 6;
+        for (int i = 0; i < keyLength; i++) {
+            int random = new Random().nextInt(16);
+            String keyItem = keyItemArr[random];
+            key += keyItem;
+
+        }
+        return key;
+    }
+    public static String gen16() {
+        String[] keyItemArr = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
+        String key = "";
+        int keyLength = 16;
+        for (int i = 0; i < keyLength; i++) {
+            int random = new Random().nextInt(16);
+            String keyItem = keyItemArr[random];
+            key += keyItem;
+
+        }
+        return key;
+    }
+
+    public static String gen32() {
+        return IdUtil.genId() + gen16() + gen16();
+    }
+}

+ 140 - 0
src/main/java/com/qqflow/engine/common/util/id/GenIntegerUtil.java

@@ -0,0 +1,140 @@
+package com.qqflow.engine.common.util.id;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 长度在整数范围内的雪花算法
+ * 按10秒一个周期进行计数
+ * 总长度在10位以内,且不大于最大的Integer,2147483647
+ * 机器位,1位
+ * 序列位,1位
+ * 时间戳,8位
+ */
+public class GenIntegerUtil {
+
+    private static final int minNum = 0;
+    private static final AtomicInteger bigAtomic = new AtomicInteger(minNum);
+    private static volatile Integer machineCode = null;
+    private static AtomicLong recordSecond = null;
+    private static final long EPOCH = 168814080L;//20230701000000时刻的10秒时间戳
+    private static int maxTime = 21474835;
+
+    static {
+        checkAndInit();
+    }
+
+    //机器码1位或2位,
+
+    private static void checkAndInit() {
+        if (machineCode == null) {
+            synchronized (GenIntegerUtil.class) {
+                if (machineCode == null) {
+                    initData();
+                }
+            }
+        }
+    }
+
+    private static void initData() {
+        recordSecond = new AtomicLong(nowSecond());
+        try {
+            machineCode = 1;
+        } catch (Exception e) {
+            machineCode = 3;
+        }
+    }
+
+    /**
+     * 获取15位长度的自增长Key
+     * 每秒可生成8970个不重复编号
+     *
+     * @return long
+     */
+    public static int gen() {
+
+        //如果周期时间不是当前时间,重置周期时间,以及重置序列号
+        long nowSecond = nowSecond();
+        long time = recordSecond.get();
+        if (nowSecond > time) {
+            synchronized (GenIntegerUtil.class) {
+                nowSecond = nowSecond();
+                time = recordSecond.get();
+                if (nowSecond > time) {
+                    recordSecond = new AtomicLong(nowSecond);
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                }
+            }
+        }
+
+        int seq = bigAtomic.getAndIncrement();
+
+        //如果序列号达到最大,重置当前时间,并等待下一周期
+        if (seq > 99) {
+            synchronized (GenIntegerUtil.class) {
+                time = recordSecond.get();
+                seq = bigAtomic.getAndIncrement();
+                if (seq > 99) {
+                    recordSecond = new AtomicLong(nextSecond(recordSecond.get()));
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                    seq = bigAtomic.getAndIncrement();
+                }
+            }
+        }
+        time = time - EPOCH;
+        while (time > maxTime) {
+            time = time % maxTime;
+        }
+        String differ = String.valueOf(time);
+        return new Integer(differ + seq).intValue();
+    }
+
+    /**
+     * 获取下一不同秒的时间戳,不能与最后的时间戳一样
+     */
+    private static long nextSecond(long second) {
+        long now = nowSecond();
+        while (now <= second) {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+            now = nowSecond();
+        }
+        return now;
+    }
+
+    /**
+     * 当前的10秒级时间戳
+     *
+     * @return
+     */
+    private static long nowSecond() {
+        return LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8")) / 10;
+    }
+
+    /**
+     * 秒级时间戳转id
+     *
+     * @param secondTimeStamp
+     * @return
+     */
+    public static int timeStamp2Id(Long secondTimeStamp) {
+        String differ = String.valueOf(secondTimeStamp/10 - EPOCH);
+        return new Integer(differ + minNum).intValue();
+    }
+
+    /**
+     * 秒级时间戳
+     * @param id
+     * @return
+     */
+    public static Long id2TimeStamp(int id) {
+        return (id / 10 + EPOCH)*10;
+    }
+}

+ 109 - 0
src/main/java/com/qqflow/engine/common/util/id/GenLong15ForTokenUtil.java

@@ -0,0 +1,109 @@
+package com.qqflow.engine.common.util.id;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class GenLong15ForTokenUtil {
+
+    private static final int minNum = 1000;
+
+    private static final AtomicInteger bigAtomic = new AtomicInteger(minNum);
+
+    private static volatile Integer machineCode = null;
+
+    private static AtomicLong recordSecond = null;
+
+    private static final long EPOCH = 590000000L;
+
+    static {
+        checkAndInit();
+    }
+
+    private static void checkAndInit() {
+        if (machineCode == null) {
+            synchronized (GenLong15ForTokenUtil.class) {
+                if (machineCode == null) {
+                    initData();
+                }
+            }
+        }
+    }
+
+    private static void initData() {
+        recordSecond = new AtomicLong(nowSecond());
+        try {
+            String machineIp = InetAddress.getLocalHost().getHostAddress();
+            String codeStr = machineIp.substring(machineIp.length() - 1);
+            machineCode = new BigInteger(codeStr).intValue();
+        } catch (UnknownHostException e) {
+            machineCode = 3;
+        }
+    }
+
+    /**
+     * 获取15位长度的自增长Key
+     * 每秒可生成8970个不重复编号
+     *
+     * @return long
+     */
+    public static Long genLong15() {
+
+        //如果周期时间不是当前时间,重置周期时间,以及重置序列号
+        long nowSecond = nowSecond();
+        long time = recordSecond.get();
+        if (nowSecond > time) {
+            synchronized (GenLong15ForTokenUtil.class) {
+                nowSecond = nowSecond();
+                time = recordSecond.get();
+                if (nowSecond > time) {
+                    recordSecond = new AtomicLong(nowSecond);
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                }
+            }
+        }
+
+        int seq = bigAtomic.getAndIncrement();
+
+        //如果序列号达到最大,重置当前时间,并等待下一周期
+        if (seq >= 9999) {
+            synchronized (GenLong15ForTokenUtil.class) {
+                time = recordSecond.get();
+                seq = bigAtomic.getAndIncrement();
+                if (seq >= 9999) {
+                    recordSecond = new AtomicLong(nextSecond(recordSecond.get()));
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                    seq = bigAtomic.getAndIncrement();
+                }
+            }
+        }
+        String differ = String.valueOf(time - EPOCH);
+        return new BigInteger(differ + seq + machineCode).longValue();
+    }
+
+    /**
+     * 获取下一不同秒的时间戳,不能与最后的时间戳一样
+     */
+    private static long nextSecond(long second) {
+        long now = nowSecond();
+        while (now <= second) {
+            now = nowSecond();
+        }
+        return now;
+    }
+
+    /**
+     * 当前的秒级时间戳
+     *
+     * @return
+     */
+    private static long nowSecond() {
+        return LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
+    }
+}

+ 116 - 0
src/main/java/com/qqflow/engine/common/util/id/GenLong15Util.java

@@ -0,0 +1,116 @@
+package com.qqflow.engine.common.util.id;
+
+
+import java.math.BigInteger;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class GenLong15Util {
+
+    private static final int minNum = 1000;
+
+    private static final AtomicInteger bigAtomic = new AtomicInteger(minNum);
+
+    private static volatile Integer machineCode = 9;
+
+    private static AtomicLong recordSecond = null;
+
+    private static final long EPOCH = 590000000L;
+
+    static {
+        checkAndInit();
+    }
+
+    private static void checkAndInit() {
+        if (machineCode == null) {
+            synchronized (GenLong15Util.class) {
+                if (machineCode == null) {
+                    initData();
+                }
+            }
+        }
+    }
+
+    private static void initData() {
+        recordSecond = new AtomicLong(nowSecond());
+    }
+
+    /**
+     * 获取15位长度的自增长Key
+     * 每秒可生成8970个不重复编号
+     *
+     * @return long
+     */
+    public static Long genLong15() {
+
+        //如果周期时间不是当前时间,重置周期时间,以及重置序列号
+        long nowSecond = nowSecond();
+        long time = recordSecond.get();
+        if (nowSecond > time) {
+            synchronized (GenLong15Util.class) {
+                nowSecond = nowSecond();
+                time = recordSecond.get();
+                if (nowSecond > time) {
+                    recordSecond = new AtomicLong(nowSecond);
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                }
+            }
+        }
+
+        int seq = bigAtomic.getAndIncrement();
+
+        //如果序列号达到最大,重置当前时间,并等待下一周期
+        if (seq >= 9999) {
+            synchronized (GenLong15Util.class) {
+                time = recordSecond.get();
+                seq = bigAtomic.getAndIncrement();
+                if (seq >= 9999) {
+                    recordSecond = new AtomicLong(nextSecond(recordSecond.get()));
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                    seq = bigAtomic.getAndIncrement();
+                }
+            }
+        }
+        String differ = String.valueOf(time - EPOCH);
+        return new BigInteger(differ + seq + machineCode).longValue();
+    }
+
+    /**
+     * 获取下一不同秒的时间戳,不能与最后的时间戳一样
+     */
+    private static long nextSecond(long second) {
+        long now = nowSecond();
+        while (now <= second) {
+            now = nowSecond();
+        }
+        return now;
+    }
+
+    /**
+     * 当前的秒级时间戳
+     *
+     * @return
+     */
+    private static long nowSecond() {
+        return LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
+    }
+
+    /**
+     * 秒级时间戳转id
+     *
+     * @param secondTimeStamp
+     * @return
+     */
+    public static Long timeStamp2Id(Long secondTimeStamp) {
+        String differ = String.valueOf(secondTimeStamp - GenLong15Util.EPOCH);
+        return new BigInteger(differ + minNum + machineCode).longValue();
+    }
+
+    public static Long id2TimeStamp(Long id) {
+        return id / 100000 + GenLong15Util.EPOCH;
+    }
+}

+ 116 - 0
src/main/java/com/qqflow/engine/common/util/id/GenLong19Util.java

@@ -0,0 +1,116 @@
+package com.qqflow.engine.common.util.id;
+
+
+import java.math.BigInteger;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 生成19位不重复长整型
+ */
+public class GenLong19Util {
+
+
+    private static final int minNum = 10000;
+
+    private static final AtomicInteger bigAtomic = new AtomicInteger(minNum);
+
+    private static volatile Integer machineCode = 9;
+
+    private static AtomicLong recordSecond = null;
+
+    private static final long EPOCH = 590000000000L;
+
+    static {
+        checkAndInit();
+    }
+
+    private static void checkAndInit() {
+        if (machineCode == null) {
+            synchronized (GenLong19Util.class) {
+                if (machineCode == null) {
+                    initData();
+                }
+            }
+        }
+    }
+
+    private static void initData() {
+        recordSecond = new AtomicLong(nowSecond());
+    }
+
+    /**
+     * 获取15位长度的自增长Key
+     * 每秒可生成8970个不重复编号
+     *
+     * @return long
+     */
+    public static Long genLong19() {
+        return new BigInteger(genString19()).longValue();
+    }
+
+    public static String genString19() {
+        //如果周期时间不是当前时间,重置周期时间,以及重置序列号
+        long nowSecond = nowSecond();
+        long time = recordSecond.get();
+        if (nowSecond > time) {
+            synchronized (GenLong19Util.class) {
+                nowSecond = nowSecond();
+                time = recordSecond.get();
+                if (nowSecond > time) {
+                    recordSecond = new AtomicLong(nowSecond);
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                }
+            }
+        }
+
+        int seq = bigAtomic.getAndIncrement();
+
+        //如果序列号达到最大,重置当前时间,并等待下一周期
+        if (seq >= 99999) {
+            synchronized (GenLong19Util.class) {
+                time = recordSecond.get();
+                seq = bigAtomic.getAndIncrement();
+                if (seq >= 99999) {
+                    recordSecond = new AtomicLong(nextSecond(recordSecond.get()));
+                    time = recordSecond.get();
+                    bigAtomic.set(minNum);
+                    seq = bigAtomic.getAndIncrement();
+                }
+            }
+        }
+        String differ = String.valueOf(time - EPOCH);
+        return differ + seq + machineCode;
+    }
+
+    /**
+     * 获取下一不同秒的时间戳,不能与最后的时间戳一样
+     */
+    private static long nextSecond(long second) {
+        long now = nowSecond();
+        while (now <= second) {
+            now = nowSecond();
+        }
+        return now;
+    }
+
+    /**
+     * 当前的秒级时间戳
+     *
+     * @return
+     */
+    private static long nowSecond() {
+        return System.currentTimeMillis();
+    }
+
+
+    public static long timeNoEpoch(long sn) {
+        return sn + EPOCH * 1000000;
+    }
+
+    public static long id2TimeStamp(long id) {
+        Long timeStamp = id/1000000 + 590000000000L;
+        return timeStamp;
+    }
+}

+ 48 - 0
src/main/java/com/qqflow/engine/common/util/id/IdUtil.java

@@ -0,0 +1,48 @@
+package com.qqflow.engine.common.util.id;
+
+
+import java.util.Date;
+
+
+public class IdUtil {
+
+
+    /**
+     * 生成15位的长整型;超过15位js的解析有精度问题
+     *
+     * @return
+     */
+    public static long genId() {
+        return GenLong15Util.genLong15();
+    }
+
+    /**
+     * 日期转id
+     * @param date
+     * @return
+     */
+    public static Long date2Id(Date date) {
+        return GenLong15Util.timeStamp2Id(date.getTime()/1000);
+    }
+
+    /**
+     * 秒级时间戳转id
+     *
+     * @param secondTimeStamp
+     * @return
+     */
+    public static Long timeStamp2Id(Long secondTimeStamp) {
+        return GenLong15Util.timeStamp2Id(secondTimeStamp);
+    }
+
+    /**
+     * id转秒级时间戳
+     * @param id
+     * @return
+     */
+    public static Long id2TimeStamp(long id) {
+        return GenLong15Util.id2TimeStamp(id);
+    }
+
+
+}

+ 23 - 0
src/main/java/com/qqflow/engine/common/util/id/TokenUtil.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.common.util.id;
+
+
+
+import java.util.Date;
+
+public class TokenUtil {
+    private static String tokenKey = "Ninuo202009";
+
+    public static String genId() {
+        long id = GenLong19Util.genLong19();
+        String token = AesUtil.encrypt(String.valueOf(id), tokenKey);
+        //String token = MD5Util.getMD5(String.valueOf(id));
+        return token;
+    }
+
+
+    public static Long token2Id(String token) {
+        String token2 = AesUtil.decrypt(token, tokenKey);
+        return Long.valueOf(token2);
+    }
+
+}

+ 15 - 0
src/main/java/com/qqflow/engine/common/util/id/TraceidUtil.java

@@ -0,0 +1,15 @@
+package com.qqflow.engine.common.util.id;
+
+public class TraceidUtil {
+
+
+    /**
+     * 生成15位的长整型;超过15位js的解析有精度问题
+     * @return
+     */
+    public static String getId() {
+        return GenLong19Util.genString19();
+    }
+
+
+}

+ 31 - 0
src/main/java/com/qqflow/engine/config/OpenApiConfig.java

@@ -0,0 +1,31 @@
+package com.qqflow.engine.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+
+    @Bean
+    public OpenAPI openAPI() {
+        return new OpenAPI()
+                .info(new Info()
+                        .title("QQFlow Engine API")
+                        .description("通用型流程审批设计与管理系统后端接口")
+                        .version("1.0.0")
+                        .contact(new Contact().name("QQFlow Team")))
+                .addSecurityItem(new SecurityRequirement().addList("Bearer"))
+                .components(new Components()
+                        .addSecuritySchemes("Bearer", new SecurityScheme()
+                                .type(SecurityScheme.Type.HTTP)
+                                .scheme("bearer")
+                                .bearerFormat("JWT")
+                                .description("在 Value 输入框中填入 JWT Token")));
+    }
+}

+ 219 - 0
src/main/java/com/qqflow/engine/domain/ewechat/api/EnterpriseWechatApi.java

@@ -0,0 +1,219 @@
+package com.qqflow.engine.domain.ewechat.api;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.map.MapBuilder;
+import cn.hutool.core.map.MapUtil;
+import com.qqflow.engine.common.exception.BusinessException;
+import com.qqflow.engine.common.util.Assert;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.*;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.*;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite;
+import lombok.RequiredArgsConstructor;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/17
+ */
+@Component
+@RequiredArgsConstructor
+public class EnterpriseWechatApi {
+    private static final String BASE_URL = "https://qyapi.weixin.qq.com";
+
+    private final RestTemplate restTemplate;
+
+    private void checkRsp(EnterpriseWechatBaseRsp rsp){
+        Assert.notNull(rsp,"返回值为空");
+        boolean condition = Objects.equals(null, rsp.getErrcode()) || Objects.equals(rsp.getErrcode(), 0);
+        Assert.isTure(condition, rsp.getErrmsg());
+    }
+
+    /**
+     * 获取企业微信后台推送的ticket
+     * suiteId 第三方应用id或者代开发应用模板id。第三方应用以ww或wx开头应用id(对应于旧的以tj开头的套件id);代开发应用以dk开头
+     * @param suiteTicket suite_ticket由企业微信后台定时推送给“指令回调URL”,每十分钟更新一次,见推送suite_ticket。
+     * @return 第三方应用凭证(用于获取永久授权码)
+     */
+    public EnterpriseWechatSuiteAccessTokenRsp getSuiteAccessToken(EnterpriseWechatSuite suite, String suiteTicket) {
+        EnterpriseWechatSuiteAccessTokenReq req = EnterpriseWechatSuiteAccessTokenReq.of(suite, suiteTicket);
+        return this.commonPost("/cgi-bin/service/get_suite_token", req, EnterpriseWechatSuiteAccessTokenRsp.class);
+    }
+
+    /** 获取授权信息 */
+    public EnterpriseWechatPermanentCodeRsp getPermanentCode(String suiteAccessToken, String authCode){
+        Map<String, Object> req = MapBuilder.<String,Object>create().put("auth_code", authCode).build();
+        String uri = "/cgi-bin/service/get_permanent_code?suite_access_token=" + suiteAccessToken;
+        return this.commonPost(uri, req, EnterpriseWechatPermanentCodeRsp.class);
+    }
+
+    /** 获取accesstoken,基本上要调代开发的接口都需要用到这个 */
+    public EnterpriseWechatAccessTokenRsp getAccessToken(String corpId, String permanentCode) {
+        String uri = "/cgi-bin/gettoken?corpid=" + corpId + "&corpsecret=" + permanentCode;
+        return this.commonGet(uri, EnterpriseWechatAccessTokenRsp.class);
+    }
+
+    /**
+     * 添加客服
+     * @param name    客服名称
+     * @param mediaId 素材ID
+     */
+    public EnterpriseWechatAddCsRsp addCs(String accessToken, String name, String mediaId) {
+        EnterpriseWechatAddCsReq req = EnterpriseWechatAddCsReq.of(name, mediaId);
+        String uri = "/cgi-bin/kf/account/add?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatAddCsRsp.class);
+    }
+
+    public EnterpriseWechatCsUrlRsp getCsUrl(String accessToken, String csId) {
+        EnterpriseWechatCsUrlReq req = EnterpriseWechatCsUrlReq.of(csId, "test1");
+        String uri = "/cgi-bin/kf/add_contact_way?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatCsUrlRsp.class);
+    }
+
+    public EnterpriseWechatBaseRsp deleteCs(String accessToken, String openKfid) {
+        String uri = "/cgi-bin/kf/account/del?access_token=" + accessToken;
+        Map<String, String> req = MapUtil.builder("open_kfid", openKfid).build();
+        return this.commonPost(uri, req, EnterpriseWechatBaseRsp.class);
+    }
+
+    /** 同步消息 */
+    public EnterpriseWechatSyncMsgRsp syncMsg(String accessToken, String cursor, String token, String openKfId) {
+        EnterpriseWechatSyncMsgReq req = EnterpriseWechatSyncMsgReq.of(cursor, token, openKfId);
+        String uri = "/cgi-bin/kf/sync_msg?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatSyncMsgRsp.class);
+    }
+
+    /** 获取会话状态 */
+    public EnterpriseWechatSessionRsp getSessionState(String accessToken, String openKfId, String userId){
+        EnterpriseWechatSessionReq req = EnterpriseWechatSessionReq.of(openKfId, userId);
+        String uri = "/cgi-bin/kf/service_state/get?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatSessionRsp.class);
+    }
+
+    /** 变更会话状态 */
+    public EnterpriseWechatChangeSessionRsp changeSessionState(String accessToken, String openKfId, String userId, Integer serviceState){
+        EnterpriseWechatChangeSessionReq req = EnterpriseWechatChangeSessionReq.of(openKfId, userId, serviceState);
+        String uri = "/cgi-bin/kf/service_state/trans?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatChangeSessionRsp.class);
+    }
+
+    /** 发送回应消息 */
+    public EnterpriseWechatSendMsgRsp sendMsgOnEvent(String accessToken, String code, String content){
+        EnterpriseWechatSendOnEventMsgReq req = EnterpriseWechatSendOnEventMsgReq.of(code, content);
+        String uri = "/cgi-bin/kf/send_msg_on_event?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
+    }
+
+    /** 发送消息 */
+    public EnterpriseWechatSendMsgRsp sendMsg(String accessToken, EnterpriseWechatSendMsgReq req){
+        String uri = "/cgi-bin/kf/send_msg?access_token=" + accessToken;
+        return this.commonPost(uri, req, EnterpriseWechatSendMsgRsp.class);
+    }
+
+    /** 上传素材文件 */
+    public EnterpriseWechatUploadRsp upload(String accessToken, String type, File file) {
+        return this.upload(accessToken, type, getResource(file.getPath()), (int) file.length());
+    }
+
+    public EnterpriseWechatUploadRsp upload(String accessToken, String type, MultipartFile multipartFile) {
+        return this.upload(accessToken, type, multipartFile.getResource(), (int) multipartFile.getSize());
+    }
+
+    private EnterpriseWechatUploadRsp upload(String accessToken, String type, Resource resource, int fileLength) {
+        String url = BASE_URL + "/cgi-bin/media/upload?access_token=" + accessToken + "&type=" + type;
+        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
+        body.add("file", resource);
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Content-disposition", "form-data; name=media;filename=" + resource.getFilename() + "; filelength=" + fileLength);
+        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
+
+        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
+        ResponseEntity<EnterpriseWechatUploadRsp> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, EnterpriseWechatUploadRsp.class);
+        this.checkRsp(response.getBody());
+        return response.getBody();
+    }
+
+    private static Resource getResource(String path) {
+        // 先尝试从 classpath 加载
+        Resource resource = new ClassPathResource(path);
+        if (resource.exists()) {
+            return resource;
+        }
+
+        // 如果不存在,再尝试从文件系统加载
+        File file = new File(path);
+        if (file.exists()) {
+            return new FileSystemResource(file);
+        }
+
+        throw new IllegalArgumentException("File not found: " + path);
+    }
+
+    /** 下载临时素材 */
+    public String downloadMedia(String accessToken, String mediaId) {
+        String url = BASE_URL + "/cgi-bin/media/get?access_token=" + accessToken + "&media_id=" + mediaId;
+        HttpHeaders headers = new HttpHeaders();
+        headers.set(HttpHeaders.USER_AGENT, "Mozilla/5.0");
+
+        RequestEntity<Void> request = new RequestEntity<>(headers, HttpMethod.GET, URI.create(url));
+        ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
+        Assert.isTure(response.getStatusCode() == HttpStatus.OK, "download media failed");
+        // 获取 Content-Disposition 头
+        HttpHeaders responseHeaders = response.getHeaders();
+        String contentType = responseHeaders.getContentType() != null ? responseHeaders.getContentType().toString() : "";
+        if (contentType.contains("application/json")) {
+            throw new BusinessException("download media failed,error:" + new String(response.getBody()));
+        }
+        String fileName = extractFileName(responseHeaders).orElseThrow(() -> new BusinessException("can not get file name"));
+        // 保存文件
+        FileUtil.writeBytes(response.getBody(),fileName);
+        return fileName;
+    }
+
+    private static Optional<String> extractFileName(HttpHeaders headers) {
+        List<String> dispositions = headers.get(HttpHeaders.CONTENT_DISPOSITION);
+        if (dispositions != null) {
+            for (String disposition : dispositions) {
+                if (disposition.contains("filename=")) {
+                    return Optional.of(disposition.split("filename=")[1].replaceAll("\"", ""));
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    private <T extends EnterpriseWechatBaseRsp> T commonPost(String uri, Object req, Class<T> rspClass) {
+        String url = BASE_URL + uri;
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<Object> requestEntity = new HttpEntity<>(req, headers);
+        T rsp = restTemplate.postForObject(url, requestEntity, rspClass);
+        this.checkRsp(rsp);
+        return rsp;
+    }
+
+    private <T extends EnterpriseWechatBaseRsp> T commonGet(String uri, Class<T> rspClass) {
+        String url = BASE_URL + uri;
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        HttpEntity<Object> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<T> exchange = restTemplate.exchange(url, HttpMethod.GET, requestEntity, rspClass);
+        this.checkRsp(exchange.getBody());
+        return exchange.getBody();
+    }
+
+}

+ 35 - 0
src/main/java/com/qqflow/engine/domain/ewechat/assemble/EnterpriseWechatAssembler.java

@@ -0,0 +1,35 @@
+package com.qqflow.engine.domain.ewechat.assemble;
+
+import com.qqflow.engine.common.assemble.CommonAssembler;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatPermanentCodeRsp;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Component
+public class EnterpriseWechatAssembler extends CommonAssembler {
+    public EnterpriseWechatCorp assembleCorp(EnterpriseWechatPermanentCodeRsp rsp, EnterpriseWechatSuite suite){
+        EnterpriseWechatCorp corp = assembleCommon(EnterpriseWechatCorp.class);
+        EnterpriseWechatPermanentCodeRsp.AuthCorpInfo authCorpInfo = rsp.getAuthCorpInfo();
+        EnterpriseWechatPermanentCodeRsp.AuthUserInfo authUserInfo = rsp.getAuthUserInfo();
+        EnterpriseWechatPermanentCodeRsp.RegisterCodeInfo registerCodeInfo = rsp.getRegisterCodeInfo();
+        corp.setPermanentCode(rsp.getPermanentCode());
+        corp.setCorpId(authCorpInfo.getCorpid());
+        corp.setSuiteId(suite.getSuiteId());
+        corp.setCallbackCode(suite.getCallbackCode());
+        corp.setCorpName(authCorpInfo.getCorpName());
+        corp.setAuthUserId(authUserInfo.getUserid());
+        corp.setAuthOpenUserId(authUserInfo.getOpenUserid());
+        if (Objects.nonNull(registerCodeInfo)) {
+            corp.setRegisterCode(registerCodeInfo.getRegisterCode());
+            corp.setTemplateId(registerCodeInfo.getTemplateId());
+        }
+        return corp;
+    }
+}

+ 71 - 0
src/main/java/com/qqflow/engine/domain/ewechat/assemble/EnterpriseWechatSessionAssembler.java

@@ -0,0 +1,71 @@
+package com.qqflow.engine.domain.ewechat.assemble;
+
+import com.qqflow.engine.common.assemble.CommonAssembler;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendMsgReq;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.EnterpriseWechatImageMsg;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.EnterpriseWechatMsg;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.EnterpriseWechatTextMsg;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSession;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSessionMsg;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Component
+public class EnterpriseWechatSessionAssembler extends CommonAssembler {
+    public EnterpriseWechatSession assembleSession(String openKfid, String externalUserid, String corpId) {
+        EnterpriseWechatSession session = this.assembleCommon(EnterpriseWechatSession.class);
+        session.setExternalUserid(externalUserid);
+        session.setOpenKfid(openKfid);
+        session.setCorpId(corpId);
+        return session;
+    }
+
+    public EnterpriseWechatSessionMsg assembleWelcomeMsg(String msgid, String value, String welcomeCode,Long sessionId) {
+        EnterpriseWechatSessionMsg sessionMsg = this.assembleCommon(EnterpriseWechatSessionMsg.class);
+        sessionMsg.setMsgId(msgid);
+        sessionMsg.setCode(welcomeCode);
+        sessionMsg.setValue(value);
+        sessionMsg.setOrigin(5);
+        sessionMsg.setMsgType(EnterpriseWechatMsgType.text);
+        sessionMsg.setSessionId(sessionId);
+        return sessionMsg;
+    }
+
+    public EnterpriseWechatSessionMsg assembleMsg(EnterpriseWechatSendMsgReq req, String msgid, Long sessionId) {
+        EnterpriseWechatSessionMsg sessionMsg = this.assembleCommon(EnterpriseWechatSessionMsg.class);
+        sessionMsg.setMsgId(msgid);
+        sessionMsg.setOrigin(5);
+        sessionMsg.setMsgType(req.getMsgtype());
+        sessionMsg.setSessionId(sessionId);
+        switch (req.getMsgtype()){
+            case text:
+                sessionMsg.setValue(req.getMsgValue());
+                break;
+            case image:
+                sessionMsg.setMediaId(req.getMsgValue());
+        }
+        return sessionMsg;
+    }
+
+    public EnterpriseWechatSessionMsg assembleMsg(EnterpriseWechatMsg msg, Long sessionId){
+        EnterpriseWechatSessionMsg sessionMsg = this.assembleCommon(EnterpriseWechatSessionMsg.class);
+        sessionMsg.setMsgId(msg.getMsgid());
+        sessionMsg.setOrigin(msg.getOrigin());
+        sessionMsg.setMsgType(msg.getMsgtype());
+        sessionMsg.setSessionId(sessionId);
+        switch (msg.getMsgtype()){
+            case text:
+                EnterpriseWechatTextMsg textMsg = ((EnterpriseWechatTextMsg) msg);
+                sessionMsg.setValue(textMsg.getText().getContent());
+                break;
+            case image:
+                EnterpriseWechatImageMsg imageMsg= ((EnterpriseWechatImageMsg) msg);
+                sessionMsg.setMediaId(imageMsg.getImage().getMediaId());
+        }
+        return sessionMsg;
+    }
+}

+ 88 - 0
src/main/java/com/qqflow/engine/domain/ewechat/component/EnterpriseWechatEventHandler.java

@@ -0,0 +1,88 @@
+package com.qqflow.engine.domain.ewechat.component;
+
+import com.qqflow.engine.common.service.RedissonService;
+import com.qqflow.engine.common.util.Assert;
+import com.qqflow.engine.common.util.RedisCache;
+import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
+import com.qqflow.engine.domain.ewechat.assemble.EnterpriseWechatAssembler;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatSuiteMapper;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatInfoType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.EnterpriseWechatEvent;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatAuthNotice;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatSuiteTicket;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatPermanentCodeRsp;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatSuiteAccessTokenRsp;
+import com.qqflow.engine.common.util.XmlUtil;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author WuWenYi
+ * @see EnterpriseWechatInfoType
+ * @since 2025/3/18
+ */
+@Component
+@RequiredArgsConstructor
+public class EnterpriseWechatEventHandler {
+    private static final String SUITE_TICKET_KEY_PREFIX = "suite_ticket::";
+
+    private static final String SUITE_ACCESS_TOKEN_KEY_PREFIX = "suite_access_token::";
+
+    private final EnterpriseWechatCorpMapper corpMapper;
+    private final EnterpriseWechatAssembler assembler;
+    private final EnterpriseWechatSuiteMapper suiteMapper;
+
+    private final RedisCache redisCache;
+    private final RedissonService redissonService;
+
+    private final EnterpriseWechatApi enterpriseWechatApi;
+
+    /** 授权事件 */
+    @EventListener(value = {EnterpriseWechatEvent.class}, condition = "#event.source.toString()=='create_auth'")
+    public void createAuth(EnterpriseWechatEvent event) {
+
+        EnterpriseWechatAuthNotice authNotice = XmlUtil.fromXml(event.getCallbackXml(), EnterpriseWechatAuthNotice.class);
+        boolean locked = redissonService.tryLock("hanle-create-auth-event::" + authNotice.getSuiteId());
+        if (!locked) return;
+        try {
+            String key = SUITE_TICKET_KEY_PREFIX + authNotice.getSuiteId();
+            String suiteTicket = redisCache.getCacheObject(key);
+            Assert.notBlank(suiteTicket, "suite_ticket不存在");
+            EnterpriseWechatSuite suite = suiteMapper.selectBySuiteId(authNotice.getSuiteId());
+            EnterpriseWechatSuiteAccessTokenRsp tokenRsp = enterpriseWechatApi.getSuiteAccessToken(suite, suiteTicket);
+            String tokenKey = SUITE_ACCESS_TOKEN_KEY_PREFIX + authNotice.getSuiteId();
+            redisCache.setCacheObject(tokenKey, tokenRsp.getSuiteAccessToken(), tokenRsp.getExpiresIn(), TimeUnit.SECONDS);
+
+            EnterpriseWechatPermanentCodeRsp permanentCodeRsp = enterpriseWechatApi.getPermanentCode(tokenRsp.getSuiteAccessToken(), authNotice.getAuthCode());
+            String corpid = permanentCodeRsp.getAuthCorpInfo().getCorpid();
+            EnterpriseWechatCorp wechatCorp = corpMapper.selectByCorpIdAndSuiteId(corpid, authNotice.getSuiteId());
+            if (wechatCorp == null) {
+                EnterpriseWechatCorp corp = assembler.assembleCorp(permanentCodeRsp, suite);
+                corpMapper.insert(corp);
+            }
+        } finally {
+            redissonService.unlock("hanle-create-auth-event::" + authNotice.getSuiteId());
+        }
+    }
+
+    /** 取消授权事件 */
+    @EventListener(value = {EnterpriseWechatEvent.class}, condition = "#event.source.toString()=='cancel_auth'")
+    public void cancelAuth(EnterpriseWechatEvent event) {
+        // TODO: 取消授权事件处理
+    }
+
+    /** 推送suite_ticket事件 */
+    @EventListener(value = {EnterpriseWechatEvent.class}, condition = "#event.source.toString()=='suite_ticket'")
+    public void suiteTicket(EnterpriseWechatEvent event) {
+        EnterpriseWechatSuiteTicket suiteTicket = XmlUtil.fromXml(event.getCallbackXml(), EnterpriseWechatSuiteTicket.class);
+        String key = SUITE_TICKET_KEY_PREFIX + suiteTicket.getSuiteId();
+        redisCache.setCacheObject(key, suiteTicket.getSuiteTicket(), 60 * 30, TimeUnit.SECONDS);
+    }
+
+}

+ 70 - 0
src/main/java/com/qqflow/engine/domain/ewechat/component/EnterpriseWechatValidator.java

@@ -0,0 +1,70 @@
+package com.qqflow.engine.domain.ewechat.component;
+
+import com.qqflow.engine.domain.ewechat.config.EnterpriseWechatProperties;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatCallbackReq;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatCallbackXmlReq;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import com.qqflow.engine.domain.ewechat.util.aes.AesException;
+import com.qqflow.engine.domain.ewechat.util.aes.WXBizJsonMsgCrypt;
+import com.qqflow.engine.common.util.XmlUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/14
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class EnterpriseWechatValidator {
+    private final EnterpriseWechatProperties properties;
+
+    private final EnterpriseWechatCorpMapper corpMapper;
+
+    public String verifyGetReq(EnterpriseWechatCallbackReq req) {
+        return this.commonVerifyGetReq(req, properties.getSCorpId());
+    }
+
+    public String decrypt(EnterpriseWechatCallbackReq req, String xmlReqStr) {
+        try {
+            EnterpriseWechatCallbackXmlReq xmlReq = XmlUtil.fromXml(xmlReqStr, EnterpriseWechatCallbackXmlReq.class);
+            WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(properties.getToken(), properties.getEncodingAESKey(), xmlReq.getToUserName());
+            String decryptMsg = wxcpt.DecryptMsg(req.getMsg_signature(), String.valueOf(req.getTimestamp()), req.getNonce(), xmlReq.getMsgEncrypt());
+            log.info("企微解密处理结果: {}", decryptMsg);
+            return decryptMsg;
+        } catch (AesException e) {
+            log.error("encrypt error", e);
+            return null;
+        }
+    }
+
+    public String appVerifyGetReq(EnterpriseWechatCallbackReq req, String code) {
+        // 获取开发应用模板
+        List<EnterpriseWechatCorp> corps = corpMapper.selectByCode(code);
+        for (EnterpriseWechatCorp corp : corps) {
+            String result = this.commonVerifyGetReq(req, corp.getCorpId());
+            if (Objects.nonNull(result)) {
+                return result;
+            }
+        }
+        return null;
+    }
+
+    public String commonVerifyGetReq(EnterpriseWechatCallbackReq req, String corpId) {
+        try {
+            WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(properties.getToken(), properties.getEncodingAESKey(), corpId);
+            String result = wxcpt.VerifyURL(req.getMsg_signature(), String.valueOf(req.getTimestamp()), req.getNonce(), req.getEchostr());
+            log.info("verifyGetReq result: {}", result);
+            return result;
+        } catch (AesException e) {
+            log.info("verifyGetReq error:{}", e.getMessage());
+            return null;
+        }
+    }
+}

+ 18 - 0
src/main/java/com/qqflow/engine/domain/ewechat/config/EnterpriseWechatConfig.java

@@ -0,0 +1,18 @@
+package com.qqflow.engine.domain.ewechat.config;
+
+import com.qqflow.engine.domain.ewechat.util.aes.AesException;
+import com.qqflow.engine.domain.ewechat.util.aes.WXBizJsonMsgCrypt;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Configuration
+public class EnterpriseWechatConfig {
+    @Bean
+    public WXBizJsonMsgCrypt encryptor(EnterpriseWechatProperties properties) throws AesException {
+        return new WXBizJsonMsgCrypt(properties.getToken(), properties.getEncodingAESKey(), properties.getSCorpId());
+    }
+}

+ 24 - 0
src/main/java/com/qqflow/engine/domain/ewechat/config/EnterpriseWechatProperties.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.domain.ewechat.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/14
+ */
+@Getter
+@Setter
+@Component
+@ConfigurationProperties(prefix = "enterprise-wechat")
+public class EnterpriseWechatProperties {
+    /** 企业id */
+    private String sCorpId;
+
+    /** token跟encodingAESKey,约定全部的代开发模板和代开发应用都用这个,否则验证不通过 */
+    private String token;
+
+    private String encodingAESKey;
+}

+ 96 - 0
src/main/java/com/qqflow/engine/domain/ewechat/controller/EnterpriseTestController.java

@@ -0,0 +1,96 @@
+package com.qqflow.engine.domain.ewechat.controller;
+
+import com.qqflow.engine.common.Result;
+import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
+import com.qqflow.engine.domain.ewechat.dto.EnterpriseCorpDto;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendMsgReq;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.*;
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatService;
+import lombok.RequiredArgsConstructor;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/25
+ */
+@Tag(name = "内部企微测试用接口")
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/assistant/api/enterprise/test/p")
+public class EnterpriseTestController {
+    private final EnterpriseWechatApi enterpriseWechatApi;
+    private final EnterpriseWechatService enterpriseWechatService;
+
+    @GetMapping("/change-session-state")
+    public Result<EnterpriseWechatChangeSessionRsp> changeSessionState(@RequestParam String userId, @RequestParam String openKfId,
+            @RequestParam Integer sessionState, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatChangeSessionRsp rsp = enterpriseWechatApi.changeSessionState(accessToken, openKfId, userId, sessionState);
+        return Result.ok(rsp);
+    }
+
+    @GetMapping("/get-session-state")
+    public Result<EnterpriseWechatSessionRsp> getSessionState(@RequestParam String userId, @RequestParam String openKfId, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatSessionRsp sessionState = enterpriseWechatApi.getSessionState(accessToken, openKfId, userId);
+        return Result.ok(sessionState);
+    }
+
+    @GetMapping("/get-media")
+    public Result<String> getMedia(@RequestParam String mediaId, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        String media = enterpriseWechatApi.downloadMedia(accessToken, mediaId);
+        File file = new File(media);
+        file.deleteOnExit();
+        return Result.ok(media);
+    }
+
+//    @PostMapping("/upload-media")
+//    public Result<EnterpriseWechatUploadRsp> uploadMedia(@RequestParam String mediaType, @RequestParam String imgUrl, EnterpriseCorpDto dto) {
+//        File file = HttpClientUtil.downloadFile(imgUrl);
+//        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+//        EnterpriseWechatUploadRsp uploadRsp = enterpriseWechatApi.upload(accessToken, mediaType, file);
+//        file.deleteOnExit();
+//        return Result.ok(uploadRsp);
+//    }
+
+    @PostMapping("/upload-media2")
+    public Result<EnterpriseWechatUploadRsp> uploadMedia(@RequestParam String mediaType, @RequestParam MultipartFile file, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatUploadRsp uploadRsp = enterpriseWechatApi.upload(accessToken, mediaType, file);
+        return Result.ok(uploadRsp);
+    }
+
+    @PostMapping("/send-img-msg")
+    public Result<EnterpriseWechatSendMsgRsp> sendImg(@RequestBody EnterpriseWechatSendMsgReq req, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatSendMsgRsp sendMsgRsp = enterpriseWechatApi.sendMsg(accessToken, req);
+        return Result.ok(sendMsgRsp);
+    }
+
+    @PostMapping("/add-cs")
+    public Result<EnterpriseWechatAddCsRsp> addCs(@RequestParam String name, @RequestParam String mediaId, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatAddCsRsp rsp = enterpriseWechatApi.addCs(accessToken, name, mediaId);
+        return Result.ok(rsp);
+    }
+
+    @PostMapping("get-cs-url")
+    public Result<EnterpriseWechatCsUrlRsp> getCsUrl(@RequestParam String csId, EnterpriseCorpDto dto) {
+        //wkU17nDAAATaRZs9Xtf1urye0rOws6Rw
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatCsUrlRsp rsp = enterpriseWechatApi.getCsUrl(accessToken, csId);
+        return Result.ok(rsp);
+    }
+
+    @PostMapping("delete-cs")
+    public Result<EnterpriseWechatBaseRsp> deleteCs(@RequestParam String openKfid, EnterpriseCorpDto dto) {
+        String accessToken = enterpriseWechatService.getAccessToken(dto.getCorpId(), dto.getSuiteId());
+        EnterpriseWechatBaseRsp rsp = enterpriseWechatApi.deleteCs(accessToken, openKfid);
+        return Result.ok(rsp);
+    }
+}

+ 58 - 0
src/main/java/com/qqflow/engine/domain/ewechat/controller/EnterpriseWechatCallbackController.java

@@ -0,0 +1,58 @@
+package com.qqflow.engine.domain.ewechat.controller;
+
+import com.qqflow.engine.common.util.JsonUtil;
+import com.qqflow.engine.domain.ewechat.component.EnterpriseWechatValidator;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatCallbackReq;
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatCallbackService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/14
+ */
+@Slf4j
+@Tag(name = "企微回调接口")
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/assistant/api/open/p/enterprise-wechat/")
+public class EnterpriseWechatCallbackController {
+    private final EnterpriseWechatValidator enterpriseWechatValidator;
+    private final EnterpriseWechatCallbackService enterpriseWechatCallbackService;
+
+    @Operation(summary = "企微回调")
+    @GetMapping("/callback")
+    public String enterpriseWechatCallback(EnterpriseWechatCallbackReq req) {
+        log.info("企微回调接口收到请求:{}", JsonUtil.entity2Json(req));
+        return enterpriseWechatValidator.verifyGetReq(req);
+    }
+
+    @Operation(summary = "企微post回调")
+    @PostMapping(value = "/callback")
+    public String enterpriseWechatPostCallback(EnterpriseWechatCallbackReq req, @RequestBody String reqBody) {
+        log.info("企微post回调接口收到请求:{}", JsonUtil.entity2Json(req));
+        log.info("企微post回调接口收到请求:{}", reqBody);
+        enterpriseWechatCallbackService.callback(req, reqBody);
+        return "success";
+    }
+
+    @Operation(summary = "企微应用验证回调")
+    @GetMapping("/{code}/callback")
+    public String wechatCallback(EnterpriseWechatCallbackReq req, @PathVariable String code) {
+        log.info("get回调接口收到请求:{}", JsonUtil.entity2Json(req));
+        return enterpriseWechatValidator.appVerifyGetReq(req, code);
+    }
+
+//    @Operation(summary = "企微应用回调")
+//    @PostMapping("/{callbackCode}/callback")
+//    public String wechatCallback(EnterpriseWechatCallbackReq req, @PathVariable String callbackCode, @RequestBody String reqBody) {
+//        log.info("企微应用post回调接口收到请求,callbackCode:{}, body::{}", callbackCode, JsonUtil.entity2Json(req));
+//        log.info("企微应用post回调接口收到请求:callbackCode:{}, body::{}", callbackCode, reqBody);
+//        enterpriseWechatCallbackService.appCallback(req, callbackCode, reqBody);
+//        return "success";
+//    }
+
+}

+ 16 - 0
src/main/java/com/qqflow/engine/domain/ewechat/dto/EnterpriseCorpDto.java

@@ -0,0 +1,16 @@
+package com.qqflow.engine.domain.ewechat.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/4/18
+ */
+@Getter
+@Setter
+public class EnterpriseCorpDto {
+    private String corpId;
+
+    private String suiteId;
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatInfoType.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.domain.ewechat.enums;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+public enum EnterpriseWechatInfoType {
+    /** 创建授权 */
+    create_auth,
+
+    /** 取消授权 */
+    cancel_auth,
+
+    /** 授权额外信息 */
+    auth_external_contact,
+
+    /** ticket更新 */
+    suite_ticket,
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatMsgEventType.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.domain.ewechat.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ * @see <a href='https://developer.work.weixin.qq.com/document/path/94699#%E4%BA%8B%E4%BB%B6%E6%B6%88%E6%81%AF'>事件类型</a>
+ */
+@Getter
+@RequiredArgsConstructor
+public enum EnterpriseWechatMsgEventType {
+    enter_session,
+    msg_send_fail,
+    servicer_status_change,
+    user_recall_msg
+    ;
+}

+ 21 - 0
src/main/java/com/qqflow/engine/domain/ewechat/enums/EnterpriseWechatMsgType.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.ewechat.enums;
+
+import com.qqflow.engine.common.enums.StringIEnum;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@RequiredArgsConstructor
+public enum EnterpriseWechatMsgType implements StringIEnum {
+    event,
+    image,
+    text,
+    file
+    ;
+
+
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatCorpMapper.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.ewechat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface EnterpriseWechatCorpMapper extends BaseMapper<EnterpriseWechatCorp> {
+    default EnterpriseWechatCorp selectByCorpIdAndSuiteId(String corpId, String suiteId) {
+        return this.selectOne(
+                Wrappers.<EnterpriseWechatCorp>lambdaQuery().eq(EnterpriseWechatCorp::getCorpId, corpId).eq(EnterpriseWechatCorp::getSuiteId, suiteId)
+                        .last("LIMIT 1"));
+    }
+
+    default EnterpriseWechatCorp selectByCorpIdAndCode(String corpId, String code) {
+        return this.selectOne(
+                Wrappers.<EnterpriseWechatCorp>lambdaQuery().eq(EnterpriseWechatCorp::getCorpId, corpId).eq(EnterpriseWechatCorp::getCallbackCode, code)
+                        .last("LIMIT 1"));
+    }
+
+    default List<EnterpriseWechatCorp> selectByCode(String code) {
+        return this.selectList(Wrappers.<EnterpriseWechatCorp>lambdaQuery().eq(EnterpriseWechatCorp::getCallbackCode, code));
+    }
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatCorpMapper.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper">
+  <resultMap id="BaseResultMap" type="com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp">
+    <!--@mbg.generated-->
+    <!--@Table enterprise_wechat_corp-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="created_time" jdbcType="TIMESTAMP" property="createdTime" />
+    <result column="updated_time" jdbcType="TIMESTAMP" property="updatedTime" />
+    <result column="permanent_code" jdbcType="VARCHAR" property="permanentCode" />
+    <result column="callback_code" jdbcType="VARCHAR" property="callbackCode" />
+    <result column="suite_id" jdbcType="VARCHAR" property="suiteId" />
+    <result column="corp_id" jdbcType="VARCHAR" property="corpId" />
+    <result column="corp_name" jdbcType="VARCHAR" property="corpName" />
+    <result column="auth_user_id" jdbcType="VARCHAR" property="authUserId" />
+    <result column="auth_open_user_id" jdbcType="VARCHAR" property="authOpenUserId" />
+    <result column="register_code" jdbcType="VARCHAR" property="registerCode" />
+    <result column="template_id" jdbcType="VARCHAR" property="templateId" />
+    <result column="state" jdbcType="VARCHAR" property="state" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, created_by, created_time, updated_by, updated_time, permanent_code, callback_code, 
+    suite_id, corp_id, corp_name, auth_user_id, auth_open_user_id, register_code, template_id, 
+    `state`
+  </sql>
+</mapper>

+ 14 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMapper.java

@@ -0,0 +1,14 @@
+package com.qqflow.engine.domain.ewechat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSession;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnterpriseWechatSessionMapper extends BaseMapper<EnterpriseWechatSession> {
+    default EnterpriseWechatSession selectByKfidAndUserId(String kfid, String userId) {
+        return this.selectOne(Wrappers.<EnterpriseWechatSession>lambdaQuery().eq(EnterpriseWechatSession::getOpenKfid, kfid)
+                .eq(EnterpriseWechatSession::getExternalUserid, userId).last("LIMIT 1"));
+    }
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMapper.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatSessionMapper">
+  <resultMap id="BaseResultMap" type="com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSession">
+    <!--@mbg.generated-->
+    <!--@Table enterprise_wechat_session-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="created_time" jdbcType="TIMESTAMP" property="createdTime" />
+    <result column="updated_time" jdbcType="TIMESTAMP" property="updatedTime" />
+    <result column="open_kfid" jdbcType="VARCHAR" property="openKfid" />
+    <result column="external_userid" jdbcType="VARCHAR" property="externalUserid" />
+    <result column="corp_id" jdbcType="VARCHAR" property="corpId" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, created_by, created_time, updated_by, updated_time, open_kfid, external_userid, 
+    corp_id
+  </sql>
+</mapper>

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMsgMapper.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.qqflow.engine.common.po.BaseModel;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSessionMsg;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface EnterpriseWechatSessionMsgMapper extends BaseMapper<EnterpriseWechatSessionMsg> {
+    default boolean existMsg(Long sessionId) {
+        return selectCount(Wrappers.<EnterpriseWechatSessionMsg>lambdaQuery().eq(EnterpriseWechatSessionMsg::getSessionId, sessionId)
+                .eq(EnterpriseWechatSessionMsg::getOrigin, 5)) > 0;
+    }
+
+    default boolean existMsg(String msgid){
+        return selectCount(Wrappers.<EnterpriseWechatSessionMsg>lambdaQuery()
+                .eq(EnterpriseWechatSessionMsg::getMsgId, msgid)) > 0;
+    }
+
+    /** 不包括欢迎语 */
+    default List<EnterpriseWechatSessionMsg> selectBySessionId(Long sessionId){
+        return selectList(Wrappers.<EnterpriseWechatSessionMsg>lambdaQuery().eq(EnterpriseWechatSessionMsg::getSessionId, sessionId)
+                .isNull(EnterpriseWechatSessionMsg::getCode).orderByDesc(BaseModel::getId).last("limit 10"));
+    }
+}

+ 24 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSessionMsgMapper.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatSessionMsgMapper">
+  <resultMap id="BaseResultMap" type="com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSessionMsg">
+    <!--@mbg.generated-->
+    <!--@Table enterprise_wechat_session_msg-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="created_time" jdbcType="TIMESTAMP" property="createdTime" />
+    <result column="updated_time" jdbcType="TIMESTAMP" property="updatedTime" />
+    <result column="session_id" jdbcType="BIGINT" property="sessionId" />
+    <result column="msg_id" jdbcType="VARCHAR" property="msgId" />
+    <result column="msg_type" jdbcType="VARCHAR" property="msgType" />
+    <result column="media_id" jdbcType="VARCHAR" property="mediaId" />
+    <result column="media_url" jdbcType="VARCHAR" property="mediaUrl" />
+    <result column="code" jdbcType="VARCHAR" property="code" />
+    <result column="value" jdbcType="VARCHAR" property="value" />
+    <result column="origin" jdbcType="TINYINT" property="origin" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, created_by, created_time, updated_by, updated_time, session_id, msg_id, msg_type,
+    media_id, media_url, code, `value`, origin
+  </sql>
+</mapper>

+ 13 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSuiteMapper.java

@@ -0,0 +1,13 @@
+package com.qqflow.engine.domain.ewechat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnterpriseWechatSuiteMapper extends BaseMapper<EnterpriseWechatSuite> {
+    default EnterpriseWechatSuite selectBySuiteId(String suiteId) {
+        return this.selectOne(Wrappers.<EnterpriseWechatSuite>lambdaQuery().eq(EnterpriseWechatSuite::getSuiteId, suiteId).last("LIMIT 1"));
+    }
+}

+ 20 - 0
src/main/java/com/qqflow/engine/domain/ewechat/mapper/EnterpriseWechatSuiteMapper.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatSuiteMapper">
+  <resultMap id="BaseResultMap" type="com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite">
+    <!--@mbg.generated-->
+    <!--@Table enterprise_wechat_suite-->
+    <id column="id" jdbcType="BIGINT" property="id" />
+    <result column="created_time" jdbcType="TIMESTAMP" property="createdTime" />
+    <result column="updated_time" jdbcType="TIMESTAMP" property="updatedTime" />
+    <result column="name" jdbcType="VARCHAR" property="name" />
+    <result column="suite_id" jdbcType="VARCHAR" property="suiteId" />
+    <result column="suite_secret" jdbcType="VARCHAR" property="suiteSecret" />
+    <result column="callback_code" jdbcType="VARCHAR" property="callbackCode" />
+  </resultMap>
+  <sql id="Base_Column_List">
+    <!--@mbg.generated-->
+    id, created_by, created_time, updated_by, updated_time, `name`, suite_id, suite_secret, 
+    callback_code
+  </sql>
+</mapper>

+ 21 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/EnterpriseWechatEvent.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise;
+
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatInfoType;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+public class EnterpriseWechatEvent extends ApplicationEvent {
+    private String callbackXml;
+
+    public EnterpriseWechatEvent(EnterpriseWechatInfoType infoType, String callbackXml) {
+        super(infoType);
+        this.callbackXml = callbackXml;
+    }
+}

+ 21 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatAuthNotice.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+public class EnterpriseWechatAuthNotice extends EnterpriseWechatDecryptBody {
+    /** 临时授权码,最长为512字节。用于获取企业永久授权码。10分钟内有效 */
+    private String authCode;
+
+    /** 构造授权链接指定的state参数,暂时没见过 */
+    private String state;
+
+    /** 额外信息,暂时没见过 */
+    private String extraInfo;
+}

+ 25 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatCallbackReq.java

@@ -0,0 +1,25 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/14
+ * @see <a href="hhttps://developer.work.weixin.qq.com/document/path/96238">企业微信网页授权</a>
+ */
+@Getter
+@Setter
+public class EnterpriseWechatCallbackReq {
+    /** 企业微信加密签名 */
+    private String msg_signature;
+
+    /** 时间戳 */
+    private Integer timestamp;
+
+    /** 随机数 */
+    private String nonce;
+
+    /** 加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid四个字段,其中msg即为消息内容明文 */
+    private String echostr;
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatCallbackXmlReq.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/14
+ * @see <a href="hhttps://developer.work.weixin.qq.com/document/path/96238">企业微信网页授权</a>
+ */
+@Getter
+@Setter
+@JacksonXmlRootElement
+public class EnterpriseWechatCallbackXmlReq {
+    /** 企业微信的CorpID,当为第三方应用回调事件时,CorpID的内容为suiteid */
+    @JacksonXmlProperty(localName = "ToUserName")
+    private String toUserName;
+
+    /** 接收的应用id,可在应用的设置页面获取。仅应用相关的回调会带该字段。 */
+    @JacksonXmlProperty(localName = "AgentID")
+    private String agentId;
+
+    /** 企业微信加密签名,msg_signature结合了企业填写的token、请求中的timestamp、nonce参数、加密的消息体 */
+    @JacksonXmlProperty(localName = "Encrypt")
+    private String msgEncrypt;
+}

+ 24 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatDecryptBody.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatInfoType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class)
+public class EnterpriseWechatDecryptBody {
+    /** suiteId 第三方应用id或者代开发应用模板id。第三方应用以ww或wx开头应用id(对应于旧的以tj开头的套件id);代开发应用以dk开头 */
+    protected String suiteId;
+
+    protected EnterpriseWechatInfoType infoType;
+
+    protected Long timestamp;
+}

+ 35 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatMsgDecryptBody.java

@@ -0,0 +1,35 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 消息回调解密后请求体
+ * @author WuWenYi
+ * @since 2025/3/20
+ * @see <a href='https://developer.work.weixin.qq.com/document/path/94699'>企业微信回调消息解密接口</a>
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.UpperCamelCaseStrategy.class)
+public class EnterpriseWechatMsgDecryptBody {
+    /** 企业微信CorpID */
+    private String toUserName;
+
+    private Long createTime;
+
+    /** 消息的类型,此时固定为:event */
+    private String msgType;
+
+    /** 事件类型,此时固定为:kf_msg_or_event */
+    private String event;
+
+    /** 调用拉取消息接口时,需要传此token,用于校验请求的合法性 */
+    private String token;
+
+    /** 有新消息的客服账号。可通过sync_msg接口指定open_kfid获取此客服账号的消息 */
+    private String openKfId;
+}

+ 15 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/callback/EnterpriseWechatSuiteTicket.java

@@ -0,0 +1,15 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.callback;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSuiteTicket extends EnterpriseWechatDecryptBody {
+    /** Ticket内容,最长为512字节 */
+    private String suiteTicket;
+}

+ 27 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatAddCsReq.java

@@ -0,0 +1,27 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * 添加客服
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor(staticName = "of")
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatAddCsReq {
+    /** 客服名称 */
+    private String name;
+
+    /** 头像,素材ID*/
+    private String mediaId;
+}

+ 33 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatChangeSessionReq.java

@@ -0,0 +1,33 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatChangeSessionReq {
+    private String openKfid;
+
+    private String externalUserid;
+
+    /** 变更的目标状态 */
+    private Integer serviceState;
+
+    /** 暂时用不到,接待人员的userid。第三方应用填密文userid,即open_userid。当state=3时要求必填,接待人员须处于“正在接待”中。 */
+    private String servicerUserid;
+
+    public static EnterpriseWechatChangeSessionReq of(String openKfid, String externalUserid, Integer serviceState) {
+        EnterpriseWechatChangeSessionReq req = new EnterpriseWechatChangeSessionReq();
+        req.setOpenKfid(openKfid);
+        req.setExternalUserid(externalUserid);
+        req.setServiceState(serviceState);
+        return req;
+    }
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatCsUrlReq.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@AllArgsConstructor(staticName = "of")
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatCsUrlReq {
+    /** 客服账号ID */
+    private String openKfid;
+
+    /**
+     * 场景值,
+     * 字符串类型,由开发者自定义。
+     * 不多于32字节
+     * 非必填
+     */
+    private String scene;
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSendMsgReq.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public abstract class EnterpriseWechatSendMsgReq {
+    /** EXTERNAL_USERID,客户id */
+    private String touser;
+
+    /** 客服id */
+    private String openKfid;
+
+    /** 消息类型 */
+    private EnterpriseWechatMsgType msgtype;
+
+    public abstract String getMsgValue();
+}

+ 39 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSendOnEventMsgReq.java

@@ -0,0 +1,39 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 发送欢迎语等事件响应消息请求体(目前只做文本
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSendOnEventMsgReq {
+    /** 事件响应消息对应的code。通过事件回调下发,仅可使用一次。 */
+    private String code;
+
+    /** 消息ID。如果请求参数指定了msgid,则原样返回,否则系统自动生成并返回。不多于32字节 */
+    private String msgid;
+
+    /** 只有两种:text-文本,msgmenu-菜单 */
+    private String msgtype;
+
+    private Text text;
+
+    @Getter
+    @Setter
+    private static class Text{
+        private String content;
+    }
+
+    public static EnterpriseWechatSendOnEventMsgReq of(String code,String content){
+        EnterpriseWechatSendOnEventMsgReq req = new EnterpriseWechatSendOnEventMsgReq();
+        req.code = code;
+        req.msgtype = "text";
+        req.text = new Text();
+        req.text.content = content;
+        return req;
+    }
+}

+ 23 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSessionReq.java

@@ -0,0 +1,23 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@AllArgsConstructor(staticName = "of")
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSessionReq {
+    /** 客服Id */
+    private String openKfid;
+
+    /** 微信客户的external_userid */
+    private String externalUserid;
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSuiteAccessTokenReq.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatSuite;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSuiteAccessTokenReq {
+    /** 第三方应用id或者代开发应用模板id。第三方应用以ww或wx开头应用id(对应于旧的以tj开头的套件id);代开发应用以dk开头 */
+    private String suiteId;
+
+    /** 第三方应用secret 或者代开发应用模板secret */
+    private String suiteSecret;
+
+    /** 企业微信后台推送的ticket */
+    private String suiteTicket;
+
+    public static EnterpriseWechatSuiteAccessTokenReq of(EnterpriseWechatSuite suite, String suiteTicket){
+        EnterpriseWechatSuiteAccessTokenReq req = new EnterpriseWechatSuiteAccessTokenReq();
+        req.setSuiteId(suite.getSuiteId());
+        req.setSuiteSecret(suite.getSuiteSecret());
+        req.setSuiteTicket(suiteTicket);
+        return req;
+    }
+}

+ 37 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/EnterpriseWechatSyncMsgReq.java

@@ -0,0 +1,37 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSyncMsgReq {
+    /** 上一次调用时返回的next_cursor,第一次拉取可以不填。若不填,从3天内最早的消息开始返回。 */
+    private String cursor;
+
+    /** 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制。 */
+    private String token;
+
+    private Integer limit =1000;
+
+    /** 语音消息类型,0-Amr 1-Silk,默认0。可通过该参数控制返回的语音格式,开发者可按需选择自己程序支持的一种格式 */
+    private Integer voiceFormat = 0;
+
+    /** 指定拉取某个客服账号的消息 */
+    private String openKfid;
+
+    public static EnterpriseWechatSyncMsgReq of(String cursor, String token, String openKfid) {
+        EnterpriseWechatSyncMsgReq req = new EnterpriseWechatSyncMsgReq();
+        req.setCursor(cursor);
+        req.setToken(token);
+        req.setOpenKfid(openKfid);
+        return req;
+    }
+}

+ 41 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/msg/EnterpriseWechatSendImgMsgReq.java

@@ -0,0 +1,41 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendMsgReq;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/24
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSendImgMsgReq extends EnterpriseWechatSendMsgReq {
+    private Body image;
+
+    @Override
+    public String getMsgValue() {
+        return image.getMediaId();
+    }
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class Body {
+        private String mediaId;
+    }
+
+    public static EnterpriseWechatSendImgMsgReq of(String toUser,String openKfid, String mediaId) {
+        EnterpriseWechatSendImgMsgReq req = new EnterpriseWechatSendImgMsgReq();
+        req.setTouser(toUser);
+        req.setOpenKfid(openKfid);
+        req.setMsgtype(EnterpriseWechatMsgType.image);
+        req.setImage(new Body());
+        req.getImage().setMediaId(mediaId);
+        return req;
+    }
+}

+ 40 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/req/msg/EnterpriseWechatSendTextMsgReq.java

@@ -0,0 +1,40 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.req.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.req.EnterpriseWechatSendMsgReq;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSendTextMsgReq extends EnterpriseWechatSendMsgReq {
+    private Body text;
+
+    @Override
+    public String getMsgValue() {
+        return text.getContent();
+    }
+
+    @Getter
+    @Setter
+    public static class Body {
+        private String content;
+    }
+
+    public static EnterpriseWechatSendTextMsgReq of(String toUser,String openKfid, String content) {
+        EnterpriseWechatSendTextMsgReq req = new EnterpriseWechatSendTextMsgReq();
+        req.setTouser(toUser);
+        req.setOpenKfid(openKfid);
+        req.setMsgtype(EnterpriseWechatMsgType.text);
+        req.setText(new Body());
+        req.getText().setContent(content);
+        return req;
+    }
+}

+ 21 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatAccessTokenRsp.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatAccessTokenRsp extends EnterpriseWechatBaseRsp {
+    /** 获取到的凭证,最长为512字节 */
+    private String accessToken;
+
+    /** 有效期(秒) */
+    private Integer expiresIn;
+}

+ 19 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatAddCsRsp.java

@@ -0,0 +1,19 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 添加客服
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatAddCsRsp extends EnterpriseWechatBaseRsp {
+    /** 新创建的客服账号ID */
+    private String openKfid;
+}

+ 16 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatBaseRsp.java

@@ -0,0 +1,16 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+public class EnterpriseWechatBaseRsp {
+    private Integer errcode;
+
+    private String errmsg;
+}

+ 21 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatChangeSessionRsp.java

@@ -0,0 +1,21 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatChangeSessionRsp extends EnterpriseWechatBaseRsp {
+    /**
+     * 用于发送响应事件消息的code,将会话初次变更为service_state为2和3时,返回回复语code,service_state为4时,返回结束语code。
+     * 可用该code调用发送事件响应消息接口给客户发送事件响应消息
+     */
+    private String msgCode;
+}

+ 18 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatCsUrlRsp.java

@@ -0,0 +1,18 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatCsUrlRsp extends EnterpriseWechatBaseRsp {
+    /** 客服链接,开发者可将该链接嵌入到H5页面中,用户点击链接即可向对应的微信客服账号发起咨询。开发者也可根据该url自行生成需要的二维码图片 */
+    private String url;
+}

+ 52 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatPermanentCodeRsp.java

@@ -0,0 +1,52 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ * @see <a href="https://developer.work.weixin.qq.com/document/path/100776">企业微信永久授权码获取</a>
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatPermanentCodeRsp extends EnterpriseWechatBaseRsp {
+    private String permanentCode;
+
+    private AuthCorpInfo authCorpInfo;
+
+    private AuthUserInfo authUserInfo;
+
+    private RegisterCodeInfo registerCodeInfo;
+
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class AuthCorpInfo{
+        private String corpid;
+        private String corpName;
+    }
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class AuthUserInfo{
+        private String userid;
+        private String openUserid;
+        private String name;
+        private String avatar;
+    }
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class RegisterCodeInfo{
+        private String registerCode;
+        private String templateId;
+        private String state;
+    }
+}

+ 14 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSendMsgRsp.java

@@ -0,0 +1,14 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSendMsgRsp extends EnterpriseWechatBaseRsp {
+    private String msgid;
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSessionRsp.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSessionRsp extends EnterpriseWechatBaseRsp {
+    /**
+     * 当前的会话状态
+     * 0	未处理	新会话接入(客户发消息咨询)。可选择:1.直接用API自动回复消息。2.放进待接入池等待接待人员接待。3.指定接待人员(接待人员须处于“正在接待”中,下同)进行接待
+     * 1	由智能助手接待	可使用API回复消息。可选择转入待接入池或者指定接待人员处理。
+     * 2	待接入池排队中	在待接入池中排队等待接待人员接入。可选择转为指定人员接待
+     * 3	由人工接待	人工接待中。可选择转接给其他接待人员处理或者结束会话。
+     * 4	已结束/未开始	会话已经结束或未开始(客户进入会话,还没上行消息)。不允许通过API变更会话状态,客户发消息咨询后会话状态变为“未处理”。接待人员通过客户端在已结束会话中成功发送消息后,会话状态变为“由人工接待”,此时会产生会话状态变更回调事件(4-重新接入已结束/已转接会话)。
+     */
+    private Integer serviceState;
+
+    /** 接待人员的userid。第三方应用为密文userid,即open_userid。仅当state=3时有效 */
+    private String servicerUserid;
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSuiteAccessTokenRsp.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+@Getter
+@Setter
+@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSuiteAccessTokenRsp extends EnterpriseWechatBaseRsp {
+    /** 第三方或者代开发应用access_token,最长为512字节 */
+    private String suiteAccessToken;
+
+    /** 有效期(秒) */
+    private Integer expiresIn;
+}

+ 31 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatSyncMsgRsp.java

@@ -0,0 +1,31 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.EnterpriseWechatMsg;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatSyncMsgRsp extends EnterpriseWechatBaseRsp {
+    /** 下次调用带上该值,则从当前的位置继续往后拉,以实现增量拉取。 */
+    private String nextCursor;
+
+    /**
+     * 是否还有更多数据。0-否;1-是。
+     * 不能通过判断msg_list是否空来停止拉取,可能会出现has_more为1,而msg_list为空的情况
+     */
+    private Boolean hasMore;
+
+    /** 消息列表 */
+    private List<EnterpriseWechatMsg> msgList;
+
+}

+ 24 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/EnterpriseWechatUploadRsp.java

@@ -0,0 +1,24 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/19
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatUploadRsp extends EnterpriseWechatBaseRsp {
+    /** 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file) */
+    private String type;
+
+    /** 媒体文件上传后获取的唯一标识,3天内有效 */
+    private String mediaId;
+
+    /** 媒体文件上传时间戳 */
+    private Long createdAt;
+}

+ 18 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatEventMsg.java

@@ -0,0 +1,18 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event.EnterpriseWechatMsgEvent;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatEventMsg extends EnterpriseWechatMsg {
+    private EnterpriseWechatMsgEvent event;
+}

+ 25 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatFileMsg.java

@@ -0,0 +1,25 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatFileMsg extends EnterpriseWechatMsg {
+    private Body file;
+
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class Body {
+        private String mediaId;
+    }
+}

+ 25 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatImageMsg.java

@@ -0,0 +1,25 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/21
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatImageMsg extends EnterpriseWechatMsg {
+    private Body image;
+
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class Body {
+        private String mediaId;
+    }
+}

+ 46 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatMsg.java

@@ -0,0 +1,46 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "msgtype", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
+@JsonSubTypes({
+        @JsonSubTypes.Type(value = EnterpriseWechatEventMsg.class, name = "event"),
+        @JsonSubTypes.Type(value = EnterpriseWechatTextMsg.class, name = "text"),
+        @JsonSubTypes.Type(value = EnterpriseWechatImageMsg.class, name = "image"),
+        @JsonSubTypes.Type(value = EnterpriseWechatFileMsg.class, name = "file"),
+})
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public abstract class EnterpriseWechatMsg {
+    private String msgid;
+
+    /** 客服账号ID(msgtype为event,该字段不返回) */
+    private String openKfid;
+
+    /** 客户UserID(msgtype为event,该字段不返回) */
+    private String externalUserid;
+
+    private Long sendTime;
+
+    /** 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息 */
+    private Integer origin;
+
+    /** 从企业微信给客户发消息的接待人员userid(即仅origin为5才返回;msgtype为event,该字段不返回) */
+    private String servicerUserid;
+
+    /** 消息类型 */
+    private EnterpriseWechatMsgType msgtype;
+
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/EnterpriseWechatTextMsg.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatTextMsg extends EnterpriseWechatMsg {
+    private Body text;
+
+    @Getter
+    @Setter
+    @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+    public static class Body{
+        private String content;
+
+        private String menuId;
+    }
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatEnterSessionMsgEvent.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatEnterSessionMsgEvent extends EnterpriseWechatMsgEvent {
+    /** 客户UserID */
+    private String externalUserid;
+
+    /**
+     * 如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
+     * 可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。
+     */
+    private String welcomeCode;
+
+    /* 其他字段先忽略 */
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatFailMsgEvent.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatFailMsgEvent extends EnterpriseWechatMsgEvent {
+    /** 客户UserID */
+    private String externalUserid;
+
+    /** 失败的消息id */
+    private String failMsgid;
+
+    /**
+     * 失败类型。0-未知原因 1-客服账号已删除 2-应用已关闭 4-会话已过期,超过48小时 5-会话已关闭 6-超过5条限制 8-主体未验证 10-用户拒收
+     * 11-企业未有成员登录企业微信App(排查方法:企业至少一个成员通过手机号验证/微信授权登录企业微信App即可)
+     * 12-发送的消息为客服组件禁发的消息类型 13-安全限制
+     */
+    private Integer failType;
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatMsgEvent.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgEventType;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "event_type", include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true)
+@JsonSubTypes({
+        @JsonSubTypes.Type(value = EnterpriseWechatEnterSessionMsgEvent.class, name = "enter_session"),
+        @JsonSubTypes.Type(value = EnterpriseWechatUserRecallMsgEvent.class, name = "user_recall_msg"),
+})
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public abstract class EnterpriseWechatMsgEvent {
+    private EnterpriseWechatMsgEventType eventType;
+
+    /** 客服账号ID */
+    private String openKfid;
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatServicerStatusMsgEvent.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/20
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatServicerStatusMsgEvent extends EnterpriseWechatMsgEvent {
+    /** 接待人员userid */
+    private String servicerUserid;
+
+    /** 状态类型。1-接待中 2-停止接待 */
+    private Integer status;
+
+    /** 接待人员的状态为「停止接待」的子类型。0:停止接待,1:暂时挂起 */
+    private Integer stopType;
+
+
+}

+ 22 - 0
src/main/java/com/qqflow/engine/domain/ewechat/model/enterprise/rsp/msg/event/EnterpriseWechatUserRecallMsgEvent.java

@@ -0,0 +1,22 @@
+package com.qqflow.engine.domain.ewechat.model.enterprise.rsp.msg.event;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author WuWenYi
+ * @since 2025/4/27
+ */
+@Getter
+@Setter
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+public class EnterpriseWechatUserRecallMsgEvent extends EnterpriseWechatMsgEvent{
+    private String externalUserid;
+
+    /** 撤回的消息id */
+    private String recallMsgid;
+
+    private String servicerUserid;
+}

+ 64 - 0
src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatCorp.java

@@ -0,0 +1,64 @@
+package com.qqflow.engine.domain.ewechat.po;
+
+import com.qqflow.engine.common.po.BaseUpdateModel;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信代开发公司
+ */
+@Getter
+@Setter
+public class EnterpriseWechatCorp extends BaseUpdateModel implements Serializable {
+    /**
+     * 企业微信永久授权码
+     */
+    private String permanentCode;
+
+    /**
+     * 回调 code
+     */
+    private String callbackCode;
+
+    /**
+     * 代开发应用模板id
+     */
+    private String suiteId;
+
+    /**
+     * 授权方企业微信id
+     */
+    private String corpId;
+
+    /**
+     * 企业简称
+     */
+    private String corpName;
+
+    /**
+     * 授权管理员id
+     */
+    private String authUserId;
+
+    /**
+     * 授权管理员open_id
+     */
+    private String authOpenUserId;
+
+    /**
+     * 注册码
+     */
+    private String registerCode;
+
+    /**
+     * 推广包ID
+     */
+    private String templateId;
+
+    /**
+     * 扫码或者授权链接中带的state值
+     */
+    private String state;
+}

+ 29 - 0
src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSession.java

@@ -0,0 +1,29 @@
+package com.qqflow.engine.domain.ewechat.po;
+
+import com.qqflow.engine.common.po.BaseUpdateModel;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信客服会话
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSession extends BaseUpdateModel implements Serializable {
+    /**
+     * 企微客服账号id
+     */
+    private String openKfid;
+
+    /**
+     * 客户userId
+     */
+    private String externalUserid;
+
+    /**
+     * 代开发公司 id
+     */
+    private String corpId;
+}

+ 55 - 0
src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSessionMsg.java

@@ -0,0 +1,55 @@
+package com.qqflow.engine.domain.ewechat.po;
+
+import com.qqflow.engine.common.po.BaseUpdateModel;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatMsgType;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信客服会话消息
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSessionMsg extends BaseUpdateModel implements Serializable {
+    /**
+     * 会话id
+     */
+    private Long sessionId;
+
+    /**
+     * 企微消息id
+     */
+    private String msgId;
+
+    /**
+     * 企微消息类型
+     */
+    private EnterpriseWechatMsgType msgType;
+
+    /**
+     * 素材id
+     */
+    private String mediaId;
+
+    /**
+     * 素材Url
+     */
+    private String mediaUrl;
+
+    /**
+     * 发消息时的code
+     */
+    private String code;
+
+    /**
+     * 消息体(根据不同类型有所不同)
+     */
+    private String value;
+
+    /**
+     * 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+     */
+    private Integer origin;
+}

+ 34 - 0
src/main/java/com/qqflow/engine/domain/ewechat/po/EnterpriseWechatSuite.java

@@ -0,0 +1,34 @@
+package com.qqflow.engine.domain.ewechat.po;
+
+import com.qqflow.engine.common.po.BaseUpdateModel;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+/**
+ * 企业微信代开发模板
+ */
+@Getter
+@Setter
+public class EnterpriseWechatSuite extends BaseUpdateModel implements Serializable {
+    /**
+     * 开发模板名称
+     */
+    private String name;
+
+    /**
+     * 模板 ID
+     */
+    private String suiteId;
+
+    /**
+     * 模板Secret
+     */
+    private String suiteSecret;
+
+    /**
+     * 回调 code
+     */
+    private String callbackCode;
+}

+ 13 - 0
src/main/java/com/qqflow/engine/domain/ewechat/service/EnterpriseWechatCallbackService.java

@@ -0,0 +1,13 @@
+package com.qqflow.engine.domain.ewechat.service;
+
+
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatCallbackReq;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/17
+ */
+public interface EnterpriseWechatCallbackService {
+    /** 企微的回调,大部分都是第三方代开发的回调信息 */
+    void callback(EnterpriseWechatCallbackReq req, String reqBody);
+}

+ 14 - 0
src/main/java/com/qqflow/engine/domain/ewechat/service/EnterpriseWechatService.java

@@ -0,0 +1,14 @@
+package com.qqflow.engine.domain.ewechat.service;
+
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+
+/**
+ * 企微通用服务
+ * @author WuWenYi
+ * @since 2025/3/19
+ */
+public interface EnterpriseWechatService {
+    String getAccessToken(String corpId, String suiteId);
+
+    String getAccessToken(EnterpriseWechatCorp corp);
+}

+ 45 - 0
src/main/java/com/qqflow/engine/domain/ewechat/service/impl/EnterpriseWechatCallbackServiceImpl.java

@@ -0,0 +1,45 @@
+package com.qqflow.engine.domain.ewechat.service.impl;
+
+import com.qqflow.engine.common.util.Assert;
+import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
+import com.qqflow.engine.domain.ewechat.component.EnterpriseWechatValidator;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatInfoType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.EnterpriseWechatEvent;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatCallbackReq;
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatCallbackService;
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatService;
+import com.qqflow.engine.domain.ewechat.util.EnterpriseWechatUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/17
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class EnterpriseWechatCallbackServiceImpl implements EnterpriseWechatCallbackService {
+    private final EnterpriseWechatValidator validator;
+
+    private final ApplicationContext context;
+
+    private final EnterpriseWechatService enterpriseWechatService;
+
+    private final EnterpriseWechatCorpMapper corpMapper;
+
+    private final EnterpriseWechatApi enterpriseWechatApi;
+
+    @Override
+    public void callback(EnterpriseWechatCallbackReq req, String reqBody) {
+        String result = validator.decrypt(req, reqBody);
+        EnterpriseWechatInfoType infoType = EnterpriseWechatUtil.getInfoType(result);
+        Assert.notNull(infoType, "Invalid infoType");
+        log.info("回调事件类型:{}", infoType);
+        context.publishEvent(new EnterpriseWechatEvent(infoType, result));
+    }
+
+}

+ 47 - 0
src/main/java/com/qqflow/engine/domain/ewechat/service/impl/EnterpriseWechatServiceImpl.java

@@ -0,0 +1,47 @@
+package com.qqflow.engine.domain.ewechat.service.impl;
+
+import com.qqflow.engine.common.util.RedisCache;
+import com.qqflow.engine.domain.ewechat.api.EnterpriseWechatApi;
+import com.qqflow.engine.domain.ewechat.mapper.EnterpriseWechatCorpMapper;
+import com.qqflow.engine.domain.ewechat.model.enterprise.rsp.EnterpriseWechatAccessTokenRsp;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+import com.qqflow.engine.domain.ewechat.service.EnterpriseWechatService;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/19
+ */
+@Service
+@RequiredArgsConstructor
+public class EnterpriseWechatServiceImpl implements EnterpriseWechatService {
+    private static final String ACCESS_TOKEN_KEY_PREFIX = "access_token::";
+
+    private final EnterpriseWechatApi enterpriseWechatApi;
+
+    private final RedisCache redisCache;
+
+    private final EnterpriseWechatCorpMapper corpMapper;
+
+    @Override
+    public String getAccessToken(String corpId, String suiteId) {
+        String key = ACCESS_TOKEN_KEY_PREFIX + suiteId + "::" + corpId;
+        String accessToken = redisCache.getCacheObject(key);
+        if(StringUtils.isBlank(accessToken)){
+            EnterpriseWechatCorp corp = corpMapper.selectByCorpIdAndSuiteId(corpId, suiteId);
+            EnterpriseWechatAccessTokenRsp tokenRsp = enterpriseWechatApi.getAccessToken(corpId, corp.getPermanentCode());
+            redisCache.setCacheObject(key, tokenRsp.getAccessToken(), tokenRsp.getExpiresIn() - 1000, TimeUnit.SECONDS);
+            accessToken = tokenRsp.getAccessToken();
+        }
+        return accessToken;
+    }
+
+    @Override
+    public String getAccessToken(EnterpriseWechatCorp corp) {
+        return this.getAccessToken(corp.getCorpId(), corp.getSuiteId());
+    }
+}

+ 28 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/EnterpriseWechatUtil.java

@@ -0,0 +1,28 @@
+package com.qqflow.engine.domain.ewechat.util;
+
+import com.qqflow.engine.common.util.XmlUtil;
+import com.qqflow.engine.domain.ewechat.enums.EnterpriseWechatInfoType;
+import com.qqflow.engine.domain.ewechat.model.enterprise.callback.EnterpriseWechatDecryptBody;
+import com.qqflow.engine.domain.ewechat.po.EnterpriseWechatCorp;
+
+/**
+ * @author WuWenYi
+ * @since 2025/3/18
+ */
+public final class EnterpriseWechatUtil {
+    private static final ThreadLocal<EnterpriseWechatCorp> corpHolder = new ThreadLocal<>();
+
+    public static EnterpriseWechatInfoType getInfoType(String decryptMsg) {
+        EnterpriseWechatDecryptBody decryptBody = XmlUtil.fromXml(decryptMsg, EnterpriseWechatDecryptBody.class);
+        return decryptBody.getInfoType();
+    }
+
+    public static EnterpriseWechatCorp getCorp() {
+        return corpHolder.get();
+    }
+
+    public static void setCorp(EnterpriseWechatCorp corp) {
+        corpHolder.set(corp);
+    }
+
+}

+ 59 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/AesException.java

@@ -0,0 +1,59 @@
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+@SuppressWarnings("serial")
+public class AesException extends Exception {
+
+	public final static int OK = 0;
+	public final static int ValidateSignatureError = -40001;
+	public final static int ParseJsonError = -40002;
+	public final static int ComputeSignatureError = -40003;
+	public final static int IllegalAesKey = -40004;
+	public final static int ValidateCorpidError = -40005;
+	public final static int EncryptAESError = -40006;
+	public final static int DecryptAESError = -40007;
+	public final static int IllegalBuffer = -40008;
+	public final static int EncodeBase64Error = -40009;
+	public final static int DecodeBase64Error = -40010;
+	public final static int GenReturnJsonError = -40011;
+
+	private int code;
+
+	private static String getMessage(int code) {
+		switch (code) {
+		case ValidateSignatureError:
+			return "签名验证错误";
+		case ParseJsonError:
+			return "json解析失败";
+		case ComputeSignatureError:
+			return "sha加密生成签名失败";
+		case IllegalAesKey:
+			return "SymmetricKey非法";
+		case ValidateCorpidError:
+			return "corpid校验失败";
+		case EncryptAESError:
+			return "aes加密失败";
+		case DecryptAESError:
+			return "aes解密失败";
+		case IllegalBuffer:
+			return "解密后得到的buffer非法";
+		case EncodeBase64Error:
+			return "base64加密错误";
+		case DecodeBase64Error:
+			return "base64解密错误";
+		case GenReturnJsonError:
+			return "josn生成失败";
+		default:
+			return null; // cannot be
+		}
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	AesException(int code) {
+		super(getMessage(code));
+		this.code = code;
+	}
+
+}

+ 26 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/ByteGroup.java

@@ -0,0 +1,26 @@
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+import java.util.ArrayList;
+
+class ByteGroup {
+	ArrayList<Byte> byteContainer = new ArrayList<Byte>();
+
+	public byte[] toBytes() {
+		byte[] bytes = new byte[byteContainer.size()];
+		for (int i = 0; i < byteContainer.size(); i++) {
+			bytes[i] = byteContainer.get(i);
+		}
+		return bytes;
+	}
+
+	public ByteGroup addBytes(byte[] bytes) {
+		for (byte b : bytes) {
+			byteContainer.add(b);
+		}
+		return this;
+	}
+
+	public int size() {
+		return byteContainer.size();
+	}
+}

+ 66 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/JsonParse.java

@@ -0,0 +1,66 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2020 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+/**
+ * 针对 org.json.JSONObject,
+ * 要编译打包架包json
+ * 官方源码下载地址 : https://github.com/stleary/JSON-java, jar包下载地址 : https://mvnrepository.com/artifact/org.json/json
+ */
+import com.fasterxml.jackson.databind.JsonNode;
+import com.qqflow.engine.common.util.JsonUtil;
+
+
+/**
+ * JsonParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+class JsonParse {
+
+	/**
+	 * 提取出 JSON 包中的加密消息
+	 * @param jsontext 待提取的json字符串
+	 * @return 提取出的加密消息字符串
+	 * @throws AesException 
+	 */
+	public static Object[] extract(String jsontext) throws AesException     {
+		Object[] result = new Object[3];
+		try {
+
+			JsonNode json = JsonUtil.str2JsonNode(jsontext);
+			String encrypt_msg = json.get("encrypt").asText();
+			String tousername  = json.get("tousername").asText();
+			String agentid     = json.get("agentid").asText();
+
+			result[0] = tousername;
+			result[1] = encrypt_msg;
+			result[2] = agentid;
+			return result;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ParseJsonError);
+		}
+	}
+
+	/**
+	 * 生成json消息
+	 * @param encrypt 加密后的消息密文
+	 * @param signature 安全签名
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @return 生成的json字符串
+	 */
+	public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+		String format = "{\"encrypt\":\"%1$s\",\"msgsignature\":\"%2$s\",\"timestamp\":\"%3$s\",\"nonce\":\"%4$s\"}";
+		return String.format(format, encrypt, signature, timestamp, nonce);
+
+	}
+}

+ 67 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/PKCS7Encoder.java

@@ -0,0 +1,67 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解密接口.
+ */
+class PKCS7Encoder {
+	static Charset CHARSET = Charset.forName("utf-8");
+	static int BLOCK_SIZE = 32;
+
+	/**
+	 * 获得对明文进行补位填充的字节.
+	 * 
+	 * @param count 需要进行填充补位操作的明文字节个数
+	 * @return 补齐用的字节数组
+	 */
+	static byte[] encode(int count) {
+		// 计算需要填充的位数
+		int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+		if (amountToPad == 0) {
+			amountToPad = BLOCK_SIZE;
+		}
+		// 获得补位所用的字符
+		char padChr = chr(amountToPad);
+		String tmp = new String();
+		for (int index = 0; index < amountToPad; index++) {
+			tmp += padChr;
+		}
+		return tmp.getBytes(CHARSET);
+	}
+
+	/**
+	 * 删除解密后明文的补位字符
+	 * 
+	 * @param decrypted 解密后的明文
+	 * @return 删除补位字符后的明文
+	 */
+	static byte[] decode(byte[] decrypted) {
+		int pad = (int) decrypted[decrypted.length - 1];
+		if (pad < 1 || pad > 32) {
+			pad = 0;
+		}
+		return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+	}
+
+	/**
+	 * 将数字转化成ASCII码对应的字符,用于对明文进行补码
+	 * 
+	 * @param a 需要转化的数字
+	 * @return 转化得到的字符
+	 */
+	static char chr(int a) {
+		byte target = (byte) (a & 0xFF);
+		return (char) target;
+	}
+
+}

+ 61 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/SHA1.java

@@ -0,0 +1,61 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/**
+ * SHA1 class
+ *
+ * 计算消息签名接口.
+ */
+class SHA1 {
+
+	/**
+	 * 用SHA1算法生成安全签名
+	 * @param token 票据
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @param encrypt 密文
+	 * @return 安全签名
+	 * @throws AesException 
+	 */
+	public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
+			  {
+		try {
+			String[] array = new String[] { token, timestamp, nonce, encrypt };
+			StringBuffer sb = new StringBuffer();
+			// 字符串排序
+			Arrays.sort(array);
+			for (int i = 0; i < 4; i++) {
+				sb.append(array[i]);
+			}
+			String str = sb.toString();
+			// SHA1签名生成
+			MessageDigest md = MessageDigest.getInstance("SHA-1");
+			md.update(str.getBytes());
+			byte[] digest = md.digest();
+
+			StringBuffer hexstr = new StringBuffer();
+			String shaHex = "";
+			for (int i = 0; i < digest.length; i++) {
+				shaHex = Integer.toHexString(digest[i] & 0xFF);
+				if (shaHex.length() < 2) {
+					hexstr.append(0);
+				}
+				hexstr.append(shaHex);
+			}
+			return hexstr.toString();
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ComputeSignatureError);
+		}
+	}
+}

+ 284 - 0
src/main/java/com/qqflow/engine/domain/ewechat/util/aes/WXBizJsonMsgCrypt.java

@@ -0,0 +1,284 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+/**
+ * 针对org.apache.commons.codec.binary.Base64,
+ * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
+ * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
+ */
+package com.qqflow.engine.domain.ewechat.util.aes;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).
+ * <ol>
+ * 	<li>第三方回复加密消息给企业微信</li>
+ * 	<li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>
+ * </ol>
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
+ * <ol>
+ * 	<li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
+ *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
+ * 	<li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
+ * 	<li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
+ * 	<li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
+ * </ol>
+ */
+public class WXBizJsonMsgCrypt {
+	static Charset CHARSET = Charset.forName("utf-8");
+	Base64 base64 = new Base64();
+	byte[] aesKey;
+	String token;
+	String receiveid;
+
+	/**
+	 * 构造函数
+	 * @param token 企业微信后台,开发者设置的token
+	 * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
+	 * @param receiveid, 不同场景含义不同,详见文档
+	 * 
+	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+	 */
+	public WXBizJsonMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
+		if (encodingAesKey.length() != 43) {
+			throw new AesException(AesException.IllegalAesKey);
+		}
+
+		this.token = token;
+		this.receiveid = receiveid;
+		aesKey = Base64.decodeBase64(encodingAesKey + "=");
+	}
+
+	// 生成4个字节的网络字节序
+	byte[] getNetworkBytesOrder(int sourceNumber) {
+		byte[] orderBytes = new byte[4];
+		orderBytes[3] = (byte) (sourceNumber & 0xFF);
+		orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
+		orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
+		orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
+		return orderBytes;
+	}
+
+	// 还原4个字节的网络字节序
+	int recoverNetworkBytesOrder(byte[] orderBytes) {
+		int sourceNumber = 0;
+		for (int i = 0; i < 4; i++) {
+			sourceNumber <<= 8;
+			sourceNumber |= orderBytes[i] & 0xff;
+		}
+		return sourceNumber;
+	}
+
+	// 随机生成16位字符串
+	String getRandomStr() {
+		String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+		Random random = new Random();
+		StringBuffer sb = new StringBuffer();
+		for (int i = 0; i < 16; i++) {
+			int number = random.nextInt(base.length());
+			sb.append(base.charAt(number));
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * 对明文进行加密.
+	 * 
+	 * @param text 需要加密的明文
+	 * @return 加密后base64编码的字符串
+	 * @throws AesException aes加密失败
+	 */
+	String encrypt(String randomStr, String text) throws AesException {
+		ByteGroup byteCollector = new ByteGroup();
+		byte[] randomStrBytes = randomStr.getBytes(CHARSET);
+		byte[] textBytes = text.getBytes(CHARSET);
+		byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
+		byte[] receiveidBytes = receiveid.getBytes(CHARSET);
+
+		// randomStr + networkBytesOrder + text + receiveid
+		byteCollector.addBytes(randomStrBytes);
+		byteCollector.addBytes(networkBytesOrder);
+		byteCollector.addBytes(textBytes);
+		byteCollector.addBytes(receiveidBytes);
+
+		// ... + pad: 使用自定义的填充方式对明文进行补位填充
+		byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
+		byteCollector.addBytes(padBytes);
+
+		// 获得最终的字节流, 未加密
+		byte[] unencrypted = byteCollector.toBytes();
+
+		try {
+			// 设置加密模式为AES的CBC模式
+			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+			SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
+			IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+			cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+
+			// 加密
+			byte[] encrypted = cipher.doFinal(unencrypted);
+
+			// 使用BASE64对加密后的字符串进行编码
+			String base64Encrypted = base64.encodeToString(encrypted);
+
+			return base64Encrypted;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.EncryptAESError);
+		}
+	}
+
+	/**
+	 * 对密文进行解密.
+	 * 
+	 * @param text 需要解密的密文
+	 * @return 解密得到的明文
+	 * @throws AesException aes解密失败
+	 */
+	String decrypt(String text) throws AesException {
+		byte[] original;
+		try {
+			// 设置解密模式为AES的CBC模式
+			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+			SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
+			IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
+			cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+
+			// 使用BASE64对密文进行解码
+			byte[] encrypted = Base64.decodeBase64(text);
+
+			// 解密
+			original = cipher.doFinal(encrypted);
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.DecryptAESError);
+		}
+
+		String jsonContent, from_receiveid;
+		try {
+			// 去除补位字符
+			byte[] bytes = PKCS7Encoder.decode(original);
+
+			// 分离16位随机字符串,网络字节序和receiveid
+			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
+
+			int jsonLength = recoverNetworkBytesOrder(networkOrder);
+
+			jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), CHARSET);
+			from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, bytes.length),
+					CHARSET);
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.IllegalBuffer);
+		}
+
+		// receiveid不相同的情况
+		if (!from_receiveid.equals(receiveid)) {
+			throw new AesException(AesException.ValidateCorpidError);
+		}
+		return jsonContent;
+
+	}
+
+	/**
+	 * 将企业微信回复用户的消息加密打包.
+	 * <ol>
+	 * 	<li>对要发送的消息进行AES-CBC加密</li>
+	 * 	<li>生成安全签名</li>
+	 * 	<li>将消息密文和安全签名打包成json格式</li>
+	 * </ol>
+	 * 
+	 * @param replyMsg 企业微信待回复用户的消息,json格式的字符串
+	 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
+	 * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
+	 * 
+	 * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串
+	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+	 */
+	public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
+		// 加密
+		String encrypt = encrypt(getRandomStr(), replyMsg);
+
+		// 生成安全签名
+		if (timeStamp == "") {
+			timeStamp = Long.toString(System.currentTimeMillis());
+		}
+
+		String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
+
+		// System.out.println("发送给平台的签名是: " + signature[1].toString());
+		// 生成发送的json
+		String result = JsonParse.generate(encrypt, signature, timeStamp, nonce);
+		return result;
+	}
+
+	/**
+	 * 检验消息的真实性,并且获取解密后的明文.
+	 * <ol>
+	 * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
+	 * 	<li>若验证通过,则提取json中的加密消息</li>
+	 * 	<li>对消息进行解密</li>
+	 * </ol>
+	 * 
+	 * @param msgSignature 签名串,对应URL参数的msg_signature
+	 * @param timeStamp 时间戳,对应URL参数的timestamp
+	 * @param nonce 随机串,对应URL参数的nonce
+	 * @param postData 密文,对应POST请求的数据
+	 * 
+	 * @return 解密后的原文
+	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+	 */
+	public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String encryptedMsg)
+			throws AesException {
+
+
+		// 验证安全签名
+		String signature = SHA1.getSHA1(token, timeStamp, nonce, encryptedMsg);
+
+		// 和URL中的签名比较是否相等
+		// System.out.println("第三方收到URL中的签名:" + msg_sign);
+		// System.out.println("第三方校验签名:" + signature);
+		if (!signature.equals(msgSignature)) {
+			throw new AesException(AesException.ValidateSignatureError);
+		}
+
+		// 解密
+        return decrypt(encryptedMsg);
+	}
+
+	/**
+	 * 验证URL
+	 * @param msgSignature 签名串,对应URL参数的msg_signature
+	 * @param timeStamp 时间戳,对应URL参数的timestamp
+	 * @param nonce 随机串,对应URL参数的nonce
+	 * @param echoStr 随机串,对应URL参数的echostr
+	 * 
+	 * @return 解密之后的echostr
+	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
+	 */
+	public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
+			throws AesException {
+		String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
+
+		if (!signature.equals(msgSignature)) {
+			throw new AesException(AesException.ValidateSignatureError);
+		}
+
+		String result = decrypt(echoStr);
+		return result;
+	}
+
+}

+ 7 - 1
src/main/resources/application-test.yml

@@ -30,4 +30,10 @@ logging:
   level:
     root: info
     com.qqflow.engine: debug
-    com.qqflow.engine.**.mapper: debug  # MyBatis mapper SQL 语句日志(不输出结果集)
+    com.qqflow.engine.**.mapper: debug  # MyBatis mapper SQL 语句日志(不输出结果集)
+
+# 企微
+enterprise-wechat:
+  sCorp-id: ww76b6c51c3935e90d
+  token: jGgY9aLKA
+  encoding-a-eSKey: IycRI8M3tgTIHrFiwWnYWJn9I1o8iIsQrhNy8M4MWnT