Переглянути джерело

Merge branch 'refs/heads/企微聊天'

ct 2 тижнів тому
батько
коміт
066de4a63f
100 змінених файлів з 3258 додано та 1456 видалено
  1. 11 46
      fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  2. 11 25
      fs-ad-api/src/main/java/com/fs/framework/config/RedisConfig.java
  3. 3 0
      fs-admin/src/main/java/com/fs/his/controller/FsUserOperationLogController.java
  4. 11 45
      fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  5. 12 25
      fs-common-api/src/main/java/com/fs/framework/config/RedisConfig.java
  6. 2 1
      fs-common/src/main/java/com/fs/common/enums/DataSourceType.java
  7. 21 0
      fs-common/src/main/java/com/fs/common/utils/PinYinUtil.java
  8. 11 45
      fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java
  9. 52 41
      fs-company-app/src/main/java/com/fs/core/config/RedisConfig.java
  10. 4 0
      fs-company/pom.xml
  11. 3 0
      fs-company/src/main/java/com/fs/company/controller/course/FsUserOperationLogController.java
  12. 22 0
      fs-company/src/main/java/com/fs/company/controller/param/QwExternalContactEditTagDTO.java
  13. 358 17
      fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java
  14. 5 4
      fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java
  15. 12 47
      fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  16. 0 20
      fs-company/src/main/java/com/fs/framework/config/RedisConfig.java
  17. 11 45
      fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  18. 10 27
      fs-doctor-app/src/main/java/com/fs/framework/config/RedisConfig.java
  19. 6 2
      fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java
  20. 11 45
      fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  21. 10 27
      fs-framework/src/main/java/com/fs/framework/config/RedisConfig.java
  22. 117 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  23. 11 45
      fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  24. 12 29
      fs-ipad-task/src/main/java/com/fs/framework/config/RedisConfig.java
  25. 11 45
      fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  26. 11 26
      fs-live-app/src/main/java/com/fs/framework/config/RedisConfig.java
  27. 4 0
      fs-qw-api-msg/pom.xml
  28. 253 6
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  29. 108 0
      fs-qw-api-msg/src/main/java/com/fs/app/socket/QwImSocket.java
  30. 34 0
      fs-qw-api-msg/src/main/java/com/fs/app/socket/configurator/QwImConfigurator.java
  31. 160 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/AudioUtils.java
  32. 5 4
      fs-qw-api-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java
  33. 11 45
      fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  34. 1 10
      fs-qw-api-msg/src/main/java/com/fs/framework/config/RedisConfig.java
  35. 2 1
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  36. 17 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java
  37. 14 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/TokenService.java
  38. 11 45
      fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  39. 19 35
      fs-qw-api/src/main/java/com/fs/framework/config/RedisConfig.java
  40. 11 45
      fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  41. 11 27
      fs-qw-mq/src/main/java/com/fs/framework/config/RedisConfig.java
  42. 11 45
      fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  43. 11 28
      fs-qw-task/src/main/java/com/fs/framework/config/RedisConfig.java
  44. 11 45
      fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  45. 11 25
      fs-qw-voice/src/main/java/com/fs/framework/config/RedisConfig.java
  46. 11 45
      fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  47. 12 28
      fs-qwhook-msg/src/main/java/com/fs/framework/config/RedisConfig.java
  48. 11 45
      fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  49. 11 26
      fs-qwhook-sop/src/main/java/com/fs/framework/config/RedisConfig.java
  50. 11 45
      fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  51. 11 28
      fs-qwhook/src/main/java/com/fs/framework/config/RedisConfig.java
  52. 11 45
      fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  53. 30 27
      fs-redis/src/main/java/com/fs/framework/config/RedisConfig.java
  54. 11 45
      fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  55. 19 33
      fs-repeat-api/src/main/java/com/fs/framework/config/RedisConfig.java
  56. 4 1
      fs-service/pom.xml
  57. 1 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  58. 5 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  59. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCourseLinkMiniParam.java
  60. 4 0
      fs-service/src/main/java/com/fs/course/param/FsCourseListBySidebarParam.java
  61. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  62. 5 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  63. 8 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  64. 105 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  65. 27 0
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogIMVO.java
  66. 3 0
      fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtApiResponse.java
  67. 3 0
      fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtBaseResponseDTO.java
  68. 3 0
      fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtStockRespDTO.java
  69. 3 0
      fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtTradeQueryResponse.java
  70. 2 7
      fs-service/src/main/java/com/fs/erp/service/impl/WdtErpGoodsServiceImpl.java
  71. 6 20
      fs-service/src/main/java/com/fs/erp/service/impl/WdtErpOrderServiceImpl.java
  72. 43 0
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  73. 572 14
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  74. 8 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserOperationLogMapper.java
  75. 5 0
      fs-service/src/main/java/com/fs/his/service/IFsUserOperationLogService.java
  76. 20 1
      fs-service/src/main/java/com/fs/his/service/impl/FsUserOperationLogServiceImpl.java
  77. 1 0
      fs-service/src/main/java/com/fs/ipad/vo/BaseVo.java
  78. 5 0
      fs-service/src/main/java/com/fs/qw/domain/QwMsg.java
  79. 17 0
      fs-service/src/main/java/com/fs/qw/domain/QwSession.java
  80. 3 0
      fs-service/src/main/java/com/fs/qw/domain/QwUser.java
  81. 23 0
      fs-service/src/main/java/com/fs/qw/dto/QwImUserDTO.java
  82. 28 0
      fs-service/src/main/java/com/fs/qw/enums/MsgType.java
  83. 20 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  84. 6 0
      fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java
  85. 5 2
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgMapper.java
  86. 27 2
      fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java
  87. 9 0
      fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java
  88. 8 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  89. 8 0
      fs-service/src/main/java/com/fs/qw/param/QwMsgSendParam.java
  90. 3 1
      fs-service/src/main/java/com/fs/qw/param/QwSessionParam.java
  91. 24 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java
  92. 21 7
      fs-service/src/main/java/com/fs/qw/service/IQwMsgService.java
  93. 10 0
      fs-service/src/main/java/com/fs/qw/service/IQwTagService.java
  94. 10 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  95. 69 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  96. 453 63
      fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java
  97. 16 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwTagServiceImpl.java
  98. 16 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  99. 12 1
      fs-service/src/main/java/com/fs/qw/vo/QwContactListVO.java
  100. 35 0
      fs-service/src/main/java/com/fs/qw/vo/QwContactVO.java

+ 11 - 46
fs-ad-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,10 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
-
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +13,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
-
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
+    private final Class<T> clazz;
 
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return com.alibaba.fastjson2.JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 25
fs-ad-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,36 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);

+ 3 - 0
fs-admin/src/main/java/com/fs/his/controller/FsUserOperationLogController.java

@@ -1,8 +1,10 @@
 package com.fs.his.controller;
 
+import com.fs.common.annotation.DataSource;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.FsUserOperationLog;
 import com.fs.his.service.IFsUserOperationLogService;
 import com.fs.his.vo.FsUserOperationLogParamVo;
@@ -29,6 +31,7 @@ public class FsUserOperationLogController extends BaseController {
     private IFsUserOperationLogService fsUserOperationLogService;
 
 //    @PreAuthorize("@ss.hasPermi('his:userOperationLog:list')")
+    @DataSource(DataSourceType.SHARDING)
     @GetMapping("/list")
     public TableDataInfo list(FsUserOperationLog fsUserOperationLog)
     {

+ 11 - 45
fs-common-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 12 - 25
fs-common-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,40 +41,42 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
     @SuppressWarnings(value = { "unchecked", "rawtypes" })
     public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
@@ -93,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);

+ 2 - 1
fs-common/src/main/java/com/fs/common/enums/DataSourceType.java

@@ -19,5 +19,6 @@ public enum DataSourceType
      * 从库
      */
     SLAVE,
-    SopREAD
+    SopREAD,
+    SHARDING
 }

+ 21 - 0
fs-common/src/main/java/com/fs/common/utils/PinYinUtil.java

@@ -91,6 +91,27 @@ public class PinYinUtil {
         }
     }
 
+    /**
+     * 获取字符串首字母
+     */
+    public static String getFirstLetter(String str) {
+        if (str == null || str.isEmpty()) {
+            return "";
+        }
+
+        // 去除前后空格
+        str = str.trim();
+        char firstChar = str.charAt(0);
+
+        char firstLetter = Char2Initial(firstChar);
+        if (Character.isLetter(firstLetter)) {
+            return String.valueOf(firstLetter).toUpperCase();
+        }
+
+        return "";
+    }
+
+
     public static void main(String[] args) throws Exception {
         System.out.println(cn2py("重庆重视发展IT行业,大多数外企,如,IBM等进驻山城"));
     }

+ 11 - 45
fs-company-app/src/main/java/com/fs/core/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.core.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 52 - 41
fs-company-app/src/main/java/com/fs/core/config/RedisConfig.java

@@ -1,14 +1,12 @@
 package com.fs.core.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.serializer.GenericToStringSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@@ -32,37 +30,13 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
-        serializer.setObjectMapper(mapper);
-
-        template.setValueSerializer(serializer);
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // Hash的key也采用StringRedisSerializer的序列化方式 这个才是redis的hash值的序列化方式 一直都没有序列化进去
-        template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(serializer);
-        template.afterPropertiesSet();
-
-        // Hash的key也采用StringRedisSerializer的序列化方式 这个才是redis的hash值的序列化方式 一直都没有序列化进去
-        template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(serializer);
-        return template;
-    }
-    @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
-        template.setConnectionFactory(connectionFactory);
-
-        // 使用StringRedisSerializer来序列化和反序列化redis的key值
-        template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setValueSerializer(serializer);
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(serializer);
 
         template.afterPropertiesSet();
         return template;
@@ -86,21 +60,18 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -114,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -130,4 +96,49 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
 }

+ 4 - 0
fs-company/pom.xml

@@ -112,6 +112,10 @@
             <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
             <version>3.1.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.shardingsphere</groupId>
+            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 3 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsUserOperationLogController.java

@@ -1,8 +1,10 @@
 package com.fs.company.controller.course;
 
+import com.fs.common.annotation.DataSource;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.DataSourceType;
 import com.fs.his.domain.FsUserOperationLog;
 import com.fs.his.service.IFsUserOperationLogService;
 import com.fs.his.vo.FsUserOperationLogVo;
@@ -26,6 +28,7 @@ public class FsUserOperationLogController extends BaseController {
     private IFsUserOperationLogService fsUserOperationLogService;
 
     //@PreAuthorize("@ss.hasPermi('his:userOperationLog:list')")
+    @DataSource(DataSourceType.SHARDING)
     @GetMapping("/list")
     public TableDataInfo list(FsUserOperationLog fsUserOperationLog)
     {

+ 22 - 0
fs-company/src/main/java/com/fs/company/controller/param/QwExternalContactEditTagDTO.java

@@ -0,0 +1,22 @@
+package com.fs.company.controller.param;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@ApiModel("外部联系人标签调整")
+@Data
+public class QwExternalContactEditTagDTO {
+
+    @NotNull(message = "企微外部联系人Id不能为空")
+    @ApiModelProperty("企微外部联系人ID")
+    private Long qwExternalContactId;
+
+    @NotEmpty(message = "企微标签Id不能为空")
+    @ApiModelProperty("企微标签ID集合")
+    private List<Long> tagIds;
+}

+ 358 - 17
fs-company/src/main/java/com/fs/company/controller/qw/QwMsgController.java

@@ -1,23 +1,45 @@
 package com.fs.company.controller.qw;
 
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.TypeReference;
+import com.fs.common.annotation.DataSource;
 import com.fs.common.annotation.Log;
+import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.exception.CustomException;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.controller.param.QwExternalContactEditTagDTO;
+import com.fs.course.param.FsCourseLinkMiniParam;
+import com.fs.course.param.FsCourseListBySidebarParam;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.vo.FsCourseListBySidebarVO;
+import com.fs.course.vo.FsCourseVideoListBySidebarVO;
+import com.fs.course.vo.FsCourseWatchLogIMVO;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
-import com.fs.qw.domain.QwMsg;
-import com.fs.qw.domain.QwUser;
+import com.fs.his.domain.FsStoreOrder;
+import com.fs.his.domain.FsUserOperationLog;
+import com.fs.his.service.IFsStoreOrderService;
+import com.fs.his.service.IFsUserOperationLogService;
+import com.fs.his.vo.FsUserOperationLogVo;
+import com.fs.qw.domain.*;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
-import com.fs.qw.service.IQwMsgService;
-import com.fs.qw.vo.QwContactListVO;
-import com.fs.qw.vo.QwMessageListVO;
+import com.fs.qw.service.*;
+import com.fs.qw.vo.*;
+import com.fs.statis.service.IStatisticsService;
+import com.fs.statistics.dto.WatchCourseStatisticsDTO;
+import com.fs.statistics.param.WatchCourseStatisticsParam;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -26,8 +48,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
  * 企微聊天记录Controller
@@ -44,6 +66,30 @@ public class QwMsgController extends BaseController
     private IQwMsgService qwMsgService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private IQwExternalContactInfoService qwExternalContactInfoService;
+    @Autowired
+    private IQwExternalContactService qwExternalContactService;
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    private IStatisticsService statisticsService;
+    @Autowired
+    private IFsUserCourseService fsUserCourseService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
+    @Autowired
+    private IFsStoreOrderService storeOrderService;
+    @Autowired
+    private IFsCourseWatchLogService watchLogService;
+    @Autowired
+    private IFsUserOperationLogService userOperationLogService;
+    @Autowired
+    private IQwSessionService sessionService;
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+    @Autowired
+    private IQwTagService tagService;
 
     /**
      * 查询企微聊天记录列表
@@ -120,24 +166,28 @@ public class QwMsgController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         //员工userId
         Long userId = loginUser.getUser().getUserId();
-        List<QwUser> qwUsers = qwMsgService.qwUserList(userId);
-        return R.ok().put("data",qwUsers);
+        return R.ok().put("data", qwUserService.selectQwUserVoListByCompanyUserId(userId));
     }
     //获取会话
     @GetMapping("/conversationList/{userId}")
     @ApiOperation("获取会话")
-    public R conversations(@PathVariable("userId")Long qwUserId){
-        List<QwContactListVO> list = qwMsgService.selectQwConversationByUserId(qwUserId);
-        for (QwContactListVO contract:list) {
-            if(StringUtils.isEmpty(contract.getDisplayName())){
-                contract.setDisplayName("群聊");
-            }
-        }
-        return R.ok().put("data",list);
+    public R conversations(@PathVariable("userId") Long qwUserId,
+                           @RequestParam(required = false) Boolean removeBlack,
+                           @RequestParam(required = false) Boolean removeRepeat){
+        Map<String, Object> params = new HashMap<>();
+        params.put("qwUserId", qwUserId.toString());
+        params.put("removeBlack", removeBlack);
+        params.put("removeRepeat", removeRepeat);
+
+        startPage();
+        List<QwContactListVO> list = qwMsgService.selectQwConversationByMap(params);
+        PageInfo<QwContactListVO> result = new PageInfo<>(list);
+        return R.ok().put("data", result);
     }
     //根据会话获取消息
     @GetMapping("/getQwMessageListBySession")
     @ApiOperation("根据会话获取消息")
+    @DataSource(DataSourceType.SHARDING)
     public R getQwMessageListBySession(QwSessionParam param){
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<QwMsg> list = qwMsgService.selectQwMsgBySession(param);
@@ -148,7 +198,9 @@ public class QwMsgController extends BaseController
         return R.ok().put("data",listPageInfo);
     }
 
+    @ApiOperation("发送企微消息")
     @PostMapping("/sendMsg")
+    @DataSource(DataSourceType.SHARDING)
     public R sendMsg(@RequestBody QwMsgSendParam param){
         return qwMsgService.sendMsg(param);
     }
@@ -156,8 +208,297 @@ public class QwMsgController extends BaseController
 
     //获取用户单条会话
     @GetMapping("/getSession")
+    @DataSource(DataSourceType.SHARDING)
     public R getSession(QwSessionParam param){
         QwContactListVO data = qwMsgService.selectQwSessionBycId(param.getConversationId(),param.getUserId());
         return R.ok().put("data",data);
     }
+
+    @ApiOperation("获取外部联系人详情")
+    @GetMapping("/getQwExternalContactDetails")
+    public R getQwExternalContactDetails(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId){
+        return R.ok().put("data", qwExternalContactService.getQwExternalContactDetailsById(qwExternalContactId));
+    }
+
+    @GetMapping("/getQwUserInfo")
+    @ApiOperation("获取外部联系人用户信息")
+    public R getQwUserInfo(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId){
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if(externalContact == null) {
+            throw new CustomException("企微外部联系人id不能为空!");
+        }
+
+        QwExternalContactInfo contactInfo = qwExternalContactInfoService.selectQwExternalContactInfoByExternalContactId(qwExternalContactId);
+        if (contactInfo==null){
+            contactInfo = new QwExternalContactInfo();
+            contactInfo.setExternalContactId(qwExternalContactId);
+            contactInfo.setName(externalContact.getName());
+            contactInfo.setSex(externalContact.getGender() == 1 ? "男" : externalContact.getGender() ==  2 ? "女" : "未知");
+            qwExternalContactInfoService.insertQwExternalContactInfo(contactInfo);
+        }
+
+        // 已购状态
+        Boolean isBuy = qwExternalContactService.getBuyStatusByExtId(qwExternalContactId);
+        contactInfo.setIsBuy(isBuy ? "是" : "否");
+
+        return R.ok().put("moreInfo",contactInfo);
+    }
+
+    @PostMapping("/updateQwUserInfo")
+    @ApiOperation("更新企微用户信息")
+    public R updateQwUserInfo(@RequestBody QwExternalContactInfo qwExternalContactInfo){
+        if(qwExternalContactInfo.getExternalContactId() == null) {
+            throw new CustomException("企微外部联系人id不能为空!");
+        }
+        qwExternalContactInfoService.updateQwExternalContactInfoByExternalContactId(qwExternalContactInfo);
+        return R.ok();
+    }
+
+    @PostMapping("/course/watch")
+    @ApiOperation("查询看课记录")
+    public R queryCourseWatchStatistics(@RequestBody WatchCourseStatisticsParam param) {
+        if(param.getQwExternalContactId() == null) {
+            throw new CustomException("外部联系人id为空!");
+        }
+
+        WatchCourseStatisticsDTO watchCourseStatisticsDTO = statisticsService.queryWatchCourse(param);
+
+        return R.ok().put("data",watchCourseStatisticsDTO);
+    }
+
+    @PostMapping("/getFsCourseListBySidebar")
+    @ApiOperation("获取视频课程下拉列表 侧边栏")
+    public R getFsCourseListBySidebar(@RequestBody FsCourseListBySidebarParam param) {
+        QwSession qwSession = sessionService.selectQwSessionBySessionId(param.getSessionId());
+        if (qwSession == null) {
+            return R.error("会话不存在");
+        }
+
+        QwUser qwUser = qwUserService.selectQwUserById(Long.parseLong(qwSession.getQwUserId()));
+        if (qwUser == null || qwUser.getCompanyId() == null) {
+            return R.error("员工未绑定 销售公司 或 未获取到员工信息,请重试!");
+        }
+        param.setCompanyId(qwUser.getCompanyId());
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseListBySidebarVO> fsCourseListBySidebar = fsUserCourseService.getFsCourseListBySidebar(param);
+        PageInfo<FsCourseListBySidebarVO> result = new PageInfo<>(fsCourseListBySidebar);
+        return R.ok().put("data", result);
+    }
+
+    @PostMapping("/getFsCourseVideoListBySidebar")
+    @ApiOperation("获取视频课程的课节下拉列表 侧边栏")
+    public R getFsCourseVideoListBySidebar(@RequestBody FsCourseListBySidebarParam param) {
+
+        if (param.getCourseId()==null){
+            return R.error("课程id不能为空");
+        }
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseVideoListBySidebarVO> videoListBySidebar = fsUserCourseVideoService.getFsCourseVideoListBySidebar(param);
+        PageInfo<FsCourseVideoListBySidebarVO> result = new PageInfo<>(videoListBySidebar);
+        return R.ok().put("data", result);
+    }
+
+    /**
+     * 创建 发客户小程序
+     */
+    @RepeatSubmit
+    @PostMapping("/createMiniLink")
+    public R createMiniLink(@RequestBody FsCourseLinkMiniParam param) {
+
+        if (Objects.isNull(param.getCourseId())){
+            return R.error("课程id不能为空");
+        }
+        if (Objects.isNull(param.getVideoId())){
+            return R.error("视频id不能为空");
+        }
+
+        if (Objects.isNull(param.getSessionId())){
+            return R.error("会话ID不能为空");
+        }
+
+        return fsUserCourseVideoService.createMiniLinkByQwIm(param);
+    }
+
+    // 获取联系人列表
+    @GetMapping("/contactList/{userId}")
+    @ApiOperation("获取联系人列表")
+    public TableDataInfo contacts(@PathVariable("userId") Long qwUserId, @RequestParam(required = false) String name) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.contactListByQwUserId(qwUserId, name);
+        return getDataTable(list);
+    }
+
+    // 获取群组列表
+    @GetMapping("/groupList/{userId}")
+    @ApiOperation("获取群组列表")
+    public TableDataInfo groups(@PathVariable("userId") Long qwUserId, @RequestParam(required = false) String name) {
+        startPage();
+        List<QwContactVO> list  = qwMsgService.groupListByQwUserId(qwUserId, name);
+        return getDataTable(list);
+    }
+
+    // 获取会话ID
+    @GetMapping("/getConversationId")
+    @ApiOperation("获取会话ID")
+    public R getConversationId(@RequestParam Long qwUserId, @RequestParam String id, @RequestParam Boolean isGroup) {
+        return R.ok().put("data", qwMsgService.getConversationIdById(qwUserId, id, isGroup));
+    }
+
+    @ApiOperation("获取外部联系人订单列表")
+    @GetMapping("/getQwExternalContactOrderList")
+    public TableDataInfo getQwExternalContactOrderList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<FsStoreOrder> orderList = new ArrayList<>();
+        if (externalContact.getFsUserId() != null) {
+            FsStoreOrder params = new FsStoreOrder();
+            params.setUserId(externalContact.getFsUserId());
+            params.setCompanyId(externalContact.getCompanyId());
+            params.setCompanyUserId(externalContact.getCompanyUserId());
+
+            startPage();
+            orderList = storeOrderService.selectFsStoreOrderList(params);
+        }
+
+        return getDataTable(orderList);
+    }
+
+    @ApiOperation("获取外部联系人看课记录列表")
+    @GetMapping("/getQwExternalContactWatchLogList")
+    public TableDataInfo getQwExternalContactWatchLogList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        Map<String, Object> params = new HashMap<>();
+        params.put("externalContactId", externalContact.getId());
+        params.put("companyId", externalContact.getCompanyId());
+        params.put("companyUserId", externalContact.getCompanyUserId());
+
+        startPage();
+        List<FsCourseWatchLogIMVO> logList = watchLogService.selectWatchLogIMVOListByMap(params);
+        return getDataTable(logList);
+    }
+
+    @DataSource(DataSourceType.SHARDING)
+    @ApiOperation("获取外部联系人访问记录列表")
+    @GetMapping("/getQwExternalContactVisitList")
+    public TableDataInfo getQwExternalContactVisitList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<FsUserOperationLogVo> logList = new ArrayList<>();
+        if (externalContact.getFsUserId() != null) {
+            FsUserOperationLog params = new FsUserOperationLog();
+            params.setUserId(externalContact.getFsUserId());
+
+            startPage();
+            logList = userOperationLogService.selectFsUserOperationLogByList(params);
+        }
+
+        return getDataTable(logList);
+    }
+
+    @ApiOperation("获取外部联系人标签列表")
+    @GetMapping("/getQwExternalContactTagList")
+    public TableDataInfo getQwExternalContactTagList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+        String tagIds = externalContact.getTagIds();
+
+        List<QwTagVO> tagList = new ArrayList<>();
+        if (StringUtils.isBlank(tagIds) || "[]".equals(tagIds)) {
+            return getDataTable(tagList);
+        }
+
+        List<String> ids = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+
+        startPage();
+        tagList = tagService.selectQwTagVOListByTagIds(ids);
+        return getDataTable(tagList);
+    }
+
+    @ApiOperation("获取企微主体标签列表")
+    @GetMapping("/getCorpTagList")
+    public TableDataInfo getCorpTagList(@RequestParam(value = "qwExternalContactId") Long qwExternalContactId,
+                                        @RequestParam(required = false) String name) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(qwExternalContactId);
+        if (Objects.isNull(externalContact)){
+            return getDataTable(new ArrayList<>());
+        }
+
+        startPage();
+        QwTagGroup params = new QwTagGroup();
+        params.setName(name);
+        params.setCorpId(externalContact.getCorpId());
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(params);
+        return getDataTable(list);
+    }
+
+    @ApiOperation("外部联系人添加标签")
+    @Log(title = "添加标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/addQwExternalContactTag")
+    public R addQwExternalContactTag(@RequestBody QwExternalContactEditTagDTO param) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(param.getQwExternalContactId());
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        List<QwTag> addTags = tagService.selectQwTagListByCorpIdAndIds(param.getTagIds(), externalContact.getCorpId());
+        if (addTags.isEmpty()) {
+            throw new ServiceException("添加标签不能为空");
+        }
+
+        List<String> addTagList = new ArrayList<>();
+        if (StringUtils.isNotBlank(externalContact.getTagIds())) {
+            List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+            addTagList = addTags.stream().map(QwTag::getTagId).filter(tagId -> !oldTags.contains(tagId)).collect(Collectors.toList());
+        }
+
+        if (addTagList.isEmpty()) {
+            throw new ServiceException("添加标签不能为空");
+        }
+
+        qwExternalContactService.addQwExternalContactTag(externalContact.getId(), addTagList);
+        return R.ok();
+    }
+
+    @ApiOperation("外部联系人删除标签")
+    @Log(title = "删除标签", businessType = BusinessType.UPDATE)
+    @PostMapping("/delQwExternalContactTag")
+    public R delQwExternalContactTag(@RequestBody QwExternalContactEditTagDTO param) {
+        QwExternalContact externalContact = qwExternalContactService.selectQwExternalContactById(param.getQwExternalContactId());
+        if (Objects.isNull(externalContact)){
+            throw new ServiceException("外部联系人不存在");
+        }
+
+        if (StringUtils.isBlank(externalContact.getTagIds())) {
+            throw new ServiceException("外部联系人不存在标签");
+        }
+
+        List<QwTag> delTags = tagService.selectQwTagListByCorpIdAndIds(param.getTagIds(), externalContact.getCorpId());
+        if (delTags.isEmpty()) {
+            throw new ServiceException("删除标签不能为空");
+        }
+
+        List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+        List<String> delTagList = delTags.stream().map(QwTag::getTagId).filter(oldTags::contains).collect(Collectors.toList());
+        if (delTagList.isEmpty()) {
+            throw new ServiceException("删除标签不能为空");
+        }
+
+        qwExternalContactService.delQwExternalContactTag(externalContact.getId(), delTagList);
+        return R.ok();
+    }
+
 }

