三七 пре 5 дана
родитељ
комит
8f62a552e9
59 измењених фајлова са 6680 додато и 3 уклоњено
  1. 1 1
      fs-qw-api-msg/src/main/resources/application.yml
  2. 1 1
      fs-service/src/main/resources/application-config-druid-sxjz.yml
  3. 148 0
      fs-service/src/main/resources/application-druid-hst-test.yml
  4. 164 0
      fs-service/src/main/resources/application-druid-sxjz-test.yml
  5. 1 1
      fs-service/src/main/resources/application-druid-syysy-test.yml
  6. 146 0
      fs-wx-task/pom.xml
  7. 14 0
      fs-wx-task/src/main/java/com/fs/FSServletInitializer.java
  8. 24 0
      fs-wx-task/src/main/java/com/fs/FsWxTaskApplication.java
  9. 132 0
      fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java
  10. 51 0
      fs-wx-task/src/main/java/com/fs/app/exception/FSException.java
  11. 81 0
      fs-wx-task/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  12. 1840 0
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java
  13. 87 0
      fs-wx-task/src/main/java/com/fs/app/task/WxTask.java
  14. 171 0
      fs-wx-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  15. 73 0
      fs-wx-task/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  16. 219 0
      fs-wx-task/src/main/java/com/fs/framework/aspectj/LogAspect.java
  17. 117 0
      fs-wx-task/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  18. 31 0
      fs-wx-task/src/main/java/com/fs/framework/config/ApplicationConfig.java
  19. 58 0
      fs-wx-task/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java
  20. 85 0
      fs-wx-task/src/main/java/com/fs/framework/config/CaptchaConfig.java
  21. 92 0
      fs-wx-task/src/main/java/com/fs/framework/config/DataSourceConfig.java
  22. 123 0
      fs-wx-task/src/main/java/com/fs/framework/config/DruidConfig.java
  23. 72 0
      fs-wx-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  24. 59 0
      fs-wx-task/src/main/java/com/fs/framework/config/FilterConfig.java
  25. 76 0
      fs-wx-task/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  26. 150 0
      fs-wx-task/src/main/java/com/fs/framework/config/MyBatisConfig.java
  27. 161 0
      fs-wx-task/src/main/java/com/fs/framework/config/RedisConfig.java
  28. 76 0
      fs-wx-task/src/main/java/com/fs/framework/config/ResourcesConfig.java
  29. 11 0
      fs-wx-task/src/main/java/com/fs/framework/config/RetryConfig.java
  30. 157 0
      fs-wx-task/src/main/java/com/fs/framework/config/SecurityConfig.java
  31. 33 0
      fs-wx-task/src/main/java/com/fs/framework/config/ServerConfig.java
  32. 121 0
      fs-wx-task/src/main/java/com/fs/framework/config/SwaggerConfig.java
  33. 115 0
      fs-wx-task/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  34. 77 0
      fs-wx-task/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  35. 27 0
      fs-wx-task/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  36. 45 0
      fs-wx-task/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  37. 115 0
      fs-wx-task/src/main/java/com/fs/framework/exception/GlobalExceptionHandler.java
  38. 56 0
      fs-wx-task/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  39. 126 0
      fs-wx-task/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  40. 56 0
      fs-wx-task/src/main/java/com/fs/framework/manager/AsyncManager.java
  41. 40 0
      fs-wx-task/src/main/java/com/fs/framework/manager/ShutdownManager.java
  42. 106 0
      fs-wx-task/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  43. 69 0
      fs-wx-task/src/main/java/com/fs/framework/security/LoginBody.java
  44. 255 0
      fs-wx-task/src/main/java/com/fs/framework/security/LoginUser.java
  45. 89 0
      fs-wx-task/src/main/java/com/fs/framework/security/SecurityUtils.java
  46. 47 0
      fs-wx-task/src/main/java/com/fs/framework/security/filter/JwtAuthenticationTokenFilter.java
  47. 35 0
      fs-wx-task/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java
  48. 54 0
      fs-wx-task/src/main/java/com/fs/framework/security/handle/LogoutSuccessHandlerImpl.java
  49. 92 0
      fs-wx-task/src/main/java/com/fs/framework/service/CompanyLoginService.java
  50. 66 0
      fs-wx-task/src/main/java/com/fs/framework/service/CompanyPermissionService.java
  51. 170 0
      fs-wx-task/src/main/java/com/fs/framework/service/PermissionService.java
  52. 236 0
      fs-wx-task/src/main/java/com/fs/framework/service/TokenService.java
  53. 75 0
      fs-wx-task/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java
  54. 1 0
      fs-wx-task/src/main/resources/META-INF/spring-devtools.properties
  55. 2 0
      fs-wx-task/src/main/resources/banner.txt
  56. 37 0
      fs-wx-task/src/main/resources/i18n/messages.properties
  57. 94 0
      fs-wx-task/src/main/resources/logback.xml
  58. 19 0
      fs-wx-task/src/main/resources/mybatis/mybatis-config.xml
  59. 1 0
      pom.xml

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

@@ -7,4 +7,4 @@ spring:
 #    active: druid-jzzx
 #    active: druid-hdt
 #    active: druid-sxjz
-    active: druid-hzyy-test
+    active: druid-sxjz-test

+ 1 - 1
fs-service/src/main/resources/application-config-druid-sxjz.yml

@@ -84,7 +84,7 @@ cloud_host:
 headerImg:
   imgUrl: https://jz-cos-1356808054.cos.ap-chengdu.myqcloud.com/fs/20250515/0877754b59814ea8a428fa3697b20e68.png
 ipad:
-  ipadUrl: http://ipad.xintaihl.cn
+  ipadUrl: http://f4469944.natappfree.cc
   aiApi: http://1.95.196.10:3000/api
   voiceApi:
 wx_miniapp_temp:

+ 148 - 0
fs-service/src/main/resources/application-druid-hst-test.yml

@@ -0,0 +1,148 @@
+# 数据源配置
+spring:
+  profiles:
+    include: common,config-druid-hst
+  # redis 配置
+  redis:
+    # 地址
+    host: 127.0.0.1
+    # 端口,默认为6379
+    port: 6379
+    # 密码
+    password:
+    # 连接超时时间
+    timeout: 30s
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 0
+        # 连接池中的最大空闲连接
+        max-idle: 8
+        # 连接池的最大数据库连接数
+        max-active: 100
+        # #连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: -1ms
+    database: 0
+  datasource:
+    mysql:
+      type: com.alibaba.druid.pool.DruidDataSource
+      driverClassName: com.mysql.cj.jdbc.Driver
+      druid:
+        # 主库数据源
+        master:
+          url: jdbc:mysql://119.29.19.149:2345/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+          username: root
+          password: ylrz17452131..!@YY
+        # 从库数据源
+        slave:
+          # 从数据源开关/默认关闭
+          enabled: false
+          url:
+          username:
+          password:
+        # 初始连接数
+        initialSize: 5
+        # 最小连接池数量
+        minIdle: 10
+        # 最大连接池数量
+        maxActive: 200
+        # 配置获取连接等待超时的时间
+        maxWait: 60000
+        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+        timeBetweenEvictionRunsMillis: 60000
+        # 配置一个连接在池中最小生存的时间,单位是毫秒
+        minEvictableIdleTimeMillis: 300000
+        # 配置一个连接在池中最大生存的时间,单位是毫秒
+        maxEvictableIdleTimeMillis: 900000
+        # 配置检测连接是否有效
+        validationQuery: SELECT 1 FROM DUAL
+        testWhileIdle: true
+        testOnBorrow: false
+        testOnReturn: false
+        webStatFilter:
+          enabled: true
+        statViewServlet:
+          enabled: false
+          # 设置白名单,不填则允许所有访问
+          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://119.29.19.149:2345/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+          username: root
+          password: ylrz17452131..!@YY
+        # 初始连接数
+        initialSize: 5
+        # 最小连接池数量
+        minIdle: 10
+        # 最大连接池数量
+        maxActive: 200
+        # 配置获取连接等待超时的时间
+        maxWait: 60000
+        # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+        timeBetweenEvictionRunsMillis: 60000
+        # 配置一个连接在池中最小生存的时间,单位是毫秒
+        minEvictableIdleTimeMillis: 300000
+        # 配置一个连接在池中最大生存的时间,单位是毫秒
+        maxEvictableIdleTimeMillis: 900000
+        # 配置检测连接是否有效
+        validationQuery: SELECT 1 FROM DUAL
+        testWhileIdle: true
+        testOnBorrow: false
+        testOnReturn: false
+        webStatFilter:
+          enabled: true
+        statViewServlet:
+          enabled: false
+          # 设置白名单,不填则允许所有访问
+          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
+openIM:
+  secret: openIM123
+  userID: imAdmin
+  url: https://web.im.fbylive.com/api
+
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: false
+
+enableRedPackAccount: 1

+ 164 - 0
fs-service/src/main/resources/application-druid-sxjz-test.yml

@@ -0,0 +1,164 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-druid-sxjz,common
+    # redis 配置
+    redis:
+        host: 127.0.0.1
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        #        clickhouse:
+        #            type: com.alibaba.druid.pool.DruidDataSource
+        #            driverClassName: 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: rt_2024
+        #            password: Yzx_19860213
+        #            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://139.155.247.58:2345/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Sxdtcbm@#+!2025
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    enabled: false
+                    url:
+                    username:
+                    password:
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 2000
+                # 配置获取连接等待超时的时间
+                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://139.155.247.58:2345/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Sxdtcbm@#+!2025
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 200
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+rocketmq:
+    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    consumer:
+        group: voice-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+# token配置
+token:
+    # 令牌自定义标识
+    header: Authorization
+    # 令牌密钥
+    secret: abcdefghijklmnopqrstuvwxyz
+    # 令牌有效期(默认30分钟)
+    expireTime: 180
+openIM:
+    secret:
+    userID:
+    url: https:/
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: false
+
+enableRedPackAccount: 1

+ 1 - 1
fs-service/src/main/resources/application-druid-syysy-test.yml

@@ -93,7 +93,7 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://172.16.0.7:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://139.199.225.59:2345/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
                     password: Ylrz_tM818782145I@
                 # 初始连接数

+ 146 - 0
fs-wx-task/pom.xml

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

+ 14 - 0
fs-wx-task/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(FsWxTaskApplication.class);
+    }
+}

+ 24 - 0
fs-wx-task/src/main/java/com/fs/FsWxTaskApplication.java

@@ -0,0 +1,24 @@
+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 FsWxTaskApplication
+{
+    public static void main(String[] args){
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(FsWxTaskApplication.class, args);
+        System.out.println("WxTask启动成功");
+    }
+}

+ 132 - 0
fs-wx-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,132 @@
+package com.fs.app.controller;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.app.service.WxTaskService;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.company.service.ICompanyVoiceRoboticCallLogCallphoneService;
+import com.fs.company.service.ICompanyWxAccountService;
+import com.fs.company.service.impl.CompanyWxServiceImpl;
+import com.fs.company.vo.CdrDetailVo;
+import com.fs.crm.service.ICrmCustomerPropertyService;
+import com.fs.wxcid.dto.message.SendImageMessageParam;
+import com.fs.wxcid.dto.message.SendVideoMessageParam;
+import com.fs.wxcid.service.MessageService;
+import io.swagger.annotations.Api;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Api("公共接口")
+@RestController
+@AllArgsConstructor
+@RequestMapping(value = "/app/common")
+public class CommonController {
+
+    private final WxTaskService taskService;
+    private final ICompanyWxAccountService companyWxAccountService;
+
+    @GetMapping("initAccountNum")
+    public void initAccountNum() {
+        taskService.initAccountNum();
+    }
+
+    @GetMapping("initAccountMsg")
+    public void initAccountMsg() {
+        taskService.initAccountMsg();
+    }
+
+    @GetMapping("addWx")
+    public void addWx(Long accountId) {
+        taskService.addWx(Collections.singletonList(accountId));
+    }
+
+    @GetMapping("isCheckContact")
+    public void isCheckContact(String formUser, Long accountId) {
+        companyWxAccountService.isCheckContact(formUser, accountId);
+    }
+
+    @GetMapping("cellRun")
+    public void cellRun() {
+        taskService.cellRun();
+    }
+
+    @GetMapping("callNextTask")
+    public void callNextTask() {
+        taskService.callNextTask();
+    }
+
+    @GetMapping("addWx4Workflow")
+    public void addWx4Workflow(Long accountId) {
+        taskService.addWx4Workflow(Collections.singletonList(accountId));
+    }
+
+    @GetMapping("cidWorkflowAddWxRun")
+    public void cidWorkflowAddWxRun() {
+        taskService.cidWorkflowAddWxRun();
+    }
+
+
+    @Autowired
+    CompanyWxServiceImpl ccompanyWxService;
+    @Autowired
+    MessageService messageService;
+
+    @GetMapping("mockAddWxSuccess/{clientId}")
+    public void mockAddWxSuccess(@PathVariable("clientId") Long clientId) {
+        ccompanyWxService.mockInterface(clientId);
+    }
+
+    @GetMapping("sendImg")
+    public void mockAddWxSuccess(Long accountId, String imgUrl, String toUser) {
+        SendImageMessageParam param = new SendImageMessageParam();
+        param.setAccountId(accountId);
+        param.setImgUrl(imgUrl);
+        param.setToUser(toUser);
+        messageService.sendImageMessage(param);
+    }
+
+    @GetMapping("sendVideo")
+    public void sendVideo(Long accountId, String thumbUrl, String videoUrl, String toUser) {
+        SendVideoMessageParam param = new SendVideoMessageParam();
+        param.setAccountId(accountId);
+        param.setThumbUrl(thumbUrl);
+        param.setVideoUrl(videoUrl);
+        param.setToUser(toUser);
+        messageService.sendVideoMessage(param);
+    }
+
+    @PostMapping("/saveCdrTest")
+    public String saveCdrTest(@RequestBody CdrDetailVo cdr) throws InstantiationException, IllegalAccessException {
+        log.info("cdr=" + cdr);
+        return  "success";
+    }
+
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
+    @Autowired
+    private ICompanyVoiceRoboticCallLogCallphoneService companyVoiceRoboticCallLogCallphoneService;
+
+    @PostMapping("/testProperty")
+    public void testProperty() throws JsonProcessingException {
+        log.info("开始解析");
+        CompanyVoiceRoboticCallLogCallphone byId = companyVoiceRoboticCallLogCallphoneService.getById(251L);
+        List<Map<String, Object>> maps = new ObjectMapper().readValue(
+                byId.getContentList(),
+                new TypeReference<List<Map<String, Object>>>() {
+                }
+        );
+        log.info("解析数据:{}", maps);
+        crmCustomerPropertyService.addPropertyByCallLog(byId);
+    }
+
+}

