Browse Source

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

caoliqin 3 weeks ago
parent
commit
25b861ed97
100 changed files with 7784 additions and 24 deletions
  1. 2 2
      fs-admin/src/main/resources/application-dev.yml
  2. 10 0
      fs-company-app/src/main/resources/application-dev.yml
  3. 27 16
      fs-company-app/src/main/resources/application.yml
  4. 3 0
      fs-company/src/main/java/com/fs/store/controller/FsStoreProductController.java
  5. 2 6
      fs-qw-api/src/main/java/com/fs/app/controller/QwController.java
  6. 142 0
      fs-qwhook-msg/pom.xml
  7. 14 0
      fs-qwhook-msg/src/main/java/com/fs/FSServletInitializer.java
  8. 27 0
      fs-qwhook-msg/src/main/java/com/fs/FsQwhookApplication.java
  9. 12 0
      fs-qwhook-msg/src/main/java/com/fs/app/annotation/Login.java
  10. 15 0
      fs-qwhook-msg/src/main/java/com/fs/app/annotation/LoginUser.java
  11. 58 0
      fs-qwhook-msg/src/main/java/com/fs/app/config/ArrayStringTypeHandler.java
  12. 43 0
      fs-qwhook-msg/src/main/java/com/fs/app/config/SystemStartSopService.java
  13. 29 0
      fs-qwhook-msg/src/main/java/com/fs/app/config/WebMvcConfig.java
  14. 306 0
      fs-qwhook-msg/src/main/java/com/fs/app/controller/CommonController.java
  15. 90 0
      fs-qwhook-msg/src/main/java/com/fs/app/controller/QwSopController.java
  16. 30 0
      fs-qwhook-msg/src/main/java/com/fs/app/controller/RocketMQAiMsgService.java
  17. 31 0
      fs-qwhook-msg/src/main/java/com/fs/app/controller/RocketMQWatchLogService.java
  18. 51 0
      fs-qwhook-msg/src/main/java/com/fs/app/exception/FSException.java
  19. 81 0
      fs-qwhook-msg/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  20. 70 0
      fs-qwhook-msg/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  21. 14 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SendAIParam.java
  22. 23 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParam.java
  23. 24 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamB.java
  24. 68 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamDetails.java
  25. 30 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamDetailsB.java
  26. 28 0
      fs-qwhook-msg/src/main/java/com/fs/app/params/SopLogsEditParam.java
  27. 31 0
      fs-qwhook-msg/src/main/java/com/fs/app/resolver/LoginUserHandlerMethodArgumentResolver.java
  28. 248 0
      fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java
  29. 26 0
      fs-qwhook-msg/src/main/java/com/fs/app/utils/JsonUtils.java
  30. 87 0
      fs-qwhook-msg/src/main/java/com/fs/app/utils/JwtUtils.java
  31. 182 0
      fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  32. 73 0
      fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  33. 245 0
      fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/LogAspect.java
  34. 117 0
      fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  35. 31 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/ApplicationConfig.java
  36. 85 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/CaptchaConfig.java
  37. 109 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java
  38. 72 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  39. 59 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/FilterConfig.java
  40. 76 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  41. 133 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/MyBatisConfig.java
  42. 121 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/RedisConfig.java
  43. 65 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/ResourcesConfig.java
  44. 51 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/SecurityConfig.java
  45. 33 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/ServerConfig.java
  46. 122 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/SwaggerConfig.java
  47. 63 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  48. 20 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java
  49. 77 0
      fs-qwhook-msg/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  50. 27 0
      fs-qwhook-msg/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  51. 45 0
      fs-qwhook-msg/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  52. 56 0
      fs-qwhook-msg/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  53. 126 0
      fs-qwhook-msg/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  54. 56 0
      fs-qwhook-msg/src/main/java/com/fs/framework/manager/AsyncManager.java
  55. 40 0
      fs-qwhook-msg/src/main/java/com/fs/framework/manager/ShutdownManager.java
  56. 103 0
      fs-qwhook-msg/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  57. 1 0
      fs-qwhook-msg/src/main/resources/META-INF/spring-devtools.properties
  58. 149 0
      fs-qwhook-msg/src/main/resources/application-dev.yml
  59. 140 0
      fs-qwhook-msg/src/main/resources/application-druid-ylrz.yml
  60. 143 0
      fs-qwhook-msg/src/main/resources/application.yml
  61. 2 0
      fs-qwhook-msg/src/main/resources/banner.txt
  62. 37 0
      fs-qwhook-msg/src/main/resources/i18n/messages.properties
  63. 18 0
      fs-qwhook-msg/src/main/resources/mybatis/mybatis-config.xml
  64. 142 0
      fs-qwhook-sop/pom.xml
  65. 13 0
      fs-qwhook-sop/src/main/java/com/fs/FSServletInitializer.java
  66. 26 0
      fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java
  67. 12 0
      fs-qwhook-sop/src/main/java/com/fs/app/annotation/Login.java
  68. 15 0
      fs-qwhook-sop/src/main/java/com/fs/app/annotation/LoginUser.java
  69. 58 0
      fs-qwhook-sop/src/main/java/com/fs/app/config/ArrayStringTypeHandler.java
  70. 29 0
      fs-qwhook-sop/src/main/java/com/fs/app/config/WebMvcConfig.java
  71. 137 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisCommonController.java
  72. 94 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisQwSopController.java
  73. 151 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/CommonController.java
  74. 97 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/QwSopController.java
  75. 71 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/RoomSopController.java
  76. 205 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/TestCourseService.java
  77. 88 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java
  78. 221 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testCourseController.java
  79. 399 0
      fs-qwhook-sop/src/main/java/com/fs/app/controller/testService.java
  80. 51 0
      fs-qwhook-sop/src/main/java/com/fs/app/exception/FSException.java
  81. 88 0
      fs-qwhook-sop/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  82. 70 0
      fs-qwhook-sop/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java
  83. 14 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SendAIParam.java
  84. 23 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParam.java
  85. 24 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamB.java
  86. 68 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamDetails.java
  87. 30 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamDetailsB.java
  88. 28 0
      fs-qwhook-sop/src/main/java/com/fs/app/params/SopLogsEditParam.java
  89. 24 0
      fs-qwhook-sop/src/main/java/com/fs/app/redis/RedisConfiguration.java
  90. 104 0
      fs-qwhook-sop/src/main/java/com/fs/app/redis/RedisKeyExpirationListener.java
  91. 249 0
      fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java
  92. 26 0
      fs-qwhook-sop/src/main/java/com/fs/app/utils/JsonUtils.java
  93. 87 0
      fs-qwhook-sop/src/main/java/com/fs/app/utils/JwtUtils.java
  94. 182 0
      fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  95. 73 0
      fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  96. 244 0
      fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/LogAspect.java
  97. 117 0
      fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  98. 31 0
      fs-qwhook-sop/src/main/java/com/fs/framework/config/ApplicationConfig.java
  99. 85 0
      fs-qwhook-sop/src/main/java/com/fs/framework/config/CaptchaConfig.java
  100. 109 0
      fs-qwhook-sop/src/main/java/com/fs/framework/config/DataSourceConfig.java

+ 2 - 2
fs-admin/src/main/resources/application-dev.yml

@@ -83,9 +83,9 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://localhost/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://42.194.245.189:3306/test_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: 123456
+                    password: YJF_2024
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 10 - 0
fs-company-app/src/main/resources/application-dev.yml

@@ -125,3 +125,13 @@ spring:
                     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: test-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey

+ 27 - 16
fs-company-app/src/main/resources/application.yml

@@ -5,11 +5,11 @@ fs:
   # 版本
   version: 1.1.0
   # 版权年份
-  copyrightYear: 2020
+  copyrightYear: 2021
   # 实例演示开关
-  demoEnabled: false
+  demoEnabled: true
   # 文件路径 示例( Windows配置D:/fs/uploadPath,Linux配置 /home/fs/uploadPath)
-  profile: C:/fs/uploadPath
+  profile: c:/fs/uploadPath
   # 获取ip地址开关
   addressEnabled: false
   # 验证码类型 math 数组计算 char 字符验证
@@ -19,12 +19,12 @@ fs:
     # 加密秘钥
     secret: f4e2e52034348f86b67cde581c0f9eb5
     # token有效时长,7天,单位秒
-    expire: 604800
+    expire: 31536000
     header: AppToken
 
 # 开发环境配置
 server:
-  # 服务器的HTTP端口,默认为
+  # 服务器的HTTP端口,默认为 7011  store 7111
   port: 7015
   servlet:
     # 应用的访问路径
@@ -40,32 +40,44 @@ server:
 # 日志配置
 logging:
   level:
-    com.fs: debug
+    com.fs: info
     org.springframework: warn
-    org.springframework.web: info
-    cn.binarywang.wx.miniapp: debug
 
 # Spring配置
 spring:
+  mvc:
+    async:
+      request-timeout: 30000
   # 资源信息
   messages:
     # 国际化资源文件路径
     basename: i18n/messages
   profiles:
-    active: druid-test
+    active: dev
     include: config
   # 文件上传
   servlet:
      multipart:
        # 单个文件大小
-       max-file-size:  10MB
+       max-file-size:  200MB
        # 设置总上传的文件大小
-       max-request-size:  20MB
+       max-request-size:  200MB
+#       enabled: false
   # 服务模块
   devtools:
     restart:
       # 热部署开关
       enabled: true
+
+
+# token配置
+token:
+    # 令牌自定义标识
+    header: Authorization
+    # 令牌密钥
+    secret: abcdefghijklmnopqrstuvwxyz
+    # 令牌有效期(默认30分钟)
+    expireTime: 180
 mybatis-plus:
   # 搜索指定包别名
   typeAliasesPackage: com.fs.**.domain,com.fs.**.bo,com.fs.**.vo
@@ -90,7 +102,6 @@ mybatis-plus:
     defaultExecutorType: REUSE
     # 允许 JDBC 支持自动生成主键
     useGeneratedKeys: true
-
 # MyBatis配置
 mybatis:
     # 搜索指定包别名
@@ -103,9 +114,10 @@ mybatis:
 # PageHelper分页插件
 pagehelper:
   helperDialect: mysql
-  reasonable: false
-  supportMethodsArguments: true
+  reasonable: false #超出后不显示
+  supportMethodsArguments: false
   params: count=countSql
+
 # Swagger配置
 swagger:
   # 是否开启swagger
@@ -118,7 +130,6 @@ xss:
   # 过滤开关
   enabled: true
   # 排除链接(多个用逗号分隔)
-  excludes: /system/notice/*
+  excludes: /system/notice/*,/system/config/*
   # 匹配链接
   urlPatterns: /system/*,/monitor/*,/tool/*
-

+ 3 - 0
fs-company/src/main/java/com/fs/store/controller/FsStoreProductController.java

@@ -3,6 +3,7 @@ package com.fs.store.controller;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.core.security.SecurityUtils;
 import com.fs.store.domain.*;
 import com.fs.store.param.FsProductAttrValueParam;
 import com.fs.store.service.IFsStoreProductAttrValueService;
@@ -41,6 +42,8 @@ public class FsStoreProductController extends BaseController
     {
         startPage();
         fsStoreProduct.setIsDel(0);
+        // 只能展示属于本公司的产品
+        fsStoreProduct.setCompanyIds(String.valueOf(SecurityUtils.getLoginUser().getCompany().getCompanyId()));
         List<FsStoreProductListVO> list = fsStoreProductService.selectFsStoreProductListVO(fsStoreProduct);
         return getDataTable(list);
     }

+ 2 - 6
fs-qw-api/src/main/java/com/fs/app/controller/QwController.java

@@ -52,12 +52,8 @@ public class QwController {
     @GetMapping("/qw/test/{corpId}")
     public String test(@PathVariable String corpId )throws Exception {
         System.out.println("11111111111111");
-
-            // 尝试读取一个不存在的文件
-        FileInputStream fileInputStream = new FileInputStream("non_existent_file.txt");
-
-
-
+       // 尝试读取一个不存在的文件
+       // FileInputStream fileInputStream = new FileInputStream("non_existent_file.txt");
         return "ok";
     }
     /**

+ 142 - 0
fs-qwhook-msg/pom.xml

@@ -0,0 +1,142 @@
+<?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>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>fs-qwhook-msg</artifactId>
+
+    <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>
+
+        <!-- 验证码 -->
+        <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-system</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>5.1.10.RELEASE</version>
+        </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>-->
+
+    </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-qwhook-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(FsQwhookApplication.class);
+    }
+}

+ 27 - 0
fs-qwhook-msg/src/main/java/com/fs/FsQwhookApplication.java

@@ -0,0 +1,27 @@
+package com.fs;
+
+
+import com.fs.common.core.redis.RedisCache;
+import org.springframework.beans.factory.annotation.Autowired;
+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 FsQwhookApplication {
+
+
+
+    public static void main(String[] args)
+    {
+        SpringApplication.run(FsQwhookApplication.class, args);
+        System.out.println("QWHookAPI启动成功");
+
+    }
+}

+ 12 - 0
fs-qwhook-msg/src/main/java/com/fs/app/annotation/Login.java

@@ -0,0 +1,12 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * app登录效验
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Login {
+}

+ 15 - 0
fs-qwhook-msg/src/main/java/com/fs/app/annotation/LoginUser.java

@@ -0,0 +1,15 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录用户信息
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LoginUser {
+
+}

+ 58 - 0
fs-qwhook-msg/src/main/java/com/fs/app/config/ArrayStringTypeHandler.java

@@ -0,0 +1,58 @@
+package com.fs.app.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);
+    }
+}

+ 43 - 0
fs-qwhook-msg/src/main/java/com/fs/app/config/SystemStartSopService.java

@@ -0,0 +1,43 @@
+package com.fs.app.config;//package com.fs.app.config;
+//
+//import com.fs.app.websocket.service.WebSocketServer;
+//import com.fs.common.core.domain.R;
+//import com.fs.common.core.redis.RedisCache;
+//import com.fs.common.utils.StringUtils;
+//import lombok.SneakyThrows;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.boot.CommandLineRunner;
+//import org.springframework.stereotype.Component;
+//
+///**
+//* 项目启动得时候,启动
+//*/
+//@Component
+//public class SystemStartSopService implements CommandLineRunner {
+//
+//    @Autowired
+//    WebSocketServer webSocketServer;
+//    @Autowired
+//    RedisCache redisCache;
+//
+//    @Override
+//    public void run(String... args) throws Exception {
+//
+//
+//        String sopFlag=redisCache.getCacheObject("sopFlag");
+//        if(StringUtils.isNotEmpty(sopFlag)&&sopFlag.equals("start")){
+//            //有了删了
+//            redisCache.deleteObject("sopFlag");
+//        }
+//
+//        Thread t1 = new Thread() {
+//            @SneakyThrows
+//            @Override
+//            public void run() {
+//                webSocketServer.sendSopMsg();
+//            }
+//        };
+//        t1.start();
+//        System.out.println("StartSopService已启动");
+//    }
+//}

+ 29 - 0
fs-qwhook-msg/src/main/java/com/fs/app/config/WebMvcConfig.java