+ 5 - 4
fs-company/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -34,13 +34,14 @@ public class DataSourceConfig {
         return new DruidDataSource();
     }
 
-
-
     @Bean
     @Primary
-    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+    public DynamicDataSource dataSource(@Qualifier("shardingSphereDataSource") DataSource shardingSphereDataSource,
+                                        @Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("sopDataSource") DataSource sopDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+        targetDataSources.put(DataSourceType.SHARDING.name(), shardingSphereDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }

+ 12 - 47
fs-company/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,72 +1,37 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
-
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
- * 
+ *
 
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
-
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
+    private final Class<T> clazz;
 
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 0 - 20
fs-company/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,16 +1,7 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
-import com.fs.framework.config.FastJson2JsonRedisSerializer;
-import org.springframework.cache.CacheManager;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
-import org.springframework.cache.concurrent.ConcurrentMapCache;
-import org.springframework.cache.support.SimpleCacheManager;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -20,7 +11,6 @@ import org.springframework.data.redis.serializer.GenericToStringSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 import java.math.BigDecimal;
-import java.util.Arrays;
 
 /**
  * redis配置
@@ -40,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -100,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);

+ 11 - 45
fs-doctor-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 10 - 27
fs-doctor-app/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -53,35 +43,35 @@ public class RedisConfig extends CachingConfigurerSupport
     }
 
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -95,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -111,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -130,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 6 - 2
fs-framework/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -41,11 +41,15 @@ public class DataSourceConfig {
 
     @Bean
     @Primary
-    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,
+                                        @Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("sopDataSource") DataSource sopDataSource,
+                                        @Qualifier("shardingSphereDataSource") DataSource shardingSphereDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource);
+        targetDataSources.put(DataSourceType.SHARDING.name(), shardingSphereDataSource);
         return new DynamicDataSource(masterDataSource, targetDataSources);
     }
 

+ 11 - 45
fs-framework/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,15 +1,10 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import com.alibaba.fastjson.parser.ParserConfig;
-import org.springframework.util.Assert;
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -18,54 +13,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 10 - 27
fs-framework/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -9,11 +9,6 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.serializer.GenericToStringSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 
 import java.math.BigDecimal;
 
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -53,35 +43,35 @@ public class RedisConfig extends CachingConfigurerSupport
     }
 
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -95,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -111,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -130,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 117 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -7,12 +7,15 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;
+import com.fs.common.utils.http.HttpUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.CompanyMiniapp;
 import com.fs.company.service.ICompanyMiniappService;
+import com.fs.config.ai.AiHostProper;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.fastGpt.service.AiHookService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.ipad.IpadSendUtils;
@@ -24,8 +27,8 @@ import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.service.IQwUserVideoService;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
-import com.fs.qwApi.param.QwExternalContactHParam;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.service.IQwSopLogsService;
 import com.fs.sop.service.impl.QwSopLogsServiceImpl;
@@ -33,12 +36,14 @@ import com.fs.wxwork.dto.*;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
+import org.json.JSONObject;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -55,6 +60,24 @@ public class IpadSendServer {
     private final IQwUserVideoService qwUserVideoService;
     private final RedisCache redisCache;
     private final ICompanyMiniappService companyMiniappService;
+    private final AiHookService aiHookService;
+    private final AiHostProper aiHostProper;
+    private final ExecutorService executor = new ThreadPoolExecutor(
+            8, 32, 60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(1000),
+            new ThreadFactory() {
+                private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
+                private int count = 1;
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread thread = defaultFactory.newThread(r);
+                    thread.setName("sop-im-exec-" + count++);
+                    return thread;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
+    );
+
     private final FsUserMapper fsUserMapper;
     private static final List<String> PROJECT_NAMES = Arrays.asList("济南联志健康", "北京存在文化");
     private void sendMiniProgram(BaseVo vo, QwSopCourseFinishTempSetting.Setting content, Map<String, FsCoursePlaySourceConfig> miniMap, Long companyId) {
@@ -122,6 +145,35 @@ public class IpadSendServer {
             content.setSendStatus(2);
             content.setSendRemarks("发送失败:" + resp.getErrmsg());
         }
+
+        try {
+            WxWorkSendAppMsgRespDTO data = resp.getData();
+
+            JSONObject json = new JSONObject();
+            json.put("appid", courseMaConfig.getAppid());
+            json.put("appName", courseMaConfig.getName());
+            json.put("weappIconUrl", courseMaConfig.getImg());
+            json.put("desc", content.getMiniprogramTitle());
+            json.put("pagepath", content.getMiniprogramPage());
+            json.put("title",courseMaConfig.getName());
+            json.put("thumbnail", content.getMiniprogramPicUrl());
+
+            // 保存聊天消息
+            QwMessageListVO message = aiHookService.saveQwMsg(vo.getQwUserId(), vo.getExId(), json.toString(), 2, vo.isRoom(), data.getMsg_id(), data.getApp_info());
+            if (message == null) {
+                return;
+            }
+            // 发送webSocket
+            executor.execute(() -> {
+                try {
+                    HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                } catch (Exception e) {
+                    log.error("转发sop发送消息失败 err: {}", e.getMessage(), e);
+                }
+            });
+        } catch (Exception e) {
+            log.error("保存sop发送消息失败 err: {}", e.getMessage(), e);
+        }
     }
 
     private void sendFile(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
@@ -146,6 +198,26 @@ public class IpadSendServer {
             content.setSendStatus(2);
             content.setSendRemarks("发送失败:" + resp.getErrmsg());
         }
+
+        try {
+            WxWorkSendTextMsgRespDTO data = resp.getData();
+
+            // 保存聊天消息
+            QwMessageListVO message = aiHookService.saveQwMsg(vo.getQwUserId(), vo.getExId(), content.getValue(), 1, vo.isRoom(), data.getMsg_id(), data.getApp_info());
+            if (message == null) {
+                return;
+            }
+            // 发送webSocket
+            executor.execute(() -> {
+                try {
+                    HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                } catch (Exception e) {
+                    log.error("转发sop发送消息失败 err: {}", e.getMessage(), e);
+                }
+            });
+        } catch (Exception e) {
+            log.error("保存sop发送消息失败 err: {}", e.getMessage(), e);
+        }
     }
 
     public void sendImg(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
@@ -157,6 +229,26 @@ public class IpadSendServer {
             content.setSendStatus(2);
             content.setSendRemarks("发送失败:" + resp.getErrmsg());
         }
+
+        try {
+            WxwSendCDNImgMsgRespDTO data = resp.getData();
+
+            // 保存聊天消息
+            QwMessageListVO message = aiHookService.saveQwMsg(vo.getQwUserId(), vo.getExId(), content.getImgUrl(), 2, vo.isRoom(), data.getMsg_id(), data.getApp_info());
+            if (message == null) {
+                return;
+            }
+            // 发送webSocket
+            executor.execute(() -> {
+                try {
+                    HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                } catch (Exception e) {
+                    log.error("转发sop发送消息失败 err: {}", e.getMessage(), e);
+                }
+            });
+        } catch (Exception e) {
+            log.error("保存sop发送消息失败 err: {}", e.getMessage(), e);
+        }
     }
 
     public void sendLink(BaseVo vo, QwSopCourseFinishTempSetting.Setting content) {
@@ -228,6 +320,30 @@ public class IpadSendServer {
             content.setSendStatus(2);
             content.setSendRemarks("发送失败:" + resp.getErrmsg());
         }
+
+        try {
+            WxwSendCDNVoiceMsgRespDTO data = resp.getData();
+
+            // 保存聊天消息
+            JSONObject json = new JSONObject();
+            json.put("url", content.getVoiceUrl());
+            json.put("content", content.getValue());
+
+            QwMessageListVO message = aiHookService.saveQwMsg(vo.getQwUserId(), vo.getExId(), json.toString(), 4, vo.isRoom(), data.getMsg_id(), data.getApp_info());
+            if (message == null) {
+                return;
+            }
+            // 发送webSocket
+            executor.execute(() -> {
+                try {
+                    HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                } catch (Exception e) {
+                    log.error("转发sop发送消息失败 err: {}", e.getMessage(), e);
+                }
+            });
+        } catch (Exception e) {
+            log.error("保存sop发送消息失败 err: {}", e.getMessage(), e);
+        }
     }
 
     public boolean isSend(QwUser qwUser, BaseVo parentVo) {

+ 11 - 45
fs-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 12 - 29
fs-ipad-task/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,41 +41,42 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
     @SuppressWarnings(value = { "unchecked", "rawtypes" })
     public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -110,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -129,8 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-live-app/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 26
fs-live-app/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,36 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -110,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();

+ 4 - 0
fs-qw-api-msg/pom.xml

@@ -117,6 +117,10 @@
             <artifactId>vosk</artifactId>
             <version>0.3.32</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fs</groupId>
             <artifactId>fs-qw-api</artifactId>

+ 253 - 6
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -2,8 +2,11 @@ package com.fs.app.controller;
 
 import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
+import com.fs.app.socket.QwImSocket;
+import com.fs.app.util.AudioUtils;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.uuid.IdUtils;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.AiHookService;
@@ -23,6 +26,7 @@ import com.fs.qw.service.IQwExternalContactService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.service.IQwUserVideoService;
 import com.fs.qw.service.IQwUserVoiceLogService;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.SopUserLogsInfoMapper;
 import com.fs.sop.params.GetQwSopLogsByJsApiParam;
@@ -33,9 +37,11 @@ import io.swagger.annotations.Api;
 import lombok.extern.slf4j.Slf4j;
 import org.json.JSONObject;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.*;
+import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -78,6 +84,9 @@ public class QwMsgController {
     private IFsExpressService expressService;
     @Autowired
     private IFsStoreOrderService storeOrderService;
+    @Autowired
+    @Qualifier("threadPoolTaskExecutor")
+    private Executor executor;
 
     @GetMapping("/sendExpressInfo/{orderId}")
     public R sendExpressInfo(@PathVariable Long orderId){
@@ -253,6 +262,7 @@ public class QwMsgController {
                 qwUser.setId(id);
                 qwUser.setVid(jsonObject.get("Vid").toString());
                 qwUser.setIpadStatus(1);
+                qwUser.setAvatar(jsonObject.get("avatar").toString());
                 qwUserMapper.updateQwUser(qwUser);
                 log.info("id:{}, 存Vid", id);
                 redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),104001,10, TimeUnit.MINUTES);
@@ -302,20 +312,30 @@ public class QwMsgController {
                 break;
             case 102000:
                 WxWorkMessageDTO wxWorkMessageDTO = JSON.parseObject(wxWorkMsgResp.getJson(), WxWorkMessageDTO.class);
-                if (wxWorkMessageDTO.getIs_room()!=0){
+                if (wxWorkMessageDTO.getReferid()!=0){
                     break;
                 }
-                if (wxWorkMessageDTO.getReferid()!=0){
+
+                try {
+                    executor.execute(() -> processImMessage(wxWorkMsgResp, wxWorkMessageDTO, id, serverId));
+                } catch (Exception e) {
+                    log.error("接收处理群消息失败 info: {} err: {}", json, e.getMessage(), e);
+                }
+
+                if (wxWorkMessageDTO.getIs_room()!=0){
                     break;
                 }
-                if (wxWorkMessageDTO.getMsgtype()==2||wxWorkMessageDTO.getMsgtype()==0||wxWorkMessageDTO.getMsgtype()==16||wxWorkMessageDTO.getMsgtype() == 101||wxWorkMessageDTO.getMsgtype() == 104){
+
+                Long receiver = wxWorkMessageDTO.getReceiver();
+                Long sender = wxWorkMessageDTO.getSender();
+
+                if (wxWorkMessageDTO.getMsgtype()==2||wxWorkMessageDTO.getMsgtype()==0||wxWorkMessageDTO.getMsgtype()==16||wxWorkMessageDTO.getMsgtype() == 101||wxWorkMessageDTO.getMsgtype() == 104||wxWorkMessageDTO.getMsgtype()==141){
 
                     String content = wxWorkMessageDTO.getContent();
                     log.info("id:{}, 接收人:"+wxWorkMessageDTO.getReceiver(), id);
                     log.info("id:{}, 发送人:"+wxWorkMessageDTO.getSender(), id);
                     log.info("id:{}, 内容:"+content, id);
-                    Long receiver = wxWorkMessageDTO.getReceiver();
-                    Long sender = wxWorkMessageDTO.getSender();
+
                     if(wxWorkMessageDTO.getMsgtype()==16){
                         WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
                         ste.setMsgid(wxWorkMessageDTO.getMsg_id());
@@ -390,7 +410,6 @@ public class QwMsgController {
                     if (wxWorkMessageDTO.getRecordtype()==null){
                         break;
                     }
-                    Long receiver = wxWorkMessageDTO.getReceiver();
                     Long extId=null;
                     long totalSeconds=0L;
                     if (2000000000000000L-receiver>0){
@@ -438,6 +457,7 @@ public class QwMsgController {
                     qwUserVoiceLogService.addQuUserVoiceByIpadCallback(id,extId,recordType,totalSeconds,wxWorkMsgResp.getUuid());
                 }
 
+
                 break;
 
         }
@@ -448,6 +468,62 @@ public class QwMsgController {
         return map;
     }
 
+    /**
+     * Im消息存档回显
+     */
+    private void processImMessage(WxWorkMsgResp wxWorkMsgResp, WxWorkMessageDTO wxWorkMessageDTO, Long qwUserId, Long serverId) {
+        log.debug("接收到消息 msg: {}", JSON.toJSONString(wxWorkMessageDTO));
+        Long receiver = wxWorkMessageDTO.getReceiver();
+        Long sender = wxWorkMessageDTO.getSender();
+        boolean isRoom = wxWorkMessageDTO.getIs_room() != 0;
+        String chatId = null;
+        String chatAvatar = null;
+        if (isRoom) {
+            WxWorkRoomId2ChatIdDTO roomId2ChatIdDTO = new WxWorkRoomId2ChatIdDTO();
+            roomId2ChatIdDTO.setUuid(wxWorkMsgResp.getUuid());
+            roomId2ChatIdDTO.setRoom_id(wxWorkMessageDTO.getRoom_conversation_id());
+            WxWorkResponseDTO<WxWorkChatId2RoomIdResp> roomId2ChatIdResp = wxWorkService.roomId2ChatId(roomId2ChatIdDTO, serverId);
+            if (roomId2ChatIdResp.getErrcode() != 0) {
+                log.warn("接收群消息  rooId2ChatId失败: {}", roomId2ChatIdResp.getErrmsg());
+                return;
+            }
+            chatId = roomId2ChatIdResp.getData().getChatid();
+            WxRoomHeaderDTO roomHeaderDTO = new WxRoomHeaderDTO();
+            roomHeaderDTO.setRoomid(roomId2ChatIdResp.getData().getRoom_id());
+            roomHeaderDTO.setUuid(wxWorkMsgResp.getUuid());
+            WxWorkResponseDTO<WxRoomHeaderResp> roomHeaderResp = wxWorkService.wxRoomHeader(roomHeaderDTO, serverId);
+            if (roomHeaderResp.getErrcode() == 0) {
+                chatAvatar = roomHeaderResp.getData().getImage_url();
+            }
+        }
+
+        // 处理文本消息
+        if (wxWorkMessageDTO.getMsgtype() == 2 || wxWorkMessageDTO.getMsgtype() == 0) {
+            processTextMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+        // 语音消息
+        if (wxWorkMessageDTO.getMsgtype() == 16) {
+            processVoiceMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+        // 图片消息
+        if (wxWorkMessageDTO.getMsgtype() == 101){
+            processImageMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+        // gif 表情消息
+        if (wxWorkMessageDTO.getMsgtype() == 104){
+            processEmotionDynamicMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+        // 小程序消息
+        if (wxWorkMessageDTO.getMsgtype() == 78) {
+            processMiniAppMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+
+        // 撤回消息
+        if (wxWorkMessageDTO.getMsgtype() == 2063) {
+            processCancelMessage(qwUserId, sender, receiver, serverId, wxWorkMessageDTO, wxWorkMsgResp, isRoom, chatId, chatAvatar);
+        }
+    }
+
     /**
      * 处理被拉黑的用户
      * @param qwUserId
@@ -562,4 +638,175 @@ public class QwMsgController {
         }
     }
 
+    /**
+     * 处理文本消息
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息内容
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processTextMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(qwUserId, senderVid, receiverVid, serverId, wxWorkMessageDTO.getContent(), wxWorkMsgResp.getUuid(), wxWorkMsgResp.getJson(), 1, isRoom, chatId, chatAvatar, wxWorkMessageDTO.getMsg_id(), wxWorkMessageDTO.getApp_info());
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理语音消息
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processVoiceMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        String voiceFileName = IdUtils.fastSimpleUUID() + ".silk";
+        WxWorkResponseDTO<String> fileUrlResp =
+                aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getVoice_id(), wxWorkMessageDTO.getAes_key(), 5, voiceFileName, wxWorkMessageDTO.getVoice_size(), serverId);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取语音地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        // silk转map3
+        String url = AudioUtils.convertSilk2Mp3(fileUrlResp.getData());
+        if (StringUtils.isBlank(url)) {
+            log.warn("转换silk语音格式失败");
+            return;
+        }
+
+        // 转换内容为空时再尝试一次
+        String content = wxWorkMessageDTO.getContent();
+        if (StringUtils.isBlank(content)) {
+            WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
+            ste.setMsgid(wxWorkMessageDTO.getMsg_id());
+            ste.setUuid(wxWorkMsgResp.getUuid());
+            WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> dto = wxWorkService.SpeechToTextEntity(ste, serverId);
+            if (dto.getErrcode() == 0) {
+                content = dto.getData().getText();
+            }
+        }
+
+        JSONObject json = new JSONObject();
+        json.put("url", url);
+        json.put("content", content);
+
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(qwUserId, senderVid, receiverVid, serverId, json.toString(), wxWorkMsgResp.getUuid(), wxWorkMsgResp.getJson(), 4, isRoom, chatId, chatAvatar, wxWorkMessageDTO.getMsg_id(), wxWorkMessageDTO.getApp_info());
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理图片消息
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processImageMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        String fileName = IdUtils.fastSimpleUUID() + ".jpg";
+        WxWorkResponseDTO<String> fileUrlResp =
+                aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getFile_id(), wxWorkMessageDTO.getAes_key(), wxWorkMessageDTO.getOpenim_cdn_authkey(), fileName, wxWorkMessageDTO.getFile_size(), serverId);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取图片地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        String content = fileUrlResp.getData();
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(qwUserId, senderVid, receiverVid, serverId, content, wxWorkMsgResp.getUuid(), wxWorkMsgResp.getJson(), 2, isRoom, chatId, chatAvatar, wxWorkMessageDTO.getMsg_id(), wxWorkMessageDTO.getApp_info());
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 处理动态表情消息
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processEmotionDynamicMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        String content = wxWorkMessageDTO.getUrl();
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(qwUserId, senderVid, receiverVid, serverId, content, wxWorkMsgResp.getUuid(), wxWorkMsgResp.getJson(), 3, isRoom, chatId, chatAvatar, wxWorkMessageDTO.getMsg_id(), wxWorkMessageDTO.getApp_info());
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 小程序消息处理
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processMiniAppMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        String thumbName = IdUtils.fastSimpleUUID() + ".jpg";
+        WxWorkResponseDTO<String> fileUrlResp =
+                aiHookService.getFileUrl(wxWorkMsgResp.getUuid(), wxWorkMessageDTO.getThumbFileId(), wxWorkMessageDTO.getThumbAESKey(), 1, thumbName, wxWorkMessageDTO.getSize(), serverId);
+        if (fileUrlResp.getErrcode() != 0) {
+            log.warn("获取图片地址失败: {}", fileUrlResp.getErrmsg());
+            return;
+        }
+
+        JSONObject json = new JSONObject();
+        json.put("appid", wxWorkMessageDTO.getAppid());
+        json.put("appName", wxWorkMessageDTO.getAppName());
+        json.put("weappIconUrl", wxWorkMessageDTO.getAppid());
+        json.put("desc", wxWorkMessageDTO.getDesc());
+        json.put("pagepath", wxWorkMessageDTO.getPagepath());
+        json.put("title", wxWorkMessageDTO.getTitle());
+        json.put("thumbnail", fileUrlResp.getData());
+
+        // 保存聊天消息
+        QwMessageListVO message = aiHookService.saveQwMsg(qwUserId, senderVid, receiverVid, serverId, json.toString(), wxWorkMsgResp.getUuid(), wxWorkMsgResp.getJson(), 5, isRoom, chatId, chatAvatar, wxWorkMessageDTO.getMsg_id(), wxWorkMessageDTO.getApp_info());
+        QwImSocket.broadcast(message);
+    }
+
+    /**
+     * 小程序消息处理
+     * @param qwUserId          企微用户ID
+     * @param senderVid         消息发送者ID
+     * @param receiverVid       消息接收者ID
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     * @param isRoom            是否群聊
+     * @param chatId            会话ID(群聊才有)
+     * @param chatAvatar        群头像(群聊才有)
+     */
+    private void processCancelMessage(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp, boolean isRoom, String chatId, String chatAvatar) {
+        // 修改聊天消息
+        QwMessageListVO message = aiHookService.updateQwMsg(qwUserId, senderVid, receiverVid, serverId, wxWorkMsgResp.getUuid(), isRoom, chatId, wxWorkMessageDTO.getRevoke_ref_appinfo());
+        QwImSocket.broadcast(message);
+    }
+
+    @PostMapping("/sendQwImMsg")
+    public void sendQwImMsg(@RequestBody QwMessageListVO message) {
+        QwImSocket.broadcast(message);
+    }
+
 }

+ 108 - 0
fs-qw-api-msg/src/main/java/com/fs/app/socket/QwImSocket.java

@@ -0,0 +1,108 @@
+package com.fs.app.socket;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.app.socket.configurator.QwImConfigurator;
+import com.fs.qw.vo.QwMessageListVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+@Slf4j
+@ServerEndpoint(value = "/qwImSocket/{companyUserId}", configurator = QwImConfigurator.class)
+@Component
+public class QwImSocket {
+
+    private static final ConcurrentHashMap<Long, CopyOnWriteArraySet<Session>> companyUserSessions = new ConcurrentHashMap<>();
+
+    /**
+     * 连接建立成功调用的方法
+     * @param session   连接会话
+     * @param companyUserId 公司ID
+     */
+    @OnOpen
+    public void onOpen(Session session, @PathParam("companyUserId") Long companyUserId) {
+        // 将当前会话加入到会话池中
+        companyUserSessions.computeIfAbsent(companyUserId, k -> new CopyOnWriteArraySet<>()).add(session);
+    }
+
+    /**
+     * 连接关闭调用的方法
+     * @param session   连接会话
+     * @param companyUserId 公司ID
+     */
+    @OnClose
+    public void onClose(Session session, @PathParam("companyUserId") Long companyUserId) {
+        // 从会话池中移除当前会话
+        CopyOnWriteArraySet<Session> sessions = companyUserSessions.get(companyUserId);
+        if (sessions != null) {
+            sessions.remove(session);
+            // 如果直播间没人了,可以移除该直播间
+            if (sessions.isEmpty()) {
+                companyUserSessions.remove(companyUserId);
+            }
+        }
+    }
+
+    /**
+     * 发生错误时调用的方法
+     * @param session   连接会话
+     * @param companyUserId 公司ID
+     * @param error     错误对象
+     */
+    @OnError
+    public void onError(Session session, @PathParam("companyUserId") Long companyUserId, Throwable error) {
+        log.error("发生错误!会话ID: {}", session.getId());
+        CopyOnWriteArraySet<Session> sessions = companyUserSessions.get(companyUserId);
+        if (sessions != null) {
+            sessions.remove(session);
+            // 如果直播间没人了,可以移除该直播间
+            if (sessions.isEmpty()) {
+                companyUserSessions.remove(companyUserId);
+            }
+        }
+    }
+
+    /**
+     * 群发消息
+     * @param message   要发送的消息
+     */
+    public static void broadcast(QwMessageListVO message) {
+        if (Objects.isNull(message) || message.getCompanyUserId() == null) {
+            return;
+        }
+
+        String msg = JSON.toJSONString(message);
+        CopyOnWriteArraySet<Session> sessions = companyUserSessions.get(message.getCompanyUserId());
+        if (sessions != null) {
+            List<Session> closeSession = new ArrayList<>();
+            for (Session session : sessions) {
+                if (!session.isOpen()) {
+                    closeSession.add(session);
+                    continue;
+                }
+
+                try {
+                    session.getBasicRemote().sendText(msg);
+                } catch (IOException e) {
+                    log.error("发送消息给会话[{}]失败: {}", session.getId(), e.getMessage());
+                    // 移除无效会话
+                    closeSession.add(session);
+                }
+            }
+            closeSession.forEach(sessions::remove);
+        }
+    }
+
+}

+ 34 - 0
fs-qw-api-msg/src/main/java/com/fs/app/socket/configurator/QwImConfigurator.java

@@ -0,0 +1,34 @@
+package com.fs.app.socket.configurator;
+
+import com.fs.app.exception.FSException;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.websocket.HandshakeResponse;
+import javax.websocket.server.HandshakeRequest;
+import javax.websocket.server.ServerEndpointConfig;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class QwImConfigurator extends ServerEndpointConfig.Configurator {
+
+    private final TokenService tokenService = SpringUtils.getBean(TokenService.class);
+
+    @Override
+    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
+        Map<String, List<String>> parameterMap = request.getParameterMap();
+        List<String> tokens = parameterMap.get("token");
+        if (tokens == null || tokens.isEmpty() || tokens.get(0).isEmpty()) {
+            throw new FSException("Unauthorized access to WebSocket endpoint.");
+        }
+
+        String token = tokens.get(0);
+        LoginUser loginUser = tokenService.getLoginUser(token);
+        if (loginUser == null) {
+            throw new FSException("Unauthorized access to WebSocket endpoint.");
+        }
+    }
+}

+ 160 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/AudioUtils.java

@@ -0,0 +1,160 @@
+package com.fs.app.util;
+
+import com.fs.common.utils.uuid.IdUtils;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.springframework.http.HttpStatus;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class AudioUtils {
+
+    /**
+     * silk转换为mp3
+     * @param silkUrl silk语音链接地址
+     * @return  mp3链接地址
+     */
+    public static String convertSilk2Mp3(String silkUrl) {
+        String uniqueId = IdUtils.fastSimpleUUID();
+        Path uploadDirPath = Paths.get(System.getProperty("java.io.tmpdir"), "/");
+        Path downloadedSilkFilePath = uploadDirPath.resolve(uniqueId + ".silk");
+        Path pcmFilePath = uploadDirPath.resolve(uniqueId + ".pcm");
+        Path mp3FilePath = uploadDirPath.resolve(uniqueId + ".mp3");
+
+        try {
+            // 1. 从网络下载 SILK 文件
+            downloadFile(silkUrl, downloadedSilkFilePath);
+            log.debug("SILK file downloaded to: {}", downloadedSilkFilePath);
+
+            // 2. 使用 silk-v3-decoder 解码 SILK 到 PCM
+            List<String> silkDecodeCommand = new ArrayList<>();
+            silkDecodeCommand.add("silk_v3_decoder");
+            silkDecodeCommand.add(downloadedSilkFilePath.toString());
+            silkDecodeCommand.add(pcmFilePath.toString());
+
+            ProcessBuilder silkDecoderPb = new ProcessBuilder(silkDecodeCommand);
+            silkDecoderPb.redirectErrorStream(true); // 将错误流合并到标准输出
+            Process silkDecoderProcess = silkDecoderPb.start();
+
+            String silkDecoderOutput = readInputStreamToString(silkDecoderProcess.getInputStream());
+
+            boolean silkDecoderExited = silkDecoderProcess.waitFor(60, TimeUnit.SECONDS);
+            if (!silkDecoderExited || silkDecoderProcess.exitValue() != 0) {
+                log.error("silk conversion failed or timed out. error: {}", silkDecoderOutput);
+                return null;
+            }
+            log.debug("SILK decoder to PCM successfully.");
+
+            // 3. 使用 FFmpeg 将 PCM 转码为 MP3
+            Process ffmpegProcess = getFfmpegProcess(pcmFilePath, mp3FilePath);
+            String ffmpegOutput = readInputStreamToString(ffmpegProcess.getInputStream());
+
+            boolean ffmpegExited = ffmpegProcess.waitFor(120, TimeUnit.SECONDS);
+            if (!ffmpegExited || ffmpegProcess.exitValue() != 0) {
+                log.error("ffmpeg conversion failed or timed out. error: {}", ffmpegOutput);
+                return null;
+            }
+            log.debug("ffmpeg conversion to MP3 successfully.");
+
+            // 4. 上传oss
+            String fileName = mp3FilePath.getFileName().toString();
+            String suffix = fileName.substring(fileName.lastIndexOf("."));
+            CloudStorageService storage = OSSFactory.build();
+            return storage.uploadSuffix(Files.newInputStream(mp3FilePath), suffix);
+
+        } catch (IOException | InterruptedException | NullPointerException e) {
+            log.error("Conversion error: {}", e.getMessage());
+            return null;
+        } finally {
+            // 清理临时文件 (重要!)
+            try {
+                if (Files.exists(downloadedSilkFilePath)) Files.delete(downloadedSilkFilePath);
+                if (Files.exists(pcmFilePath)) Files.delete(pcmFilePath);
+                if (Files.exists(mp3FilePath)) Files.delete(mp3FilePath);
+            } catch (IOException e) {
+                log.error("Error cleaning up temporary files:: {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * 执行ffmpeg
+     * @param pcmFilePath   pcm文件
+     * @param mp3FilePath   mp3地址
+     * @return  process
+     * @throws IOException exception
+     */
+    private static Process getFfmpegProcess(Path pcmFilePath, Path mp3FilePath) throws IOException {
+        List<String> ffmpegCommand = new ArrayList<>();
+        ffmpegCommand.add("ffmpeg");
+        ffmpegCommand.add("-y");
+        ffmpegCommand.add("-f");
+        ffmpegCommand.add("s16le");
+        ffmpegCommand.add("-ar");
+        ffmpegCommand.add("24000"); // 注意:这里假设是 24kHz,如果你的 SILK 文件是其他采样率,请调整
+        ffmpegCommand.add("-ac");
+        ffmpegCommand.add("1");
+        ffmpegCommand.add("-i");
+        ffmpegCommand.add(pcmFilePath.toString());
+        ffmpegCommand.add(mp3FilePath.toString());
+
+        ProcessBuilder ffmpegPb = new ProcessBuilder(ffmpegCommand);
+        ffmpegPb.redirectErrorStream(true);
+        return ffmpegPb.start();
+    }
+
+    /**
+     * 处理文件流
+     * @param is 输入流
+     * @return  输出
+     * @throws IOException exception
+     */
+    private static String readInputStreamToString(InputStream is) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int len;
+        while ((len = is.read(buffer)) != -1) {
+            bos.write(buffer, 0, len);
+        }
+        return bos.toString("UTF-8"); // 使用 UTF-8 编码
+    }
+
+    /**
+     * 下载网络文件
+     * @param fileUrl       网络文件
+     * @param destination   临时文件
+     * @throws IOException  exception
+     */
+    private static void downloadFile(String fileUrl, Path destination) throws IOException {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault();
+             CloseableHttpResponse response = httpClient.execute(new HttpGet(fileUrl));
+             InputStream inputStream = response.getEntity().getContent();
+             FileOutputStream outputStream = new FileOutputStream(destination.toFile())) {
+
+            if (response.getStatusLine().getStatusCode() != HttpStatus.OK.value()) {
+                throw new IOException("Failed to download file from " + fileUrl + ", HTTP Status: " + response.getStatusLine().getStatusCode());
+            }
+
+            byte[] buffer = new byte[4096];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+        }
+    }
+}

+ 5 - 4
fs-qw-api-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -51,11 +51,12 @@ public class DataSourceConfig {
     public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,
                                         @Qualifier("masterDataSource") DataSource masterDataSource,
                                         @Qualifier("sopDataSource") DataSource sopDataSource,
-                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource,
+                                        @Qualifier("shardingSphereDataSource") DataSource shardingSphereDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
-        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
-
-        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
+        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+        targetDataSources.put(DataSourceType.SHARDING.name(), shardingSphereDataSource);
+        targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
         targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
         targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource); // Ensure matching key
         return new DynamicDataSource(masterDataSource, targetDataSources);

+ 11 - 45
fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length <= 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 1 - 10
fs-qw-api-msg/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -13,6 +13,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
 import java.math.BigDecimal;
@@ -35,11 +36,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -113,11 +109,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);

+ 2 - 1
fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -106,7 +106,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/**/*.html",
                         "/**/*.css",
                         "/**/*.js",
-                        "/profile/**"
+                        "/profile/**",
+                        "/qwImSocket/**"
                 ).permitAll()
 
                 .antMatchers("/**").anonymous()

+ 17 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java

@@ -0,0 +1,17 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+public class WebSocketConfig {
+    /**
+     * ServerEndpointExporter 作用
+     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
+     */
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 14 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/service/TokenService.java

@@ -82,6 +82,20 @@ public class TokenService
         return null;
     }
 
+    /**
+     * 获取用户身份信息
+     *
+     * @return 用户信息
+     */
+    public LoginUser getLoginUser(String token) {
+        Claims claims = parseToken(token);
+        // 解析对应的权限以及用户信息
+        String uuid = (String) claims.get(Constants.COMPANY_LOGIN_USER_KEY);
+        String userKey = getTokenKey(uuid);
+        return redisCache.getCacheObject(userKey);
+
+    }
+
     /**
      * 设置用户身份信息
      */

+ 11 - 45
fs-qw-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 19 - 35
fs-qw-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,66 +41,61 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    @SuppressWarnings(value = { "unchecked", "rawtypes" })
-    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Object> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
-        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
-
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(serializer);
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(serializer);
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(serializer);
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(serializer);
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -129,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 27
fs-qw-mq/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,37 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -95,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -129,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-qw-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 28
fs-qw-task/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,37 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -95,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -111,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -130,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-qw-voice/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 25
fs-qw-voice/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,36 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);

+ 11 - 45
fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 12 - 28
fs-qwhook-msg/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,41 +41,42 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
     @SuppressWarnings(value = { "unchecked", "rawtypes" })
     public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -110,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -129,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-qwhook-sop/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 26
fs-qwhook-sop/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,36 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -110,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();

+ 11 - 45
fs-qwhook/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 11 - 28
fs-qwhook/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,37 +41,37 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
@@ -95,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -111,7 +96,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
         RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
@@ -130,7 +114,6 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-redis/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 30 - 27
fs-redis/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -12,10 +7,11 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.core.script.DefaultRedisScript;
-import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.GenericToStringSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
+import java.math.BigDecimal;
+
 /**
  * redis配置
  *
@@ -34,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -52,40 +43,40 @@ public class RedisConfig extends CachingConfigurerSupport
     }
 
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
     @SuppressWarnings(value = { "unchecked", "rawtypes" })
     public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
@@ -94,11 +85,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -110,7 +96,24 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
 
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
     @Bean
     public DefaultRedisScript<Long> limitScript()
     {

+ 11 - 45
fs-repeat-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -1,16 +1,11 @@
 package com.fs.framework.config;
 
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.parser.ParserConfig;
-import com.alibaba.fastjson.serializer.SerializerFeature;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
 import org.springframework.data.redis.serializer.RedisSerializer;
 import org.springframework.data.redis.serializer.SerializationException;
-import org.springframework.util.Assert;
 
-import java.nio.charset.Charset;
 
 /**
  * Redis使用FastJson序列化
@@ -19,54 +14,25 @@ import java.nio.charset.Charset;
  */
 public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
 {
-    @SuppressWarnings("unused")
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<T> clazz;
 
-    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-
-    private Class<T> clazz;
-
-    static
-    {
-        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
-    }
-
-    public FastJson2JsonRedisSerializer(Class<T> clazz)
-    {
-        super();
+    public FastJson2JsonRedisSerializer(Class<T> clazz) {
         this.clazz = clazz;
     }
 
     @Override
-    public byte[] serialize(T t) throws SerializationException
-    {
-        if (t == null)
-        {
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
             return new byte[0];
         }
-        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+        return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
     }
 
     @Override
-    public T deserialize(byte[] bytes) throws SerializationException
-    {
-        if (bytes == null || bytes.length <= 0)
-        {
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
             return null;
         }
-        String str = new String(bytes, DEFAULT_CHARSET);
-
-        return JSON.parseObject(str, clazz);
-    }
-
-    public void setObjectMapper(ObjectMapper objectMapper)
-    {
-        Assert.notNull(objectMapper, "'objectMapper' must not be null");
-        this.objectMapper = objectMapper;
-    }
-
-    protected JavaType getJavaType(Class<?> clazz)
-    {
-        return TypeFactory.defaultInstance().constructType(clazz);
+        return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType, JSONReader.Feature.SupportClassForName);
     }
 }

+ 19 - 33
fs-repeat-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -1,10 +1,5 @@
 package com.fs.framework.config;
 
-import com.fasterxml.jackson.annotation.JsonAutoDetect;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.annotation.PropertyAccessor;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import org.springframework.cache.annotation.CachingConfigurerSupport;
 import org.springframework.cache.annotation.EnableCaching;
 import org.springframework.context.annotation.Bean;
@@ -35,11 +30,6 @@ public class RedisConfig extends CachingConfigurerSupport
 
         FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
 
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
         template.setValueSerializer(serializer);
@@ -51,61 +41,57 @@ public class RedisConfig extends CachingConfigurerSupport
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
 
         template.afterPropertiesSet();
         return template;
     }
-
     @Bean
-    @SuppressWarnings(value = { "unchecked", "rawtypes" })
-    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Object> template = new RedisTemplate<>();
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
-        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
-
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
-        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
-        serializer.setObjectMapper(mapper);
-
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(serializer);
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(serializer);
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
 
         template.afterPropertiesSet();
         return template;
     }
+
     @Bean
-    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
         template.setConnectionFactory(connectionFactory);
 
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
         // 使用StringRedisSerializer来序列化和反序列化redis的key值
         template.setKeySerializer(new StringRedisSerializer());
-
-        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
-        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setValueSerializer(serializer);
 
         // Hash的key也采用StringRedisSerializer的序列化方式
         template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+        template.setHashValueSerializer(serializer);
 
         template.afterPropertiesSet();
         return template;

+ 4 - 1
fs-service/pom.xml

@@ -290,7 +290,10 @@
             <artifactId>ecloud-sdk-ecs</artifactId>
             <version>1.1.26</version>
         </dependency>
-
+        <dependency>
+            <groupId>org.apache.shardingsphere</groupId>
+            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 1 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -136,6 +136,7 @@ public class CompanyServiceImpl implements ICompanyService
         liveService.asyncToCache();
     }
 
