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

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

# Conflicts:
#	fs-user-app/src/main/java/com/fs/app/controller/CourseController.java
caoliqin 1 тиждень тому
батько
коміт
c582cda715
100 змінених файлів з 9275 додано та 325 видалено
  1. 3 0
      fs-ad-api/src/main/java/com/fs/FsAdApiApplication.java
  2. 0 31
      fs-ad-api/src/main/java/com/fs/app/mq/RocketMQAiMsgService.java
  3. 1 1
      fs-ad-api/src/main/java/com/fs/app/mq/RocketMQConsumerService.java
  4. 0 26
      fs-ad-api/src/main/java/com/fs/app/mq/RocketMQConsumerServiceByQw.java
  5. 4 2
      fs-ad-api/src/main/resources/application.yml
  6. 2 2
      fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java
  7. 45 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  8. 2 2
      fs-company/src/main/resources/application.yml
  9. 161 0
      fs-qw-api-msg/pom.xml
  10. 14 0
      fs-qw-api-msg/src/main/java/com/fs/FSServletInitializer.java
  11. 25 0
      fs-qw-api-msg/src/main/java/com/fs/FsQwMsgAppApplication.java
  12. 22 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/CommonController.java
  13. 273 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  14. 103 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/imgTest.java
  15. 46 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/imgToken.java
  16. 34 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/test.java
  17. 126 0
      fs-qw-api-msg/src/main/java/com/fs/app/controller/testTask.java
  18. 51 0
      fs-qw-api-msg/src/main/java/com/fs/app/exception/FSException.java
  19. 133 0
      fs-qw-api-msg/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  20. 59 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/AesException.java
  21. 26 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/ByteGroup.java
  22. 74 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/KeyLoader.java
  23. 44 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/MediaData.java
  24. 67 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/PKCS7Encoder.java
  25. 62 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/SHA1.java
  26. 289 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/WXBizMsgCrypt.java
  27. 104 0
      fs-qw-api-msg/src/main/java/com/fs/app/util/XMLParse.java
  28. 171 0
      fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  29. 73 0
      fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  30. 219 0
      fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/LogAspect.java
  31. 117 0
      fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  32. 31 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/ApplicationConfig.java
  33. 58 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java
  34. 85 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/CaptchaConfig.java
  35. 92 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java
  36. 72 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  37. 59 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/FilterConfig.java
  38. 76 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  39. 149 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/MyBatisConfig.java
  40. 121 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/RedisConfig.java
  41. 65 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/ResourcesConfig.java
  42. 157 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  43. 33 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/ServerConfig.java
  44. 120 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/SwaggerConfig.java
  45. 63 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  46. 77 0
      fs-qw-api-msg/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  47. 27 0
      fs-qw-api-msg/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  48. 45 0
      fs-qw-api-msg/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  49. 115 0
      fs-qw-api-msg/src/main/java/com/fs/framework/exception/GlobalExceptionHandler.java
  50. 56 0
      fs-qw-api-msg/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  51. 126 0
      fs-qw-api-msg/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  52. 56 0
      fs-qw-api-msg/src/main/java/com/fs/framework/manager/AsyncManager.java
  53. 40 0
      fs-qw-api-msg/src/main/java/com/fs/framework/manager/ShutdownManager.java
  54. 106 0
      fs-qw-api-msg/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  55. 69 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/LoginBody.java
  56. 255 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/LoginUser.java
  57. 89 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/SecurityUtils.java
  58. 47 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/filter/JwtAuthenticationTokenFilter.java
  59. 35 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java
  60. 54 0
      fs-qw-api-msg/src/main/java/com/fs/framework/security/handle/LogoutSuccessHandlerImpl.java
  61. 92 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/CompanyLoginService.java
  62. 66 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/CompanyPermissionService.java
  63. 170 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/PermissionService.java
  64. 236 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/TokenService.java
  65. 75 0
      fs-qw-api-msg/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java
  66. 128 0
      fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java
  67. 1 0
      fs-qw-api-msg/src/main/resources/META-INF/spring-devtools.properties
  68. 0 152
      fs-qw-api-msg/src/main/resources/application-druid.yml
  69. 13 0
      fs-qw-api-msg/src/main/resources/application.yml
  70. 2 0
      fs-qw-api-msg/src/main/resources/banner.txt
  71. 37 0
      fs-qw-api-msg/src/main/resources/i18n/messages.properties
  72. 0 0
      fs-qw-api-msg/src/main/resources/jniLibs/WeWorkFinanceSdk.dll
  73. 0 0
      fs-qw-api-msg/src/main/resources/jniLibs/libcrypto-3-x64.dll
  74. 0 0
      fs-qw-api-msg/src/main/resources/jniLibs/libcurl-x64.dll
  75. 93 0
      fs-qw-api-msg/src/main/resources/logback.xml
  76. 19 0
      fs-qw-api-msg/src/main/resources/mybatis/mybatis-config.xml
  77. 28 0
      fs-qw-api-msg/src/main/resources/privatekey.pem
  78. 4 2
      fs-qw-api/src/main/resources/application.yml
  79. 1 1
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  80. 58 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseAnswerReward.java
  81. 25 10
      fs-service/src/main/java/com/fs/course/dto/FsCourseQuestionBankImportDTO.java
  82. 1 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseQuestionBankMapper.java
  83. 16 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCategoryMapper.java
  84. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java
  85. 324 84
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  86. 24 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java
  87. 21 9
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  88. 22 0
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  89. 21 0
      fs-service/src/main/java/com/fs/fastGpt/service/AiNewService.java
  90. 1455 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  91. 1205 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java
  92. 48 0
      fs-service/src/main/java/com/fs/fastgptApi/result/AiImgResult.java
  93. 102 0
      fs-service/src/main/java/com/fs/fastgptApi/util/AiImgUtil.java
  94. 7 0
      fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java
  95. 15 0
      fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java
  96. 3 0
      fs-service/src/main/java/com/fs/his/service/IFsIntegralGoodsService.java
  97. 94 0
      fs-service/src/main/java/com/fs/his/service/impl/FsIntegralGoodsServiceImpl.java
  98. 50 0
      fs-service/src/main/java/com/fs/qw/domain/QwIpadServer.java
  99. 46 0
      fs-service/src/main/java/com/fs/qw/domain/QwIpadServerLog.java
  100. 38 0
      fs-service/src/main/java/com/fs/qw/domain/QwIpadServerUser.java

+ 3 - 0
fs-ad-api/src/main/java/com/fs/FsAdApiApplication.java

@@ -1,8 +1,10 @@
 package com.fs;
 
+import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.Import;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
@@ -11,6 +13,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
  * 启动程序
  */
 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@Import({ RocketMQAutoConfiguration.class })
 @EnableTransactionManagement
 @EnableAsync
 @EnableScheduling

+ 0 - 31
fs-ad-api/src/main/java/com/fs/app/mq/RocketMQAiMsgService.java

@@ -1,31 +0,0 @@
-//package com.fs.app.mq;
-//
-//import cn.hutool.json.JSONUtil;
-//import com.alibaba.fastjson.JSON;
-//import com.fs.ad.service.IAdHtmlClickLogService;
-//import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
-//import com.fs.fastGpt.service.IFastGptChatSessionService;
-//import com.fs.qw.vo.AdUploadVo;
-//import com.fs.qwHookApi.vo.QwHookMsgVO;
-//import lombok.AllArgsConstructor;
-//import lombok.extern.slf4j.Slf4j;
-//import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
-//import org.apache.rocketmq.spring.core.RocketMQListener;
-//import org.springframework.beans.factory.annotation.Autowired;
-//import org.springframework.stereotype.Service;
-//
-//@Slf4j
-//@Service
-//@AllArgsConstructor
-//@RocketMQMessageListener(topic = "msg", consumerGroup = "msg-group")
-//public class RocketMQAiMsgService implements RocketMQListener<String> {
-//
-//    @Autowired
-//    private IFastGptChatSessionService fastGptChatSessionService;
-//    @Override
-//    public void onMessage(String message) {
-//        log.info("消息队列接收到消息:{}",  message);
-//        QwHookMsgVO msgVo= JSONUtil.toBean(message,QwHookMsgVO.class);
-//        fastGptChatSessionService.qwHookNotifyAddMsg(msgVo);
-//    }
-//}

+ 1 - 1
fs-ad-api/src/main/java/com/fs/app/mq/RocketMQConsumerService.java

@@ -14,7 +14,7 @@ import org.springframework.stereotype.Service;
 @Slf4j
 @Service
 @AllArgsConstructor
-//@RocketMQMessageListener(topic = "${rocketmq.consumer.topic}", consumerGroup = "${rocketmq.consumer.group}")
+@RocketMQMessageListener(topic = "${rocketmq.consumer.topic}", consumerGroup = "${rocketmq.consumer.group}")
 public class RocketMQConsumerService implements RocketMQListener<String> {
 
     private final IAdHtmlClickLogService  adHtmlClickLogService;

+ 0 - 26
fs-ad-api/src/main/java/com/fs/app/mq/RocketMQConsumerServiceByQw.java

@@ -1,26 +0,0 @@
-//package com.fs.app.mq;
-//
-//import com.alibaba.fastjson.JSON;
-//import com.fs.ad.service.IAdHtmlClickLogService;
-//import com.fs.qw.vo.AdUploadVo;
-//import lombok.AllArgsConstructor;
-//import lombok.extern.slf4j.Slf4j;
-//import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
-//import org.apache.rocketmq.spring.core.RocketMQListener;
-//import org.springframework.stereotype.Service;
-//
-//@Slf4j
-//@Service
-//@AllArgsConstructor
-////@RocketMQMessageListener(topic = "ad-qw-external-contact", consumerGroup = "ad-group")
-//@RocketMQMessageListener(topic = "ad-qw-external-contact", consumerGroup = "test-group")
-//public class RocketMQConsumerServiceByQw implements RocketMQListener<String> {
-//
-//
-//    @Override
-//    public void onMessage(String message) {
-//
-//        log.info("消息队列接收到消息qw:{}",  message);
-//
-//    }
-//}

+ 4 - 2
fs-ad-api/src/main/resources/application.yml

@@ -4,5 +4,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
-    include: common,config-dev
+    #    active: dev
+    #    include: common,config-dev
+    active: druid-hdt
+    include: common,config-druid-hdt,mq-hdt

+ 2 - 2
fs-admin/src/main/java/com/fs/course/controller/FsCourseQuestionBankController.java

@@ -55,8 +55,8 @@ public class FsCourseQuestionBankController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(FsCourseQuestionBank fsCourseQuestionBank)
     {
-        List<FsCourseQuestionBank> list = fsCourseQuestionBankService.selectFsCourseQuestionBankList(fsCourseQuestionBank);
-        ExcelUtil<FsCourseQuestionBank> util = new ExcelUtil<FsCourseQuestionBank>(FsCourseQuestionBank.class);
+        List<FsCourseQuestionBankImportDTO> list = fsCourseQuestionBankService.exportData(fsCourseQuestionBank);
+        ExcelUtil<FsCourseQuestionBankImportDTO> util = new ExcelUtil<>(FsCourseQuestionBankImportDTO.class);
         return util.exportExcel(list, "题库数据");
     }
 

+ 45 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java

@@ -111,7 +111,52 @@ public class QwUserController extends BaseController
         return getDataTable(list);
     }
 
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwIpad")
+    public R loginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.loginQwIpad(loginParam);
+    }
+
+
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getQwIpad")
+    @RepeatSubmit
+    public R getQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/delQwIpad")
+    @RepeatSubmit
+    public R delQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.delQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/qrCodeStatus")
+    public R qrCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeStatus(loginParam);
+    }
+    //输入验证码
+    @PostMapping("/qrCodeVerify")
+    public R qrCodeVerify(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeVerify(loginParam);
+    }
+
+    @PostMapping("/outLoginQwIpad")
+    public R outLoginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.outLoginQwIpad(loginParam);
+    }
 
+    @PostMapping("/twoCode")
+    public R twoCode(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCode(loginParam);
+    }
+    @PostMapping("/twoCodeStatus")
+    public R TwoCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCodeStatus(loginParam);
+    }
     /**
     * 直接授权key
     */

+ 2 - 2
fs-company/src/main/resources/application.yml

@@ -3,8 +3,8 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-myhk
-    include: common,config-myhk
+    active: dev
+    include: common,config-dev
 #    active: druid-jzzx
 #    include: common,config-druid-jzzx
 #    active: druid-hdt

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

@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <version>1.1.0</version>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.fs</groupId>
+    <artifactId>fs-qw-api-msg</artifactId>
+    <description>
+        公司接口
+    </description>
+
+    <dependencies>
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!--clickhouse-->
+        <dependency>
+            <groupId>com.clickhouse</groupId>
+            <artifactId>clickhouse-jdbc</artifactId>
+            <version>0.4.6</version>
+        </dependency>
+
+        <!--        <dependency>-->
+<!--            <groupId>ru.yandex.clickhouse</groupId>-->
+<!--            <artifactId>clickhouse-jdbc</artifactId>-->
+<!--            <version>0.3.2</version>-->
+<!--        </dependency>-->
+
+        <!-- 验证码 -->
+        <dependency>
+            <groupId>com.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 获取系统信息 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+        <!-- 获取音频信息 -->
+        <dependency>
+            <groupId>org</groupId>
+            <artifactId>jaudiotagger</artifactId>
+            <version>2.0.3</version>
+        </dependency>
+
+        <!-- 语音识别 -->
+        <dependency>
+            <groupId>net.java.dev.jna</groupId>
+            <artifactId>jna</artifactId>
+            <version>5.7.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alphacephei</groupId>
+            <artifactId>vosk</artifactId>
+            <version>0.3.32</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-qw-api</artifactId>
+            <version>1.1.0</version>
+            <scope>compile</scope>
+        </dependency>
+
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+
+</project>

+ 14 - 0
fs-qw-api-msg/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsQwMsgAppApplication.class);
+    }
+}

+ 25 - 0
fs-qw-api-msg/src/main/java/com/fs/FsQwMsgAppApplication.java

@@ -0,0 +1,25 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableTransactionManagement
+@EnableAsync
+@EnableScheduling
+public class FsQwMsgAppApplication
+{
+    public static void main(String[] args)
+    {
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(FsQwMsgAppApplication.class, args);
+        System.out.println("QwAPI启动成功");
+    }
+}

+ 22 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,22 @@
+package com.fs.app.controller;
+
+import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+import io.swagger.annotations.Api;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.List;
+
+
+@Api("公共接口")
+@RestController
+@RequestMapping(value="/app/common")
+public class CommonController {
+
+
+
+}

+ 273 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -0,0 +1,273 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.uuid.IdUtils;
+import com.fs.fastGpt.service.AiHookService;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.QwUserVideo;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwUserVoiceLogService;
+import com.fs.qw.vo.QwMessageListVO;
+import com.fs.wxwork.dto.*;
+import com.fs.wxwork.service.WxWorkService;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONObject;
+import org.omg.CORBA.LongHolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+@Api("企微消息")
+@RestController
+@RequestMapping(value="/msg")
+@Slf4j
+public class QwMsgController {
+    @Autowired
+     RedisCache redisCache;
+    @Autowired
+    AiHookService aiHookService;
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    WxWorkService wxWorkService;
+    @Autowired
+    IQwUserVoiceLogService qwUserVoiceLogService;
+
+    @PostMapping("/callback/{serverId}")
+    @ResponseBody
+    public Map<String,String> callback(@RequestBody String json,@PathVariable Long serverId ){
+      //  System.out.println(json);
+        WxWorkMsgResp wxWorkMsgResp = JSON.parseObject(json, WxWorkMsgResp.class);
+        Integer type = wxWorkMsgResp.getType();
+        HashMap<String, String> map = new HashMap<>();
+        map.put("errorcode","0");
+        map.put("errmsg","ok");
+        Long id = redisCache.getCacheObject("qrCode:uuid:"+wxWorkMsgResp.getUuid());
+        if (id==null){
+            return map;
+        }
+        switch (type){
+            case 100001:
+                System.out.println("扫码返回");
+                break;
+            case 100002:
+                System.out.println("取消扫码");
+              //  redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),100002,10, TimeUnit.MINUTES);
+                break;
+            case 100003:
+                System.out.println("确认扫码返回");
+
+                break;
+            case 104001:
+                System.out.println("登录成功");
+
+                JSONObject jsonObject = new JSONObject(wxWorkMsgResp.getJson());
+                QwUser qu = qwUserMapper.selectQwUserById(id);
+                Long corpId = jsonObject.getLong("Corpid");
+                System.out.println("回调中的"+corpId);
+                String sCorpId = wxWorkService.getCorpId(wxWorkMsgResp.getUuid(), corpId,serverId);
+                if (sCorpId==null&& sCorpId.isEmpty()){
+                    break;
+                }
+                if (!qu.getCorpId().equals(sCorpId)){
+                    redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),22,10, TimeUnit.MINUTES);
+                    System.out.println("公司不匹配不给登录");
+                    WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
+                    wxWorkGetQrCodeDTO.setUuid(wxWorkMsgResp.getUuid());
+                    wxWorkService.LoginOut(wxWorkGetQrCodeDTO,serverId);
+                    System.out.println("调用退出登录");
+                    break;
+                }
+                if (!qu.getQwUserId().equals(jsonObject.get("acctid"))){
+                    redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),23,10, TimeUnit.MINUTES);
+                    System.out.println("账号不匹配不给登录");
+                    WxWorkGetQrCodeDTO wxWorkGetQrCodeDTO = new WxWorkGetQrCodeDTO();
+                    wxWorkGetQrCodeDTO.setUuid(wxWorkMsgResp.getUuid());
+                    wxWorkService.LoginOut(wxWorkGetQrCodeDTO,serverId);
+                    System.out.println("调用退出登录");
+                    break;
+                }
+                QwUser qwUser = new QwUser();
+                qwUser.setId(id);
+                qwUser.setVid(jsonObject.get("Vid").toString());
+                qwUser.setIpadStatus(1);
+                qwUserMapper.updateQwUser(qwUser);
+                System.out.println("存Vid");
+                redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),104001,10, TimeUnit.MINUTES);
+                break;
+            case 100006:
+                System.out.println("企业切换");
+                break;
+            case 100004:
+                System.out.println("需要验证二维码消息");
+                redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),100004,10, TimeUnit.MINUTES);
+                break;
+            case 100012:
+                System.out.println("需要二次验证");
+                redisCache.setCacheObject("qrCodeUid:"+wxWorkMsgResp.getUuid(),100012,10, TimeUnit.MINUTES);
+
+                break;
+            case 100005:
+                System.out.println("手机端结束登录");
+                log.info("手机端结束登录:"+wxWorkMsgResp.getJson());
+                qwUserStatus(wxWorkMsgResp.getUuid(),0);
+                break;
+            case 100008:
+                System.out.println("当前账号在其他设备登录");
+                log.info("当前账号在其他设备登录:"+wxWorkMsgResp.getJson());
+                qwUserStatus(wxWorkMsgResp.getUuid(),0);
+                break;
+            case 100007:
+                System.out.println("异常断开");
+                log.info("异常断开:"+wxWorkMsgResp.getJson());
+                qwUserStatus(wxWorkMsgResp.getUuid(),0);
+                break;
+            case 100009:
+                System.out.println("二次验证");
+                log.info("二次验证:"+wxWorkMsgResp.getJson());
+                qwUserStatus(wxWorkMsgResp.getUuid(),0);
+                break;
+            case 102000:
+
+                WxWorkMessageDTO wxWorkMessageDTO = JSON.parseObject(wxWorkMsgResp.getJson(), WxWorkMessageDTO.class);
+                if (wxWorkMessageDTO.getIs_room()!=0){
+                    break;
+                }
+                if (wxWorkMessageDTO.getReferid()!=0){
+                    break;
+                }
+                if (wxWorkMessageDTO.getMsgtype()==2||wxWorkMessageDTO.getMsgtype()==0||wxWorkMessageDTO.getMsgtype()==16||wxWorkMessageDTO.getMsgtype() == 101||wxWorkMessageDTO.getMsgtype() == 104){
+
+                    String content = wxWorkMessageDTO.getContent();
+                    System.out.println("接收人:"+wxWorkMessageDTO.getReceiver());
+                    System.out.println("发送人:"+wxWorkMessageDTO.getSender());
+                    System.out.println("内容:"+content);
+                    Long receiver = wxWorkMessageDTO.getReceiver();
+                    Long sender = wxWorkMessageDTO.getSender();
+                    if(wxWorkMessageDTO.getMsgtype()==16){
+                        WxwSpeechToTextEntityDTO ste = new WxwSpeechToTextEntityDTO();
+                        ste.setMsgid(wxWorkMessageDTO.getMsg_id());
+                        ste.setUuid(wxWorkMsgResp.getUuid());
+                        WxWorkResponseDTO<WxwSpeechToTextEntityRespDTO> dto = wxWorkService.SpeechToTextEntity(ste, serverId);
+                        System.out.println(dto);
+                        WxwSpeechToTextEntityRespDTO data = dto.getData();
+                        content = data.getText();
+                        System.out.println("语音消息"+content);
+                    }
+                    else if (wxWorkMessageDTO.getMsgtype() == 101){
+                        content = processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp);
+                        System.out.println("用户发送图片"+content);
+                    }
+                    // gif 表情消息
+                    else if (wxWorkMessageDTO.getMsgtype() == 104){
+                        content = wxWorkMessageDTO.getUrl();
+                        System.out.println("用户发送表情"+content);
+                    }
+
+                    if (2000000000000000L-receiver>0){
+                        System.out.println("客户发送");
+                        aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype());
+                    }else {
+                        System.out.println("销售发送");
+                        aiHookService.qwHookNotifyAddMsg(id,receiver,content,wxWorkMsgResp.getUuid());
+                    }
+
+                }
+                //语音通话
+                if (wxWorkMessageDTO.getMsgtype()==40){
+                    if (wxWorkMessageDTO.getRecordtype()==null){
+                        break;
+                    }
+                    Long receiver = wxWorkMessageDTO.getReceiver();
+                    Long extId=null;
+                    Integer totalSeconds=0;
+                    if (2000000000000000L-receiver>0){
+                         extId = wxWorkMessageDTO.getSender();
+                        System.out.println("客户发起");
+                    }else {
+                        System.out.println("销售发起");
+                        extId = wxWorkMessageDTO.getReceiver();
+                    }
+                    Integer recordType = wxWorkMessageDTO.getRecordtype();
+                    if (recordType==5){
+                        String recordwording = wxWorkMessageDTO.getRecordwording();
+                        //String recordwording = "通话时长01:20:07";
+                        if (recordwording != null && !recordwording.isEmpty()){
+                            // 同时匹配 "HH:mm:ss" 和 "mm:ss" 格式
+                            Pattern pattern = Pattern.compile("(\\d+):(\\d+)(?::(\\d+))?");
+                            Matcher matcher = pattern.matcher(recordwording);
+                            if (matcher.find()) {
+                                int hours = 0;
+                                int minutes;
+                                int seconds;
+
+                                // 如果有小时部分(匹配到3个组)
+                                if (matcher.group(3) != null) {
+                                    hours = Integer.parseInt(matcher.group(1));
+                                    minutes = Integer.parseInt(matcher.group(2));
+                                    seconds = Integer.parseInt(matcher.group(3));
+                                }
+                                else {// 只有分钟和秒(匹配到2个组)
+                                    minutes = Integer.parseInt(matcher.group(1));
+                                    seconds = Integer.parseInt(matcher.group(2));
+                                }
+                                totalSeconds = hours * 3600 + minutes * 60 + seconds;
+                                System.out.println("总通话秒数: " + totalSeconds);
+                            }
+                        }
+                    } else if (recordType==2){
+                        System.out.println("通话挂断");
+                    }else {
+                        break;
+                    }
+
+                    qwUserVoiceLogService.addQuUserVoiceByIpadCallback(id,extId,recordType,totalSeconds,wxWorkMsgResp.getUuid());
+                }
+
+                break;
+
+        }
+
+
+
+
+        return map;
+    }
+
+    /**
+     * 处理图片消息
+     * @param serverId          服务器ID
+     * @param wxWorkMessageDTO  消息DTO
+     * @param wxWorkMsgResp     回调信息对象
+     */
+    private String processImageMessage(Long serverId, WxWorkMessageDTO wxWorkMessageDTO, WxWorkMsgResp wxWorkMsgResp) {
+        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 null;
+        }
+        return fileUrlResp.getData();
+    }
+
+
+
+
+    void qwUserStatus(String uid,Integer status){
+        Long id = redisCache.getCacheObject("qrCode:uuid:"+uid);
+        QwUser qwUser = new QwUser();
+        qwUser.setId(id);
+        qwUser.setIpadStatus(status);
+        qwUserMapper.updateQwUser(qwUser);
+    }
+
+}

+ 103 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/imgTest.java