@@ -0,0 +1,29 @@
+package com.fs.app.config;
+
+
+import com.fs.app.interceptor.AuthorizationInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * MVC配置
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+    @Autowired
+    private AuthorizationInterceptor authorizationInterceptor;
+//    @Autowired
+//    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**");
+    }
+//
+//    @Override
+//    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
+//        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
+//    }
+}

+ 306 - 0
fs-qwhook-msg/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,306 @@
+package com.fs.app.controller;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.app.params.SendSopParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
+import com.fs.fastGpt.service.AiService;
+import com.fs.store.domain.FsAppVersion;
+import com.fs.store.service.IFsAppVersionService;
+import com.fs.qw.domain.*;
+import com.fs.qw.mapper.*;
+import com.fs.qw.param.QwConfigSignatureParam;
+import com.fs.qw.service.*;
+import com.fs.qw.vo.QwHookAuthVO;
+import com.fs.qwApi.param.QwExternalContactHParam;
+import com.fs.qwApi.param.QwSendMsgParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.fs.qwHookApi.service.QwHookApiService;
+import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.voice.utils.StringUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+@Api("公共接口")
+@RestController
+@RequestMapping(value="/app/common")
+@Slf4j
+public class CommonController {
+    @Autowired
+    private QwHookApiService qwHookApiService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    FsCourseWatchLogMapper   watchLogMapper;
+    @Autowired
+    IQwJsApiService qwGetJsapiTicketService;
+
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
+
+    @Autowired
+    QwExternalContactCrmMapper qwExternalContactCrmMapper;
+    @Autowired
+    private IFsAppVersionService appVersionService;
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    private AiService aiService;
+
+    @Autowired
+    private IQwUserVideoService qwUserVideoService;
+
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+
+    @PostMapping("/qwHookNotify")
+    public R qwHookNotify(@RequestBody String body) {
+        QwHookVO vo= JSONUtil.toBean(body,QwHookVO.class);
+        QwHookMsgVO msgVo= JSONUtil.toBean(vo.getData(),QwHookMsgVO.class);
+        if (StringUtil.strIsNullOrEmpty(msgVo.getKey())){
+            return R.ok();
+        }
+        msgVo.setKey(msgVo.getKey().replaceAll("[^a-zA-Z0-9\\s]", ""));
+        if(vo.getType().equals(104012)||vo.getType().equals(104014)){
+            //文件消息
+           return aiService.qwHookNotifyAiReply(msgVo,vo.getType());
+        }
+        //本地消息
+        else if(vo.getType().equals(101003)){
+            //文件消息
+            rocketMQTemplate.syncSend("msg", JSON.toJSONString(msgVo));
+            //文件消息
+            return R.ok();
+        }
+        else if(vo.getType().equals(101007)||vo.getType().equals(101008)||vo.getType().equals(101004)||vo.getType().equals(101001)){
+           //转人工
+            aiService.artificial(vo);
+        }
+        else if(vo.getType().equals(104001)){
+            //登录成功
+            updateQwUserByAppKey(msgVo.getKey(),1L,msgVo.getVersion());
+            //成功后 清除redis中的二维码
+            redisCache.deleteObject("QwLoginCode:"+msgVo.getKey());
+
+        }else if (vo.getType().equals(103006)){
+            //弹出二维码了-那就是掉线了,改状态
+            updateQwUserByAppKey(msgVo.getKey(),0L,msgVo.getVersion());
+            //还是存redis,二维码一分钟过期
+            redisCache.setCacheObject("QwLoginCode:"+msgVo.getKey().trim(),msgVo.getQr_code(),1, TimeUnit.MINUTES);
+
+            String msg="<font color=\"warning\">您的云主机,企业微信登录需要【重新扫描二维码】!请扫描二维码登录</font>";
+            sendQwMsg(msgVo.getKey(),msg);
+
+        }else if (vo.getType().equals(104002)){
+            //退出登录
+            updateQwUserByAppKey(msgVo.getKey(),0L,msgVo.getVersion());
+            redisCache.deleteObject("QwLoginCode:"+msgVo.getKey());
+
+            String msg="<font color=\"warning\">您的云主机,企业微信【已退出登录】!!望悉知~</font>";
+            sendQwMsg(msgVo.getKey(),msg);
+
+        }else if (vo.getType().equals(104000)){
+
+            //客户端关闭
+            updateQwUserByAppKey(msgVo.getKey(),0L,msgVo.getVersion());
+
+            redisCache.deleteObject("QwLoginCode:"+msgVo.getKey());
+
+        }
+        //重新登录
+        else if (vo.getType().equals(108022)){
+            SendSopParam sopParam=new SendSopParam();
+            sopParam.setCmd("login");
+            sopParam.setKey(msgVo.getKey());
+            qwUserMapper.updateQwUserRemarkByAppKey(msgVo.getKey());
+        }else if (vo.getType().equals(108024)){
+
+            //企微崩溃
+            qwUserMapper.updateQwUserDelRemarkByAppKey(msgVo.getKey());
+
+            String msg="<font color=\"warning\">您的云主机,企业微信【崩溃】啦!!请联系管理员处理~~望悉知~</font>";
+            sendQwMsg(msgVo.getKey(),msg);
+        }
+        //接收到视频号
+        else if (vo.getType().equals(104017)){
+            QwUserVideo userVideo=new QwUserVideo();
+            userVideo.setAppKey(msgVo.getKey());
+            userVideo.setSenderName(msgVo.getSender_name());
+            userVideo.setNickName(msgVo.getNickname());
+            userVideo.setObjectId(msgVo.getObjectId());
+            userVideo.setCoverUrl(msgVo.getCover_url());
+            userVideo.setThumbUrl(msgVo.getThumb_url());
+            userVideo.setAvatar(msgVo.getAvatar());
+            userVideo.setDesc(msgVo.getDesc());
+            userVideo.setUrl(msgVo.getUrl());
+            userVideo.setExtras(msgVo.getExtras());
+            qwUserVideoService.insertQwUserVideo(userVideo);
+        }
+        else if (vo.getType().equals(102019)){
+
+            handleSopBlockOrDel(msgVo,5);
+        }
+        else if (vo.getType().equals(102018)){
+
+            handleSopBlockOrDel(msgVo,6);
+        }
+        return R.ok();
+    }
+
+    /**
+    * 处理拉黑或删除的客户的营期
+    */
+    private void handleSopBlockOrDel(QwHookMsgVO msgVo,Integer status){
+        //处理拉黑的
+        String appKey = msgVo.getKey();
+        //客户编号
+        String receiverOpenid = msgVo.getReceiver_openid();
+
+        if (StringUtil.strIsNullOrEmpty(appKey) || StringUtil.strIsNullOrEmpty(receiverOpenid)){
+            log.error("删除或拉黑-处理营期失败数据不对:"+appKey+"|"+receiverOpenid);
+            return;
+        }
+
+        try {
+            QwUser qwUser = qwUserMapper.selectQwUserByAppKey(appKey);
+            if (qwUser!=null){
+
+                //修改客户状态
+                QwExternalContact contact=new QwExternalContact();
+                contact.setStatus(status);
+                contact.setUserId(qwUser.getQwUserId());
+                contact.setCorpId(qwUser.getCorpId());
+                contact.setExternalUserId(receiverOpenid);
+                qwExternalContactMapper.updateQwExternalContactByUseridBlock(contact);
+
+
+                log.info("删除或拉黑-处理营期-"+qwUser.getQwUserId()+"|"+qwUser.getCorpId()+"|"+receiverOpenid);
+
+                //删除营期
+                sopUserLogsInfoMapper.deleteByQwUserIdAndCorpIdToContactId(qwUser.getQwUserId(),qwUser.getCorpId(),receiverOpenid);
+
+            }
+
+        }catch (Exception e){
+            log.error("删除或拉黑-处理营期失败:"+appKey+"|"+receiverOpenid+"|"+e.getMessage());
+        }
+
+    }
+
+    /**
+    * 给企业微信的应用发送消息
+    */
+    private void sendQwMsg(String appKey,String msg){
+        QwUser qwUserByAppKey = qwUserMapper.selectQwUserByAppKey(appKey);
+
+        QwSendMsgParam sendMsgParam = new QwSendMsgParam();
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(qwUserByAppKey.getCorpId());
+        sendMsgParam.setAgentid(Integer.parseInt(qwCompany.getServerAgentId().trim()));
+        sendMsgParam.setTouser(qwUserByAppKey.getQwUserId());
+
+        QwSendMsgParam.Markdown markdown = new QwSendMsgParam.Markdown();
+        markdown.setContent(msg);
+
+        sendMsgParam.setMarkdown(markdown);
+        sendMsgParam.setMsgtype("markdown");
+        qwApiService.sendMsg(sendMsgParam, qwCompany.getCorpId());
+
+    }
+    /**
+    * 修改企业微信登录状态
+    */
+    private void  updateQwUserByAppKey(String appKey,Long status,String version){
+        QwUser qwUser=new QwUser();
+        qwUser.setAppKey(appKey);
+        qwUser.setLoginStatus(status);
+        qwUser.setVersion(version);
+        qwUserMapper.updateQwUserByAppKey(qwUser);
+    }
+
+    @PostMapping("/qwHookSendMsg")
+    public R qwHookSendMsg(@RequestBody QwHookSendMsgParam param ) {
+        param.setClientId(2);
+        return qwHookApiService.sendMsg(param);
+    }
+
+    @GetMapping("/qwHookAuth")
+    public R qwHookAuth(@RequestParam(value = "key", required = false) String key) {
+        QwHookAuthVO qwHookAuthVO = qwUserService.selectQwUserByAppKeyAuth(key);
+        if(qwHookAuthVO!=null){
+            return R.ok().put("data",qwHookAuthVO);
+        }
+        else {
+            return R.error("未查询到相关成员信息,请检查授权码是否正确");
+        }
+    }
+
+    @GetMapping("/qwHookCheck")
+    public R qwHookCheckCorpId(@RequestParam(value = "key", required = false) String key,@RequestParam(value = "qwHookId", required = false) String qwHookId,@RequestParam(value = "corpId", required = false) String corpId) {
+        QwUser user=qwUserService.selectQwUserByAppKey(key);
+        if(user.getCorpId().equals(corpId)){
+            if(user.getQwHookId().equals(qwHookId)){
+                return R.ok();
+            }
+            else{
+                return R.error("此帐号绑定的企业微信未授权");
+            }
+        }
+        else{
+           return R.error("此帐号绑定的企业微信未授权");
+        }
+    }
+    //获取企业微信签名
+    @PostMapping("/getConfigSignature")
+    public R getConfigSignature(@RequestBody QwConfigSignatureParam qwConfigSignatureParam) throws Exception {
+        return qwGetJsapiTicketService.getQwJsapiTicket(qwConfigSignatureParam);
+    }
+
+    //根据userid和外部联系人id获取到客户详情
+    @PostMapping("/getQwExternalContactDetails")
+    public R getQwExternalContactDetails(@RequestBody QwExternalContactHParam param){
+        return qwGetJsapiTicketService.getQwExternalContactDetails(param);
+    }
+
+
+
+    @ApiOperation("获取最新版本")
+    @GetMapping("/getNewAppVersion")
+    public R getNewAppVersion(@RequestParam("appType")Integer appType)
+    {
+//        System.out.println("111");
+        FsAppVersion version=appVersionService.getNewVersion(appType,3);
+        return R.ok().put("data",version);
+    }
+
+}

+ 90 - 0
fs-qwhook-msg/src/main/java/com/fs/app/controller/QwSopController.java

@@ -0,0 +1,90 @@
+package com.fs.app.controller;
+
+import com.fs.fastGpt.service.IFastGptChatSessionService;
+import com.fs.qw.param.SopMsgParam;
+import com.fs.qw.result.QwExternalContactByQwResult;
+import com.fs.sop.params.GetQwSopLogsByJsApiParam;
+import com.fs.app.params.SopLogsEditParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.params.SendSopParamDetailsC;
+import com.fs.sop.service.IQwSopLogsService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+@RestController
+@RequestMapping("/app/qwSop")
+public class QwSopController {
+
+
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    IFastGptChatSessionService fastGptChatSessionService;
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+
+
+
+
+
+    /**
+     * 更新AI发送状态
+     */
+    @PostMapping("/updateQwSopLogs")
+    public R updateCourseSopLogs(@RequestBody SopLogsEditParam param){
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            QwSopLogs qwSopLogs=new QwSopLogs();
+            qwSopLogs.setId(param.getId());
+            qwSopLogs.setReceivingStatus(param.getReceivingStatus());
+            qwSopLogs.setSendStatus(param.getSendStatus());
+            qwSopLogs.setRealSendTime(sdf.format(new Date()));
+            qwSopLogs.setRemark(param.getRemark());
+            qwSopLogsService.updateQwSopLogsSendType(qwSopLogs);
+            return R.ok("更新成功");
+        }catch (Exception e){
+            return R.error("更新失败");
+        }
+
+    }
+
+    //主动获取发送信息
+    @PostMapping("/getQwSopLogsByJsApi")
+    public R getQwSopLogsByJsApi(@RequestBody GetQwSopLogsByJsApiParam param) {
+
+        SendSopParamDetailsC qwSopLogsByJsApi = qwSopLogsService.getQwSopLogsByJsApi(param);
+
+        return R.ok().put("data",qwSopLogsByJsApi);
+    }
+
+    //获取销售的某个联系人
+    @GetMapping("/getExternalContactByAppKey/{appKey}")
+    public R getExternalContactByAppKey(@PathVariable("appKey") String appKey) {
+        QwExternalContactByQwResult result=qwSopLogsService.getExternalContactByAppKey(appKey);
+        return R.ok().put("data",result);
+    }
+
+    //清除不是当前员工的 外部联系以及营期
+    @PostMapping("/deleteQwSopLogsByJsApi")
+    public R deleteQwSopLogsByJsApi(@RequestBody GetQwSopLogsByJsApiParam param) {
+        return qwSopLogsService.deleteQwSopLogsByJsApi(param);
+    }
+    //获取侧边栏 显示的记录
+    @GetMapping("/getQwSopLogs")
+    public R getQwSopLogs(SopMsgParam param) throws Exception {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        System.out.println("传参:"+param);
+        List<QwSopLogs> list = qwSopLogsService.selectQwSopLogsListVO(param);
+        System.out.println("参数:"+list);
+        PageInfo<QwSopLogs> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+
+}

+ 30 - 0
fs-qwhook-msg/src/main/java/com/fs/app/controller/RocketMQAiMsgService.java

@@ -0,0 +1,30 @@
+package com.fs.app.controller;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
+import com.fs.fastGpt.service.AiService;
+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 AiService aiService;
+    @Override
+    public void onMessage(String message) {
+
+        QwHookMsgVO msgVo= JSONUtil.toBean(message,QwHookMsgVO.class);
+        aiService.qwHookNotifyAddMsg(msgVo);
+    }
+}

+ 31 - 0
fs-qwhook-msg/src/main/java/com/fs/app/controller/RocketMQWatchLogService.java