+ 51 - 0
fs-wx-task/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-wx-task/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());
+	}
+}

+ 1840 - 0
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -0,0 +1,1840 @@
+package com.fs.app.service;
+
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.core.redis.RedisCacheT;
+import com.fs.common.utils.PubFun;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.*;
+import com.fs.company.mapper.*;
+import com.fs.company.param.ExecutionContext;
+import com.fs.company.service.*;
+import com.fs.company.service.impl.*;
+import com.fs.company.service.impl.call.node.AiAddWxTaskNode;
+import com.fs.company.service.impl.call.node.AiQwAddWxTaskNode;
+import com.fs.company.service.impl.call.node.WorkflowNodeFactory;
+import com.fs.company.vo.CompanyWxClient4WorkFlowVO;
+import com.fs.course.config.RedisKeyScanner;
+import com.fs.crm.param.SmsSendBatchParam;
+import com.fs.enums.ExecutionStatusEnum;
+import com.fs.enums.NodeTypeEnum;
+import com.fs.company.util.ObjectPlaceholderResolver;
+import com.fs.company.vo.SendMsgVo;
+import com.fs.course.config.WxConfig;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.service.ICrmCustomerService;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qwApi.domain.QwLinkCreateResult;
+import com.fs.qwApi.param.QwLinkCreateParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.system.service.ISysConfigService;
+import com.fs.voice.utils.StringUtil;
+import com.fs.wxcid.dto.friend.AddContactParam;
+import com.fs.wxcid.service.FriendService;
+import com.fs.wxcid.vo.AddContactVo;
+import com.fs.wxwork.dto.WxAddSearchDTO;
+import com.fs.wxwork.dto.WxSearchContactDTO;
+import com.fs.wxwork.dto.WxSearchContactResp;
+import com.fs.wxwork.dto.WxWorkResponseDTO;
+import com.fs.wxwork.service.WxWorkService;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.AllArgsConstructor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class WxTaskService {
+
+
+    @Value("${cid-group-no:0}")
+    private Integer cidGroupNo;
+
+    private final ICompanyWxAccountService companyWxAccountService;
+    private final ISysConfigService sysConfigService;
+    private final ICompanyWxClientService companyWxClientService;
+    private final ICompanyWxDialogService companyWxDialogService;
+    private final ICompanyVoiceRoboticService companyVoiceRoboticService;
+    private final ObjectPlaceholderResolver objectPlaceholderResolver;
+    private final ICrmCustomerService crmCustomerService;
+    private final FriendService friendService;
+    private final CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+    private final RedisCacheT<String> redisCache;
+    private final CompanyVoiceRoboticCalleesMapper companyVoiceRoboticCalleesMapper;
+    private final CompanyVoiceRoboticWxMapper companyVoiceRoboticWxMapper;
+    private final CompanyWxClientMapper companyWxClientMapper;
+    private final CompanyVoiceRoboticWxServiceImpl companyVoiceRoboticWxServiceImpl;
+    private final CompanyWxAccountMapper companyWxAccountMapper;
+    private final CompanyVoiceRoboticCalleesServiceImpl companyVoiceRoboticCalleesServiceImpl;
+    private RedissonClient redissonClient;
+    private final CompanyVoiceRoboticServiceImpl companyVoiceRoboticServiceImpl;
+    private final CompanyVoiceRoboticCallLogCallphoneServiceImpl companyVoiceRoboticCallLogCallphoneService;
+    private final CompanyAiWorkflowExecMapper companyAiWorkflowExecMapper;
+    private final CompanyWorkflowEngine companyWorkflowEngine;
+    private final CompanyVoiceRoboticBusinessMapper companyVoiceRoboticBusinessMapper;
+    private final WorkflowNodeFactory workflowNodeFactory;
+    private final CompanySmsTempServiceImpl smsTempService;
+    private final ICompanySmsService companySmsService;
+    private final ICompanyUserService companyUserService;
+    private final CompanyVoiceRoboticCallLogSendmsgServiceImpl companyVoiceRoboticCallLogSendmsgService;
+    private final QwApiService qwApiService;
+    private final RedisCache redisCache2;
+    private final ExecutorService cidExcutor = new ThreadPoolExecutor(
+            32,
+            64,
+            60L,
+            TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(1000),
+            r -> new Thread(r, "callPool-" + System.currentTimeMillis()),
+            new ThreadPoolExecutor.CallerRunsPolicy()
+    );
+    private final RedisKeyScanner redisKeyScanner;
+    private final QwUserMapper qwUserMapper;
+    private final WxWorkService wxWorkService;
+    private final QwExternalContactMapper qwExternalContactMapper;
+    private final CompanyAiWorkflowExecLogMapper companyAiWorkflowExecLogMapper;
+
+    public void addWx(List<Long> accountIdList) {
+        log.info("==========执行加微信任务开始==========");
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        // 需要添加微信的列表
+        List<CompanyWxClient> list = companyWxClientService.getAddWxList(accountIdList,1);
+        //排除掉没到达加微步骤的人
+        List<CompanyVoiceRoboticCallees> exList = companyVoiceRoboticCalleesMapper.selectExcludeList(list, 1);
+        List<CompanyVoiceRoboticCallees> collect =
+                exList.stream().filter(e -> !Constants.ADD_WX.equals(getNextTaskOptimized(e.getTaskFlow(), e.getRunTaskFlow())))
+                        .collect(Collectors.toList());
+        Set<String> existingKeys = collect.stream()
+                .map(callee -> callee.getRoboticId() + "_" + callee.getUserId())
+                .collect(Collectors.toSet());
+
+        list = list.stream()
+                .filter(client -> {
+                    String key = client.getRoboticId() + "_" + client.getCustomerId();
+                    return !existingKeys.contains(key);
+                })
+                .collect(Collectors.toList());
+
+        log.info("需要添加微信的数量:{}", list.size());
+        if (list.isEmpty()) return;
+        List<CompanyWxClient> addList = new ArrayList<>();
+        Map<Long, CompanyWxClient> clientMap = PubFun.listToMapByGroupObject(list, CompanyWxClient::getAccountId);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(clientMap.keySet()));
+        log.info("查询加微的账号数量:{}", list.size());
+        List<CompanyWxAccount> addAccountList = accountList.stream().filter(e -> {
+            int newAddWxMinute = RandomUtil.randomInt(config.getNewAccountAddWxMin(), config.getNewAccountAddWxMax());
+            int addWxMinute = RandomUtil.randomInt(config.getAccountAddWxMin(), config.getAccountAddWxMax());
+            if (e.getLastAddWxTime() == null) {
+                log.info("加微时间为空,可以加微:{}", e.getWxNickName());
+                return true;
+            }
+            int minute = addWxMinute;
+            if (e.getIsNew() == 0) {
+                minute = newAddWxMinute;
+            }
+            long until = e.getLastAddWxTime().until(LocalDateTime.now(), ChronoUnit.MINUTES);
+            log.info("判断{}是否达到最低加微时间:上次加微时间:{},时间间隔:{},当前时间:{}", e.getWxNickName(), e.getLastAddWxTime(), until, LocalDateTime.now());
+            return until > minute;
+        }).collect(Collectors.toList());
+        log.info("实际加微的账号数量:{}", addAccountList.size());
+        addAccountList.forEach(e -> {
+            CompanyWxClient client = clientMap.get(e.getId());
+            if (client != null) {
+                String task = redisCache.getCacheObject(Constants.TASK_ID + client.getRoboticId());
+                log.info("ROBOTIC-ID:{},CLIENT-ID:{},当前任务执行状态:{}", client.getRoboticId(), client.getId(), task);
+                if (StringUtils.isNotEmpty(task) && Constants.ADD_WX.equals(task)) {
+                    CompanyWxDialog dialog = companyWxDialogService.getById(client.getDialogId());
+                    CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+                    String newTxt = objectPlaceholderResolver.resolvePlaceholders(crmCustomer, dialog.getTemplateDetails());
+                    AddContactParam addParam = new AddContactParam();
+                    addParam.setAccountId(e.getId());
+                    addParam.setMobile(crmCustomer.getMobile());
+                    addParam.setTxt(newTxt);
+                    addParam.setClientId(client.getId());
+                    AddContactVo vo = friendService.addContact(addParam);
+                    JSONObject runParam = new JSONObject();
+                    runParam.put("id", e.getId());
+                    runParam.put("mobile", crmCustomer.getMobile());
+                    runParam.put("txt", newTxt);
+                    runParam.put("clientId", client.getId());
+                    CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                            runParam.toJSONString(), client.getId(), client.getRoboticId(), e.getId(), e.getCompanyId(),0);
+                    log.info("ROBOTIC-ID:{},CLIENT-ID:{},执行加微:{},客户:{}-{},使用话术:{}", client.getRoboticId(), client.getId(), e.getId(), client.getCustomerId(), crmCustomer.getCustomerName(), dialog.getName());
+                    if (vo.isSuccess()) {
+                        e.setLastAddWxTime(LocalDateTime.now());
+                        e.setIsAddNum(e.getIsAddNum() + 1);
+                        client.setIsAdd(2);
+                        client.setAddTime(LocalDateTime.now());
+                        client.setWxV3(vo.getV3());
+                        client.setWxV4(vo.getV4());
+                        addList.add(client);
+                        addLog.setStatus(2);
+                        addLog.setResult(JSON.toJSONString(vo));
+                    } else {
+                        log.error("ROBOTIC-ID:{},加微失败:{}", client.getRoboticId(), vo);
+                        addLog.setStatus(3);
+                        addLog.setResult(JSON.toJSONString(vo));
+                    }
+                    asyncSaveCompanyVoiceRoboticCallLog(addLog);
+                } else {
+                    log.error("ROBOTIC-ID:{},当前任务没有执行加微任务", client.getRoboticId());
+                }
+            } else {
+                log.error("当前账号暂无需要添加微信:{}-{}", e.getId(), e.getWxNickName());
+            }
+        });
+        if (!addList.isEmpty()) {
+            companyWxClientService.updateBatchById(addList);
+            //根据加微成功的用户,判定是否加入延时执行下一步任务
+            Set<Long> roboticIdSet = addList.stream().map(CompanyWxClient::getRoboticId).collect(Collectors.toSet());
+            Set<Long> userIdSet = addList.stream().map(CompanyWxClient::getCustomerId).collect(Collectors.toSet());
+
+            //找到任务
+            List<CompanyVoiceRobotic> companyVoiceRobotics = companyVoiceRoboticMapper.selectBatchIds(roboticIdSet);
+            Map<Long, CompanyVoiceRobotic> roboticsMp = companyVoiceRobotics.stream().collect(Collectors.toMap(CompanyVoiceRobotic::getId, Function.identity(), (existing, replacement) -> existing));
+            //找到callees数据
+            List<CompanyVoiceRoboticCallees> companyVoiceRoboticCallees = companyVoiceRoboticCalleesMapper.selectCalleesListByRoboticIdsAndUserIds(userIdSet, roboticIdSet);
+            Map<String, CompanyVoiceRoboticCallees> calleesMp = companyVoiceRoboticCallees.stream().collect(Collectors.toMap(e -> e.getUserId() + "-" + e.getRoboticId(), Function.identity(), (existing, replacement) -> existing));
+
+            long l = System.currentTimeMillis();
+
+            //根据加微成功
+            for (CompanyWxClient client : addList) {
+                CompanyVoiceRobotic clientRobotic = roboticsMp.getOrDefault(client.getRoboticId(), null);
+                if (null == clientRobotic) {
+                    log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有找到任务", client.getRoboticId(), client.getId());
+                    continue;
+                }
+                CompanyVoiceRoboticCallees callees = calleesMp.getOrDefault(client.getCustomerId() + "-" + client.getRoboticId(), null);
+                if (null == callees) {
+                    log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有找到任务", client.getRoboticId(), client.getId());
+                    continue;
+                }
+                Integer addWxTime = clientRobotic.getAddWxTime();
+                if (null == addWxTime) {
+                    log.error("ROBOTIC-ID:{},CLIENT-ID:{},没有设置加微后置等待时间", client.getRoboticId(), client.getId());
+                } else {
+                    long endT = System.currentTimeMillis() + addWxTime * 60 * 1000;
+                    StringBuilder sb = new StringBuilder(Constants.CID_NEXT_TASK_ID).append(callees.getRoboticId()).append(":").append(callees.getId());
+                    redisCache.setCacheObject(sb.toString(), String.valueOf(endT));
+                }
+            }
+            companyVoiceRoboticCallees.forEach(robotic ->
+                    robotic.setRunTaskFlow(
+                            StringUtils.isBlank(robotic.getRunTaskFlow()) ?
+                                    Constants.ADD_WX : robotic.getRunTaskFlow() + "," + Constants.ADD_WX
+                    )
+            );
+            companyVoiceRoboticCalleesServiceImpl.updateBatchById(companyVoiceRoboticCallees);
+            companyVoiceRoboticServiceImpl.finishAddWxByCallees(roboticIdSet);
+        }
+        if (!addAccountList.isEmpty()) {
+            companyWxAccountService.updateBatchById(addAccountList);
+        }
+
+    }
+
+    public void addWx4Workflow(List<Long> accountIdList) {
+        log.info("==========执行加微信任务开始==========");
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        // 需要添加微信的列表
+        List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getAddWxList4Workflow(accountIdList,cidGroupNo);
+        log.info("需要添加微信的数量:{}", list.size());
+        if (list.isEmpty()) return;
+        List<CompanyWxClient> addList = new ArrayList<>();
+        Map<Long, CompanyWxClient4WorkFlowVO> clientMap = PubFun.listToMapByGroupObject(list, CompanyWxClient4WorkFlowVO::getAccountId);
+        List<CompanyWxAccount> accountList = new ArrayList<>(companyWxAccountService.listByIds(clientMap.keySet()));
+        log.info("查询加微的账号数量:{}", list.size());
+        List<CompanyWxAccount> addAccountList = accountList.stream().filter(e -> {
+            int newAddWxMinute = RandomUtil.randomInt(config.getNewAccountAddWxMin(), config.getNewAccountAddWxMax());
+            int addWxMinute = RandomUtil.randomInt(config.getAccountAddWxMin(), config.getAccountAddWxMax());
+            if (e.getLastAddWxTime() == null) {
+                log.info("加微时间为空,可以加微:{}", e.getWxNickName());
+                return true;
+            }
+            int minute = addWxMinute;
+            if (e.getIsNew() == 0) {
+                minute = newAddWxMinute;
+            }
+            long until = e.getLastAddWxTime().until(LocalDateTime.now(), ChronoUnit.MINUTES);
+            log.info("判断{}是否达到最低加微时间:上次加微时间:{},时间间隔:{},当前时间:{}", e.getWxNickName(), e.getLastAddWxTime(), until, LocalDateTime.now());
+            return until > minute;
+        }).collect(Collectors.toList());
+        log.info("实际加微的账号数量:{}", addAccountList.size());
+        addAccountList.forEach(e -> {
+            CompanyWxClient4WorkFlowVO client = clientMap.get(e.getId());
+            if (client != null) {
+                    CompanyWxDialog dialog = companyWxDialogService.getById(client.getDialogId());
+                    CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+                    String newTxt = objectPlaceholderResolver.resolvePlaceholders(crmCustomer, dialog.getTemplateDetails());
+                    AddContactParam addParam = new AddContactParam();
+                    addParam.setAccountId(e.getId());
+                    addParam.setMobile(crmCustomer.getMobile());
+                    addParam.setTxt(newTxt);
+                    addParam.setClientId(client.getId());
+                    AddContactVo vo = friendService.addContact(addParam);
+                    JSONObject runParam = new JSONObject();
+                    runParam.put("id", e.getId());
+                    runParam.put("mobile", crmCustomer.getMobile());
+                    runParam.put("txt", newTxt);
+                    runParam.put("clientId", client.getId());
+                    CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                            runParam.toJSONString(), client.getId(), client.getRoboticId(), e.getId(), e.getCompanyId(),0);
+                    log.info("ROBOTIC-ID:{},CLIENT-ID:{},执行加微:{},客户:{}-{},使用话术:{}", client.getRoboticId(), client.getId(), e.getId(), client.getCustomerId(), crmCustomer.getCustomerName(), dialog.getName());
+                    if (vo.isSuccess()) {
+                        e.setLastAddWxTime(LocalDateTime.now());
+//                        todo 删除还原 以下为测试所用
+                        e.setLastAddWxTime(LocalDateTime.now().plus(-1, ChronoUnit.DAYS));
+                        e.setIsAddNum(e.getIsAddNum() + 1);
+                        client.setIsAdd(2);
+                        client.setAddTime(LocalDateTime.now());
+                        client.setWxV3(vo.getV3());
+                        client.setWxV4(vo.getV4());
+                        CompanyWxClient addItem = new CompanyWxClient();
+                        BeanUtils.copyProperties(client, addItem);
+                        addList.add(addItem);
+                        addLog.setStatus(2);
+                        addLog.setResult(JSON.toJSONString(vo));
+                    } else {
+                        log.error("ROBOTIC-ID:{},加微失败:{}", client.getRoboticId(), vo);
+                        addLog.setStatus(3);
+                        addLog.setResult(JSON.toJSONString(vo));
+                    }
+                    asyncSaveCompanyVoiceRoboticCallLog(addLog);
+            } else {
+                log.error("当前账号暂无需要添加微信:{}-{}", e.getId(), e.getWxNickName());
+            }
+        });
+        if (!addList.isEmpty()) {
+            companyWxClientService.updateBatchById(addList);
+            long l = System.currentTimeMillis();
+            //根据加微成功
+            for (CompanyWxClient client : addList) {
+                CompanyWxClient4WorkFlowVO vo = clientMap.get(client.getAccountId());
+                IWorkflowNode node = workflowNodeFactory.createNode(vo.getCurrentNodeKey(),
+                        NodeTypeEnum.fromValue(vo.getCurrentNodeType()),
+                        vo.getCurrentNodeName(), null);
+                if (node instanceof AiAddWxTaskNode) {
+                    CompletableFuture.runAsync(() -> {
+                        AiAddWxTaskNode addWxNode = (AiAddWxTaskNode) node;
+                        addWxNode.doneAddwx(vo.getWorkflowInstanceId());
+                    }, cidExcutor);
+                }
+            }
+            if (!addAccountList.isEmpty()) {
+                companyWxAccountService.updateBatchById(addAccountList);
+            }
+        }
+    }
+
+    public void initAccountNum() {
+        LocalDateTime now = LocalDateTime.now();
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        List<CompanyWxAccount> list = companyWxAccountService.list();
+        list.forEach(e -> {
+            if (e.getAccountCreateTime() != null) {
+                long until = e.getAccountCreateTime().until(now.toLocalDate(), ChronoUnit.DAYS);
+                if (until > config.getNewAccountTime()) {
+                    e.setIsNew(1);
+                }
+            }
+            if (e.getIsNew() == 0) {
+                e.setAddNum(config.getNewAccountAddNum());
+            } else {
+                e.setAddNum(RandomUtil.randomInt(config.getAccountAddMax(), config.getAccountAddMin()));
+            }
+            e.setIsAddNum(0);
+            e.setAllocateNum(0);
+        });
+        companyWxAccountService.updateBatchById(list);
+    }
+
+
+    public void initAccountMsg() {
+        String json = sysConfigService.selectConfigByKey("wx.config");
+        WxConfig config = JSONUtil.toBean(json, WxConfig.class);
+        List<CompanyWxAccount> list = companyWxAccountService.list();
+        list.forEach(e -> {
+            int txtNum = RandomUtil.randomInt(config.getTxtMsgMinNum(), config.getTxtMsgMaxNum());
+            int imgNum = RandomUtil.randomInt(config.getImgMsgMinNum(), config.getImgMsgMaxNum());
+            e.setSendMsgJson(JSON.toJSONString(SendMsgVo.builder().txt(txtNum).img(imgNum).build()));
+        });
+        companyWxAccountService.updateBatchById(list);
+    }
+
+    public void cellRun() {
+        log.info("===========CID任务开始执行===========");
+        List<CompanyVoiceRobotic> list = companyVoiceRoboticMapper.selectList(new QueryWrapper<CompanyVoiceRobotic>().eq("task_status", 1));
+        List<CompanyVoiceRobotic> successList = list.stream().filter(e -> StringUtils.isNotEmpty(e.getRunTaskFlow()) && e.getTaskFlow().length() == e.getRunTaskFlow().length()).collect(Collectors.toList());
+        List<CompanyVoiceRobotic> waitList = list.stream().filter(e -> StringUtils.isEmpty(e.getRunTaskFlow()) || e.getTaskFlow().length() != e.getRunTaskFlow().length()).collect(Collectors.toList());
+        successList.forEach(e -> e.setTaskStatus(3));
+        if (!successList.isEmpty()) {
+            log.info("已经完成任务:{}", successList.size());
+            companyVoiceRoboticService.updateBatchById(successList);
+        }
+        waitList.forEach(e -> {
+            log.info("ROBOTIC-ID:{},开始执行小任务:{}", e.getId(), e.getName());
+            List<String> taskFlow = new ArrayList<>(Arrays.asList(e.getTaskFlow().split(",")));
+            log.info("ROBOTIC-ID:{},当前任务顺序:{}", e.getId(), e.getTaskFlow());
+            String runTaskFlow = e.getRunTaskFlow();
+            log.info("ROBOTIC-ID:{},已有任务:{}", e.getId(), e.getRunTaskFlow());
+            if (StringUtils.isNotEmpty(runTaskFlow)) {
+                Arrays.asList(runTaskFlow.split(",")).forEach(taskFlow::remove);
+            }
+            log.info("ROBOTIC-ID:{},当前还剩余任务:{}", e.getId(), taskFlow);
+            e.setNowTask(taskFlow.get(0));
+            log.info("ROBOTIC-ID:{},当前需要执行任务:{}", e.getId(), e.getNowTask());
+        });
+        Function<CompanyVoiceRobotic, String> getKey = e -> Constants.TASK_ID + e.getId();
+        waitList.forEach(e -> {
+            if (redisCache.getCacheObject(getKey.apply(e)) != null) {
+                log.info("ROBOTIC-ID:{},已有正在执行任务", e.getId());
+                return;
+            }
+            redisCache.setCacheObject(getKey.apply(e), e.getNowTask());
+            try {
+                switch (e.getNowTask()) {
+                    case Constants.CELL_PHONE:
+                        companyVoiceRoboticService.addTask(e);
+                        log.info("ROBOTIC-ID:{},打电话任务创建完成", e.getId());
+                        break;
+                    case Constants.ADD_WX:
+                        //第一步是调用添加微信步骤
+                        if (StringUtils.isBlank(e.getRunTaskFlow()) && StringUtils.isNotBlank(e.getTaskFlow()) && e.getTaskFlow().startsWith(Constants.ADD_WX)) {
+                            companyVoiceRoboticServiceImpl.allocateWx(e);
+//                            CompletableFuture.supplyAsync(()->{
+//                                //分配个微账号
+//                                return allocateWx(e);
+//                            },cidExcutor).thenApply(result->{
+//                                //逐条添加微信,且判定是否任务
+//                                for (CompanyWxClient client : result) {
+//                                    ArrayList<Long> addWxParamList = new ArrayList<>();
+//                                    addWxParamList.add(client.getAccountId());
+//                                    //添加微信 todo 暂时注释掉 不在添加微信 发布时需要开放
+////                                    addWx(addWxParamList);
+//                                    //判定任务是否有加微后等待时间设定,加入到待执行任务redis
+//                                    if(null != e.getAddWxTime() && e.getAddWxTime() > 0){
+//                                        long endT = System.currentTimeMillis() + e.getAddWxTime() * 60 * 1000;
+//                                        //通过任务+用户id找到calles记录
+//                                        CompanyVoiceRoboticCallees callees = companyVoiceRoboticCalleesMapper.getCalleesByUserIdAndTaskId(client.getCustomerId(), e.getId());
+//                                        if(null != callees && !ObjectUtil.isEmpty(callees)){
+//                                            Long calleesId = callees.getId();
+//                                            StringBuilder sb = new StringBuilder(Constants.CID_NEXT_TASK_ID).append(e.getId()).append(":").append(calleesId);
+//                                            redisCache.setCacheObject(sb.toString(), String.valueOf(endT),e.getAddWxTime() + 5, TimeUnit.MINUTES);
+//                                            if(StringUtils.isNotBlank(callees.getRunTaskFlow())){
+//                                                callees.setRunTaskFlow(callees.getRunTaskFlow() + "," + Constants.ADD_WX);
+//                                            }else{
+//                                                callees.setRunTaskFlow(Constants.ADD_WX);
+//                                            }
+//                                            companyVoiceRoboticCalleesMapper.updateById(callees);
+//                                        }
+//                                    }
+//                                    e.setRunTaskFlow(Constants.ADD_WX);
+//                                    //更新任务 已跑任务值
+//                                    companyVoiceRoboticMapper.updateById(e);
+//                                }
+//                                return null;
+//                            }).exceptionally(ex -> {
+//                                log.error("ROBOTIC-ID:{},任务执行异常:{}", e.getId(), e.getNowTask(), ex);
+//                                return null;
+//                            });
+                        } else {
+                            //todo 接入原有加微逻辑
+                        }
+                        break;
+                    case Constants.SEND_MSG:
+
+                        break;
+                }
+            } catch (Exception exception) {
+                log.error("ROBOTIC-ID:{},任务执行失败:{}", e.getId(), e.getNowTask(), exception);
+                redisCache.deleteObject(getKey.apply(e));
+            }
+        });
+        log.info("===========CID任务执行完成===========");
+    }
+
+//    /**
+//     * 分配账号
+//     * @param robotic
+//     */
+//    @Transactional
+//    @Deprecated
+//    public List<CompanyWxClient> allocateWx(CompanyVoiceRobotic robotic) {
+//        List<CompanyWxClient> resArr = new ArrayList<>();
+//        //找到任务指定的微信用户
+//        List<CompanyVoiceRoboticWx> companyVoiceRoboticWxes = companyVoiceRoboticWxMapper.selectByRoboticIdWithGroupBy(robotic.getId());
+//        Integer totalSize = 0;
+//        if (null != companyVoiceRoboticWxes && !companyVoiceRoboticWxes.isEmpty()) {
+//            totalSize = companyVoiceRoboticWxes.size();
+//        } else {
+//            log.error("分配对象空,数据异常");
+//            throw new RuntimeException("没有找到任务指定的微信用户");
+//        }
+//        List<CompanyWxAccount> accountIds = companyWxAccountMapper.selectBatchIds(PubFun.listToNewList(companyVoiceRoboticWxes, CompanyVoiceRoboticWx::getAccountId));
+//        Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(accountIds, CompanyWxAccount::getId);
+//        List<CompanyWxClient> companyWxClients = companyWxClientMapper.selectListByRoboticId(robotic.getId());
+//        List<Long> ids = PubFun.listToNewList(companyWxClients, CompanyWxClient::getCustomerId);
+//        List<CrmCustomer> crmCustomerList = crmCustomerService.selectCrmCustomerListByIds(ids.stream().map(e -> e + "").collect(Collectors.joining(",")));
+//        Map<Long, CrmCustomer> customerMap = PubFun.listToMapByGroupObject(crmCustomerList, CrmCustomer::getCustomerId);
+//        if (null == companyWxClients || companyWxClients.isEmpty()) {
+//            log.error("分配个微空,数据异常");
+//            throw new RuntimeException("没有找到需要分配微信用户");
+//        }
+//        Integer allocateIndex = 0;
+//        List<CompanyVoiceRoboticWx> updateCompanyVoiceRoboticWxList = new ArrayList<>();
+//        //分配客户
+//        for (CompanyWxClient companyWxClient : companyWxClients) {
+//            new CompanyVoiceRoboticWx();
+//            CompanyVoiceRoboticWx wx = companyVoiceRoboticWxes.get(allocateIndex++ % totalSize);
+//            CompanyWxAccount account = accountMap.get(wx.getAccountId());
+//            CrmCustomer crmCustomer = customerMap.get(companyWxClient.getCustomerId());
+//            companyWxClient.setRoboticWxId(wx.getId());
+//            companyWxClient.setAccountId(wx.getAccountId());
+//            companyWxClient.setDialogId(wx.getWxDialogId());
+//            companyWxClient.setCompanyUserId(account.getCompanyUserId());
+//            companyWxClient.setNickName(crmCustomer.getCustomerName());
+//            companyWxClient.setPhone(crmCustomer.getMobile());
+//            resArr.add(companyWxClient);
+//            if (wx.getNum() == null) {
+//                wx.setNum(1);
+//            } else {
+//                wx.setNum(wx.getNum() + 1);
+//            }
+//            updateCompanyVoiceRoboticWxList.add(wx);
+//        }
+//        //保存数据库
+//        companyWxClientService.updateBatchById(companyWxClients);
+//        companyVoiceRoboticWxServiceImpl.updateBatchById(updateCompanyVoiceRoboticWxList);
+//        return resArr;
+//    }
+
+    /**
+     * 单任务加微
+     *
+     * @param roboticId
+     * @param callerId
+     * @return
+     */
+    private Boolean addWxOne(Long roboticId, Long callerId) {
+
+        return Boolean.TRUE;
+    }
+
+    /**
+     * 单个任务执行且为单条执行对象
+     *
+     * @param roboticId
+     * @param callerId
+     */
+    public String cellRunOne(Long roboticId, Long callerId) {
+
+        //查询任务执行情况
+        CompanyVoiceRoboticCallees data = companyVoiceRoboticCalleesMapper.selectDataByCalleesId(callerId);
+        CompanyVoiceRobotic robotic = companyVoiceRoboticMapper.selectCompanyVoiceRoboticById(roboticId);
+        String taskFlow = data.getTaskFlow();
+        if (null == data || null == robotic) {
+            log.error("没有查询到任务执行数据,roboticId:{},callerId:{}", roboticId, callerId);
+            return null;
+        }
+        if (Integer.valueOf(3).equals(robotic.getTaskStatus())) {
+            log.error("执行任务已经完成了,roboticId:{}", roboticId);
+            return null;
+        }
+        String nextTask;
+        if (StringUtils.isNotBlank(data.getRunTaskFlow()) && StringUtils.isNotBlank(taskFlow)) {
+            nextTask = getNextTaskOptimized(taskFlow, data.getRunTaskFlow());
+        } else {
+            //如果个人任务流程没有维护,执行当前个人在任务流程中的任务流程节点
+            nextTask = getNextTaskOptimized(robotic.getTaskFlow(), robotic.getRunTaskFlow());
+            taskFlow = robotic.getTaskFlow();
+            data.setRunTaskFlow(robotic.getRunTaskFlow());
+//            return null;
+        }
+        if (StringUtils.isBlank(nextTask)) {
+            log.error("任务没有下个执行任务,标记完成,roboticId:{}", roboticId);
+            companyVoiceRoboticMapper.finishRobotic(roboticId);
+            return null;
+        }
+        log.info("单人单任务执行ROBOTIC-ID:{},caller_id:{},当前需要执行任务:{}", roboticId, callerId, nextTask);
+        String nextTaskOptimized = null;
+        try {
+            switch (nextTask) {
+                case Constants.CELL_PHONE:
+                    companyVoiceRoboticService.callPhoneOne(roboticId, callerId);
+                    nextTaskOptimized = getNextTaskOptimized(taskFlow, data.getRunTaskFlow() + "," + Constants.CELL_PHONE);
+                    break;
+                case Constants.ADD_WX:
+                    Boolean success = addWxOne(roboticId, callerId);
+                    break;
+                case Constants.SEND_MSG:
+                    if (Integer.valueOf(0).equals(data.getIsSendMsg())) {
+                        //发送短信前一个任务如果是打电话 等待电话打完以后再执行发送
+                        String lastTaskOptimized = getLastTaskOptimized(taskFlow);
+                        if (Constants.CELL_PHONE.equals(lastTaskOptimized)) {
+                            //是否打电话结束有回调值 完成电话动作以后执行下一步
+                            CompanyVoiceRoboticCallLogCallphone companyVoiceRoboticCallLogCallphone = companyVoiceRoboticCallLogCallphoneService.selectLogByRoboticIdAndCallerId(roboticId, callerId);
+                            if (null != companyVoiceRoboticCallLogCallphone && companyVoiceRoboticCallLogCallphone.getStatus() == 1) {
+                                nextTaskOptimized = "wait callPhone";
+                                break;
+                            }
+//                            todo AI电话执行失败了 是否执行发送短信
+//                            if(null != companyVoiceRoboticCallLogCallphone && companyVoiceRoboticCallLogCallphone.getStatus() == 3){
+//                                nextTaskOptimized = "error callPhone";
+//                                break;
+//                            }
+                        }
+                        companyVoiceRoboticService.sendMsgOne(roboticId, callerId);
+                        nextTaskOptimized = getNextTaskOptimized(taskFlow, data.getRunTaskFlow() + "," + Constants.SEND_MSG);
+                        break;
+                    } else {
+                        log.info("不再需要发送短信处理,roboticId:{},callerId:{}", roboticId, callerId);
+                        nextTaskOptimized = getNextTaskOptimized(taskFlow, data.getRunTaskFlow() + "," + Constants.SEND_MSG);
+                        break;
+                    }
+            }
+        } catch (Exception ex) {
+            log.error("执行任务异常,roboticId:{},callerId:{},nextTask:{}", roboticId, callerId, nextTask, ex);
+            nextTaskOptimized = "exception";
+        }
+
+        if (StringUtils.isNotBlank(nextTaskOptimized)) {
+            return nextTaskOptimized;
+        } else {
+            //任务执行完了 没有下一步 直接完成任务
+//            companyVoiceRoboticMapper.finishRobotic(roboticId);
+            return null;
+        }
+
+    }
+
+    /**
+     * 获取下一个任务
+     *
+     * @param taskFlow
+     * @param runTaskFlow
+     * @return
+     */
+    public String getNextTaskOptimized(String taskFlow, String runTaskFlow) {
+        if (StringUtils.isBlank(taskFlow)) {
+            return null;
+        }
+        String[] allTasks = taskFlow.split(",");
+        Set<String> executedTasks = new HashSet<>();
+
+        if (StringUtils.isNotBlank(runTaskFlow)) {
+            executedTasks.addAll(Arrays.asList(runTaskFlow.split(",")));
+        }
+        for (String task : allTasks) {
+            if (!executedTasks.contains(task.trim())) {
+                return task.trim();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 获取最后一个执行的任务
+     *
+     * @param taskFlow
+     * @return
+     */
+    public String getLastTaskOptimized(String taskFlow) {
+        return taskFlow.substring(taskFlow.lastIndexOf(",") + 1);
+    }
+
+    /**
+     * 调用下一个任务
+     */
+    public void callNextTask() {
+        //
+        RLock lock = redissonClient.getLock("CID_CALL_NEXT_TASK");
+        try {
+            lock.lock();
+            log.info("===========CID扫描执行下一个任务任务执行开始===========");
+            long l = System.currentTimeMillis();
+            Collection<String> keys = redisCache.keys(Constants.CID_NEXT_TASK_ID + "*");
+            keys.parallelStream().forEach(key -> {
+                String[] keyArr = key.split(":");
+                String taskId = keyArr[keyArr.length - 2];
+                String callerId = keyArr[keyArr.length - 1];
+                Long runTime = Long.valueOf(redisCache.getCacheObject(key));
+                log.info("任务执行时间:{},当前时间:{}", runTime, l);
+                //到了该执行时间
+                if (runTime.compareTo(l) <= 0) {
+                    log.info("开始执行任务:{},callerId:{}", taskId, callerId);
+                    //得到待执行任务
+                    CompletableFuture.supplyAsync(() -> this.cellRunOne(Long.valueOf(taskId), Long.valueOf(callerId)), cidExcutor).thenApply(res -> {
+                        if (StringUtils.isBlank(res)) {
+                            redisCache.deleteObject(key);
+                            redisCache.deleteObject(Constants.TASK_ID + taskId);
+                        }
+                        return null;
+                    }).exceptionally(throwable -> {
+                        log.error("单项任务执行或删除失败,taskId: {},callerId:{}", taskId, callerId, throwable);
+                        return null;
+                    });
+                } else {
+                    // todo 加入新逻辑 没有到执行时间的待执行任务 检查上一个任务的执行状态
+                    // 如果是已经完成的状态 修改待执行时间为现在 下次进入任务会直接执行对应的下个任务
+
+                }
+            });
+
+        } catch (Exception ex) {
+            log.error("CID任务自动调用调用下一个任务失败", ex);
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+
+    @Autowired
+    CompanyVoiceRoboticCallLogServiceImpl companyVoiceRoboticCallLogService;
+
+    @Autowired
+    CompanyVoiceRoboticCallLogAddwxServiceImpl companyVoiceRoboticCallLogAddwxService;
+
+    /**
+     * 记录任务执行日志 addWx
+     *
+     * @param logAddwx
+     */
+    public void asyncSaveCompanyVoiceRoboticCallLog(CompanyVoiceRoboticCallLogAddwx logAddwx) {
+        try {
+            companyVoiceRoboticCallLogAddwxService.asyncInsertCompanyVoiceRoboticCallLog(logAddwx);
+        } catch (Exception ex) {
+            log.error("记录任务执行日志失败:失败数据:{}", logAddwx, ex);
+        }
+    }
+
+    /**
+     * 工作流加微超时检测
+     * 扫描Redis中的加微超时检测Key,如果超时则触发工作流继续执行
+     */
+    public void checkWorkflowAddWxTimeout() {
+        RLock lock = redissonClient.getLock("WORKFLOW_ADD_WX_TIMEOUT_CHECK");
+        try {
+            lock.lock();
+            log.info("===========工作流加微超时检测开始===========");
+            long currentTime = System.currentTimeMillis();
+
+            // 扫描所有加微超时检测Key
+            Collection<String> keys = redisCache.keys(Constants.WORKFLOW_ADD_WX_TIMEOUT + "*");
+            if (keys == null || keys.isEmpty()) {
+                log.info("没有待检测的加微超时Key");
+                return;
+            }
+
+            log.info("找到 {} 个待检测的加微超时Key", keys.size());
+
+            keys.parallelStream().forEach(key -> {
+                try {
+                    // 解析Key: workflow:addwx:timeout:{workflowInstanceId}:{wxClientId}
+                    String[] keyParts = key.split(":");
+                    if (keyParts.length < 5) {
+                        log.warn("无效的加微超时Key格式: {}", key);
+                        return;
+                    }
+                    String workflowInstanceId = keyParts[keyParts.length - 2];
+                    Long wxClientId = Long.parseLong(keyParts[keyParts.length - 1]);
+
+                    // 获取超时时间戳
+                    String timeoutStr = redisCache.getCacheObject(key);
+                    if (StringUtils.isBlank(timeoutStr)) {
+                        log.warn("加微超时Key值为空: {}", key);
+                        redisCache.deleteObject(key);
+                        return;
+                    }
+                    long timeoutTimestamp = Long.parseLong(timeoutStr);
+
+                    // 检查是否超时
+                    if (currentTime < timeoutTimestamp) {
+                        // 还没到超时时间
+                        return;
+                    }
+
+                    log.info("加微超时,准备触发工作流继续执行 - workflowInstanceId: {}, wxClientId: {}",
+                            workflowInstanceId, wxClientId);
+
+                    // 互斥检查:如果已经被执行过(回调成功路径),则不再执行
+                    if (!AiAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+                        log.info("工作流已被其他路径执行,跳过超时处理 - workflowInstanceId: {}, wxClientId: {}",
+                                workflowInstanceId, wxClientId);
+                        // 清除超时Key
+                        redisCache.deleteObject(key);
+                        return;
+                    }
+
+                    // 清除超时Key
+                    redisCache.deleteObject(key);
+
+                    // 查找等待中的加微工作流实例
+                    CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+                            wxClientId,
+                            ExecutionStatusEnum.WAITING.getValue(),
+                            NodeTypeEnum.AI_ADD_WX_TASK.getValue());
+
+                    if (waitingExec == null) {
+                        log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
+                        return;
+                    }
+
+                    String currentNodeKey = waitingExec.getCurrentNodeKey();
+
+                    // 触发工作流继续执行(超时路径)
+                    Map<String, Object> inputData = new HashMap<>();
+                    inputData.put("addWxSuccess", false);  // 超时意味着加微未成功
+                    inputData.put("wxClientId", wxClientId);
+                    inputData.put("triggerType", "timeout"); // 超时触发
+
+                    companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
+
+                    log.info("加微超时触发工作流继续执行完成 - workflowInstanceId: {}, wxClientId: {}",
+                            workflowInstanceId, wxClientId);
+
+                } catch (Exception ex) {
+                    log.error("处理加微超时检测异常 - key: {}", key, ex);
+                }
+            });
+
+            log.info("===========工作流加微超时检测结束===========");
+
+        } catch (Exception ex) {
+            log.error("工作流加微超时检测任务异常", ex);
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+    /**
+     * 扫描工作流延时任务
+     */
+    public void cidWorkflowAddWxRun() {
+        log.info("===========工作流延时任务开始扫描===========");
+        String delayAddWxKeyPrefix = AiAddWxTaskNode.getDelayAddWxKeyPrefix(cidGroupNo,null) + "*";
+        Set<String> keys = redisKeyScanner.scanMatchKey(delayAddWxKeyPrefix);
+        log.info("共扫描到 {} 个待处理键", keys.size());
+        keys.parallelStream().forEach(key -> {
+            try {
+                //doExec
+                CompletableFuture.runAsync(()->{
+                    try {
+                        ExecutionContext context = redisCache2.getCacheObject(key);
+                        context.setVariable("callRedisKey",key);
+                        context.setVariable("callSource","addWxTimer");
+                        companyWorkflowEngine.timeDoExecute(context.getWorkflowInstanceId(),context.getCurrentNodeKey(),context.getVariables());
+                    } catch (Exception e) {
+                        log.error("处理工作流延时任务异常 - key: {}", key, e);
+                    }
+                }, cidExcutor).thenRun(()->{
+                    redisCache2.deleteObject(key);
+                });
+
+            } catch (Exception ex) {
+                log.error("处理工作流延时任务异常 - key: {}", key, ex);
+            }
+        });
+        log.info("===========工作流延时任务扫描结束===========");
+    }
+
+    /**
+     * 企微加微信任务
+     *
+     * @param accountIdList 企微成员id
+     */
+
+    /**
+     * 企微加微方式:主动申请
+     */
+    private static final int QW_ADD_WX_TYPE_APPLY = 1;
+
+    /**
+     * 企微加微方式:短信获客链接
+     */
+    private static final int QW_ADD_WX_TYPE_SMS_LINK = 2;
+
+    /**
+     * 默认加微方式 ID
+     */
+    private static final int DEFAULT_QW_WX_ADD_WAY_ID = 1;
+
+    public void qwAddWx(List<Long> accountIdList) {
+        log.info("==========执行申请企微加好友任务开始==========");
+        try {
+            // 获取需要添加微信的列表
+            List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getQwAddWxList4Workflow(accountIdList);
+            log.info("申请企微加好友任务需要添加微信的数量:{}", list.size());
+            if (list.isEmpty()) {
+                return;
+            }
+
+            // 构建客户映射
+            Map<Long, CompanyWxClient4WorkFlowVO> clientMap = PubFun.listToMapByGroupObject(
+                    list, CompanyWxClient4WorkFlowVO::getAccountId);
+
+            // 获取有效的企微用户列表
+            List<QwUser> addAccountList = qwUserMapper.selectBatchIds(clientMap.keySet()).stream()
+                    .filter(this::isValidQwUser)
+                    .collect(Collectors.toList());
+            log.info("企微申请加好友任务需要企微的账号数量:{}", addAccountList.size());
+            if (addAccountList.isEmpty()) {
+                return;
+            }
+
+            // 处理每个企微用户的加微请求
+            List<CompanyWxClient> updateList = processQwAddWxRequests(addAccountList, clientMap);
+
+            // 批量更新并触发后续流程
+            if (!updateList.isEmpty()) {
+                companyWxClientService.updateBatchById(updateList);
+                triggerNextWorkflowSteps(updateList, clientMap);
+            }
+
+        } catch (Exception e) {
+            log.error("企微申请加好友任务执行异常", e);
+        }
+        log.info("==========执行企微申请加好友任务结束==========");
+    }
+
+    /**
+     * 处理所有企微用户的加微请求
+     */
+    private List<CompanyWxClient> processQwAddWxRequests(
+            List<QwUser> addAccountList,
+            Map<Long, CompanyWxClient4WorkFlowVO> clientMap) {
+
+        List<CompanyWxClient> updateList = new ArrayList<>();
+
+        for (QwUser qwUser : addAccountList) {
+            CompanyWxClient4WorkFlowVO client = clientMap.get(qwUser.getId());
+            if (client == null) {
+                log.error("企微申请加好友任务当前账号暂无需要添加微信:{}-{}",
+                        qwUser.getId(), qwUser.getQwUserName());
+                continue;
+            }
+
+            try {
+                // 解析配置参数
+                NodeConfig config = parseNodeConfig(client.getNodeConfig());
+
+                // 根据加微方式处理
+                CompanyWxClient result = handleQwAddWxByType(qwUser, client, config);
+                if (result != null) {
+                    updateList.add(result);
+                }
+
+            } catch (Exception e) {
+                log.error("处理企微加微请求失败 - accountId: {}, qwUserId: {}",
+                        client.getAccountId(), qwUser.getId(), e);
+            }
+        }
+
+        return updateList;
+    }
+
+    /**
+     * 解析节点配置
+     */
+    private NodeConfig parseNodeConfig(String nodeConfigStr) {
+        NodeConfig config = new NodeConfig();
+        config.qwWxAddWayId = DEFAULT_QW_WX_ADD_WAY_ID;
+        config.smsTempId = DEFAULT_QW_WX_ADD_WAY_ID;
+
+        if (StringUtils.isBlank(nodeConfigStr)) {
+            return config;
+        }
+
+        try {
+            JsonObject configJson = JsonParser.parseString(nodeConfigStr).getAsJsonObject();
+            if (configJson.has("qwWxAddWayId") && !configJson.get("qwWxAddWayId").isJsonNull()) {
+                config.qwWxAddWayId = configJson.get("qwWxAddWayId").getAsInt();
+            }
+            if (configJson.has("smsTempId") && !configJson.get("smsTempId").isJsonNull()) {
+                config.smsTempId = configJson.get("smsTempId").getAsInt();
+            }
+        } catch (Exception e) {
+            log.error("解析配置出错,使用默认值:{}", e.getMessage());
+        }
+
+        return config;
+    }
+
+    /**
+     * 根据加微方式类型处理
+     */
+    private CompanyWxClient handleQwAddWxByType(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            NodeConfig config) {
+
+        switch (config.qwWxAddWayId) {
+            case QW_ADD_WX_TYPE_APPLY:
+                return handleApplyAddWx(qwUser, client, config);
+            case QW_ADD_WX_TYPE_SMS_LINK:
+                return handleSmsLinkAddWx(qwUser, client, config);
+            default:
+                log.warn("未知的加微方式:{}", config.qwWxAddWayId);
+                return null;
+        }
+    }
+
+    /**
+     * 处理主动申请加微
+     */
+    private CompanyWxClient handleApplyAddWx(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            NodeConfig config) {
+
+        CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+
+        // 发起加微申请
+        WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(
+                crmCustomer.getMobile(),
+                qwUser.getUid(),
+                qwUser.getServerId(),
+                qwUser.getVid(),
+                qwUser.getQwUserName()
+        );
+
+        // 准备日志参数
+        JSONObject runParam = buildAddWxRunParam(qwUser, client, crmCustomer.getMobile());
+        CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                runParam.toJSONString(),
+                client.getId(),
+                client.getRoboticId(),
+                qwUser.getId(),
+                qwUser.getCompanyId(),
+                config.qwWxAddWayId
+        );
+
+        // 处理结果
+        if (resp != null && resp.getErrcode() == 0) {
+            // 加微成功
+            client.setIsAdd(2);
+            client.setAddTime(LocalDateTime.now());
+
+            CompanyWxClient addItem = new CompanyWxClient();
+            BeanUtils.copyProperties(client, addItem);
+
+            addLog.setStatus(2);
+            addLog.setResult(JSON.toJSONString(resp));
+            addLog.setIsWeCom(2);
+            log.info("ROBOTIC-ID:{},企微申请加好友任务申请成功", client.getRoboticId());
+
+            asyncSaveCompanyVoiceRoboticCallLog(addLog);
+            return addItem;
+        } else {
+            // 加微失败
+            client.setIsAdd(3);
+            client.setAddTime(LocalDateTime.now());
+
+
+            CompanyWxClient addItem = new CompanyWxClient();
+            BeanUtils.copyProperties(client, addItem);
+
+
+            addLog.setStatus(3);
+            addLog.setResult(JSON.toJSONString(runParam));
+            log.error("ROBOTIC-ID:{},企微申请加好友任务加微失败:{}",
+                    client.getRoboticId(), runParam);
+
+            asyncSaveCompanyVoiceRoboticCallLog(addLog);
+            return addItem;
+        }
+    }
+
+    /**
+     * 处理短信获客链接加微
+     */
+    private CompanyWxClient handleSmsLinkAddWx(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            NodeConfig config) {
+
+        // 查询短信模板
+        CompanySmsTemp temp = smsTempService.selectCompanySmsTempById((long) config.smsTempId);
+        if (temp == null || !temp.getStatus().equals(1) || !temp.getIsAudit().equals(1)) {
+            log.error("短信模板无效或未审核:{}", temp);
+            throw new RuntimeException("短信模板无效或未审核");
+        }
+
+        // 查询公司短信信息
+        CompanySms sms = companySmsService.selectCompanySmsByCompanyId(qwUser.getCompanyId());
+        if (sms == null) {
+            log.error("公司短信信息不存在:companyId: {}", qwUser.getCompanyId());
+            throw new RuntimeException("公司短信信息不存在");
+        }
+
+        if (sms.getRemainSmsCount() <= 0) {
+            log.error("剩余短信数量不足:companyId: {}", qwUser.getCompanyId());
+            throw new RuntimeException("剩余短信数量不足,请充值");
+        }
+
+
+        String linkUrl = getLinkUrl(qwUser);
+
+        if (StringUtil.strIsNullOrEmpty(linkUrl)) {
+            return handleLinkGenerationFailure(client, qwUser, config);
+        }
+
+        return handleSmsSendAndAddWx(qwUser, client, temp, linkUrl, config);
+
+
+    }
+    /**
+     * 处理链接生成失败
+     */
+    private CompanyWxClient handleLinkGenerationFailure(
+            CompanyWxClient4WorkFlowVO client,
+            QwUser qwUser,
+            NodeConfig config) {
+
+        client.setIsAdd(3);
+        client.setAddTime(LocalDateTime.now());
+
+        CompanyWxClient addItem = new CompanyWxClient();
+        BeanUtils.copyProperties(client, addItem);
+
+        CompanyVoiceRoboticCallLogAddwx addLogAddWx = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                "获客链接生成失败",
+                client.getId(),
+                client.getRoboticId(),
+                qwUser.getId(),
+                qwUser.getCompanyId(),
+                config.qwWxAddWayId
+        );
+
+        addLogAddWx.setStatus(3);
+        addLogAddWx.setResult(JSON.toJSONString(R.ok()));
+        addLogAddWx.setIsWeCom(2);
+
+        asyncSaveCompanyVoiceRoboticCallLog(addLogAddWx);
+        return addItem;
+    }
+
+    /**
+     * 处理短信发送和加微
+     */
+    private CompanyWxClient handleSmsSendAndAddWx(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            CompanySmsTemp temp,
+            String linkUrl,
+            NodeConfig config) {
+
+        SmsSendBatchParam smsSendBatchParam = buildSmsSendParam(qwUser, client, temp, linkUrl);
+        JSONObject runParamSms = (JSONObject) JSON.toJSON(smsSendBatchParam);
+        runParamSms.put("temp", temp);
+
+        //发送短信并记录日志
+        sendSmsWithLog(smsSendBatchParam, runParamSms, client, qwUser, temp);
+
+        //保存加微日志
+        saveAddWxLog(runParamSms, client, qwUser, config);
+
+        client.setIsAdd(2);
+        client.setAddTime(LocalDateTime.now());
+
+
+        CompanyWxClient addItem = new CompanyWxClient();
+        BeanUtils.copyProperties(client, addItem);
+
+        return addItem;
+    }
+
+    /**
+     * 发送短信并记录日志
+     */
+    private void sendSmsWithLog(
+            SmsSendBatchParam smsSendBatchParam,
+            JSONObject runParamSms,
+            CompanyWxClient4WorkFlowVO client,
+            QwUser qwUser,
+            CompanySmsTemp temp) {
+
+        CompanyVoiceRoboticCallLogSendmsg addLogSms = CompanyVoiceRoboticCallLogSendmsg.initCallLog(
+                runParamSms.toJSONString(),
+                client.getCalleeId(),
+                client.getRoboticId(),
+                qwUser.getCompanyId(),
+                qwUser.getCompanyUserId(),
+                temp.getTempId()
+        );
+
+        try {
+            String callbackUuid = UUID.randomUUID().toString();
+            int smsContentLen = getSmsContentLen(smsSendBatchParam);
+
+            addLogSms.setContentLen(smsContentLen);
+            companyVoiceRoboticServiceImpl.sendMsgBatch(temp, smsSendBatchParam);
+
+            addLogSms.setStatus(2);
+            addLogSms.setCallbackUuid(callbackUuid);
+        } catch (Exception ex) {
+            addLogSms.setStatus(3);
+            addLogSms.setResult(ex.getMessage());
+            log.error("sendMsgBatch 异常:", ex);
+        } finally {
+            companyVoiceRoboticCallLogSendmsgService.asyncInsertCompanyVoiceRoboticCallLog(addLogSms);
+        }
+    }
+
+    /**
+     * 保存加微日志
+     */
+    private void saveAddWxLog(
+            JSONObject runParamSms,
+            CompanyWxClient4WorkFlowVO client,
+            QwUser qwUser,
+            NodeConfig config) {
+
+        CompanyVoiceRoboticCallLogAddwx addLogAddWx = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+                runParamSms.toJSONString(),
+                client.getId(),
+                client.getRoboticId(),
+                qwUser.getId(),
+                qwUser.getCompanyId(),
+                config.qwWxAddWayId
+        );
+
+        addLogAddWx.setStatus(1);
+        addLogAddWx.setResult(JSON.toJSONString(R.ok()));
+        addLogAddWx.setIsWeCom(2);
+
+        asyncSaveCompanyVoiceRoboticCallLog(addLogAddWx);
+    }
+
+
+    /**
+    * 获取获客链接
+    */
+    private String getLinkUrl(QwUser qwUser){
+
+        String link = redisCache2.getCacheObject("customerLink:"+qwUser.getId());
+        if (link!=null && !StringUtil.strIsNullOrEmpty(link)){
+            return link;
+        }
+
+        //获取获客链接
+        QwLinkCreateParam createParam=new QwLinkCreateParam();
+        createParam.setLink_name(qwUser.getQwUserName()+"的获客链接");
+
+        QwLinkCreateParam.Range range=new QwLinkCreateParam.Range();
+        range.setUser_list(Collections.singletonList(qwUser.getQwUserId()));
+        createParam.setRange(range);
+
+        QwLinkCreateResult result = qwApiService.linkCreate(createParam, qwUser.getCorpId());
+
+        if (result.getErrcode()==0){
+
+            redisCache2.setCacheObject("customerLink:"+qwUser.getId(),result.getUrl());
+
+            return  result.getUrl();
+        }else {
+            return null;
+        }
+
+
+    }
+
+    /**
+     * 构建加微请求参数
+     */
+    private JSONObject buildAddWxRunParam(QwUser qwUser, CompanyWxClient4WorkFlowVO client, String mobile) {
+        JSONObject runParam = new JSONObject();
+        runParam.put("qwId", qwUser.getId());
+        runParam.put("mobile", mobile);
+        runParam.put("qwUid", qwUser.getUid());
+        runParam.put("clientId", client.getId());
+        return runParam;
+    }
+
+    /**
+     * 构建短信发送参数
+     */
+    private SmsSendBatchParam buildSmsSendParam(
+            QwUser qwUser,
+            CompanyWxClient4WorkFlowVO client,
+            CompanySmsTemp temp,
+            String cardUrl) {
+
+        SmsSendBatchParam param = new SmsSendBatchParam();
+        param.setCompanyId(qwUser.getCompanyId());
+        param.setCompanyUserId(qwUser.getCompanyUserId());
+        param.setSmsType(temp.getTempType());
+        param.setTempCode(temp.getTempCode());
+        param.setContent(temp.getContent());
+        param.setSenderName(qwUser.getQwUserName());
+        param.setCustomerIds(new Long[]{client.getCustomerId()});
+        if (!StringUtil.strIsNullOrEmpty(client.getTraceId())){
+            param.setCardUrl(cardUrl+"?customer_channel="+client.getTraceId());
+        }else {
+            param.setCardUrl(cardUrl);
+        }
+
+        return param;
+    }
+
+
+    /**
+     * 触发后续工作流步骤
+     */
+    private void triggerNextWorkflowSteps(
+            List<CompanyWxClient> updateList,
+            Map<Long, CompanyWxClient4WorkFlowVO> clientMap) {
+
+        for (CompanyWxClient client : updateList) {
+            CompanyWxClient4WorkFlowVO vo = clientMap.get(client.getAccountId());
+            IWorkflowNode node = workflowNodeFactory.createNode(
+                    vo.getCurrentNodeKey(),
+                    NodeTypeEnum.fromValue(vo.getCurrentNodeType()),
+                    vo.getCurrentNodeName(),
+                    null
+            );
+
+            if (node instanceof AiQwAddWxTaskNode) {
+                CompletableFuture.runAsync(() -> {
+                    AiQwAddWxTaskNode qwAddWxNode = (AiQwAddWxTaskNode) node;
+                    qwAddWxNode.doneQwAddWx(vo.getWorkflowInstanceId());
+                }, cidExcutor);
+            }
+        }
+    }
+
+    /**
+     * 节点配置内部类
+     */
+    private static class NodeConfig {
+        private int qwWxAddWayId;
+        private int smsTempId;
+    }
+
+// ... existing code ...
+
+//    public void qwAddWx(List<Long> accountIdList) {
+//        log.info("==========执行申请企微加好友任务开始==========");
+//        try {
+//            // 需要添加微信的列表
+//            List<CompanyWxClient4WorkFlowVO> list = companyWxClientService.getQwAddWxList4Workflow(accountIdList);
+//            log.info("申请企微加好友任务需要添加微信的数量:{}", list.size());
+//            if (list.isEmpty()) return;
+//            List<CompanyWxClient> addList = new ArrayList<>();
+//            Map<Long, CompanyWxClient4WorkFlowVO> clientMap = PubFun.listToMapByGroupObject(list, CompanyWxClient4WorkFlowVO::getAccountId);
+//            // 获取实际企微用户信息
+//            List<QwUser> addAccountList = qwUserMapper.selectBatchIds(clientMap.keySet()).stream()
+//                    .filter(this::isValidQwUser)
+//                    .collect(Collectors.toList());
+//            log.info("企微申请加好友任务需要企微的账号数量:{}", addAccountList.size());
+//            addAccountList.forEach(qwUser -> {
+//                CompanyWxClient4WorkFlowVO client = clientMap.get(qwUser.getId());
+//                if (client != null) {
+//                    CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
+//
+//                    int qwWxAddWayId = 1;  // 先固定为1
+//                    int smsTempId= 1; // 先固定为1
+//                    try {
+//                        String nodeConfig = client.getNodeConfig();
+//                        if (nodeConfig != null && !nodeConfig.trim().isEmpty()) {
+//                            JsonObject configJson = JsonParser.parseString(nodeConfig).getAsJsonObject();
+//                            if (configJson.has("qwWxAddWayId") && !configJson.get("qwWxAddWayId").isJsonNull()) {
+//                                qwWxAddWayId = configJson.get("qwWxAddWayId").getAsInt();
+//                            }
+//                            smsTempId = configJson.get("smsTempId").getAsInt();
+//                        }
+//                    } catch (Exception e) {
+//                        // 报错了也不处理,继续使用1
+//                        log.error("解析配置出错,但保持加微使用默认值1: " + e.getMessage());
+//                    }
+//
+//                    switch (qwWxAddWayId){
+//                        case 1:
+//                            // 开始主动申请加微
+//                            WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(crmCustomer.getMobile(), qwUser.getUid(),qwUser.getServerId(),qwUser.getVid(),qwUser.getQwUserName());
+//                            JSONObject runParam = new JSONObject();
+//                            runParam.put("qwId", qwUser.getId());
+//                            runParam.put("mobile", crmCustomer.getMobile());
+//                            runParam.put("qwUid", qwUser.getUid());
+//                            runParam.put("clientId", client.getId());
+//
+//                            CompanyVoiceRoboticCallLogAddwx addLog = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+//                                    runParam.toJSONString(), client.getId(), client.getRoboticId(), qwUser.getId(), qwUser.getCompanyId(),qwWxAddWayId);
+//                            if (resp != null && resp.getErrcode() == 0) {
+//                                // 加微消息已发送成功
+//                                client.setIsAdd(2);
+//                                client.setAddTime(LocalDateTime.now());
+//                                CompanyWxClient addItem = new CompanyWxClient();
+//                                BeanUtils.copyProperties(client, addItem);
+//                                addList.add(addItem);
+//                                addLog.setStatus(1);
+//                                addLog.setResult(JSON.toJSONString(resp));
+//                                addLog.setIsWeCom(2);
+//                                log.info("ROBOTIC-ID:{},企微申请加好友任务申请成功", client.getRoboticId());
+//                            } else {
+//                                log.error("ROBOTIC-ID:{},企微申请加好友任务加微失败:{}", client.getRoboticId(), runParam);
+//                                addLog.setStatus(3);
+//                                addLog.setResult(JSON.toJSONString(runParam));
+//
+//                                client.setIsAdd(3);
+//                                client.setAddTime(LocalDateTime.now());
+//                                CompanyWxClient addItem = new CompanyWxClient();
+//                                BeanUtils.copyProperties(client, addItem);
+//                                addList.add(addItem);
+//                            }
+//                            asyncSaveCompanyVoiceRoboticCallLog(addLog);
+//                            break;
+//                        case 2:
+//                            //短信-获客链接
+//
+//                            CompanySmsTemp temp = smsTempService.selectCompanySmsTempById((long) smsTempId);
+//
+//                            if (temp != null && temp.getStatus().equals(1) && temp.getIsAudit().equals(1)) {
+//
+//                                CompanySms sms=companySmsService.selectCompanySmsByCompanyId(qwUser.getCompanyId());
+//                                if(sms!=null){
+//                                    if(sms.getRemainSmsCount()>0){
+//                                        SmsSendBatchParam smsSendBatchParam = new SmsSendBatchParam();
+//                                        smsSendBatchParam.setCompanyId(qwUser.getCompanyId());
+//                                        smsSendBatchParam.setCompanyUserId(qwUser.getCompanyUserId());
+//                                        smsSendBatchParam.setSmsType(temp.getTempType());
+//                                        smsSendBatchParam.setTempCode(temp.getTempCode());
+//                                        smsSendBatchParam.setContent(temp.getContent());
+//                                        smsSendBatchParam.setSenderName(client.getWxNickName());
+//                                        smsSendBatchParam.setCustomerIds(new Long[]{client.getCustomerId()});
+//                                        //记录工作流级短信日志
+//                                        JSONObject runParamSms = (JSONObject) JSON.toJSON(smsSendBatchParam);
+//                                        runParamSms.put("temp", temp);
+//                                        CompanyVoiceRoboticCallLogSendmsg addLogSms = CompanyVoiceRoboticCallLogSendmsg.initCallLog(
+//                                                runParamSms.toJSONString(),
+//                                                null,
+//                                                null,
+//                                                qwUser.getCompanyId(),
+//                                                qwUser.getCompanyUserId(),
+//                                                temp.getTempId()
+//                                        );
+//
+//                                        addLogSms.setStatus(1);
+//                                        try{
+//                                            String callbackUuid = UUID.randomUUID().toString();
+//                                            int smsContentLen = getSmsContentLen(smsSendBatchParam);
+//                                            addLogSms.setContentLen(smsContentLen);
+//                                            companyVoiceRoboticServiceImpl.sendMsgBatch(temp,smsSendBatchParam);
+//                                            addLogSms.setStatus(2);
+//                                            addLogSms.setCallbackUuid(callbackUuid);
+//                                        } catch(Exception ex){
+//                                            addLogSms.setStatus(3);
+//                                            addLogSms.setResult(ex.getMessage());
+//                                            log.error("sendMsgOne异常:",ex);
+//                                        } finally {
+//                                            //短信记录表
+//                                            companyVoiceRoboticCallLogSendmsgService.asyncInsertCompanyVoiceRoboticCallLog(addLogSms);
+//                                        }
+//
+//                                        CompanyVoiceRoboticCallLogAddwx addLogAddWx = CompanyVoiceRoboticCallLogAddwx.initCallLog(
+//                                                runParamSms.toJSONString(), client.getId(), client.getRoboticId(), qwUser.getId(), qwUser.getCompanyId(),qwWxAddWayId);
+//
+//                                            // 加微中
+//                                            client.setIsAdd(2);
+//                                            client.setAddTime(LocalDateTime.now());
+//                                            CompanyWxClient addItem = new CompanyWxClient();
+//                                            BeanUtils.copyProperties(client, addItem);
+//                                            addList.add(addItem);
+//
+//                                            addLogAddWx.setStatus(1);
+//                                            addLogAddWx.setResult(JSON.toJSONString(R.ok()));
+//                                            addLogAddWx.setIsWeCom(2);
+//
+//                                        asyncSaveCompanyVoiceRoboticCallLog(addLogAddWx);
+//
+//                                    }
+//                                    else{
+//                                        log.error("qw剩余短信数量不足,请充值:{}",client.getCompanyId());
+//                                        throw new RuntimeException("剩余短信数量不足,请充值");
+//                                    }
+//                                }
+//                                else{
+//                                    log.error("qw请充值:companyId:{}",client.getCompanyId());
+//                                    throw new RuntimeException("请充值");
+//                                }
+//
+//                            } else {
+//                                log.error("模板未审核:smsTemp:{}", temp);
+//                                throw new RuntimeException("模板未审核");
+//                            }
+//
+//
+//                            break;
+//                        default:
+//                            break;
+//                    }
+//
+//
+//                } else {
+//                    log.error("企微申请加好友任务当前账号暂无需要添加微信:{}-{}", qwUser.getId(), qwUser.getQwUserName());
+//                }
+//            });
+//            if (!addList.isEmpty()) {
+//                companyWxClientService.updateBatchById(addList);
+//                for (CompanyWxClient client : addList) {
+//                    CompanyWxClient4WorkFlowVO vo = clientMap.get(client.getAccountId());
+//                    IWorkflowNode node = workflowNodeFactory.createNode(vo.getCurrentNodeKey(),
+//                            NodeTypeEnum.fromValue(vo.getCurrentNodeType()),
+//                            vo.getCurrentNodeName(), null);
+//                    if (node instanceof AiQwAddWxTaskNode) {
+//                        CompletableFuture.runAsync(() -> {
+//                            AiQwAddWxTaskNode qwAddWxNode = (AiQwAddWxTaskNode) node;
+//                            qwAddWxNode.doneQwAddWx(vo.getWorkflowInstanceId());
+//                        }, cidExcutor);
+//                    }
+//                }
+//            }
+//        } catch (Exception e) {
+//            log.error("企微申请加好友任务执行异常", e);
+//        }
+//        log.info("==========执行企微申请加好友任务结束==========");
+//    }
+
+
+
+    public int getSmsContentLen(SmsSendBatchParam param){
+        CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+
+        CrmCustomer crmCustomer=crmCustomerService.selectCrmCustomerById(param.getCustomerIds()[0].longValue());
+        String  content=param.getContent();
+        if(StringUtils.isNotEmpty(crmCustomer.getCustomerName())){
+            content=content.replace("${sms.csName}",crmCustomer.getCustomerName());
+        }
+        if(companyUser!=null&& StringUtils.isNotEmpty(companyUser.getPhonenumber())){
+            content=content.replace("${sms.phoneNumber}",companyUser.getPhonenumber());
+        }
+        if(StringUtils.isNotEmpty(param.getCardUrl())){
+            content=content.replace("${sms.cardUrl}",param.getCardUrl());
+        }
+        if(StringUtils.isNotEmpty(param.getSenderName())){
+            content=content.replace("${sms.senderName}",param.getSenderName());
+        }
+
+        return content.length();
+    }
+
+    /**
+     * 企微加微结果处理
+     */
+    public void qwAddWxResult(List<Long> accountIdList) {
+        log.info("==========执行企微申请加微结果查询任务开始==========");
+        try {
+            //is_add = 2,状态为加微中且是企微类型
+            List<CompanyWxClient> clients = companyWxClientService.getQwAddWxList(accountIdList, 2);
+           log.info("企微申请加微结果查询任务需要查询的数量:{}", clients.size());
+
+            if (clients.isEmpty()) return;
+            // 处理每个客户的加微结果
+            List<CompanyWxClient> upClientList = new ArrayList<>();
+            clients.parallelStream().forEach(client -> {
+                try {
+                    processSingleClientResult(client, upClientList);
+                } catch (Exception e) {
+                    log.error("企微申请加微结果查询任务处理客户{}加微结果异常", client.getId(), e);
+                }
+            });
+
+            // 批量更新和后续处理
+            if (!upClientList.isEmpty()) {
+                batchUpdateClients(upClientList);
+            }
+
+        } catch (Exception e) {
+            log.error("企微申请加微结果查询任务处理异常", e);
+        }
+        log.info("==========执行企微申请加微结果查询任务结束==========");
+    }
+
+    /**
+     * 扫描企微加微工作流延时任务
+     */
+    public void cidWorkflowQwAddWxRun() {
+        log.info("===========企微加微工作流延时任务开始扫描===========");
+        String delayAddWxKeyPrefix = AiQwAddWxTaskNode.getDelayAddWxKeyPrefix(cidGroupNo,null) + "*";
+        Set<String> keys = redisKeyScanner.scanMatchKey(delayAddWxKeyPrefix);
+        log.info("企微加微共扫描到 {} 个待处理键", keys.size());
+        keys.parallelStream().forEach(key -> {
+            try {
+                //doExec
+                CompletableFuture.runAsync(()->{
+                    try {
+                        ExecutionContext context = redisCache2.getCacheObject(key);
+                        context.setVariable("callRedisKey",key);
+                        context.setVariable("callSource","qwAddWxTimer");
+                        companyWorkflowEngine.timeDoExecute(context.getWorkflowInstanceId(),context.getCurrentNodeKey(),context.getVariables());
+                    } catch (Exception e) {
+                        log.error("处理工作流延时任务异常 - key: {}", key, e);
+                    }
+                }, cidExcutor).thenRun(()->{
+                    redisCache2.deleteObject(key);
+                });
+
+            } catch (Exception ex) {
+                log.error("处理工作流延时任务异常 - key: {}", key, ex);
+            }
+        });
+        log.info("===========工作流延时任务扫描结束===========");
+    }
+
+
+    /**
+     * 验证企微用户有效性
+     */
+    private boolean isValidQwUser(QwUser qwUser) {
+        if (StringUtils.isBlank(qwUser.getUid()) || qwUser.getServerId() == null) {
+            log.info("企微账号{}的uid或serverId为空,跳过执行", qwUser.getQwUserName());
+            return false;
+        }
+        return true;
+    }
+
+
+    /**
+     * 企微加个微调用ipad端
+     * @param mobile  手机号
+     * @param qwUid   企微uid
+     * @param serverId   服务器id
+     * @return String 结果
+     */
+    private WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId,String vid,String qwUserName) {
+        if (StringUtils.isBlank(mobile) || StringUtils.isBlank(qwUid) || serverId == null) {
+            log.warn("企微申请加好友任务参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
+            return null;
+        }
+
+        try {
+            WxAddSearchDTO wxAddSearchDTO = new WxAddSearchDTO();
+            wxAddSearchDTO.setUuid(qwUid);
+            wxAddSearchDTO.setVid(Long.valueOf(vid));
+            wxAddSearchDTO.setPhone(mobile);
+
+            WxSearchContactDTO contactDTO=new WxSearchContactDTO();
+            contactDTO.setUuid(qwUid);
+            contactDTO.setPhoneNumber(mobile);
+
+
+            WxWorkResponseDTO<WxSearchContactResp> respWxWorkResponseDTO = wxWorkService.searchContact(contactDTO, serverId);
+            WxSearchContactResp.UserList user = respWxWorkResponseDTO.getData().getUserList().stream()
+                    .filter(u -> u.getState().equals("2"))
+                    .findFirst()
+                    .orElse(null); // 或者 .orElseThrow(() -> new RuntimeException("未找到指定用户"))
+
+            wxAddSearchDTO.setOptionid(user.getOpenid());
+            wxAddSearchDTO.setTicket(user.getTicket());
+            wxAddSearchDTO.setContent("你好,我是你的专属助手:"+qwUserName+",有什么问题都可以问我哦~");
+
+            WxWorkResponseDTO<String> response = wxWorkService.addSearch(wxAddSearchDTO, serverId);
+            log.debug("企微加微接口调用结果: errcode={}, errmsg={}",
+                    response != null ? response.getErrcode() : "null",
+                    response != null ? response.getErrmsg() : "null");
+
+            return response;
+
+            // 测试代码
+//            WxWorkResponseDTO<String> response = new WxWorkResponseDTO<>();
+//            response.setErrcode(0);
+//            return response;
+        } catch (Exception e) {
+            log.error("企微申请加好友任务请求接口异常: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 处理单个客户加微结果
+     */
+    private void processSingleClientResult(CompanyWxClient client, List<CompanyWxClient> upClientList) {
+        if (StringUtils.isBlank(client.getPhone())) {
+            handleFailedAddWx(client, upClientList, "无电话号码",0);
+            return;
+        }
+
+        // 查询外部联系人表是否有数据
+        QwExternalContact qwExternalContact = qwExternalContactMapper.queryQwUserIdIsAddContact(
+                client.getAccountId(), client.getPhone(), 2);
+
+        if (qwExternalContact != null && qwExternalContact.getId() > 0) {
+            handleSuccessfulAddWx(client, upClientList);
+        } else {
+            handleFailedAddWxWithRetry(client, upClientList);
+        }
+
+        //测试代码
+//        handleSuccessfulAddWx(client, upClientList);
+//        handleFailedAddWx(client, upClientList, "无电话号码",0);
+    }
+
+    /**
+     * 处理加微成功的情况
+     */
+    private void handleSuccessfulAddWx(CompanyWxClient client, List<CompanyWxClient> upClientList) {
+        // 更新记录状态
+        companyVoiceRoboticCallLogAddwxService.lambdaUpdate()
+                .eq(CompanyVoiceRoboticCallLogAddwx::getRoboticId, client.getRoboticId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getWxClientId, client.getId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getWxAccountId, client.getAccountId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getIsWeCom, 2)
+                .set(CompanyVoiceRoboticCallLogAddwx::getStatus, 2)
+                .update();
+
+        client.setIsAdd(1);
+        client.setAddTime(LocalDateTime.now());
+        upClientList.add(client);
+        redisCache.deleteObject("qwAddWx_" + client.getId());
+        log.info("ROBOTIC-ID:{},企微申请加微结果查询任务加微成功:{}", client.getRoboticId(), client.getId());
+    }
+
+    /**
+     * 处理加微失败的情况
+     */
+    private void handleFailedAddWx(CompanyWxClient client, List<CompanyWxClient> upClientList, String reason,Integer isAdd) {
+        String taskName = isAdd == 1 ? "企微申请加好友任务" : "加微结果查询任务";
+        log.error("ROBOTIC-ID:{},{}:{},clientId={}", client.getRoboticId(),taskName, reason, client.getId());
+        client.setIsAdd(3);
+        client.setUpdateTime(new Date());
+        upClientList.add(client);
+        // 更新日志记录
+        companyVoiceRoboticCallLogAddwxService.lambdaUpdate()
+                .eq(CompanyVoiceRoboticCallLogAddwx::getRoboticId, client.getRoboticId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getWxClientId, client.getId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getWxAccountId, client.getAccountId())
+                .eq(CompanyVoiceRoboticCallLogAddwx::getIsWeCom, 2)
+                .set(CompanyVoiceRoboticCallLogAddwx::getStatus, 3)
+                .set(CompanyVoiceRoboticCallLogAddwx::getResult, reason)
+                .update();
+    }
+
+    /**
+     * 处理加微失败并重试计数
+     */
+    private void handleFailedAddWxWithRetry(CompanyWxClient client, List<CompanyWxClient> upClientList) {
+        String taskName = 0 == 1 ? "企微申请加好友任务" : "加微结果查询任务";
+        log.error("ROBOTIC-ID:{},{}失败:{}", client.getRoboticId(),taskName, client.getId());
+        String failCountStr = redisCache.getCacheObject("qwAddWx_" + client.getId());
+        int failCount = 1;
+
+        if (StringUtils.isNotBlank(failCountStr)) {
+            if (Integer.parseInt(failCountStr) >= 60 * 24) { // 超过一天
+                handleFailedAddWx(client, upClientList, "超过最大重试次数", 0);
+                redisCache.deleteObject("qwAddWx_" + client.getId());
+            } else {
+                failCount += Integer.parseInt(failCountStr);
+                redisCache.setCacheObject("qwAddWx_" + client.getId(), String.valueOf(failCount-1));
+            }
+        } else {
+            redisCache.setCacheObject("qwAddWx_" + client.getId(), String.valueOf(failCount), 25, TimeUnit.HOURS);
+        }
+    }
+
+    /**
+     * 批量更新客户和相关数据
+     */
+    private void batchUpdateClients(List<CompanyWxClient> upClientList) {
+        companyWxClientService.updateBatchById(upClientList);
+        // 从 upClientList 中筛选出 isAdd=1和3加微失败的数据
+        List<CompanyWxClient> successClients = upClientList.stream()
+                .filter(client -> client.getIsAdd() != null && (client.getIsAdd() == 1 || client.getIsAdd() == 3))
+                .collect(Collectors.toList());
+        if(!successClients.isEmpty()){
+            successClients.forEach(client -> {
+                triggerWorkflowOnAddWxSuccess(client.getId());
+            });
+        }
+    }
+
+
+    /**
+     * 加微结果触发工作流继续执行
+     * @param wxClientId 加微客户ID
+     */
+    private void triggerWorkflowOnAddWxSuccess(Long wxClientId) {
+        try {
+            // 查找等待中的加微工作流实例
+            CompanyAiWorkflowExec waitingExec = companyAiWorkflowExecMapper.selectWaitingAddWxWorkflowByWxClientId(
+                    wxClientId,
+                    ExecutionStatusEnum.WAITING.getValue(),
+                    NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+            if (waitingExec == null) {
+                log.info("未找到等待中的加微工作流实例 - wxClientId: {}", wxClientId);
+                return;
+            }
+            //查询工作流加微执行日志是否未更新状态
+            CompanyAiWorkflowExecLog queryP = new CompanyAiWorkflowExecLog();
+            queryP.setWorkflowInstanceId(waitingExec.getWorkflowInstanceId());
+            queryP.setNodeType(NodeTypeEnum.AI_QW_ADD_WX_TASK.getValue());
+            queryP.setStatus(ExecutionStatusEnum.WAITING.getValue());
+            List<CompanyAiWorkflowExecLog> companyAiWorkflowExecLogs = companyAiWorkflowExecLogMapper.selectCompanyAiWorkflowExecLogList(queryP);
+            companyAiWorkflowExecLogs.forEach(log -> {
+                        log.setStatus(ExecutionStatusEnum.SUCCESS.getValue());
+                        companyAiWorkflowExecLogMapper.updateById(log);
+                    }
+            );
+
+            String workflowInstanceId = waitingExec.getWorkflowInstanceId();
+            String currentNodeKey = waitingExec.getCurrentNodeKey();
+
+            log.info("加微成功回调,尝试触发工作流继续执行 - workflowInstanceId: {}, nodeKey: {}, wxClientId: {}",
+                    workflowInstanceId, currentNodeKey, wxClientId);
+
+            // 互斥检查:如果已经被执行过(超时路径或其他回调),则不再执行
+            if (!AiQwAddWxTaskNode.tryMarkAsExecuted(workflowInstanceId, wxClientId)) {
+                log.info("企微申请加微结果查询任务工作流已被其他路径执行,跳过 - workflowInstanceId: {}, wxClientId: {}",
+                        workflowInstanceId, wxClientId);
+                return;
+            }
+
+            // 清除超时检测Key(回调成功了,不需要超时检测了)
+            AiQwAddWxTaskNode.clearTimeoutKey(workflowInstanceId, wxClientId);
+
+            // 触发工作流继续执行
+            Map<String, Object> inputData = new HashMap<>();
+            inputData.put("addWxSuccess", true);
+            inputData.put("wxClientId", wxClientId);
+            inputData.put("triggerType", "callback"); // 回调触发
+
+            companyWorkflowEngine.resumeFromBlockingNode(workflowInstanceId, currentNodeKey, inputData);
+
+            log.info("企微申请加微结果查询任务加微成功回调触发工作流继续执行完成 - workflowInstanceId: {}, wxClientId: {}",
+                    workflowInstanceId, wxClientId);
+
+        } catch (Exception ex) {
+            log.error("企微申请加微结果查询任务加微成功回调触发工作流异常 - wxClientId: {}", wxClientId, ex);
+        }
+    }
+
+}

+ 87 - 0
fs-wx-task/src/main/java/com/fs/app/task/WxTask.java

@@ -0,0 +1,87 @@
+package com.fs.app.task;
+
+import com.fs.app.service.WxTaskService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 企业微信SOP定时任务管理类
+ * 负责处理各种定时任务,包括SOP规则检查、消息发送、数据清理等
+ *
+ * @author 系统
+ * @version 1.0
+ */
+@Component
+@Slf4j
+public class WxTask {
+
+    @Autowired
+    private WxTaskService taskService;
+
+//    @Scheduled(cron = "0 0/30 * * * ?")
+//    public void addWx() {
+//        taskService.addWx(null);
+//    }
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void addWx4Workflow() {
+        taskService.addWx4Workflow(null);
+    }
+    @Scheduled(cron = "0 0 0 * * ?")
+    public void initAccountNum() {
+        taskService.initAccountNum();
+    }
+
+    @Scheduled(cron = "0 0 * * * ?")
+    public void initAccountMsg() {
+        taskService.initAccountMsg();
+    }
+
+//    @Scheduled(cron = "0 0/10 * * * ?")
+//    public void cellRun() {
+//        taskService.cellRun();
+//    }
+//    @Scheduled(cron = "0 0/1 * * * ?")
+//    public void callNextTask(){
+//        taskService.callNextTask();
+//    }
+
+    /**
+     * 工作流加微超时检测
+     * 每分钟执行一次,检查是否有加微超时的工作流需要继续执行
+     */
+//    @Scheduled(cron = "0 0/1 * * * ?")
+//    public void checkWorkflowAddWxTimeout(){
+//        taskService.checkWorkflowAddWxTimeout();
+//    }
+
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void cidWorkflowAddWxRun(){
+        taskService.cidWorkflowAddWxRun();
+    }
+
+
+    /**
+     * 企微加微
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void qwAddWx() {
+        taskService.qwAddWx(null);
+    }
+    /**
+     * 企微加微结果查询
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void qwAddWxResult() {
+        taskService.qwAddWxResult(null);
+    }
+    /**
+     * 企微加微工作流超时检测
+     * 每分钟执行一次,检查是否有加微超时的工作流需要继续执行
+     */
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void cidWorkflowQwAddWxRun(){
+        taskService.cidWorkflowQwAddWxRun();
+    }
+}

+ 171 - 0
fs-wx-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

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

+ 73 - 0
fs-wx-task/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

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

+ 219 - 0
fs-wx-task/src/main/java/com/fs/framework/aspectj/LogAspect.java

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

+ 117 - 0
fs-wx-task/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-wx-task/src/main/java/com/fs/framework/config/ApplicationConfig.java

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

+ 58 - 0
fs-wx-task/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java

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

+ 85 - 0
fs-wx-task/src/main/java/com/fs/framework/config/CaptchaConfig.java

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

+ 92 - 0
fs-wx-task/src/main/java/com/fs/framework/config/DataSourceConfig.java

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

+ 123 - 0
fs-wx-task/src/main/java/com/fs/framework/config/DruidConfig.java

@@ -0,0 +1,123 @@
+package com.fs.framework.config;//package com.fs.framework.config;
+//
+//import com.alibaba.druid.pool.DruidDataSource;
+//import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
+//import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+//import com.alibaba.druid.util.Utils;
+//import com.fs.framework.datasource.DynamicDataSource;
+//import com.fs.common.enums.DataSourceType;
+//import com.fs.common.utils.spring.SpringUtils;
+//import com.fs.framework.config.properties.DruidProperties;
+//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;
+//
+///**
+// * druid 配置多数据源
+// *
+//
+// */
+//@Configuration
+//public class DruidConfig
+//{
+//    @Bean
+//    @ConfigurationProperties("spring.datasource.mysql.druid.master")
+//    public DataSource masterDataSource(DruidProperties druidProperties)
+//    {
+//        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+//        return druidProperties.dataSource(dataSource);
+//    }
+//
+//    @Bean
+//    @ConfigurationProperties("spring.datasource.mysql.druid.slave")
+//    @ConditionalOnProperty(prefix = "spring.datasource.mysql.druid.slave", name = "enabled", havingValue = "true")
+//    public DataSource slaveDataSource(DruidProperties druidProperties)
+//    {
+//        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+//        return druidProperties.dataSource(dataSource);
+//    }
+//
+//    @Bean(name = "dynamicDataSource")
+//    @Primary
+//    public DynamicDataSource dataSource(DataSource masterDataSource)
+//    {
+//        Map<Object, Object> targetDataSources = new HashMap<>();
+//        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+//        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+//        return new DynamicDataSource(masterDataSource, targetDataSources);
+//    }
+//
+//    /**
+//     * 设置数据源
+//     *
+//     * @param targetDataSources 备选数据源集合
+//     * @param sourceName 数据源名称
+//     * @param beanName bean名称
+//     */
+//    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
+//    {
+//        try
+//        {
+//            DataSource dataSource = SpringUtils.getBean(beanName);
+//            targetDataSources.put(sourceName, dataSource);
+//        }
+//        catch (Exception e)
+//        {
+//        }
+//    }
+//
+//    /**
+//     * 去除监控页面底部的广告
+//     */
+//    @SuppressWarnings({ "rawtypes", "unchecked" })
+//    @Bean
+//    @ConditionalOnProperty(name = "spring.datasource.mysql.druid.statViewServlet.enabled", havingValue = "true")
+//    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+//    {
+//        // 获取web监控页面的参数
+//        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+//        // 提取common.js的配置路径
+//        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+//        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+//        final String filePath = "support/http/resources/js/common.js";
+//        // 创建filter进行过滤
+//        Filter filter = new Filter()
+//        {
+//            @Override
+//            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+//            {
+//            }
+//            @Override
+//            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+//                    throws IOException, ServletException
+//            {
+//                chain.doFilter(request, response);
+//                // 重置缓冲区,响应头不会被重置
+//                response.resetBuffer();
+//                // 获取common.js
+//                String text = Utils.readFromResource(filePath);
+//                // 正则替换banner, 除去底部的广告信息
+//                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+//                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+//                response.getWriter().write(text);
+//            }
+//            @Override
+//            public void destroy()
+//            {
+//            }
+//        };
+//        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+//        registrationBean.setFilter(filter);
+//        registrationBean.addUrlPatterns(commonJsPattern);
+//        return registrationBean;
+//    }
+//}

+ 72 - 0
fs-wx-task/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-wx-task/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-wx-task/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();
+    }
+}

+ 150 - 0
fs-wx-task/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,150 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+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();
+//    }
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 161 - 0
fs-wx-task/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,161 @@
+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;
+
+import java.math.BigDecimal;
+
+/**
+ * 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
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        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, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 76 - 0
fs-wx-task/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -0,0 +1,76 @@
+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.format.FormatterRegistry;
+import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
+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;
+
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 通用配置
+ *
+
+ */
+@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);
+    }
+
+    @Override
+    public void addFormatters(FormatterRegistry registry) {
+        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
+        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd")); // 统一日期格式
+        registrar.registerFormatters(registry);
+    }
+}

+ 11 - 0
fs-wx-task/src/main/java/com/fs/framework/config/RetryConfig.java

@@ -0,0 +1,11 @@
+package com.fs.framework.config;
+
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.retry.annotation.EnableRetry;
+
+@Configuration
+@EnableRetry
+public class RetryConfig {
+
+}

+ 157 - 0
fs-wx-task/src/main/java/com/fs/framework/config/SecurityConfig.java

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

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

+ 121 - 0
fs-wx-task/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,121 @@
+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", "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();
+    }
+}