+
     @Override
     @Transactional
     public void addCompanyTuiLiveMoney(LiveOrder order) {

+ 5 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -707,6 +707,11 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     // 统计当天各公司的观看人数和完播人数, 存到redis中,定时任务每 ? 分钟执行一次
     List<WatchCourseStatisticsResultDTO> watchCourseStatisticsGroupByCompany(@Param("params") Map<String, Object> params);
 
+    /**
+     * 查询用户看课记录
+     */
+    List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(@Param("params") Map<String, Object> params);
+
     @Select({"<script>" +
             " select qw_external_contact_id from fs_course_watch_log where log_id in  " +
             "        <foreach collection=\"watchLogIds\" item=\"id\" open=\"(\" separator=\",\" close=\")\">\n" +

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseLinkMiniParam.java

@@ -25,5 +25,9 @@ public class FsCourseLinkMiniParam {
     * 客户的小程序id
     */
     private Long fsUserId;
+    /**
+     * sessionId
+     */
+    private Long sessionId;
 
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseListBySidebarParam.java

@@ -36,6 +36,10 @@ public class FsCourseListBySidebarParam implements Serializable {
     * 客户信息的长字符串id
     */
     private String externalUserId;
+    /**
+     * 会话ID
+     */
+    private Long sessionId;
 
     /**
      * 用于过滤用户当日应该看课的视频id

+ 7 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
+import com.fs.qw.param.QwWatchLogStatisticsListParam;
+import com.fs.course.vo.*;
 import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 
@@ -148,6 +150,11 @@ public interface IFsCourseWatchLogService extends IService<FsCourseWatchLog> {
      * */
     List<FsCourseWatchLogStatisticsListVO> selectQwFsCourseWatchLogStatisticsListVO(QwSidebarStatsParam param);
 
+    /**
+     * 查询用户看课记录
+     */
+    List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(Map<String, Object> params);
+
     /**
      * 根据看课记录id获取所有的外部联系人ids
      * @param watchLogIds

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -211,5 +211,10 @@ public interface IFsUserCourseVideoService
 
     R sendAppReward(FsCourseSendRewardUParam param);
 
+    /**
+     * 企微聊天创建小程序链接
+     */
+    R createMiniLinkByQwIm(FsCourseLinkMiniParam param);
+
     R isSaveKf(FsUserCourseVideoAddKfUParam param);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -1324,4 +1324,12 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
 
 
 
+    /**
+     * 查询用户看课记录
+     */
+    @Override
+    public List<FsCourseWatchLogIMVO> selectWatchLogIMVOListByMap(Map<String, Object> params) {
+        return fsCourseWatchLogMapper.selectWatchLogIMVOListByMap(params);
+    }
+
 }

+ 105 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -59,6 +59,15 @@ import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.domain.*;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwSession;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwGroupChatMapper;
+import com.fs.qw.mapper.QwGroupChatUserMapper;
+import com.fs.qw.mapper.QwSessionMapper;
+import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.qw.service.IQwCompanyService;
@@ -254,6 +263,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     @Autowired
     ConfigUtil configUtil;
+    @Autowired
+    private QwSessionMapper sessionMapper;
 
     @Autowired
     private IFsUserCompanyBindService fsUserCompanyBindService;
@@ -3615,5 +3626,99 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
 
 
+    /**
+     * 企微聊天创建小程序链接
+     */
+    @Override
+    public R createMiniLinkByQwIm(FsCourseLinkMiniParam param) {
+        QwSession qwSession = sessionMapper.selectQwSessionBySessionId(param.getSessionId());
+        if (qwSession == null){
+            return R.error("会话不存在");
+        }
+
+        QwUser qwUser = qwUserMapper.selectQwUserById(Long.valueOf(qwSession.getQwUserId()));
+        if (Objects.isNull(qwUser) || Objects.isNull(qwUser.getCompanyId()) || Objects.isNull(qwUser.getCompanyUserId())){
+            return R.error("员工未绑定销售公司或销售请先绑定");
+        }
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUser.getCorpId());
+        if (Objects.isNull(qwCompany)) {
+            return R.error().put("msg","企业不存在,请联系管理员");
+        }
+
+        if (qwSession.getIsRoom() == 0){
+            QwExternalContact qwExternalContact = qwExternalContactMapper.selectById(qwSession.getQwExtId());
+            if (Objects.isNull(qwExternalContact)) {
+                return R.error("客户不存在");
+            }
+
+            //看课记录
+            addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), qwExternalContact.getFsUserId(), qwUser, qwExternalContact.getId());
+
+            //生成小程序链接
+            String linkByMiniApp = createLinkByMiniApp(new Date(), param.getCourseId(), param.getVideoId(), qwUser, qwExternalContact.getId(),2,null, 0);
+            return R.ok().put("data", linkByMiniApp);
+        } else {
+            List<QwExternalContact> contacts = qwExternalContactMapper.selectGroupContactByChatIdAndQwUserId(qwSession.getChatId(), qwUser.getId());
+
+            if (contacts.isEmpty()) {
+                return R.error("群组客户不存在");
+            }
+
+            contacts.forEach(contact -> {
+                //看课记录
+                addWatchLogIfNeeded(param.getVideoId(), param.getCourseId(), contact.getFsUserId(), qwUser, contact.getId());
+            });
+
+            //生成小程序链接
+            String linkByMiniApp = createLinkByMiniApp(qwUser.getId(), qwUser.getCompanyId(), qwUser.getCompanyUserId(), param.getCourseId(), param.getVideoId(), qwUser.getCorpId(), qwSession.getChatId());
+            return R.ok().put("data", linkByMiniApp);
+        }
+    }
+
+    /**
+     * 小程序-发群链接
+     */
+    private String createLinkByMiniApp(Long qwUserId, Long companyId, Long companyUserId, Long courseId, Long videoId, String corpId, String chatId) {
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(companyId);
+        link.setQwUserId(qwUserId);
+        link.setCompanyUserId(companyUserId);
+        link.setVideoId(videoId);
+        link.setCorpId(corpId);
+        link.setCourseId(courseId);
+        link.setChatId(chatId);
+        link.setLinkType(3); //小程序
+        link.setUNo(UUID.randomUUID().toString());
+        link.setIsRoom(1);
+        String randomString = generateRandomStringWithLock();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(new Date());
+        link.setProjectCode(cloudHostProper.getProjectCode());
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link,courseMap);
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = miniappRealLink + courseJson;
+        link.setRealLink(realLinkFull);
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = new Date().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(0);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+
+        link.setUpdateTime(updateTime);
+        //存短链-
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
+    }
+
 }
 