@@ -0,0 +1,31 @@
+package com.fs.app.controller;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.impl.FsCourseWatchLogServiceImpl;
+import com.fs.fastGpt.service.AiService;
+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;
+
+import java.util.List;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+@RocketMQMessageListener(topic = "watchLog", consumerGroup = "watchLog-group")
+public class RocketMQWatchLogService implements RocketMQListener<String> {
+
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+    @Override
+    public void onMessage(String message) {
+            fsCourseWatchLogService.addCourseWatchLogDay();
+    }
+}

+ 51 - 0
fs-qwhook-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;
+	}
+	
+	
+}

+ 81 - 0
fs-qwhook-msg/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,81 @@
+package com.fs.app.exception;
+
+
+
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+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;
+
+
+/**
+ * 异常处理器
+ */
+@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);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+
+
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		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){
+
+		return R.error(e.getMessage());
+	}
+}

+ 70 - 0
fs-qwhook-msg/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -0,0 +1,70 @@
+package com.fs.app.interceptor;
+
+
+import com.fs.app.annotation.Login;
+import com.fs.app.exception.FSException;
+import com.fs.app.utils.JwtUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+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;
+
+/**
+ * 权限(Token)验证
+ */
+@Component
+public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
+    @Autowired
+    private JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+    public static final String USER_KEY = "userId";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        Login annotation;
+        if(handler instanceof HandlerMethod) {
+            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
+        }else{
+            return true;
+        }
+
+        if(annotation == null){
+            return true;
+        }
+
+        //获取用户凭证
+        String token = request.getHeader(jwtUtils.getHeader());
+        if(StringUtils.isBlank(token)){
+            token = request.getParameter(jwtUtils.getHeader());
+        }
+
+        //凭证为空
+        if(StringUtils.isBlank(token)){
+            throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        Claims claims = jwtUtils.getClaimByToken(token);
+        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        //查询用户的TOKEN是否和REDIS中的一样
+//        String redisToken=redisCache.getCacheObject("AiChatToken:"+ Long.parseLong(claims.getSubject()));
+//        if(redisToken==null||!redisToken.equals(token)){
+//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+//        }
+//        long l = Long.parseLong(claims.getSubject());
+        //设置userId到request里,后续根据userId,获取用户信息
+        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
+
+        return true;
+    }
+}

+ 14 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SendAIParam.java

@@ -0,0 +1,14 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+@Data
+public class SendAIParam {
+
+    private String cmd;
+
+    private String key;
+
+    private String data;
+
+}

+ 23 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParam.java

@@ -0,0 +1,23 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParam implements Serializable {
+    /**
+    * 固定SendSop
+    */
+    private String cmd;
+    /**
+    * sopLogs记录
+    */
+    private String data;
+    /**
+    * 用户的appKey
+    */
+    private String key;
+}

+ 24 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamB.java

@@ -0,0 +1,24 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParamB implements Serializable {
+
+    /**
+     * 固定SendSop
+     */
+    private String cmd;
+    /**
+     * sopLogs记录
+     */
+    private List<QwSopTempSetting.Content.Setting> data;
+    /**
+     * 用户的appKey
+     */
+    private String key;
+}

+ 68 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamDetails.java

@@ -0,0 +1,68 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class SendSopParamDetails implements Serializable {
+
+    /**
+    * QwSopLog的主键
+    */
+    private String sopLogId;
+    /**
+    * 员工的id
+    */
+    private String qwUserid;
+
+    /**
+    * 客户的id
+    */
+    private String externalUserId;
+
+
+    /**
+     * 内容类型 文字 图片
+     */
+    private int contentType;
+
+    /**
+     * 消息内容
+     */
+    private String value;
+
+    /**
+     *  内容类型是图片时的 图片url
+     */
+    private String imgUrl;
+
+    /**
+     * 链接标题
+     */
+    private String linkTitle;
+
+    /**
+     * 链接描述
+     */
+    private String linkDescribe;
+
+    /**
+     * 链接封面
+     */
+    private String linkImageUrl;
+
+    /**
+     * 链接url
+     */
+    private String linkUrl;
+    /**
+    * 文件
+    */
+    private String fileUrl;
+    /**
+    * 视频
+    */
+    private String videoUrl;
+
+}

+ 30 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SendSopParamDetailsB.java

@@ -0,0 +1,30 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParamDetailsB implements Serializable {
+
+    /**
+     * QwSopLog的主键
+     */
+    private String sopLogId;
+    /**
+     * 员工的id
+     */
+    private String qwUserid;
+
+    /**
+     * 客户的id
+     */
+    private String externalUserId;
+    private String externalUserName;
+
+    private List<QwSopTempSetting.Content.Setting> setting;
+
+
+}

+ 28 - 0
fs-qwhook-msg/src/main/java/com/fs/app/params/SopLogsEditParam.java

@@ -0,0 +1,28 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+@Data
+public class SopLogsEditParam {
+
+    /**
+     * qw_sop_Logs的主键(修改时参数)
+     */
+    private String id;
+
+    /**
+     * 发送(给成员)状态 0发送失败 1发送成功 3待发送
+     */
+    private Long sendStatus;
+
+    /**
+     *  接收(客户的)状态:0-未发送 1-已发送 2发送失败
+     */
+    private Long receivingStatus;
+
+    /**
+    * 备注
+    */
+    private String remark;
+
+}

+ 31 - 0
fs-qwhook-msg/src/main/java/com/fs/app/resolver/LoginUserHandlerMethodArgumentResolver.java

@@ -0,0 +1,31 @@
+package com.fs.app.resolver;
+
+
+/**
+ * @LoginUser注解的方法参数,注入当前登录用户
+ */
+//@Component
+//public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
+//    @Autowired
+//    private UserService userService;
+//
+//    @Override
+//    public boolean supportsParameter(MethodParameter parameter) {
+//        return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
+//    }
+//
+//    @Override
+//    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
+//                                  NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
+//        //获取用户ID
+//        Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
+//        if(object == null){
+//            return null;
+//        }
+//
+//        //获取用户信息
+//        UserEntity user = userService.getById((Long)object);
+//
+//        return user;
+//    }
+//}

+ 248 - 0
fs-qwhook-msg/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -0,0 +1,248 @@
+package com.fs.app.utils;
+import com.fs.common.exception.ServiceException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class AudioUtils {
+    /**
+     * 工具地址
+     **/
+    static String path = "c:\\";
+
+    public static void main(String[] args) {
+        // TODO: mp3 转 silk
+        transferAudioSilk("路径\\", "文件名.mp3", false);
+        // TODO: wav 转 silk
+        transferAudioSilk("路径\\", "文件名.wav", false);
+        // TODO: mp3转amr
+        transferMp3Amr("路径\\文件名.mp3", "路径\\文件名.amr");
+    }
+
+    /**
+     * MP3/WAV转SILk格式
+     * @param filePath 例:D:\\file\\audio.mp3
+     * @param isSource isSource 是否清空原文件
+     * @return
+     */
+    public static String transferAudioSilk(String filePath, boolean isSource){
+        Integer index = filePath.lastIndexOf("\\") + 1;
+        return transferAudioSilk(filePath.substring(0, index), filePath.substring(index, filePath.length()), isSource);
+    }
+
+    /**
+     * MP3/WAV转SILk格式
+     *
+     * @param path 文件路径 例:D:\\file\\
+     * @param name 文件名称 例:audio.mp3/audio.wav
+     * @param isSource 是否清空原文件
+     * @return silk文件路径
+     * @throws Exception
+     */
+    public static String transferAudioSilk(String path, String name, boolean isSource) {
+        try {
+            // 判断后缀格式
+            String suffix = name.split("\\.")[1];
+            if (!suffix.toLowerCase().equals("mp3") && !suffix.toLowerCase().equals("wav")) {
+                throw new ServiceException("文件格式必须是mp3/wav");
+            }
+            String filePath = path + name;
+            File file = new File(filePath);
+            if (!file.exists()) {
+                throw new Exception("文件不存在!");
+            }
+            // 文件名时拼接
+            SimpleDateFormat ttime = new SimpleDateFormat("yyyyMMddhhMMSS");
+            String time = ttime.format(new Date());
+            // 导出的pcm格式路径
+            String pcmPath = path + "PCM_" + time + ".pcm";
+            // 先将mp3/wav转换成pcm格式
+            transferAudioPcm(filePath, pcmPath);
+            // 导出的silk格式路径
+            String silkPath = path + "SILK_" + time + ".silk";
+            // 转换成silk格式
+            transferPcmSilk(pcmPath, silkPath);
+            // 删除pcm文件
+            File pcmFile = new File(pcmPath);
+            if (pcmFile.exists()) {
+                pcmFile.delete();
+            }
+            if (isSource) {
+                File audioFile = new File(filePath);
+                if (audioFile.exists()) {
+                    audioFile.delete();
+                }
+            }
+            return silkPath;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 调用ffmpeg,wav转 pcm
+     *
+     * @param wavPath wav文件地址
+     * @param target  转后文件地址
+     */
+    public static void transferWavPcm (String wavPath, String target) {
+        // ffmpeg -i input.wav -f s16le -ar 44100 -acodec pcm_s16le output.raw
+        transferAudioPcm(wavPath, target);
+    }
+
+    /**
+     * 调用ffmpeg,mp3转 pcm
+     *
+     * @param mp3Path mp3文件地址
+     * @param target  转后文件地址
+     */
+    public static void transferMp3Pcm(String mp3Path, String target) {
+        //ffmpeg -y -i 源文件 -f s16le -ar 24000 -ac 1 转换后文件位置
+        transferAudioPcm(mp3Path, target);
+    }
+
+    /**
+     * mp3/wav 通用
+     * @param fpath
+     * @param target
+     */
+    private static void transferAudioPcm(String fpath, String target) {
+        List<String> commend = new ArrayList<String>();
+        commend.add(path + "ffmpeg.exe");
+        commend.add("-y");
+        commend.add("-i");
+        commend.add(fpath);
+        commend.add("-f");
+        commend.add("s16le");
+        commend.add("-ar");
+        commend.add("24000");
+        commend.add("-ac");
+        commend.add("1");
+        commend.add(target);
+        try {
+            ProcessBuilder builder = new ProcessBuilder();
+            builder.command(commend);
+            Process p = builder.start();
+            p.waitFor();
+            p.destroy();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * silk_v3_encoder.exe,转成Silk格式
+     * @param pcmPath pcm 文件地址
+     * @param target  转换后的silk地址
+     * silk_v3_encoder.exe 路径
+     * pcm文件地址
+     * silk输出地址
+     * -Fs_API <Hz>            : API sampling rate in Hz, default: 24000
+     * -Fs_maxInternal <Hz>    : Maximum internal sampling rate in Hz, default: 24000
+     * -packetlength <ms>      : Packet interval in ms, default: 20
+     * -rate <bps>            : Target bitrate;   default: 25000
+     * -loss <perc>          : Uplink loss estimate, in percent (0-100);  default: 0
+     * -complexity <comp>   : Set complexity, 0: low, 1: medium, 2: high; default: 2
+     * -DTX <flag>          : Enable DTX (0/1); default: 0
+     * -quiet               : Print only some basic values
+     * -tencent             : Compatible with QQ/Wechat
+     */
+    public static void transferPcmSilk(String pcmPath, String target) {
+        Process process = null;
+        try {
+            /**
+             // 1、这一节的,语音长度太长会使音频长度丢失
+             List<String> commend = new ArrayList<>();
+             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
+             commend.add(path + "silk_v3_encoder.exe");
+             commend.add(pcmPath);
+             commend.add(target);
+             commend.add("-tencent");
+             ProcessBuilder builder = new ProcessBuilder();
+             builder.command(commend);
+             process = builder.start();
+             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
+             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
+             */
+            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
+            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
+            process .waitFor();
+            Thread.sleep(1000);
+            // 有更好的方法会后续慢慢更新..
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (process != null) {
+                process.destroy();
+            }
+        }
+    }
+
+
+    /**
+     * mp3转amr(低质量qq语音)
+     * @param mp3Path MP3文件地址
+     * @param target 转换后文件地址
+     * return
+     */
+    public static void transferMp3Amr(String mp3Path, String target) {
+        // 被转换文件地址
+        File source = new File(path);
+        try {
+            if (!source.exists()) {
+                throw new Exception("文件不存在!");
+            }
+            List<String> commend = new ArrayList<String>();
+            commend.add(path + "ffmpeg.exe");
+            commend.add("-y");
+            commend.add("-i");
+            commend.add(mp3Path);
+            commend.add("-ac");
+            commend.add("1");
+            commend.add("-ar");
+            commend.add("8000");
+            commend.add(target);
+            try {
+                ProcessBuilder builder = new ProcessBuilder();
+                builder.command(commend);
+                Process p = builder.start();
+                p.waitFor();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } catch (Exception e) {
+            System.out.println("mp3转amr异常");
+        }
+    }
+
+    /**
+     * 一个音频转byte类型的方法
+     * @param filePath
+     * @return
+     */
+    public static byte[] byteAudio(String filePath) {
+        try {
+            InputStream inStream = new FileInputStream(filePath);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inStream.read(buffer)) > 0) {
+                baos.write(buffer, 0, bytesRead);
+            }
+            inStream.close();
+            baos.close();
+            return baos.toByteArray();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+}

+ 26 - 0
fs-qwhook-msg/src/main/java/com/fs/app/utils/JsonUtils.java

@@ -0,0 +1,26 @@
+package com.fs.app.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+
+public class JsonUtils {
+    private static final ObjectMapper JSON = new ObjectMapper();
+
+    static {
+        JSON.setSerializationInclusion(Include.NON_NULL);
+        JSON.configure(SerializationFeature.INDENT_OUTPUT, Boolean.TRUE);
+    }
+
+    public static String toJson(Object obj) {
+        try {
+            return JSON.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}

+ 87 - 0
fs-qwhook-msg/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -0,0 +1,87 @@
+package com.fs.app.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * jwt工具类
+
+ */
+@ConfigurationProperties(prefix = "fs.jwt")
+@Component
+public class JwtUtils {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+
+    private String secret;
+    private long expire;
+    private String header;
+
+    /**
+     * 生成jwt token
+     */
+    public String generateToken(long userId) {
+        Date nowDate = new Date();
+        //过期时间
+        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
+
+        return Jwts.builder()
+                .setHeaderParam("typ", "JWT")
+                .setSubject(userId+"")
+                .setIssuedAt(nowDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    public Claims getClaimByToken(String token) {
+        try {
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        }catch (Exception e){
+            logger.debug("validate is token error ", e);
+            return null;
+        }
+    }
+
+    /**
+     * token是否过期
+     * @return  true:过期
+     */
+    public boolean isTokenExpired(Date expiration) {
+        return expiration.before(new Date());
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public String getHeader() {
+        return header;
+    }
+
+    public void setHeader(String header) {
+        this.header = header;
+    }
+}

+ 182 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+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
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser 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, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole 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 sys_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 sys_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 ");
+                }
+            }
+        }
+
+        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;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-qwhook-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);
+    }
+}

+ 245 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,245 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+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.validation.BindingResult;
+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.Collection;
+import java.util.Iterator;
+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 = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            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, SysOperLog 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, SysOperLog 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 (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     * 
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 117 - 0
fs-qwhook-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-qwhook-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());
+    }
+}

+ 85 - 0
fs-qwhook-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;
+    }
+}