+ 115 - 0
fs-wx-task/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,115 @@
+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.TaskScheduler;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+@EnableAsync
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 300;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+
+    @Bean
+    public TaskScheduler taskScheduler(){
+        ThreadPoolTaskScheduler scheduler=new ThreadPoolTaskScheduler();
+        scheduler.setPoolSize(18);
+        scheduler.setThreadNamePrefix("scheduled-task-");
+        scheduler.setAwaitTerminationSeconds(60);
+        scheduler.setWaitForTasksToCompleteOnShutdown(true);
+        return scheduler;
+    }
+
+    @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);
+            }
+        };
+    }
+
+    @Bean(name = "sopTaskExecutor")
+    public Executor sopTaskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(16); // 根据需求调整
+        executor.setMaxPoolSize(32);  // 根据需求调整
+        executor.setQueueCapacity(800); // 根据需求调整
+        executor.setThreadNamePrefix("SopTask-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+
+    @Bean(name = "sopChatTaskExecutor")
+    public Executor batchInsertExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(16);
+        executor.setMaxPoolSize(32);
+        executor.setQueueCapacity(800);
+        executor.setThreadNamePrefix("BatchInsert-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+
+    @Bean(name = "sopRatingExecutor")
+    public Executor sopRatingExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(16); // 根据需求调整
+        executor.setMaxPoolSize(32);  // 根据需求调整
+        executor.setQueueCapacity(800); // 根据需求调整
+        executor.setThreadNamePrefix("SopRating-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+}

+ 77 - 0
fs-wx-task/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-wx-task/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-wx-task/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

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

+ 115 - 0
fs-wx-task/src/main/java/com/fs/framework/exception/GlobalExceptionHandler.java

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

+ 56 - 0
fs-wx-task/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-wx-task/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-wx-task/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-wx-task/src/main/java/com/fs/framework/manager/ShutdownManager.java

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

+ 106 - 0
fs-wx-task/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

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

+ 69 - 0
fs-wx-task/src/main/java/com/fs/framework/security/LoginBody.java

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

+ 255 - 0
fs-wx-task/src/main/java/com/fs/framework/security/LoginUser.java

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

+ 89 - 0
fs-wx-task/src/main/java/com/fs/framework/security/SecurityUtils.java

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

+ 47 - 0
fs-wx-task/src/main/java/com/fs/framework/security/filter/JwtAuthenticationTokenFilter.java

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

+ 35 - 0
fs-wx-task/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java

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

+ 54 - 0
fs-wx-task/src/main/java/com/fs/framework/security/handle/LogoutSuccessHandlerImpl.java

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

+ 92 - 0
fs-wx-task/src/main/java/com/fs/framework/service/CompanyLoginService.java

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

+ 66 - 0
fs-wx-task/src/main/java/com/fs/framework/service/CompanyPermissionService.java

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

+ 170 - 0
fs-wx-task/src/main/java/com/fs/framework/service/PermissionService.java

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

+ 236 - 0
fs-wx-task/src/main/java/com/fs/framework/service/TokenService.java

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

+ 75 - 0
fs-wx-task/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java

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

+ 1 - 0
fs-wx-task/src/main/resources/META-INF/spring-devtools.properties

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

+ 2 - 0
fs-wx-task/src/main/resources/banner.txt

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

+ 37 - 0
fs-wx-task/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}]

+ 94 - 0
fs-wx-task/src/main/resources/logback.xml

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

+ 19 - 0
fs-wx-task/src/main/resources/mybatis/mybatis-config.xml

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

+ 1 - 0
pom.xml

@@ -283,6 +283,7 @@
         <module>fs-repeat-api</module>
         <module>fs-ipad-task</module>
         <module>fs-websocket</module>
+        <module>fs-wx-task</module>
     </modules>
 
     <packaging>pom</packaging>