+ 27 - 0
fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogIMVO.java

@@ -0,0 +1,27 @@
+package com.fs.course.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class FsCourseWatchLogIMVO {
+
+    @ApiModelProperty("记录ID")
+    private Long logId;
+
+    @ApiModelProperty("小节封面")
+    private String thumbnail;
+
+    @ApiModelProperty("看课时长")
+    private Long duration;
+
+    @ApiModelProperty("记录类型 1看课中 2完课 3待看课 4看课中断")
+    private Integer logType;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @ApiModelProperty("创建时间")
+    private LocalDateTime createTime;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtApiResponse.java

@@ -1,5 +1,7 @@
 package com.fs.erp.dto.wdt;
 
+import com.alibaba.fastjson.PropertyNamingStrategy;
+import com.alibaba.fastjson.annotation.JSONType;
 import lombok.Data;
 import org.apache.commons.lang3.StringEscapeUtils;
 
@@ -9,6 +11,7 @@ import org.apache.commons.lang3.StringEscapeUtils;
  * @date 2025-02-27
  */
 @Data
+@JSONType(naming = PropertyNamingStrategy.CamelCase)
 public class ErpWdtApiResponse {
 
     /**

+ 3 - 0
fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtBaseResponseDTO.java

@@ -1,5 +1,7 @@
 package com.fs.erp.dto.wdt;
 
+import com.alibaba.fastjson.PropertyNamingStrategy;
+import com.alibaba.fastjson.annotation.JSONType;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -13,6 +15,7 @@ import java.util.List;
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
+@JSONType(naming = PropertyNamingStrategy.CamelCase)
 public class ErpWdtBaseResponseDTO {
 
     /**

+ 3 - 0
fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtStockRespDTO.java

@@ -1,5 +1,7 @@
 package com.fs.erp.dto.wdt;
 
+import com.alibaba.fastjson.PropertyNamingStrategy;
+import com.alibaba.fastjson.annotation.JSONType;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 
@@ -7,6 +9,7 @@ import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.util.List;
 
+@JSONType(naming = PropertyNamingStrategy.CamelCase)
 @Data
 public class ErpWdtStockRespDTO implements Serializable {
     /**

+ 3 - 0
fs-service/src/main/java/com/fs/erp/dto/wdt/ErpWdtTradeQueryResponse.java

@@ -1,5 +1,7 @@
 package com.fs.erp.dto.wdt;
 
+import com.alibaba.fastjson.PropertyNamingStrategy;
+import com.alibaba.fastjson.annotation.JSONType;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -21,6 +23,7 @@ import java.util.List;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
+@JSONType(naming = PropertyNamingStrategy.CamelCase)
 public class ErpWdtTradeQueryResponse {
 
     /**

+ 2 - 7
fs-service/src/main/java/com/fs/erp/service/impl/WdtErpGoodsServiceImpl.java

@@ -47,10 +47,7 @@ public class WdtErpGoodsServiceImpl implements IErpGoodsService {
         Asserts.notNull(param.getCode(),"barcode不能为空!");
         try {
             String response = client.execute("goods_query.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtBaseResponseDTO erpWdtBaseResponseDTO =
-                    JSON.parseObject(response, ErpWdtBaseResponseDTO.class, config);
+            ErpWdtBaseResponseDTO erpWdtBaseResponseDTO = JSON.parseObject(response, ErpWdtBaseResponseDTO.class);
             if(ObjectUtils.equals(0, erpWdtBaseResponseDTO.getCode())){
                 List<ErpGoods> list = new ArrayList<>();
 
@@ -100,9 +97,7 @@ public class WdtErpGoodsServiceImpl implements IErpGoodsService {
         map.put("spec_no",barcode);
         try {
             String response = client.execute("stock_query.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtStockRespDTO erpWdtStockRespDTO = JSON.parseObject(response, ErpWdtStockRespDTO.class,config);
+            ErpWdtStockRespDTO erpWdtStockRespDTO = JSON.parseObject(response, ErpWdtStockRespDTO.class);
             List<ErpGoodsStock> list = new ArrayList<>();
             if(ObjectUtils.equals(0,erpWdtStockRespDTO.getCode())){
                 List<ErpWdtStockDTO> stocks = erpWdtStockRespDTO.getStocks();

+ 6 - 20
fs-service/src/main/java/com/fs/erp/service/impl/WdtErpOrderServiceImpl.java

@@ -269,9 +269,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
 
         try {
             String response = client.execute("trade_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0,erpWdtApiResponse.getCode())){
                 log.info("订单推送成功: {}", response);
                 ErpOrderResponse erpOrderResponse = new ErpOrderResponse();
@@ -449,9 +447,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
 
         try {
             String response = client.execute("trade_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0,erpWdtApiResponse.getCode())){
                 log.info("订单推送成功: {}", response);
                 ErpOrderResponse erpOrderResponse = new ErpOrderResponse();
@@ -624,9 +620,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
 
         try {
             String response = client.execute("trade_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0,erpWdtApiResponse.getCode())){
                 log.info("订单推送成功: {}", response);
                 ErpOrderResponse erpOrderResponse = new ErpOrderResponse();
@@ -958,10 +952,8 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
 
         try {
             String response = client.execute("trade_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
              log.info("Erp推送传参: {}", map);
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0,erpWdtApiResponse.getCode())){
                 log.info("订单推送成功: {}", response);
                 ErpOrderResponse erpOrderResponse = new ErpOrderResponse();
@@ -1002,8 +994,6 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
         map.put("src_tid",param.getCode());
         try {
             String execute = client.execute("sales_trade_query.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
             ErpWdtTradeQueryResponse tradeQueryResponseDTO = JSON.parseObject(execute, ErpWdtTradeQueryResponse.class);
             if(ObjectUtil.equal(0,tradeQueryResponseDTO.getCode())){
 //                if (tradeQueryResponseDTO.getTrades().isEmpty()){
@@ -1097,9 +1087,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
         map.put("api_refund_list",convertToSnakeCase(erpWdtApiRefunds));
         try {
             String execute = client.execute("sales_refund_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(execute, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(execute, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0, erpWdtApiResponse.getCode())){
                 log.info("退款单更新成功: {}", erpWdtApiResponse);
                 return new BaseResponse();
@@ -1274,9 +1262,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
 
         try {
             String response = client.execute("trade_push.php", map);
-            ParserConfig config = new ParserConfig();
-            config.propertyNamingStrategy = PropertyNamingStrategy.CamelCase;
-            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class,config);
+            ErpWdtApiResponse erpWdtApiResponse = JSON.parseObject(response, ErpWdtApiResponse.class);
             if(ObjectUtil.equal(0,erpWdtApiResponse.getCode())){
                 log.info("订单推送成功: {}", response);
                 ErpOrderResponse erpOrderResponse = new ErpOrderResponse();

+ 43 - 0
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -2,6 +2,7 @@ package com.fs.fastGpt.service;
 
 import com.fs.common.core.domain.R;
 import com.fs.im.vo.OpenImMsgCallBackVO;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookVO;
 import com.fs.wxwork.dto.WxWorkResponseDTO;
 
@@ -25,4 +26,46 @@ public interface AiHookService {
     void expireAiMsg();
 
     WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, String authKey, String fileName, Integer fileSize, Long serverId);
+
+    /**
+     * 获取文件地址
+     * @param uuid      uuid
+     * @param fileId    fileId
+     * @param aesKey    aesKey
+     * @param fileType  fileType
+     * @param fileName  fileName
+     * @param fileSize  fileSize
+     * @param serverId  serverId
+     * @return  WxWorkResponseDTO
+     */
+    WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, Integer fileType, String fileName, Integer fileSize, Long serverId);
+
+    /**
+     * 保存企微聊天信息
+     *
+     * @param qwUserId      企微用户ID
+     * @param senderVid     消息发送者ID
+     * @param receiverVid   消息接收者ID
+     * @param serverId      服务器ID
+     * @param content       聊天内容
+     * @param uuid          UUID
+     * @param json          消息json
+     * @param msgType       消息类型 1文本 2图片 3动态表情 4语音 5小程序
+     * @param isRoom        是否群聊
+     * @param chatId        会话ID(群聊才有)
+     * @param chatAvatar    群头像(群聊才有)
+     * @param qwMsgId       企微消息ID
+     * @param qwAppInfo       企微appInfo
+     */
+    QwMessageListVO saveQwMsg(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, String content, String uuid, String json, int msgType, boolean isRoom, String chatId, String chatAvatar, Long qwMsgId, String qwAppInfo);
+
+    /**
+     * 保存企微聊天信息
+     */
+    QwMessageListVO saveQwMsg(Long qwUserId, String exId, String content, int msgType, boolean isRoom, Long qwMsgId, String qwAppInfo);
+
+    /**
+     * 修改消息
+     */
+    QwMessageListVO updateQwMsg(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, String uuid, boolean isRoom, String chatId, String qwAppInfo);
 }