+ 109 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,109 @@
+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.clickhouse")
+    public DataSource clickhouseDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,
+                                        @Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("sopDataSource") DataSource sopDataSource,
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+
+        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource); // Ensure matching key
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.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-qwhook-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-qwhook-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-qwhook-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();
+    }
+}

+ 133 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,133 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.StringUtils;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+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();
+    }
+}

+ 121 - 0
fs-qwhook-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-qwhook-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);
+    }
+}

+ 51 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,51 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+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;
+
+/**
+ * spring security配置
+ *
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * 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 http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .antMatchers("/app/crmCustomer/**").anonymous()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

+ 33 - 0
fs-qwhook-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();
+    }
+}

+ 122 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,122 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.models.auth.In;
+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", "AppToken", "header"));
+        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-qwhook-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 = 100;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 1000;
+
+    // 队列最大长度
+    private int queueCapacity = 3000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 500;
+
+    @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);
+            }
+        };
+    }
+}

+ 20 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/config/WebSocketConfig.java

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

+ 77 - 0
fs-qwhook-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-qwhook-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-qwhook-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();
+    }
+}

+ 56 - 0
fs-qwhook-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-qwhook-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-qwhook-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-qwhook-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);
+        }
+    }
+}

+ 103 - 0
fs-qwhook-msg/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,103 @@
+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.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.domain.SysLogininfor;
+import com.fs.system.domain.SysOperLog;
+import com.fs.system.service.ISysLogininforService;
+import com.fs.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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 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();
+                // 封装对象
+                SysLogininfor logininfor = new SysLogininfor();
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                // 插入数据
+                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     * 
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final SysOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+            }
+        };
+    }
+}

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

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

+ 149 - 0
fs-qwhook-msg/src/main/resources/application-dev.yml

@@ -0,0 +1,149 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: localhost
+        # 端口,默认为6379
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 20s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        clickhouse:
+            type: com.alibaba.druid.pool.DruidDataSource
+            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://42.194.245.189:3306/rt_fs_his_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: YJF_2024
+                # 从库数据源
+                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://42.194.245.189:3306/test_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: YJF_2024
+                # 初始连接数
+                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: test-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey

+ 140 - 0
fs-qwhook-msg/src/main/resources/application-druid-ylrz.yml

@@ -0,0 +1,140 @@
+# 数据源配置
+spring:
+    # redis 配置
+    redis:
+        # 地址
+        host: localhost
+        # 端口,默认为6379
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 20s
+        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://139.186.211.165: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://42.194.245.189:3306/rt_fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: YJF_2024
+                # 从库数据源
+                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://42.194.245.189:3306/test_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: YJF_2024
+                # 初始连接数
+                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
+

+ 143 - 0
fs-qwhook-msg/src/main/resources/application.yml

@@ -0,0 +1,143 @@
+# 项目相关配置
+fs:
+  # 名称
+  name: fs
+  # 版本
+  version: 1.1.0
+  # 版权年份
+  copyrightYear: 2021
+  # 实例演示开关
+  demoEnabled: true
+  # 文件路径 示例( Windows配置D:/fs/uploadPath,Linux配置 /home/fs/uploadPath)
+  profile: c:/fs/uploadPath
+  # 获取ip地址开关
+  addressEnabled: false
+  # 验证码类型 math 数组计算 char 字符验证
+  captchaType: math
+
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 7777
+  servlet:
+    # 应用的访问路径
+    context-path: /
+  tomcat:
+    # tomcat的URI编码
+    uri-encoding: UTF-8
+    # tomcat最大线程数,默认为200
+    max-threads: 1200
+    # Tomcat启动初始化的线程数,默认值25
+    min-spare-threads: 30
+
+# 日志配置
+logging:
+  level:
+    com.fs: info
+    org.springframework: warn
+
+# Spring配置
+spring:
+  # 资源信息
+  messages:
+    # 国际化资源文件路径
+    basename: i18n/messages
+  profiles:
+    active: dev
+#    active: druid-yjf
+#    active: dev
+    include: config
+  # 文件上传
+  servlet:
+     multipart:
+       # 单个文件大小
+       max-file-size:  10MB
+       # 设置总上传的文件大小
+       max-request-size:  20MB
+  # 服务模块
+  devtools:
+    restart:
+      # 热部署开关
+      enabled: true
+hook:
+    path: https://qwtool.ylrzcloud.com
+# token配置
+token:
+    # 令牌自定义标识
+    header: Authorization
+    # 令牌密钥
+    secret: abcdefghijklmnopqrstuvwxyz
+    # 令牌有效期(默认30分钟)
+    expireTime: 180
+mybatis-plus:
+  # 搜索指定包别名
+  typeAliasesPackage: com.fs.**.domain,com.fs.**.bo
+  # 配置mapper的扫描,找到所有的mapper.xml映射文件
+  mapperLocations: classpath*:/mapper/**/*.xml
+  configLocation: classpath:mybatis/mybatis-config.xml
+  # 全局配置
+  global-config:
+    db-config:
+      # 主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
+      idType: AUTO
+      # 字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
+      fieldStrategy: NOT_EMPTY
+    banner: false
+    # 配置
+  configuration:
+    # 驼峰式命名
+    mapUnderscoreToCamelCase: true
+    # 全局映射器启用缓存
+    cacheEnabled: true
+    # 配置默认的执行器
+    defaultExecutorType: REUSE
+    # 允许 JDBC 支持自动生成主键
+    useGeneratedKeys: true
+
+# MyBatis配置
+mybatis:
+    # 搜索指定包别名
+    typeAliasesPackage: com.fs.**.domain
+    # 配置mapper的扫描,找到所有的mapper.xml映射文件
+    mapperLocations: classpath*:mapper/**/*Mapper.xml
+    # 加载全局的配置文件
+    configLocation: classpath:mybatis/mybatis-config.xml
+
+# PageHelper分页插件
+pagehelper:
+  helperDialect: mysql
+  supportMethodsArguments: true
+  params: count=countSql
+
+# Swagger配置
+swagger:
+  # 是否开启swagger
+  enabled: true
+  # 请求前缀
+  pathMapping: /dev-api
+# 防止XSS攻击
+xss:
+  # 过滤开关
+  enabled: true
+  # 排除链接(多个用逗号分隔)
+  excludes: /system/notice,/system/config/*
+  # 匹配链接
+  urlPatterns: /system/*,/monitor/*,/tool/*
+wx:
+  miniapp:
+    configs:
+      - appid: wxbe53e91d9ad11ca6
+        secret: 447135e6ca602fa4745b81216f600615
+        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+  pay:
+    appId: wx782bacb12a6b5d4f #微信公众号或者小程序等的appid
+    mchId: 1518509741 #微信支付商户号
+    mchKey: Jinrichengzhang88888888888888888 #微信支付商户密钥
+    subAppId:  #服务商模式下的子商户公众账号ID
+    subMchId:  #服务商模式下的子商户号
+    keyPath: c:\tools\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl: https://api.haitujia.com/app/wxpay/wxPayNotify
+baidu:
+  token: 123aa

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

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

+ 37 - 0
fs-qwhook-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}]

+ 18 - 0
fs-qwhook-msg/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,18 @@
+<?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.app.config.ArrayStringTypeHandler"/>
+	</typeHandlers>
+</configuration>

+ 142 - 0
fs-qwhook-sop/pom.xml

@@ -0,0 +1,142 @@
+<?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>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>fs-qwhook-sop</artifactId>
+
+    <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>
+
+        <!-- 验证码 -->
+        <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-system</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>5.1.10.RELEASE</version>
+        </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>-->
+
+    </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>

+ 13 - 0
fs-qwhook-sop/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,13 @@
+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(FsQwhookSopApplication.class);
+    }
+}

+ 26 - 0
fs-qwhook-sop/src/main/java/com/fs/FsQwhookSopApplication.java

@@ -0,0 +1,26 @@
+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 FsQwhookSopApplication {
+
+
+    public static void main(String[] args)
+
+    {
+        SpringApplication.run(FsQwhookSopApplication.class, args);
+
+        System.out.println("QWHookSopAPI启动成功");
+
+    }
+}

+ 12 - 0
fs-qwhook-sop/src/main/java/com/fs/app/annotation/Login.java

@@ -0,0 +1,12 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * app登录效验
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Login {
+}

+ 15 - 0
fs-qwhook-sop/src/main/java/com/fs/app/annotation/LoginUser.java

@@ -0,0 +1,15 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录用户信息
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LoginUser {
+
+}

+ 58 - 0
fs-qwhook-sop/src/main/java/com/fs/app/config/ArrayStringTypeHandler.java

@@ -0,0 +1,58 @@
+package com.fs.app.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);
+    }
+}

+ 29 - 0
fs-qwhook-sop/src/main/java/com/fs/app/config/WebMvcConfig.java

@@ -0,0 +1,29 @@
+package com.fs.app.config;
+
+
+import com.fs.app.interceptor.AuthorizationInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * MVC配置
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+    @Autowired
+    private AuthorizationInterceptor authorizationInterceptor;
+//    @Autowired
+//    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**");
+    }
+//
+//    @Override
+//    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
+//        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
+//    }
+}

+ 137 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisCommonController.java

@@ -0,0 +1,137 @@
+package com.fs.app.controller;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.app.params.SendSopParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
+import com.fs.store.domain.FsAppVersion;
+import com.fs.store.service.IFsAppVersionService;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.QwUserVideo;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwExternalContactCrmMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwConfigSignatureParam;
+import com.fs.qw.service.IQwJsApiService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.service.IQwUserVideoService;
+import com.fs.qw.vo.QwHookAuthVO;
+import com.fs.qwApi.param.QwExternalContactHParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.fs.qwHookApi.service.QwHookApiService;
+import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.voice.utils.StringUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.concurrent.TimeUnit;
+
+
+@Api("公共接口")
+@RestController
+@RequestMapping(value="/apis/app/common")
+@Slf4j
+public class ApisCommonController {
+
+    @Autowired
+    private QwHookApiService qwHookApiService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    FsCourseWatchLogMapper   watchLogMapper;
+    @Autowired
+    IQwJsApiService qwGetJsapiTicketService;
+
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
+
+    @Autowired
+    QwExternalContactCrmMapper qwExternalContactCrmMapper;
+    @Autowired
+    private IFsAppVersionService appVersionService;
+    @Autowired
+    RedisCache redisCache;
+
+
+    @Autowired
+    private IQwUserVideoService qwUserVideoService;
+
+
+    @PostMapping("/qwHookSendMsg")
+    public R qwHookSendMsg(@RequestBody QwHookSendMsgParam param ) {
+        param.setClientId(2);
+        return qwHookApiService.sendMsg(param);
+    }
+
+    @GetMapping("/qwHookAuth")
+    public R qwHookAuth(@RequestParam(value = "key", required = false) String key) {
+        QwHookAuthVO qwHookAuthVO = qwUserService.selectQwUserByAppKeyAuth(key);
+        if(qwHookAuthVO!=null){
+            return R.ok().put("data",qwHookAuthVO);
+        }
+        else {
+            return R.error("查询到相关成员信息");
+        }
+    }
+
+    @GetMapping("/qwHookCheck")
+    public R qwHookCheckCorpId(@RequestParam(value = "key", required = false) String key,
+                               @RequestParam(value = "qwHookId", required = false) String qwHookId,
+                               @RequestParam(value = "corpId", required = false) String corpId) {
+        QwUser user=qwUserService.selectQwUserByAppKey(key);
+        if(user.getCorpId().equals(corpId)){
+            if(user.getQwHookId().equals(qwHookId)){
+                return R.ok();
+            }
+            else{
+                return R.error("此帐号绑定的企业微信未授权");
+            }
+        }
+        else{
+           return R.error("此帐号绑定的企业微信未授权");
+        }
+    }
+
+    //获取企业微信签名
+    @PostMapping("/getConfigSignature")
+    public R getConfigSignature(@RequestBody QwConfigSignatureParam qwConfigSignatureParam) throws Exception {
+        return qwGetJsapiTicketService.getQwJsapiTicket(qwConfigSignatureParam);
+    }
+
+    //根据userid和外部联系人id获取到客户详情
+    @PostMapping("/getQwExternalContactDetails")
+    public R getQwExternalContactDetails(@RequestBody QwExternalContactHParam param){
+        return qwGetJsapiTicketService.getQwExternalContactDetails(param);
+    }
+
+    @ApiOperation("获取最新版本")
+    @GetMapping("/getNewAppVersion")
+    public R getNewAppVersion(@RequestParam("appType")Integer appType)
+    {
+        FsAppVersion version=appVersionService.getNewVersion(appType,3);
+        return R.ok().put("data",version);
+    }
+
+}

+ 94 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/ApisQwSopController.java

@@ -0,0 +1,94 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.app.params.SendSopParam;
+import com.fs.app.params.SopLogsEditParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.fastGpt.param.SendHookAIParam;
+import com.fs.fastGpt.service.IFastGptChatSessionService;
+import com.fs.qw.param.QwLoginParam;
+import com.fs.qw.param.SopMsgParam;
+import com.fs.qw.result.QwExternalContactByQwResult;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.params.GetQwSopLogsByJsApiParam;
+import com.fs.sop.params.SendSopParamDetailsC;
+import com.fs.sop.service.IQwSopLogsService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+
+@RestController
+@RequestMapping("/apis/app/qwSop")
+public class ApisQwSopController {
+
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    IFastGptChatSessionService fastGptChatSessionService;
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+    /**
+     * 更新AI发送状态
+     */
+    @PostMapping("/updateQwSopLogs")
+    public R updateCourseSopLogs(@RequestBody SopLogsEditParam param){
+
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            QwSopLogs qwSopLogs=new QwSopLogs();
+            qwSopLogs.setId(param.getId());
+            qwSopLogs.setReceivingStatus(param.getReceivingStatus());
+            qwSopLogs.setSendStatus(param.getSendStatus());
+            qwSopLogs.setRealSendTime(sdf.format(new Date()));
+            qwSopLogs.setRemark(param.getRemark());
+            qwSopLogsService.updateQwSopLogsSendType(qwSopLogs);
+                return  R.ok();
+        }catch (Exception e){
+                return R.error("更新失败");
+            }
+
+    }
+
+    //主动获取发送信息
+    @PostMapping("/getQwSopLogsByJsApi")
+    public R getQwSopLogsByJsApi(@RequestBody GetQwSopLogsByJsApiParam param) {
+
+        SendSopParamDetailsC qwSopLogsByJsApi = qwSopLogsService.getQwSopLogsByJsApi(param);
+
+        return R.ok().put("data",qwSopLogsByJsApi);
+    }
+
+    //获取销售的某个联系人
+    @GetMapping("/getExternalContactByAppKey/{appKey}")
+    public R getExternalContactByAppKey(@PathVariable("appKey") String appKey) {
+
+        QwExternalContactByQwResult result=qwSopLogsService.getExternalContactByAppKey(appKey);
+
+        return R.ok().put("data",result);
+    }
+
+    //清除不是当前员工的 外部联系以及营期
+    @PostMapping("/deleteQwSopLogsByJsApi")
+    public R deleteQwSopLogsByJsApi(@RequestBody GetQwSopLogsByJsApiParam param) {
+
+        return qwSopLogsService.deleteQwSopLogsByJsApi(param);
+
+    }
+
+    @GetMapping("/getQwSopLogs")
+    public R getQwSopLogs(SopMsgParam param) throws Exception {
+        //获取记录
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<QwSopLogs> list = qwSopLogsService.selectQwSopLogsListVO(param);
+        PageInfo<QwSopLogs> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+}