@@ -0,0 +1,103 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.app.config.ConfigProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+public class imgTest {
+
+    private static final String ARK_API = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
+    private static final String IMAGE_URL = "https://cos.his.cdwjyyh.com/fs/20250526/a17565cb7ba74aefb27e27ac91f802f2.jpg";
+    public  void main(String[] args) {
+        try {
+
+            String apiKey = "208d3549-8dc9-4ef6-b3fa-5aa358f1ab20";
+            String requestBody = String.format(
+                    "{" +
+                            "\"model\": \"doubao-vision-lite-32k-241015\"," +
+                            "\"messages\": [{" +
+                            "\"role\": \"user\"," +
+                            "\"content\": [" +
+                            "{" +
+                            "\"type\": \"image_url\"," +
+                            "\"image_url\": {\"url\": \""+IMAGE_URL+"\"}" +
+                            "}," +
+                            "{" +
+                            "\"type\": \"text\"," +
+                            "\"text\": \"解析一下这张图片\"" +
+                            "}" +
+                            "]" +
+                            "}]" +
+                            "}"
+            );
+
+            // 发送请求
+            String response = sendHttpRequest(apiKey, requestBody);
+            System.out.println("API响应: " + response);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+
+
+    private static String sendHttpRequest(String apiKey, String requestBody) throws IOException {
+        HttpURLConnection connection = null;
+        try {
+            // 配置连接
+            URL url = new URL(ARK_API);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Authorization", "Bearer " + apiKey);
+            connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            connection.setDoOutput(true);
+            connection.setConnectTimeout(10000); // 10秒连接超时
+            connection.setReadTimeout(30000);   // 30秒读取超时
+
+            // 发送请求体
+            try (OutputStream os = connection.getOutputStream()) {
+                os.write(requestBody.getBytes(StandardCharsets.UTF_8));
+            }
+
+            // 处理响应
+            int status = connection.getResponseCode();
+            if (status == HttpURLConnection.HTTP_OK) {
+                try (BufferedReader br = new BufferedReader(
+                        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
+                    StringBuilder response = new StringBuilder();
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        response.append(line);
+                    }
+                    return response.toString();
+                }
+            } else {
+                // 读取错误流
+                try (BufferedReader br = new BufferedReader(
+                        new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
+                    StringBuilder errorResponse = new StringBuilder();
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        errorResponse.append(line);
+                    }
+                    throw new IOException("HTTP错误 " + status + ": " + errorResponse.toString());
+                }
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+}

+ 46 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/imgToken.java

@@ -0,0 +1,46 @@
+package com.fs.app.controller;
+
+
+
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class imgToken {
+    private static final int TOKEN_DIVISOR = 784;
+    private static final int TOKEN_LIMIT = 1312;
+
+    public static void main(String[] args) {
+        // 替换为你自己的图片 URL
+        String imageUrl = "https://cos.his.cdwjyyh.com/fs/20250526/a17565cb7ba74aefb27e27ac91f802f2.jpg";
+
+        try {
+            BufferedImage image = ImageIO.read(new URL(imageUrl));
+            if (image == null) {
+                System.out.println("无法加载图片,请检查 URL 是否正确");
+                return;
+            }
+
+            int width = image.getWidth();
+            int height = image.getHeight();
+
+            int tokenEstimate = (int) Math.ceil((width * height) / (double) TOKEN_DIVISOR);
+
+            System.out.println("图片尺寸: " + width + " x " + height);
+            System.out.println("估算消耗 Token: " + tokenEstimate);
+            System.out.println("Token 上限: " + TOKEN_LIMIT);
+            if (tokenEstimate > TOKEN_LIMIT) {
+                System.out.println("⚠️ 超出 token 上限!");
+            } else {
+                System.out.println("✅ 在 token 限制范围内");
+            }
+
+        } catch (IOException e) {
+            System.out.println("图片加载失败:" + e.getMessage());
+        }
+    }
+}

+ 34 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/test.java

@@ -0,0 +1,34 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Excel;
+import com.fs.fastGpt.domain.FastGptChatSession;
+import com.fs.fastgptApi.util.AiImgUtil;
+import com.fs.qw.domain.QwExternalContactInfo;
+import com.fs.wxwork.dto.WxwSilkVoceDTO;
+import com.fs.wxwork.utils.WxWorkHttpUtil;
+
+import javax.validation.constraints.Null;
+import java.lang.reflect.Field;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class test {
+    public static void main(String[] args) {
+        AiImgUtil aiImgUtil = new AiImgUtil();
+        String imageParse = aiImgUtil.getImageParse("https://wework.qpic.cn/wwpic3az/wwwx_3cf5c3be937316fcad124d8fb1b1f1b2/0");
+        System.out.println(imageParse);
+    }
+
+
+
+
+    void voice(){
+        String  url="http://162.14.193.126:8009/app/common/voice?voice="+"你好"+"&id="+2020;
+        String json = WxWorkHttpUtil.get(url);
+        System.out.println(json);
+        WxwSilkVoceDTO wxwSilkVoceDTO = JSON.parseObject(json, WxwSilkVoceDTO.class);
+    }
+}

+ 126 - 0
fs-qw-api-msg/src/main/java/com/fs/app/controller/testTask.java

@@ -0,0 +1,126 @@
+//package com.fs.app.controller;
+//
+//import com.alibaba.fastjson.JSON;
+//import com.alibaba.fastjson.JSONObject;
+//import com.fs.app.service.QwDataCallbackService;
+//import com.fs.common.core.domain.R;
+//import com.fs.fastGpt.domain.FastGptChatSession;
+//import com.fs.fastGpt.service.AiNewService;
+//import com.fs.qw.domain.QwExternalContact;
+//import com.fs.qw.domain.QwMessageGather;
+//import com.fs.qw.domain.QwUser;
+//import com.fs.qw.domain.QwUserVoiceLog;
+//import com.fs.qw.mapper.QwExternalContactMapper;
+//import com.fs.qw.mapper.QwUserMapper;
+//import com.fs.qw.mapper.QwUserVoiceLogMapper;
+//import com.fs.qw.service.IQwUserService;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.scheduling.annotation.Scheduled;
+//import org.springframework.stereotype.Service;
+//
+//import java.util.Date;
+//import java.util.List;
+//import java.util.UUID;
+//
+//@Service
+//public class testTask {
+//
+//    @Autowired
+//    QwDataCallbackService qwDataCallbackService;
+//    @Autowired
+//    private AiNewService aiService;
+//
+//    @Autowired
+//    private QwUserMapper qwUserMapper;
+//
+//    @Autowired
+//    private QwUserVoiceLogMapper qwUserVoiceLogMapper;
+//
+//    @Autowired
+//    private QwExternalContactMapper qwExternalContactMapper;
+//
+//    //@Scheduled(fixedRate = 5000) // 每10执行一次
+//    public void getMsg(){
+//
+//        String corpId="ww5a88c4f879f204c5";
+//        // 拉取会话存档内容
+//        List<String> chatContents = qwDataCallbackService.getChatData(corpId);
+//        for (String chatContent : chatContents) {
+//            QwMessageGather qwMessageGather = JSON.parseObject(chatContent, QwMessageGather.class);
+//            if(qwMessageGather.getMsgtype().equals("voiptext")){//音视频通话获取通话时长
+//                if (extracted(qwMessageGather, corpId)) {
+//                    continue;
+//                }
+//            }
+//            if (!qwMessageGather.getAction().equals("send")){
+//                System.out.println("非send");
+//                continue;
+//            }
+//            if (!qwMessageGather.getMsgtype().equals("text")){
+//                System.out.println("非text");
+//                continue;
+//            }
+//            if (qwMessageGather.getRoomid()!=null&& !qwMessageGather.getRoomid().isEmpty()){
+//                System.out.println("群聊消息");
+//                continue;
+//            }
+//
+//            String from = qwMessageGather.getFrom();
+//            if (from.startsWith("wo")||from.startsWith("wm")){
+//                System.out.println("用户发消息");
+//                aiService.qwHookNotifyAiReply(qwMessageGather,corpId);
+//            }else {
+//                aiService.qwHookNotifyAddMsg(qwMessageGather,corpId);
+//                System.out.println("客服发送消息");
+//            }
+//            QwMessageGather.TextContent text = qwMessageGather.getText();
+//            System.out.println(text.getContent());
+//        }
+//    }
+//
+//    /**
+//     * 读取会话内容中的音视频通话时长
+//     * @param qwMessageGather
+//     * @param corpId
+//     * @return
+//     */
+//    private boolean extracted(QwMessageGather qwMessageGather, String corpId) {
+//        String info1 = qwMessageGather.getInfo();
+//        JSONObject infoJson = JSONObject.parseObject(info1);
+//        Long callDuration = infoJson.getLong("callduration");//音视频通话持续时间
+//        QwUser qwUser = qwUserMapper.selectQwUserByQwUseridAndCorpId(qwMessageGather.getFrom(), corpId);
+//
+//        if (qwUser != null){
+//            List<String> tolist = qwMessageGather.getTolist();
+//            if (qwMessageGather.getTolist().isEmpty()){
+//                return true;
+//            }
+//            String ExtId = tolist.get(0);
+//            QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(ExtId, corpId,qwUser.getQwUserId());
+//            if (qwExternalContacts==null){
+//                return true;
+//            }
+//
+//            QwUserVoiceLog userVoiceLog = new QwUserVoiceLog();
+//            userVoiceLog.setQwUserId(qwUser.getId());
+//            userVoiceLog.setExtId(qwExternalContacts.getId());
+//            userVoiceLog.setDuration(callDuration);
+//            userVoiceLog.setCorpId(corpId);
+//            userVoiceLog.setCompanyId(qwUser.getCompanyId());
+//            userVoiceLog.setCompanyUserId(qwUser.getCompanyUserId());
+//            userVoiceLog.setCreateTime(new Date());
+//            userVoiceLog.setTitle("通话已接听");
+//            userVoiceLog.setStatus(1L);
+//            qwUserVoiceLogMapper.insertQwUserVoiceLog(userVoiceLog);
+//        }
+//        return false;
+//    }
+//
+//
+//    //@Scheduled(cron = "0 0/30 * * * ?")// 每10执行一次
+//    public void expire(){
+//
+//        aiService.expireAiMsg();
+//    }
+//
+//}

+ 51 - 0
fs-qw-api-msg/src/main/java/com/fs/app/exception/FSException.java

@@ -0,0 +1,51 @@
+package com.fs.app.exception;
+
+/**
+ * 自定义异常
+ */
+public class FSException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
+	
+    private String msg;
+    private int code = 500;
+    
+    public FSException(String msg) {
+		super(msg);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+	}
+	
+	public FSException(String msg, int code) {
+		super(msg);
+		this.msg = msg;
+		this.code = code;
+	}
+	
+	public FSException(String msg, int code, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+		this.code = code;
+	}
+
+	public String getMsg() {
+		return msg;
+	}
+
+	public void setMsg(String msg) {
+		this.msg = msg;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public void setCode(int code) {
+		this.code = code;
+	}
+	
+	
+}

+ 133 - 0
fs-qw-api-msg/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,133 @@
+package com.fs.app.exception;
+
+
+
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.util.stream.Collectors;
+
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class FSExceptionHandler {
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	/**
+	 * 处理自定义异常
+	 */
+	@ExceptionHandler(FSException.class)
+	public R handleRRException(FSException e){
+		R r = new R();
+		r.put("code", e.getCode());
+		r.put("msg", e.getMessage());
+
+		return r;
+	}
+
+	@ExceptionHandler(NoHandlerFoundException.class)
+	public R handlerNoFoundException(Exception e) {
+		logger.error(e.getMessage(), e);
+		logger.error("handlerNoFoundException");
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		logger.error("handleDuplicateKeyException");
+		return R.error("数据库中已存在该记录");
+	}
+
+	private String getRequestData(HttpServletRequest request) throws IOException {
+		try (BufferedReader reader = request.getReader()) {
+			return reader.lines().collect(Collectors.joining(System.lineSeparator()));
+		}
+	}
+	@ExceptionHandler(IOException.class)
+	public R handleIOException(Exception e, HttpServletRequest webRequest){
+		logger.error("handleIOException");
+		logger.error("请求数据: " + webRequest);
+		logger.error("链接中断路径: " + webRequest.getRequestURI()+"请求数据: ");
+		HttpServletRequest request = ServletUtils.getRequest();
+		String requestData = "";
+		try {
+			// 尝试获取请求数据
+			requestData = getRequestData(request);
+		} catch (IOException ex) {
+			// 若获取请求数据时再次出错,记录日志
+			logger.error("获取请求数据时发生错误: " + ex.getMessage());
+
+		}
+		// 记录原始异常信息\
+		logger.error("发生 IOException: " + e.getMessage());
+
+		// 打印请求数据
+		logger.error("请求数据: " + requestData);
+		logger.error("请求数据2: " + webRequest);
+
+		try{
+			String requestPath = request.getRequestURI();
+			// 打印或记录请求路径
+
+			logger.error("链接中断路径: " + requestPath+"请求数据: " + requestData);
+			logger.error("链接中断路径2: " + webRequest.getRequestURI()+"请求数据: " + webRequest);
+			//todo  判断如果是进线,则重发一下欢迎语
+		}
+		catch (Exception e1){
+			logger.error("获取url错误: ");
+			HttpServletRequest request2 = ServletUtils.getRequest();
+			logger.error("获取url错误路径: " + request2.getRequestURI());
+		}
+		return R.error();
+	}
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error("handleException");
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error("handleAccessDeniedException");
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		logger.error("bindExceptionHandler");
+		FieldError error = e.getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public R exceptionHandler(MethodArgumentNotValidException e) {
+		FieldError error = e.getBindingResult().getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+	@ExceptionHandler(CustomException.class)
+	public R handleException(CustomException e){
+		logger.error("handleException");
+		return R.error(e.getMessage());
+	}
+}

+ 59 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/AesException.java

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

+ 26 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/ByteGroup.java

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

+ 74 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/KeyLoader.java

@@ -0,0 +1,74 @@
+package com.fs.app.util;
+
+import org.springframework.core.io.ClassPathResource;
+import javax.crypto.Cipher;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+
+public class KeyLoader {
+
+    private final String privateKeyPath;
+
+    public KeyLoader(String privateKeyPath) {
+        this.privateKeyPath = privateKeyPath;
+    }
+
+    // 读取 InputStream 为 byte[]
+    private byte[] readInputStream(InputStream is) throws Exception {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        byte[] data = new byte[1024];
+        int bytesRead;
+        while ((bytesRead = is.read(data, 0, data.length)) != -1) {
+            buffer.write(data, 0, bytesRead);
+        }
+        return buffer.toByteArray();
+    }
+
+    // 加载私钥(支持 ClassPath 和外部路径)
+    public PrivateKey loadPrivateKey() throws Exception {
+        // 从 classpath 加载资源文件
+        ClassPathResource resource = new ClassPathResource(privateKeyPath);
+        try (InputStream is = resource.getInputStream();
+             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                baos.write(buffer, 0, len);
+            }
+            byte[] keyBytes = baos.toByteArray();
+
+            // 解析 PEM 格式
+            String privateKeyPEM = new String(keyBytes)
+                    .replace("-----BEGIN PRIVATE KEY-----", "")
+                    .replace("-----END PRIVATE KEY-----", "")
+                    .replaceAll("\\s", "");
+            byte[] decodedKey = Base64.getDecoder().decode(privateKeyPEM);
+
+            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
+            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+            return keyFactory.generatePrivate(keySpec);
+        }
+    }
+
+    // 解密加密的随机密钥
+    public String decryptRandomKey(String encryptedRandomKey) {
+        try {
+            PrivateKey privateKey = loadPrivateKey(); // 加载私钥
+
+            Cipher cipher = Cipher.getInstance("RSA");
+            cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedRandomKey));
+            return new String(decryptedBytes);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}

+ 44 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/MediaData.java

@@ -0,0 +1,44 @@
+package com.fs.app.util;
+
+public class MediaData {
+    private byte[] data;           // 存储媒体数据
+    private String outIndexBuf;    // 下次拉取的索引信息
+    private boolean isFinish;      // 标记是否拉取完成
+
+    // 构造方法
+    public MediaData(byte[] data, String outIndexBuf, boolean isFinish) {
+        this.data = data;
+        this.outIndexBuf = outIndexBuf;
+        this.isFinish = isFinish;
+    }
+
+    // 获取媒体数据
+    public byte[] getData() {
+        return data;
+    }
+
+    // 设置媒体数据
+    public void setData(byte[] data) {
+        this.data = data;
+    }
+
+    // 获取下次索引信息
+    public String getOutIndexBuf() {
+        return outIndexBuf;
+    }
+
+    // 设置下次索引信息
+    public void setOutIndexBuf(String outIndexBuf) {
+        this.outIndexBuf = outIndexBuf;
+    }
+
+    // 获取拉取是否完成标记
+    public boolean isFinish() {
+        return isFinish;
+    }
+
+    // 设置拉取是否完成标记
+    public void setFinish(boolean finish) {
+        isFinish = finish;
+    }
+}

+ 67 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/PKCS7Encoder.java

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

+ 62 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/SHA1.java

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

+ 289 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/WXBizMsgCrypt.java

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

+ 104 - 0
fs-qw-api-msg/src/main/java/com/fs/app/util/XMLParse.java

@@ -0,0 +1,104 @@
+/**
+ * 对企业微信发送给企业后台的消息加解密示例代码.
+ * 
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.fs.app.util;
+
+import java.io.StringReader;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+/**
+ * XMLParse class
+ *
+ * 提供提取消息格式中的密文及生成回复消息格式的接口.
+ */
+class XMLParse {
+
+	/**
+	 * 提取出xml数据包中的加密消息
+	 * @param xmltext 待提取的xml字符串
+	 * @return 提取出的加密消息字符串
+	 * @throws com.qq.weixin.mp.aes.AesException
+	 */
+	public static Object[] extract(String xmltext) throws AesException {
+		Object[] result = new Object[3];
+		try {
+			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+			
+			String FEATURE = null;
+			// This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
+			// Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
+			FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
+			dbf.setFeature(FEATURE, true);
+			
+			// If you can't completely disable DTDs, then at least do the following:
+			// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
+			// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
+			// JDK7+ - http://xml.org/sax/features/external-general-entities 
+			FEATURE = "http://xml.org/sax/features/external-general-entities";
+			dbf.setFeature(FEATURE, false);
+			
+			// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
+			// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
+			// JDK7+ - http://xml.org/sax/features/external-parameter-entities 
+			FEATURE = "http://xml.org/sax/features/external-parameter-entities";
+			dbf.setFeature(FEATURE, false);
+			
+			// Disable external DTDs as well
+			FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+			dbf.setFeature(FEATURE, false);
+			
+			// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
+			dbf.setXIncludeAware(false);
+			dbf.setExpandEntityReferences(false);
+			
+			// And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then 
+			// ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
+			// (http://cwe.mitre.org/data/definitions/918.html) and denial 
+			// of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."
+			
+			// remaining parser logic
+			DocumentBuilder db = dbf.newDocumentBuilder();
+			StringReader sr = new StringReader(xmltext);
+			InputSource is = new InputSource(sr);
+			Document document = db.parse(is);
+
+			Element root = document.getDocumentElement();
+			NodeList nodelist1 = root.getElementsByTagName("Encrypt");
+			result[0] = 0;
+			result[1] = nodelist1.item(0).getTextContent();
+			return result;
+		} catch (Exception e) {
+			e.printStackTrace();
+			throw new AesException(AesException.ParseXmlError);
+		}
+	}
+
+	/**
+	 * 生成xml消息
+	 * @param encrypt 加密后的消息密文
+	 * @param signature 安全签名
+	 * @param timestamp 时间戳
+	 * @param nonce 随机字符串
+	 * @return 生成的xml字符串
+	 */
+	public static String generate(String encrypt, String signature, String timestamp, String nonce) {
+
+		String format = "<xml>\n" + "<Encrypt><![CDATA[%1$s]]></Encrypt>\n"
+				+ "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n"
+				+ "<TimeStamp>%3$s</TimeStamp>\n" + "<Nonce><![CDATA[%4$s]]></Nonce>\n" + "</xml>";
+		return String.format(format, encrypt, signature, timestamp, nonce);
+
+	}
+}

+ 171 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,171 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.company.domain.CompanyRole;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+        if (StringUtils.isNotNull(loginUser))
+        {
+            CompanyUser currentUser = loginUser.getUser();
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, CompanyUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (CompanyRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM company_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM company_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    //sqlString.append(" OR 1=0 ");
+                        sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+}

+ 73 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ * 
+ 
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 219 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,219 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.company.domain.CompanyOperLog;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.HandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ * 
+
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.Log)")
+    public void logPointCut()
+    {
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
+    {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     * 
+     * @param joinPoint 切点
+     * @param e 异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
+    {
+        handleLog(joinPoint, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
+    {
+        try
+        {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null)
+            {
+                return;
+            }
+
+            // 获取当前的用户
+            LoginUser loginUser = SpringUtils.getBean(TokenService.class).getLoginUser(ServletUtils.getRequest());
+
+            // *========数据库日志=========*//
+            CompanyOperLog operLog = new CompanyOperLog();
+            operLog.setCompanyId(loginUser.getCompany().getCompanyId());
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(JSON.toJSONString(jsonResult));
+
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (loginUser != null)
+            {
+                operLog.setOperName(loginUser.getUsername());
+            }
+
+            if (e != null)
+            {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog);
+            // 保存数据库
+            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+        }
+        catch (Exception exp)
+        {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     * 
+     * @param log 日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, CompanyOperLog operLog) throws Exception
+    {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData())
+        {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     * 
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, CompanyOperLog operLog) throws Exception
+    {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
+        {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        }
+        else
+        {
+            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray)
+    {
+        String params = "";
+        if (paramsArray != null && paramsArray.length > 0)
+        {
+            for (int i = 0; i < paramsArray.length; i++)
+            {
+                if (!isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     * 
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    public boolean isFilterObject(final Object o)
+    {
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
+    }
+}

+ 117 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.RateLimiter;
+import com.fs.common.enums.LimitType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 限流处理
+ *
+
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    private RedisScript<Long> limitScript;
+
+    @Autowired
+    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+    {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Autowired
+    public void setLimitScript(RedisScript<Long> limitScript)
+    {
+        this.limitScript = limitScript;
+    }
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.RateLimiter)")
+    public void rateLimiterPointCut()
+    {
+    }
+
+    @Before("rateLimiterPointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        RateLimiter rateLimiter = getAnnotationRateLimiter(point);
+        String key = rateLimiter.key();
+        int time = rateLimiter.time();
+        int count = rateLimiter.count();
+
+        String combineKey = getCombineKey(rateLimiter, point);
+        List<Object> keys = Collections.singletonList(combineKey);
+        try
+        {
+            Long number = redisTemplate.execute(limitScript, keys, count, time);
+            if (StringUtils.isNull(number) || number.intValue() > count)
+            {
+                throw new ServiceException("访问过于频繁,请稍后再试");
+            }
+            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
+        }
+        catch (ServiceException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("服务器限流异常,请稍后再试");
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private RateLimiter getAnnotationRateLimiter(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(RateLimiter.class);
+        }
+        return null;
+    }
+
+    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+    {
+        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+        if (rateLimiter.limitType() == LimitType.IP)
+        {
+            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        }
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
+        return stringBuffer.toString();
+    }
+}

+ 31 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.framework.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * 程序注解配置
+ *
+
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.fs.**.mapper")
+public class ApplicationConfig
+{
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+    {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 58 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java

@@ -0,0 +1,58 @@
+package com.fs.framework.config;
+
+import org.apache.ibatis.type.BaseTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.springframework.context.annotation.Configuration;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+@Configuration
+public class ArrayStringTypeHandler extends BaseTypeHandler<List<String>> {
+
+    @Override
+    public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
+        // 将 List<String> 转换为字符串,ClickHouse 支持的格式为 "['item1', 'item2']"
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        for (int j = 0; j < parameter.size(); j++) {
+            sb.append("'").append(parameter.get(j)).append("'");
+            if (j < parameter.size() - 1) {
+                sb.append(",");
+            }
+        }
+        sb.append("]");
+        ps.setString(i, sb.toString());
+    }
+
+    @Override
+    public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
+        // 处理查询结果,将其转换为 List<String>
+        String result = rs.getString(columnName);
+        return parseArray(result);
+    }
+
+    @Override
+    public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
+        String result = rs.getString(columnIndex);
+        return parseArray(result);
+    }
+
+    @Override
+    public List<String> getNullableResult(java.sql.CallableStatement cs, int columnIndex) throws SQLException {
+        String result = cs.getString(columnIndex);
+        return parseArray(result);
+    }
+
+    private List<String> parseArray(String arrayStr) {
+        // 将 ClickHouse 的 Array 字符串转换为 List<String>
+        if (arrayStr == null || arrayStr.isEmpty()) {
+            return null;
+        }
+        arrayStr = arrayStr.substring(1, arrayStr.length() - 1);  // 去掉 "[" 和 "]"
+        String[] elements = arrayStr.split(",");
+        return java.util.Arrays.asList(elements);
+    }
+}

+ 85 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/CaptchaConfig.java

@@ -0,0 +1,85 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * 验证码配置
+ * 
+
+ */
+@Configuration
+public class CaptchaConfig
+{
+    @Bean(name = "captchaProducer")
+    public DefaultKaptcha getKaptchaBean()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+
+    @Bean(name = "captchaProducerMath")
+    public DefaultKaptcha getKaptchaBeanMath()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 边框颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+        // 验证码文本生成器
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.fs.framework.config.KaptchaTextCreator");
+        // 验证码文本字符间距 默认为2
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 验证码噪点颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+        // 干扰实现类
+        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}

+ 92 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,92 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.sop.druid.master")
+    public DataSource sopDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.mysql.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+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 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);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    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);
+    }
+}

+ 59 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/FilterConfig.java

@@ -0,0 +1,59 @@
+package com.fs.framework.config;
+
+import com.fs.common.filter.RepeatableFilter;
+import com.fs.common.filter.XssFilter;
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filter配置
+ *
+
+ */
+@Configuration
+@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+public class FilterConfig
+{
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

+ 76 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/KaptchaTextCreator.java

@@ -0,0 +1,76 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+import java.util.Random;
+
+/**
+ * 验证码文本生成器
+ * 
+
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+    @Override
+    public String getText()
+    {
+        Integer result = 0;
+        Random random = new Random();
+        int x = random.nextInt(10);
+        int y = random.nextInt(10);
+        StringBuilder suChinese = new StringBuilder();
+        int randomoperands = (int) Math.round(Math.random() * 2);
+        if (randomoperands == 0)
+        {
+            result = x * y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("*");
+            suChinese.append(CNUMBERS[y]);
+        }
+        else if (randomoperands == 1)
+        {
+            if (!(x == 0) && y % x == 0)
+            {
+                result = y / x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("/");
+                suChinese.append(CNUMBERS[x]);
+            }
+            else
+            {
+                result = x + y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("+");
+                suChinese.append(CNUMBERS[y]);
+            }
+        }
+        else if (randomoperands == 2)
+        {
+            if (x >= y)
+            {
+                result = x - y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[y]);
+            }
+            else
+            {
+                result = y - x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[x]);
+            }
+        }
+        else
+        {
+            result = x + y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("+");
+            suChinese.append(CNUMBERS[y]);
+        }
+        suChinese.append("=?@" + result);
+        return suChinese.toString();
+    }
+}

+ 149 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,149 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations)
+    {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null)
+        {
+            for (String mapperLocation : mapperLocations)
+            {
+                try
+                {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                }
+                catch (IOException e)
+                {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+//    @Bean
+//    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+//    {
+//        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+//        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+//        String configLocation = env.getProperty("mybatis.configLocation");
+//        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+//        VFS.addImplClass(SpringBootVFS.class);
+//
+//        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+//        sessionFactory.setDataSource(dataSource);
+//        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+//        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+//        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+//        return sessionFactory.getObject();
+//    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 121 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,121 @@
+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;
+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;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> 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);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        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));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @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);
+
+        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);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        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;";
+    }
+}

+ 65 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -0,0 +1,65 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 通用配置
+ *
+
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+    @Autowired
+    private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry)
+    {
+        /** 本地文件上传路径 */
+        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + FSConfig.getProfile() + "/");
+
+        /** swagger配置 */
+        registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry)
+    {
+        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+    }
+
+    /**
+     * 跨域配置
+     */
+    @Bean
+    public CorsFilter corsFilter()
+    {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置访问源地址
+        config.addAllowedOrigin("*");
+        // 设置访问源请求头
+        config.addAllowedHeader("*");
+        // 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 对接口配置跨域设置
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+}

+ 157 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,157 @@
+package com.fs.framework.config;
+
+
+import com.fs.framework.security.filter.JwtAuthenticationTokenFilter;
+import com.fs.framework.security.handle.AuthenticationEntryPointImpl;
+import com.fs.framework.security.handle.LogoutSuccessHandlerImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.filter.CorsFilter;
+
+/**
+ * spring security配置
+ *
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+    /**
+     * 自定义用户认证逻辑
+     */
+    @Autowired
+    private UserDetailsService userDetailsService;
+
+    /**
+     * 认证失败处理类
+     */
+    @Autowired
+    private AuthenticationEntryPointImpl unauthorizedHandler;
+
+    /**
+     * 退出处理类
+     */
+    @Autowired
+    private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+    /**
+     * token认证过滤器
+     */
+    @Autowired
+    private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+    /**
+     * 跨域过滤器
+     */
+    @Autowired
+    private CorsFilter corsFilter;
+
+    /**
+     * 解决 无法直接注入 AuthenticationManager
+     *
+     * @return
+     * @throws Exception
+     */
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception
+    {
+        return super.authenticationManagerBean();
+    }
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity httpSecurity) throws Exception
+    {
+        httpSecurity
+                // CSRF禁用,因为不使用session
+                .csrf().disable()
+                // 认证失败处理类
+                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
+                // 基于token,所以不需要session
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+                // 过滤请求
+                .authorizeRequests()
+                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+                .antMatchers("/chat/upload/**","/login", "/register", "/captchaImage").anonymous()
+                .antMatchers(
+                        HttpMethod.GET,
+                        "/",
+                        "/*.html",
+                        "/**/*.html",
+                        "/**/*.css",
+                        "/**/*.js",
+                        "/profile/**"
+                ).permitAll()
+
+                .antMatchers("/**").anonymous()
+                .antMatchers("/msg/**").anonymous()
+                .antMatchers("/msg/**/**").anonymous()
+                .antMatchers("/msg").anonymous()
+                .antMatchers("/testSop").anonymous()
+                .antMatchers("/common/getId**").anonymous()
+                .antMatchers("/common/uploadOSS**").anonymous()
+                .antMatchers("/common/uploadWang**").anonymous()
+                .antMatchers("/common/download**").anonymous()
+                .antMatchers("/common/download/resource**").anonymous()
+                .antMatchers("/swagger-ui.html").anonymous()
+                .antMatchers("/swagger-resources/**").anonymous()
+                .antMatchers("/webjars/**").anonymous()
+                .antMatchers("/*/api-docs").anonymous()
+                .antMatchers("/druid/**").anonymous()
+                .antMatchers("/qw/data/**").anonymous()
+                // 除上面外的所有请求全部需要鉴权认证
+                .anyRequest().authenticated()
+                .and()
+                .headers().frameOptions().disable();
+        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
+        // 添加JWT filter
+        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+        // 添加CORS filter
+        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
+        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
+    }
+
+    /**
+     * 强散列哈希加密实现
+     */
+    @Bean
+    public BCryptPasswordEncoder bCryptPasswordEncoder()
+    {
+        return new BCryptPasswordEncoder();
+    }
+
+    /**
+     * 身份认证接口
+     */
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception
+    {
+        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
+    }
+}

+ 33 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ *
+
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     *
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 120 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,120 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ *
+
+ */
+@Configuration
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private FSConfig fsConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.fs.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<ApiKey> securitySchemes()
+    {
+        List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
+        apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:FS管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact(fsConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + fsConfig.getVersion())
+                .build();
+    }
+}

+ 63 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 1000;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 600;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService()
+    {
+        return new ScheduledThreadPoolExecutor(corePoolSize,
+                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
+        {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 77 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.framework.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * druid 配置属性
+ *
+
+ */
+@Configuration
+public class DruidProperties
+{
+    @Value("${spring.datasource.mysql.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.mysql.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.mysql.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.mysql.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.mysql.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.mysql.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.mysql.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.mysql.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.mysql.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource)
+    {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 27 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ *
+
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 45 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ *
+
+ */
+public class DynamicDataSourceContextHolder
+{
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+//        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 115 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/exception/GlobalExceptionHandler.java

@@ -0,0 +1,115 @@
+package com.fs.framework.exception;
+
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.exception.DemoModeException;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 全局异常处理器
+ *
+
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler
+{
+    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    /**
+     * 权限校验异常
+     */
+    @ExceptionHandler(AccessDeniedException.class)
+    public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request)
+    {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
+        return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
+    }
+
+    /**
+     * 请求方式不支持
+     */
+    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+    public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
+            HttpServletRequest request)
+    {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
+        return AjaxResult.error(e.getMessage());
+    }
+
+    /**
+     * 业务异常
+     */
+    @ExceptionHandler(ServiceException.class)
+    public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request)
+    {
+        log.error(e.getMessage(), e);
+        Integer code = e.getCode();
+        return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
+    }
+
+    /**
+     * 拦截未知的运行时异常
+     */
+    @ExceptionHandler(RuntimeException.class)
+    public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
+    {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',发生未知异常.", requestURI, e);
+        return AjaxResult.error(e.getMessage());
+    }
+
+    /**
+     * 系统异常
+     */
+    @ExceptionHandler(Exception.class)
+    public AjaxResult handleException(Exception e, HttpServletRequest request)
+    {
+        String requestURI = request.getRequestURI();
+        log.error("请求地址'{}',发生系统异常.", requestURI, e);
+        return AjaxResult.error(e.getMessage());
+    }
+
+    /**
+     * 自定义验证异常
+     */
+    @ExceptionHandler(BindException.class)
+    public AjaxResult handleBindException(BindException e)
+    {
+        log.error(e.getMessage(), e);
+        String message = e.getAllErrors().get(0).getDefaultMessage();
+        return AjaxResult.error(message);
+    }
+
+    /**
+     * 自定义验证异常
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
+    {
+        log.error(e.getMessage(), e);
+        String message = e.getBindingResult().getFieldError().getDefaultMessage();
+        return AjaxResult.error(message);
+    }
+
+    /**
+     * 演示模式异常
+     */
+    @ExceptionHandler(DemoModeException.class)
+    public AjaxResult handleDemoModeException(DemoModeException e)
+    {
+        return AjaxResult.error("演示模式,不允许操作");
+    }
+}

+ 56 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,56 @@
+package com.fs.framework.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+
+/**
+ * 防止重复提交拦截器
+ *
+
+ */
+@Component
+public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
+{
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+    {
+        if (handler instanceof HandlerMethod)
+        {
+            HandlerMethod handlerMethod = (HandlerMethod) handler;
+            Method method = handlerMethod.getMethod();
+            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+            if (annotation != null)
+            {
+                if (this.isRepeatSubmit(request))
+                {
+                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
+                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+            return true;
+        }
+        else
+        {
+            return super.preHandle(request, response, handler);
+        }
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request);
+}

+ 126 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,126 @@
+package com.fs.framework.interceptor.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.filter.RepeatedlyRequestWrapper;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.http.HttpHelper;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
+
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
+
+    // 令牌自定义标识
+    @Value("${token.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 间隔时间,单位:秒 默认10秒
+     *
+     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
+     */
+    private int intervalTime = 10;
+
+    public void setIntervalTime(int intervalTime)
+    {
+        this.intervalTime = intervalTime;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request)
+    {
+        String nowParams = "";
+        if (request instanceof RepeatedlyRequestWrapper)
+        {
+            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+        }
+
+        // body参数为空,获取Parameter的数据
+        if (StringUtils.isEmpty(nowParams))
+        {
+            nowParams = JSONObject.toJSONString(request.getParameterMap());
+        }
+        Map<String, Object> nowDataMap = new HashMap<String, Object>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放cache的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = request.getHeader(header);
+        if (StringUtils.isEmpty(submitKey))
+        {
+            submitKey = url;
+        }
+
+        // 唯一标识(指定key + 消息头)
+        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+        if (sessionObj != null)
+        {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url))
+            {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
+        return false;
+    }
+
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
+
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < (this.intervalTime * 1000))
+        {
+            return true;
+        }
+        return false;
+    }
+}

+ 56 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/manager/AsyncManager.java

@@ -0,0 +1,56 @@
+package com.fs.framework.manager;
+
+import com.fs.common.utils.Threads;
+import com.fs.common.utils.spring.SpringUtils;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 异步任务管理器
+ * 
+ 
+ */
+public class AsyncManager
+{
+    /**
+     * 操作延迟10毫秒
+     */
+    private final int OPERATE_DELAY_TIME = 10;
+
+    /**
+     * 异步操作任务调度线程池
+     */
+    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+    /**
+     * 单例模式
+     */
+    private AsyncManager(){}
+
+    private static AsyncManager me = new AsyncManager();
+
+    public static AsyncManager me()
+    {
+        return me;
+    }
+
+    /**
+     * 执行任务
+     * 
+     * @param task 任务
+     */
+    public void execute(TimerTask task)
+    {
+        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 停止任务线程池
+     */
+    public void shutdown()
+    {
+        Threads.shutdownAndAwaitTermination(executor);
+    }
+}

+ 40 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/manager/ShutdownManager.java

@@ -0,0 +1,40 @@
+package com.fs.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+ 
+ */
+@Component
+public class ShutdownManager
+{
+    private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+    @PreDestroy
+    public void destroy()
+    {
+        shutdownAsyncManager();
+    }
+
+    /**
+     * 停止异步执行任务
+     */
+    private void shutdownAsyncManager()
+    {
+        try
+        {
+            logger.info("====关闭后台任务任务线程池====");
+            AsyncManager.me().shutdown();
+        }
+        catch (Exception e)
+        {
+            logger.error(e.getMessage(), e);
+        }
+    }
+}

+ 106 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,106 @@
+package com.fs.framework.manager.factory;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.utils.LogUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.company.domain.CompanyLogininfor;
+import com.fs.company.domain.CompanyOperLog;
+import com.fs.company.service.ICompanyLogininforService;
+import com.fs.company.service.ICompanyOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.TimerTask;
+
+/**
+ * 异步工厂(产生任务用)
+ * 
+
+ */
+public class AsyncFactory
+{
+    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+    /**
+     * 记录登录信息
+     * 
+     * @param username 用户名
+     * @param status 状态
+     * @param message 消息
+     * @param args 列表
+     * @return 任务task
+     */
+    public static TimerTask recordLogininfor(final Long companyId,final String username, final String status, final String message,
+            final Object... args)
+    {
+        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                String address = AddressUtils.getRealAddressByIP(ip);
+                StringBuilder s = new StringBuilder();
+                s.append(LogUtils.getBlock(ip));
+                s.append(address);
+                s.append(LogUtils.getBlock(username));
+                s.append(LogUtils.getBlock(status));
+                s.append(LogUtils.getBlock(message));
+                // 打印信息到日志
+                sys_user_logger.info(s.toString(), args);
+                // 获取客户端操作系统
+                String os = userAgent.getOperatingSystem().getName();
+                // 获取客户端浏览器
+                String browser = userAgent.getBrowser().getName();
+                // 封装对象
+                CompanyLogininfor logininfor = new CompanyLogininfor();
+                logininfor.setCompanyId(companyId);
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (Constants.LOGIN_SUCCESS.equals(status) || Constants.LOGOUT.equals(status))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                logininfor.setLoginTime(new Date());
+                // 插入数据
+                SpringUtils.getBean(ICompanyLogininforService.class).insertCompanyLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     * 
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final CompanyOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperTime(new Date());
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ICompanyOperLogService.class).insertCompanyOperLog(operLog);
+            }
+        };
+    }
+}

+ 69 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/LoginBody.java

@@ -0,0 +1,69 @@
+package com.fs.framework.security;
+
+/**
+ * 用户登录对象
+ * 
+
+ */
+public class LoginBody
+{
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 用户密码
+     */
+    private String password;
+
+    /**
+     * 验证码
+     */
+    private String code;
+
+    /**
+     * 唯一标识
+     */
+    private String uuid = "";
+
+    public String getUsername()
+    {
+        return username;
+    }
+
+    public void setUsername(String username)
+    {
+        this.username = username;
+    }
+
+    public String getPassword()
+    {
+        return password;
+    }
+
+    public void setPassword(String password)
+    {
+        this.password = password;
+    }
+
+    public String getCode()
+    {
+        return code;
+    }
+
+    public void setCode(String code)
+    {
+        this.code = code;
+    }
+
+    public String getUuid()
+    {
+        return uuid;
+    }
+
+    public void setUuid(String uuid)
+    {
+        this.uuid = uuid;
+    }
+}

+ 255 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/LoginUser.java

@@ -0,0 +1,255 @@
+package com.fs.framework.security;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyUser;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ * 
+
+ */
+public class LoginUser implements UserDetails
+{
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户唯一标识
+     */
+    private String token;
+
+    /**
+     * 登录时间
+     */
+    private Long loginTime;
+
+    /**
+     * 过期时间
+     */
+    private Long expireTime;
+
+    /**
+     * 登录IP地址
+     */
+    private String ipaddr;
+
+    /**
+     * 登录地点
+     */
+    private String loginLocation;
+
+    /**
+     * 浏览器类型
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+
+
+    /**
+     * 权限列表
+     */
+    private Set<String> permissions;
+
+    /**
+     * 用户信息
+     */
+    private CompanyUser user;
+
+    private Company company;
+
+
+
+    public static long getSerialVersionUID() {
+        return serialVersionUID;
+    }
+
+
+
+    public Company getCompany() {
+        return company;
+    }
+
+    public void setCompany(Company company) {
+        this.company = company;
+    }
+
+    public String getToken()
+    {
+        return token;
+    }
+
+    public void setToken(String token)
+    {
+        this.token = token;
+    }
+
+    public LoginUser()
+    {
+    }
+    public LoginUser(CompanyUser user, Set<String> permissions, Company company)
+    {
+        this.user = user;
+        this.permissions = permissions;
+        this.company=company;
+    }
+    public LoginUser(CompanyUser user, Set<String> permissions)
+    {
+        this.user = user;
+        this.permissions = permissions;
+    }
+
+    @JsonIgnore
+    @Override
+    public String getPassword()
+    {
+        return user.getPassword();
+    }
+
+    @Override
+    public String getUsername()
+    {
+        return user.getUserName();
+    }
+
+    /**
+     * 账户是否未过期,过期无法验证
+     */
+    @JsonIgnore
+    @Override
+    public boolean isAccountNonExpired()
+    {
+        return true;
+    }
+
+    /**
+     * 指定用户是否解锁,锁定的用户无法进行身份验证
+     * 
+     * @return
+     */
+    @JsonIgnore
+    @Override
+    public boolean isAccountNonLocked()
+    {
+        return true;
+    }
+
+    /**
+     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
+     * 
+     * @return
+     */
+    @JsonIgnore
+    @Override
+    public boolean isCredentialsNonExpired()
+    {
+        return true;
+    }
+
+    /**
+     * 是否可用 ,禁用的用户不能身份验证
+     * 
+     * @return
+     */
+    @JsonIgnore
+    @Override
+    public boolean isEnabled()
+    {
+        return true;
+    }
+
+    public Long getLoginTime()
+    {
+        return loginTime;
+    }
+
+    public void setLoginTime(Long loginTime)
+    {
+        this.loginTime = loginTime;
+    }
+
+    public String getIpaddr()
+    {
+        return ipaddr;
+    }
+
+    public void setIpaddr(String ipaddr)
+    {
+        this.ipaddr = ipaddr;
+    }
+
+    public String getLoginLocation()
+    {
+        return loginLocation;
+    }
+
+    public void setLoginLocation(String loginLocation)
+    {
+        this.loginLocation = loginLocation;
+    }
+
+    public String getBrowser()
+    {
+        return browser;
+    }
+
+    public void setBrowser(String browser)
+    {
+        this.browser = browser;
+    }
+
+    public String getOs()
+    {
+        return os;
+    }
+
+    public void setOs(String os)
+    {
+        this.os = os;
+    }
+
+    public Long getExpireTime()
+    {
+        return expireTime;
+    }
+
+    public void setExpireTime(Long expireTime)
+    {
+        this.expireTime = expireTime;
+    }
+
+    public Set<String> getPermissions()
+    {
+        return permissions;
+    }
+
+    public void setPermissions(Set<String> permissions)
+    {
+        this.permissions = permissions;
+    }
+
+    public CompanyUser getUser()
+    {
+        return user;
+    }
+
+    public void setUser(CompanyUser user)
+    {
+        this.user = user;
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities()
+    {
+        return null;
+    }
+}

+ 89 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/SecurityUtils.java

@@ -0,0 +1,89 @@
+package com.fs.framework.security;
+
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.exception.CustomException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+/**
+ * 安全服务工具类
+ * 
+
+ */
+public class SecurityUtils
+{
+    /**
+     * 获取用户账户
+     **/
+    public static String getUsername()
+    {
+        try
+        {
+            return getLoginUser().getUsername();
+        }
+        catch (Exception e)
+        {
+            throw new CustomException("获取用户账户异常", HttpStatus.UNAUTHORIZED);
+        }
+    }
+
+    /**
+     * 获取用户
+     **/
+    public static LoginUser getLoginUser()
+    {
+        try
+        {
+            return (LoginUser) getAuthentication().getPrincipal();
+        }
+        catch (Exception e)
+        {
+            throw new CustomException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
+        }
+    }
+
+    /**
+     * 获取Authentication
+     */
+    public static Authentication getAuthentication()
+    {
+        return SecurityContextHolder.getContext().getAuthentication();
+    }
+
+    /**
+     * 生成BCryptPasswordEncoder密码
+     *
+     * @param password 密码
+     * @return 加密字符串
+     */
+    public static String encryptPassword(String password)
+    {
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.encode(password);
+    }
+
+    /**
+     * 判断密码是否相同
+     *
+     * @param rawPassword 真实密码
+     * @param encodedPassword 加密后字符
+     * @return 结果
+     */
+    public static boolean matchesPassword(String rawPassword, String encodedPassword)
+    {
+        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
+        return passwordEncoder.matches(rawPassword, encodedPassword);
+    }
+
+    /**
+     * 是否为管理员
+     * 
+     * @param userId 用户ID
+     * @return 结果
+     */
+    public static boolean isAdmin(Long userId)
+    {
+        return userId != null && 1L == userId;
+    }
+}

+ 47 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/filter/JwtAuthenticationTokenFilter.java

@@ -0,0 +1,47 @@
+package com.fs.framework.security.filter;
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * token过滤器 验证token有效性
+ *
+
+ */
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
+{
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private RedisCache redisCache;
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException
+    {
+        LoginUser loginUser = tokenService.getLoginUser(request);
+        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
+        {
+            tokenService.verifyToken(loginUser);
+            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+        }
+        chain.doFilter(request, response);
+    }
+}

+ 35 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java

@@ -0,0 +1,35 @@
+package com.fs.framework.security.handle;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.Serializable;
+
+/**
+ * 认证失败处理类 返回未授权
+ * 
+ 
+ */
+@Component
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
+{
+    private static final long serialVersionUID = -8970718410437077606L;
+
+    @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
+            throws IOException
+    {
+        int code = HttpStatus.UNAUTHORIZED;
+        String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
+        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
+    }
+}

+ 54 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/security/handle/LogoutSuccessHandlerImpl.java

@@ -0,0 +1,54 @@
+package com.fs.framework.security.handle;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.constant.Constants;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 自定义退出处理类 返回成功
+ *
+
+ */
+@Configuration
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
+{
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 退出处理
+     *
+     * @return
+     */
+    @Override
+    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
+            throws IOException, ServletException
+    {
+        LoginUser loginUser = tokenService.getLoginUser(request);
+        if (StringUtils.isNotNull(loginUser))
+        {
+            String userName = loginUser.getUsername();
+            // 删除用户缓存记录
+            tokenService.delLoginUser(loginUser.getToken());
+            // 记录用户退出日志
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getCompany().getCompanyId(),userName, Constants.LOGOUT, "退出成功"));
+        }
+        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(HttpStatus.SUCCESS, "退出成功")));
+    }
+}

+ 92 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/service/CompanyLoginService.java

@@ -0,0 +1,92 @@
+package com.fs.framework.service;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.exception.user.CaptchaException;
+import com.fs.common.exception.user.CaptchaExpireException;
+import com.fs.common.exception.user.UserPasswordNotMatchException;
+import com.fs.common.utils.MessageUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.framework.security.LoginUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 登录校验方法
+ *
+
+ */
+@Component
+public class CompanyLoginService
+{
+    @Autowired
+    private TokenService tokenService;
+
+    @Resource
+    private AuthenticationManager authenticationManager;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 登录验证
+     *
+     * @param username 用户名
+     * @param password 密码
+     * @param code 验证码
+     * @param uuid 唯一标识
+     * @return 结果
+     */
+    public String login(String username, String password, String code, String uuid)
+    {
+        String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid;
+        String captcha = redisCache.getCacheObject(verifyKey);
+        redisCache.deleteObject(verifyKey);
+        if (captcha == null)
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+            throw new CaptchaExpireException();
+        }
+        if (!code.equalsIgnoreCase(captcha))
+        {
+            AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+            throw new CaptchaException();
+        }
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(0l,username, Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+        AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUser().getCompanyId(),username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+        redisCache.setCacheObject("companyId:"+loginUser.getUser().getUserId(),loginUser.getUser().getCompanyId(),604800, TimeUnit.SECONDS);
+        // 生成token
+        return tokenService.createToken(loginUser);
+    }
+
+}

+ 66 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/service/CompanyPermissionService.java

@@ -0,0 +1,66 @@
+package com.fs.framework.service;
+
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyMenuService;
+import com.fs.company.service.ICompanyRoleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 用户权限处理
+ * 
+ 
+ */
+@Component
+public class CompanyPermissionService
+{
+    @Autowired
+    private ICompanyRoleService roleService;
+    @Autowired
+    private ICompanyMenuService menuService;
+
+    /**
+     * 获取角色数据权限
+     * 
+     * @param user 用户信息
+     * @return 角色权限信息
+     */
+    public Set<String> getRolePermission(CompanyUser user)
+    {
+        Set<String> roles = new HashSet<String>();
+        // 管理员拥有所有权限
+        if (user.isAdmin())
+        {
+            roles.add("admin");
+        }
+        else
+        {
+            roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
+        }
+        return roles;
+    }
+
+    /**
+     * 获取菜单数据权限
+     * 
+     * @param user 用户信息
+     * @return 菜单权限信息
+     */
+    public Set<String> getMenuPermission(CompanyUser user)
+    {
+        Set<String> perms = new HashSet<String>();
+        // 管理员拥有所有权限
+        if (user.isAdmin())
+        {
+            perms.add("*:*:*");
+        }
+        else
+        {
+            perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
+        }
+        return perms;
+    }
+}

+ 170 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/service/PermissionService.java

@@ -0,0 +1,170 @@
+package com.fs.framework.service;
+
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyRole;
+import com.fs.framework.security.LoginUser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Set;
+
+/**
+ * 自定义权限实现,ss取自SpringSecurity首字母
+ */
+@Service("ss")
+public class PermissionService
+{
+    /** 所有权限标识 */
+    private static final String ALL_PERMISSION = "*:*:*";
+
+    /** 管理员角色权限标识 */
+    private static final String SUPER_ADMIN = "admin";
+
+    private static final String ROLE_DELIMETER = ",";
+
+    private static final String PERMISSION_DELIMETER = ",";
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 验证用户是否具备某权限
+     * 
+     * @param permission 权限字符串
+     * @return 用户是否具备某权限
+     */
+    public boolean hasPermi(String permission)
+    {
+        if (StringUtils.isEmpty(permission))
+        {
+            return false;
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+        {
+            return false;
+        }
+        return hasPermissions(loginUser.getPermissions(), permission);
+    }
+
+    /**
+     * 验证用户是否不具备某权限,与 hasPermi逻辑相反
+     *
+     * @param permission 权限字符串
+     * @return 用户是否不具备某权限
+     */
+    public boolean lacksPermi(String permission)
+    {
+        return hasPermi(permission) != true;
+    }
+
+    /**
+     * 验证用户是否具有以下任意一个权限
+     *
+     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
+     * @return 用户是否具有以下任意一个权限
+     */
+    public boolean hasAnyPermi(String permissions)
+    {
+        if (StringUtils.isEmpty(permissions))
+        {
+            return false;
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+        {
+            return false;
+        }
+        Set<String> authorities = loginUser.getPermissions();
+        for (String permission : permissions.split(PERMISSION_DELIMETER))
+        {
+            if (permission != null && hasPermissions(authorities, permission))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断用户是否拥有某个角色
+     * 
+     * @param role 角色字符串
+     * @return 用户是否具备某角色
+     */
+    public boolean hasRole(String role)
+    {
+        if (StringUtils.isEmpty(role))
+        {
+            return false;
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+        {
+            return false;
+        }
+        for (CompanyRole sysRole : loginUser.getUser().getRoles())
+        {
+            String roleKey = sysRole.getRoleKey();
+            if (SUPER_ADMIN.contains(roleKey) || roleKey.contains(StringUtils.trim(role)))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 验证用户是否不具备某角色,与 isRole逻辑相反。
+     *
+     * @param role 角色名称
+     * @return 用户是否不具备某角色
+     */
+    public boolean lacksRole(String role)
+    {
+        return hasRole(role) != true;
+    }
+
+    /**
+     * 验证用户是否具有以下任意一个角色
+     *
+     * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
+     * @return 用户是否具有以下任意一个角色
+     */
+    public boolean hasAnyRoles(String roles)
+    {
+        if (StringUtils.isEmpty(roles))
+        {
+            return false;
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+        {
+            return false;
+        }
+        for (String role : roles.split(ROLE_DELIMETER))
+        {
+            if (hasRole(role))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断是否包含权限
+     * 
+     * @param permissions 权限列表
+     * @param permission 权限字符串
+     * @return 用户是否具备某权限
+     */
+    private boolean hasPermissions(Set<String> permissions, String permission)
+    {
+        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
+    }
+
+
+}

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

@@ -0,0 +1,236 @@
+package com.fs.framework.service;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.uuid.IdUtils;
+import com.fs.framework.security.LoginUser;
+import eu.bitwalker.useragentutils.UserAgent;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * token验证处理
+ *
+ 
+ */
+@Component
+public class TokenService
+{
+    // 令牌自定义标识
+    @Value("${token.header}")
+    private String header;
+
+    // 令牌秘钥
+    @Value("${token.secret}")
+    private String secret;
+
+    // 令牌有效期(默认30分钟)
+    @Value("${token.expireTime}")
+    private int expireTime;
+
+    protected static final long MILLIS_SECOND = 1000;
+
+    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+
+    private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 100000000000L;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 获取用户身份信息
+     *
+     * @return 用户信息
+     */
+    public LoginUser getLoginUser(HttpServletRequest request)
+    {
+        // 获取请求携带的令牌
+        String token = getToken(request);
+        if (StringUtils.isNotEmpty(token))
+        {
+            Claims claims = parseToken(token);
+            // 解析对应的权限以及用户信息
+            String uuid = (String) claims.get(Constants.COMPANY_LOGIN_USER_KEY);
+            String userKey = getTokenKey(uuid);
+            LoginUser user = redisCache.getCacheObject(userKey);
+            return user;
+        }
+        token=getUrlToken(request);
+        if (StringUtils.isNotEmpty(token))
+        {
+            Claims claims = parseToken(token);
+            // 解析对应的权限以及用户信息
+            String uuid = (String) claims.get(Constants.COMPANY_LOGIN_USER_KEY);
+            String userKey = getTokenKey(uuid);
+            LoginUser user = redisCache.getCacheObject(userKey);
+            return user;
+        }
+
+        return null;
+    }
+
+    /**
+     * 设置用户身份信息
+     */
+    public void setLoginUser(LoginUser loginUser)
+    {
+        if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
+        {
+            refreshToken(loginUser);
+        }
+    }
+
+    /**
+     * 删除用户身份信息
+     */
+    public void delLoginUser(String token)
+    {
+        if (StringUtils.isNotEmpty(token))
+        {
+            String userKey = getTokenKey(token);
+            redisCache.deleteObject(userKey);
+        }
+    }
+
+    /**
+     * 创建令牌
+     *
+     * @param loginUser 用户信息
+     * @return 令牌
+     */
+    public String createToken(LoginUser loginUser)
+    {
+        String token = IdUtils.fastUUID();
+        loginUser.setToken(token);
+        setUserAgent(loginUser);
+        refreshToken(loginUser);
+
+        Map<String, Object> claims = new HashMap<>();
+        claims.put(Constants.COMPANY_LOGIN_USER_KEY, token);
+        return createToken(claims);
+    }
+
+    /**
+     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
+     *
+     * @param loginUser
+     * @return 令牌
+     */
+    public void verifyToken(LoginUser loginUser)
+    {
+        long expireTime = loginUser.getExpireTime();
+        long currentTime = System.currentTimeMillis();
+        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
+        {
+            refreshToken(loginUser);
+        }
+    }
+
+    /**
+     * 刷新令牌有效期
+     *
+     * @param loginUser 登录信息
+     */
+    public void refreshToken(LoginUser loginUser)
+    {
+        loginUser.setLoginTime(System.currentTimeMillis());
+        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
+        // 根据uuid将loginUser缓存
+        String userKey = getTokenKey(loginUser.getToken());
+        redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+    }
+
+    /**
+     * 设置用户代理信息
+     *
+     * @param loginUser 登录信息
+     */
+    public void setUserAgent(LoginUser loginUser)
+    {
+        UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        loginUser.setIpaddr(ip);
+        loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+        loginUser.setBrowser(userAgent.getBrowser().getName());
+        loginUser.setOs(userAgent.getOperatingSystem().getName());
+    }
+
+    /**
+     * 从数据声明生成令牌
+     *
+     * @param claims 数据声明
+     * @return 令牌
+     */
+    private String createToken(Map<String, Object> claims)
+    {
+        String token = Jwts.builder()
+                .setClaims(claims)
+                .signWith(SignatureAlgorithm.HS512, secret).compact();
+        return token;
+    }
+
+    /**
+     * 从令牌中获取数据声明
+     *
+     * @param token 令牌
+     * @return 数据声明
+     */
+    private Claims parseToken(String token)
+    {
+        return Jwts.parser()
+                .setSigningKey(secret)
+                .parseClaimsJws(token)
+                .getBody();
+    }
+
+    /**
+     * 从令牌中获取用户名
+     *
+     * @param token 令牌
+     * @return 用户名
+     */
+    public String getUsernameFromToken(String token)
+    {
+        Claims claims = parseToken(token);
+        return claims.getSubject();
+    }
+
+    /**
+     * 获取请求token
+     *
+     * @param request
+     * @return token
+     */
+    private String getToken(HttpServletRequest request)
+    {
+        String token = request.getHeader(header);
+        if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
+        {
+            token = token.replace(Constants.TOKEN_PREFIX, "");
+        }
+        return token;
+    }
+    private String getUrlToken(HttpServletRequest request)
+    {
+        String token = request.getParameter("token");
+        return token;
+    }
+
+    private String getTokenKey(String uuid)
+    {
+        return Constants.COMPANY_LOGIN_TOKEN_KEY + uuid;
+    }
+}

+ 75 - 0
fs-qw-api-msg/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java

@@ -0,0 +1,75 @@
+package com.fs.framework.service;
+
+
+import com.fs.common.enums.UserStatus;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.Company;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyService;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.framework.security.LoginUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * 用户验证处理
+ *
+ 
+ */
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService
+{
+    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
+
+    @Autowired
+    private ICompanyUserService userService;
+
+    @Autowired
+    private CompanyPermissionService permissionService;
+
+    @Autowired
+    private ICompanyService companyService;
+
+
+
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
+    {
+
+
+        CompanyUser user = userService.selectUserByUserName(username);
+        if (StringUtils.isNull(user))
+        {
+            log.info("登录用户:{} 不存在.", username);
+            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
+        }
+        Company company=companyService.selectCompanyById(user.getCompanyId()) ;
+        if(company==null||company.getStatus()==0||company.getIsDel()==1){
+            throw new CustomException("此用户所属公司不存在或已停用");
+        }
+        if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
+        {
+            log.info("登录用户:{} 已被删除.", username);
+            throw new CustomException("对不起,您的账号:" + username + " 已被删除");
+        }
+        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
+        {
+            log.info("登录用户:{} 已被停用.", username);
+            throw new CustomException("对不起,您的账号:" + username + " 已停用");
+        }
+
+        return createLoginUser(user);
+    }
+
+    public UserDetails createLoginUser(CompanyUser user)
+    {
+        return new LoginUser(user, permissionService.getMenuPermission(user),companyService.selectCompanyById(user.getCompanyId()));
+    }
+}

+ 128 - 0
fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java

@@ -0,0 +1,128 @@
+package com.tencent.wework;
+
+/* sdk
+typedef struct Slice_t {
+    char* buf;
+    int len;
+} Slice_t;
+
+typedef struct MediaData {
+    char* outindexbuf;
+    int out_len;
+    char* data;    
+    int data_len;
+    int is_finish;
+} MediaData_t;
+*/
+
+public class Finance {
+    public native static long NewSdk();
+
+    /**
+     * ��ʼ������
+     * Returnֵ=0��ʾ��API���óɹ�
+     *
+     * @param [in] sdk			NewSdk���ص�sdkָ��
+     * @param [in] corpid      ������ҵ����ҵid�����磺wwd08c8exxxx5ab44d����������ҵ΢�Ź����--�ҵ���ҵ--��ҵ��Ϣ�鿴
+     * @param [in] secret		�������ݴ浵��Secret����������ҵ΢�Ź����--������--�������ݴ浵�鿴
+     * @return �����Ƿ��ʼ���ɹ�
+     * 0   - �ɹ�
+     * !=0 - ʧ��
+     */
+    public native static int Init(long sdk, String corpid, String secret);
+
+    /**
+     * ��ȡ�����¼����
+     * Returnֵ=0��ʾ��API���óɹ�
+     *
+     * @param [in]  sdk				NewSdk���ص�sdkָ��
+     * @param [in]  seq				��ָ����seq��ʼ��ȡ��Ϣ��ע����Ƿ��ص���Ϣ��seq+1��ʼ���أ�seqΪ֮ǰ�ӿڷ��ص����seqֵ���״�ʹ����ʹ��seq:0
+     * @param [in]  limit			һ����ȡ����Ϣ���������ֵ1000��������1000���᷵�ش���
+     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
+     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
+     * @param [out] chatDatas		���ر�����ȡ��Ϣ�����ݣ�slice�ṹ��.���ݰ���errcode/errmsg���Լ�ÿ����Ϣ���ݡ�
+     * @return �����Ƿ���óɹ�
+     * 0   - �ɹ�
+     * !=0 - ʧ��
+     */
+    public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);
+
+    /**
+     * ��ȡý����Ϣ����
+     * Returnֵ=0��ʾ��API���óɹ�
+     *
+     * @param [in]  sdk				NewSdk���ص�sdkָ��
+     * @param [in]  sdkFileid		��GetChatData���ص�������Ϣ�У�ý����Ϣ������sdkfileid
+     * @param [in]  proxy			ʹ�ô����������Ҫ�����������ӡ��磺socks5://10.0.0.1:8081 ���� http://10.0.0.1:8081
+     * @param [in]  passwd			�����˺����룬��Ҫ���������˺����롣�� user_name:passwd_123
+     * @param [in]  indexbuf		ý����Ϣ��Ƭ��ȡ����Ҫ����ÿ����ȡ��������Ϣ���״β���Ҫ��д��Ĭ����ȡ512k������ÿ�ε���ֻ��Ҫ���ϴε��÷��ص�outindexbuf���뼴�ɡ�
+     * @param [out] media_data		���ر�����ȡ��ý������.MediaData�ṹ��.���ݰ���data(��������)/outindexbuf(�´�����)/is_finish(��ȡ��ɱ��)
+     * @return �����Ƿ���óɹ�
+     * 0   - �ɹ�
+     * !=0 - ʧ��
+     */
+    public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);
+
+    /**
+     * @param [in]  encrypt_key, getchatdata���ص�encrypt_key
+     * @param [in]  encrypt_msg, getchatdata���ص�content
+     * @param [out] msg, ���ܵ���Ϣ����
+     * @return �����Ƿ���óɹ�
+     * 0   - �ɹ�
+     * !=0 - ʧ��
+     * @brief ��������
+     */
+    public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
+
+    public native static void DestroySdk(long sdk);
+
+    public native static long NewSlice();
+
+    /**
+     * @return
+     * @brief �ͷ�slice����NewSlice�ɶ�ʹ��
+     */
+    public native static void FreeSlice(long slice);
+
+    /**
+     * @return ����
+     * @brief ��ȡslice����
+     */
+    public native static String GetContentFromSlice(long slice);
+
+    /**
+     * @return ����
+     * @brief ��ȡslice���ݳ���
+     */
+    public native static int GetSliceLen(long slice);
+
+    public native static long NewMediaData();
+
+    public native static void FreeMediaData(long mediaData);
+
+    /**
+     * @return outindex
+     * @brief ��ȡmediadata outindex
+     */
+    public native static String GetOutIndexBuf(long mediaData);
+
+    /**
+     * @return data
+     * @brief ��ȡmediadata data����
+     */
+    public native static byte[] GetData(long mediaData);
+
+    public native static int GetIndexLen(long mediaData);
+
+    public native static int GetDataLen(long mediaData);
+
+    /**
+     * @return 1��ɡ�0δ���
+     * @brief �ж�mediadata�Ƿ����
+     */
+    public native static int IsMediaDataFinish(long mediaData);
+
+    static {
+        System.loadLibrary("WeWorkFinanceSdk");
+    }
+}

+ 1 - 0
fs-qw-api-msg/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 0 - 152
fs-qw-api-msg/src/main/resources/application-druid.yml

@@ -1,152 +0,0 @@
-# 数据源配置
-spring:
-    # redis 配置
-    redis:
-        # 地址  localhost
-        host: 127.0.0.1
-        #        host: 127.0.0.1
-        # 端口,默认为6379
-        port: 6379
-        # 数据库索引
-        database: 0
-        # 密码
-        password:
-        #        password:
-        # 连接超时时间
-        timeout: 10s
-        lettuce:
-            pool:
-                # 连接池中的最小空闲连接
-                min-idle: 0
-                # 连接池中的最大空闲连接
-                max-idle: 8
-                # 连接池的最大数据库连接数
-                max-active: 8
-                # #连接池最大阻塞等待时间(使用负值表示没有限制)
-                max-wait: -1ms
-    datasource:
-        clickhouse:
-            type: com.alibaba.druid.pool.DruidDataSource
-            #            driverClassName: ru.yandex.clickhouse.ClickHouseDriver
-            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
-            url: jdbc:clickhouse://1.14.104.71:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
-            username: default
-            password: rt2024
-            initialSize: 10
-            maxActive: 100
-            minIdle: 10
-            maxWait: 6000
-        mysql:
-            type: com.alibaba.druid.pool.DruidDataSource
-            driverClassName: com.mysql.cj.jdbc.Driver
-            druid:
-                # 主库数据源
-                master:
-                    url: jdbc:mysql://cq-cdb-95qvu08p.sql.tencentcdb.com:63998/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: Rtyy_2023
-                # 从库数据源
-                slave:
-                    # 从数据源开关/默认关闭
-                    enabled: false
-                    url:
-                    username:
-                    password:
-                # 初始连接数
-                initialSize: 5
-                # 最小连接池数量
-                minIdle: 10
-                # 最大连接池数量
-                maxActive: 20
-                # 配置获取连接等待超时的时间
-                maxWait: 60000
-                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
-                timeBetweenEvictionRunsMillis: 60000
-                # 配置一个连接在池中最小生存的时间,单位是毫秒
-                minEvictableIdleTimeMillis: 300000
-                # 配置一个连接在池中最大生存的时间,单位是毫秒
-                maxEvictableIdleTimeMillis: 900000
-                # 配置检测连接是否有效
-                validationQuery: SELECT 1 FROM DUAL
-                testWhileIdle: true
-                testOnBorrow: false
-                testOnReturn: false
-                webStatFilter:
-                    enabled: true
-                statViewServlet:
-                    enabled: true
-                    # 设置白名单,不填则允许所有访问
-                    allow:
-                    url-pattern: /druid/*
-                    # 控制台管理用户名和密码
-                    login-username: fs
-                    login-password: 123456
-                filter:
-                    stat:
-                        enabled: true
-                        # 慢SQL记录
-                        log-slow-sql: true
-                        slow-sql-millis: 1000
-                        merge-sql: true
-                    wall:
-                        config:
-                            multi-statement-allow: true
-        sop:
-            type: com.alibaba.druid.pool.DruidDataSource
-            driverClassName: com.mysql.cj.jdbc.Driver
-            druid:
-                # 主库数据源
-                master:
-                    url: jdbc:mysql://gz-cdb-of55khc9.sql.tencentcdb.com:23620/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: Rtyy_2023
-                # 初始连接数a
-                initialSize: 5
-                # 最小连接池数量
-                minIdle: 10
-                # 最大连接池数量
-                maxActive: 20
-                # 配置获取连接等待超时的时间
-                maxWait: 60000
-                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
-                timeBetweenEvictionRunsMillis: 60000
-                # 配置一个连接在池中最小生存的时间,单位是毫秒
-                minEvictableIdleTimeMillis: 300000
-                # 配置一个连接在池中最大生存的时间,单位是毫秒
-                maxEvictableIdleTimeMillis: 900000
-                # 配置检测连接是否有效
-                validationQuery: SELECT 1 FROM DUAL
-                testWhileIdle: true
-                testOnBorrow: false
-                testOnReturn: false
-                webStatFilter:
-                    enabled: true
-                statViewServlet:
-                    enabled: true
-                    # 设置白名单,不填则允许所有访问
-                    allow:
-                    url-pattern: /druid/*
-                    # 控制台管理用户名和密码
-                    login-username: fs
-                    login-password: 123456
-                filter:
-                    stat:
-                        enabled: true
-                        # 慢SQL记录
-                        log-slow-sql: true
-                        slow-sql-millis: 1000
-                        merge-sql: true
-                    wall:
-                        config:
-                            multi-statement-allow: true
-rocketmq:
-    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
-    producer:
-        group: my-producer-group
-        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
-    consumer:
-        group: voice-group
-        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
-        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
-

+ 13 - 0
fs-qw-api-msg/src/main/resources/application.yml

@@ -0,0 +1,13 @@
+server:
+  port: 8006
+# Spring配置
+spring:
+  profiles:
+    active: dev
+    include: common,config-dev
+#    active: druid-jzzx
+#    include: common,config-druid-jzzx
+#    active: druid-hdt
+#    include: common,config-druid-
+#    active: druid-sxjz
+#    include: common,config-druid-sxjz

+ 2 - 0
fs-qw-api-msg/src/main/resources/banner.txt

@@ -0,0 +1,2 @@
+Application Version: ${fs.version}
+Spring Boot Version: ${spring-boot.version}

+ 37 - 0
fs-qw-api-msg/src/main/resources/i18n/messages.properties

@@ -0,0 +1,37 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 0 - 0
fs-qw-api-msg/src/main/resources/jniLibs/WeWorkFinanceSdk.dll


+ 0 - 0
fs-qw-api-msg/src/main/resources/jniLibs/libcrypto-3-x64.dll


+ 0 - 0
fs-qw-api-msg/src/main/resources/jniLibs/libcurl-x64.dll


+ 93 - 0
fs-qw-api-msg/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/fs-qw-api-msg/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.fs" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 19 - 0
fs-qw-api-msg/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+
+	<settings>
+		<setting name="cacheEnabled"             value="true" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+
+	<typeHandlers>
+		<typeHandler handler="com.fs.framework.config.ArrayStringTypeHandler"/>
+	</typeHandlers>
+
+</configuration>

+ 28 - 0
fs-qw-api-msg/src/main/resources/privatekey.pem

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9hPVCMTesjh4Q
+GjbC/x07fBP053/FtIUCvpxrY72TJhfIh5pCNuTtclARNRxINgLUZOoqAWbbKasK
+ZUowFcQh6UqGBGGDekS6a7tYSzcqKWdbKrlOsbeHBvHPBpEjhU/0pEqL5hMYnUg4
+DoJFmWZfEEo+KAAGA+vASxXtp93p6RLedkO4QZ6Lj7w1CqpAMsI6s1FWyc4WA9Hw
+I0/yxEP+vZofDgv/akPjih7/+4JBYYrHHdIZ63EDXIwFf0NCWfvy3w8pXrKGOp2X
+6vCBs8SzNvYJsAPPDUe+3zxQs3XsT1GajCmjupgkkfDYInF+gLqLpa5blznp+ywT
+Mw5PpoRnAgMBAAECggEAOTMb4vJ/3zC1mh6PUKM263EzhMMqKT2mt4FYLIFaAkOJ
+AhkLUdjHl3Lc/5SMCK9kgz5B/pOVu6gD24terMyk+aczuucvl0Mb4zgIv0lUSQFe
+FIH8k+fHgJVz/gv5WkMuMqQza63xLYiq92BJqT/3SdHc1o20FByy1sLSx3vW2nXd
+KJsMjyfUkp042I29ywNzr7IDvza/P60hw7gsgge/uUVR4eEpWgitkhozGCuHrb8S
+YzWSy9WvRiJqVn2w8G5UJ3Oj/uziB2of7cYEMDRqWYe/6vZCSif4Hp1RVTQrvN3H
+xvilGc6ib+D3lyubykvvRXsEyMU2eOrtEL1HBeuVYQKBgQDeXN56pi+9PEXpa8OX
+4ltp6l3UF4ZCNL1O5LVdesL4IcLGYhcebfFsJxFddmi4up2bwCelRT2VO83+OvPr
++b2PJ1BHTcK9cJ8/a9VUVVUrNGfzBmc4GdT/xAYEksudhv46XozqBjljYk041myn
+Rw2uELHtCG0eRBwXRovDaseQ4wKBgQDaMDVoizSRtVrCTPIWclNmxL3P2vRBZ3T7
+cwv/Em4X0lqCDmkJsIHoUXeaAbJD11W2+60nevTYknimNvL9pKqnbUnHdlY2J57l
+jPi/wxAIQb7KCATbpcNodSlBBxaUuDtW2Wwrpvtg/T3XHkwXEdEKs6lvympdG9X+
+9QDbV1nprQKBgBGFZ9JqPeC/WxLldugE4gcafII1yFJ3LWOGCfRvs+UxY9OV7xRq
+vVc0TN38cov7yGd5YKZZKl0T9UtPAeFaMGGTS3QS4IFk2md467pVbvyUiIxRuNhm
+0K3GmGLL3ldvR6D5yki2bNcz67nKy4zywFUy45I108NKwpvi5yhixY7ZAoGAZr5e
+2Az9Q0QAKhDryvApH/KzY0a60HW0W5gQZRu7phITNr0sbzij489BH89+r32eSl13
+a2j2FWnb0IF9qT06cz7+iCPMcxHzPCtHE536VbkH68Ec8IW9X9oUBCAfXQboQnx9
++IxYQZLe5QbKU+663mw99Ht/3Lp36ehbb+5RU70CgYEAoBoi60nBccIFjbqtRK2p
+9XNwTKUmFN/5GnONzv9wlnLRLo3WpBAfyUp6qHFmTzebKjj1nMwj1UHyvj3AWQGn
+31E4HzRzJvRyJcWIklfSbA3hgHqPxtC0jCgUAaKHFDzDhMDKXQOJpLHZszHMnGUK
+rcNWbTLtL+upW1BgZJSDGS0=
+-----END PRIVATE KEY-----

+ 4 - 2
fs-qw-api/src/main/resources/application.yml

@@ -5,5 +5,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: dev
-    include: common,config-dev
+#    active: dev
+#    include: common,config-dev
+    active: druid-hdt
+    include: common,config-druid-hdt,mq-hdt

+ 1 - 1
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -1667,7 +1667,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                 finishLogsToUpdate.add(finishLog);
 
                 if (finishTemp == null) {
-                    log.error("完课模板不存在: " + finishLog.getCompanyUserId() + ", " + finishLog.getVideoId());
+//                    log.error("完课模板不存在: " + finishLog.getCompanyUserId() + ", " + finishLog.getVideoId());
                     continue;
                 }
 

+ 58 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseAnswerReward.java

@@ -0,0 +1,58 @@
+package com.fs.course.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class FsCourseAnswerReward {
+
+    /**
+     * 用户当前积分
+     */
+    private Integer currentPoints;
+
+    /**
+     * 奖励兑换进度(百分比)
+     */
+    private Integer exchangeProgress;
+
+    /**
+     * 可兑换的商品列表
+     */
+    private List<RewardProduct> products;
+
+    /**
+     * 本次获得的芳华币数量
+     */
+    private Integer availableCoins;
+
+    @Data
+    public static class RewardProduct {
+        /**
+         * 商品ID
+         */
+        private Long productId;
+
+        /**
+         * 商品名称
+         */
+        private String productName;
+
+        /**
+         * 所需积分
+         */
+        private Integer requiredPoints;
+
+        /**
+         * 商品图片URL
+         */
+        private String imageUrl;
+
+        /**
+         * 商品图片URL
+         */
+        private BigDecimal otPrice;
+    }
+}

+ 25 - 10
fs-service/src/main/java/com/fs/course/dto/FsCourseQuestionBankImportDTO.java

@@ -6,21 +6,36 @@ import lombok.Data;
 @Data
 public class FsCourseQuestionBankImportDTO {
 
-    @Excel(name = "标题")
+    @Excel(name = "标题(必填)")
     private String title;
 
-    @Excel(name = "问题类别")
-    private String questionType;
+    @Excel(name = "序号(必填)")
+    private Long sort;
 
-    @Excel(name = "题目类型: 单选 多选")
+    @Excel(name = "类别(必填)")
     private String type;
 
-    @Excel(name = "选项: 多个用 | 隔开")
-    private String question;
+    @Excel(name = "题目类别(非必填)")
+    private String questionType;
 
-    @Excel(name = "答案: 多个用 | 隔开")
-    private String answer;
+    @Excel(name = "题目子类别(非必填)")
+    private String questionSubTyp;
+
+    @Excel(name = "状态(非必填)")
+    private String status;
+
+    @Excel(name = "选项A(必填)")
+    private String questionA;
 
-    @Excel(name = "序号")
-    private long sort;
+    @Excel(name = "选项B(必填)")
+    private String questionB;
+
+    @Excel(name = "选项C(必填)")
+    private String questionC;
+
+    @Excel(name = "选项D(非必填)")
+    private String questionD;
+
+    @Excel(name = "答案(必填)")
+    private String answer;
 }

+ 1 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseQuestionBankMapper.java

@@ -73,4 +73,5 @@ public interface FsCourseQuestionBankMapper
     public List<FsCourseQuestionBank> selectFsCourseQuestionBankByIdVO(@Param("list") String[] questionBankId);
 
 
+    int insertFsCourseQuestionBankBatch(@Param("list") List<FsCourseQuestionBank> fsCourseQuestionBank);
 }

+ 16 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCategoryMapper.java

@@ -2,10 +2,12 @@ package com.fs.course.mapper;
 
 import com.fs.course.domain.FsUserCourseCategory;
 import com.fs.his.vo.OptionsVO;
+import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 课堂分类Mapper接口
@@ -79,4 +81,18 @@ public interface FsUserCourseCategoryMapper
      */
     @Select("select * from fs_user_course_category where cate_name = #{name} limit 1")
     FsUserCourseCategory selectFsUserCourseCategoryByName(@Param("name")String name);
+
+    /**
+     * 查询所有分类
+     */
+    @Select("select cate_id as cateId, cate_name as cateName, pid from fs_user_course_category where is_del=0")
+    @MapKey("cateName")
+    Map<String,FsUserCourseCategory> queryAllCategoryData();
+
+    /**
+     * 查询所有分类
+     */
+    @Select("select cate_id as cateId,cate_name as cateName from fs_user_course_category where is_del = 0")
+    @MapKey("cateId")
+    Map<Long, FsUserCourseCategory> queryAllIdKeyCategoryData();
 }

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

@@ -81,4 +81,11 @@ public interface IFsCourseQuestionBankService
      * @return  list
      */
     List<FsCourseQuestionBank> selectFsCourseQuestionBankByIds(List<Long> ids);
+
+    /**
+     * 题目导出
+     * @param fsCourseQuestionBank  参数
+     * @return  list
+     */
+    List<FsCourseQuestionBankImportDTO> exportData(FsCourseQuestionBank fsCourseQuestionBank);
 }

+ 324 - 84
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -20,12 +20,15 @@ import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsStorePaymentService;
 import com.fs.system.service.ISysConfigService;
+import lombok.Getter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.validation.constraints.Size;
 import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -247,107 +250,289 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
             throw new ServiceException("导入数据不能为空");
         }
 
-        int successNum = 0;
-        int failureNum = 0;
-        StringBuilder importSuccessMsg = new StringBuilder();
-        StringBuilder importErrorMsg = new StringBuilder();
-        StringBuilder importMsg = new StringBuilder();
+        ImportResult result = new ImportResult();
+        List<FsCourseQuestionBank> importData = new ArrayList<>();
+        Map<String, FsUserCourseCategory> categoryData = courseCategoryMapper.queryAllCategoryData();
 
         for (FsCourseQuestionBankImportDTO importDTO : list) {
             try {
-                String title = importDTO.getTitle();
-                String type = importDTO.getType();
-                String questionType = importDTO.getQuestionType();
-                String question = importDTO.getQuestion();
-                String answer = importDTO.getAnswer();
-
-                if (StringUtils.isBlank(title) || StringUtils.isBlank(type) || StringUtils.isBlank(question) || StringUtils.isBlank(answer)) {
-                    String msg = "<br/>" + failureNum + "、题目 " + title + " 导入失败:信息不完整";
-                    importErrorMsg.append(msg);
-                    failureNum++;
+                // 数据验证
+                ValidationResult validation = validateImportData(importDTO);
+                if (!validation.isValid()) {
+                    result.addFailure(importDTO.getTitle(), validation.getErrorMessage());
                     continue;
                 }
 
-                if (!type.contains("单") && !type.contains("多")) {
-                    String msg = "<br/>" + failureNum + "、题目 " + title + " 导入失败:题目类型不正确";
-                    importErrorMsg.append(msg);
-                    failureNum++;
-                    continue;
-                }
+                // 构建题目对象
+                FsCourseQuestionBank questionBank = buildQuestionBank(importDTO, categoryData, nickName);
+                importData.add(questionBank);
+                result.addSuccess(importDTO.getTitle());
 
-                // 判断答案是否在选项里
-                String[] questions = question.split("\\|");
-                String[] answers = answer.split("\\|");
+            } catch (Exception e) {
+                result.addFailure(importDTO.getTitle(), "导入异常: " + e.getMessage());
+            }
+        }
 
-                if (type.contains("多") && answers.length < 2) {
-                    String msg = "<br/>" + failureNum + "、题目 " + title + " 导入失败:多选题答案不能少于2个";
-                    importErrorMsg.append(msg);
-                    failureNum++;
-                    continue;
-                }
+        // 批量保存
+        if (!importData.isEmpty()) {
+            fsCourseQuestionBankMapper.insertFsCourseQuestionBankBatch(importData);
+        }
 
-                List<String> optionsList = Arrays.asList(questions);
-                boolean allAnswersInOptions = true;
+        return result.buildResultMessage();
+    }
 
-                // 遍历每一个正确答案文本
-                for (String correctAnswer : answers) {
-                    // 检查当前正确答案文本是否存在于选项列表中
-                    if (!optionsList.contains(correctAnswer.trim())) {
-                        allAnswersInOptions = false; // 发现一个不在选项里的答案
-                        break;
-                    }
-                }
+    /**
+     * 验证导入数据
+     */
+    private ValidationResult validateImportData(FsCourseQuestionBankImportDTO importDTO) {
+        // 基础字段验证
+        if (StringUtils.isBlank(importDTO.getTitle())) {
+            return ValidationResult.fail("标题不能为空");
+        }
+        if (StringUtils.isBlank(importDTO.getType())) {
+            return ValidationResult.fail("类别不能为空");
+        }
+        if (Objects.isNull(importDTO.getSort())) {
+            return ValidationResult.fail("排序不能为空");
+        }
+        if (StringUtils.isBlank(importDTO.getAnswer())) {
+            return ValidationResult.fail("答案不能为空");
+        }
 
-                if (!allAnswersInOptions) {
-                    String msg = "<br/>" + failureNum + "、题目 " + title + " 导入失败:答案不在选项中";
-                    importErrorMsg.append(msg);
-                    failureNum++;
-                    continue;
-                }
+        // 选项验证
+        ValidationResult optionValidation = validateOptions(importDTO);
+        if (!optionValidation.isValid()) {
+            return optionValidation;
+        }
 
-                JSONArray questionArray = new JSONArray();
-                optionsList = Arrays.asList(answers);
-                for (int i = 0; i < questions.length; i++) {
-                    JSONObject optionObj = new JSONObject();
-                    optionObj.put("name", questions[i]);
-                    optionObj.put("isAnswer", optionsList.contains(questions[i].trim()) ? 1 : 0);
-                    optionObj.put("indexId", i);
-                    questionArray.add(optionObj);
-                }
+        // 题目类型验证
+        if (!isValidQuestionType(importDTO.getType())) {
+            return ValidationResult.fail("题目类型不正确,仅支持单选题和多选题");
+        }
 
-                // 分类
-                Long questionTypeId = null;
-                FsUserCourseCategory category = courseCategoryMapper.selectFsUserCourseCategoryByName(questionType);
-                if (Objects.nonNull(category)) {
-                    questionTypeId = category.getCateId();
-                }
+        // 答案验证
+        return validateAnswers(importDTO);
+    }
 
-                FsCourseQuestionBank questionBank = new FsCourseQuestionBank();
-                questionBank.setTitle(title);
-                questionBank.setType(type.contains("单") ? 1L : 2L);
-                questionBank.setQuestionType(questionTypeId);
-                questionBank.setStatus(1L);
-                questionBank.setSort(importDTO.getSort());
-                questionBank.setQuestion(JSON.toJSONString(questionArray));
-                questionBank.setAnswer(answers.length > 1 ? JSON.toJSONString(answers) : answer);
-                questionBank.setCreateTime(new Date());
-                questionBank.setCreateBy(nickName);
-                fsCourseQuestionBankMapper.insertFsCourseQuestionBank(questionBank);
-
-                importSuccessMsg.append("<br/>").append(successNum).append("、题目 ").append(title).append(" 导入成功");
-                successNum++;
-            } catch (Exception e) {
-                String msg = "<br/>" + failureNum + "、题目 " + importDTO.getTitle() + " 导入异常:";
-                importErrorMsg.append(msg).append(e.getMessage());
-                failureNum++;
+    /**
+     * 验证选项
+     */
+    private ValidationResult validateOptions(FsCourseQuestionBankImportDTO importDTO) {
+        if (StringUtils.isBlank(importDTO.getQuestionA()) ||
+                StringUtils.isBlank(importDTO.getQuestionB()) ||
+                StringUtils.isBlank(importDTO.getQuestionC())) {
+            return ValidationResult.fail("选项A、B、C不能为空");
+        }
+        return ValidationResult.success();
+    }
+
+    /**
+     * 验证题目类型
+     */
+    private boolean isValidQuestionType(String type) {
+        return type.contains("单") || type.contains("多");
+    }
+
+    /**
+     * 验证答案
+     */
+    private ValidationResult validateAnswers(FsCourseQuestionBankImportDTO importDTO) {
+        String[] answers = importDTO.getAnswer().trim().toUpperCase().split("");
+
+        // 多选题答案数量验证
+        if (importDTO.getType().contains("多") && answers.length < 2) {
+            return ValidationResult.fail("多选题答案不能少于2个");
+        }
+
+        // 答案选项验证
+        List<String> validOptions = getValidOptions(importDTO);
+        for (String answer : answers) {
+            if (!validOptions.contains(answer.trim())) {
+                return ValidationResult.fail("答案不在选项中");
+            }
+        }
+
+        return ValidationResult.success();
+    }
+
+    /**
+     * 获取有效选项列表
+     */
+    private List<String> getValidOptions(FsCourseQuestionBankImportDTO importDTO) {
+        List<String> options = new ArrayList<>(Arrays.asList("A", "B", "C"));
+        if (StringUtils.isNotBlank(importDTO.getQuestionD())) {
+            options.add("D");
+        }
+        return options;
+    }
+
+    /**
+     * 构建题目对象
+     */
+    private FsCourseQuestionBank buildQuestionBank(FsCourseQuestionBankImportDTO importDTO,
+                                                   Map<String, FsUserCourseCategory> categoryData,
+                                                   String nickName) {
+        FsCourseQuestionBank questionBank = new FsCourseQuestionBank();
+
+        // 基础信息
+        questionBank.setTitle(importDTO.getTitle());
+        questionBank.setType(getQuestionTypeCode(importDTO.getType()));
+        questionBank.setSort(importDTO.getSort());
+        questionBank.setStatus(parseStatus(importDTO.getStatus()));
+        questionBank.setCreateTime(new Date());
+        questionBank.setCreateBy(nickName);
+
+        // 分类信息
+        setQuestionCategories(questionBank, importDTO, categoryData);
+
+        // 选项和答案
+        questionBank.setQuestion(buildQuestionOptions(importDTO));
+        questionBank.setAnswer(buildAnswer(importDTO));
+
+        return questionBank;
+    }
+
+    /**
+     * 获取题目类型编码
+     */
+    private Long getQuestionTypeCode(String type) {
+        return type.contains("单") ? 1L : 2L;
+    }
+
+    /**
+     * 解析状态
+     */
+    private Long parseStatus(String statusStr) {
+        if (StringUtils.isBlank(statusStr)) {
+            return 1L;
+        }
+        try {
+            return statusStr.contains("正常") ? 1L : 0L;
+        } catch (Exception e) {
+            return 1L;
+        }
+    }
+
+    /**
+     * 设置题目分类
+     */
+    private void setQuestionCategories(FsCourseQuestionBank questionBank,
+                                       FsCourseQuestionBankImportDTO importDTO,
+                                       Map<String, FsUserCourseCategory> categoryData) {
+        if (StringUtils.isBlank(importDTO.getQuestionType())) {
+            return;
+        }
+
+        FsUserCourseCategory category = categoryData.get(importDTO.getQuestionType().trim());
+        if (category != null) {
+            questionBank.setQuestionType(category.getCateId());
+
+            // 子分类
+            if (StringUtils.isNotBlank(importDTO.getQuestionSubTyp())) {
+                FsUserCourseCategory subCategory = categoryData.get(importDTO.getQuestionSubTyp().trim());
+                if (subCategory != null && Objects.equals(subCategory.getPid(), category.getCateId())) {
+                    questionBank.setQuestionSubType(subCategory.getCateId());
+                }
             }
         }
+    }
+
+    /**
+     * 构建选项JSON
+     */
+    private String buildQuestionOptions(FsCourseQuestionBankImportDTO importDTO) {
+        JSONArray questionArray = new JSONArray();
+        String[] answers = importDTO.getAnswer().trim().toUpperCase().split("");
+        List<String> answerList = Arrays.asList(answers);
+
+        // 构建选项
+        addOption(questionArray, importDTO.getQuestionA(), answerList.contains("A"), 0);
+        addOption(questionArray, importDTO.getQuestionB(), answerList.contains("B"), 1);
+        addOption(questionArray, importDTO.getQuestionC(), answerList.contains("C"), 2);
+
+        if (StringUtils.isNotBlank(importDTO.getQuestionD())) {
+            addOption(questionArray, importDTO.getQuestionD(), answerList.contains("D"), 3);
+        }
+
+        return JSON.toJSONString(questionArray);
+    }
+
+    /**
+     * 添加选项
+     */
+    private void addOption(JSONArray questionArray, String optionText,
+                           boolean isAnswer, int indexId) {
+        JSONObject option = new JSONObject();
+        option.put("name", optionText.trim());
+        option.put("isAnswer", isAnswer ? 1 : 0);
+        option.put("indexId", indexId);
+        questionArray.add(option);
+    }
+
+    /**
+     * 构建答案
+     */
+    private String buildAnswer(FsCourseQuestionBankImportDTO importDTO) {
+        String[] answers = importDTO.getAnswer().trim().toUpperCase().split("");
+        List<String> answerList = Arrays.asList(answers);
+
+        Map<String, String> questionMap = new HashMap<>();
+        questionMap.put("A", importDTO.getQuestionA().trim());
+        questionMap.put("B", importDTO.getQuestionB().trim());
+        questionMap.put("C", importDTO.getQuestionC().trim());
+        questionMap.put("D", importDTO.getQuestionD().trim());
+
+        List<String> selectedAnswers = answerList.stream()
+                .map(questionMap::get)
+                .collect(Collectors.toList());
+
+        if (answerList.size() == 1) {
+            return selectedAnswers.get(0);
+        }
+
+        return JSON.toJSONString(selectedAnswers);
+    }
+
+    // 辅助类
+    @Getter
+    private static class ValidationResult {
+        private final boolean valid;
+        private final String errorMessage;
+
+        private ValidationResult(boolean valid, String errorMessage) {
+            this.valid = valid;
+            this.errorMessage = errorMessage;
+        }
+
+        public static ValidationResult success() {
+            return new ValidationResult(true, null);
+        }
+
+        public static ValidationResult fail(String message) {
+            return new ValidationResult(false, message);
+        }
 
-        // 在所有导入处理完成后,构建最终的导入结果消息
-        importMsg.insert(0, "导入完成!成功" + successNum + " 条,失败" + failureNum + "条。");
-        importMsg.append(importErrorMsg);
-        importMsg.append(importSuccessMsg);
-        return importMsg.toString();
+    }
+
+    private static class ImportResult {
+        private int successNum = 0;
+        private int failureNum = 0;
+        private final StringBuilder successMsg = new StringBuilder();
+        private final StringBuilder errorMsg = new StringBuilder();
+
+        public void addSuccess(String title) {
+            successNum++;
+            successMsg.append("<br/>").append(successNum).append("、题目 ").append(title).append(" 导入成功");
+        }
+
+        public void addFailure(String title, String error) {
+            failureNum++;
+            errorMsg.append("<br/>").append(failureNum).append("、题目 ").append(title).append(" 导入失败:").append(error);
+        }
+
+        public String buildResultMessage() {
+            return "导入完成!成功" + successNum + " 条,失败" + failureNum + "条。" + errorMsg + successMsg;
+        }
     }
 
     /**
@@ -360,6 +545,61 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         return fsCourseQuestionBankMapper.selectFsCourseQuestionBankByIds(ids);
     }
 
+    /**
+     * 题目导出
+     * @param fsCourseQuestionBank  参数
+     * @return  list
+     */
+    @Override
+    public List<FsCourseQuestionBankImportDTO> exportData(FsCourseQuestionBank fsCourseQuestionBank) {
+        List<FsCourseQuestionBank> fsCourseQuestionBanks = fsCourseQuestionBankMapper.selectFsCourseQuestionBankList(fsCourseQuestionBank);
+        Map<Long, FsUserCourseCategory> categoryMap = courseCategoryMapper.queryAllIdKeyCategoryData();
+
+        // 辅助方法:根据ID从Map获取分类名称
+        Function<Long, String> getCategoryName = id -> Optional.ofNullable(categoryMap.get(id))
+                .map(FsUserCourseCategory::getCateName)
+                .orElse(null);
+
+        Map<Integer, BiConsumer<FsCourseQuestionBankImportDTO, String>> questionSetters = new HashMap<>();
+        questionSetters.put(0, FsCourseQuestionBankImportDTO::setQuestionA);
+        questionSetters.put(1, FsCourseQuestionBankImportDTO::setQuestionB);
+        questionSetters.put(2, FsCourseQuestionBankImportDTO::setQuestionC);
+        questionSetters.put(3, FsCourseQuestionBankImportDTO::setQuestionD);
+
+        Map<Integer, String> answerLettersMap = new HashMap<>();
+        answerLettersMap.put(0, "A");
+        answerLettersMap.put(1, "B");
+        answerLettersMap.put(2, "C");
+        answerLettersMap.put(3, "D");
+
+        return fsCourseQuestionBanks.stream().map(q -> {
+            FsCourseQuestionBankImportDTO dto = new FsCourseQuestionBankImportDTO();
+            dto.setTitle(q.getTitle());
+            dto.setSort(q.getSort());
+            dto.setType(q.getType() == 1 ? "单选" : "多选");
+            Optional.ofNullable(q.getQuestionType()).map(getCategoryName).ifPresent(dto::setQuestionType);
+            Optional.ofNullable(q.getQuestionSubType()).map(getCategoryName).ifPresent(dto::setQuestionSubTyp);
+            dto.setStatus(q.getStatus() == 1 ? "正常" : "停用");
+
+            StringBuilder answersBuilder = new StringBuilder();
+            JSONArray array = JSON.parseArray(q.getQuestion());
+            array.forEach(jsonObject -> {
+                JSONObject json = (JSONObject) jsonObject;
+                int index = json.getInteger("indexId");
+                String name = json.getString("name");
+                int isAnswer = json.getInteger("isAnswer");
+
+                questionSetters.get(index).accept(dto, name);
+                if (isAnswer == 1) {
+                    answersBuilder.append(answerLettersMap.get(index));
+                }
+
+            });
+            dto.setAnswer(answersBuilder .toString());
+            return dto;
+        }).collect(Collectors.toList());
+    }
+
 
     public static String[] convertStringToArray(String inputString) {
         String cleanString = inputString.replaceAll("[\\[\\]\"\\s]", "");

+ 24 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodDaysServiceImpl.java

@@ -4,16 +4,19 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.common.utils.date.TimeTypeEnum;
 import com.fs.course.domain.FsUserCoursePeriod;
 import com.fs.course.domain.FsUserCoursePeriodDays;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsUserCoursePeriodDaysMapper;
 import com.fs.course.mapper.FsUserCoursePeriodMapper;
 import com.fs.course.param.CompanyRedPacketParam;
 import com.fs.course.param.CourseAnalysisParam;
 import com.fs.course.param.PeriodCountParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsPeriodCountVO;
 import com.fs.course.vo.PeriodRedPacketVO;
 import com.fs.course.vo.UpdateCourseTimeVo;
@@ -54,6 +57,7 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
     private final FsUserMapper fsUserMapper;
 
     private final FsUserCoursePeriodDaysMapper fsUserCoursePeriodDaysMapper;
+    private final IFsUserCourseVideoService fsUserCourseVideoService;
 
     /**
      * 查询营期课程
@@ -143,15 +147,32 @@ public class FsUserCoursePeriodDaysServiceImpl extends ServiceImpl<FsUserCourseP
         if(dayList.size() + entity.getVideoIds().size() > days) return R.error("课程不能超过营期范围");
         if(dayList.stream().anyMatch(e -> entity.getVideoIds().contains(e.getVideoId()))) return R.error("不能添加相同章节");
         AtomicInteger i = new AtomicInteger(0);
+        FsUserCourseVideo fsUserCourseVideo = new FsUserCourseVideo();
+        fsUserCourseVideo.setCourseId(entity.getCourseId());
+        List<FsUserCourseVideo> videoList = fsUserCourseVideoService.selectFsUserCourseVideoListByCourseId(fsUserCourseVideo);
+        Map<Long, FsUserCourseVideo> videoMap = PubFun.listToMapByGroupObject(videoList, FsUserCourseVideo::getVideoId);
         List<FsUserCoursePeriodDays> collect = entity.getVideoIds().stream().map(e -> {
+            FsUserCourseVideo video = videoMap.get(e);
             FsUserCoursePeriodDays day = new FsUserCoursePeriodDays();
             day.setPeriodId(entity.getPeriodId());
             day.setLesson(dayList.size() + i.getAndIncrement());
             day.setDayDate(period.getPeriodStartingTime().plusDays(day.getLesson()));
             day.setCourseId(entity.getCourseId());
-            day.setStartDateTime(LocalDateTime.of(day.getDayDate(), entity.getStartTime()));
-            day.setEndDateTime(LocalDateTime.of(day.getDayDate(), entity.getEndTime1()));
-            day.setLastJoinTime(LocalDateTime.of(day.getDayDate(), entity.getJoinTime()));
+            if(entity.getStartTime() != null){
+                day.setStartDateTime(LocalDateTime.of(day.getDayDate(), entity.getStartTime()));
+            }else if(video.getViewStartTime() != null){
+                day.setStartDateTime(LocalDateTime.of(day.getDayDate(), video.getViewStartTime()));
+            }
+            if(entity.getEndTime1() != null){
+                day.setEndDateTime(LocalDateTime.of(day.getDayDate(), entity.getEndTime1()));
+            }else if(video.getViewEndTime() != null){
+                day.setEndDateTime(LocalDateTime.of(day.getDayDate(), video.getViewEndTime()));
+            }
+            if(entity.getJoinTime() != null){
+                day.setLastJoinTime(LocalDateTime.of(day.getDayDate(), entity.getJoinTime()));
+            }else if(video.getLastJoinTime() != null){
+                day.setLastJoinTime(LocalDateTime.of(day.getDayDate(), video.getLastJoinTime()));
+            }
             day.setVideoId(e);
             day.setCreateTime(new Date());
             // 默认开启今天及以后的两天

+ 21 - 9
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -382,18 +382,30 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
     }
 
     private R handleRoom(FsUserCourseVideoAddKfUParam param,FsUser user) {
-        if (user.getQwExtId()==null){
+        //查询客户列表
+        List<QwExternalContact> contacts = qwExternalContactMapper.selectQwExternalContactListVOByfsUserId(user.getUserId());
+        if (contacts.isEmpty()){
             return R.error("未注册");
         }
-        param.setQwExternalId(user.getQwExtId());
-        //查询是否有添加客服
-        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(param.getQwExternalId());
-        if (externalContact==null){
-            return R.error("客户不存在!");
-        }
-        if (!externalContact.getQwUserId().equals(param.getUserId())){
-            return R.error("无权限观看,添加群主非本群主");
+        //找出对应销售匹配的客户
+        QwExternalContact matchedContact = contacts.stream()
+                .filter(contact -> contact.getQwUserId().equals(Long.parseLong(param.getQwUserId())))
+                .findFirst()
+                .orElse(null);
+
+        if (matchedContact==null){
+            return R.error("无权限观看,添加本群主");
         }
+
+        param.setQwExternalId(matchedContact.getId());
+//        //查询是否有添加客服
+//        QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(param.getQwExternalId());
+//        if (externalContact==null){
+//            return R.error("客户不存在!");
+//        }
+//        if (!externalContact.getQwUserId().equals(param.getUserId())){
+//            return R.error("无权限观看,添加群主非本群主");
+//        }
         FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideoByExt(param.getQwExternalId(), param.getVideoId(),param.getQwUserId());
         if (log==null){
             createWatchLog(param);

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

@@ -0,0 +1,22 @@
+package com.fs.fastGpt.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.wxwork.dto.WxWorkResponseDTO;
+
+public interface AiHookService {
+    /** 发送ai提醒 **/
+    R AiRemind();
+
+    /** ai回复**/
+    R qwHookNotifyAiReply(Long qwUserID, Long sender,String count,String uid,Integer type);
+
+    /** 转人工 **/
+    void artificial(QwHookVO vo);
+
+    R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid);
+
+    void expireAiMsg();
+
+    WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, String authKey, String fileName, Integer fileSize, Long serverId);
+}

+ 21 - 0
fs-service/src/main/java/com/fs/fastGpt/service/AiNewService.java

@@ -0,0 +1,21 @@
+package com.fs.fastGpt.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.qw.domain.QwMessageGather;
+import com.fs.qwHookApi.vo.QwHookVO;
+
+public interface AiNewService {
+    /** 发送ai提醒 **/
+    void AiRemind();
+
+    /** ai回复**/
+    R qwHookNotifyAiReply(QwMessageGather qwMessageGather,String corpId);
+
+    /** 转人工 **/
+    void artificial(QwHookVO vo);
+
+    R qwHookNotifyAddMsg(QwMessageGather qwMessageGather,String corpId);
+
+    void expireAiMsg();
+
+}

+ 1455 - 0
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -0,0 +1,1455 @@
+package com.fs.fastGpt.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsCourseLinkService;
+import com.fs.course.vo.FsCourseWatchLogVO;
+import com.fs.fastGpt.config.ModeConfig;
+import com.fs.fastGpt.domain.*;
+import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
+import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
+import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
+import com.fs.fastGpt.param.SendAIParam;
+import com.fs.fastGpt.service.AiHookService;
+import com.fs.fastGpt.service.IFastGptChatMsgService;
+import com.fs.fastGpt.service.IFastGptRoleService;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
+import com.fs.fastgptApi.service.ChatService;
+import com.fs.fastgptApi.service.Impl.AudioServiceImpl;
+import com.fs.fastgptApi.util.AiImgUtil;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwExternalContactInfoMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.*;
+import com.fs.qwApi.param.QwSendMsgParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.wxwork.dto.*;
+import com.fs.wxwork.service.WxWorkService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.lang.reflect.Field;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class AiHookServiceImpl implements AiHookService {
+    @Autowired
+    private FastGptChatSessionMapper fastGptChatSessionMapper;
+    @Autowired
+    private ChatService chatService;
+    @Autowired
+    private QwUserMapper qwUserMapper;
+    @Autowired
+    private IFastGptRoleService roleService;
+    @Autowired
+    private QwExternalContactInfoMapper qwExternalContactInfoMapper;
+    @Autowired
+    private IFastGptChatMsgService fastGptChatMsgService;
+    @Autowired
+    private IQwContactWayService qwContactWayService;
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    FsCourseWatchLogMapper   watchLogMapper;
+    @Autowired
+    IQwJsApiService qwGetJsapiTicketService;
+    @Autowired
+    private IFsCourseLinkService iFsCourseLinkService;
+    @Autowired
+    FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    private IQwUserService qwUserService;
+    @Autowired
+    IQwWorkTaskService qwWorkTaskService;
+    @Autowired
+    AiImgUtil aiImgUtil;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private IQwMsgService qwMsgService;
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+    @Autowired
+    private FastGptChatReplaceWordsMapper fastGptChatReplaceWordsMapper;
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+    @Autowired
+    WxWorkService wxWorkService;
+
+//    @Autowired
+//    private IFastGptKeywordSendService fastGptKeywordSendService;
+
+    /** Ai半小时未回复提醒 **/
+    /**
+     *
+     * sender    像是fastgpt_chat_msg中的user_id字段
+     * receiver     qw_user表中的vid字段
+     * uuid         qw_user表中的uid字段
+     * type  fasgpt_chat_msg中的msg_type字段
+     * @return
+     */
+    @Override
+    public R AiRemind() {
+        List<FastGptChatSession> fastGptChatSessions = fastGptChatSessionMapper.selectFastGptChatSessionByRemind();
+        if(fastGptChatSessions != null && !fastGptChatSessions.isEmpty()){
+            for (FastGptChatSession fastGptChatSession : fastGptChatSessions) {
+                String qwContent = "用户未回复";
+                Long qwUserId = fastGptChatSession.getQwUserId();
+
+                if(fastGptChatSession.getUserId() != null){
+                    Long sender = Long.valueOf(fastGptChatSession.getUserId());
+                    QwUser user = qwUserMapper.selectQwUserById(qwUserId);
+                    String uid = user.getUid();
+                    //查询接收人
+                    if(user==null){
+                        System.out.println("查询接收人为空");
+                        return R.ok();
+                    }
+                    if(user.getFastGptRoleId()==null){
+                        System.out.println("未绑定角色");
+                        return R.ok();
+                    }
+                    Long serverId = user.getServerId();
+                    System.out.println("服务器id"+serverId);
+                    if (serverId == null) {
+                        System.out.println("服务id为空");
+                        return R.ok();
+                    }
+                    FastGptRole role=roleService.selectFastGptRoleTypeByRoleId(user.getFastGptRoleId());
+                    //没用ai角色跳过
+                    if(role==null){
+                        System.out.println("没用ai角色跳过");
+                        return R.ok();
+                    }
+                    String modeConfig=role.getModeConfigJson();
+                    //key不为空
+                    if(StringUtils.isEmpty(modeConfig)){
+                        System.out.println("没有aiKey");
+                        return R.ok();
+                    }
+                    ModeConfig config=JSONUtil.toBean(modeConfig,ModeConfig.class);
+                    if(StringUtils.isEmpty(config.getAPPKey())){
+                        System.out.println("没有aiKey");
+                        return R.ok();
+                    }
+
+                    QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactById(fastGptChatSession.getQwExtId());
+                    if (qwExternalContacts==null){
+                        System.out.println("没有外部联系人");
+                        return R.ok();
+                    }
+                    if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+
+                        //判断是否转人工
+                        if (fastGptChatSession.getIsArtificial()==1){
+                            System.out.println("转人工了");
+                            return R.ok();
+                        }
+
+                        //用户首次发送消息
+                        redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),1,5,TimeUnit.MINUTES);
+                        redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),qwContent,5,TimeUnit.MINUTES);
+                        System.out.println("等待");
+                        R r= sendAiMsg(1,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts);
+                        System.out.println(r);
+                        //完成对话 删除消息记录
+                        redisCache.deleteObject("reply:" + fastGptChatSession.getSessionId());
+                        redisCache.deleteObject("msg:" + fastGptChatSession.getSessionId());
+                        if(!r.get("code").equals(200)){
+                            log.info("ai报错转人工:"+role.getRoleId()+":"+qwExternalContacts.getName());
+                            notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai报错转人工",qwExternalContacts.getId());
+                            return R.ok();
+                        }
+
+                        ChatDetailTStreamFResult result=(ChatDetailTStreamFResult)r.get("data");
+                        //是否转人工
+                        if(result.getChoices().isEmpty()){
+                            return R.ok();
+                        }
+                        String contentKh = result.getChoices().get(0).getMessage().getContent();
+                        String content = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+                        //计算token
+                        List<ChatDetailTStreamFResult.ResponseNode> responseData = result.getResponseData();
+                        int token=0;
+                        for (ChatDetailTStreamFResult.ResponseNode responseDatum : responseData) {
+                            int tokens = responseDatum.getTokens();
+                            token+=tokens;
+                        }
+                        //存聊天记录
+                        addSaveAiMsg(2,2,contentKh,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),result.getUsage().prompt_tokens,result.getUsage().completion_tokens,token);
+                        if (!content.isEmpty()){
+                            getFastGptSession(qwExternalContacts,user);
+                            //从fastgpt_chat_artificial_words表中查询所有转人工文本
+//                            List<FastgptChatArtificialWords> chatArtificialWords = qwExternalContactMapper.selectChatGptChatArtificialWords();
+//                            List<String> collect = chatArtificialWords.stream().map(m -> m.getContent()).collect(Collectors.toList());
+//                            if (collect.stream().anyMatch(contentKh::contains)){
+//                                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 触发关键词",qwExternalContacts.getId());
+//                                return R.ok();
+//                            }
+                            if (result.isLongText()){
+                                //新增用户信息
+                                addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+                                sendAiMsg(content,sender,uid,serverId);
+
+                            }else {
+                                String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
+                                String nr = replace(sa);
+                                String[] split = nr.split("\n");
+                                if (split.length>6){
+                                    notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName(),"回复长度异常",qwExternalContacts.getId());
+                                    return R.ok();
+                                }
+                                List<String> countList = countString(content);
+                                //新增用户信息
+                                addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+                                for (String msg : countList) {
+                                    sendAiMsg(msg,sender,uid,serverId);
+                                    try {
+                                        Thread.sleep(10000);
+                                    } catch (InterruptedException e) {
+
+                                    }
+                                    Integer replyH = redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+                                    //用户正在发送消息 后面的消息不发了
+                                    if (replyH!=null&&replyH!=0){
+                                        return R.ok();
+                                    }
+                                }
+                            }
+                        }
+                        if (result.isArtificial()){
+                            notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai请求人工协助",qwExternalContacts.getId());
+                        }
+                    }
+                }
+            }
+        }
+        return R.ok();
+    }
+    /** Ai回复 **/
+    @Async
+    @Override
+    public R qwHookNotifyAiReply(Long qwUserId, Long sender,String qwContent,String uid,Integer type) {
+        if (qwContent==null||qwContent.isEmpty()){
+            return R.ok();
+        }
+        if (qwContent.trim().isEmpty()){
+            return R.ok();
+        }
+        if (qwContent.contains("我已经添加了你")){
+                return R.ok();
+        }
+        System.out.println(qwUserId);
+        QwUser user = qwUserMapper.selectQwUserById(qwUserId);
+        //查询接收人
+        if(user==null){
+            System.out.println("查询接收人为空");
+            return R.ok();
+        }
+        if(user.getFastGptRoleId()==null){
+            System.out.println("未绑定角色");
+            return R.ok();
+        }
+        Long serverId = user.getServerId();
+        System.out.println("服务器id"+serverId);
+        if (serverId == null) {
+            System.out.println("服务id为空");
+            return R.ok();
+        }
+        FastGptRole role=roleService.selectFastGptRoleTypeByRoleId(user.getFastGptRoleId());
+        //没用ai角色跳过
+        if(role==null){
+            System.out.println("没用ai角色跳过");
+            return R.ok();
+        }
+        String modeConfig=role.getModeConfigJson();
+        //key不为空
+        if(StringUtils.isEmpty(modeConfig)){
+            System.out.println("没有aiKey");
+            return R.ok();
+        }
+        ModeConfig config=JSONUtil.toBean(modeConfig,ModeConfig.class);
+        if(StringUtils.isEmpty(config.getAPPKey())){
+            System.out.println("没有aiKey");
+            return R.ok();
+        }
+        WxWorkVid2UserIdDTO wxWorkVid2UserIdDTO = new WxWorkVid2UserIdDTO();
+        wxWorkVid2UserIdDTO.setUser_id(Arrays.asList(sender));
+        wxWorkVid2UserIdDTO.setUuid(uid);
+        WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> WxWorkVid2UserIdRespDTO = wxWorkService.Vid2UserId(wxWorkVid2UserIdDTO,serverId);
+        List<WxWorkVid2UserIdRespDTO> data = WxWorkVid2UserIdRespDTO.getData();
+        if (data==null|| data.isEmpty()){
+
+            System.out.println("未获取到extId"+wxWorkVid2UserIdDTO);
+            return R.ok();
+        }
+        com.fs.wxwork.dto.WxWorkVid2UserIdRespDTO dto = data.get(0);
+
+        QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(dto.getOpenid(), user.getCorpId(),user.getQwUserId());
+        if (qwExternalContacts==null){
+            System.out.println("没有外部联系人");
+            return R.ok();
+        }
+        if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+            FastGptChatSession fastGptChatSession= getFastGptSession(qwExternalContacts,user,dto);
+            if(type == 104||type == 101){
+                String imageParse = aiImgUtil.getImageParse(qwContent);
+                qwContent="用户发送图片内容:"+"\""+imageParse+"\"";
+            }
+            String contentEmj = replaceWxEmo(qwContent);
+            if(!contentEmj.isEmpty()){
+                addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+            }else {
+                contentEmj ="用户发送表情:"+qwContent;
+                if (type==16){
+                    return R.ok();
+                }
+            }
+
+            //判断是否转人工
+            if (fastGptChatSession.getIsArtificial()==1){
+                System.out.println("转人工了");
+                return R.ok();
+            }
+            //获取是用户是否发送消息
+            Integer reply = redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+            Integer replyI=1;
+            //用户正在发送消息 不发
+            if (reply!=null&&reply!=0){
+                //更新用户发送消息次数
+                redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),reply+1,5, TimeUnit.MINUTES);
+                //获取用户之前发送的消息
+                String msg = redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+                if (!msg.isEmpty()){
+                    //更新用户发送消息内容
+                    redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),msg+","+contentEmj,5,TimeUnit.MINUTES);
+                }
+                //本次跳过
+                System.out.println("正在对话");
+                return R.ok();
+            }
+            //用户首次发送消息
+            redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),1,5,TimeUnit.MINUTES);
+            redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),contentEmj,5,TimeUnit.MINUTES);
+            System.out.println("等待");
+            R r= sendAiMsg(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts);
+            System.out.println(r);
+            //完成对话 删除消息记录
+            redisCache.deleteObject("reply:" + fastGptChatSession.getSessionId());
+            redisCache.deleteObject("msg:" + fastGptChatSession.getSessionId());
+            if(!r.get("code").equals(200)){
+                log.info("ai报错转人工:"+role.getRoleId()+":"+qwExternalContacts.getName());
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai报错转人工",qwExternalContacts.getId());
+                return R.ok();
+            }
+            ChatDetailTStreamFResult result=(ChatDetailTStreamFResult)r.get("data");
+            //是否转人工
+            if(result.getChoices().isEmpty()){
+                return R.ok();
+            }
+            String contentKh = result.getChoices().get(0).getMessage().getContent();
+            String content = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+            //计算token
+            List<ChatDetailTStreamFResult.ResponseNode> responseData = result.getResponseData();
+            int token=0;
+            for (ChatDetailTStreamFResult.ResponseNode responseDatum : responseData) {
+                int tokens = responseDatum.getTokens();
+                token+=tokens;
+            }
+            //存聊天记录
+            addSaveAiMsg(2,2,contentKh,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),result.getUsage().prompt_tokens,result.getUsage().completion_tokens,token);
+            if (!content.isEmpty()){
+                addSaveAiMsg(1,2,content,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+                //从fastgpt_chat_artificial_words表中查询所有转人工文本
+//                List<FastgptChatArtificialWords> chatArtificialWords = qwExternalContactMapper.selectChatGptChatArtificialWords();
+//                List<String> collect = chatArtificialWords.stream().map(m -> m.getContent()).collect(Collectors.toList());
+//                if (collect.stream().anyMatch(contentKh::contains)){
+//                    notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 触发关键词",qwExternalContacts.getId());
+//                    return R.ok();
+//                }
+                if (result.isLongText()){
+                    //新增用户信息
+                    addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+                    if (type==16){
+                        sendAiVoiceMsg(content,sender,uid,serverId,user);
+                    }else {
+                        sendAiMsg(content,sender,uid,serverId);
+                    }
+
+                }else {
+                    String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
+                    String nr = replace(sa);
+                    String[] split = nr.split("\n");
+                    if (split.length>6){
+                        notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName(),"回复长度异常",qwExternalContacts.getId());
+                        return R.ok();
+                    }
+                    List<String> countList = countString(content);
+                    //新增用户信息
+                    addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+                    for (String msg : countList) {
+                        if (type==16){
+                            sendAiVoiceMsg(msg,sender,uid,serverId,user);
+                        }else {
+                            sendAiMsg(msg,sender,uid,serverId);
+                        }
+                        try {
+                            Thread.sleep(10000);
+                        } catch (InterruptedException e) {
+
+                        }
+                        Integer replyH = redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+                        //用户正在发送消息 后面的消息不发了
+                        if (replyH!=null&&replyH!=0){
+                            return R.ok();
+                        }
+                    }
+                }
+            }
+
+            //发送短链
+            //根据模板发送图片
+//            Long roleId = role.getRoleId();
+//            List<FastGptKeywordSend> fastGptKeywordSends = fastGptKeywordSendService.selectFastGptKeywordSendByRoleId(roleId);
+//            if(fastGptKeywordSends != null && !fastGptKeywordSends.isEmpty()){
+//                for (FastGptKeywordSend fastGptKeywordSend : fastGptKeywordSends) {
+//                    if (qwContent.contains(fastGptKeywordSend.getKeyword())){
+//                        sendKeyWordMsg(fastGptKeywordSend,sender,uid,serverId);
+//                    }
+//                }
+//            }
+
+
+            if (result.isArtificial()){
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai请求人工协助",qwExternalContacts.getId());
+            }
+        }
+
+        return R.ok();
+    }
+
+    private void sendAiVoiceMsg(String content, Long sendId , String uuid,Long serverId,QwUser user) {
+        if (content == null || content.trim().isEmpty()){
+            System.out.println("输出为空格");
+            return;
+        }
+        content = voiceHomo(content);
+        System.out.println("过滤后的文字:"+content);
+        if (content.isEmpty()){
+            return;
+        }
+
+        WxwSilkVoceDTO silkVoice = wxWorkService.getSilkVoice(content, user.getCompanyUserId());
+        if (silkVoice == null){
+            return;
+        }
+        if (silkVoice.getCode()!=200){
+            return;
+        }
+        WxwSilkVoceDTO.Data data = silkVoice.getData();
+        WxwUploadCdnLinkFileDTO wxwUploadCdnLinkFileDTO = new WxwUploadCdnLinkFileDTO();
+        wxwUploadCdnLinkFileDTO.setUrl(data.getUrl());
+        wxwUploadCdnLinkFileDTO.setFilename(data.getUrl());
+        wxwUploadCdnLinkFileDTO.setUuid(uuid);
+        WxWorkResponseDTO<WxwUploadCdnLinkFileRespDTO> dto = wxWorkService.uploadCdnLinkFile(wxwUploadCdnLinkFileDTO, serverId);
+        WxwUploadCdnLinkFileRespDTO voice = dto.getData();
+        WxwSendCDNVoiceMsgDTO wxwSendCDNVoiceMsgDTO = new WxwSendCDNVoiceMsgDTO();
+        wxwSendCDNVoiceMsgDTO.setSend_userid(sendId);
+        System.out.println("语音size"+voice.getSize());
+        wxwSendCDNVoiceMsgDTO.setVoice_time(data.getDuration());
+        wxwSendCDNVoiceMsgDTO.setIsRoom(false);
+        wxwSendCDNVoiceMsgDTO.setCdnkey(voice.getCdn_key());
+        wxwSendCDNVoiceMsgDTO.setAeskey(voice.getAes_key());
+        wxwSendCDNVoiceMsgDTO.setMd5(voice.getMd5());
+        wxwSendCDNVoiceMsgDTO.setFileSize(voice.getSize());
+        wxwSendCDNVoiceMsgDTO.setUuid(uuid);
+        WxWorkResponseDTO<WxwSendCDNVoiceMsgRespDTO> wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO = wxWorkService.SendCDNVoiceMsg(wxwSendCDNVoiceMsgDTO, serverId);
+        System.out.println(wxwSendCDNVoiceMsgRespDTOWxWorkResponseDTO);
+
+    }
+
+    @Autowired
+    QwSopLogsMapper qwSopLogsMapper;
+    private void sendAiMsg(String content, Long sendId , String uuid,Long serverId) {
+        if (content == null || content.trim().isEmpty()){
+            System.out.println("输出为空格");
+            return;
+        }
+        WxWorkSendTextMsgDTO wxWorkSendTextMsgDTO = new WxWorkSendTextMsgDTO();
+        wxWorkSendTextMsgDTO.setSend_userid(sendId);
+        wxWorkSendTextMsgDTO.setUuid(uuid);
+        wxWorkSendTextMsgDTO.setContent(content);
+        wxWorkSendTextMsgDTO.setIsRoom(false);
+        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(wxWorkSendTextMsgDTO,serverId);
+        WxWorkSendTextMsgRespDTO data = wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getData();
+
+
+    }
+
+//    private void sendKeyWordMsg(FastGptKeywordSend fastGptKeywordSend, Long sendId , String uuid,Long serverId) {
+//        if (fastGptKeywordSend == null || fastGptKeywordSend.getContent().isEmpty()){
+//            System.out.println("输出为空格");
+//            return;
+//        }
+//
+//        WxWorkResponseDTO<WxwUploadCdnLinkImgRespDTO> dto = new WxWorkResponseDTO<>();
+//
+//        //1.上传cdn网络图片
+//        if(fastGptKeywordSend.getImgUrl() != null){
+//            WxwUploadCdnLinkImgDTO wxwUploadCdnLinkImgDTO = new WxwUploadCdnLinkImgDTO();
+//            wxwUploadCdnLinkImgDTO.setUuid(uuid);
+//            wxwUploadCdnLinkImgDTO.setUrl(fastGptKeywordSend.getImgUrl());
+//            dto  = wxWorkService.uploadCdnLinkImg(wxwUploadCdnLinkImgDTO,serverId);
+//        }
+//
+//        //2.发送模板中的文字内容
+//        String content = fastGptKeywordSend.getContent();
+//        WxWorkSendTextMsgDTO wxWorkSendTextMsgDTO = new WxWorkSendTextMsgDTO();
+//        wxWorkSendTextMsgDTO.setSend_userid(sendId);
+//        wxWorkSendTextMsgDTO.setUuid(uuid);
+//        wxWorkSendTextMsgDTO.setContent(content);
+//        wxWorkSendTextMsgDTO.setIsRoom(false);
+//        WxWorkResponseDTO<WxWorkSendTextMsgRespDTO> wxWorkSendTextMsgRespDTOWxWorkResponseDTO = wxWorkService.SendTextMsg(wxWorkSendTextMsgDTO,serverId);
+//        WxWorkSendTextMsgRespDTO data = wxWorkSendTextMsgRespDTOWxWorkResponseDTO.getData();
+//
+//        //图片上传成功后再发送图片
+//        if("成功".equals(dto.getErrmsg())){
+//            WxwUploadCdnLinkImgRespDTO imgRespDTO = dto.getData();
+//            WxwSendCDNImgMsgDTO wxwSendCDNImgMsgDTO = new WxwSendCDNImgMsgDTO();
+//            wxwSendCDNImgMsgDTO.setSend_userid(sendId);
+//            wxwSendCDNImgMsgDTO.setUuid(uuid);
+//            wxwSendCDNImgMsgDTO.setIsRoom(false);
+//            wxwSendCDNImgMsgDTO.setCdnkey(imgRespDTO.getCdn_key());
+//            wxwSendCDNImgMsgDTO.setAeskey(imgRespDTO.getAes_key());
+//            wxwSendCDNImgMsgDTO.setMd5(imgRespDTO.getMd5());
+//            wxwSendCDNImgMsgDTO.setFileSize(imgRespDTO.getSize());
+//            wxWorkService.SendCDNImgMsg(wxwSendCDNImgMsgDTO, serverId);
+//        }
+//
+//    }
+
+    /** 回调转人工  **/
+    private void notifyArtificial(Long sessionId, QwUser user, String name,String reason,Long extId) {
+        FastGptChatSession s = new FastGptChatSession();
+        s.setIsArtificial(1);
+        s.setRemindStatus(0);
+        s.setIsReply(0);
+        s.setSessionId(sessionId);
+        s.setLastTime(new Date());
+        fastGptChatSessionMapper.updateFastGptChatSession(s);
+        log.info("转人工:"+name);
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(user.getCorpId());
+        sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),"您的客户:"+name+"因"+reason+"  需要转人工,请及时回复");
+        sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),name);
+        qwWorkTaskService.addQwWorkByAiNotifyArtificial(user,extId,"Ai转人工");
+    }
+
+    void sendQwAppMsg(String corpId, Integer agentId,String qwUserId,String content){
+        QwSendMsgParam qwSendMsgParam = new QwSendMsgParam();
+        qwSendMsgParam.setAgentid(agentId);
+        qwSendMsgParam.setTouser(qwUserId);
+        QwSendMsgParam.Text text = new QwSendMsgParam.Text();
+        text.setContent(content);
+        qwSendMsgParam.setText(text);
+        qwSendMsgParam.setMsgtype("text");
+        qwApiService.sendMsg(qwSendMsgParam, corpId);
+    }
+
+
+    private void saveQwUserMsg(FastGptChatSession fastGptChatSession,Integer sendType,String content) {
+        content = replaceWxEmo(content);
+        if(content.isEmpty()){
+            return;
+        }
+        FastGptChatMsg fastGptChatMsgAi = new FastGptChatMsg();
+        fastGptChatMsgAi.setContent(content);
+        fastGptChatMsgAi.setSessionId(fastGptChatSession.getSessionId());
+        fastGptChatMsgAi.setRoleId(Long.parseLong(fastGptChatSession.getKfId()));
+        fastGptChatMsgAi.setSendType(sendType);
+        fastGptChatMsgAi.setCompanyId(fastGptChatSession.getCompanyId());
+        fastGptChatMsgAi.setUserId(fastGptChatSession.getUserId());
+        fastGptChatMsgAi.setUserType(1);
+        fastGptChatMsgAi.setMsgType(1);
+        fastGptChatMsgAi.setStatus(0);
+        fastGptChatMsgAi.setAvatar(fastGptChatSession.getAvatar());
+        fastGptChatMsgAi.setNickName(fastGptChatSession.getNickName());
+        fastGptChatMsgAi.setCreateTime(new Date());
+        fastGptChatMsgAi.setExtId(fastGptChatSession.getQwExtId()+"");
+        fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsgAi);
+        log.info("新增消息:"+fastGptChatMsgAi);
+    }
+    /** 存聊天记录  **/
+    private void addSaveAiMsg(Integer msgType,Integer sendType,String content, QwUser user, Long sessionId, Long roleId, QwExternalContact qwExternalContacts, String userId, Integer promptTokens, Integer completionTokens, Integer token) {
+
+        FastGptChatMsg fastGptChatMsgAi = new FastGptChatMsg();
+        fastGptChatMsgAi.setContent(content);
+        fastGptChatMsgAi.setSessionId(sessionId);
+        fastGptChatMsgAi.setRoleId(roleId);
+        fastGptChatMsgAi.setSendType(sendType);
+        fastGptChatMsgAi.setCompanyId(user.getCompanyId());
+        fastGptChatMsgAi.setUserId(userId);
+        fastGptChatMsgAi.setCompanyUserId(user.getCompanyUserId());
+        fastGptChatMsgAi.setUserType(1);
+        fastGptChatMsgAi.setMsgType(msgType);
+        fastGptChatMsgAi.setStatus(0);
+        fastGptChatMsgAi.setAvatar(qwExternalContacts.getAvatar());
+        fastGptChatMsgAi.setNickName(qwExternalContacts.getName());
+        fastGptChatMsgAi.setCreateTime(new Date());
+        fastGptChatMsgAi.setExtId(qwExternalContacts.getId()+"");
+        fastGptChatMsgAi.setPromptTokens(promptTokens);
+        fastGptChatMsgAi.setCompletionTokens(completionTokens);
+        fastGptChatMsgAi.setTotalTokens(token);
+        fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsgAi);
+    }
+    /** 获取会话  **/
+    private FastGptChatSession getFastGptSession(QwExternalContact qwExternalContacts, QwUser user) {
+        FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), user.getId());
+        if (fastGptChatSession==null){
+            fastGptChatSession = new FastGptChatSession();
+            String chatId = UUID.randomUUID().toString();
+            fastGptChatSession.setChatId(chatId);
+            fastGptChatSession.setKfId(user.getFastGptRoleId().toString());
+            fastGptChatSession.setStatus(1);
+            fastGptChatSession.setRemindCount(0);
+            fastGptChatSession.setRemindStatus(0);
+            fastGptChatSession.setCreateTime(new Date());
+            fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+            fastGptChatSession.setQwUserId(user.getId());
+            fastGptChatSession.setIsArtificial(0);
+            fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+            fastGptChatSession.setNickName(qwExternalContacts.getName());
+            fastGptChatSession.setCompanyId(user.getCompanyId());
+            fastGptChatSession.setLastTime(new Date());
+            fastGptChatSession.setIsReply(0);
+            fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+            addUserSex(qwExternalContacts);
+        }else {
+            if (fastGptChatSession.getRemindStatus()==1){
+                FastGptChatSession ss = new FastGptChatSession();
+                ss.setSessionId(fastGptChatSession.getSessionId());
+                ss.setRemindStatus(0);
+                ss.setIsReply(0);
+                fastGptChatSessionMapper.updateFastGptChatSession(ss);
+            }
+        }
+        return fastGptChatSession;
+    }
+    private FastGptChatSession getFastGptSession(QwExternalContact qwExternalContacts, QwUser user,WxWorkVid2UserIdRespDTO dto) {
+        FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), user.getId());
+        if (fastGptChatSession==null){
+            fastGptChatSession = new FastGptChatSession();
+            String chatId = UUID.randomUUID().toString();
+            fastGptChatSession.setChatId(chatId);
+            fastGptChatSession.setKfId(user.getFastGptRoleId().toString());
+            fastGptChatSession.setStatus(1);
+            fastGptChatSession.setRemindCount(0);
+            fastGptChatSession.setRemindStatus(0);
+            fastGptChatSession.setCreateTime(new Date());
+            fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+            fastGptChatSession.setQwUserId(user.getId());
+            fastGptChatSession.setIsArtificial(0);
+            fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+            fastGptChatSession.setNickName(qwExternalContacts.getName());
+            fastGptChatSession.setCompanyId(user.getCompanyId());
+            fastGptChatSession.setLastTime(new Date());
+            fastGptChatSession.setIsReply(0);
+            fastGptChatSession.setUserId(String.valueOf(dto.getUser_id()));
+            fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+            addUserSex(qwExternalContacts);
+        }else {
+            if (fastGptChatSession.getRemindStatus()==1){
+                FastGptChatSession ss = new FastGptChatSession();
+                ss.setSessionId(fastGptChatSession.getSessionId());
+                ss.setRemindStatus(0);
+                ss.setIsReply(0);
+                ss.setUserId(String.valueOf(dto.getUser_id()));
+                fastGptChatSessionMapper.updateFastGptChatSession(ss);
+            }
+        }
+        return fastGptChatSession;
+    }
+    /** 特殊转人工 **/
+    @Override
+    public void artificial(QwHookVO vo) {
+        QwHookMsgVO msgVo= JSONUtil.toBean(vo.getData(),QwHookMsgVO.class);
+        if (msgVo.getIs_room()!=null&&msgVo.getIs_room()==0){
+            QwUser user=qwUserService.selectQwUserByAppKey(msgVo.getKey());
+            if(user!=null&&user.getFastGptRoleId()!=null) {
+                FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByUserIdAndKfId(msgVo.getSender(), user.getId());
+                if (fastGptChatSession!=null&&fastGptChatSession.getIsArtificial()!=1){
+                    QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(msgVo.getSender_openid(), user.getCorpId(),user.getQwUserId());
+                    if (qwExternalContacts!=null){
+                        FastGptChatSession s = new FastGptChatSession();
+                        s.setIsArtificial(1);
+                        s.setSessionId(fastGptChatSession.getSessionId());
+                        fastGptChatSessionMapper.updateFastGptChatSession(s);
+                        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(user.getCorpId());
+                        qwSendAppMsg(qwCompany.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),"您的客户:"+qwExternalContacts.getName()+" 因发送图片或视频,需要转人工,请及时回复");
+                        qwSendAppMsg(qwCompany.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),qwExternalContacts.getName());
+                        if (fastGptChatSession.getRemindStatus()==1){
+                            FastGptChatSession ss = new FastGptChatSession();
+                            ss.setSessionId(fastGptChatSession.getSessionId());
+                            ss.setRemindStatus(0);
+                            ss.setIsReply(0);
+                            fastGptChatSessionMapper.updateFastGptChatSession(ss);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void qwSendAppMsg(String corpId,Integer agentId, String qwUserId,String content){
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+        QwSendMsgParam qwSendMsgParam2 = new QwSendMsgParam();
+        qwSendMsgParam2.setAgentid(agentId);
+        qwSendMsgParam2.setTouser(qwUserId);
+        QwSendMsgParam.Text text2 = new QwSendMsgParam.Text();
+        text2.setContent(content);
+        qwSendMsgParam2.setText(text2);
+        qwSendMsgParam2.setMsgtype("text");
+        qwApiService.sendMsg(qwSendMsgParam2, qwCompany.getCorpId());
+    }
+    /** 添加企微性别 **/
+    private void addUserSex(QwExternalContact qwExternalContacts) {
+
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(qwExternalContacts.getId());
+        if (qwExternalContacts.getGender()!=0){
+            if (info!=null){
+                info.setSex(qwExternalContacts.getGender()==1?"男":"女");
+                qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+
+            }else {
+                QwExternalContactInfo qwExternalContactInfo = new QwExternalContactInfo();
+                qwExternalContactInfo.setExternalContactId(qwExternalContacts.getId());
+                qwExternalContactInfo.setSex(qwExternalContacts.getGender()==1?"男":"女");
+                qwExternalContactInfo.setCreateTime(new Date());
+                qwExternalContactInfoMapper.insertQwExternalContactInfo(qwExternalContactInfo);
+
+            }
+        }
+
+
+
+    }
+    /** Ai发送课程链接 **/
+    private void sendUrlLink(String content, QwHookMsgVO msgVo, QwUser user, QwSession session) {
+        if (content.contains("【发送课程:当天课程】")){
+            FsCourseWatchLogVO fsCourseWatchLogVO = watchLogMapper.selectFsCourseWatchLogByExtIdAndQwUserId(session.getQwExtId(), user.getId());
+            if (fsCourseWatchLogVO!=null){
+                FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                param.setVideoId(fsCourseWatchLogVO.getVideoId());
+                param.setQwUserId(String.valueOf(user.getId()));
+                param.setDays(1);
+                param.setCorpId(user.getCorpId());
+                param.setCourseId(fsCourseWatchLogVO.getCourseId());
+                param.setCompanyUserId(user.getCompanyUserId());
+                param.setCompanyId(user.getCompanyId());
+                param.setQwExternalId(Long.parseLong(session.getQwExtId()));
+                param.setSendTime(new Date());
+                R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsCourseWatchLogVO.getVideoId()+""),
+                        Integer.valueOf(fsCourseWatchLogVO.getCourseId()+""),
+                        String.valueOf(user.getId()),
+                        String.valueOf(user.getCompanyUserId()),
+                        String.valueOf(user.getCompanyId()),
+                        String.valueOf(session.getQwExtId()));
+                if (linkUrl != null && linkUrl.get("url") != null) {
+                    String s = (String)linkUrl.get("url");
+                    sendWebSocketMsg(s,msgVo,user,session);
+                }
+            }
+        }
+        else if (content.contains("【发送课程")){
+            Pattern c = Pattern.compile("【发送课程:(.*?)】", Pattern.DOTALL);
+            Matcher cMatcher = c.matcher(content);
+            while  (cMatcher.find()) {
+                String trim = cMatcher.group(1).trim();
+                if(trim!=null&&!trim.equals("")){
+                    FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoStringId(trim);
+                    if (fsUserCourseVideo==null){
+                        FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                        param.setVideoId(fsUserCourseVideo.getVideoId());
+                        param.setQwUserId(String.valueOf(user.getId()));
+                        param.setDays(1);
+                        param.setCorpId(user.getCorpId());
+                        param.setCourseId(fsUserCourseVideo.getCourseId());
+                        param.setCompanyUserId(user.getCompanyUserId());
+                        param.setCompanyId(user.getCompanyId());
+                        param.setQwExternalId(Long.parseLong(session.getQwExtId()));
+                        param.setSendTime(new Date());
+                        R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                        qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsUserCourseVideo.getVideoId()+""),
+                                Integer.valueOf(fsUserCourseVideo.getCourseId()+""),
+                                String.valueOf(user.getId()),
+                                String.valueOf(user.getCompanyUserId()),
+                                String.valueOf(user.getCompanyId()),
+                                String.valueOf(session.getQwExtId()));
+                        if (linkUrl != null && linkUrl.get("url") != null) {
+                            String s = (String)linkUrl.get("url");
+                            sendWebSocketMsg(s,msgVo,user,session);
+                        }
+                    }
+
+                }else {
+                    log.error("发送课程不存在:"+trim);
+                }
+
+            }
+        }
+    }
+    /** 发送Ai消息 **/
+    private R  sendAiMsg(Integer i,FastGptChatSession fastGptChatSession, FastGptRole role,QwUser user,Long qwExternalContactsId,String appKey,QwExternalContact qwExternalContacts){
+        //等待5秒
+        try {
+            Thread.sleep(10000); // 5000 毫秒 = 5 秒
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        //获取现在的次数
+        Integer reply = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+        if (reply!=i){
+            //次数变动 重新等待5秒
+            return   sendAiMsg(reply,fastGptChatSession,role,user,qwExternalContactsId,appKey,qwExternalContacts);
+        }else {
+            System.out.println("开始ai回答");
+            ChatParam param=new ChatParam();
+            param.setChatId(fastGptChatSession.getChatId());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Variables variables=new ChatParam.Variables();
+            variables.setUid(user.getFastGptRoleId().toString());
+            variables.setName("test");
+            param.setVariables(variables);
+            List<ChatParam.Message> messageList=new ArrayList<ChatParam.Message>();
+            param.setMessages(messageList);
+            //添加看客记录
+            addCourseWatchLog(qwExternalContactsId);
+            String msgC = (String)redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+            //添加关键词
+            addPromptWord(messageList,msgC,qwExternalContactsId,role.getReminderWords(), role.getContactInfo(),fastGptChatSession.getSessionId());
+            R r = chatService.initiatingTakeChat(param,"http://154.8.194.176:3000/api",appKey);
+            Integer reply2 = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+            //次数变动 重新等待5秒
+            if (reply2!=i){
+                System.out.println("等待");
+                return   sendAiMsg(reply,fastGptChatSession,role,user,qwExternalContactsId,appKey,qwExternalContacts);
+            }
+            addSaveAiMsg(2,1,messageList.get(0).getContent(),user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+            return r;
+        }
+
+    }
+    /** 增加课程信息 **/
+    private void addCourseWatchLog(Long id) {
+        FsCourseWatchLogVO log = fsCourseWatchLogMapper.selectFsCourseWatchLogByExtId(id);
+        if (log!=null){
+            QwExternalContactInfo qwExternalContactInfo = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(id);
+            QwExternalContactInfo info = new QwExternalContactInfo();
+//            Date dateToCheck = log.getCreateTime(); // 假设这是你要检查的日期
+//            Date today = new Date(); // 获取当前日期
+           // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 格式化日期
+            String name = log.getCourseName() + log.getTitle();
+            name = name.replaceAll("[【】]", "");
+//            if (sdf.format(dateToCheck).equals(sdf.format(today))) {
+                info.setStudy(name);
+                info.setCourseStatus(log.getLogType()==3?"待看课":log.getLogType()==1?"已完课":log.getLogType()==2?"已完课":"看课中断");
+
+//            }else {
+//                info.setStudy(name);
+//                info.setCourseStatus("待看课");
+//
+//
+//            }
+            if(qwExternalContactInfo!=null){
+                info.setId(qwExternalContactInfo.getId());
+                qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+            }else {
+                info.setExternalContactId(id);
+                info.setCreateTime(new Date());
+                qwExternalContactInfoMapper.insertQwExternalContactInfo(info);
+
+            }
+        }
+    }
+    /** 组装发送AI内容 **/
+    private void addPromptWord(List<ChatParam.Message> messageList,String count,Long extId,String words,String countInfo,Long sessionId){
+
+        String  str="";
+        List<FastGptChatMsg> msgs=fastGptChatMsgService.selectFastGptChatMsgByMsgSessionId(sessionId);
+        if (!msgs.isEmpty()){
+            Collections.reverse(msgs);
+            msgs.remove(msgs.size() - 1);
+            str="【历史聊天内容:\n";
+            for (FastGptChatMsg msg : msgs) {
+                Integer sendType = msg.getSendType();
+                String content = msg.getContent();
+                if(sendType!=1){
+                    if (content!=null&&content.length()>150){
+                        continue;
+                    }
+                }
+
+                str +=(sendType==1?"用户:":"AI:")+content+"\n";
+            }
+            str+="】\n";
+        }
+
+        // 这里获取后台的提示词进行匹配
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
+        if(info==null){
+            info=new QwExternalContactInfo();
+        }
+        if (info!=null){
+            str+="【用户状态信息\n";
+            Field[] fields = info.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                field.setAccessible(true);
+                Excel annotation = field.getAnnotation(Excel.class);
+                if (annotation != null) {
+                    String name = field.getName();
+                    String fieldName = annotation.name();
+                    String[] split = countInfo.split(",");
+                    for (String zName : split) {
+                        if (zName.equals(name)) {
+                            Object value = null;
+                            try {
+                                value = field.get(info);
+                            } catch (IllegalAccessException e) {
+                            }
+                            if (value != null) {
+                                str += fieldName + ": " + value.toString() + "\n";
+                            }else {
+                                str += fieldName + ":  \n";
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        str+="】\n";
+        if (words!=null&&!"".equals(words)){
+            str+="【你的角色信息:以下内容为你的信息状态的补充而非用户信息,相当于放在角色任务里面,问到了需要知晓,但是如果无关的时候请无视此段内容 "+"\""+words+"\""+"】\n";
+        }
+        if (count!=null&&!"".equals(count)){
+            str+="【用户说的话内容(之前的内容仅仅为背景,你知道即可,以下才是用户真实说的话的内容)\n" +
+                    "\""+count+"\""+"\n" +
+                    "】";
+        }
+
+
+        ChatParam.Message message1=new ChatParam.Message();
+        message1.setRole("user");
+        message1.setContent(str);
+        messageList.add(message1);
+    }
+    /** 组装表情 **/
+    public static List<String> countString(String input) {
+
+        List<String> stringList = new ArrayList<>();
+        // 所有的回车都分段发送
+        String[] split = input.split("\n");
+        for (String s : split) {
+            List<String> sList = subCount(s);
+            stringList.addAll(sList);
+        }
+//        if (isEmoji){
+//            Random random = new Random();
+//            String[] emojiMorning = new String[] {
+//                    "😊",  // 微笑
+//                    "☀️",  // 太阳
+//                    "🌹",  // 玫瑰
+//                    "☕️",  // 茶杯
+//                    "💪",  // 强壮
+//                    "❤️"   // 爱心
+//            };
+//            String[] emojiEvening = new String[] {
+//                    "😊",  // 微笑
+//                    "🌟",  // 星星
+//                    "🌹",  // 玫瑰
+//                    "☕️",  // 茶杯
+//                    "💪",  // 强壮
+//                    "❤️"   // 爱心
+//            };
+//            List<String> sebdList = new ArrayList<>();
+//            for (String segment : stringList) {
+//                if(!segment.trim().equals("")){
+//                    LocalTime currentTime = LocalTime.now();
+//                    LocalTime startTime = LocalTime.of(5, 0);
+//                    LocalTime endTime   = LocalTime.of(18, 0);
+//                    int sj = random.nextInt(2);
+//                    // 判断当前时间是否在5点到18点之间
+//                    String emj="";
+//                    if (sj==0){
+//                        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
+//                            int i = random.nextInt(1);
+//                            emj=emojiMorning[random.nextInt(emojiMorning.length-1)];
+//                        } else {
+//                            emj=emojiEvening[random.nextInt(emojiEvening.length-1)];
+//                        }
+//                    }
+//                    sebdList.add(segment+emj) ;
+//                }
+//
+//            }
+//            return sebdList;
+//        }
+
+
+        return stringList;
+    }
+    /** 内容分段 **/
+    static List<String> subCount(String s){
+        ArrayList<String> a = new ArrayList<>();
+        if (s.length()>30){
+            String substring = s.substring(30);
+            Pattern pattern = Pattern.compile("([~。!?]+)");
+            String[] segments = pattern.split(substring);
+            Matcher matcher = pattern.matcher(substring);
+            if (matcher.find()&&segments.length>1) {
+                int dh=0;
+                String group = matcher.group();
+                if (group.equals("。")){
+                    group="";
+                    dh=1;
+                }
+                String s1 = s.substring(0, 30) + segments[0] + group;
+                a.addAll(Arrays.asList(s1));
+                if (s.substring(s1.length())!=null&&!s.substring(s1.length()+dh).equals("")){
+                    List<String> add = subCount(s.substring(s1.length()+dh));
+                    a.addAll(add);
+                }
+            }else {
+                a.add(s);
+                return  a;
+            }
+        }else {
+            a.add(s);
+        }
+        return  a;
+    }
+    /** 增加用户信息以及打标签 **/
+    private void addUserInfo(String word,Long extId,FastGptChatSession fastGptChatSession)  {
+        Pattern pattern = Pattern.compile("【用户状态信息(.*?)】", Pattern.DOTALL);
+        Matcher matcher = pattern.matcher(word);
+
+        while  (matcher.find()) {
+            QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
+            String trim = matcher.group(1).trim();
+            String[] zd = trim.split("\n");
+            boolean b=false;
+
+            if (info==null){
+                info=new QwExternalContactInfo();
+                b=true;
+            }
+            Field[] fields = info.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                field.setAccessible(true);
+                Excel annotation = field.getAnnotation(Excel.class);
+                if (annotation != null) {
+                    //中文名称
+                    String fieldName = annotation.name();
+                    String valueName ="";
+                    Object value = null;
+                    try {
+                        value = field.get(info);
+                    } catch (IllegalAccessException e) {
+                    }
+                    if (value != null) {
+                        valueName= value.toString();
+                    }
+                    for (String s : zd) {
+                        String[] zdName=null;
+                        if (s.contains(":")){
+                            zdName = s.split(":");
+                        }
+                        if (s.contains(":")){
+                            zdName = s.split(":");
+                        }
+                        if (zdName!=null&&zdName.length==2){
+                            String name1 = zdName[0];
+                            String name2 = zdName[1];
+                            if (name1.trim().equals(fieldName)){
+                                String name2Trim = name2.trim();
+                                if (!name2Trim.isEmpty()){
+                                    if(name1.equals("交流状态")){
+                                        if (name2Trim.equals("非首次交流")){
+                                            if (fastGptChatSession.getRemindStatus()!=null&&fastGptChatSession.getRemindStatus()==1){
+                                                FastGptChatSession s1 = new FastGptChatSession();
+                                                s1.setSessionId(fastGptChatSession.getSessionId());
+                                                s1.setRemindStatus(0);
+                                                s1.setRemindCount(0);
+                                                fastGptChatSessionMapper.updateFastGptChatSession(s1);
+                                            }
+                                        }
+                                    }
+                                    if (!valueName.trim().equals(name2Trim)){
+                                        if(name1.equals("学习到的章节")||name1.equals("今日课程完成情况")){
+                                            continue;
+                                        }
+
+                                        if (name2Trim.contains("delete")&&name2Trim.contains(";")){
+                                            name2Trim = name2Trim.replaceAll("delete.*?;", "");
+                                        }else if (name2Trim.contains("delete")){
+                                            name2Trim=" ";
+                                        }
+                                        try {
+                                            // 允许修改私有属性
+                                            field.setAccessible(true);
+                                            // 修改 name 属性的值
+                                            field.set(info, name2Trim);
+                                            b=true;
+                                        } catch (Exception e) {
+                                            System.out.println("修改错误");
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if (b){
+                if (info.getId()!=null){
+                    qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+                }else {
+                    info.setExternalContactId(extId);
+                    info.setCreateTime(new Date());
+                    qwExternalContactInfoMapper.insertQwExternalContactInfo(info);
+                }
+            }
+        }
+        Pattern tag = Pattern.compile("【标签:(.*?)】", Pattern.DOTALL);
+        Matcher TagMatcher = tag.matcher(word);
+        while  (TagMatcher.find()) {
+            String trimTag = TagMatcher.group(1).trim();
+            if(trimTag!=null&&!trimTag.equals("")){
+                qwTagGroupService.addQwTagByAi(trimTag,extId);
+            }
+
+        }
+
+        Pattern delTag = Pattern.compile("【移除标签:(.*?)】", Pattern.DOTALL);
+        Matcher delTagMatcher = delTag.matcher(word);
+        while  (delTagMatcher.find()) {
+            String deTag = delTagMatcher.group(1).trim();
+            if(deTag!=null&&!deTag.equals("")){
+                qwTagGroupService.delQwTagByAi(deTag,extId);
+            }
+
+        }
+
+        Pattern addTime = Pattern.compile("【开始计时:(.*?)】", Pattern.DOTALL);
+        Matcher addTimeMather = addTime.matcher(word);
+        while  (addTimeMather.find()) {
+            try {
+                String time = addTimeMather.group(1).trim();
+                int t = Integer.parseInt(time);
+                    FastGptChatSession s1 = new FastGptChatSession();
+                    s1.setSessionId(fastGptChatSession.getSessionId());
+                    Calendar calendar = Calendar.getInstance(); // 获取当前时间
+                    calendar.add(Calendar.MINUTE, t); // 增加t分钟
+                    s1.setRemindTime(calendar.getTime());
+                    s1.setRemindStatus(1);
+                    s1.setRemindCount(0);
+                    fastGptChatSessionMapper.updateFastGptChatSession(s1);
+
+            }catch (Exception e){
+                log.info("ai计时错误"+word);
+            }
+
+        }
+    }
+    /** 过滤[] **/
+    private static String replace(String s){
+
+        if(org.springframework.util.StringUtils.isEmpty(s)) return "";
+
+        String regex1 = "<llnnerThoughtBeginl>[\\s\\S]*?<lnnerThoughtEnd|>";
+        // 创建 Pattern 对象
+        Pattern pattern1 = Pattern.compile(regex1);
+        // 创建 Matcher 对象
+        Matcher matcher1 = pattern1.matcher(s);
+        s = matcher1.replaceAll("");
+
+        String regex = "【[\\s\\S]*?】";
+        // 创建 Pattern 对象
+        Pattern pattern = Pattern.compile(regex);
+        // 创建 Matcher 对象
+        Matcher matcher = pattern.matcher(s);
+        // 替换匹配到的内容
+        return matcher.replaceAll("");
+    }
+    /** 过滤[] **/
+    private static String replaceWxEmo(String s){
+
+        if(org.springframework.util.StringUtils.isEmpty(s)) return "";
+        // 替换匹配到的内容
+        return s.replaceAll("\\[.*?]", "").trim();
+    }
+    /** 发送语音过滤 */
+    private  String voiceHomo(String content){
+        List<FastgptChatVoiceHomo> homos = fastgptChatVoiceHomoMapper.selectFastgptChatVoiceHomoList(new FastgptChatVoiceHomo());
+        for (FastgptChatVoiceHomo homo : homos) {
+            if (content.contains(homo.getContent())) {
+                // 如果包含目标字段,则替换
+                content= content.replace(homo.getContent(), homo.getChangeCount());
+            } else {
+            }
+        }
+        content = content.replaceAll("[a-zA-Z]", "")
+                .replaceAll("\\s", "");
+        return content;
+    }
+    /** 替换违禁词 **/
+    private  String replaceWords(String content){
+        List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
+        for (FastGptChatReplaceWords word : words) {
+            if (content.contains(word.getContent())) {
+                // 如果包含目标字段,则替换
+                content= content.replace(word.getContent(), word.getChangeCount());
+            } else {
+            }
+        }
+        return content;
+    }
+    /** 发送语音回复 */
+    private static AudioVO getSilk(String count,Long userId) {
+        AudioServiceImpl audioService = new AudioServiceImpl();
+        AudioVO Silk = audioService.TextToVoice(count,userId);
+        return Silk;
+    }
+    /** 发送普通消息 */
+    private void sendWebSocketMsg(String count, QwHookMsgVO msgVo, QwUser user,QwSession session) {
+        //发送socket
+        if (count!=null&& !count.trim().isEmpty()){
+            qwMsgService.addAiMsg(session,count,1,user);
+            QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
+            QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
+            sendMsgParam.setType(101003);
+            sendMsgData.setMsg(replaceWords(count));
+            sendMsgData.setSendId(msgVo.getSender());
+            sendMsgData.setSyncKey("1");
+            sendMsgParam.setData(sendMsgData);
+            SendAIParam sendAIParam = new SendAIParam();
+            sendAIParam.setCmd("aiReplyMsg");
+            sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+            sendAIParam.setKey(user.getAppKey());
+            redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
+        }
+
+
+    }
+
+    /** 发送定时任务课程链接 */
+    private void sendTaskUrlLink(String content, String sendId, QwUser user, FastGptChatSession session) {
+        if (content.contains("【发送课程:当天课程】")){
+            FsCourseWatchLogVO fsCourseWatchLogVO = watchLogMapper.selectFsCourseWatchLogByExtIdAndQwUserId(session.getQwExtId().toString(), user.getId());
+            if (fsCourseWatchLogVO!=null){
+                FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                param.setVideoId(fsCourseWatchLogVO.getVideoId());
+                param.setQwUserId(String.valueOf(user.getId()));
+                param.setDays(1);
+                param.setCorpId(user.getCorpId());
+                param.setCourseId(fsCourseWatchLogVO.getCourseId());
+                param.setCompanyUserId(user.getCompanyUserId());
+                param.setCompanyId(user.getCompanyId());
+                param.setQwExternalId(session.getQwExtId());
+                param.setSendTime(new Date());
+                R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsCourseWatchLogVO.getVideoId()+""),
+                        Integer.valueOf(fsCourseWatchLogVO.getCourseId()+""),
+                        String.valueOf(user.getId()),
+                        String.valueOf(user.getCompanyUserId()),
+                        String.valueOf(user.getCompanyId()),
+                        String.valueOf(session.getQwExtId()));
+                if (linkUrl != null && linkUrl.get("url") != null) {
+                    String s = (String)linkUrl.get("url");
+                    sendWebTaskSocketMsg(s,sendId,user);
+                }
+            }
+        }
+        else if (content.contains("【发送课程")){
+            Pattern c = Pattern.compile("【发送课程:(.*?)】", Pattern.DOTALL);
+            Matcher cMatcher = c.matcher(content);
+            while  (cMatcher.find()) {
+                String trim = cMatcher.group(1).trim();
+                if(trim!=null&&!trim.equals("")){
+                    FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoStringId(trim);
+                    System.out.println("课程:"+fsUserCourseVideo);
+                    FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                    param.setVideoId(fsUserCourseVideo.getVideoId());
+                    param.setQwUserId(String.valueOf(user.getId()));
+                    param.setDays(1);
+                    param.setCorpId(user.getCorpId());
+                    param.setCourseId(fsUserCourseVideo.getCourseId());
+                    param.setCompanyUserId(user.getCompanyUserId());
+                    param.setCompanyId(user.getCompanyId());
+                    param.setQwExternalId(session.getQwExtId());
+                    param.setSendTime(new Date());
+                    R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                    qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsUserCourseVideo.getVideoId()+""),
+                            Integer.valueOf(fsUserCourseVideo.getCourseId()+""),
+                            String.valueOf(user.getId()),
+                            String.valueOf(user.getCompanyUserId()),
+                            String.valueOf(user.getCompanyId()),
+                            String.valueOf(session.getQwExtId()));
+                    if (linkUrl != null && linkUrl.get("url") != null) {
+                        String s = (String)linkUrl.get("url");
+                        sendWebTaskSocketMsg(s,sendId,user);
+                    }
+                }
+
+            }
+        }
+
+
+
+    }
+    /** 发送定时任务消息 */
+    private void sendWebTaskSocketMsg(String count, String sendId, QwUser user) {
+        //发送socket
+        //文本消息
+        QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
+        QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
+        sendMsgParam.setType(101003);
+        sendMsgData.setMsg(count);
+        sendMsgData.setSendId(sendId);
+        sendMsgData.setSyncKey("1");
+        sendMsgParam.setData(sendMsgData);
+        SendAIParam sendAIParam = new SendAIParam();
+        sendAIParam.setCmd("aiReplyMsg");
+        sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+        sendAIParam.setKey(user.getAppKey());
+        redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
+    }
+    @Override
+    public R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid) {
+        QwUser sendUser = qwUserMapper.selectQwUserById(qwUserID);
+
+
+        if (sendUser!=null){
+
+            String extId = getExtId(sender, uid, sendUser.getServerId());
+            QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(extId, sendUser.getCorpId(),sendUser.getQwUserId());
+            if (qwExternalContacts==null){
+                return R.ok();
+            }
+             FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), sendUser.getId());
+            if (fastGptChatSession!=null){
+                saveQwUserMsg(fastGptChatSession,2,count);
+            }else {
+
+                if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+                    if(sendUser.getFastGptRoleId()!=null){
+                        fastGptChatSession = new FastGptChatSession();
+                        String chatId = UUID.randomUUID().toString();
+                        fastGptChatSession.setChatId(chatId);
+                        fastGptChatSession.setKfId(sendUser.getFastGptRoleId().toString());
+                        fastGptChatSession.setStatus(1);
+                        fastGptChatSession.setRemindCount(0);
+                        fastGptChatSession.setRemindStatus(0);
+                        fastGptChatSession.setCreateTime(new Date());
+                        fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+                        fastGptChatSession.setQwUserId(sendUser.getId());
+                        fastGptChatSession.setIsArtificial(0);
+                        fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+                        fastGptChatSession.setNickName(qwExternalContacts.getName());
+                        fastGptChatSession.setCompanyId(sendUser.getCompanyId());
+                        fastGptChatSession.setLastTime(new Date());
+                        fastGptChatSession.setIsReply(0);
+                        fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+                        addUserSex(qwExternalContacts);
+                        saveQwUserMsg(fastGptChatSession,2,count);
+                    }
+                }
+            }
+        }
+        return R.ok();
+    }
+
+    @Override
+    public void expireAiMsg() {
+//        List<QwSopLogs> qwSopLogs = qwSopLogsMapper.selectExpireAiMsg();
+//        if (qwSopLogs==null|| qwSopLogs.isEmpty()){
+//            System.out.println("无");
+//            return;
+//        }
+//        qwSopLogsMapper.batchUpdateQwSopLogsById(qwSopLogs);
+//        List<QwSopLogs> distinctList = new ArrayList<>(qwSopLogs.stream()
+//                .collect(Collectors.toMap(
+//                        QwSopLogs::getExternalId,  // 以 extId 作为 Key
+//                        log -> log,            // Value 是对象本身
+//                        (existing, replacement) -> existing  // 遇到重复时保留已有值(第一个)
+//                ))
+//                .values());
+//        for (QwSopLogs logs : distinctList) {
+//            log.info("转人工:"+logs.getCorpId());
+//            QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(logs.getCorpId());
+//            sendQwAppMsg(logs.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),logs.getQwUserid(),"您的客户:"+logs.getExternalUserName()+"因  AI回复内容过期转人工,请及时登录插件确定定位配置准确");
+//        }
+
+    }
+
+
+    String getExtId(Long id,String uid,Long serverId){
+        WxWorkVid2UserIdDTO wxWorkVid2UserIdDTO = new WxWorkVid2UserIdDTO();
+        wxWorkVid2UserIdDTO.setUser_id(Arrays.asList(id));
+        wxWorkVid2UserIdDTO.setUuid(uid);
+        WxWorkResponseDTO<List<WxWorkVid2UserIdRespDTO>> WxWorkVid2UserIdRespDTO = wxWorkService.Vid2UserId(wxWorkVid2UserIdDTO,serverId);
+        List<WxWorkVid2UserIdRespDTO> data = WxWorkVid2UserIdRespDTO.getData();
+        if (data==null&& data.isEmpty()){
+            System.out.println("未获取到extId");
+            return "";
+        }
+        com.fs.wxwork.dto.WxWorkVid2UserIdRespDTO dto = data.get(0);
+        return dto.getOpenid();
+    }
+
+    @Override
+    public WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, String authKey, String fileName, Integer fileSize, Long serverId) {
+        WxwDownloadWeChatFileDTO weChatFileDTO = new WxwDownloadWeChatFileDTO();
+        weChatFileDTO.setUuid(uuid);
+        weChatFileDTO.setUrl(fileId);
+        weChatFileDTO.setAes_key(aesKey);
+        weChatFileDTO.setAuth_key(authKey);
+        weChatFileDTO.setFile_name(fileName);
+        weChatFileDTO.setSize(fileSize);
+        return wxWorkService.downloadWeChatFile(weChatFileDTO, serverId);
+    }
+
+}

+ 1205 - 0
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiNewServiceImpl.java

@@ -0,0 +1,1205 @@
+package com.fs.fastGpt.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.param.FsCourseLinkCreateParam;
+import com.fs.course.service.IFsCourseLinkService;
+import com.fs.course.vo.FsCourseWatchLogVO;
+import com.fs.fastGpt.config.ModeConfig;
+import com.fs.fastGpt.domain.*;
+import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
+import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
+import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
+import com.fs.fastGpt.param.SendAIParam;
+import com.fs.fastGpt.service.AiNewService;
+import com.fs.fastGpt.service.IFastGptChatMsgService;
+import com.fs.fastGpt.service.IFastGptRoleService;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
+import com.fs.fastgptApi.service.ChatService;
+import com.fs.fastgptApi.service.Impl.AudioServiceImpl;
+import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwExternalContactInfoMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.*;
+import com.fs.qwApi.param.QwSendMsgParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.lang.reflect.Field;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class AiNewServiceImpl implements AiNewService {
+    @Autowired
+    private FastGptChatSessionMapper fastGptChatSessionMapper;
+    @Autowired
+    private ChatService chatService;
+    @Autowired
+    private QwUserMapper qwUserMapper;
+    @Autowired
+    private IFastGptRoleService roleService;
+    @Autowired
+    private QwExternalContactInfoMapper qwExternalContactInfoMapper;
+    @Autowired
+    private IFastGptChatMsgService fastGptChatMsgService;
+    @Autowired
+    private IQwContactWayService qwContactWayService;
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    FsCourseWatchLogMapper   watchLogMapper;
+    @Autowired
+    IQwJsApiService qwGetJsapiTicketService;
+    @Autowired
+    private IFsCourseLinkService iFsCourseLinkService;
+    @Autowired
+    FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    private IQwUserService qwUserService;
+
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    private IQwMsgService qwMsgService;
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+    @Autowired
+    private FastGptChatReplaceWordsMapper fastGptChatReplaceWordsMapper;
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+
+
+    /** Ai半小时未回复提醒 **/
+    @Override
+    public void AiRemind() {
+        List<FastGptChatSession> fastGptChatSessions = fastGptChatSessionMapper.selectFastGptChatSessionByRemind();
+        for (FastGptChatSession fastGptChatSession : fastGptChatSessions) {
+            FastGptRole role=roleService.selectFastGptRoleTypeByRoleId(Long.parseLong(fastGptChatSession.getKfId()) );
+            if (role!=null){
+                String modeConfig=role.getModeConfigJson();
+                if (modeConfig==null|| modeConfig.isEmpty()){
+                    return;
+                }
+                ModeConfig config= JSONUtil.toBean(modeConfig,ModeConfig.class);
+                String timeCount="用户未回复";
+                ChatParam param=new ChatParam();
+                param.setChatId(fastGptChatSession.getChatId());
+                param.setStream(false);
+                param.setDetail(true);
+                ChatParam.Variables variables=new ChatParam.Variables();
+                variables.setUid(fastGptChatSession.getKfId());
+                variables.setName("test");
+                param.setVariables(variables);
+                List<ChatParam.Message> messageList=new ArrayList<ChatParam.Message>();
+                param.setMessages(messageList);
+                addPromptWord(messageList,timeCount,fastGptChatSession.getQwExtId(),role.getReminderWords(), role.getContactInfo(),fastGptChatSession.getSessionId());
+                R r = chatService.initiatingTakeChat(param,"http://154.8.194.176:3000/api",config.getAPPKey());
+                if(r.get("code").equals(200)){
+                    ChatDetailTStreamFResult result=(ChatDetailTStreamFResult)r.get("data");
+                    String content = result.getChoices().get(0).getMessage().getContent();
+                    String count = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+                    QwUser qwUser = qwUserMapper.selectQwUserById(fastGptChatSession.getQwUserId());
+                    if(count!=null&& !count.isEmpty()) {
+                        QwHookSendMsgParam sendMsgParam = new QwHookSendMsgParam();
+                        QwHookSendMsgParam.QwHookSendMsgData sendMsgData = new QwHookSendMsgParam.QwHookSendMsgData();
+                        sendMsgParam.setType(101003);
+                        sendMsgData.setMsg(count);
+                        sendMsgData.setSendId(fastGptChatSession.getUserId());
+                        sendMsgData.setSyncKey("1");
+                        sendMsgParam.setData(sendMsgData);
+                        SendAIParam sendAIParam = new SendAIParam();
+                        sendAIParam.setCmd("aiReplyMsg");
+                        sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+                        sendAIParam.setKey(qwUser.getAppKey());
+                        redisTemplate.opsForList().leftPush("AiMsg:" + qwUser.getAppKey(), JSON.toJSONString(sendAIParam));
+                    }
+                    FastGptChatMsg fastGptChatMsgAi = new FastGptChatMsg();
+                    fastGptChatMsgAi.setContent(result.getChoices().get(0).getMessage().getContent());
+                    fastGptChatMsgAi.setSessionId(fastGptChatSession.getSessionId());
+                    fastGptChatMsgAi.setRoleId(role.getRoleId());
+                    fastGptChatMsgAi.setSendType(2);
+                    fastGptChatMsgAi.setCompanyId(qwUser.getCompanyId());
+                    fastGptChatMsgAi.setUserId(fastGptChatSession.getQwExtId().toString());
+                    fastGptChatMsgAi.setCompanyUserId(qwUser.getCompanyUserId());
+                    fastGptChatMsgAi.setUserType(1);
+                    fastGptChatMsgAi.setMsgType(1);
+                    fastGptChatMsgAi.setStatus(0);
+                    fastGptChatMsgAi.setAvatar(fastGptChatSession.getAvatar());
+                    fastGptChatMsgAi.setNickName(fastGptChatSession.getNickName());
+                    fastGptChatMsgAi.setCreateTime(new Date());
+                    fastGptChatMsgAi.setPromptTokens(result.getUsage().prompt_tokens);
+                    fastGptChatMsgAi.setCompletionTokens(result.getUsage().completion_tokens);
+                    List<ChatDetailTStreamFResult.ResponseNode> responseData = result.getResponseData();
+                    int token=0;
+                    for (ChatDetailTStreamFResult.ResponseNode responseDatum : responseData) {
+                        int tokens = responseDatum.getTokens();
+                        token+=tokens;
+                    }
+                    fastGptChatMsgAi.setTotalTokens(token);
+                    fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsgAi);
+                    addUserInfo(result.getChoices().get(0).getMessage().getContent(), fastGptChatSession.getQwExtId(),fastGptChatSession);
+                    addUserInfo(content, fastGptChatSession.getQwExtId(),fastGptChatSession);
+                    sendTaskUrlLink(content,fastGptChatSession.getUserId(),qwUser,fastGptChatSession);
+                }
+            }
+            FastGptChatSession ss = new FastGptChatSession();
+            ss.setSessionId(fastGptChatSession.getSessionId());
+            ss.setRemindStatus(0);
+            ss.setRemindCount(0);
+            fastGptChatSessionMapper.updateFastGptChatSession(ss);
+        }
+
+    }
+    /** Ai回复 **/
+    @Async
+    @Override
+    public R qwHookNotifyAiReply(QwMessageGather qwMessageGather,String corpId) {
+        //是否是群聊
+        String qwContent = qwMessageGather.getText().getContent();
+        if (qwContent.contains("我已经添加了你")){
+                return R.ok();
+        }
+        List<String> tolist = qwMessageGather.getTolist();
+        if (qwMessageGather.getTolist().size() == 0){
+            return R.ok();
+        }
+        String userId = tolist.get(0);
+        System.out.println(userId);
+        System.out.println(corpId);
+        QwUser user = qwUserMapper.selectQwUserByQwUseridAndCorpId(userId,corpId);
+        //查询接收人
+        if(user==null||user.getFastGptRoleId()==null){
+            System.out.println("查询接收人为空");
+            return R.ok();
+        }
+        FastGptRole role=roleService.selectFastGptRoleTypeByRoleId(user.getFastGptRoleId());
+        //没用ai角色跳过
+        if(role==null){
+            System.out.println("没用ai角色跳过");
+            return R.ok();
+        }
+        String modeConfig=role.getModeConfigJson();
+        //key不为空
+        if(StringUtils.isEmpty(modeConfig)){
+            System.out.println("没有aiKey");
+            return R.ok();
+        }
+        ModeConfig config=JSONUtil.toBean(modeConfig,ModeConfig.class);
+        if(StringUtils.isEmpty(config.getAPPKey())){
+            System.out.println("没有aiKey");
+            return R.ok();
+        }
+
+        QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(qwMessageGather.getFrom(), user.getCorpId(),user.getQwUserId());
+        if (qwExternalContacts==null){
+            System.out.println("没有外部联系人");
+            return R.ok();
+        }
+        if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+            FastGptChatSession fastGptChatSession= getFastGptSession(qwExternalContacts,user);
+            String contentEmj = replaceWxEmo(qwContent);
+            if(!contentEmj.isEmpty()){
+                addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+            }
+            //判断是否转人工
+            if (fastGptChatSession.getIsArtificial()==1){
+                System.out.println("转人工了");
+                return R.ok();
+            }
+            if (contentEmj.isEmpty()){
+                contentEmj ="用户发送表情:"+qwContent;
+            }
+            //获取是用户是否发送消息
+            Integer reply = redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+            Integer replyI=1;
+            //用户正在发送消息 不发
+            if (reply!=null&&reply!=0){
+                //更新用户发送消息次数
+                redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),reply+1,5, TimeUnit.MINUTES);
+                //获取用户之前发送的消息
+                String msg = redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+                if (!msg.isEmpty()){
+                    //更新用户发送消息内容
+                    redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),msg+","+contentEmj,5,TimeUnit.MINUTES);
+                }
+                //本次跳过
+                System.out.println("正在对话");
+                return R.ok();
+            }
+            //用户首次发送消息
+            redisCache.setCacheObject("reply:" + fastGptChatSession.getSessionId(),1,5,TimeUnit.MINUTES);
+            redisCache.setCacheObject("msg:" + fastGptChatSession.getSessionId(),contentEmj,5,TimeUnit.MINUTES);
+            System.out.println("等待");
+            R r= sendAiMsg(replyI,fastGptChatSession,role,user,qwExternalContacts.getId(),config.getAPPKey(),qwExternalContacts);
+            System.out.println(r);
+            //完成对话 删除消息记录
+            redisCache.deleteObject("reply:" + fastGptChatSession.getSessionId());
+            redisCache.deleteObject("msg:" + fastGptChatSession.getSessionId());
+            if(!r.get("code").equals(200)){
+                log.info("ai报错转人工:"+role.getRoleId()+":"+qwExternalContacts.getName());
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai报错转人工");
+                return R.ok();
+            }
+            ChatDetailTStreamFResult result=(ChatDetailTStreamFResult)r.get("data");
+            //是否转人工
+            if(result.getChoices().isEmpty()){
+                return R.ok();
+            }
+            String contentKh = result.getChoices().get(0).getMessage().getContent();
+            String content = replace(result.getChoices().get(0).getMessage().getContent()).trim();
+            //计算token
+            List<ChatDetailTStreamFResult.ResponseNode> responseData = result.getResponseData();
+            int token=0;
+            for (ChatDetailTStreamFResult.ResponseNode responseDatum : responseData) {
+                int tokens = responseDatum.getTokens();
+                token+=tokens;
+            }
+            //存聊天记录
+            addSaveAiMsg(2,2,contentKh,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),result.getUsage().prompt_tokens,result.getUsage().completion_tokens,token);
+            if (!content.isEmpty()){
+                //addSaveAiMsg(1,2,content,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+                if (contentKh.contains("FunctionCallBegin")||contentKh.contains("FunctionCallEnd")||contentKh.contains("不清楚")||contentKh.contains("对不起")||contentKh.contains("客服")||contentKh.contains("不太清楚")||contentKh.contains("不明白")||contentKh.contains("不太明白")||contentKh.contains("http")){
+                    notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 触发关键词");
+                    return R.ok();
+                }
+                if (result.isLongText()){
+                    sendAiMsgSop(content,qwExternalContacts,user);
+                }else {
+                    String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
+                    String nr = replace(sa);
+                    String[] split = nr.split("\n");
+                    if (split.length>6){
+                        notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName(),"回复长度异常");
+                        return R.ok();
+                    }
+                    List<String> countList = countString(content);
+                    for (String msg : countList) {
+                        sendAiMsgSop(msg,qwExternalContacts,user);
+
+                    }
+                }
+
+            }
+            //新增用户信息
+            addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
+            //发送短链
+
+            if (result.isArtificial()){
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," ai请求人工协助");
+            }
+        }
+
+        return R.ok();
+    }
+    @Autowired
+    QwSopLogsMapper qwSopLogsMapper;
+    private void sendAiMsgSop(String content, QwExternalContact qwExternalContacts, QwUser user) {
+        QwSopLogs qwSopLogs = new QwSopLogs();
+        qwSopLogs.setQwUserid(user.getQwUserId());
+        qwSopLogs.setExternalUserId(qwExternalContacts.getExternalUserId());
+        qwSopLogs.setLogType(2);
+        qwSopLogs.setSendType(8);
+        qwSopLogs.setExternalUserName(qwExternalContacts.getName());
+        qwSopLogs.setSendStatus(3L);
+        qwSopLogs.setExternalId(qwExternalContacts.getId());
+        qwSopLogs.setCompanyId(user.getCompanyId());
+        qwSopLogs.setReceivingStatus(0L);
+        qwSopLogs.setSort(88888888);
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        qwSopLogs.setSendTime(sdf.format(new Date()));
+        qwSopLogs.setCorpId(user.getCorpId());
+        qwSopLogs.setContentJson("{\n" +
+                "\"contentType\": \"1\",\n" +
+                "\"setting\": [\n" +
+                "{\n" +
+                "\"contentType\": \"1\",\n" +
+                "\"isBindUrl\": \"2\",\n" +
+                "\"value\": \""+content+"\"\n" +
+                "}\n" +
+                "],\n" +
+                "\"type\": 1\n" +
+                "}");
+        qwSopLogsMapper.insertQwSopLogs(qwSopLogs);
+        System.out.println(content);
+    }
+
+    /** 回调转人工  **/
+    private void notifyArtificial(Long sessionId, QwUser user, String name,String reason) {
+        FastGptChatSession s = new FastGptChatSession();
+        s.setIsArtificial(1);
+        s.setRemindStatus(0);
+        s.setIsReply(0);
+        s.setSessionId(sessionId);
+        s.setLastTime(new Date());
+        fastGptChatSessionMapper.updateFastGptChatSession(s);
+        log.info("转人工:"+name);
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(user.getCorpId());
+        sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),"您的客户:"+name+"因"+reason+"  需要转人工,请及时回复");
+        sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),name);
+    }
+
+    void sendQwAppMsg(String corpId, Integer agentId,String qwUserId,String content){
+        QwSendMsgParam qwSendMsgParam = new QwSendMsgParam();
+        qwSendMsgParam.setAgentid(agentId);
+        qwSendMsgParam.setTouser(qwUserId);
+        QwSendMsgParam.Text text = new QwSendMsgParam.Text();
+        text.setContent(content);
+        qwSendMsgParam.setText(text);
+        qwSendMsgParam.setMsgtype("text");
+        qwApiService.sendMsg(qwSendMsgParam, corpId);
+    }
+
+
+    private void saveQwUserMsg(FastGptChatSession fastGptChatSession,Integer sendType,String content) {
+        content = replaceWxEmo(content);
+        if(content.isEmpty()){
+            return;
+        }
+        FastGptChatMsg fastGptChatMsgAi = new FastGptChatMsg();
+        fastGptChatMsgAi.setContent(content);
+        fastGptChatMsgAi.setSessionId(fastGptChatSession.getSessionId());
+        fastGptChatMsgAi.setRoleId(Long.parseLong(fastGptChatSession.getKfId()));
+        fastGptChatMsgAi.setSendType(sendType);
+        fastGptChatMsgAi.setCompanyId(fastGptChatSession.getCompanyId());
+        fastGptChatMsgAi.setUserId(fastGptChatSession.getUserId());
+        fastGptChatMsgAi.setUserType(1);
+        fastGptChatMsgAi.setMsgType(1);
+        fastGptChatMsgAi.setStatus(0);
+        fastGptChatMsgAi.setAvatar(fastGptChatSession.getAvatar());
+        fastGptChatMsgAi.setNickName(fastGptChatSession.getNickName());
+        fastGptChatMsgAi.setCreateTime(new Date());
+        fastGptChatMsgAi.setExtId(fastGptChatSession.getQwExtId()+"");
+        fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsgAi);
+        log.info("新增消息:"+fastGptChatMsgAi);
+    }
+    /** 存聊天记录  **/
+    private void addSaveAiMsg(Integer msgType,Integer sendType,String content, QwUser user, Long sessionId, Long roleId, QwExternalContact qwExternalContacts, String userId, Integer promptTokens, Integer completionTokens, Integer token) {
+
+        FastGptChatMsg fastGptChatMsgAi = new FastGptChatMsg();
+        fastGptChatMsgAi.setContent(content);
+        fastGptChatMsgAi.setSessionId(sessionId);
+        fastGptChatMsgAi.setRoleId(roleId);
+        fastGptChatMsgAi.setSendType(sendType);
+        fastGptChatMsgAi.setCompanyId(user.getCompanyId());
+        fastGptChatMsgAi.setUserId(userId);
+        fastGptChatMsgAi.setCompanyUserId(user.getCompanyUserId());
+        fastGptChatMsgAi.setUserType(1);
+        fastGptChatMsgAi.setMsgType(msgType);
+        fastGptChatMsgAi.setStatus(0);
+        fastGptChatMsgAi.setAvatar(qwExternalContacts.getAvatar());
+        fastGptChatMsgAi.setNickName(qwExternalContacts.getName());
+        fastGptChatMsgAi.setCreateTime(new Date());
+        fastGptChatMsgAi.setExtId(qwExternalContacts.getId()+"");
+        fastGptChatMsgAi.setPromptTokens(promptTokens);
+        fastGptChatMsgAi.setCompletionTokens(completionTokens);
+        fastGptChatMsgAi.setTotalTokens(token);
+        fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsgAi);
+    }
+    /** 获取会话  **/
+    private FastGptChatSession getFastGptSession(QwExternalContact qwExternalContacts, QwUser user) {
+        FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), user.getId());
+        if (fastGptChatSession==null){
+            fastGptChatSession = new FastGptChatSession();
+            String chatId = UUID.randomUUID().toString();
+            fastGptChatSession.setChatId(chatId);
+            fastGptChatSession.setKfId(user.getFastGptRoleId().toString());
+            fastGptChatSession.setStatus(1);
+            fastGptChatSession.setRemindCount(0);
+            fastGptChatSession.setRemindStatus(0);
+            fastGptChatSession.setCreateTime(new Date());
+            fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+            fastGptChatSession.setQwUserId(user.getId());
+            fastGptChatSession.setIsArtificial(0);
+            fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+            fastGptChatSession.setNickName(qwExternalContacts.getName());
+            fastGptChatSession.setCompanyId(user.getCompanyId());
+            fastGptChatSession.setLastTime(new Date());
+            fastGptChatSession.setIsReply(0);
+            fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+            addUserSex(qwExternalContacts);
+        }else {
+            if (fastGptChatSession.getRemindStatus()==1){
+                FastGptChatSession ss = new FastGptChatSession();
+                ss.setSessionId(fastGptChatSession.getSessionId());
+                ss.setRemindStatus(0);
+                ss.setIsReply(0);
+                fastGptChatSessionMapper.updateFastGptChatSession(ss);
+            }
+        }
+        return fastGptChatSession;
+    }
+    /** 特殊转人工 **/
+    @Override
+    public void artificial(QwHookVO vo) {
+        QwHookMsgVO msgVo= JSONUtil.toBean(vo.getData(),QwHookMsgVO.class);
+        if (msgVo.getIs_room()!=null&&msgVo.getIs_room()==0){
+            QwUser user=qwUserService.selectQwUserByAppKey(msgVo.getKey());
+            if(user!=null&&user.getFastGptRoleId()!=null) {
+                FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByUserIdAndKfId(msgVo.getSender(), user.getId());
+                if (fastGptChatSession!=null&&fastGptChatSession.getIsArtificial()!=1){
+                    QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(msgVo.getSender_openid(), user.getCorpId(),user.getQwUserId());
+                    if (qwExternalContacts!=null){
+                        FastGptChatSession s = new FastGptChatSession();
+                        s.setIsArtificial(1);
+                        s.setSessionId(fastGptChatSession.getSessionId());
+                        fastGptChatSessionMapper.updateFastGptChatSession(s);
+                        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(user.getCorpId());
+                        qwSendAppMsg(qwCompany.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),"您的客户:"+qwExternalContacts.getName()+" 因发送图片或视频,需要转人工,请及时回复");
+                        qwSendAppMsg(qwCompany.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),qwExternalContacts.getName());
+                        if (fastGptChatSession.getRemindStatus()==1){
+                            FastGptChatSession ss = new FastGptChatSession();
+                            ss.setSessionId(fastGptChatSession.getSessionId());
+                            ss.setRemindStatus(0);
+                            ss.setIsReply(0);
+                            fastGptChatSessionMapper.updateFastGptChatSession(ss);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void qwSendAppMsg(String corpId,Integer agentId, String qwUserId,String content){
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+        QwSendMsgParam qwSendMsgParam2 = new QwSendMsgParam();
+        qwSendMsgParam2.setAgentid(agentId);
+        qwSendMsgParam2.setTouser(qwUserId);
+        QwSendMsgParam.Text text2 = new QwSendMsgParam.Text();
+        text2.setContent(content);
+        qwSendMsgParam2.setText(text2);
+        qwSendMsgParam2.setMsgtype("text");
+        qwApiService.sendMsg(qwSendMsgParam2, qwCompany.getCorpId());
+    }
+    /** 添加企微性别 **/
+    private void addUserSex(QwExternalContact qwExternalContacts) {
+
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(qwExternalContacts.getId());
+        if (qwExternalContacts.getGender()!=0){
+            if (info!=null){
+                info.setSex(qwExternalContacts.getGender()==1?"男":"女");
+                qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+
+            }else {
+                QwExternalContactInfo qwExternalContactInfo = new QwExternalContactInfo();
+                qwExternalContactInfo.setExternalContactId(qwExternalContacts.getId());
+                qwExternalContactInfo.setSex(qwExternalContacts.getGender()==1?"男":"女");
+                qwExternalContactInfo.setCreateTime(new Date());
+                qwExternalContactInfoMapper.insertQwExternalContactInfo(qwExternalContactInfo);
+
+            }
+        }
+
+
+
+    }
+    /** Ai发送课程链接 **/
+    private void sendUrlLink(String content, QwHookMsgVO msgVo, QwUser user, QwSession session) {
+        if (content.contains("【发送课程:当天课程】")){
+            FsCourseWatchLogVO fsCourseWatchLogVO = watchLogMapper.selectFsCourseWatchLogByExtIdAndQwUserId(session.getQwExtId(), user.getId());
+            if (fsCourseWatchLogVO!=null){
+                FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                param.setVideoId(fsCourseWatchLogVO.getVideoId());
+                param.setQwUserId(String.valueOf(user.getId()));
+                param.setDays(1);
+                param.setCorpId(user.getCorpId());
+                param.setCourseId(fsCourseWatchLogVO.getCourseId());
+                param.setCompanyUserId(user.getCompanyUserId());
+                param.setCompanyId(user.getCompanyId());
+                param.setQwExternalId(Long.parseLong(session.getQwExtId()));
+                param.setSendTime(new Date());
+                R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsCourseWatchLogVO.getVideoId()+""),
+                        Integer.valueOf(fsCourseWatchLogVO.getCourseId()+""),
+                        String.valueOf(user.getId()),
+                        String.valueOf(user.getCompanyUserId()),
+                        String.valueOf(user.getCompanyId()),
+                        String.valueOf(session.getQwExtId()));
+                if (linkUrl != null && linkUrl.get("url") != null) {
+                    String s = (String)linkUrl.get("url");
+                    sendWebSocketMsg(s,msgVo,user,session);
+                }
+            }
+        }
+        else if (content.contains("【发送课程")){
+            Pattern c = Pattern.compile("【发送课程:(.*?)】", Pattern.DOTALL);
+            Matcher cMatcher = c.matcher(content);
+            while  (cMatcher.find()) {
+                String trim = cMatcher.group(1).trim();
+                if(trim!=null&&!trim.equals("")){
+                    FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoStringId(trim);
+                    if (fsUserCourseVideo==null){
+                        FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                        param.setVideoId(fsUserCourseVideo.getVideoId());
+                        param.setQwUserId(String.valueOf(user.getId()));
+                        param.setDays(1);
+                        param.setCorpId(user.getCorpId());
+                        param.setCourseId(fsUserCourseVideo.getCourseId());
+                        param.setCompanyUserId(user.getCompanyUserId());
+                        param.setCompanyId(user.getCompanyId());
+                        param.setQwExternalId(Long.parseLong(session.getQwExtId()));
+                        param.setSendTime(new Date());
+                        R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                        qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsUserCourseVideo.getVideoId()+""),
+                                Integer.valueOf(fsUserCourseVideo.getCourseId()+""),
+                                String.valueOf(user.getId()),
+                                String.valueOf(user.getCompanyUserId()),
+                                String.valueOf(user.getCompanyId()),
+                                String.valueOf(session.getQwExtId()));
+                        if (linkUrl != null && linkUrl.get("url") != null) {
+                            String s = (String)linkUrl.get("url");
+                            sendWebSocketMsg(s,msgVo,user,session);
+                        }
+                    }
+
+                }else {
+                    log.error("发送课程不存在:"+trim);
+                }
+
+            }
+        }
+    }
+    /** 发送Ai消息 **/
+    private R  sendAiMsg(Integer i,FastGptChatSession fastGptChatSession, FastGptRole role,QwUser user,Long qwExternalContactsId,String appKey,QwExternalContact qwExternalContacts){
+        //等待5秒
+        try {
+            Thread.sleep(15000); // 5000 毫秒 = 5 秒
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        //获取现在的次数
+        Integer reply = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+        if (reply!=i){
+            //次数变动 重新等待5秒
+            return   sendAiMsg(reply,fastGptChatSession,role,user,qwExternalContactsId,appKey,qwExternalContacts);
+        }else {
+            System.out.println("开始ai回答");
+            ChatParam param=new ChatParam();
+            param.setChatId(fastGptChatSession.getChatId());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Variables variables=new ChatParam.Variables();
+            variables.setUid(user.getFastGptRoleId().toString());
+            variables.setName("test");
+            param.setVariables(variables);
+            List<ChatParam.Message> messageList=new ArrayList<ChatParam.Message>();
+            param.setMessages(messageList);
+            //添加看客记录
+            addCourseWatchLog(qwExternalContactsId);
+            String msgC = (String)redisCache.getCacheObject("msg:" + fastGptChatSession.getSessionId());
+            //添加关键词
+            addPromptWord(messageList,msgC,qwExternalContactsId,role.getReminderWords(), role.getContactInfo(),fastGptChatSession.getSessionId());
+            R r = chatService.initiatingTakeChat(param,"http://154.8.194.176:3000/api",appKey);
+            Integer reply2 = (Integer)redisCache.getCacheObject("reply:" + fastGptChatSession.getSessionId());
+            //次数变动 重新等待5秒
+            if (reply2!=i){
+                System.out.println("等待");
+                return   sendAiMsg(reply,fastGptChatSession,role,user,qwExternalContactsId,appKey,qwExternalContacts);
+            }
+            addSaveAiMsg(2,1,messageList.get(0).getContent(),user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
+            return r;
+        }
+
+    }
+    /** 增加课程信息 **/
+    private void addCourseWatchLog(Long id) {
+        FsCourseWatchLogVO log = fsCourseWatchLogMapper.selectFsCourseWatchLogByExtId(id);
+        if (log!=null){
+            QwExternalContactInfo qwExternalContactInfo = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(id);
+            QwExternalContactInfo info = new QwExternalContactInfo();
+//            Date dateToCheck = log.getCreateTime(); // 假设这是你要检查的日期
+//            Date today = new Date(); // 获取当前日期
+           // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // 格式化日期
+            String name = log.getCourseName() + log.getTitle();
+            name = name.replaceAll("[【】]", "");
+//            if (sdf.format(dateToCheck).equals(sdf.format(today))) {
+                info.setStudy(name);
+                info.setCourseStatus(log.getLogType()==3?"待看课":log.getLogType()==1?"已完课":log.getLogType()==2?"已完课":"看课中断");
+
+//            }else {
+//                info.setStudy(name);
+//                info.setCourseStatus("待看课");
+//
+//
+//            }
+            if(qwExternalContactInfo!=null){
+                info.setId(qwExternalContactInfo.getId());
+                qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+            }else {
+                info.setExternalContactId(id);
+                info.setCreateTime(new Date());
+                qwExternalContactInfoMapper.insertQwExternalContactInfo(info);
+
+            }
+        }
+    }
+    /** 组装发送AI内容 **/
+    private void addPromptWord(List<ChatParam.Message> messageList,String count,Long extId,String words,String countInfo,Long sessionId){
+
+        String  str="";
+        List<FastGptChatMsg> msgs=fastGptChatMsgService.selectFastGptChatMsgByMsgSessionId(sessionId);
+        if (!msgs.isEmpty()){
+            Collections.reverse(msgs);
+            str="【历史聊天内容:\n";
+            for (FastGptChatMsg msg : msgs) {
+                Integer sendType = msg.getSendType();
+                String content = msg.getContent();
+                str +=(sendType==1?"用户:":"AI:")+content+"\n";
+            }
+            str+="】\n";
+        }
+
+        // 这里获取后台的提示词进行匹配
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
+        if(info==null){
+            info=new QwExternalContactInfo();
+        }
+        if (info!=null){
+            str+="【用户状态信息\n";
+            Field[] fields = info.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                field.setAccessible(true);
+                Excel annotation = field.getAnnotation(Excel.class);
+                if (annotation != null) {
+                    String name = field.getName();
+                    String fieldName = annotation.name();
+                    String[] split = countInfo.split(",");
+                    for (String zName : split) {
+                        if (zName.equals(name)) {
+                            Object value = null;
+                            try {
+                                value = field.get(info);
+                            } catch (IllegalAccessException e) {
+                            }
+                            if (value != null) {
+                                str += fieldName + ": " + value.toString() + "\n";
+                            }else {
+                                str += fieldName + ":  \n";
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        str+="】\n";
+        if (words!=null&&!"".equals(words)){
+            str+="【你的角色信息:以下内容为你的信息状态的补充而非用户信息,相当于放在角色任务里面,问到了需要知晓,但是如果无关的时候请无视此段内容 "+"\""+words+"\""+"】\n";
+        }
+        if (count!=null&&!"".equals(count)){
+            str+="【用户说的话内容(之前的内容仅仅为背景,你知道即可,以下才是用户真实说的话的内容)\n" +
+                    "\""+count+"\""+"\n" +
+                    "】";
+        }
+
+
+        ChatParam.Message message1=new ChatParam.Message();
+        message1.setRole("user");
+        message1.setContent(str);
+        messageList.add(message1);
+    }
+    /** 组装表情 **/
+    public static List<String> countString(String input) {
+
+        List<String> stringList = new ArrayList<>();
+        // 所有的回车都分段发送
+        String[] split = input.split("\n");
+        for (String s : split) {
+            List<String> sList = subCount(s);
+            stringList.addAll(sList);
+        }
+//        if (isEmoji){
+//            Random random = new Random();
+//            String[] emojiMorning = new String[] {
+//                    "😊",  // 微笑
+//                    "☀️",  // 太阳
+//                    "🌹",  // 玫瑰
+//                    "☕️",  // 茶杯
+//                    "💪",  // 强壮
+//                    "❤️"   // 爱心
+//            };
+//            String[] emojiEvening = new String[] {
+//                    "😊",  // 微笑
+//                    "🌟",  // 星星
+//                    "🌹",  // 玫瑰
+//                    "☕️",  // 茶杯
+//                    "💪",  // 强壮
+//                    "❤️"   // 爱心
+//            };
+//            List<String> sebdList = new ArrayList<>();
+//            for (String segment : stringList) {
+//                if(!segment.trim().equals("")){
+//                    LocalTime currentTime = LocalTime.now();
+//                    LocalTime startTime = LocalTime.of(5, 0);
+//                    LocalTime endTime   = LocalTime.of(18, 0);
+//                    int sj = random.nextInt(2);
+//                    // 判断当前时间是否在5点到18点之间
+//                    String emj="";
+//                    if (sj==0){
+//                        if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) {
+//                            int i = random.nextInt(1);
+//                            emj=emojiMorning[random.nextInt(emojiMorning.length-1)];
+//                        } else {
+//                            emj=emojiEvening[random.nextInt(emojiEvening.length-1)];
+//                        }
+//                    }
+//                    sebdList.add(segment+emj) ;
+//                }
+//
+//            }
+//            return sebdList;
+//        }
+
+
+        return stringList;
+    }
+    /** 内容分段 **/
+    static List<String> subCount(String s){
+        ArrayList<String> a = new ArrayList<>();
+        if (s.length()>30){
+            String substring = s.substring(30);
+            Pattern pattern = Pattern.compile("([~。!?]+)");
+            String[] segments = pattern.split(substring);
+            Matcher matcher = pattern.matcher(substring);
+            if (matcher.find()&&segments.length>1) {
+                int dh=0;
+                String group = matcher.group();
+                if (group.equals("。")){
+                    group="";
+                    dh=1;
+                }
+                String s1 = s.substring(0, 30) + segments[0] + group;
+                a.addAll(Arrays.asList(s1));
+                if (s.substring(s1.length())!=null&&!s.substring(s1.length()+dh).equals("")){
+                    List<String> add = subCount(s.substring(s1.length()+dh));
+                    a.addAll(add);
+                }
+            }else {
+                a.add(s);
+                return  a;
+            }
+        }else {
+            a.add(s);
+        }
+        return  a;
+    }
+    /** 增加用户信息以及打标签 **/
+    private void addUserInfo(String word,Long extId,FastGptChatSession fastGptChatSession)  {
+        Pattern pattern = Pattern.compile("【用户状态信息(.*?)】", Pattern.DOTALL);
+        Matcher matcher = pattern.matcher(word);
+        while  (matcher.find()) {
+            String trim = matcher.group(1).trim();
+            String[] zd = trim.split("\n");
+            boolean b=false;
+            QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
+            if (info==null){
+                info=new QwExternalContactInfo();
+                b=true;
+            }
+            Field[] fields = info.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                field.setAccessible(true);
+                Excel annotation = field.getAnnotation(Excel.class);
+                if (annotation != null) {
+                    //中文名称
+                    String fieldName = annotation.name();
+                    String valueName ="";
+                    Object value = null;
+                    try {
+                        value = field.get(info);
+                    } catch (IllegalAccessException e) {
+                    }
+                    if (value != null) {
+                        valueName= value.toString();
+                    }
+                    for (String s : zd) {
+                        String[] zdName=null;
+                        if (s.contains(":")){
+                            zdName = s.split(":");
+                        }
+                        if (s.contains(":")){
+                            zdName = s.split(":");
+                        }
+                        if (zdName!=null&&zdName.length==2){
+                            String name1 = zdName[0];
+                            String name2 = zdName[1];
+                            if (name1.trim().equals(fieldName)){
+                                String name2Trim = name2.trim();
+                                if (!name2Trim.isEmpty()){
+                                    if(name1.equals("交流状态")){
+                                        if (name2Trim.equals("非首次交流")){
+                                            if (fastGptChatSession.getRemindStatus()!=null&&fastGptChatSession.getRemindStatus()==1){
+                                                FastGptChatSession s1 = new FastGptChatSession();
+                                                s1.setSessionId(fastGptChatSession.getSessionId());
+                                                s1.setRemindStatus(0);
+                                                s1.setRemindCount(0);
+                                                fastGptChatSessionMapper.updateFastGptChatSession(s1);
+                                            }
+                                        }
+                                    }
+                                    if (!valueName.trim().equals(name2Trim)){
+                                        if(name1.equals("学习到的章节")||name1.equals("今日课程完成情况")){
+                                            continue;
+                                        }
+
+                                        if (name2Trim.contains("delete")&&name2Trim.contains(";")){
+                                            name2Trim = name2Trim.replaceAll("delete.*?;", "");
+                                        }else if (name2Trim.contains("delete")){
+                                            name2Trim=" ";
+                                        }
+                                        try {
+                                            // 允许修改私有属性
+                                            field.setAccessible(true);
+                                            // 修改 name 属性的值
+                                            field.set(info, name2Trim);
+                                            b=true;
+                                        } catch (Exception e) {
+                                            System.out.println("修改错误");
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if (b){
+                if (info.getId()!=null){
+                    qwExternalContactInfoMapper.updateQwExternalContactInfo(info);
+                }else {
+                    info.setExternalContactId(extId);
+                    info.setCreateTime(new Date());
+                    qwExternalContactInfoMapper.insertQwExternalContactInfo(info);
+                }
+            }
+        }
+        Pattern tag = Pattern.compile("【标签:(.*?)】", Pattern.DOTALL);
+        Matcher TagMatcher = tag.matcher(word);
+        while  (TagMatcher.find()) {
+            String trimTag = TagMatcher.group(1).trim();
+            if(trimTag!=null&&!trimTag.equals("")){
+                qwTagGroupService.addQwTagByAi(trimTag,extId);
+            }
+
+        }
+
+        Pattern delTag = Pattern.compile("【移除标签:(.*?)】", Pattern.DOTALL);
+        Matcher delTagMatcher = delTag.matcher(word);
+        while  (delTagMatcher.find()) {
+            String deTag = delTagMatcher.group(1).trim();
+            if(deTag!=null&&!deTag.equals("")){
+                qwTagGroupService.delQwTagByAi(deTag,extId);
+            }
+
+        }
+
+        Pattern addTime = Pattern.compile("【开始计时:(.*?)】", Pattern.DOTALL);
+        Matcher addTimeMather = addTime.matcher(word);
+        while  (addTimeMather.find()) {
+            try {
+                String time = addTimeMather.group(1).trim();
+                int t = Integer.parseInt(time);
+                    FastGptChatSession s1 = new FastGptChatSession();
+                    s1.setSessionId(fastGptChatSession.getSessionId());
+                    Calendar calendar = Calendar.getInstance(); // 获取当前时间
+                    calendar.add(Calendar.MINUTE, t); // 增加t分钟
+                    s1.setRemindTime(calendar.getTime());
+                    s1.setRemindStatus(1);
+                    s1.setRemindCount(0);
+                    fastGptChatSessionMapper.updateFastGptChatSession(s1);
+
+            }catch (Exception e){
+                log.info("ai计时错误"+word);
+            }
+
+        }
+    }
+    /** 过滤[] **/
+    private static String replace(String s){
+
+        if(org.springframework.util.StringUtils.isEmpty(s)) return "";
+
+        String regex1 = "<llnnerThoughtBeginl>[\\s\\S]*?<lnnerThoughtEnd|>";
+        // 创建 Pattern 对象
+        Pattern pattern1 = Pattern.compile(regex1);
+        // 创建 Matcher 对象
+        Matcher matcher1 = pattern1.matcher(s);
+        s = matcher1.replaceAll("");
+
+        String regex = "【[\\s\\S]*?】";
+        // 创建 Pattern 对象
+        Pattern pattern = Pattern.compile(regex);
+        // 创建 Matcher 对象
+        Matcher matcher = pattern.matcher(s);
+        // 替换匹配到的内容
+        return matcher.replaceAll("");
+    }
+    /** 过滤[] **/
+    private static String replaceWxEmo(String s){
+
+        if(org.springframework.util.StringUtils.isEmpty(s)) return "";
+        // 替换匹配到的内容
+        return s.replaceAll("\\[.*?]", "").trim();
+    }
+    /** 发送语音过滤 */
+    private  String voiceHomo(String content){
+        List<FastgptChatVoiceHomo> homos = fastgptChatVoiceHomoMapper.selectFastgptChatVoiceHomoList(new FastgptChatVoiceHomo());
+        for (FastgptChatVoiceHomo homo : homos) {
+            if (content.contains(homo.getContent())) {
+                // 如果包含目标字段,则替换
+                content= content.replace(homo.getContent(), homo.getChangeCount());
+            } else {
+            }
+        }
+        return content;
+    }
+    /** 替换违禁词 **/
+    private  String replaceWords(String content){
+        List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
+        for (FastGptChatReplaceWords word : words) {
+            if (content.contains(word.getContent())) {
+                // 如果包含目标字段,则替换
+                content= content.replace(word.getContent(), word.getChangeCount());
+            } else {
+            }
+        }
+        return content;
+    }
+    /** 发送语音回复 */
+    private static AudioVO getSilk(String count,Long userId) {
+        AudioServiceImpl audioService = new AudioServiceImpl();
+        AudioVO Silk = audioService.TextToVoice(count,userId);
+        return Silk;
+    }
+    /** 发送普通消息 */
+    private void sendWebSocketMsg(String count, QwHookMsgVO msgVo, QwUser user,QwSession session) {
+        //发送socket
+        if (count!=null&& !count.trim().isEmpty()){
+            qwMsgService.addAiMsg(session,count,1,user);
+            QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
+            QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
+            sendMsgParam.setType(101003);
+            sendMsgData.setMsg(replaceWords(count));
+            sendMsgData.setSendId(msgVo.getSender());
+            sendMsgData.setSyncKey("1");
+            sendMsgParam.setData(sendMsgData);
+            SendAIParam sendAIParam = new SendAIParam();
+            sendAIParam.setCmd("aiReplyMsg");
+            sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+            sendAIParam.setKey(user.getAppKey());
+            redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
+        }
+
+
+    }
+    /** 发送语音回复 */
+    private void sendWebSocketVoiceMsg(String count, QwHookMsgVO msgVo, QwUser user,QwSession session,Long companyUserId) {
+        //发送socket
+        qwMsgService.addAiMsg(session,count,2,user);
+        AudioVO skl = getSilk(voiceHomo(count),companyUserId);
+        QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
+        QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
+        sendMsgParam.setType(101019);
+        sendMsgData.setVoiceDuration(skl.getDuration());
+        sendMsgData.setVoiceUrl(skl.getUrl());
+        sendMsgData.setSendId(msgVo.getSender());
+        sendMsgData.setSyncKey("1");
+        sendMsgParam.setData(sendMsgData);
+        SendAIParam sendAIParam = new SendAIParam();
+        sendAIParam.setCmd("aiReplyMsg");
+        sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+        sendAIParam.setKey(user.getAppKey());
+        redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
+
+    }
+    /** 发送定时任务课程链接 */
+    private void sendTaskUrlLink(String content, String sendId, QwUser user, FastGptChatSession session) {
+        if (content.contains("【发送课程:当天课程】")){
+            FsCourseWatchLogVO fsCourseWatchLogVO = watchLogMapper.selectFsCourseWatchLogByExtIdAndQwUserId(session.getQwExtId().toString(), user.getId());
+            if (fsCourseWatchLogVO!=null){
+                FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                param.setVideoId(fsCourseWatchLogVO.getVideoId());
+                param.setQwUserId(String.valueOf(user.getId()));
+                param.setDays(1);
+                param.setCorpId(user.getCorpId());
+                param.setCourseId(fsCourseWatchLogVO.getCourseId());
+                param.setCompanyUserId(user.getCompanyUserId());
+                param.setCompanyId(user.getCompanyId());
+                param.setQwExternalId(session.getQwExtId());
+                param.setSendTime(new Date());
+                R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsCourseWatchLogVO.getVideoId()+""),
+                        Integer.valueOf(fsCourseWatchLogVO.getCourseId()+""),
+                        String.valueOf(user.getId()),
+                        String.valueOf(user.getCompanyUserId()),
+                        String.valueOf(user.getCompanyId()),
+                        String.valueOf(session.getQwExtId()));
+                if (linkUrl != null && linkUrl.get("url") != null) {
+                    String s = (String)linkUrl.get("url");
+                    sendWebTaskSocketMsg(s,sendId,user);
+                }
+            }
+        }
+        else if (content.contains("【发送课程")){
+            Pattern c = Pattern.compile("【发送课程:(.*?)】", Pattern.DOTALL);
+            Matcher cMatcher = c.matcher(content);
+            while  (cMatcher.find()) {
+                String trim = cMatcher.group(1).trim();
+                if(trim!=null&&!trim.equals("")){
+                    FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoStringId(trim);
+                    System.out.println("课程:"+fsUserCourseVideo);
+                    FsCourseLinkCreateParam param = new FsCourseLinkCreateParam();
+                    param.setVideoId(fsUserCourseVideo.getVideoId());
+                    param.setQwUserId(String.valueOf(user.getId()));
+                    param.setDays(1);
+                    param.setCorpId(user.getCorpId());
+                    param.setCourseId(fsUserCourseVideo.getCourseId());
+                    param.setCompanyUserId(user.getCompanyUserId());
+                    param.setCompanyId(user.getCompanyId());
+                    param.setQwExternalId(session.getQwExtId());
+                    param.setSendTime(new Date());
+                    R linkUrl = iFsCourseLinkService.createLinkUrlWc(param);
+                    qwContactWayService.addWatchLogIfNeeded(Integer.valueOf(fsUserCourseVideo.getVideoId()+""),
+                            Integer.valueOf(fsUserCourseVideo.getCourseId()+""),
+                            String.valueOf(user.getId()),
+                            String.valueOf(user.getCompanyUserId()),
+                            String.valueOf(user.getCompanyId()),
+                            String.valueOf(session.getQwExtId()));
+                    if (linkUrl != null && linkUrl.get("url") != null) {
+                        String s = (String)linkUrl.get("url");
+                        sendWebTaskSocketMsg(s,sendId,user);
+                    }
+                }
+
+            }
+        }
+
+
+
+    }
+    /** 发送定时任务消息 */
+    private void sendWebTaskSocketMsg(String count, String sendId, QwUser user) {
+        //发送socket
+        //文本消息
+        QwHookSendMsgParam sendMsgParam=new QwHookSendMsgParam();
+        QwHookSendMsgParam.QwHookSendMsgData sendMsgData=new QwHookSendMsgParam.QwHookSendMsgData();
+        sendMsgParam.setType(101003);
+        sendMsgData.setMsg(count);
+        sendMsgData.setSendId(sendId);
+        sendMsgData.setSyncKey("1");
+        sendMsgParam.setData(sendMsgData);
+        SendAIParam sendAIParam = new SendAIParam();
+        sendAIParam.setCmd("aiReplyMsg");
+        sendAIParam.setData(JSONUtil.toJsonStr(sendMsgParam));
+        sendAIParam.setKey(user.getAppKey());
+        redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
+    }
+    @Override
+    public R qwHookNotifyAddMsg(QwMessageGather qwMessageGather,String corpId) {
+        QwUser sendUser=qwUserMapper.selectQwUserByQwUseridAndCorpId(qwMessageGather.getFrom(),corpId);
+
+
+        if (sendUser!=null){
+            List<String> tolist = qwMessageGather.getTolist();
+            if (qwMessageGather.getTolist().isEmpty()){
+                return R.ok();
+            }
+            String ExtId = tolist.get(0);
+            QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(ExtId, sendUser.getCorpId(),sendUser.getQwUserId());
+            if (qwExternalContacts==null){
+                return R.ok();
+            }
+             FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), sendUser.getId());
+            if (fastGptChatSession!=null){
+                saveQwUserMsg(fastGptChatSession,2,qwMessageGather.getText().getContent());
+            }else {
+
+                if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+                    if(sendUser.getFastGptRoleId()!=null){
+                        fastGptChatSession = new FastGptChatSession();
+                        String chatId = UUID.randomUUID().toString();
+                        fastGptChatSession.setChatId(chatId);
+                        fastGptChatSession.setKfId(sendUser.getFastGptRoleId().toString());
+                        fastGptChatSession.setStatus(1);
+                        fastGptChatSession.setRemindCount(0);
+                        fastGptChatSession.setRemindStatus(0);
+                        fastGptChatSession.setCreateTime(new Date());
+                        fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+                        fastGptChatSession.setQwUserId(sendUser.getId());
+                        fastGptChatSession.setIsArtificial(0);
+                        fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+                        fastGptChatSession.setNickName(qwExternalContacts.getName());
+                        fastGptChatSession.setCompanyId(sendUser.getCompanyId());
+                        fastGptChatSession.setLastTime(new Date());
+                        fastGptChatSession.setIsReply(0);
+                        fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+                        addUserSex(qwExternalContacts);
+                        saveQwUserMsg(fastGptChatSession,2,qwMessageGather.getText().getContent());
+                    }
+                }
+            }
+        }
+        return R.ok();
+    }
+
+    @Override
+    public void expireAiMsg() {
+
+    }
+
+//    @Override
+//    public void expireAiMsg() {
+//        List<QwSopLogs> qwSopLogs = qwSopLogsMapper.selectExpireAiMsg();
+//        if (qwSopLogs==null|| qwSopLogs.isEmpty()){
+//            System.out.println("无");
+//            return;
+//        }
+//        qwSopLogsMapper.batchUpdateQwSopLogsById(qwSopLogs);
+//        List<QwSopLogs> distinctList = new ArrayList<>(qwSopLogs.stream()
+//                .collect(Collectors.toMap(
+//                        QwSopLogs::getExternalId,  // 以 extId 作为 Key
+//                        log -> log,            // Value 是对象本身
+//                        (existing, replacement) -> existing  // 遇到重复时保留已有值(第一个)
+//                ))
+//                .values());
+//        for (QwSopLogs logs : distinctList) {
+//            log.info("转人工:"+logs.getCorpId());
+//            QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(logs.getCorpId());
+//            sendQwAppMsg(logs.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),logs.getQwUserid(),"您的客户:"+logs.getExternalUserName()+"因  AI回复内容过期转人工,请及时登录插件确定定位配置准确");
+//        }
+//
+//    }
+}

+ 48 - 0
fs-service/src/main/java/com/fs/fastgptApi/result/AiImgResult.java

@@ -0,0 +1,48 @@
+package com.fs.fastgptApi.result;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AiImgResult {
+    private List<Choice> choices;
+    private long created;
+    private String id;
+    private String model;
+    private String service_tier;
+    private String object;
+    private Usage usage;
+
+
+    @Data
+    public static class Choice {
+        private String finish_reason;
+        private int index;
+        private Object logprobs; // 或定义一个具体类型
+        private Message message;
+
+    }
+    @Data
+    public static class Message {
+        private String content;
+        private String role;
+
+    }
+
+    @Data
+    public static class Usage {
+        private int completion_tokens;
+        private int prompt_tokens;
+        private int total_tokens;
+        private TokenDetails prompt_tokens_details;
+        private TokenDetails completion_tokens_details;
+
+    }
+    @Data
+    public static class TokenDetails {
+        private int cached_tokens;
+        private int reasoning_tokens; // optional depending on context
+
+    }
+}

+ 102 - 0
fs-service/src/main/java/com/fs/fastgptApi/util/AiImgUtil.java

@@ -0,0 +1,102 @@
+package com.fs.fastgptApi.util;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.fastgptApi.result.AiImgResult;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+@Service
+public class AiImgUtil {
+
+
+    public String getImageParse(String imageUrl) {
+        try {
+            String requestBody = String.format(
+                    "{" +
+                            "\"model\": \"doubao-vision-lite-32k-241015\"," +
+                            "\"messages\": [{" +
+                            "\"role\": \"user\"," +
+                            "\"content\": [" +
+                            "{" +
+                            "\"type\": \"image_url\"," +
+                            "\"image_url\": {\"url\": \"" + imageUrl + "\"}" +
+                            "}," +
+                            "{" +
+                            "\"type\": \"text\"," +
+                            "\"text\": \"解析一下这张图片\"" +
+                            "}" +
+                            "]" +
+                            "}]" +
+                            "}"
+            );
+
+            // 发送请求
+            String response = sendAiImgHttpRequest(requestBody);
+            System.out.println("API响应: " + response);
+            AiImgResult aiImgResult = JSON.parseObject(response, AiImgResult.class);
+            String content = aiImgResult.getChoices().get(0).getMessage().getContent();
+            System.out.println(content);
+            return content;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+
+
+    private  String sendAiImgHttpRequest(String requestBody) throws IOException {
+        HttpURLConnection connection = null;
+        try {
+            // 配置连接
+            URL url = new URL("https://ark.cn-beijing.volces.com/api/v3/chat/completions");
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setRequestProperty("Authorization", "Bearer " + "208d3549-8dc9-4ef6-b3fa-5aa358f1ab20");
+            connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
+            connection.setDoOutput(true);
+            connection.setConnectTimeout(10000); // 10秒连接超时
+            connection.setReadTimeout(30000);   // 30秒读取超时
+
+            // 发送请求体
+            try (OutputStream os = connection.getOutputStream()) {
+                os.write(requestBody.getBytes(StandardCharsets.UTF_8));
+            }
+
+            // 处理响应
+            int status = connection.getResponseCode();
+            if (status == HttpURLConnection.HTTP_OK) {
+                try (BufferedReader br = new BufferedReader(
+                        new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
+                    StringBuilder response = new StringBuilder();
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        response.append(line);
+                    }
+                    return response.toString();
+                }
+            } else {
+                // 读取错误流
+                try (BufferedReader br = new BufferedReader(
+                        new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
+                    StringBuilder errorResponse = new StringBuilder();
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        errorResponse.append(line);
+                    }
+                    throw new IOException("HTTP错误 " + status + ": " + errorResponse.toString());
+                }
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+}

+ 7 - 0
fs-service/src/main/java/com/fs/his/mapper/FsIntegralGoodsMapper.java

@@ -83,4 +83,11 @@ public interface FsIntegralGoodsMapper
             " order by g.goods_id desc "+
             "</script>"})
     List<FsIntegralGoodsListUVO> selectFsIntegralGoodsListUVO(@Param("maps")FsIntegralGoodsListUParam param);
+
+    /**
+     * 查询指定积分区间的商品
+     * @param param 查询参数
+     * @return 商品列表
+     */
+    List<FsIntegralGoods> selectFsIntegralGoodsByStage(FsIntegralGoodsListUParam param);
 }

+ 15 - 0
fs-service/src/main/java/com/fs/his/param/FsIntegralGoodsListUParam.java

@@ -13,4 +13,19 @@ public class FsIntegralGoodsListUParam  implements Serializable {
     @ApiModelProperty(value = "页大小,默认为10")
     private Integer pageSize = 10;
     private String keyword;
+
+    /**
+     * 最小积分
+     */
+    private Integer minPoints;
+
+    /**
+     * 最大积分
+     */
+    private Integer maxPoints;
+
+    /**
+     * 商品状态(1:上架 2:下架)
+     */
+    private Integer status;
 }

+ 3 - 0
fs-service/src/main/java/com/fs/his/service/IFsIntegralGoodsService.java

@@ -1,5 +1,6 @@
 package com.fs.his.service;
 
+import com.fs.common.core.domain.R;
 import com.fs.his.domain.FsIntegralGoods;
 import com.fs.his.param.FsIntegralGoodsListUParam;
 import com.fs.his.vo.FsIntegralGoodsListUVO;
@@ -68,4 +69,6 @@ public interface IFsIntegralGoodsService
     List<FsIntegralGoodsListUVO> selectFsIntegralGoodsListUVO(FsIntegralGoodsListUParam param);
 
     String importIntegralGoodsService(List<FsIntegralGoods> list);
+
+    R getCourseIntegralGoods(Long userId);
 }

+ 94 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsIntegralGoodsServiceImpl.java

@@ -1,18 +1,28 @@
 package com.fs.his.service.impl;
 
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
 import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsCourseAnswerReward;
 import com.fs.his.domain.FsChineseMedicine;
 import com.fs.his.domain.FsIntegralGoods;
+import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsIntegralGoodsMapper;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FsIntegralGoodsListUParam;
 import com.fs.his.service.IFsIntegralGoodsService;
 import com.fs.his.vo.FsIntegralGoodsListUVO;
 import com.fs.his.vo.FsIntegralGoodsListVO;
+import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Random;
 
 /**
  * 积分商品Service业务层处理
@@ -21,10 +31,16 @@ import java.util.List;
  * @date 2023-11-02
  */
 @Service
+@Slf4j
 public class FsIntegralGoodsServiceImpl implements IFsIntegralGoodsService
 {
     @Autowired
     private FsIntegralGoodsMapper fsIntegralGoodsMapper;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private ISysConfigService configService;
 
     /**
      * 查询积分商品
@@ -149,4 +165,82 @@ public class FsIntegralGoodsServiceImpl implements IFsIntegralGoodsService
         }
         return successMsg.toString();
     }
+
+    @Override
+    public R getCourseIntegralGoods(Long userId) {
+        log.info("获取答题奖励信息, userId:{}", userId);
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+
+        // 1. 创建返回对象
+        FsCourseAnswerReward reward = new FsCourseAnswerReward();
+
+        // 2. 设置当前积分
+        Integer currentPoints = user.getIntegral().intValue();
+        reward.setCurrentPoints(currentPoints);
+
+        // 3. 计算用户所在阶段(每5000积分一个阶段)
+        int currentStage = (currentPoints / 5000) + 1;
+        int stageMaxPoints = currentStage * 5000;
+
+        // 4. 获取当前阶段的积分商品(随机获取3个)
+        FsIntegralGoodsListUParam param = new FsIntegralGoodsListUParam();
+        param.setMinPoints(currentPoints);
+        param.setMaxPoints(stageMaxPoints);
+        List<FsIntegralGoods> stageGoods = selectRandomStageGoods(param);
+
+        // 5. 转换为奖励商品列表
+        List<FsCourseAnswerReward.RewardProduct> products = new ArrayList<>();
+        int minRequiredPoints = stageMaxPoints; // 用于计算进度的最小积分要求
+
+        for (FsIntegralGoods goods : stageGoods) {
+            FsCourseAnswerReward.RewardProduct product = new FsCourseAnswerReward.RewardProduct();
+            product.setProductId(goods.getGoodsId());
+            product.setProductName(goods.getGoodsName());
+            product.setRequiredPoints(goods.getIntegral().intValue());
+            product.setImageUrl(goods.getImgUrl());
+            product.setOtPrice(goods.getOtPrice());
+            products.add(product);
+
+            // 更新最小所需积分
+            if (goods.getIntegral().intValue() < minRequiredPoints) {
+                minRequiredPoints = goods.getIntegral().intValue();
+            }
+        }
+        reward.setProducts(products);
+
+        // 6. 计算兑换进度(基于最低积分商品)
+        int exchangeProgress = (int) ((currentPoints * 100.0) / minRequiredPoints);
+        // 进度最大显示100%
+        reward.setExchangeProgress(Math.min(exchangeProgress, 100));
+        reward.setAvailableCoins(config.getAnswerIntegral());
+        log.info("答题奖励信息: {}", reward);
+        return R.ok().put("data", reward);
+    }
+
+    /**
+     * 随机获取当前阶段的积分商品
+     */
+    private List<FsIntegralGoods> selectRandomStageGoods(FsIntegralGoodsListUParam param) {
+        // 查询当前阶段的所有商品
+        List<FsIntegralGoods> allGoods = fsIntegralGoodsMapper.selectFsIntegralGoodsByStage(param);
+
+        // 如果商品数量不足3个,直接返回全部
+        if (allGoods.size() <= 3) {
+            return allGoods;
+        }
+
+        // 随机选择3个商品
+        List<FsIntegralGoods> selectedGoods = new ArrayList<>();
+        Random random = new Random();
+
+        while (selectedGoods.size() < 3 && !allGoods.isEmpty()) {
+            int index = random.nextInt(allGoods.size());
+            selectedGoods.add(allGoods.get(index));
+            allGoods.remove(index);
+        }
+
+        return selectedGoods;
+    }
 }

+ 50 - 0
fs-service/src/main/java/com/fs/qw/domain/QwIpadServer.java

@@ -0,0 +1,50 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * ipad服务器对象 qw_ipad_server
+ *
+ * @author fs
+ * @date 2025-04-29
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class QwIpadServer extends BaseEntity{
+
+    /** id */
+    private Long id;
+
+    /** 标题 */
+    @Excel(name = "标题")
+    private String title;
+
+    /** 地址 */
+    @Excel(name = "地址")
+    private String addressId;
+
+    /** ip */
+    @Excel(name = "ip")
+    private String ip;
+
+    /** 地址 */
+    @Excel(name = "地址")
+    private String port;
+
+    /** url */
+    @Excel(name = "url")
+    private String url;
+
+    /** 总数 */
+    @Excel(name = "总数")
+    private Long totalCount;
+
+    /** 剩余数量 */
+    @Excel(name = "剩余数量")
+    private Long count;
+
+
+}

+ 46 - 0
fs-service/src/main/java/com/fs/qw/domain/QwIpadServerLog.java

@@ -0,0 +1,46 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * ipad服务器日志对象 qw_ipad_server_log
+ *
+ * @author fs
+ * @date 2025-04-29
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class QwIpadServerLog extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 服务器id */
+    @Excel(name = "服务器id")
+    private Long serverId;
+
+    /** 企微用户 */
+    @Excel(name = "企微用户")
+    private Long qwUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 公司用户id */
+    @Excel(name = "公司用户id")
+    private Long companyUserId;
+
+    /** 标题 */
+    @Excel(name = "标题")
+    private String tilie;
+
+    /** 类别 1 绑定 2 解绑 */
+    @Excel(name = "类别 1 绑定 2 解绑")
+    private Integer type;
+
+
+}

+ 38 - 0
fs-service/src/main/java/com/fs/qw/domain/QwIpadServerUser.java

@@ -0,0 +1,38 @@
+package com.fs.qw.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * ipad用户对象 qw_ipad_server_user
+ *
+ * @author fs
+ * @date 2025-04-29
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class QwIpadServerUser extends BaseEntity{
+
+    /** id */
+    private Long id;
+
+    /** 服务器id */
+    @Excel(name = "服务器id")
+    private Long serverId;
+
+    /** 企微用户id */
+    @Excel(name = "企微用户id")
+    private Long qwUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 公司用户id */
+    @Excel(name = "公司用户id")
+    private Long companyUserId;
+
+
+}

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