+ 572 - 14
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -3,11 +3,14 @@ package com.fs.fastGpt.service.impl;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONArray;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.annotation.DataSource;
 import com.fs.common.annotation.Excel;
-import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.PinYinUtil;
+import com.fs.common.utils.http.HttpUtils;
 import com.fs.company.domain.CompanyConfig;
 import com.fs.company.mapper.CompanyConfigMapper;
 import com.fs.config.ai.AiHostProper;
@@ -38,16 +41,17 @@ import com.fs.his.domain.FsStoreOrder;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.dto.TracesDTO;
 import com.fs.his.enums.ShipperCodeEnum;
-import com.fs.his.mapper.FsStoreMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
 import com.fs.his.service.IFsExpressService;
 import com.fs.his.service.IFsStoreOrderService;
-import com.fs.im.dto.OpenImMsgDTO;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.qw.domain.*;
+import com.fs.qw.dto.QwImUserDTO;
+import com.fs.qw.enums.MsgType;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwAutoTagsRulesTags;
 import com.fs.qw.service.*;
+import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwApi.domain.QwResult;
 import com.fs.qwApi.param.QwEditUserTagParam;
 import com.fs.qwApi.param.QwSendMsgParam;
@@ -63,9 +67,11 @@ import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
 import com.vdurmont.emoji.EmojiParser;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.Nullable;