+ 151 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,151 @@
+package com.fs.app.controller;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.app.params.SendSopParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
+import com.fs.store.domain.FsAppVersion;
+import com.fs.store.service.IFsAppVersionService;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.domain.QwUserVideo;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwExternalContactCrmMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwConfigSignatureParam;
+import com.fs.qw.service.IQwJsApiService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.service.IQwUserVideoService;
+import com.fs.qw.vo.QwHookAuthVO;
+import com.fs.qwApi.param.QwExternalContactHParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.fs.qwHookApi.service.QwHookApiService;
+import com.fs.qwHookApi.vo.QwHookMsgVO;
+import com.fs.qwHookApi.vo.QwHookVO;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.GetQwSopLogsByJsApiParam;
+import com.fs.voice.utils.StringUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.concurrent.TimeUnit;
+
+
+@Api("公共接口")
+@RestController
+@RequestMapping(value="/app/common")
+@Slf4j
+public class CommonController {
+
+    @Autowired
+    private QwHookApiService qwHookApiService;
+
+    @Autowired
+    private IQwUserService qwUserService;
+
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    FsCourseWatchLogMapper   watchLogMapper;
+    @Autowired
+    IQwJsApiService qwGetJsapiTicketService;
+
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
+
+    @Autowired
+    QwExternalContactCrmMapper qwExternalContactCrmMapper;
+    @Autowired
+    private IFsAppVersionService appVersionService;
+
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+    @Autowired
+    private IQwUserVideoService qwUserVideoService;
+
+
+
+    @Autowired
+    private QwSopLogsMapper qwSopLogsMapper;
+
+
+    @PostMapping("/qwHookSendMsg")
+    public R qwHookSendMsg(@RequestBody QwHookSendMsgParam param ) {
+        param.setClientId(2);
+        return qwHookApiService.sendMsg(param);
+    }
+
+    @GetMapping("/qwHookAuth")
+    public R qwHookAuth(@RequestParam(value = "key", required = false) String key) {
+        QwHookAuthVO qwHookAuthVO = qwUserService.selectQwUserByAppKeyAuth(key);
+        if(qwHookAuthVO!=null){
+            return R.ok().put("data",qwHookAuthVO);
+        }
+        else {
+            return R.error("查询到相关成员信息");
+        }
+    }
+
+    @GetMapping("/qwHookCheck")
+    public R qwHookCheckCorpId(@RequestParam(value = "key", required = false) String key,
+                               @RequestParam(value = "qwHookId", required = false) String qwHookId,
+                               @RequestParam(value = "corpId", required = false) String corpId) {
+        QwUser user=qwUserService.selectQwUserByAppKey(key);
+        if(user.getCorpId().equals(corpId)){
+            if(user.getQwHookId().equals(qwHookId)){
+                return R.ok();
+            }
+            else{
+                return R.error("此帐号绑定的企业微信未授权");
+            }
+        }
+        else{
+           return R.error("此帐号绑定的企业微信未授权");
+        }
+    }
+
+    //获取企业微信签名
+    @PostMapping("/getConfigSignature")
+    public R getConfigSignature(@RequestBody QwConfigSignatureParam qwConfigSignatureParam) throws Exception {
+        return qwGetJsapiTicketService.getQwJsapiTicket(qwConfigSignatureParam);
+    }
+
+    //根据userid和外部联系人id获取到客户详情
+    @PostMapping("/getQwExternalContactDetails")
+    public R getQwExternalContactDetails(@RequestBody QwExternalContactHParam param){
+        return qwGetJsapiTicketService.getQwExternalContactDetails(param);
+    }
+
+    @ApiOperation("获取最新版本")
+    @GetMapping("/getNewAppVersion")
+    public R getNewAppVersion(@RequestParam("appType")Integer appType)
+    {
+        FsAppVersion version=appVersionService.getNewVersion(appType,3);
+        return R.ok().put("data",version);
+    }
+
+}

+ 97 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/QwSopController.java

@@ -0,0 +1,97 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.app.params.SendSopParam;
+import com.fs.app.params.SopLogsEditParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.fastGpt.param.SendHookAIParam;
+import com.fs.fastGpt.service.IFastGptChatSessionService;
+import com.fs.qw.param.QwLoginParam;
+import com.fs.qw.param.SopMsgParam;
+import com.fs.qw.result.QwExternalContactByQwResult;
+import com.fs.sop.domain.QwSopLogs;
+import com.fs.sop.params.GetQwSopLogsByJsApiParam;
+import com.fs.sop.params.SendSopParamDetailsC;
+import com.fs.sop.service.IQwSopLogsService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+
+@RestController
+@RequestMapping("/app/qwSop")
+public class QwSopController {
+
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    IFastGptChatSessionService fastGptChatSessionService;
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+    /**
+     * 更新AI发送状态
+     */
+    @PostMapping("/updateQwSopLogs")
+    public R updateCourseSopLogs(@RequestBody SopLogsEditParam param){
+
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            QwSopLogs qwSopLogs=new QwSopLogs();
+            qwSopLogs.setId(param.getId());
+            qwSopLogs.setReceivingStatus(param.getReceivingStatus());
+            qwSopLogs.setSendStatus(param.getSendStatus());
+            qwSopLogs.setRealSendTime(sdf.format(new Date()));
+            qwSopLogs.setRemark(param.getRemark());
+
+            qwSopLogsService.updateQwSopLogsSendType(qwSopLogs);
+
+            return R.ok();
+        }catch (Exception e){
+
+            return R.error();
+        }
+
+
+    }
+
+    //主动获取发送信息
+    @PostMapping("/getQwSopLogsByJsApi")
+    public R getQwSopLogsByJsApi(@Valid @RequestBody GetQwSopLogsByJsApiParam param) {
+
+        SendSopParamDetailsC qwSopLogsByJsApi = qwSopLogsService.getQwSopLogsByJsApi(param);
+
+        return R.ok().put("data",qwSopLogsByJsApi);
+    }
+
+    //获取销售的某个联系人
+    @GetMapping("/getExternalContactByAppKey/{appKey}")
+    public R getExternalContactByAppKey(@PathVariable("appKey") String appKey) {
+
+        QwExternalContactByQwResult result=qwSopLogsService.getExternalContactByAppKey(appKey);
+
+        return R.ok().put("data",result);
+    }
+
+    //清除不是当前员工的 外部联系以及营期
+    @PostMapping("/deleteQwSopLogsByJsApi")
+    public R deleteQwSopLogsByJsApi(@Valid @RequestBody GetQwSopLogsByJsApiParam param) {
+         return  qwSopLogsService.deleteQwSopLogsByJsApi(param);
+    }
+
+    //获取侧边栏显示记录
+    @GetMapping("/getQwSopLogs")
+    public R getQwSopLogs(SopMsgParam param) throws Exception {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<QwSopLogs> list = qwSopLogsService.selectQwSopLogsListVO(param);
+        PageInfo<QwSopLogs> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+}

+ 71 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/RoomSopController.java

@@ -0,0 +1,71 @@
+package com.fs.app.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.param.FsUserCourseParam;
+import com.fs.course.param.FsUserCourseVideoParam;
+import com.fs.course.param.SopRoomCourseParam;
+import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.service.IFsUserCourseVideoService;
+import com.fs.course.vo.FsUserCourseListPVO;
+import com.fs.course.vo.FsUserCourseListUVO;
+import com.fs.course.vo.FsUserCourseVideoVO;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qwHookApi.param.QwHookSendMsgParam;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/app/room")
+public class RoomSopController {
+    @Autowired
+    IQwUserService qwUserService;
+
+    @Autowired
+    IFsUserCourseService courseService;
+
+    @Autowired
+    IFsUserCourseVideoService courseVideoService;
+
+    @GetMapping("/getCourseList")
+    public R getCourseList(SopRoomCourseParam param) {
+        Long companyId = qwUserService.getQwUserCompanyId(param.getCorpId(),param.getQwUserid());
+        FsUserCourseParam fsUserCourse = new FsUserCourseParam();
+        fsUserCourse.setCompanyId(companyId);
+        fsUserCourse.setIsPrivate(1);
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsUserCourseListPVO> list = courseService.selectFsUserCourseListCompanyPVO(fsUserCourse);
+        PageInfo<FsUserCourseListPVO> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+    @GetMapping("/getVideoList")
+    public R getVideoList(FsUserCourseVideoParam param) {
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setCourseId(param.getCourseId());
+        List<FsUserCourseVideoVO> list = courseVideoService.selectFsUserCourseVideoListByCourseIdAndCompany(param);
+        PageInfo<FsUserCourseVideoVO> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+
+    @GetMapping("/createLink")
+    public R createLink(SopRoomCourseParam param) {
+//        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+//        param.setCourseId(param.getCourseId());
+//        List<FsUserCourseVideoVO> list = courseVideoService.selectFsUserCourseVideoListByCourseIdAndCompany(param);
+//        PageInfo<FsUserCourseVideoVO> listPageInfo=new PageInfo<>(list);
+//        return R.ok().put("data",listPageInfo);
+        return R.ok();
+    }
+
+
+
+
+
+}

+ 205 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/TestCourseService.java

@@ -0,0 +1,205 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.param.FsCourseWatchLogByFinishTimeParam;
+import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+import com.fs.sop.params.SopUserLogsParamByDate;
+import com.fs.sop.service.IQwSopLogsService;
+import com.fs.sop.service.ISopUserLogsService;
+import com.fs.voice.utils.StringUtil;
+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.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class TestCourseService {
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+
+    @Autowired
+    private QwSopMapper qwSopMapper;
+
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    private ISopUserLogsService sopUserLogsService;
+
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+
+    @Async
+    public void sysncCousre(List<FsCourseWatchLogByFinishTimeParam> fsCourseWatchLogByFinishTimeParams){
+
+        fsCourseWatchLogByFinishTimeParams.stream().forEach(item->{
+
+            // 将标签字符串解析为 List    //客户总标签
+            List<String> tagIdsList = new ArrayList<>();
+
+            if (item.getTagIds() != null && !item.getTagIds().isEmpty()) {
+                tagIdsList = JSON.parseArray(item.getTagIds(), String.class);
+            }
+//            分时段里符合的标签-再用来创建自动SOP
+            QwSopAutoByTags qwSopAutoByTags=new QwSopAutoByTags();
+            qwSopAutoByTags.setQwUserId(String.valueOf(item.getQwUserId()));
+            qwSopAutoByTags.setCorpId(item.getCorpId());
+            qwSopAutoByTags.setTagsIdsSelectList(tagIdsList);
+            qwSopAutoByTags.setSendType(2);
+
+            if (!tagIdsList.isEmpty()){
+                List<QwSopRuleTimeVO> qwSopRuleTimeVOS = qwSopMapper.selectQwSopAutoByTagsByForeachNotAuto(qwSopAutoByTags);
+
+                if (qwSopRuleTimeVOS != null && !qwSopRuleTimeVOS.isEmpty()){
+
+                    Date campPeriodTime = item.getCampPeriodTime();
+                    // 将 Date 转换为 LocalDate
+                    LocalDate currentDate = campPeriodTime.toInstant()
+                            .atZone(ZoneId.systemDefault())
+                            .toLocalDate();
+                    //SOP规则
+                    qwSopRuleTimeTools(qwSopRuleTimeVOS,item.getQwUser(),item.getQwUserId(),item.getCompanyUserId(),item.getCompanyId(),
+                            item.getCorpId(),item.getExternalUserId(),item.getExternalContactName(),item.getQwExternalContactId(),
+                            item.getUserId(),currentDate,tagIdsList);
+                }
+            }
+
+
+        });
+
+    }
+
+    public void qwSopRuleTimeTools(List<QwSopRuleTimeVO> qwSopRuleTimeVOS, String qwUserChar, Long qwUserId,
+                                   Long companyUserId, Long companyId, String cropId,
+                                   String externalUserID, String externalContactName, Long contactId, Long contactFsUserId,
+                                   LocalDate currentDate, List<String> combinedTagsList) {
+        // 定义日期和时间格式化器
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        // sop任务
+        qwSopRuleTimeVOS.forEach(ruleTimeVO -> {
+
+            // 将排除的字符串转成列表
+            List<String> excludedTagsList=new ArrayList<>();
+            if (ruleTimeVO.getExcludeTags() != null && !ruleTimeVO.getExcludeTags().isEmpty()){
+                excludedTagsList= Arrays.asList( ruleTimeVO.getExcludeTags().split(","));
+            }
+
+            // 检查 combinedTagsList 是否包含排除列表中的任意一个标签
+            boolean containsExcludedTag = combinedTagsList.stream()
+                    .anyMatch(excludedTagsList::contains);
+
+            //含任意一个排除标签
+            if (containsExcludedTag) {
+                return;
+            }
+
+
+            // 用于查询/或新增
+            SopUserLogs userLogs = new SopUserLogs();
+            userLogs.setSopId(ruleTimeVO.getId());
+            userLogs.setSopTempId(ruleTimeVO.getTempId());
+            userLogs.setCorpId(ruleTimeVO.getCorpId());
+            userLogs.setStatus(1);
+
+            // 用于今天的新增
+            SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
+            userLogsParamByDate.setSopId(ruleTimeVO.getId());
+            userLogsParamByDate.setSopTempId(ruleTimeVO.getTempId());
+            userLogsParamByDate.setCorpId(ruleTimeVO.getCorpId());
+            userLogsParamByDate.setStatus(1);
+
+
+            // 设定用户信息
+            userLogs.setQwUserId(qwUserChar);
+            userLogs.setUserId(qwUserId + "|" + companyUserId + "|" + companyId);
+            userLogsParamByDate.setQwUserId(qwUserChar);
+            userLogsParamByDate.setUserId(qwUserId + "|" + companyUserId + "|" + companyId);
+
+            // 创建 SopUserLogsInfo
+            SopUserLogsInfo logsInfo = new SopUserLogsInfo();
+            logsInfo.setQwUserId(qwUserChar);
+            logsInfo.setCorpId(cropId);
+            logsInfo.setExternalContactId(externalUserID.trim());
+            logsInfo.setExternalId(contactId);
+            logsInfo.setFsUserId(contactFsUserId);
+            logsInfo.setExternalUserName(externalContactName);
+            logsInfo.setSopId(ruleTimeVO.getId());
+
+            // 判断 SOP 任务的开始时间
+            qwSopRuleTimeToolsCheck(currentDate,dateFormatter,userLogs,logsInfo,userLogsParamByDate);
+        });
+    }
+
+    public void qwSopRuleTimeToolsCheck(LocalDate currentDate,DateTimeFormatter dateFormatter,
+                                        SopUserLogs userLogs,SopUserLogsInfo logsInfo,SopUserLogsParamByDate userLogsParamByDate) {
+
+
+        // 今天的日期字符串
+        String todayStr = currentDate.format(dateFormatter);
+        userLogs.setStartTime(todayStr);
+
+        // 查询今天的营期表
+        String unionSopStartId2 = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+        if (!StringUtil.strIsNullOrEmpty(unionSopStartId2)) {
+
+            try {
+                // 查询客户是否已存在
+            SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(logsInfo);
+            if (userLogsInfo == null) {
+
+                logsInfo.setUserLogsId(unionSopStartId2);
+
+                System.out.println("客户加入营期:"+logsInfo.getSopId()+":"+logsInfo.getQwUserId()+":"+logsInfo.getExternalContactId()+":"+logsInfo.getExternalUserName()+":"+logsInfo.getCorpId());
+                //客户入营期
+                sopUserLogsInfoMapper.insertSopUserLogsInfo(logsInfo);
+            }else {
+                //如果客户不为空-看营期
+
+                System.out.println("营期--:"+userLogsInfo);
+
+                if (!userLogsInfo.getUserLogsId().equals(unionSopStartId2)){
+
+                    userLogsInfo.setUserLogsId(unionSopStartId2);
+
+                    sopUserLogsInfoMapper.updateSopUserLogsInfoToTime(userLogsInfo);
+
+                    System.out.println("客户重回营期:"+todayStr+":"+logsInfo.getSopId()+":"+logsInfo.getQwUserId()+":"+logsInfo.getExternalContactId()+":"+logsInfo.getExternalUserName()+":"+logsInfo.getCorpId());
+
+                }
+
+
+            }
+
+
+            }catch (Exception e){
+                System.out.println("客户入营期失败:logsInfo:"+logsInfo+"msg----"+e.getMessage());
+            }
+
+
+        }
+
+    }
+
+}

+ 88 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/testController.java

@@ -0,0 +1,88 @@
+package com.fs.app.controller;
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.service.ICompanyConfigService;
+import com.fs.course.mapper.FsCourseSopLogsMapper;
+import com.fs.course.mapper.FsCourseSopMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.service.IFsCourseLinkService;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.store.mapper.FsUserMapper;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.*;
+import com.fs.qw.param.QwAutoSopTimeParam;
+import com.fs.qw.service.IQwContactWayService;
+import com.fs.qw.service.IQwExternalContactCrmService;
+import com.fs.qw.service.IQwExternalErrRetryService;
+import com.fs.qw.service.impl.AsyncSopService;
+import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+import com.fs.sop.params.SopUserLogsParamByDate;
+import com.fs.sop.service.IQwSopService;
+import com.fs.sop.service.ISopUserLogsService;
+import com.fs.voice.utils.StringUtil;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shade.okhttp3.Cache;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+@RestController
+@RequestMapping(value="/app/test")
+public class testController {
+
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    testService testService;
+    @Autowired
+    private RedisCache redisCache;
+
+    @GetMapping("/qwHookNotify")
+    public R qwHookNotify() {
+
+//        redisCache.setCacheObject("qwUserRd:"+12313+":"+12313 ,JSON.toJSONString(null),1, TimeUnit.HOURS);
+
+//
+//        List<QwUser> qwUserAllKey = qwUserMapper.getQwUserAllKey();
+//
+//
+//        int i=1;
+//
+//        for (QwUser qwUser : qwUserAllKey) {
+//            System.out.println(qwUser);
+//            i++;
+//            System.out.println("执行到第:"+i);
+//            testService.add(qwUser);
+//
+//        }
+
+
+        return  R.ok();
+    }
+
+
+
+
+
+}

+ 221 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/testCourseController.java

@@ -0,0 +1,221 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.param.FsCourseWatchLogByFinishTimeParam;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.QwExternalContactAddTagParam;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwUserService;
+import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.qwApi.param.QwSendMsgParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+import com.fs.sop.params.SopUserLogsParamByDate;
+import com.fs.sop.service.IQwSopLogsService;
+import com.fs.sop.service.ISopUserLogsService;
+import com.fs.voice.utils.StringUtil;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.codehaus.jettison.json.JSONException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+@RestController
+@RequestMapping(value="/app/test")
+public class testCourseController {
+
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private IQwSopLogsService qwSopLogsService;
+
+    @Autowired
+    private QwSopMapper qwSopMapper;
+
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    private ISopUserLogsService sopUserLogsService;
+
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+
+    @Autowired
+    private IQwExternalContactService contactService;
+
+    @Autowired
+    private IQwUserService iQwUserService;
+
+    @Autowired
+    private TestCourseService testService;
+
+    @Autowired
+    QwUserMapper qwUserMapper;
+
+    @Autowired
+    QwCompanyMapper qwCompanyMapper;
+
+    @Autowired
+    QwApiService qwApiService;
+
+    @GetMapping("/test1")
+    public R test1() {
+
+        String msg="<font color=\"warning\">您的云主机,企业微信【已经退出登录】!!望悉知~</font>";
+
+        QwUser qwUserByAppKey = qwUserMapper.selectQwUserByAppKey("6563");
+
+        QwSendMsgParam sendMsgParam = new QwSendMsgParam();
+        QwCompany qwCompany = qwCompanyMapper.selectQwCompanyByCorpId(qwUserByAppKey.getCorpId());
+        sendMsgParam.setAgentid(Integer.parseInt(qwCompany.getServerAgentId().trim()));
+        sendMsgParam.setTouser(qwUserByAppKey.getQwUserId());
+
+        QwSendMsgParam.Markdown markdown = new QwSendMsgParam.Markdown();
+        markdown.setContent(msg);
+
+        sendMsgParam.setMarkdown(markdown);
+        sendMsgParam.setMsgtype("markdown");
+        qwApiService.sendMsg(sendMsgParam, qwCompany.getCorpId());
+//        List<String> combinedTags = new ArrayList<>();
+//        combinedTags.add("et7tWFCgAAvIJDB-xSXUDpUIukUwqRrA");
+//        combinedTags.add("etfFKfDQAAS4NYg9yWCx6-KCF9V8Az2g");
+//        combinedTags.add("etfFKfDQAAIWzaju4DodqIc1ZJAyxJfw");
+//
+//        //分时段里符合的标签-再用来创建自动SOP
+//        QwSopAutoByTags qwSopAutoByTags=new QwSopAutoByTags();
+//        qwSopAutoByTags.setQwUserId("8799");
+//        qwSopAutoByTags.setCorpId("ww5a88c4f879f204c5");
+//        qwSopAutoByTags.setTagsIdsSelectList(combinedTags);
+//        qwSopAutoByTags.setSendType(2);
+//        List<QwSopRuleTimeVO> qwSopRuleTimeVOS = qwSopMapper.selectQwSopAutoByTagsByForeach(qwSopAutoByTags);
+
+        return  R.ok();
+    }
+
+    @GetMapping("/text3")
+    public R text3() throws JSONException {
+        List<Long> list1 = new ArrayList<>();
+        list1.add(7234165L);
+
+        List<String> listTag = new ArrayList<>();
+        listTag.add("et7tWFCgAAHZOsxTFXpJ3vaFtmv87CqQ");
+        listTag.add(" et7tWFCgAAkco8tquT4V3pLmQR6v6-aQ");
+        listTag.add(" et7tWFCgAA2lmDrcj6bqRvEHgOccN0Mw");
+
+        QwExternalContactAddTagParam param=new QwExternalContactAddTagParam();
+        param.setUserIds(list1);
+        param.setTagIds(listTag);
+        param.setCorpId("ww5a88c4f879f204c5");
+        contactService.addUserTag(param);
+
+        return  R.ok();
+    }
+
+    @GetMapping("/text2")
+    public R text2() {
+
+
+        return  R.ok();
+    }
+
+    /**
+    * 一键导入授权码
+    */
+    @PostMapping("/importExcel")
+    public R importExcel(@RequestParam("file") MultipartFile file) throws IOException {
+
+        R r = null;
+
+
+        List<String> appkeyList=new ArrayList<>();
+        List<String> appkeyUnNow=new ArrayList<>();
+
+        if (file.isEmpty()) {
+            throw new IllegalArgumentException("File is empty!");
+        }
+
+        // 创建工作簿对象
+        Workbook workbook = new XSSFWorkbook(file.getInputStream());
+
+        // 获取第一个工作表
+        Sheet sheet = workbook.getSheetAt(0);
+
+        // 遍历每一行
+        for (Row row : sheet) {
+            List<String> rowData = new ArrayList<>();
+
+            // 获取A列(第1列)
+            Cell cellA = row.getCell(0);
+            String ip = getCellValueAsString(cellA);
+
+            // 获取B列(第2列)
+            Cell cellB = row.getCell(5);
+            String appKey = getCellValueAsString(cellB);
+
+
+            if (!StringUtil.strIsNullOrEmpty(appKey)
+                    && !("UNKNOWN".equals(appKey))
+                    && appKey.matches("\\d+")) {  // 只允许纯数字
+
+//                System.out.println(ip + "-----------" + appKey);
+
+                r= iQwUserService.qwBindCloudHostByIp2(appKey, ip,appkeyList,appkeyUnNow);
+
+            }
+
+        }
+
+        // 关闭工作簿
+        workbook.close();
+        return  r;
+    }
+    /**
+     * 将单元格的值转换为字符串
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return ""; // 如果单元格为空,返回空字符串
+        }
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    return String.valueOf(cell.getNumericCellValue());
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            default:
+                return "UNKNOWN";
+        }
+    }
+}

+ 399 - 0
fs-qwhook-sop/src/main/java/com/fs/app/controller/testService.java

@@ -0,0 +1,399 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.service.ICompanyConfigService;
+import com.fs.course.mapper.FsCourseSopLogsMapper;
+import com.fs.course.mapper.FsCourseSopMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.course.service.IFsCourseLinkService;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.store.mapper.FsUserMapper;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.*;
+import com.fs.qw.param.QwAutoSopTimeParam;
+import com.fs.qw.service.IQwContactWayService;
+import com.fs.qw.service.IQwExternalContactCrmService;
+import com.fs.qw.service.IQwExternalErrRetryService;
+import com.fs.qw.service.impl.AsyncSopService;
+import com.fs.qw.vo.QwSopRuleTimeVO;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.domain.SopUserLogs;
+import com.fs.sop.domain.SopUserLogsInfo;
+import com.fs.sop.mapper.QwSopLogsMapper;
+import com.fs.sop.mapper.QwSopMapper;
+import com.fs.sop.mapper.SopUserLogsInfoMapper;
+import com.fs.sop.params.QwSopAutoByTags;
+import com.fs.sop.params.SopUserLogsParamByDate;
+import com.fs.sop.service.IQwSopService;
+import com.fs.sop.service.ISopUserLogsService;
+import com.fs.voice.utils.StringUtil;
+import org.springframework.beans.BeanUtils;
+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.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class testService {
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    QwExternalContactMapper qwExternalContactMapper;
+    @Autowired
+    QwUserMapper qwUserMapper;
+    @Autowired
+    RedisTemplate<String, String> redisTemplate;
+    @Autowired
+    IQwSopService qwSopService;
+    @Autowired
+    QwSopMapper qwSopMapper;
+
+    @Autowired
+    ICompanyConfigService companyConfigService;
+
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    QwContactWayMapper qwContactWayMapper;
+
+    @Autowired
+    IQwContactWayService qwContactWayService;
+    @Autowired
+    QwContactWayUserMapper qwContactWayUserMapper;
+    @Autowired
+    QwContactWayLogsMapper qwContactWayLogsMapper;
+
+    @Autowired
+    QwFriendWelcomeMapper qwFriendWelcomeMapper;
+
+    @Autowired
+    QwAutoTagsMapper qwAutoTagsMapper;
+    @Autowired
+    CrmCustomerMapper crmCustomerMapper;
+    @Autowired
+    QwAutoTagsLogsMapper qwAutoTagsLogsMapper;
+
+    @Autowired
+    IQwExternalContactCrmService contactCrmService;
+
+    @Autowired
+    FsCourseSopLogsMapper fsSopLogsMapper;
+
+    @Autowired
+    FsCourseSopMapper fsSopMapper;
+
+    @Autowired
+    CrmCustomerMapper customerMapper;
+
+    @Autowired
+    FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    QwAppContactWayLogsMapper qwAppContactWayLogsMapper;
+
+    @Autowired
+    private SopUserLogsInfoMapper sopUserLogsInfoMapper;
+
+
+    @Autowired
+    private ISopUserLogsService sopUserLogsService;
+
+    int i=0;
+
+
+    public void add(QwUser qwUser) {
+        List<QwExternalContact> qwExternalContacts = qwExternalContactMapper.selectExternalUserIdByQwUserId(qwUser.getId());
+        System.out.println("客户数量"+qwExternalContacts.size());
+        for (QwExternalContact qwExternalContact : qwExternalContacts) {
+            i++;
+            System.out.println("客户"+i);
+            Date date = qwExternalContact.getCreateTime();
+            LocalDate currentDate = date.toInstant()
+                    .atZone(ZoneId.systemDefault()) // 使用系统默认时区
+                    .toLocalDate();
+
+            LocalTime localTime = date.toInstant()
+                    .atZone(ZoneId.systemDefault()) // 使用系统默认时区
+                    .toLocalTime();
+            // 获取当前系统时间 (HH:mm)
+
+            QwSopAutoByTags qwSopAutoByTags=new QwSopAutoByTags();
+            qwSopAutoByTags.setQwUserId(String.valueOf(qwUser.getId()));
+            qwSopAutoByTags.setCorpId(qwExternalContact.getCorpId());
+            List<String> parsedTags = JSON.parseArray(qwExternalContact.getTagIds(), String.class);
+            qwSopAutoByTags.setTagsIdsSelectList(parsedTags);
+            qwSopAutoByTags.setSendType(2);
+            if (parsedTags!=null&&parsedTags.size()!=0){
+
+                List<QwSopRuleTimeVO> qwSopRuleTimeVOS = qwSopMapper.selectQwSopAutoByTagsByForeachNotAuto(qwSopAutoByTags);
+
+                if (qwSopRuleTimeVOS != null && !qwSopRuleTimeVOS.isEmpty()){
+                    //SOP规则
+                    qwSopRuleTimeTools(qwSopRuleTimeVOS,qwExternalContact.getUserId(),qwUser,qwExternalContact.getCorpId(),qwExternalContact.getExternalUserId(),qwExternalContact.getName(),qwExternalContact,currentDate,localTime,parsedTags);
+                }
+            }else {
+                System.out.println("标签为空");
+            }
+
+
+        }
+    }
+
+
+    public void qwSopRuleTimeTools(List<QwSopRuleTimeVO> qwSopRuleTimeVOS, String userID, QwUser qwUser, String corpId,
+                                   String externalUserID, String externalContactName, QwExternalContact contact,
+                                   LocalDate currentDate, LocalTime localTime, List<String> combinedTagsList) {
+        // 定义日期和时间格式化器
+        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
+
+        // sop任务
+        qwSopRuleTimeVOS.forEach(ruleTimeVO -> {
+
+            // 将排除的字符串转成列表
+            List<String> excludedTagsList=new ArrayList<>();
+            if (ruleTimeVO.getExcludeTags() != null && !ruleTimeVO.getExcludeTags().isEmpty()){
+                excludedTagsList= Arrays.asList( ruleTimeVO.getExcludeTags().split(","));
+            }
+
+            // 检查 combinedTagsList 是否包含排除列表中的任意一个标签
+            boolean containsExcludedTag = combinedTagsList.stream()
+                    .anyMatch(excludedTagsList::contains);
+
+            //含任意一个排除标签
+            if (containsExcludedTag) {
+                return;
+            }
+
+            // 自动sop的规则
+            QwAutoSopTimeParam qwAutoSopTimeParam = JSON.parseObject(ruleTimeVO.getAutoSopTime(), QwAutoSopTimeParam.class);
+
+            // 用于查询/或新增
+            SopUserLogs userLogs = new SopUserLogs();
+            userLogs.setSopId(ruleTimeVO.getId());
+            userLogs.setSopTempId(ruleTimeVO.getTempId());
+            userLogs.setCorpId(ruleTimeVO.getCorpId());
+            userLogs.setStatus(1);
+
+            // 用于今天的新增
+            SopUserLogsParamByDate userLogsParamByDate = new SopUserLogsParamByDate();
+            userLogsParamByDate.setSopId(ruleTimeVO.getId());
+            userLogsParamByDate.setSopTempId(ruleTimeVO.getTempId());
+            userLogsParamByDate.setCorpId(ruleTimeVO.getCorpId());
+            userLogsParamByDate.setStatus(1);
+
+            // 从数据库中取到的开始时间(Date类型),转换为 LocalDate
+            Date sopStartTime = ruleTimeVO.getStartTime();
+            //开始时间
+            LocalDate sopStartLocalDate = sopStartTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+            // 设定用户信息
+            userLogs.setQwUserId(userID);
+            userLogs.setUserId(qwUser.getId() + "|" + qwUser.getCompanyUserId() + "|" + qwUser.getCompanyId());
+            userLogsParamByDate.setQwUserId(userID);
+            userLogsParamByDate.setUserId(qwUser.getId() + "|" + qwUser.getCompanyUserId() + "|" + qwUser.getCompanyId());
+
+            // 创建 SopUserLogsInfo
+            SopUserLogsInfo logsInfo = new SopUserLogsInfo();
+            logsInfo.setQwUserId(userID.trim());
+            logsInfo.setCorpId(qwUser.getCorpId());
+            logsInfo.setExternalContactId(externalUserID.trim());
+            logsInfo.setExternalId(contact.getId());
+            logsInfo.setFsUserId(contact.getFsUserId());
+            logsInfo.setExternalUserName(externalContactName);
+            logsInfo.setSopId(ruleTimeVO.getId());
+            // 判断 SOP 任务的开始时间
+            qwSopRuleTimeToolsCheck(sopStartLocalDate,currentDate,dateFormatter,userLogs,logsInfo,userLogsParamByDate,qwAutoSopTimeParam,timeFormatter,localTime);
+        });
+    }
+
+    public void qwSopRuleTimeToolsCheck(LocalDate sopStartLocalDate,LocalDate currentDate,DateTimeFormatter dateFormatter,
+                                        SopUserLogs userLogs,SopUserLogsInfo logsInfo,SopUserLogsParamByDate userLogsParamByDate,
+                                        QwAutoSopTimeParam qwAutoSopTimeParam,DateTimeFormatter timeFormatter,LocalTime localTime) {
+        // 还没开始 (未来,不包括今天)
+        if (sopStartLocalDate.isAfter(currentDate)) {
+            System.out.println("还没开始");
+            // 大于当前日期,说明还没有开始:
+            // -> 以 SOP 任务的开始时间为营期
+            String sopStartLocalDateStr = sopStartLocalDate.format(dateFormatter);
+
+            userLogs.setStartTime(sopStartLocalDateStr);
+            // 查询开始时间营期表
+            String unionSopStartId = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+            // 如果营期表里有,则加入;否则创建新营期
+            if (!StringUtil.strIsNullOrEmpty(unionSopStartId)) {
+                logsInfo.setUserLogsId(unionSopStartId);
+                // 查询这个人是否已在该营期里
+                handleInsertSopUserLogsInfo(logsInfo);
+            }
+            else {
+                // 没有营期记录就先插入
+                userLogsParamByDate.setStartTime(sopStartLocalDateStr);
+
+                sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+
+                // 再次查询 拿营期主键
+                String unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+                logsInfo.setUserLogsId(unionSopStartIdNew);
+
+                // 查询是否在营期里,如果没有再插入
+                handleInsertSopUserLogsInfo(logsInfo);
+            }
+        }
+        else {
+
+            // sopStartLocalDate <= currentDate -> 说明已经开始
+            // -> 以“今天 / 明天”作为营期
+            // 获取 AutoSopType
+            Integer autoSopType = qwAutoSopTimeParam.getAutoSopType();
+
+            // 今天的日期字符串
+            String todayStr = currentDate.format(dateFormatter);
+            userLogs.setStartTime(todayStr);
+
+            // 查询今天的营期表
+            String unionSopStartId2 = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+            // 明天日期字符串
+            LocalDate tomorrowDate = currentDate.plusDays(1);
+            String tomorrowStr = tomorrowDate.format(dateFormatter);
+
+            // 复制一份 userLogs,设为明天
+            SopUserLogs userLogsTomorrow = new SopUserLogs();
+            BeanUtils.copyProperties(userLogs, userLogsTomorrow);
+            userLogsTomorrow.setStartTime(tomorrowStr);
+
+            // 复制一份 SopUserLogsParamByDate,设为明天
+            SopUserLogsParamByDate paramByDateTomorrow=new SopUserLogsParamByDate();
+            BeanUtils.copyProperties(userLogsParamByDate, paramByDateTomorrow);
+            paramByDateTomorrow.setStartTime(tomorrowStr);
+
+            // 查询明天的营期表
+            String unionTomorrowSopId2 = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogsTomorrow);
+            // 根据 autoSopType 分支处理
+            switch (autoSopType) {
+                // 立即执行
+                case 1: {
+                    String autoStartTime = qwAutoSopTimeParam.getAutoStartTime();
+                    String autoEndTime = qwAutoSopTimeParam.getAutoEndTime();
+
+                    // 如果结束时间为 24:00,则替换成 23:59
+                    if ("24:00".equals(autoEndTime)) {
+                        autoEndTime = "23:59";
+                    }
+
+                    // 转为 LocalTime
+                    LocalTime startTime = LocalTime.parse(autoStartTime, timeFormatter);
+                    LocalTime endTime = LocalTime.parse(autoEndTime, timeFormatter);
+
+                    // 判断当前时间是否在 [startTime, endTime] 范围内
+                    if (!localTime.isBefore(startTime) && !localTime.isAfter(endTime)) {
+                        // 在范围内 => 加入到今天的营期
+
+                        if (!StringUtil.strIsNullOrEmpty(unionSopStartId2)) {
+
+                            logsInfo.setUserLogsId(unionSopStartId2);
+                            // 查询客户是否已存在
+                            handleInsertSopUserLogsInfo(logsInfo);
+
+                        } else {
+                            // 没有今天天的营期就先建
+                            userLogsParamByDate.setStartTime(todayStr);
+                            sopUserLogsService.insertSopUserLogsByDate(userLogsParamByDate);
+
+                            // 查询客户是否已存在
+                            SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(logsInfo);
+                            if (userLogsInfo == null) {
+
+                                // 再查今天的营期的主键
+                                userLogs.setStartTime(todayStr);
+                                String unionSopStartIdNew = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogs);
+
+                                logsInfo.setUserLogsId(unionSopStartIdNew);
+                                //客户入营期
+                                sopUserLogsInfoMapper.insertSopUserLogsInfo(logsInfo);
+
+                            }
+                        }
+                    } else {
+
+                        // 不在范围内 => 加入到明天的营期
+                        if (!StringUtil.strIsNullOrEmpty(unionTomorrowSopId2)) {
+                            logsInfo.setUserLogsId(unionTomorrowSopId2);
+                            // 查询是否已存在客户入营期
+
+                            handleInsertSopUserLogsInfo(logsInfo);
+                        } else {
+                            // 没有明天的营期就先建
+
+                            handleNextDayExecution( paramByDateTomorrow, logsInfo, userLogsTomorrow);
+                        }
+                    }
+                    break;
+                }
+                // 次日执行
+                case 2: {
+                    // 直接加入到明天的营期没有明天的营期就先建
+                    if (!StringUtil.strIsNullOrEmpty(unionTomorrowSopId2)) {
+
+                        logsInfo.setUserLogsId(unionTomorrowSopId2);
+                        //客户入营期
+                        handleInsertSopUserLogsInfo(logsInfo);
+                    } else {
+
+                        // 没有明天的营期就先建
+                        handleNextDayExecution( paramByDateTomorrow, logsInfo, userLogsTomorrow);
+                    }
+                    break;
+                }
+                default:
+                    // 其他类型可根据需要自行扩展
+                    break;
+            }
+        }
+    }
+    public void handleNextDayExecution(SopUserLogsParamByDate paramByDateTomorrow,SopUserLogsInfo sopUserLogsInfo,SopUserLogs userLogsTomorrow) {
+
+        sopUserLogsService.insertSopUserLogsByDate(paramByDateTomorrow);
+
+        SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(sopUserLogsInfo);
+        if (userLogsInfo == null) {
+
+            String unionTomorrowSopId2New = sopUserLogsService.selectSopUserLogsByUnionSopId(userLogsTomorrow);
+            sopUserLogsInfo.setUserLogsId(unionTomorrowSopId2New);
+
+            sopUserLogsInfoMapper.insertSopUserLogsInfo(sopUserLogsInfo);
+        }
+    }
+    public void handleInsertSopUserLogsInfo(SopUserLogsInfo sopUserLogsInfo){
+        SopUserLogsInfo userLogsInfo = sopUserLogsInfoMapper.selectSopUserLogsInfo(sopUserLogsInfo);
+
+        if (userLogsInfo == null) {
+            try {
+                sopUserLogsInfoMapper.insertSopUserLogsInfo(sopUserLogsInfo);
+                System.out.println("新增OK");
+            }catch (Exception e){
+                System.out.println("新增出错");
+            }
+
+        }else {
+            System.out.println("已经存在营期");
+        }
+    }
+
+
+
+
+}

+ 51 - 0
fs-qwhook-sop/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;
+	}
+	
+	
+}

+ 88 - 0
fs-qwhook-sop/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,88 @@
+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.context.request.WebRequest;
+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);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+
+
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		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){
+
+		return R.error(e.getMessage());
+	}
+}

+ 70 - 0
fs-qwhook-sop/src/main/java/com/fs/app/interceptor/AuthorizationInterceptor.java

@@ -0,0 +1,70 @@
+package com.fs.app.interceptor;
+
+
+import com.fs.app.annotation.Login;
+import com.fs.app.exception.FSException;
+import com.fs.app.utils.JwtUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+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;
+
+/**
+ * 权限(Token)验证
+ */
+@Component
+public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
+    @Autowired
+    private JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+    public static final String USER_KEY = "userId";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        Login annotation;
+        if(handler instanceof HandlerMethod) {
+            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
+        }else{
+            return true;
+        }
+
+        if(annotation == null){
+            return true;
+        }
+
+        //获取用户凭证
+        String token = request.getHeader(jwtUtils.getHeader());
+        if(StringUtils.isBlank(token)){
+            token = request.getParameter(jwtUtils.getHeader());
+        }
+
+        //凭证为空
+        if(StringUtils.isBlank(token)){
+            throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        Claims claims = jwtUtils.getClaimByToken(token);
+        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        //查询用户的TOKEN是否和REDIS中的一样
+//        String redisToken=redisCache.getCacheObject("AiChatToken:"+ Long.parseLong(claims.getSubject()));
+//        if(redisToken==null||!redisToken.equals(token)){
+//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+//        }
+//        long l = Long.parseLong(claims.getSubject());
+        //设置userId到request里,后续根据userId,获取用户信息
+        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
+
+        return true;
+    }
+}

+ 14 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SendAIParam.java

@@ -0,0 +1,14 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+@Data
+public class SendAIParam {
+
+    private String cmd;
+
+    private String key;
+
+    private String data;
+
+}

+ 23 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParam.java

@@ -0,0 +1,23 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParam implements Serializable {
+    /**
+    * 固定SendSop
+    */
+    private String cmd;
+    /**
+    * sopLogs记录
+    */
+    private String data;
+    /**
+    * 用户的appKey
+    */
+    private String key;
+}

+ 24 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamB.java

@@ -0,0 +1,24 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParamB implements Serializable {
+
+    /**
+     * 固定SendSop
+     */
+    private String cmd;
+    /**
+     * sopLogs记录
+     */
+    private List<QwSopTempSetting.Content.Setting> data;
+    /**
+     * 用户的appKey
+     */
+    private String key;
+}

+ 68 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamDetails.java

@@ -0,0 +1,68 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class SendSopParamDetails implements Serializable {
+
+    /**
+    * QwSopLog的主键
+    */
+    private String sopLogId;
+    /**
+    * 员工的id
+    */
+    private String qwUserid;
+
+    /**
+    * 客户的id
+    */
+    private String externalUserId;
+
+
+    /**
+     * 内容类型 文字 图片
+     */
+    private int contentType;
+
+    /**
+     * 消息内容
+     */
+    private String value;
+
+    /**
+     *  内容类型是图片时的 图片url
+     */
+    private String imgUrl;
+
+    /**
+     * 链接标题
+     */
+    private String linkTitle;
+
+    /**
+     * 链接描述
+     */
+    private String linkDescribe;
+
+    /**
+     * 链接封面
+     */
+    private String linkImageUrl;
+
+    /**
+     * 链接url
+     */
+    private String linkUrl;
+    /**
+    * 文件
+    */
+    private String fileUrl;
+    /**
+    * 视频
+    */
+    private String videoUrl;
+
+}

+ 30 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SendSopParamDetailsB.java

@@ -0,0 +1,30 @@
+package com.fs.app.params;
+
+import com.fs.qw.vo.QwSopTempSetting;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class SendSopParamDetailsB implements Serializable {
+
+    /**
+     * QwSopLog的主键
+     */
+    private String sopLogId;
+    /**
+     * 员工的id
+     */
+    private String qwUserid;
+
+    /**
+     * 客户的id
+     */
+    private String externalUserId;
+    private String externalUserName;
+
+    private List<QwSopTempSetting.Content.Setting> setting;
+
+
+}

+ 28 - 0
fs-qwhook-sop/src/main/java/com/fs/app/params/SopLogsEditParam.java

@@ -0,0 +1,28 @@
+package com.fs.app.params;
+
+import lombok.Data;
+
+@Data
+public class SopLogsEditParam {
+
+    /**
+     * qw_sop_Logs的主键(修改时参数)
+     */
+    private String id;
+
+    /**
+     * 发送(给成员)状态 0发送失败 1发送成功 3待发送
+     */
+    private Long sendStatus;
+
+    /**
+     *  接收(客户的)状态:0-未发送 1-已发送 2发送失败
+     */
+    private Long receivingStatus;
+
+    /**
+    * 备注
+    */
+    private String remark;
+
+}

+ 24 - 0
fs-qwhook-sop/src/main/java/com/fs/app/redis/RedisConfiguration.java

@@ -0,0 +1,24 @@
+package com.fs.app.redis;
+
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+@Configuration
+public class RedisConfiguration {
+    @Autowired
+    private RedisConnectionFactory redisConnectionFactory;
+
+    @Bean
+    public RedisMessageListenerContainer redisMessageListenerContainer() {
+        RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
+        redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
+        return redisMessageListenerContainer;
+    }
+
+
+
+}

+ 104 - 0
fs-qwhook-sop/src/main/java/com/fs/app/redis/RedisKeyExpirationListener.java

@@ -0,0 +1,104 @@
+//package com.fs.app.redis;
+//
+//import cn.hutool.http.HttpRequest;
+//import com.alibaba.fastjson.JSON;
+//import com.fs.common.constant.FsConstants;
+//import com.fs.common.core.redis.RedisCache;
+//import com.fs.his.config.FsSysConfig;
+//import com.fs.his.utils.ConfigUtil;
+//import com.fs.qw.param.QwLoginParam;
+//import com.fs.qw.service.IQwUserService;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.data.redis.connection.Message;
+//import org.springframework.data.redis.core.StringRedisTemplate;
+//import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
+//import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+//import org.springframework.stereotype.Component;
+//
+//import java.nio.charset.StandardCharsets;
+//import java.util.concurrent.TimeUnit;
+//
+//@Component
+//public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
+//
+//    private static final Logger log = LoggerFactory.getLogger(RedisKeyExpirationListener.class);
+//
+//    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
+//        super(listenerContainer);
+//    }
+//
+//    @Autowired
+//    private IQwUserService qwUserService;
+//    @Autowired
+//    private ConfigUtil configUtil;
+//
+//    @Value("${hook.path}")
+//    private String hookPath;
+//
+//    @Autowired
+//    private StringRedisTemplate redisTemplate;  // 使用 RedisTemplate 进行分布式锁
+//
+//    /**
+//     * 针对redis数据失效事件,进行数据处理
+//     * @param message
+//     * @param pattern
+//     */
+//    @Override
+//    public void onMessage(Message message, byte[] pattern) {
+//        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
+//        //过期的key
+//        String key = new String(message.getBody(),StandardCharsets.UTF_8);
+//
+//        //判断是否是appKey失效
+//        if(key.contains(FsConstants.REDIS_QW_appKey_Active)) {
+//
+//            log.info("监听Qw过期appKey-redis过期:pattern={},channel={},key={}",new String(pattern),channel,key);
+//
+//            String lockKey = "lockAppKey:" + key;  // 分布式锁的 key
+//
+//            // 尝试获取锁,设置 30 秒自动过期,防止死锁
+//            Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
+//
+//            if (Boolean.TRUE.equals(lockAcquired)) {  // 只有一个实例会成功获取锁
+//                try {
+//                    String[] parts = key.split(":");
+//
+//                    if (parts.length == 3) {
+//                        String corpId = parts[1].trim();
+//                        String qwUserId = parts[2].trim();
+//
+//                        String appKey = qwUserService.selectQwUserByQwUserIdAndCorId(qwUserId, corpId);
+//
+//                        // 重新调起登录
+//                        QwLoginParam loginParam = new QwLoginParam();
+//                        loginParam.setAppKey(appKey);
+//
+////                        HttpRequest.post(hookPath+"/app/qwSop/qwLogin")
+////                                .body(JSON.toJSONString(loginParam), "application/json;charset=UTF-8")
+////                                .execute().body();
+//
+////                        HttpRequest.post("http://127.0.0.1:7771/app/qwSop/qwLogin")
+////                                .body(JSON.toJSONString(loginParam), "application/json;charset=UTF-8")
+////                                .execute().body();
+//                        FsSysConfig config = configUtil.getSysConfig();
+//                        String domainName = config.getHookUrl();
+//                        HttpRequest.post(domainName+"/app/qwSop/qwLogin")
+//                                .body(JSON.toJSONString(loginParam),"application/json;charset=UTF-8")
+//                                .execute().body();
+//                    } else {
+//                        System.out.println("监听appKey失效!");
+//                    }
+//                } finally {
+//                    // 释放锁,避免影响后续任务
+//                    redisTemplate.delete(lockKey);
+//                }
+//            } else {
+//                log.info("另一个实例已经处理了 key={},当前实例跳过", key);
+//            }
+//
+//        }
+//    }
+//}

+ 249 - 0
fs-qwhook-sop/src/main/java/com/fs/app/utils/AudioUtils.java

@@ -0,0 +1,249 @@
+package com.fs.app.utils;
+
+import com.fs.common.exception.ServiceException;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class AudioUtils {
+    /**
+     * 工具地址
+     **/
+    static String path = "c:\\";
+
+    public static void main(String[] args) {
+        // TODO: mp3 转 silk
+        transferAudioSilk("路径\\", "文件名.mp3", false);
+        // TODO: wav 转 silk
+        transferAudioSilk("路径\\", "文件名.wav", false);
+        // TODO: mp3转amr
+        transferMp3Amr("路径\\文件名.mp3", "路径\\文件名.amr");
+    }
+
+    /**
+     * MP3/WAV转SILk格式
+     * @param filePath 例:D:\\file\\audio.mp3
+     * @param isSource isSource 是否清空原文件
+     * @return
+     */
+    public static String transferAudioSilk(String filePath, boolean isSource){
+        Integer index = filePath.lastIndexOf("\\") + 1;
+        return transferAudioSilk(filePath.substring(0, index), filePath.substring(index, filePath.length()), isSource);
+    }
+
+    /**
+     * MP3/WAV转SILk格式
+     *
+     * @param path 文件路径 例:D:\\file\\
+     * @param name 文件名称 例:audio.mp3/audio.wav
+     * @param isSource 是否清空原文件
+     * @return silk文件路径
+     * @throws Exception
+     */
+    public static String transferAudioSilk(String path, String name, boolean isSource) {
+        try {
+            // 判断后缀格式
+            String suffix = name.split("\\.")[1];
+            if (!suffix.toLowerCase().equals("mp3") && !suffix.toLowerCase().equals("wav")) {
+                throw new ServiceException("文件格式必须是mp3/wav");
+            }
+            String filePath = path + name;
+            File file = new File(filePath);
+            if (!file.exists()) {
+                throw new Exception("文件不存在!");
+            }
+            // 文件名时拼接
+            SimpleDateFormat ttime = new SimpleDateFormat("yyyyMMddhhMMSS");
+            String time = ttime.format(new Date());
+            // 导出的pcm格式路径
+            String pcmPath = path + "PCM_" + time + ".pcm";
+            // 先将mp3/wav转换成pcm格式
+            transferAudioPcm(filePath, pcmPath);
+            // 导出的silk格式路径
+            String silkPath = path + "SILK_" + time + ".silk";
+            // 转换成silk格式
+            transferPcmSilk(pcmPath, silkPath);
+            // 删除pcm文件
+            File pcmFile = new File(pcmPath);
+            if (pcmFile.exists()) {
+                pcmFile.delete();
+            }
+            if (isSource) {
+                File audioFile = new File(filePath);
+                if (audioFile.exists()) {
+                    audioFile.delete();
+                }
+            }
+            return silkPath;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 调用ffmpeg,wav转 pcm
+     *
+     * @param wavPath wav文件地址
+     * @param target  转后文件地址
+     */
+    public static void transferWavPcm (String wavPath, String target) {
+        // ffmpeg -i input.wav -f s16le -ar 44100 -acodec pcm_s16le output.raw
+        transferAudioPcm(wavPath, target);
+    }
+
+    /**
+     * 调用ffmpeg,mp3转 pcm
+     *
+     * @param mp3Path mp3文件地址
+     * @param target  转后文件地址
+     */
+    public static void transferMp3Pcm(String mp3Path, String target) {
+        //ffmpeg -y -i 源文件 -f s16le -ar 24000 -ac 1 转换后文件位置
+        transferAudioPcm(mp3Path, target);
+    }
+
+    /**
+     * mp3/wav 通用
+     * @param fpath
+     * @param target
+     */
+    private static void transferAudioPcm(String fpath, String target) {
+        List<String> commend = new ArrayList<String>();
+        commend.add(path + "ffmpeg.exe");
+        commend.add("-y");
+        commend.add("-i");
+        commend.add(fpath);
+        commend.add("-f");
+        commend.add("s16le");
+        commend.add("-ar");
+        commend.add("24000");
+        commend.add("-ac");
+        commend.add("1");
+        commend.add(target);
+        try {
+            ProcessBuilder builder = new ProcessBuilder();
+            builder.command(commend);
+            Process p = builder.start();
+            p.waitFor();
+            p.destroy();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * silk_v3_encoder.exe,转成Silk格式
+     * @param pcmPath pcm 文件地址
+     * @param target  转换后的silk地址
+     * silk_v3_encoder.exe 路径
+     * pcm文件地址
+     * silk输出地址
+     * -Fs_API <Hz>            : API sampling rate in Hz, default: 24000
+     * -Fs_maxInternal <Hz>    : Maximum internal sampling rate in Hz, default: 24000
+     * -packetlength <ms>      : Packet interval in ms, default: 20
+     * -rate <bps>            : Target bitrate;   default: 25000
+     * -loss <perc>          : Uplink loss estimate, in percent (0-100);  default: 0
+     * -complexity <comp>   : Set complexity, 0: low, 1: medium, 2: high; default: 2
+     * -DTX <flag>          : Enable DTX (0/1); default: 0
+     * -quiet               : Print only some basic values
+     * -tencent             : Compatible with QQ/Wechat
+     */
+    public static void transferPcmSilk(String pcmPath, String target) {
+        Process process = null;
+        try {
+            /**
+             // 1、这一节的,语音长度太长会使音频长度丢失
+             List<String> commend = new ArrayList<>();
+             // 指令,可参照方法注释, 请不要在commend.add()里同时写【-参数 值】
+             commend.add(path + "silk_v3_encoder.exe");
+             commend.add(pcmPath);
+             commend.add(target);
+             commend.add("-tencent");
+             ProcessBuilder builder = new ProcessBuilder();
+             builder.command(commend);
+             process = builder.start();
+             // 如果删除下班这行写process.waitFor() ,太长的语音会阻塞,BufferedReader 打印出来太长的语音也会阻塞
+             process = Runtime.getRuntime().exec("taskkill -f -t -im silk_v3_encoder.exe");
+             */
+            // 方法2,除了会弹出弹窗,没什么问题 cmd /c 极为重要,执行完毕后会自动关闭
+            process = Runtime.getRuntime().exec("cmd /c start " + path + "silk_v3_encoder.exe " + pcmPath + " " + target + " -tencent");
+            process .waitFor();
+            Thread.sleep(1000);
+            // 有更好的方法会后续慢慢更新..
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (process != null) {
+                process.destroy();
+            }
+        }
+    }
+
+
+    /**
+     * mp3转amr(低质量qq语音)
+     * @param mp3Path MP3文件地址
+     * @param target 转换后文件地址
+     * return
+     */
+    public static void transferMp3Amr(String mp3Path, String target) {
+        // 被转换文件地址
+        File source = new File(path);
+        try {
+            if (!source.exists()) {
+                throw new Exception("文件不存在!");
+            }
+            List<String> commend = new ArrayList<String>();
+            commend.add(path + "ffmpeg.exe");
+            commend.add("-y");
+            commend.add("-i");
+            commend.add(mp3Path);
+            commend.add("-ac");
+            commend.add("1");
+            commend.add("-ar");
+            commend.add("8000");
+            commend.add(target);
+            try {
+                ProcessBuilder builder = new ProcessBuilder();
+                builder.command(commend);
+                Process p = builder.start();
+                p.waitFor();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        } catch (Exception e) {
+            System.out.println("mp3转amr异常");
+        }
+    }
+
+    /**
+     * 一个音频转byte类型的方法
+     * @param filePath
+     * @return
+     */
+    public static byte[] byteAudio(String filePath) {
+        try {
+            InputStream inStream = new FileInputStream(filePath);
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inStream.read(buffer)) > 0) {
+                baos.write(buffer, 0, bytesRead);
+            }
+            inStream.close();
+            baos.close();
+            return baos.toByteArray();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+}

+ 26 - 0
fs-qwhook-sop/src/main/java/com/fs/app/utils/JsonUtils.java

@@ -0,0 +1,26 @@
+package com.fs.app.utils;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+
+public class JsonUtils {
+    private static final ObjectMapper JSON = new ObjectMapper();
+
+    static {
+        JSON.setSerializationInclusion(Include.NON_NULL);
+        JSON.configure(SerializationFeature.INDENT_OUTPUT, Boolean.TRUE);
+    }
+
+    public static String toJson(Object obj) {
+        try {
+            return JSON.writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}

+ 87 - 0
fs-qwhook-sop/src/main/java/com/fs/app/utils/JwtUtils.java

@@ -0,0 +1,87 @@
+package com.fs.app.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * jwt工具类
+
+ */
+@ConfigurationProperties(prefix = "fs.jwt")
+@Component
+public class JwtUtils {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+
+    private String secret;
+    private long expire;
+    private String header;
+
+    /**
+     * 生成jwt token
+     */
+    public String generateToken(long userId) {
+        Date nowDate = new Date();
+        //过期时间
+        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
+
+        return Jwts.builder()
+                .setHeaderParam("typ", "JWT")
+                .setSubject(userId+"")
+                .setIssuedAt(nowDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    public Claims getClaimByToken(String token) {
+        try {
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        }catch (Exception e){
+            logger.debug("validate is token error ", e);
+            return null;
+        }
+    }
+
+    /**
+     * token是否过期
+     * @return  true:过期
+     */
+    public boolean isTokenExpired(Date expiration) {
+        return expiration.before(new Date());
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public String getHeader() {
+        return header;
+    }
+
+    public void setHeader(String header) {
+        this.header = header;
+    }
+}

+ 182 - 0
fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+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
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser 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, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole 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 sys_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 sys_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 ");
+                }
+            }
+        }
+
+        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;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-qwhook-sop/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);
+    }
+}

+ 244 - 0
fs-qwhook-sop/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,244 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+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.validation.BindingResult;
+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.Collection;
+import java.util.Iterator;
+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 = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            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, SysOperLog 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, SysOperLog 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 (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 117 - 0
fs-qwhook-sop/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-qwhook-sop/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());
+    }
+}

+ 85 - 0
fs-qwhook-sop/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;
+    }
+}

+ 109 - 0
fs-qwhook-sop/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,109 @@
+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.clickhouse")
+    public DataSource clickhouseDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,
+                                        @Qualifier("masterDataSource") DataSource masterDataSource,
+                                        @Qualifier("sopDataSource") DataSource sopDataSource,
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+
+        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource); // Ensure matching key
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.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;
+    }
+}

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