+import org.json.JSONObject;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Async;
@@ -77,7 +83,7 @@ import java.time.LocalDate;
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -165,10 +171,33 @@ public class AiHookServiceImpl implements AiHookService {
     private IFastGptChatReplaceTextService fastGptChatReplaceTextService;
     @Autowired
     private ICrmMsgService crmMsgService;
+    @Autowired
+    private QwSessionMapper qwSessionMapper;
+    @Autowired
+    private QwMsgMapper qwMsgMapper;
+    @Autowired
+    private QwGroupChatMapper qwGroupChatMapper;
+    @Autowired
+    private RedissonClient redissonClient;
 
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
 
+    private final ExecutorService executor = new ThreadPoolExecutor(
+            8, 32, 60L, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(1000),
+            new ThreadFactory() {
+                private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
+                private int count = 1;
+                @Override
+                public Thread newThread(Runnable r) {
+                    Thread thread = defaultFactory.newThread(r);
+                    thread.setName("ai-im-exec-" + count++);
+                    return thread;
+                }
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
+    );
 
     /** Ai半小时未回复提醒 **/
     /**
@@ -288,7 +317,7 @@ public class AiHookServiceImpl implements AiHookService {
                             if (result.isLongText()){
                                 //新增用户信息
                                 addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
-                                sendAiMsg(content,sender,uid,serverId);
+                                sendAiMsg(content,sender,uid,serverId, user.getId(), qwExternalContacts.getExternalUserId());
 
                             }else {
                                 String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
@@ -302,7 +331,7 @@ public class AiHookServiceImpl implements AiHookService {
                                 //新增用户信息
                                 addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
                                 for (String msg : countList) {
-                                    sendAiMsg(msg,sender,uid,serverId);
+                                    sendAiMsg(msg,sender,uid,serverId, user.getId(), qwExternalContacts.getExternalUserId());
                                     try {
                                         Thread.sleep(10000);
                                     } catch (InterruptedException e) {
@@ -564,9 +593,9 @@ public class AiHookServiceImpl implements AiHookService {
                     //新增用户信息
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
                     if (type==16){
-                        sendAiVoiceMsg(content,sender,uid,serverId,user);
+                        sendAiVoiceMsg(content,sender,uid,serverId,user, qwExternalContacts.getExternalUserId());
                     }else {
-                        sendAiMsg(content,sender,uid,serverId);
+                        sendAiMsg(content,sender,uid,serverId, user.getId(), qwExternalContacts.getExternalUserId());
                     }
 
                 }else {
@@ -583,9 +612,9 @@ public class AiHookServiceImpl implements AiHookService {
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
                     for (String msg : countList) {
                         if (type==16){
-                            sendAiVoiceMsg(msg,sender,uid,serverId,user);
+                            sendAiVoiceMsg(msg,sender,uid,serverId,user, qwExternalContacts.getExternalUserId());
                         }else {
-                            sendAiMsg(msg,sender,uid,serverId);
+                            sendAiMsg(msg,sender,uid,serverId, user.getId(), qwExternalContacts.getExternalUserId());
                         }
                         try {
                             Thread.sleep(10000);
@@ -950,7 +979,7 @@ public class AiHookServiceImpl implements AiHookService {
         return maskedContent;
     }
 
-    private void sendAiVoiceMsg(String content, Long sendId , String uuid,Long serverId,QwUser user) {
+    private void sendAiVoiceMsg(String content, Long sendId , String uuid,Long serverId,QwUser user, String extUserId) {
         if (content == null || content.trim().isEmpty()){
             System.out.println("输出为空格");
             return;
@@ -989,11 +1018,37 @@ public class AiHookServiceImpl implements AiHookService {
         WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO> wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO = wxWorkService.SendCDNVoiceMsg(wxwSendCDNVoiceMsgDTO, serverId);
         System.out.println(wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO);
 
+        if (wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO.getErrcode() == 0) {
+            try {
+                WxwSendCDNVoiceMsgRespDTO dtoData = wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO.getData();
+
+                JSONObject json = new JSONObject();
+                json.put("url", data.getUrl());
+                json.put("content", content);
+
+                // 保存聊天消息
+                QwMessageListVO message = this.saveQwMsg(user.getId(), extUserId, json.toString(), 4, false, dtoData.getMsg_id(), dtoData.getApp_info());
+                if (message == null) {
+                    return;
+                }
+                // 发送webSocket
+                executor.execute(() -> {
+                    try {
+                        HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                    } catch (Exception e) {
+                        log.error("转发ai回复消息失败 err: {}", e.getMessage(), e);
+                    }
+                });
+            } catch (Exception e) {
+                log.error("保存ai回复消息失败 err: {}", e.getMessage(), e);
+            }
+        }
+
     }
 
     @Autowired
     QwSopLogsMapper qwSopLogsMapper;
-    private void sendAiMsg(String content, Long sendId , String uuid,Long serverId) {
+    private void sendAiMsg(String content, Long sendId , String uuid,Long serverId, Long qwUserId, String extUserId) {
         if (content == null || content.trim().isEmpty()){
             System.out.println("输出为空格");
             return;
@@ -1006,6 +1061,27 @@ public class AiHookServiceImpl implements AiHookService {
         WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(wxWorkSendTextMsgDTO,serverId);
         WxWorkSendTextMsgRespDTO data = wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getData();
 
+        if (wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getErrcode() == 0) {
+            try {
+                WxWorkSendTextMsgRespDTO dtoData = wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getData();
+
+                // 保存聊天消息
+                QwMessageListVO message = this.saveQwMsg(qwUserId, extUserId, content, 1, false, dtoData.getMsg_id(), dtoData.getApp_info());
+                if (message == null) {
+                    return;
+                }
+                // 发送webSocket
+                executor.execute(() -> {
+                    try {
+                        HttpUtils.doPost(aiHostProper.getIpadUrl() + "/msg/sendQwImMsg", JSON.toJSONString(message));
+                    } catch (Exception e) {
+                        log.error("转发ai回复消息失败 err: {}", e.getMessage(), e);
+                    }
+                 });
+             } catch (Exception e) {
+                log.error("保存ai回复消息失败 err: {}", e.getMessage(), e);
+            }
+        }
 
     }
     /**
@@ -2117,4 +2193,486 @@ public class AiHookServiceImpl implements AiHookService {
         return wxWorkService.downloadWeChatFile(weChatFileDTO, serverId);
     }
 
+    /**
+     * 获取文件地址
+     * @param uuid      uuid
+     * @param fileId    fileId
+     * @param aesKey    aesKey
+     * @param fileType  fileType
+     * @param fileName  fileName
+     * @param fileSize  fileSize
+     * @param serverId  serverId
+     * @return  WxWorkResponseDTO
+     */
+    @Override
+    public WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, Integer fileType, String fileName, Integer fileSize, Long serverId) {
+        WxDownloadFileDTO downloadFileDTO = new WxDownloadFileDTO();
+        downloadFileDTO.setUuid(uuid);
+        downloadFileDTO.setFileid(fileId);
+        downloadFileDTO.setAes_key(aesKey);
+        downloadFileDTO.setFiletype(fileType);
+        downloadFileDTO.setFile_name(fileName);
+        downloadFileDTO.setSize(fileSize);
+        return wxWorkService.downloadFile(downloadFileDTO, serverId);
+    }
+
+    /**
+     * 保存企微聊天信息
+     *
+     * @param qwUserId      企微用户ID
+     * @param senderVid     消息发送者ID
+     * @param receiverVid   消息接收者ID
+     * @param serverId      服务器ID
+     * @param content       聊天内容
+     * @param uuid          UUID
+     * @param json          消息json
+     * @param msgType       消息类型 1文本 2图片 3动态表情 4语音 5小程序
+     * @param isRoom        是否群聊
+     * @param chatId        会话ID(群聊才有)
+     * @param chatAvatar    群头像(群聊才有)
+     * @param qwMsgId       企微消息ID
+     * @param qwAppInfo       企微appInfo
+     */
+    @DataSource(DataSourceType.SHARDING)
+    @Override
+    public QwMessageListVO saveQwMsg(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, String content, String uuid, String json, int msgType, boolean isRoom, String chatId, String chatAvatar, Long qwMsgId, String qwAppInfo) {
+        // 查询企微用户
+        QwUser qwUser = qwUserService.selectQwUserById(qwUserId);
+        if (Objects.isNull(qwUser)) {
+            log.warn("企微用户不存在 qwUserId: {}", qwUserId);
+            return null;
+        }
+
+        if (qwUser.getCompanyUserId() == null) {
+            log.warn("企微用户未归属销售 qwUserId: {}", qwUserId);
+            return null;
+        }
+
+        // 获取发送人
+        QwImUserDTO sender = getUserByVid(senderVid, uuid, serverId, qwUser.getCorpId(), qwUser.getQwUserId());
+        if (Objects.isNull(sender)) {
+            log.warn("sender用户不存在 senderVid: {}", senderVid);
+            return null;
+        }
+
+        // 获取接收者
+        QwImUserDTO receiver = getUserByVid(receiverVid, uuid, serverId, qwUser.getCorpId(), qwUser.getQwUserId());
+
+        // 查询会话
+        QwSession qwSession = null;
+        if (isRoom) {
+            qwSession = getGroupQwSession(chatId, chatAvatar, qwUser);
+        } else {
+            QwImUserDTO extUser = null;
+            Long extWxId = null;
+
+            // 获取外部联系人
+            if (sender.getUserType() == 1) {
+                extUser = sender;
+                extWxId = senderVid;
+            }
+            if (receiver != null && receiver.getUserType() == 1) {
+                extUser = receiver;
+                extWxId = receiverVid;
+            }
+            if (extUser != null) {
+                qwSession = getQwSession(extUser, qwUser, extWxId);
+            }
+        }
+
+        if (qwSession == null) {
+            log.warn("获取session失败 senderVid: {}, receiverVid: {}", senderVid, receiverVid);
+            return null;
+        }
+
+        // 保存聊天消息
+        QwMsg qwMsg = new QwMsg();
+        qwMsg.setContent(content);
+        qwMsg.setSessionId(qwSession.getSessionId());
+        qwMsg.setSendType(sender.getUserType());
+        qwMsg.setCompanyId(qwUser.getCompanyId());
+        qwMsg.setCompanyUserId(qwUser.getCompanyUserId());
+        qwMsg.setMsgType(msgType);
+        qwMsg.setMsgJson(json);
+        qwMsg.setStatus(0);
+        qwMsg.setQwUserId(qwSession.getQwUserId());
+        qwMsg.setCreateTime(new Date());
+        if (sender.getUserType() == 1) {
+            qwMsg.setQwExtId(sender.getUserId().toString());
+        }
+        qwMsg.setAvatar(sender.getAvatar());
+        qwMsg.setNickName(sender.getUserName());
+        qwMsg.setQwMsgId(qwMsgId);
+        qwMsg.setQwAppInfo(qwAppInfo);
+        qwMsgMapper.insertQwMsg(qwMsg);
+        log.debug("保存企微聊天记录 msgId: {}", qwMsg.getMsgId());
+
+        String type = "text";
+        MsgType messageType = MsgType.getMsgType(msgType);
+        if (Objects.nonNull(messageType)){
+            type = messageType.getValue();
+        }
+
+        qwSession.setLastMsgId(qwMsg.getMsgId());
+        qwSession.setLastMsgType(type);
+        qwSession.setLastSendTime(qwMsg.getCreateTime().getTime());
+        qwSession.setLastContent(qwMsg.getContent());
+        qwSessionMapper.updateQwSession(qwSession);
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        qwFromUser.setId(sender.getUserId());
+        qwFromUser.setAvatar(sender.getAvatar());
+        qwFromUser.setDisplayName(sender.getUserName());
+
+        listVO.setCompanyUserId(qwUser.getCompanyUserId());
+        listVO.setType(type);
+        listVO.setStatus("succeed");
+        listVO.setExtId(qwMsg.getQwExtId());
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(qwSession.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return listVO;
+    }
+
+    /**
+     * 保存企微聊天信息
+     */
+    @DataSource(DataSourceType.SHARDING)
+    @Override
+    public QwMessageListVO saveQwMsg(Long qwUserId, String exId, String content, int msgType, boolean isRoom, Long qwMsgId, String qwAppInfo) {
+        QwUser qwUser = qwUserService.selectQwUserById(qwUserId);
+        if (Objects.isNull(qwUser)) {
+            log.warn("企微用户不存在 qwUserId: {}", qwUserId);
+            return null;
+        }
+
+        // 查询会话
+        QwSession qwSession;
+        if (isRoom) {
+            qwSession = qwSessionMapper.selectQwSessionByChatIdAndQwUserId(exId, qwUser.getId());
+        } else {
+            QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(exId, qwUser.getCorpId(), qwUser.getQwUserId());
+            if (Objects.isNull(externalContact)) {
+                log.warn("企微外部联系人不存在 extId: {}", exId);
+                return null;
+            }
+            qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(externalContact.getId(), qwUser.getId());
+        }
+
+        if (qwSession == null) {
+            log.warn("获取session失败 qwUserId: {}, extId: {}", qwUserId, exId);
+            return null;
+        }
+
+        // 保存聊天消息
+        QwMsg qwMsg = new QwMsg();
+        qwMsg.setContent(content);
+        qwMsg.setSessionId(qwSession.getSessionId());
+        qwMsg.setSendType(2);
+        qwMsg.setCompanyId(qwUser.getCompanyId());
+        qwMsg.setCompanyUserId(qwUser.getCompanyUserId());
+        qwMsg.setMsgType(msgType);
+        qwMsg.setMsgJson(content);
+        qwMsg.setStatus(0);
+        qwMsg.setQwUserId(qwSession.getQwUserId());
+        qwMsg.setCreateTime(new Date());
+        qwMsg.setAvatar(qwUser.getAvatar());
+        qwMsg.setNickName(qwUser.getQwUserName());
+        qwMsg.setQwMsgId(qwMsgId);
+        qwMsg.setQwAppInfo(qwAppInfo);
+        qwMsgMapper.insertQwMsg(qwMsg);
+        log.debug("保存企微聊天记录 msgId: {}", qwMsg.getMsgId());
+
+        String type = "text";
+        MsgType messageType = MsgType.getMsgType(msgType);
+        if (Objects.nonNull(messageType)){
+            type = messageType.getValue();
+        }
+
+        qwSession.setLastMsgId(qwMsg.getMsgId());
+        qwSession.setLastMsgType(type);
+        qwSession.setLastSendTime(qwMsg.getCreateTime().getTime());
+        qwSession.setLastContent(qwMsg.getContent());
+        qwSessionMapper.updateQwSession(qwSession);
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        qwFromUser.setId(qwUser.getId());
+        qwFromUser.setAvatar(qwUser.getAvatar());
+        qwFromUser.setDisplayName(qwUser.getQwUserName());
+
+        listVO.setCompanyUserId(qwUser.getCompanyUserId());
+        listVO.setType(type);
+        listVO.setStatus("succeed");
+        listVO.setExtId(qwMsg.getQwExtId());
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(qwSession.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return listVO;
+    }
+
+    /**
+     * 获取单聊session
+     */
+    private QwSession getQwSession(QwImUserDTO extUser, QwUser qwUser, Long extWxId) {
+        QwSession qwSession;
+        qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(extUser.getUserId(), qwUser.getId());
+        String firstLetter = PinYinUtil.getFirstLetter(extUser.getUserName());
+        if (qwSession == null) {
+
+            RLock lock = redissonClient.getLock("addSession:" + extUser.getUserId() + ":" + qwUser.getId());
+            try {
+                boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                if (!tryLock) {
+                    log.warn("添加单聊Session获取锁失败");
+                    return null;
+                }
+
+                qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(extUser.getUserId(), qwUser.getId());
+                if (qwSession == null) {
+                    qwSession = new QwSession();
+                    String chatId = UUID.randomUUID().toString();
+                    qwSession.setChatId(chatId);
+                    qwSession.setCorpId(qwUser.getCorpId());
+                    qwSession.setQwExtWxId(String.valueOf(extWxId));
+                    qwSession.setQwExtId(extUser.getUserId().toString());
+                    qwSession.setQwUserId(qwUser.getId().toString());
+                    qwSession.setStatus(1);
+                    qwSession.setAvatar(extUser.getAvatar());
+                    qwSession.setNickName(extUser.getUserName());
+                    qwSession.setCompanyId(qwUser.getCompanyId());
+                    qwSession.setCompanyUserId(qwUser.getCompanyUserId());
+                    qwSession.setCreateTime(new Date());
+                    qwSession.setUpdateTime(new Date());
+                    qwSession.setIsRoom(0);
+                    qwSession.setFirstLetter(firstLetter);
+                    qwSessionMapper.insertQwSession(qwSession);
+                }
+            } catch (InterruptedException e) {
+                log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                return null;
+            } finally {
+                lock.unlock();
+            }
+        }else {
+            qwSession.setUpdateTime(new Date());
+            qwSession.setNickName(extUser.getUserName());
+            qwSession.setFirstLetter(firstLetter);
+            qwSessionMapper.updateQwSession(qwSession);
+        }
+        return qwSession;
+    }
+
+    /**
+     * 获取群聊session
+     */
+    private QwSession getGroupQwSession(String chatId, String chatAvatar, QwUser qwUser) {
+        QwSession qwSession;
+        QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(chatId);
+        if (Objects.isNull(qwGroupChat)){
+            log.warn("群聊不存在 serverId: {}, corpId: {}, qwUserId: {}, chatId: {}", qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId(), chatId);
+            return null;
+        }
+
+        qwSession = qwSessionMapper.selectQwSessionByChatIdAndQwUserId(chatId, qwUser.getId());
+        String firstLetter = PinYinUtil.getFirstLetter(qwGroupChat.getName());
+        if (qwSession == null) {
+            RLock lock = redissonClient.getLock("addSession:" + qwGroupChat.getChatId() + ":" + qwUser.getId());
+            try {
+                boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                if (!tryLock) {
+                    log.warn("添加群聊Session获取锁失败");
+                    return null;
+                }
+
+                qwSession = qwSessionMapper.selectQwSessionByChatIdAndQwUserId(chatId, qwUser.getId());
+                if (qwSession == null) {
+                    qwSession = new QwSession();
+                    qwSession.setChatId(chatId);
+                    qwSession.setCorpId(qwGroupChat.getCorpId());
+                    qwSession.setQwUserId(qwUser.getId().toString());
+                    qwSession.setStatus(1);
+                    qwSession.setAvatar(chatAvatar);
+                    qwSession.setNickName(qwGroupChat.getName());
+                    qwSession.setCompanyId(qwUser.getCompanyId());
+                    qwSession.setCreateTime(new Date());
+                    qwSession.setUpdateTime(new Date());
+                    qwSession.setIsRoom(1);
+                    qwSession.setFirstLetter(firstLetter);
+                    qwSessionMapper.insertQwSession(qwSession);
+                }
+            } catch (InterruptedException e) {
+                log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                return null;
+            } finally {
+                lock.unlock();
+            }
+        }else {
+            qwSession.setUpdateTime(new Date());
+            qwSession.setQwUserId(qwUser.getId().toString());
+            qwSession.setNickName(qwGroupChat.getName());
+            qwSession.setAvatar(chatAvatar);
+            qwSession.setFirstLetter(firstLetter);
+            qwSessionMapper.updateQwSession(qwSession);
+        }
+        return qwSession;
+    }
+
+    /**
+     * 查询外部联系人
+     * @param userId    用户ID
+     * @param uuid      UUID
+     * @param serverId  服务ID
+     * @param corpId    企微ID
+     * @param qwUserId  企微用户ID
+     * @return  QwExternalContact
+     */
+    private QwExternalContact getExternalContact(Long userId, String uuid, Long serverId, String corpId, String qwUserId) {
+        return qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(getExtId(userId, uuid, serverId), corpId, qwUserId);
+    }
+
+    /**
+     * 根据vid获取用户信息
+     */
+    private QwImUserDTO getUserByVid(Long vid, String uuid, Long serverId, String corpId, String qwUserId) {
+        if (vid == 0) {
+            return null;
+        }
+
+        // 外部联系人
+        if (vid > 2000000000000000L) {
+            QwExternalContact qwExternalContact = getExternalContact(vid, uuid, serverId, corpId, qwUserId);
+            if (Objects.isNull(qwExternalContact)) {
+                log.warn("外部联系人不存在 vid: {}, uuid: {}, serverId: {}, corpId: {}, qwUserId: {}", vid, uuid, serverId, corpId, qwUserId);
+                return null;
+            }
+
+            QwImUserDTO userDTO = new QwImUserDTO();
+            userDTO.setUserId(qwExternalContact.getId());
+            userDTO.setUserName(qwExternalContact.getName());
+            userDTO.setAvatar(qwExternalContact.getAvatar());
+            userDTO.setUserType(1);
+            return userDTO;
+        }
+        // 企微用户
+        else {
+            QwUser qwUser = qwUserService.selectQwUserByVid(vid);
+            if (Objects.isNull(qwUser)){
+                log.warn("企微用户不存在 vid: {}", vid);
+                return null;
+            }
+
+            QwImUserDTO userDTO = new QwImUserDTO();
+            userDTO.setUserId(qwUser.getId());
+            userDTO.setUserName(qwUser.getQwUserName());
+            userDTO.setAvatar(qwUser.getAvatar());
+            userDTO.setUserType(2);
+            return userDTO;
+        }
+    }
+
+
+    /**
+     * 修改消息
+     */
+    @DataSource(DataSourceType.SHARDING)
+    @Override
+    public QwMessageListVO updateQwMsg(Long qwUserId, Long senderVid, Long receiverVid, Long serverId, String uuid, boolean isRoom, String chatId, String qwAppInfo) {
+        QwUser qwUser = qwUserService.selectQwUserById(qwUserId);
+        if (Objects.isNull(qwUser)) {
+            log.warn("企微用户不存在 qwUserId: {}", qwUserId);
+            return null;
+        }
+
+        // 获取发送人
+        QwImUserDTO sender = getUserByVid(senderVid, uuid, serverId, qwUser.getCorpId(), qwUser.getQwUserId());
+        if (Objects.isNull(sender)) {
+            log.warn("sender用户不存在 senderVid: {}", senderVid);
+            return null;
+        }
+
+        // 获取接收者
+        QwImUserDTO receiver = getUserByVid(receiverVid, uuid, serverId, qwUser.getCorpId(), qwUser.getQwUserId());
+
+        // 查询会话
+        QwSession qwSession = null;
+        if (isRoom) {
+            QwGroupChat qwGroupChat = qwGroupChatMapper.selectQwGroupChatByChatId(chatId);
+            if (Objects.isNull(qwGroupChat)){
+                log.warn("群聊不存在 serverId: {}, corpId: {}, qwUserId: {}, chatId: {}", qwUser.getServerId(), qwUser.getCorpId(), qwUser.getQwUserId(), chatId);
+                return null;
+            }
+
+            qwSession = qwSessionMapper.selectQwSessionByChatIdAndQwUserId(chatId, qwUser.getId());
+        } else {
+            QwImUserDTO extUser = null;
+
+            // 获取外部联系人
+            if (sender.getUserType() == 1) {
+                extUser = sender;
+            }
+            if (receiver != null && receiver.getUserType() == 1) {
+                extUser = receiver;
+            }
+            if (extUser != null) {
+                qwSession = qwSessionMapper.selectQwSessionByExtIdAndQwUserId(extUser.getUserId(), qwUser.getId());
+            }
+        }
+
+        if (qwSession == null) {
+            log.warn("获取session失败 senderVid: {}, receiverVid: {}", senderVid, receiverVid);
+            return null;
+        }
+
+        QwMsg qwMsg = qwMsgMapper.selectQwMsgBySessionIdAndQwAppInfo(qwSession.getSessionId(), qwAppInfo);
+        if (qwMsg == null) {
+            log.warn("消息不存在 sessionId: {}, appInfo: {}", qwSession.getSessionId(), qwAppInfo);
+            return null;
+        }
+
+        qwMsg.setMsgType(6);
+        qwMsg.setSessionId(null);
+        qwMsgMapper.updateQwMsg(qwMsg);
+
+        String type = "text";
+        MsgType messageType = MsgType.getMsgType(qwMsg.getMsgType());
+        if (Objects.nonNull(messageType)){
+            type = messageType.getValue();
+        }
+
+        qwSession.setLastMsgId(qwMsg.getMsgId());
+        qwSession.setLastMsgType(type);
+        qwSession.setLastSendTime(qwMsg.getCreateTime().getTime());
+        qwSession.setLastContent("");
+        qwSessionMapper.updateQwSession(qwSession);
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        qwFromUser.setId(sender.getUserId());
+        qwFromUser.setAvatar(sender.getAvatar());
+        qwFromUser.setDisplayName(sender.getUserName());
+
+        listVO.setCompanyUserId(qwUser.getCompanyUserId());
+        listVO.setType(type);
+        listVO.setStatus("succeed");
+        listVO.setExtId(qwMsg.getQwExtId());
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(qwSession.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return listVO;
+    }
+
 }

+ 8 - 1
fs-service/src/main/java/com/fs/his/mapper/FsUserOperationLogMapper.java

@@ -4,8 +4,9 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.his.domain.FsUserOperationLog;
 import com.fs.his.param.FsUserOperationLogQueryParam;
-import com.fs.his.vo.FsUserOperationLogPageVo;
 import com.fs.his.vo.FsUserOperationLogVo;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 用户操作日志Mapper接口
@@ -63,4 +64,10 @@ public interface FsUserOperationLogMapper extends BaseMapper<FsUserOperationLog>
     int deleteFsUserOperationLogByLogIds(Long[] logIds);
 
     List<FsUserOperationLogVo> selectFsUserOperationLogByList(FsUserOperationLog fsUserOperationLog);
+
+    /**
+     * 查询用户历史同行为操作
+     */
+    @Select("select * from fs_user_operation_log where user_id = #{userId} and operation_type = #{operationType} and param = #{param} limit 1")
+    FsUserOperationLog selectUserOldOperationLog(@Param("userId") Long userId, @Param("operationType") String operationType, @Param("param") String param);
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserOperationLogService.java

@@ -67,4 +67,9 @@ public interface IFsUserOperationLogService extends IService<FsUserOperationLog>
     int deleteFsUserOperationLogByLogId(Long logId);
 
     List<String> getOperationType();
+
+    /**
+     * 新增或修改用户行为
+     */
+    void saveOrUpdateOperationLog(FsUserOperationLog operationLog);
 }

+ 20 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsUserOperationLogServiceImpl.java

@@ -1,13 +1,15 @@
 package com.fs.his.service.impl;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.StringUtils;
@@ -157,6 +159,23 @@ public class FsUserOperationLogServiceImpl extends ServiceImpl<FsUserOperationLo
         return labels;
     }
 
+    /**
+     * 新增或修改用户行为
+     */
+    @DataSource(DataSourceType.SHARDING)
+    @Override
+    public void saveOrUpdateOperationLog(FsUserOperationLog operationLog) {
+        FsUserOperationLog oldOperationLog = baseMapper.selectUserOldOperationLog(operationLog.getUserId(), operationLog.getOperationType(), operationLog.getParam());
+        if (oldOperationLog == null) {
+            baseMapper.insertFsUserOperationLog(operationLog);
+        } else {
+            oldOperationLog.setCreateTime(operationLog.getCreateTime());
+            oldOperationLog.setDetails(operationLog.getDetails());
+            oldOperationLog.setUserId(null);
+            baseMapper.updateFsUserOperationLog(oldOperationLog);
+        }
+    }
+
     @Override
     public List<FsUserOperationLogVo> selectFsUserOperationLogByList(FsUserOperationLog fsUserOperationLog) {
         List<FsUserOperationLogVo> list = baseMapper.selectFsUserOperationLogByList(fsUserOperationLog);

+ 1 - 0
fs-service/src/main/java/com/fs/ipad/vo/BaseVo.java

@@ -17,6 +17,7 @@ public class BaseVo{
     private boolean isRoom;
     private Long qwUserId;
 
+
     public void setBase(BaseVo vo){
         this.uuid = vo.getUuid();
         this.serverId = vo.getServerId();

+ 5 - 0
fs-service/src/main/java/com/fs/qw/domain/QwMsg.java

@@ -75,6 +75,11 @@ public class QwMsg extends BaseEntity implements Serializable
     @Excel(name = "昵称")
     private String avatar;
 
+    /** 企微消息ID **/
+    private Long qwMsgId;
+
+    /** 企微消息info **/
+    private String qwAppInfo;
 
 
 }

+ 17 - 0
fs-service/src/main/java/com/fs/qw/domain/QwSession.java

@@ -65,5 +65,22 @@ public class QwSession extends BaseEntity implements Serializable
 
     private String qwExtWxId;//企微外部联系人wxid
 
+    /** 是否群聊 0否 1是 **/
+    private Integer isRoom;
+
+    /** 首字母 **/
+    private String firstLetter;
+
+    /** 最后一条消息id **/
+    private Long lastMsgId;
+
+    /** 最后一条消息类型 **/
+    private String lastMsgType;
+
+    /** 最后一条消息发送时间 **/
+    private Long lastSendTime;
+
+    /** 最后一条消息内容 **/
+    private String lastContent;
 
 }

+ 3 - 0
fs-service/src/main/java/com/fs/qw/domain/QwUser.java

@@ -28,6 +28,9 @@ public class QwUser extends BaseEntity
     @Excel(name = "企微用户名")
     private String qwUserName;
 
+    /** 头像 **/
+    private String avatar;
+
     /** 所属部门id */
     @Excel(name = "所属部门id")
     private String department;

+ 23 - 0
fs-service/src/main/java/com/fs/qw/dto/QwImUserDTO.java

@@ -0,0 +1,23 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+@Data
+public class QwImUserDTO {
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    /**
+     * 用户类型 1外部联系人 2企微用户
+     */
+    private Integer userType;
+    /**
+     * 昵称
+     */
+    private String userName;
+    /**
+     * 头像
+     */
+    private String avatar;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/qw/enums/MsgType.java

@@ -0,0 +1,28 @@
+package com.fs.qw.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+@Getter
+public enum MsgType {
+    TEXT(1, "text"),
+    IMAGE(2, "image"),
+    EMOTION_DYNAMIC(3, "emotionDynamic"),
+    VOICE(4, "voice"),
+    MINI_PROGRAM(5, "miniprogram"),
+    CANCEL(6, "cancel"),
+    ;
+
+    private final Integer code;
+    private final String value;
+
+    MsgType(Integer code, String value) {
+        this.code = code;
+        this.value = value;
+    }
+
+    public static MsgType getMsgType(Integer code) {
+        return Stream.of(values()).filter(t -> t.getCode().equals(code)).findFirst().orElse(null);
+    }
+}

+ 20 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -538,6 +538,26 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
      * */
     QwExternalContact selectQwUserListVOByQwUserIdAndCorpIdAndExternalUserId(ExternalContactParam externalContactParam);
 
+    @Select("SELECT * FROM qw_external_contact WHERE id = #{qwExternalContactId}")
+    QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId);
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    Boolean getBuyStatusByExtId(@Param("qwExternalContactId") Long qwExternalContactId);
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    List<QwContactVO> getContactListByQwUserId(@Param("qwUserId") Long qwUserId, @Param("name") String name);
+
+    /**
+     * 查询群组指定企微关联外部联系人
+     */
+    List<QwExternalContact> selectGroupContactByChatIdAndQwUserId(@Param("chatId") String chatId, @Param("qwUserId") Long qwUserId);
+
     void updateJoinGroup(List<Long> longs);
 
 

+ 6 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatMapper.java

@@ -2,6 +2,7 @@ package com.fs.qw.mapper;
 
 import com.fs.qw.domain.QwGroupChat;
 import com.fs.qw.param.QwGroupChatParam;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwGroupChatOptionsVO;
 import com.fs.qw.vo.QwGroupChatTransferVO;
 import com.fs.qw.vo.QwGroupChatVO;
@@ -129,5 +130,10 @@ public interface QwGroupChatMapper
      */
     List<QwGroupChatTransferVO> selectQwGroupChatTransferList(QwGroupChatParam qwGroupChat);
 
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    List<QwContactVO> getGroupListByQwUserId(@Param("qwUserId") Long qwUserId, @Param("name") String name);
+
     List<QwGroupChat> selectSopAndQwUser(@Param("qwUserId") String qwUserId, @Param("sopId") String sopId);
 }

+ 5 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwMsgMapper.java

@@ -63,6 +63,9 @@ public interface QwMsgMapper  extends BaseMapper<QwMsg>
      */
     public int deleteQwMsgByMsgIds(Long[] msgIds);
 
-    @Select("select * from qw_msg where session_id = #{sessionId} order by msg_id desc ")
-    List<QwMsg> selectQwMsgListBySession(@Param("sessionId") String conversationId);
+    /**
+     * 根据会话ID和QwAppInfo查询消息
+     */
+    @Select("select * from qw_msg where session_id = #{sessionId} and qw_app_info = #{qwAppInfo}")
+    QwMsg selectQwMsgBySessionIdAndQwAppInfo(@Param("sessionId") Long sessionId, @Param("qwAppInfo") String qwAppInfo);
 }

+ 27 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwSessionMapper.java

@@ -2,10 +2,12 @@ package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.qw.domain.QwSession;
+import com.fs.qw.vo.QwContactListVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企微会话Mapper接口
@@ -63,7 +65,30 @@ public interface QwSessionMapper extends BaseMapper<QwSession>
      */
     public int deleteQwSessionBySessionIds(Long[] sessionIds);
 
-
-    @Select("select * from qw_session where qw_ext_id = #{qwExtId} and qw_user_id = #{qwUserId}")
+    /**
+     * 查询单聊会话
+     */
+    @Select("select * from qw_session where qw_ext_id = #{qwExtId} and qw_user_id = #{qwUserId, jdbcType=VARCHAR} and is_room = 0")
     QwSession selectQwSessionByExtIdAndQwUserId(@Param("qwExtId") Long qwExtId, @Param("qwUserId") Long id);
+
+    /**
+     * 根据企微用户ID查询会话列表
+     */
+    List<QwContactListVO> selectQwConversationByMap(@Param("params") Map<String, Object> params);
+
+    /**
+     * 查询群聊会话
+     */
+    @Select("select * from qw_session where chat_id = #{chatId} and qw_user_id = #{qwUserId} and is_room = 1")
+    QwSession selectQwSessionByChatIdAndQwUserId(@Param("chatId") String chatId, @Param("qwUserId") Long id);
+
+    /**
+     * 根据群组ID查询会话
+     */
+    QwSession selectQwSessionByGroupChatId(@Param("groupChatId") String groupChatId);
+
+    /**
+     * 根据外部联系人ID查询会话
+     */
+    QwSession selectQwSessionByContactId(@Param("contactId") Long contactId);
 }

+ 9 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java

@@ -107,4 +107,13 @@ public interface QwTagMapper
             "</script>")
     List<ExternalContactTagVO> selectQwTagListByTagIds(@Param("tagIds") List<String> tagIds);
 
+    /**
+     * 根据tagIds查询标签
+     */
+    List<QwTagVO> selectQwTagVOListByTagIds(@Param("tagIds") List<String> tagIds);
+
+    /**
+     * 根据ids查询标签列表
+     */
+    List<QwTag> selectQwTagListByCorpIdAndIds(@Param("ids") List<Long> ids, @Param("corpId") String corpId);
 }

+ 8 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -479,6 +479,14 @@ public interface QwUserMapper extends BaseMapper<QwUser>
     @Select("  select company_id from qw_user where qw_user_id = #{owner} and corp_id = #{corpId}  limit 1")
     Long getCompanyIdByCorpIdAndOwner(@Param("corpId")String corpId, @Param("owner")String owner);
 
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    List<QwUserVO> selectQwUserVoListByCompanyUserId(@Param("companyUserId") Long companyUserId);
+
+    @Select("select * from qw_user where vid = #{vid} order by id desc limit 1")
+    QwUser selectQwUserByVid(@Param("vid") Long vid);
+
     @Select("<script>" +
             "select * from qw_user where qw_user_id in " +
             "<foreach collection='qwUserIdList' item='item' open='(' separator=',' close=')'> " +

+ 8 - 0
fs-service/src/main/java/com/fs/qw/param/QwMsgSendParam.java

@@ -9,5 +9,13 @@ public class QwMsgSendParam implements Serializable {
     private Long sessionId;
     private String content;//内容
     private String appKey;
+    // 消息类型 1文本 2图片 3动态表情 4语音 5小程序 6撤销
+    private Integer msgType;
+    // 小程序标题
+    private String title;
+    // 小程序图片
+    private String image;
+    // 消息ID
+    private Long msgId;
 
 }

+ 3 - 1
fs-service/src/main/java/com/fs/qw/param/QwSessionParam.java

@@ -5,6 +5,8 @@ import lombok.Data;
 
 @Data
 public class QwSessionParam extends BaseQueryParam {
-    private String conversationId;
+    private Long conversationId;
     private Long userId;//企微id
+    // 消息ID
+    private Long msgId;
 }

+ 24 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.course.param.FsCourseListBySidebarParam;
 import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwTag;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.param.*;
 import com.fs.qw.param.newparam.ExternalContactPageListParam;
@@ -257,6 +258,29 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
      * 根据qw_user_id+crop_id+external_user_id查询外部联系人信息
      * */
     QwExternalContact selectQwUserListVOByQwUserIdAndCorpIdAndExternalUserId(ExternalContactParam externalContactParam);
+    /**
+     * 根据id查询外部联系人信息
+     * @param qwExternalContactId id
+     * @return QwExternalContact
+     */
+    QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId);
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    Boolean getBuyStatusByExtId(Long qwExternalContactId);
+
+    /**
+     * 外部联系人添加标签
+     */
+    void addQwExternalContactTag(Long id, List<String> addTags);
+
+    /**
+     * 外部联系人删除标签
+     */
+    void delQwExternalContactTag(Long id, List<String> delTags);
 
     List<QwExternalContact> selectQwUserAndLevel(List<Long> qwUserIdList, List<String> levelList, boolean isReg);
 }

+ 21 - 7
fs-service/src/main/java/com/fs/qw/service/IQwMsgService.java

@@ -8,10 +8,12 @@ import com.fs.qw.domain.QwUser;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
 import com.fs.qw.vo.QwContactListVO;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookMsgVO;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企微聊天记录Service接口
@@ -76,12 +78,11 @@ public interface IQwMsgService extends IService<QwMsg>
      * @return
      */
     List<QwMsg> getQwMessageList(Long companyId);
+
     /**
-     * 查询会话
-     * @param userId
-     * @return
+     * 查询会话列表
      */
-    List<QwContactListVO> selectQwConversationByUserId(Long userId);
+    List<QwContactListVO> selectQwConversationByMap(Map<String, Object> params);
 
     List<QwMsg> selectQwMsgBySession(QwSessionParam param);
 
@@ -91,9 +92,22 @@ public interface IQwMsgService extends IService<QwMsg>
 
     R addAiMsg(QwSession session, String content, Integer msgType,QwUser user);
 
-    List<QwUser> qwUserList(Long userId);
-
     R sendMsg(QwMsgSendParam param);
 
-    QwContactListVO selectQwSessionBycId(String conversationId, Long qwUserId);
+    QwContactListVO selectQwSessionBycId(Long conversationId, Long qwUserId);
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    List<QwContactVO> contactListByQwUserId(Long qwUserId, String name);
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    List<QwContactVO> groupListByQwUserId(Long qwUserId, String name);
+
+    /**
+     * 获取会话ID
+     */
+    QwContactListVO getConversationIdById(Long qwUserid, String id, Boolean isGroup);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/qw/service/IQwTagService.java

@@ -72,4 +72,14 @@ public interface IQwTagService
     public List<String> selectQwTagListByTagIds(QwTagSearchParam param);
 
     List<QwTagVO> getTagListByUserId(ContactTagListParam param);
+
+    /**
+     * 根据tagIds查询标签
+     */
+    List<QwTagVO> selectQwTagVOListByTagIds(List<String> tagIds);
+
+    /**
+     * 根据ids查询标签列表
+     */
+    List<QwTag> selectQwTagListByCorpIdAndIds(List<Long> ids, String corpId);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/qw/service/IQwUserService.java

@@ -203,5 +203,15 @@ public interface IQwUserService
 
     List<Long> selectDeptByParentId(Long deptId,String cropId);
 
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    List<QwUserVO> selectQwUserVoListByCompanyUserId(Long companyUserId);
+
+    /**
+     * 根据Vid查询企微用户
+     */
+    QwUser selectQwUserByVid(Long vid);
+
     List<QwUser> selectQwUserByIds(List<Long> qwUserIdList);
 }

+ 69 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.date.DateUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONException;
+import com.alibaba.fastjson.TypeReference;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -12,6 +13,7 @@ import com.fs.ad.enums.AdUploadType;
 import com.fs.ad.service.IAdHtmlClickLogService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyConfigService;
@@ -5977,4 +5979,71 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     }
 
 
+    /**
+     * 根据id查询外部联系人信息
+     * @param qwExternalContactId id
+     * @return QwExternalContact
+     */
+    @Override
+    public QwExternalContact getQwExternalContactDetailsById(Long qwExternalContactId) {
+        return qwExternalContactMapper.getQwExternalContactDetailsById(qwExternalContactId);
+    }
+
+    /**
+     * 根据外部联系人ID查询用户是否已购产品
+     * @param qwExternalContactId   外部联系人ID
+     * @return  Boolean
+     */
+    @Override
+    public Boolean getBuyStatusByExtId(Long qwExternalContactId) {
+        return qwExternalContactMapper.getBuyStatusByExtId(qwExternalContactId);
+    }
+
+    /**
+     * 外部联系人添加标签
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void addQwExternalContactTag(Long id, List<String> addTags) {
+        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(id);
+
+        QwEditUserTagParam param = new QwEditUserTagParam();
+        param.setAdd_tag(addTags);
+        param.setUserid(externalContact.getUserId());
+        param.setExternal_userid(externalContact.getExternalUserId());
+        QwResult qwResult = qwApiService.editUserTag(param, externalContact.getCorpId());
+        if (qwResult.getErrcode() != 0) {
+            throw new ServiceException(qwResult.getErrmsg());
+        }
+
+        List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+        oldTags.addAll(addTags);
+        externalContact.setTagIds(JSON.toJSONString(oldTags));
+        qwExternalContactMapper.updateById(externalContact);
+    }
+
+    /**
+     * 外部联系人删除标签
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void delQwExternalContactTag(Long id, List<String> delTags) {
+        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(id);
+
+        QwEditUserTagParam param = new QwEditUserTagParam();
+        param.setRemove_tag(delTags);
+        param.setUserid(externalContact.getUserId());
+        param.setExternal_userid(externalContact.getExternalUserId());
+        QwResult qwResult = qwApiService.editUserTag(param, externalContact.getCorpId());
+        if (qwResult.getErrcode() != 0) {
+            throw new ServiceException(qwResult.getErrmsg());
+        }
+
+        List<String> oldTags = JSON.parseObject(externalContact.getTagIds(), new TypeReference<List<String>>(){}.getType());
+        delTags.forEach(oldTags::remove);
+        externalContact.setTagIds(JSON.toJSONString(oldTags));
+        qwExternalContactMapper.updateById(externalContact);
+    }
+
+
 }

+ 453 - 63
fs-service/src/main/java/com/fs/qw/service/impl/QwMsgServiceImpl.java

@@ -2,31 +2,44 @@ package com.fs.qw.service.impl;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.http.HttpRequest;
-import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PinYinUtil;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyMiniapp;
+import com.fs.company.mapper.CompanyMiniappMapper;
+import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.mapper.FsCoursePlaySourceConfigMapper;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.qw.Bean.MsgBean;
 import com.fs.qw.domain.*;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwMsgMapper;
-import com.fs.qw.mapper.QwSessionMapper;
-import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.enums.MsgType;
+import com.fs.qw.mapper.*;
 import com.fs.qw.param.QwMsgSendParam;
 import com.fs.qw.param.QwSessionParam;
 import com.fs.qw.service.IQwMsgService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.QwContactListVO;
+import com.fs.qw.vo.QwContactVO;
 import com.fs.qw.vo.QwMessageListVO;
 import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.wxwork.dto.*;
+import com.fs.wxwork.service.WxWorkService;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -35,6 +48,7 @@ import java.util.stream.Collectors;
  * @author fs
  * @date 2024-12-17
  */
+@Slf4j
 @Service
 public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements IQwMsgService {
     @Autowired
@@ -50,6 +64,18 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
     @Autowired
     private ConfigUtil configUtil;
+    @Autowired
+    private WxWorkService wxWorkService;
+    @Autowired
+    private CompanyMiniappMapper companyMiniappMapper;
+    @Autowired
+    private FsCoursePlaySourceConfigMapper playSourceConfigMapper;
+    @Autowired
+    private QwExternalContactMapper externalContactMapper;
+    @Autowired
+    private QwGroupChatMapper groupChatMapper;
+    @Autowired
+    private RedissonClient redissonClient;
 
     /**
      * 查询企微聊天记录
@@ -139,7 +165,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                     qwSession.setQwUserId(qwuser.getId().toString());
                     qwSession.setStatus(1);
                     qwSession.setAvatar(qwExternalContact.getAvatar());
-                    qwSession.setNickName(qwExternalContact.getRemark());
+                    qwSession.setNickName(qwExternalContact.getName());
                     qwSession.setCompanyId(qwuser.getCompanyId());
                     qwSession.setCompanyUserId(qwuser.getCompanyUserId());
                     qwSession.setCreateTime(new Date());
@@ -147,7 +173,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                     qwSessionMapper.insertQwSession(qwSession);
                 }else {
                     qwSession.setUpdateTime(new Date());
-                    qwSession.setNickName(qwExternalContact.getRemark());
+                    qwSession.setNickName(qwExternalContact.getName());
                     qwSessionMapper.updateQwSession(qwSession);
                 }
                 QwMsg qwMsg = new QwMsg();
@@ -161,7 +187,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
                 qwMsg.setQwUserId(qwSession.getQwUserId());
                 qwMsg.setQwExtId(qwSession.getQwExtId());
                 qwMsg.setAvatar(qwExternalContact.getAvatar());
-                qwMsg.setNickName(qwExternalContact.getRemark());
+                qwMsg.setNickName(qwExternalContact.getName());
                 qwMsg.setCreateTime(new Date());
                 if (qwMsgMapper.insertQwMsg(qwMsg) > 0) {
                     //发送socket
@@ -235,7 +261,7 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             //客服发送
             qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
             qwFromUser.setDisplayName(user.getQwUserName());
-            qwFromUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png");
+            qwFromUser.setAvatar(user.getAvatar());
             msg.setFromUser(qwFromUser);
             sendSocket("receiveMsg",JSONObject.toJSONString(msg),user.getAppKey());
         }
@@ -244,24 +270,280 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
     @Override
     public R sendMsg(QwMsgSendParam param) {
-        FsSysConfig config = configUtil.getSysConfig();
-        String domainName = config.getHookUrl();
-        HttpRequest.post(domainName+"/app/qwmsg/sendMsg")
-                .body(JSON.toJSONString(param),"application/json;charset=UTF-8")
-                .execute().body();
-        return R.ok();
-    }
+        if (StringUtils.isBlank(param.getContent()) && param.getMsgType() != 6) {
+            return R.error("消息内容不能为空");
+        }
 
-    @Override
-    public List<QwUser> qwUserList(Long userId) {
-        LambdaQueryWrapper<QwUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-        lambdaQueryWrapper.eq(QwUser::getCompanyUserId, userId);
-        lambdaQueryWrapper.eq(QwUser::getLoginStatus,1);
-        lambdaQueryWrapper.eq(QwUser::getToolStatus,1);
-        List<QwUser> qwUsers = qwUserMapper.selectList(lambdaQueryWrapper);
-        return qwUsers;
-    }
+        if (Objects.isNull(param.getSessionId())) {
+            return R.error("会话ID不能为空");
+        }
+
+        if (Objects.isNull(param.getMsgType())) {
+            return R.error("消息类型不能为空");
+        }
+
+        // 查询会话
+        QwSession qwSession = qwSessionMapper.selectQwSessionBySessionId(param.getSessionId());
+        if (Objects.isNull(qwSession)) {
+            return R.error("会话不存在");
+        }
+
+        // 企微用户
+        QwUser qwUser = qwUserMapper.selectQwUserById(Long.parseLong(qwSession.getQwUserId()));
+        if (Objects.isNull(qwUser)) {
+            return R.error("用户不存在");
+        }
+
+        Long serverId = qwUser.getServerId();
+        String uuid = qwUser.getUid();
+        String sCorpId = qwUser.getCorpId();
+        boolean isRoom = qwSession.getIsRoom() == 1;
+
+        long sendUserId;
+        QwExternalContact qwExternalContact = null;
+        if (isRoom) {
+            WxWorkChatId2RoomIdDTO chatId2RoomIdDTO = new WxWorkChatId2RoomIdDTO();
+            chatId2RoomIdDTO.setUuid(uuid);
+            chatId2RoomIdDTO.setChatid(qwSession.getChatId());
+            WxWorkResponseDTO<WxWorkChatId2RoomIdResp> chatId2RoomIdResp = wxWorkService.chatId2RoomId(chatId2RoomIdDTO, serverId);
+            if (chatId2RoomIdResp.getErrcode() != 0) {
+                return R.error(chatId2RoomIdResp.getErrmsg());
+            }
+            sendUserId = chatId2RoomIdResp.getData().getRoom_id();
+        } else {
+            // 外部联系人
+            qwExternalContact = qwExternalContactMapper.selectQwExternalContactById(Long.valueOf(qwSession.getQwExtId()));
+            if (Objects.isNull(qwExternalContact)) {
+                return R.error("联系人不存在");
+            }
+            String openId = qwExternalContact.getExternalUserId();
+            WxWorkUserId2VidDTO params = new WxWorkUserId2VidDTO();
+            params.setOpenid(Collections.singletonList(openId));
+            params.setUuid(uuid);
+            params.setScorpid(sCorpId);
+            WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> listWxWorkResponseDTO = wxWorkService.UserId2Vid(params, serverId);
+
+            if (listWxWorkResponseDTO.getErrcode() != 0) {
+                return R.error(listWxWorkResponseDTO.getErrmsg());
+            }
+            sendUserId = listWxWorkResponseDTO.getData().get(0).getUser_id();
+        }
+
+        String msgJson;
 
+        Long qwMsgId = null;
+        String qwAppInfo = null;
+        MsgType msgType = MsgType.getMsgType(param.getMsgType());
+        // 发送消息  文本
+        if (MsgType.TEXT == msgType) {
+            WxWorkSendTextMsgDTO textMsgDTO = new WxWorkSendTextMsgDTO();
+            textMsgDTO.setUuid(uuid);
+            textMsgDTO.setSend_userid(sendUserId);
+            textMsgDTO.setIsRoom(isRoom);
+            textMsgDTO.setContent(param.getContent());
+            WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> msgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(textMsgDTO, serverId);
+
+            if (msgRespDTOWxWorkResponseDTO.getErrcode() != 0) {
+                return R.error(msgRespDTOWxWorkResponseDTO.getErrmsg());
+            }
+            msgJson = JSONObject.toJSONString(textMsgDTO);
+            WxWorkSendTextMsgRespDTO data = msgRespDTOWxWorkResponseDTO.getData();
+            qwMsgId = data.getMsg_id();
+            qwAppInfo = data.getApp_info();
+        }
+
+        // 图片
+        else if (MsgType.IMAGE == msgType) {
+            WxCdnUploadImgLinkDTO linkDTO = new WxCdnUploadImgLinkDTO();
+            linkDTO.setUuid(uuid);
+            linkDTO.setUrl(param.getContent());
+            WxWorkResponseDTO<WxCdnUploadImgLinkResp> imgLinkResp = wxWorkService.cdnUploadImgLink(linkDTO, serverId);
+            if (imgLinkResp.getErrcode() != 0) {
+                return R.error(imgLinkResp.getErrmsg());
+            }
+            WxCdnUploadImgLinkResp data = imgLinkResp.getData();
+
+            // 发送图片消息
+            WxwSendCDNImgMsgDTO imgMsgDTO = new WxwSendCDNImgMsgDTO();
+            imgMsgDTO.setUuid(uuid);
+            imgMsgDTO.setSend_userid(sendUserId);
+            imgMsgDTO.setIsRoom(isRoom);
+            imgMsgDTO.setCdnkey(data.getCdn_key());
+            imgMsgDTO.setAeskey(data.getAes_key());
+            imgMsgDTO.setMd5(data.getMd5());
+            imgMsgDTO.setFileSize(data.getSize());
+            WxWorkResponseDTO<WxwSendCDNImgMsgRespDTO> imgMsgResp = wxWorkService.SendCDNImgMsg(imgMsgDTO, serverId);
+            if (imgMsgResp.getErrcode() != 0) {
+                return R.error(imgMsgResp.getErrmsg());
+            }
+            msgJson = JSONObject.toJSONString(imgMsgDTO);
+            WxwSendCDNImgMsgRespDTO imgData = imgMsgResp.getData();
+            qwMsgId = imgData.getMsg_id();
+            qwAppInfo = imgData.getApp_info();
+        }
+        // 小程序
+        else if (MsgType.MINI_PROGRAM == msgType) {
+            String pagepath = param.getContent();
+
+            // 查询公司对应小程序配置
+            CompanyMiniapp miniappParams = new CompanyMiniapp();
+            miniappParams.setCompanyId(qwUser.getCompanyId());
+            List<CompanyMiniapp> companyMiniapps = companyMiniappMapper.selectCompanyMiniappList(miniappParams);
+            if (companyMiniapps == null || companyMiniapps.isEmpty()) {
+                return R.error("用户所属销售公司主备小程序未配置");
+            }
+
+            FsCoursePlaySourceConfig config = null;
+            companyMiniapps.sort(Comparator.comparing(CompanyMiniapp::getType));
+            for (CompanyMiniapp companyMiniapp : companyMiniapps) {
+                if (config == null) {
+                    Wrapper<FsCoursePlaySourceConfig> queryWrapper = Wrappers.<FsCoursePlaySourceConfig>lambdaQuery()
+                            .eq(FsCoursePlaySourceConfig::getAppid, companyMiniapp.getAppId())
+                            .eq(FsCoursePlaySourceConfig::getIsDel, 0).last("limit 1");
+                    config = playSourceConfigMapper.selectOne(queryWrapper);
+                }
+            }
+
+            if (config == null) {
+                return R.error("用户所属销售公司主备小程序配置错误");
+            }
+
+            String img = StringUtils.isNotBlank(param.getImage()) ? param.getImage() : config.getImg();
+            WxCdnUploadImgLinkDTO linkDTO = new WxCdnUploadImgLinkDTO();
+            linkDTO.setUuid(uuid);
+            linkDTO.setUrl(img);
+            WxWorkResponseDTO<WxCdnUploadImgLinkResp> imgLinkResp = wxWorkService.cdnUploadImgLink(linkDTO, serverId);
+            if (imgLinkResp.getErrcode() != 0) {
+                return R.error(imgLinkResp.getErrmsg());
+            }
+            WxCdnUploadImgLinkResp data = imgLinkResp.getData();
+
+            // 发送小程序消息
+            WxWorkSendAppMsgDTO appMsgDTO = new WxWorkSendAppMsgDTO();
+            appMsgDTO.setUuid(uuid);
+            appMsgDTO.setSend_userid(sendUserId);
+            appMsgDTO.setDesc(param.getTitle());
+            appMsgDTO.setTitle(config.getName());
+            appMsgDTO.setWeappIconUrl(img);
+            appMsgDTO.setPagepath(pagepath);
+            appMsgDTO.setUsername(config.getOriginalId() + "@app");
+            appMsgDTO.setAppid(config.getAppid());
+            appMsgDTO.setCdnkey(data.getCdn_key());
+            appMsgDTO.setMd5(data.getMd5());
+            appMsgDTO.setAeskey(data.getAes_key());
+            appMsgDTO.setFileSize(data.getSize());
+            appMsgDTO.setIsRoom(isRoom);
+            WxWorkResponseDTO<WxWorkSendAppMsgRespDTO> appMsgResp = wxWorkService.SendAppMsg(appMsgDTO, serverId);
+            if (appMsgResp.getErrcode() != 0) {
+                return R.error(appMsgResp.getErrmsg());
+            }
+
+            JSONObject json = new JSONObject();
+            json.put("appid", config.getAppid());
+            json.put("appName", config.getName());
+            json.put("weappIconUrl", img);
+            json.put("desc", param.getTitle());
+            json.put("pagepath", pagepath);
+            json.put("title", param.getTitle());
+            json.put("thumbnail", img);
+
+            msgJson = json.toJSONString();
+            param.setContent(msgJson);
+
+            WxWorkSendAppMsgRespDTO appData = appMsgResp.getData();
+            qwMsgId = appData.getMsg_id();
+            qwAppInfo = appData.getApp_info();
+        }
+        // 消息撤销
+        else if (MsgType.CANCEL == msgType) {
+            QwMsg qwMsg = qwMsgMapper.selectQwMsgByMsgId(param.getMsgId());
+            if (qwMsg == null || qwMsg.getQwMsgId() == null) {
+                return R.error("消息不存在");
+            }
+
+            if (!Objects.equals(qwSession.getSessionId(), param.getSessionId())) {
+                return R.error("会话错误,操作不允许");
+            }
+
+            WxwRevokeMsgDTO params = new WxwRevokeMsgDTO();
+            params.setUuid(uuid);
+            params.setMsgid(qwMsg.getQwMsgId().intValue());
+            params.setRoomid(isRoom ? sendUserId : 0);
+            WxWorkResponseDTO wxWorkResponseDTO = wxWorkService.RevokeMsg(params, serverId);
+            if (wxWorkResponseDTO.getErrcode() != 0) {
+                return R.error(wxWorkResponseDTO.getErrmsg());
+            }
+
+            qwMsg.setMsgType(6);
+            qwMsg.setSessionId(null);
+            qwMsgMapper.updateQwMsg(qwMsg);
+
+            // 组装返回消息结构
+            QwMessageListVO listVO = new QwMessageListVO();
+            QWFromUser qwFromUser = new QWFromUser();
+            qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
+            qwFromUser.setDisplayName(qwUser.getQwUserName());
+            qwFromUser.setAvatar(qwUser.getAvatar());
+            listVO.setType(msgType.getValue());
+            listVO.setStatus("succeed");
+            listVO.setFromUser(qwFromUser);
+            listVO.setSendTime(qwMsg.getCreateTime().getTime());
+            listVO.setId(qwMsg.getMsgId().toString());
+            listVO.setContent(qwMsg.getContent());
+            listVO.setToContactId(String.valueOf(param.getSessionId()));
+            listVO.setAppKey(qwUser.getAppKey());
+            return R.ok().put("data", listVO);
+        } else {
+            return R.error("暂不支持的消息类型");
+        }
+
+        // 消息保存本地数据库
+        QwMsg qwMsg = new QwMsg();
+        qwMsg.setContent(param.getContent());
+        qwMsg.setSessionId(qwSession.getSessionId());
+        qwMsg.setSendType(2);
+        qwMsg.setCompanyId(qwUser.getCompanyId());
+        qwMsg.setCompanyUserId(qwUser.getCompanyUserId());
+        qwMsg.setMsgType(param.getMsgType());
+        qwMsg.setMsgJson(msgJson);
+        qwMsg.setStatus(0);
+        qwMsg.setQwUserId(qwSession.getQwUserId());
+        qwMsg.setCreateTime(new Date());
+        qwMsg.setQwMsgId(qwMsgId);
+        qwMsg.setQwAppInfo(qwAppInfo);
+
+        if (Objects.isNull(qwExternalContact)) {
+            qwMsg.setAvatar(qwUser.getAvatar());
+            qwMsg.setNickName(qwUser.getQwUserName());
+        } else {
+            qwMsg.setQwExtId(qwExternalContact.getId().toString());
+            qwMsg.setAvatar(qwExternalContact.getAvatar());
+            qwMsg.setNickName(qwExternalContact.getName());
+        }
+        qwMsgMapper.insertQwMsg(qwMsg);
+
+        qwSession.setLastMsgId(qwMsg.getMsgId());
+        qwSession.setLastMsgType(msgType.getValue());
+        qwSession.setLastSendTime(qwMsg.getCreateTime().getTime());
+        qwSession.setLastContent(qwMsg.getContent());
+        qwSessionMapper.updateQwSession(qwSession);
+
+        // 组装返回消息结构
+        QwMessageListVO listVO = new QwMessageListVO();
+        QWFromUser qwFromUser = new QWFromUser();
+        qwFromUser.setId(Long.parseLong(qwMsg.getQwUserId()));
+        qwFromUser.setDisplayName(qwUser.getQwUserName());
+        qwFromUser.setAvatar(qwUser.getAvatar());
+        listVO.setType(msgType.getValue());
+        listVO.setStatus("succeed");
+        listVO.setFromUser(qwFromUser);
+        listVO.setSendTime(qwMsg.getCreateTime().getTime());
+        listVO.setId(qwMsg.getMsgId().toString());
+        listVO.setContent(qwMsg.getContent());
+        listVO.setToContactId(String.valueOf(param.getSessionId()));
+        listVO.setAppKey(qwUser.getAppKey());
+        return R.ok().put("data", listVO);
+    }
 
     @Override
     public List<QwMsg> getQwMessageList(Long companyId) {
@@ -286,46 +568,22 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         return qwMsgs;
     }
 
+    /**
+     * 查询会话列表
+     */
     @Override
-    public List<QwContactListVO> selectQwConversationByUserId(Long userId) {
-        LambdaQueryWrapper<QwSession> sessionWrapper = new LambdaQueryWrapper<>();
-        sessionWrapper.eq(QwSession::getQwUserId, userId);
-        sessionWrapper.orderByDesc(QwSession::getUpdateTime);
-        List<QwSession> qwSessions = qwSessionMapper.selectList(sessionWrapper);
-        if (CollectionUtil.isEmpty(qwSessions)){
-            return Collections.EMPTY_LIST;
-        }
-        ArrayList<QwContactListVO> qwContactListVOS = new ArrayList<>();
-        for (QwSession qwSession : qwSessions) {
-            QwContactListVO listVO = new QwContactListVO();
-            listVO.setId(userId);
-            listVO.setAvatar(qwSession.getAvatar());
-            listVO.setConversationId(qwSession.getSessionId().toString());
-            listVO.setDisplayName(qwSession.getNickName());
-//            listVO.setIndex(qwSession.getNickName().substring(0, 1));
-            LambdaQueryWrapper<QwMsg> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-            lambdaQueryWrapper.eq(QwMsg::getSessionId, qwSession.getSessionId());
-            lambdaQueryWrapper.orderByDesc(QwMsg::getMsgId);
-            lambdaQueryWrapper.last("limit 1");
-            List<QwMsg> qwMsgs = qwMsgMapper.selectList(lambdaQueryWrapper);
-            if (CollectionUtil.isEmpty(qwMsgs)){
-                listVO.setLastContent("");
-                listVO.setLastSendTime(new Date().getTime());
-                qwContactListVOS.add(listVO);
-                break;
-            }
-            listVO.setLastContent(qwMsgs.get(0).getContent());
-            listVO.setLastSendTime(qwMsgs.get(0).getCreateTime().getTime());
-            listVO.setUnread(0);
-            qwContactListVOS.add(listVO);
-        }
-        return qwContactListVOS;
+    public List<QwContactListVO> selectQwConversationByMap(Map<String, Object> params) {
+        return qwSessionMapper.selectQwConversationByMap(params);
     }
 
     @Override
     public List<QwMsg> selectQwMsgBySession(QwSessionParam param) {
         LambdaQueryWrapper<QwMsg> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+        lambdaQueryWrapper.select(QwMsg.class, q -> !q.getColumn().equals("remark"));
         lambdaQueryWrapper.eq(QwMsg::getSessionId, param.getConversationId());
+        if (Objects.nonNull(param.getMsgId())) {
+            lambdaQueryWrapper.gt(QwMsg::getMsgId, param.getMsgId());
+        }
         lambdaQueryWrapper.orderByDesc(QwMsg::getMsgId);
         List<QwMsg> records = qwMsgMapper.selectList(lambdaQueryWrapper);
         return records;
@@ -337,7 +595,12 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         List<QwMessageListVO> qwMessageVOS = new ArrayList<>();
         for (QwMsg record : list) {
             QwMessageListVO listVO = new QwMessageListVO();
-            listVO.setType("text");
+            String type = "text";
+            MsgType msgType = MsgType.getMsgType(record.getMsgType());
+            if (Objects.nonNull(record.getMsgType())) {
+                type = msgType.getValue();
+            }
+            listVO.setType(type);
             listVO.setStatus("succeed");
             QWFromUser qwFromUser = new QWFromUser();
             //用户发送
@@ -348,13 +611,15 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
             }else if(record.getSendType() == 2){
                 qwFromUser.setId(Long.parseLong(record.getQwUserId()));
                 qwFromUser.setDisplayName(user.getQwUserName());
-                qwFromUser.setAvatar("https://cos.his.cdwjyyh.com/fs/20241231/22a765a96da247d1b83ea94fef438a41.png");
+                qwFromUser.setAvatar(user.getAvatar());
             }
+            listVO.setExtId(record.getQwExtId());
             listVO.setFromUser(qwFromUser);
             listVO.setSendTime(record.getCreateTime().getTime());
             listVO.setId(record.getMsgId().toString());
             listVO.setContent(record.getContent());
-            listVO.setToContactId(param.getConversationId());
+            listVO.setToContactId(String.valueOf(param.getConversationId()));
+            listVO.setAppKey(user.getAppKey());
             qwMessageVOS.add(listVO);
         }
         return qwMessageVOS;
@@ -362,8 +627,8 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
 
 
     @Override
-    public QwContactListVO selectQwSessionBycId(String sessionId, Long qwUserId) {
-        QwSession session = qwSessionMapper.selectQwSessionBySessionId(Long.parseLong(sessionId));
+    public QwContactListVO selectQwSessionBycId(Long sessionId, Long qwUserId) {
+        QwSession session = qwSessionMapper.selectQwSessionBySessionId(sessionId);
         QwContactListVO listVO = new QwContactListVO();
         listVO.setId(Long.parseLong(session.getQwUserId()));
         listVO.setAvatar(session.getAvatar());
@@ -385,4 +650,129 @@ public class QwMsgServiceImpl extends ServiceImpl<QwMsgMapper, QwMsg> implements
         listVO.setUnread(0);
         return listVO;
     }
+
+    /**
+     * 根据企微用户ID查询联系人列表
+     */
+    @Override
+    public List<QwContactVO> contactListByQwUserId(Long qwUserId, String name) {
+        return externalContactMapper.getContactListByQwUserId(qwUserId, name);
+    }
+
+    /**
+     * 根据企微用户ID查询群组列表
+     */
+    @Override
+    public List<QwContactVO> groupListByQwUserId(Long qwUserId, String name) {
+        return groupChatMapper.getGroupListByQwUserId(qwUserId, name);
+    }
+
+    /**
+     * 获取会话ID
+     */
+    @Override
+    public QwContactListVO getConversationIdById(Long qwUserId, String id, Boolean isGroup) {
+
+        QwUser qwUser = qwUserMapper.selectQwUserById(qwUserId);
+        if (qwUser == null) {
+            throw new ServiceException("企微用户不存在");
+        }
+
+        QwSession qwSession;
+        if (isGroup) {
+            qwSession = qwSessionMapper.selectQwSessionByGroupChatId(id);
+            if (qwSession == null) {
+                QwGroupChat qwGroupChat = groupChatMapper.selectQwGroupChatByChatId(id);
+                if (qwGroupChat == null) {
+                    throw new ServiceException("群聊不存在");
+                }
+
+                RLock lock = redissonClient.getLock("addSession:" + id + ":" + qwUserId);
+                try {
+                    boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                    if (!tryLock) {
+                        throw new ServiceException("操作太频繁,请稍后再试");
+                    }
+
+                    qwSession = qwSessionMapper.selectQwSessionByGroupChatId(id);
+                    if (qwSession == null) {
+                        String firstLetter = PinYinUtil.getFirstLetter(qwGroupChat.getName());
+                        qwSession = new QwSession();
+                        qwSession.setChatId(qwGroupChat.getChatId());
+                        qwSession.setCorpId(qwGroupChat.getCorpId());
+                        qwSession.setQwUserId(qwUserId.toString());
+                        qwSession.setStatus(1);
+                        qwSession.setNickName(qwGroupChat.getName());
+                        qwSession.setCompanyId(qwGroupChat.getCompanyId());
+                        qwSession.setCreateTime(new Date());
+                        qwSession.setUpdateTime(new Date());
+                        qwSession.setIsRoom(1);
+                        qwSession.setFirstLetter(firstLetter);
+                        qwSessionMapper.insertQwSession(qwSession);
+                    }
+                } catch (InterruptedException e) {
+                    log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                    throw new ServiceException("操作太频繁,请稍后再试");
+                } finally {
+                    lock.unlock();
+                }
+            }
+        } else {
+            qwSession = qwSessionMapper.selectQwSessionByContactId(Long.parseLong(id));
+            if (qwSession == null) {
+                QwExternalContact qwExternalContact = externalContactMapper.selectQwExternalContactById(Long.valueOf(id));
+                if (qwExternalContact == null) {
+                    throw new ServiceException("外部联系人不存在");
+                }
+
+                RLock lock = redissonClient.getLock("addSession:" + id + ":" + qwUserId);
+                try {
+                    boolean tryLock = lock.tryLock(2, 3, TimeUnit.SECONDS);
+                    if (!tryLock) {
+                        throw new ServiceException("操作太频繁,请稍后再试");
+                    }
+
+                    qwSession = qwSessionMapper.selectQwSessionByContactId(Long.parseLong(id));
+                    if (qwSession == null) {
+                        String firstLetter = PinYinUtil.getFirstLetter(qwExternalContact.getName());
+                        qwSession = new QwSession();
+                        String chatId = UUID.randomUUID().toString();
+                        qwSession.setChatId(chatId);
+                        qwSession.setCorpId(qwUser.getCorpId());
+                        qwSession.setQwExtId(qwExternalContact.getId().toString());
+                        qwSession.setQwUserId(qwUser.getId().toString());
+                        qwSession.setStatus(1);
+                        qwSession.setAvatar(qwExternalContact.getAvatar());
+                        qwSession.setNickName(qwExternalContact.getName());
+                        qwSession.setCompanyId(qwUser.getCompanyId());
+                        qwSession.setCompanyUserId(qwUser.getCompanyUserId());
+                        qwSession.setCreateTime(new Date());
+                        qwSession.setUpdateTime(new Date());
+                        qwSession.setIsRoom(0);
+                        qwSession.setFirstLetter(firstLetter);
+                        qwSessionMapper.insertQwSession(qwSession);
+                    }
+                } catch (InterruptedException e) {
+                    log.warn("获取锁失败 err: {}", e.getMessage(), e);
+                    throw new ServiceException("操作太频繁,请稍后再试");
+                } finally {
+                    lock.unlock();
+                }
+            }
+        }
+
+        QwContactListVO qwContactListVO = new QwContactListVO();
+        qwContactListVO.setId(qwSession.getSessionId());
+        qwContactListVO.setConversationId(String.valueOf(qwSession.getSessionId()));
+        qwContactListVO.setExtId(qwSession.getQwExtId());
+        qwContactListVO.setAvatar(qwSession.getAvatar());
+        qwContactListVO.setDisplayName(qwSession.getNickName());
+        qwContactListVO.setIndex(qwSession.getFirstLetter());
+        qwContactListVO.setIsGroup(isGroup);
+        qwContactListVO.setUnread(0);
+        qwContactListVO.setLastSendTime(new Date().getTime());
+        qwContactListVO.setLastContent("");
+
+        return qwContactListVO;
+    }
 }

+ 16 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwTagServiceImpl.java

@@ -209,4 +209,20 @@ public class QwTagServiceImpl implements IQwTagService
         }
         return qwTagMapper.selectTagListByUserId(param, keywords);
     }
+
+    /**
+     * 根据tagIds查询标签
+     */
+    @Override
+    public List<QwTagVO> selectQwTagVOListByTagIds(List<String> tagIds) {
+        return qwTagMapper.selectQwTagVOListByTagIds(tagIds);
+    }
+
+    /**
+     * 根据ids查询标签列表
+     */
+    @Override
+    public List<QwTag> selectQwTagListByCorpIdAndIds(List<Long> ids, String corpId) {
+        return qwTagMapper.selectQwTagListByCorpIdAndIds(ids, corpId);
+    }
 }

+ 16 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1588,6 +1588,22 @@ public class QwUserServiceImpl implements IQwUserService
         return qwUserMapper.selectDeptByParentId(deptId,cropId);
     }
 
+    /**
+     * 根据销售ID查询企微用户列表
+     */
+    @Override
+    public List<QwUserVO> selectQwUserVoListByCompanyUserId(Long companyUserId) {
+        return qwUserMapper.selectQwUserVoListByCompanyUserId(companyUserId);
+    }
+
+    /**
+     * 根据Vid查询企微用户
+     */
+    @Override
+    public QwUser selectQwUserByVid(Long vid) {
+        return qwUserMapper.selectQwUserByVid(vid);
+    }
+
 
     /**
      * 构建查询条件

+ 12 - 1
fs-service/src/main/java/com/fs/qw/vo/QwContactListVO.java

@@ -14,5 +14,16 @@ public class QwContactListVO {
     private Long roomId;
     private Long lastSendTime;
     private String lastContent;
-
+    // 消息ID
+    private Long msgId;
+    // 消息类型
+    private String type;
+    // 外部联系人ID
+    private String extId;
+    // 是否黑粉
+    private Boolean isBlack;
+    // 是否重粉
+    private Boolean isRepeat;
+    // 是否待处理
+    private Boolean isPend;
 }

+ 35 - 0
fs-service/src/main/java/com/fs/qw/vo/QwContactVO.java

@@ -0,0 +1,35 @@
+package com.fs.qw.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class QwContactVO {
+
+    @ApiModelProperty("唯一ID")
+    private String id;
+
+    @ApiModelProperty("会话ID")
+    private Long conversationId;
+
+    @ApiModelProperty("名称")
+    private String displayName;
+
+    @ApiModelProperty("头像")
+    private String avatar;
+
+    @ApiModelProperty("通讯录索引,传入字母或数字进行排序,索引可以显示自定义文字“[1]群组”")
+    private String index;
+
+    @ApiModelProperty("未读消息数")
+    private Integer unread;
+
+    @ApiModelProperty("最近一条消息的时间戳,13位毫秒")
+    private Long lastSendTime;
+
+    @ApiModelProperty("最近一条消息的内容")
+    private String lastContent;
+
+    @ApiModelProperty("是否群聊")
+    private Boolean isGroup;
+}

Деякі файли не було показано, через те що забагато файлів було змінено