فهرست منبع

个微加微,发送

吴树波 9 ساعت پیش
والد
کامیت
6b3d5a78cd
100فایلهای تغییر یافته به همراه6500 افزوده شده و 351 حذف شده
  1. 36 0
      fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerController.java
  2. 1 1
      fs-ai-call-task/src/main/resources/application.yml
  3. 1 5
      fs-cid-workflow/src/main/resources/application.yml
  4. 4 0
      fs-common/src/main/java/com/fs/common/core/domain/AjaxResult.java
  5. 8 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java
  6. 28 0
      fs-company/src/main/java/com/fs/company/controller/company/WxContactController.java
  7. 28 0
      fs-company/src/main/java/com/fs/company/controller/company/WxMsgLogController.java
  8. 37 6
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  9. 10 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopController.java
  10. 85 0
      fs-company/src/main/java/com/fs/company/controller/qw/WxSopUserLogsController.java
  11. 96 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java
  12. 93 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java
  13. 73 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserController.java
  14. 73 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserInfoController.java
  15. 28 6
      fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java
  16. 8 0
      fs-service/src/main/java/com/fs/core/config/WxPayConfiguration.java
  17. 1 0
      fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java
  18. 299 253
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java
  19. 4 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerExportVO.java
  20. 6 1
      fs-service/src/main/java/com/fs/enums/ExecutionStatusEnum.java
  21. 37 14
      fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java
  22. 24 0
      fs-service/src/main/java/com/fs/ipad/WxIpadSendUtils.java
  23. 9 9
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  24. 2 3
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java
  25. 7 7
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java
  26. 6 3
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  27. 12 0
      fs-service/src/main/java/com/fs/wx/sop/params/UpdateWxSopUserLogDateVo.java
  28. 4 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java
  29. 11 6
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java
  30. 15 12
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  31. 7 7
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java
  32. 6 6
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java
  33. 23 5
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java
  34. 6 0
      fs-service/src/main/java/com/fs/wxcid/service/impl/WxContactServiceImpl.java
  35. 1 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  36. 237 0
      fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml
  37. 180 0
      fs-service/src/main/resources/mapper/wx/WxSopMapper.xml
  38. 144 0
      fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml
  39. 150 0
      fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml
  40. 2 1
      fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java
  41. 11 6
      fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java
  42. 146 0
      fs-wx-ipad-task/pom.xml
  43. 14 0
      fs-wx-ipad-task/src/main/java/com/fs/FSServletInitializer.java
  44. 25 0
      fs-wx-ipad-task/src/main/java/com/fs/FsWxIpadTaskApplication.java
  45. 51 0
      fs-wx-ipad-task/src/main/java/com/fs/app/exception/FSException.java
  46. 82 0
      fs-wx-ipad-task/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  47. 34 0
      fs-wx-ipad-task/src/main/java/com/fs/app/service/CustomThreadPoolConfig.java
  48. 65 0
      fs-wx-ipad-task/src/main/java/com/fs/app/service/WxIpadSendServer.java
  49. 256 0
      fs-wx-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  50. 171 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  51. 73 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  52. 219 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/LogAspect.java
  53. 117 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  54. 175 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/SopTenantDataSourceAspect.java
  55. 31 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/ApplicationConfig.java
  56. 58 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/ArrayStringTypeHandler.java
  57. 85 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/CaptchaConfig.java
  58. 115 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/DataSourceConfig.java
  59. 123 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/DruidConfig.java
  60. 72 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  61. 59 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/FilterConfig.java
  62. 76 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  63. 150 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/MyBatisConfig.java
  64. 76 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/ResourcesConfig.java
  65. 11 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/RetryConfig.java
  66. 158 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/SecurityConfig.java
  67. 33 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/ServerConfig.java
  68. 121 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/SwaggerConfig.java
  69. 102 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  70. 77 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  71. 27 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  72. 45 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  73. 102 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java
  74. 115 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/exception/GlobalExceptionHandler.java
  75. 56 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  76. 126 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  77. 56 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/manager/AsyncManager.java
  78. 40 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/manager/ShutdownManager.java
  79. 106 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  80. 69 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/LoginBody.java
  81. 255 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/LoginUser.java
  82. 89 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/SecurityUtils.java
  83. 47 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/filter/JwtAuthenticationTokenFilter.java
  84. 35 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java
  85. 54 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/security/handle/LogoutSuccessHandlerImpl.java
  86. 92 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/service/CompanyLoginService.java
  87. 66 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/service/CompanyPermissionService.java
  88. 170 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/service/PermissionService.java
  89. 236 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/service/TokenService.java
  90. 75 0
      fs-wx-ipad-task/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java
  91. 1 0
      fs-wx-ipad-task/src/main/resources/META-INF/spring-devtools.properties
  92. 2 0
      fs-wx-ipad-task/src/main/resources/banner.txt
  93. 6 0
      fs-wx-ipad-task/src/main/resources/bat/1.bat
  94. 6 0
      fs-wx-ipad-task/src/main/resources/bat/10.bat
  95. 6 0
      fs-wx-ipad-task/src/main/resources/bat/11.bat
  96. 6 0
      fs-wx-ipad-task/src/main/resources/bat/2.bat
  97. 6 0
      fs-wx-ipad-task/src/main/resources/bat/3.bat
  98. 6 0
      fs-wx-ipad-task/src/main/resources/bat/4.bat
  99. 6 0
      fs-wx-ipad-task/src/main/resources/bat/5.bat
  100. 6 0
      fs-wx-ipad-task/src/main/resources/bat/6.bat

+ 36 - 0
fs-admin/src/main/java/com/fs/crm/controller/CrmCustomerController.java

@@ -20,7 +20,9 @@ import com.fs.crm.param.*;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.vo.CrmCustomerExportVO;
 import com.fs.crm.vo.CrmCustomerListVO;
+import com.fs.framework.web.service.PermissionService;
 import com.fs.framework.web.service.TokenService;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.system.service.ISysRoleService;
 import com.github.pagehelper.PageHelper;
 import org.springframework.beans.BeanUtils;
@@ -49,6 +51,8 @@ public class CrmCustomerController extends BaseController
     private ICompanyService companyService;
     @Autowired
     private TokenService tokenService;
+    @Autowired
+    private PermissionService permissionService;
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
     @GetMapping("/lineList")
     public TableDataInfo line(CrmCustomer crmCustomer)
@@ -70,6 +74,22 @@ public class CrmCustomerController extends BaseController
         return toAjax(crmCustomerService.insertCrmCustomer(crmCustomer));
     }
 
+    /**
+     * 查看手机号
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看手机号", businessType = BusinessType.GRANT)
+    @GetMapping(value = "/queryPhone/{customerId}")
+    public AjaxResult queryPhone(@PathVariable("customerId") Long customerId)
+    {
+        CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
+        String mobile = customer.getMobile();
+        if (mobile != null && mobile.length() > 11) {
+            mobile = PhoneUtil.decryptPhone(mobile);
+        }
+        return AjaxResult.success().put("mobile", mobile);
+    }
+
     /**
      * 修改客户
      */
@@ -110,10 +130,18 @@ public class CrmCustomerController extends BaseController
         crmCustomer.setIsLine(1);
         List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListVO(crmCustomer);
         List<CrmCustomerExportVO> exportList=new ArrayList<>();
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerListVO customer:list){
             CrmCustomerExportVO vo=new CrmCustomerExportVO();
             vo.setSource(customer.getSource().toString());
             BeanUtils.copyProperties(customer,vo);
+            if(hasQueryPhonePermission && StringUtils.isNotEmpty(customer.getMobile())){
+                if(customer.getMobile().length() > 11){
+                    vo.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                }else{
+                    vo.setMobilePlain(customer.getMobile());
+                }
+            }
             exportList.add(vo);
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -152,6 +180,7 @@ public class CrmCustomerController extends BaseController
         List<CrmCustomerListVO> list = crmCustomerService.selectCrmCustomerListVO(crmCustomer);
         List<CrmCustomerExportVO> exportList=new ArrayList<>();
         SysRole sysRole = isCheckPermission();
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerListVO customer:list){
             CrmCustomerExportVO vo=new CrmCustomerExportVO();
             if(customer.getSource()!=null){
@@ -161,6 +190,13 @@ public class CrmCustomerController extends BaseController
             if(customer.getMobile()!=null && !(sysRole.getIsCheckPhone()==1)){
                 vo.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
             }
+            if(hasQueryPhonePermission && StringUtils.isNotEmpty(customer.getMobile())){
+                if(customer.getMobile().length() > 11){
+                    vo.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                }else{
+                    vo.setMobilePlain(customer.getMobile());
+                }
+            }
             exportList.add(vo);
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);

+ 1 - 1
fs-ai-call-task/src/main/resources/application.yml

@@ -14,6 +14,6 @@ spring:
 #    active: druid-sxjz
 #    active: druid-hdt
 #    active: druid-myhk-test
-cid-group-no: 3
+cid-group-no: 5
 # 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
 tenant-service-marker: aiCall00

+ 1 - 5
fs-cid-workflow/src/main/resources/application.yml

@@ -2,10 +2,6 @@
 server:
   # 服务器的HTTP端口,默认为8080
   port: 7201
-logging:
-  level:
-    org: INFO
-    com: DEBUG
 # Spring配置
 spring:
   profiles:
@@ -14,7 +10,7 @@ spring:
 #    active: druid-sxjz
 #    active: druid-hdt
 #    active: druid-myhk-test
-cid-group-no: 3
+cid-group-no: 5
 tenant-id: 11
 # 配置了服务标记后,请在tenant_service_config 中配置服务应用租户ids信息
 tenant-service-marker: cidWorkflow00

+ 4 - 0
fs-common/src/main/java/com/fs/common/core/domain/AjaxResult.java

@@ -145,4 +145,8 @@ public class AjaxResult extends HashMap<String, Object>
     {
         return new AjaxResult(code, msg, null);
     }
+    public AjaxResult put(String key, Object value){
+        super.put(key, value);
+        return this;
+    }
 }

+ 8 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWxAccountController.java

@@ -120,4 +120,12 @@ public class CompanyWxAccountController extends BaseController
     public R companyListAll(){
         return R.ok().put("data", companyWxAccountService.companyListAllCompany(new CompanyUser()));
     }
+    /**
+     * 同步微信联系人和群聊
+     */
+    @GetMapping("/syncWx")
+    public R syncWx(Long accountId){
+        companyWxAccountService.syncWx(accountId);
+        return R.ok();
+    }
 }

+ 28 - 0
fs-company/src/main/java/com/fs/company/controller/company/WxContactController.java

@@ -0,0 +1,28 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.service.IWxContactService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/company/wxContact")
+public class WxContactController extends BaseController
+{
+    @Autowired
+    private IWxContactService iWxContactService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxContact wxContact)
+    {
+        startPage();
+        List<WxContact> list = iWxContactService.selectWxContactList(wxContact);
+        return getDataTable(list);
+    }
+}

+ 28 - 0
fs-company/src/main/java/com/fs/company/controller/company/WxMsgLogController.java

@@ -0,0 +1,28 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.wxcid.domain.WxMsgLog;
+import com.fs.wxcid.service.IWxMsgLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/company/wxMsgLog")
+public class WxMsgLogController extends BaseController
+{
+    @Autowired
+    private IWxMsgLogService iWxMsgLogService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxMsgLog wxMsgLog)
+    {
+        startPage();
+        List<WxMsgLog> list = iWxMsgLogService.selectWxMsgLogList(wxMsgLog);
+        return getDataTable(list);
+    }
+}

+ 37 - 6
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -23,9 +23,11 @@ import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerUserService;
 import com.fs.crm.vo.*;
 import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.PermissionService;
 import com.fs.framework.service.TokenService;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysDictTypeService;
+import com.fs.his.utils.PhoneUtil;
 import com.github.pagehelper.PageHelper;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -67,6 +69,8 @@ public class CrmCustomerController extends BaseController
     private ISysConfigService configService;
     @Autowired
     private ISysDictTypeService dictTypeService;
+    @Autowired
+    private PermissionService permissionService;
 
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
@@ -419,16 +423,20 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getCompany().getCompanyId());
-
-
-
-
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListQuery(param);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
-
                     customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
             }
+            if(hasQueryPhonePermission && StringUtils.isNotEmpty(customer.getMobile())){
+                String originalMobile = customer.getMobile().replaceAll("\\*+", "");
+                if(originalMobile.length() >= 11){
+                    customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                }else{
+                    customer.setMobilePlain(originalMobile);
+                }
+            }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
         return util.exportExcel(list, "客户");
@@ -439,14 +447,21 @@ public class CrmCustomerController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         List<CrmCustomerExportVO> list = crmCustomerService.selectCrmCustomerExportListByIds(customerIds);
+        boolean hasQueryPhonePermission = permissionService.hasPermi("crm:customer:queryPhone");
         for(CrmCustomerExportVO customer:list){
             if(StringUtils.isNotEmpty(customer.getMobile())){
                 if(loginUser.getUser().getUserType().equals("00")){
+                    if(hasQueryPhonePermission){
+                        if(customer.getMobile().length() > 11){
+                            customer.setMobilePlain(PhoneUtil.decryptPhone(customer.getMobile()));
+                        }else{
+                            customer.setMobilePlain(customer.getMobile());
+                        }
+                    }
                 }
                 else{
                     customer.setMobile(customer.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                 }
-
             }
         }
         ExcelUtil<CrmCustomerExportVO> util = new ExcelUtil<CrmCustomerExportVO>(CrmCustomerExportVO.class);
@@ -479,6 +494,22 @@ public class CrmCustomerController extends BaseController
         return R.ok().put("data",list);
     }
 
+    /**
+     * 查看手机号
+     */
+    @PreAuthorize("@ss.hasPermi('crm:customer:queryPhone')")
+    @Log(title = "查看手机号", businessType = BusinessType.GRANT)
+    @GetMapping(value = "/queryPhone/{customerId}")
+    public AjaxResult queryPhone(@PathVariable("customerId") Long customerId)
+    {
+        CrmCustomer customer = crmCustomerService.selectCrmCustomerById(customerId);
+        String mobile = customer.getMobile();
+        if (mobile != null && mobile.length() > 11) {
+            mobile = PhoneUtil.decryptPhone(mobile);
+        }
+        return AjaxResult.success().put("mobile", mobile);
+    }
+
     @ApiOperation("动态数据字典查询")
     @GetMapping("/tradeDicts")
     public AjaxResult getTradeDicts() {

+ 10 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwSopController.java

@@ -29,6 +29,7 @@ import com.fs.sop.service.IQwSopService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.vo.SopVoiceListVo;
+import com.fs.wx.sop.service.IWxSopService;
 import org.apache.commons.beanutils.ConvertUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -80,6 +81,9 @@ public class QwSopController extends BaseController
     @Autowired
     private IQwSopTempVoiceService voiceService;
 
+    @Autowired
+    private IWxSopService wxSopService;
+
     /**
      * 查询企微sop列表
      */
@@ -373,6 +377,12 @@ public class QwSopController extends BaseController
         return qwSopService.updateStatusQwSopByIds(ids);
     }
 
+    @GetMapping(value = "/updateWxStatus/{ids}")
+    public R batchDoWxSop(@PathVariable Long[] ids)
+    {
+        return wxSopService.updateStatusWxSopByIds(ids);
+    }
+
     /**
      * 修改sop员工
      */

+ 85 - 0
fs-company/src/main/java/com/fs/company/controller/qw/WxSopUserLogsController.java

@@ -0,0 +1,85 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.params.UpdateWxSopUserLogDateVo;
+import com.fs.wx.sop.service.IWxSopUserService;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/wxSop/sopUserLogsWx")
+public class WxSopUserLogsController extends BaseController {
+
+    @Autowired
+    private IWxSopUserService wxSopUserService;
+
+    @Autowired
+    private IWxSopUserInfoService wxSopUserInfoService;
+
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUser wxSopUser) {
+        startPage();
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id) {
+        return AjaxResult.success(wxSopUserService.selectWxSopUserById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/listBySopId/{sopId}")
+    public TableDataInfo listBySopId(@PathVariable("sopId") Long sopId) {
+        startPage();
+        WxSopUser queryParam = new WxSopUser();
+        queryParam.setSopId(sopId);
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(queryParam);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/detail/{sopUserId}")
+    public TableDataInfo detail(@PathVariable("sopUserId") Long sopUserId) {
+        startPage();
+        WxSopUserInfo queryParam = new WxSopUserInfo();
+        queryParam.setSopUserId(sopUserId);
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(queryParam);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微SOP营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(wxSopUserService.deleteWxSopUserByIds(ids));
+    }
+
+    @Log(title = "批量修改个微SOP营期时间", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateLogDate")
+    public R updateLogDate(@RequestBody UpdateWxSopUserLogDateVo vo) {
+        return wxSopUserService.updateLogDate(vo);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @Log(title = "个微SOP营期", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopUser wxSopUser) {
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        ExcelUtil<WxSopUser> util = new ExcelUtil<WxSopUser>(WxSopUser.class);
+        return util.exportExcel(list, "个微SOP营期数据");
+    }
+}

+ 96 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopController.java

@@ -0,0 +1,96 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.params.SendWxSopMsgParam;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.wx.sop.service.IWxSopService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+@RestController
+@RequestMapping("/wx/wxSop")
+public class WxSopController extends BaseController
+{
+    @Autowired
+    private IWxSopService wxSopService;
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private IWxSopLogsService wxSopLogsService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        startPage();
+        List<WxSop> list = wxSopService.selectWxSopList(wxSop);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微SOP", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<WxSop> list = wxSopService.selectWxSopList(wxSop);
+        ExcelUtil<WxSop> util = new ExcelUtil<WxSop>(WxSop.class);
+        return util.exportExcel(list, "个微SOP数据");
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopService.selectWxSopDetailById(id));
+    }
+
+    @Log(title = "个微SOP", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(wxSopService.insertWxSop(wxSop));
+    }
+
+    @Log(title = "个微SOP", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSop wxSop)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        wxSop.setCompanyId(loginUser.getCompany().getCompanyId());
+        return toAjax(wxSopService.updateWxSop(wxSop));
+    }
+
+    @Log(title = "个微SOP", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopService.deleteWxSopByIds(ids));
+    }
+
+    @PostMapping("/sendMsg")
+    public R sendWxSopMsg(@RequestBody SendWxSopMsgParam param) {
+        return wxSopLogsService.sendWxSopMsg(param);
+    }
+}

+ 93 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java

@@ -0,0 +1,93 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+@RestController
+@RequestMapping("/wx/wxSopLogs")
+public class WxSopLogsController extends BaseController
+{
+    @Autowired
+    private IWxSopLogsService wxSopLogsService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopLogs wxSopLogs)
+    {
+        startPage();
+        List<WxSopLogs> list = wxSopLogsService.selectWxSopLogsList(wxSopLogs);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微发送记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopLogs wxSopLogs)
+    {
+        List<WxSopLogs> list = wxSopLogsService.selectWxSopLogsList(wxSopLogs);
+        ExcelUtil<WxSopLogs> util = new ExcelUtil<WxSopLogs>(WxSopLogs.class);
+        return util.exportExcel(list, "个微发送记录数据");
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopLogsService.selectWxSopLogsById(id));
+    }
+
+    @Log(title = "个微发送记录", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopLogs wxSopLogs)
+    {
+        return toAjax(wxSopLogsService.insertWxSopLogs(wxSopLogs));
+    }
+
+    @Log(title = "个微发送记录", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopLogs wxSopLogs)
+    {
+        return toAjax(wxSopLogsService.updateWxSopLogs(wxSopLogs));
+    }
+
+    @Log(title = "个微发送记录", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopLogsService.deleteWxSopLogsByIds(ids));
+    }
+
+    @GetMapping("/listCVO")
+    public TableDataInfo listCVO(WxSopLogsParam param)
+    {
+        startPage();
+        List<WxSopLogsListVO> list = wxSopLogsService.selectWxSopLogsListBySopId(param);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微SOP执行记录", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportCVO")
+    public void exportCVO(HttpServletResponse response, WxSopLogsParam param) throws Exception
+    {
+        List<WxSopLogsListVO> list = wxSopLogsService.selectWxSopLogsListBySopId(param);
+        ExcelUtil<WxSopLogsListVO> util = new ExcelUtil<WxSopLogsListVO>(WxSopLogsListVO.class);
+        util.exportExcel(response, list, "个微SOP执行记录数据");
+    }
+}

+ 73 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserController.java

@@ -0,0 +1,73 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.service.IWxSopUserService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+@RestController
+@RequestMapping("/wx/wxSopUser")
+public class WxSopUserController extends BaseController
+{
+    @Autowired
+    private IWxSopUserService wxSopUserService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUser wxSopUser)
+    {
+        startPage();
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微营期", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopUser wxSopUser)
+    {
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+        ExcelUtil<WxSopUser> util = new ExcelUtil<WxSopUser>(WxSopUser.class);
+        return util.exportExcel(list, "个微营期数据");
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopUserService.selectWxSopUserById(id));
+    }
+
+    @Log(title = "个微营期", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopUser wxSopUser)
+    {
+        return toAjax(wxSopUserService.insertWxSopUser(wxSopUser));
+    }
+
+    @Log(title = "个微营期", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopUser wxSopUser)
+    {
+        return toAjax(wxSopUserService.updateWxSopUser(wxSopUser));
+    }
+
+    @Log(title = "个微营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopUserService.deleteWxSopUserByIds(ids));
+    }
+}

+ 73 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopUserInfoController.java

@@ -0,0 +1,73 @@
+package com.fs.company.controller.wx.controller;
+
+import java.util.List;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+@RestController
+@RequestMapping("/wx/wxSopUserInfo")
+public class WxSopUserInfoController extends BaseController
+{
+    @Autowired
+    private IWxSopUserInfoService wxSopUserInfoService;
+
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUserInfo wxSopUserInfo)
+    {
+        startPage();
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(wxSopUserInfo);
+        return getDataTable(list);
+    }
+
+    @Log(title = "个微营期详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(WxSopUserInfo wxSopUserInfo)
+    {
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(wxSopUserInfo);
+        ExcelUtil<WxSopUserInfo> util = new ExcelUtil<WxSopUserInfo>(WxSopUserInfo.class);
+        return util.exportExcel(list, "个微营期详情数据");
+    }
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(wxSopUserInfoService.selectWxSopUserInfoById(id));
+    }
+
+    @Log(title = "个微营期详情", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody WxSopUserInfo wxSopUserInfo)
+    {
+        return toAjax(wxSopUserInfoService.insertWxSopUserInfo(wxSopUserInfo));
+    }
+
+    @Log(title = "个微营期详情", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody WxSopUserInfo wxSopUserInfo)
+    {
+        return toAjax(wxSopUserInfoService.updateWxSopUserInfo(wxSopUserInfo));
+    }
+
+    @Log(title = "个微营期详情", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(wxSopUserInfoService.deleteWxSopUserInfoByIds(ids));
+    }
+}

+ 28 - 6
fs-service/src/main/java/com/fs/company/service/impl/call/node/AiAddWxTaskNewNode.java

@@ -2,6 +2,7 @@ package com.fs.company.service.impl.call.node;
 
 import cn.hutool.core.util.RandomUtil;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.constant.Constants;
 import com.fs.common.core.redis.RedisCacheT;
 import com.fs.common.exception.CustomException;
@@ -18,6 +19,7 @@ import com.fs.company.vo.AiAddWxConfigVO;
 import com.fs.company.vo.AiCallWorkflowConditionVo;
 import com.fs.company.vo.ExecutionResult;
 import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.enums.ExecutionStatusEnum;
 import com.fs.enums.NodeTypeEnum;
@@ -50,6 +52,7 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
     public static final String DELAY_ADD_WX_NEW_KEY = "addWxTaskNew:delay:%s:%s:%s:";
     private static final CompanyWxDialogMapper companyWxDialogMapper = SpringUtils.getBean(CompanyWxDialogMapper.class);
     private static final CrmCustomerServiceImpl crmCustomerService = SpringUtils.getBean(CrmCustomerServiceImpl.class);
+    private static final CrmCustomerMapper crmCustomerMapper = SpringUtils.getBean(CrmCustomerMapper.class);
     private static final ObjectPlaceholderResolver objectPlaceholderResolver = SpringUtils.getBean(ObjectPlaceholderResolver.class);
     private static final ISysConfigService sysConfigService = SpringUtils.getBean(ISysConfigService.class);
     private static final CidIpadServerMapper cidIpadServerMapper = SpringUtils.getBean(CidIpadServerMapper.class);
@@ -98,10 +101,25 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             }
 
             WxContact wxQuery = companyAiWorkflowExecMapper.selectWxContectByWorkflowInstanceId(context.getWorkflowInstanceId());
-            wxQuery.setRemark(wxQuery.getRemark() + RandomUtil.randomNumbers(10));
-            wxQuery.setNickName(wxQuery.getRemark());
-            wxQuery.setFriends(0);
-            wxContactMapper.insert(wxQuery);
+            Long crmUserId = wxQuery.getCrmUserId();
+            WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("phone", wxQuery.getPhone()).eq("account_id", wxQuery.getAccountId()));
+            if(wxContact != null){
+                wxQuery = wxContact;
+                wxQuery.setRemark(wxQuery.getRemark());
+                wxQuery.setNickName(wxQuery.getRemark());
+                wxQuery.setFriends(0);
+                wxQuery.setCustomerId(crmUserId);
+                wxContactMapper.updateById(wxQuery);
+            }else{
+                wxQuery.setRemark(wxQuery.getRemark());
+                wxQuery.setNickName(wxQuery.getRemark());
+                wxQuery.setFriends(0);
+                wxQuery.setCrmUserId(crmUserId);
+                wxContactMapper.insert(wxQuery);
+            }
+            CrmCustomer crmCustomer = crmCustomerMapper.selectById(crmUserId);
+            crmCustomer.setWxContactId(wxQuery.getId());
+            crmCustomerService.updateCrmCustomer(crmCustomer);
 
             CompanyVoiceRoboticBusiness roboticBusiness = getRoboticBusiness(context.getWorkflowInstanceId());
             CompanyWxClient update = new CompanyWxClient();
@@ -112,7 +130,8 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
             pendingAddWx(wxQuery.getAccountId(), wxQuery.getRemark(),
                     wxQuery.getPhone(),
                     addWxConfig.getDialogId(),
-                    wxQuery.getCrmUserId(),
+                    crmUserId,
+                    wxQuery.getId(),
                     context.getWorkflowInstanceId(),
                     context.getCurrentNodeKey());
             return ExecutionResult.paused()
@@ -279,7 +298,7 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
      * @param instanceId
      * @param nodeKey
      */
-    private void pendingAddWx(Long accountId, String remark, String phone, Long dialogId, Long crmUserId,String instanceId,String nodeKey) {
+    private void pendingAddWx(Long accountId, String remark, String phone, Long dialogId, Long crmUserId, Long wxContactId,String instanceId,String nodeKey) {
         try {
             // 1. 获取基础数据
             CompanyWxAccount companyWxAccount = companyWxAccountMapper.selectCompanyWxAccountById(accountId);
@@ -308,6 +327,9 @@ public class AiAddWxTaskNewNode extends AbstractWorkflowNode {
                     .fluentPut("instanceId",instanceId)
                     .fluentPut("nodeKey",nodeKey)
                     .fluentPut("accountId",companyWxAccount.getId())
+                    .fluentPut("wxContactId",wxContactId)
+                    .fluentPut("crmUserId",crmUserId)
+                    .fluentPut("accountId",companyWxAccount.getId())
                     .fluentPut("tenantId", TenantHelper.getTenantId());
             param.setBizJson(bizJson.toJSONString());
             wxService.addWx(param);

+ 8 - 0
fs-service/src/main/java/com/fs/core/config/WxPayConfiguration.java

@@ -6,11 +6,15 @@ import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Scope;
+import org.springframework.core.env.Environment;
+
+import java.util.Objects;
 
 @Slf4j
 @Configuration
@@ -18,6 +22,7 @@ import org.springframework.context.annotation.Scope;
 @AllArgsConstructor
 public class WxPayConfiguration {
   private WxPayProperties properties;
+  private Environment environment;
 
   @Bean
   @ConditionalOnMissingBean
@@ -26,6 +31,9 @@ public class WxPayConfiguration {
     try {
       // 每次创建服务时都获取最新的配置
       log.info("创建微信支付服务,检查配置...");
+      if(Objects.equals(environment.getProperty("spring.profiles.active"), "dev")){
+        return new WxPayServiceImpl();
+      }
 
       WxPayConfig payConfig = new WxPayConfig();
 

+ 1 - 0
fs-service/src/main/java/com/fs/crm/domain/CrmCustomer.java

@@ -177,6 +177,7 @@ public class CrmCustomer extends BaseEntity
     private String sourceCode;
 
     private String pushTime;
+    private Long wxContactId;
 
     private String pushCode;
     private String intention;

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 299 - 253
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java


+ 4 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerExportVO.java

@@ -25,6 +25,10 @@ public class CrmCustomerExportVO implements Serializable
     @Excel(name = "手机号")
     private String mobile;
 
+    /** 明文手机号(有权限时显示) */
+    @Excel(name = "明文手机号")
+    private String mobilePlain;
+
     /** 性别 */
     @Excel(name = "性别",readConverterExp = "1=男,2=女,0=未知")
     private Integer sex;

+ 6 - 1
fs-service/src/main/java/com/fs/enums/ExecutionStatusEnum.java

@@ -58,7 +58,12 @@ public enum ExecutionStatusEnum {
     /**
      * 等待人工外呼
      */
-    WAITING_DO_CALL("WAITINGDOCALL", "等待人工外呼", 11);
+    WAITING_DO_CALL("WAITINGDOCALL", "等待人工外呼", 11),
+
+    /**
+     * 等待AI外呼
+     */
+    WAITING_AI_CALL("WAITINGAICALL", "等待AI外呼", 12);
 
     private final String code;
     private final String description;

+ 37 - 14
fs-service/src/main/java/com/fs/his/utils/PhoneUtil.java

@@ -22,10 +22,23 @@ public class PhoneUtil {
         }
         return encryptedText;
     }
+    /**
+    * 判断是否为正常手机号(11位纯数字)
+    */
+    private static boolean isNormalPhone(String text) {
+        return text != null && text.matches("^1\\d{10}$");
+    }
+
     /**
     * 解密
     */
     public static String decryptPhone(String encryptedText) {
+        if (encryptedText == null || encryptedText.isEmpty()) {
+            return encryptedText;
+        }
+        if (isNormalPhone(encryptedText)) {
+            return encryptedText;
+        }
         String text=null;
         try {
             SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
@@ -42,6 +55,12 @@ public class PhoneUtil {
     * 解密加*
     */
     public static String decryptPhoneMk(String encryptedText) {
+        if (encryptedText == null || encryptedText.isEmpty()) {
+            return encryptedText;
+        }
+        if (isNormalPhone(encryptedText)) {
+            return encryptedText.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+        }
         String text=null;
         try {
             SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
@@ -58,21 +77,25 @@ public class PhoneUtil {
 
     public static String decryptAutoPhoneMk(String encryptedText) {
         String text=null;
-        if (encryptedText!=null&&encryptedText!="") {
-            if (encryptedText.length()>11){
-                try {
-                    SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
-                    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
-                    cipher.init(Cipher.DECRYPT_MODE, secretKey);
-                    byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
-                    text = new String(decryptedBytes);
-                    text =text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
-                } catch (Exception e) {
-                    e.printStackTrace();
-                }
-            }else {
-                text =  ParseUtils.parsePhone(encryptedText);
+        if (encryptedText==null||encryptedText=="") {
+            return text;
+        }
+        if (isNormalPhone(encryptedText)) {
+            return encryptedText.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+        }
+        if (encryptedText.length()>11){
+            try {
+                SecretKeySpec secretKey = new SecretKeySpec("AESAabCdeREssREA".getBytes(), "AES");
+                Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+                cipher.init(Cipher.DECRYPT_MODE, secretKey);
+                byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
+                text = new String(decryptedBytes);
+                text =text.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
+            } catch (Exception e) {
+                e.printStackTrace();
             }
+        }else {
+            text =  ParseUtils.parsePhone(encryptedText);
         }
 
         return text;

+ 24 - 0
fs-service/src/main/java/com/fs/ipad/WxIpadSendUtils.java

@@ -0,0 +1,24 @@
+package com.fs.ipad;
+
+
+import com.fs.ipad.vo.WxBaseVo;
+import com.fs.ipad.vo.WxTxtVo;
+import com.fs.wxwork.service.WxIpadService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@AllArgsConstructor
+public class WxIpadSendUtils {
+
+    private final WxIpadService wxIpadService;
+
+    public void sendTxt(WxBaseVo baseVo, String txt){
+        WxTxtVo vo = new WxTxtVo();
+        vo.setBase(baseVo);
+        vo.setContent(txt);
+        wxIpadService.sendTxt(vo);
+    }
+}

+ 9 - 9
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -23,7 +23,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param id 个微发送记录主键
      * @return 个微发送记录
      */
-    @DataSource(DataSourceType.SOP)
+    
     WxSopLogs selectWxSopLogsById(Long id);
 
     /**
@@ -32,7 +32,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 个微发送记录集合
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
 
     /**
@@ -41,7 +41,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param param 查询参数
      * @return 执行记录集合
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
 
     /**
@@ -50,7 +50,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int insertWxSopLogs(WxSopLogs wxSopLogs);
 
     /**
@@ -59,7 +59,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param wxSopLogs 个微发送记录
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int updateWxSopLogs(WxSopLogs wxSopLogs);
 
     /**
@@ -68,7 +68,7 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param id 个微发送记录主键
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int deleteWxSopLogsById(Long id);
 
     /**
@@ -77,12 +77,12 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int deleteWxSopLogsByIds(Long[] ids);
 
-    @DataSource(DataSourceType.SOP)
+    
     void batchInsertWxSopLogs(List<WxSopLogs> logsToInsert);
 
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopLogs> selectByWxId(@Param("id") Long id);
 }

+ 2 - 3
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java

@@ -22,7 +22,7 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
      * @param id 个微SOP主键
      * @return 个微SOP
      */
-    @DataSource(DataSourceType.SOP)
+    
     WxSop selectWxSopById(Long id);
 
     /**
@@ -31,7 +31,7 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
      * @param wxSop 个微SOP
      * @return 个微SOP集合
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSop> selectWxSopList(WxSop wxSop);
 
     /**
@@ -89,6 +89,5 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
      * @param param 筛选参数
      * @return 客户结果列表
      */
-    @DataSource(DataSourceType.MASTER)
     List<WxFilterSopCustomersResult> selectFilterWxSopCustomers(WxSopTagsParam param);
 }

+ 7 - 7
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java

@@ -20,7 +20,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param id 个微营期详情主键
      * @return 个微营期详情
      */
-    @DataSource(DataSourceType.SOP)
+    
     WxSopUserInfo selectWxSopUserInfoById(Long id);
 
     /**
@@ -29,7 +29,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 个微营期详情集合
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -38,7 +38,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -47,7 +47,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo);
 
     /**
@@ -56,7 +56,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param id 个微营期详情主键
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int deleteWxSopUserInfoById(Long id);
 
     /**
@@ -65,7 +65,7 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
-    @DataSource(DataSourceType.SOP)
+    
     int deleteWxSopUserInfoByIds(Long[] ids);
 
     /**
@@ -74,6 +74,6 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @param wxSopUserInfo 个微营期详情
      * @return 个微营期详情
      */
-    @DataSource(DataSourceType.SOP)
+    
     WxSopUserInfo selectWxSopUserInfoByCondition(WxSopUserInfo wxSopUserInfo);
 }

+ 6 - 3
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.params.UpdateWxSopUserLogDateVo;
 import com.fs.wx.sop.vo.WxSopUserMsgGenVO;
 
 import java.util.List;
@@ -14,7 +15,7 @@ import java.util.List;
  * @author 吴树波
  * @date 2026-02-24
  */
-@DataSource(DataSourceType.SOP)
+
 public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
     /**
      * 查询个微营期
@@ -30,7 +31,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
      * @param wxSopUser 个微营期
      * @return 个微营期集合
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser);
 
     /**
@@ -86,6 +87,8 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
      *
      * @return 营期客户信息列表
      */
-    @DataSource(DataSourceType.SOP)
+    
     List<WxSopUserMsgGenVO> selectActiveWxSopUserForMsgGen();
+
+    int updateWxSopUserDateById(UpdateWxSopUserLogDateVo vo);
 }

+ 12 - 0
fs-service/src/main/java/com/fs/wx/sop/params/UpdateWxSopUserLogDateVo.java

@@ -0,0 +1,12 @@
+package com.fs.wx.sop.params;
+
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+public class UpdateWxSopUserLogDateVo {
+    private List<String> ids;
+    private LocalDate newStartTime;
+}

+ 4 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopUserService.java

@@ -1,7 +1,9 @@
 package com.fs.wx.sop.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
 import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.params.UpdateWxSopUserLogDateVo;
 
 import java.util.List;
 
@@ -59,4 +61,6 @@ public interface IWxSopUserService extends IService<WxSopUser>{
      * @return 结果
      */
     int deleteWxSopUserById(Long id);
+
+    R updateLogDate(UpdateWxSopUserLogDateVo vo);
 }

+ 11 - 6
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java

@@ -16,6 +16,7 @@ import com.fs.wx.sop.mapper.WxSopMapper;
 import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
 import com.fs.wx.sop.mapper.WxSopUserMapper;
 import com.fs.wx.sop.service.IWxSopExecuteService;
+import com.fs.wxcid.mapper.WxContactMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -47,11 +48,14 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
     @Autowired
     private CrmCustomerMapper crmCustomerMapper;
 
+    @Autowired
+    private WxContactMapper wxContactMapper;
+
     @Autowired
     private ISysDictDataService sysDictDataService;
 
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public R processTagFilterWxSop(WxSop wxSop) {
         try {
             log.info("====== 开始执行标签筛选SOP ======");
@@ -93,7 +97,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
     }
 
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public R processGroupFilterWxSop(WxSop wxSop) {
         try {
             // 群聊筛选逻辑
@@ -109,7 +113,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
     }
 
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public R createSopUserLogsWx(WxSop wxSop) {
         try {
             // 根据SOP的筛选方式进行不同的客户筛选
@@ -197,7 +201,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
      * @param customerResults 客户结果列表
      */
     @Transactional(rollbackFor = Exception.class)
-    @DataSource(DataSourceType.SOP)
+    
     public void createSopUserLogsWxForCustomers(WxSop wxSop, List<WxFilterSopCustomersResult> customerResults) {
         log.info("====== 开始为 {} 个客户创建营期记录 ======", customerResults.size());
         // 按执行账号分组创建营期
@@ -492,7 +496,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
      * @param customer 客户信息
      * @param sop SOP信息
      */
-    @DataSource(DataSourceType.SOP)
+    
     public void addCustomerToSop(CrmCustomer customer, WxSop sop) {
         try {
             WxFilterSopCustomersResult customerResult = new WxFilterSopCustomersResult();
@@ -522,6 +526,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
             wxSopUserInfo.setSopId(sop.getId());
             wxSopUserInfo.setSopUserId(sopUserId);
             wxSopUserInfo.setCustomerId(customer.getCustomerId());
+            wxSopUserInfo.setWxContactId(customer.getWxContactId());
             wxSopUserInfo.setStatus(0); // 正常状态
             wxSopUserInfo.setTagNames(customer.getTags());
 
@@ -540,7 +545,7 @@ public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
      * @param sop SOP信息
      * @param existingInfos 已存在的营期成员记录
      */
-    @DataSource(DataSourceType.SOP)
+    
     public void removeCustomerFromSop(CrmCustomer customer, WxSop sop, List<WxSopUserInfo> existingInfos) {
         try {
             for (WxSopUserInfo info : existingInfos) {

+ 15 - 12
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -60,7 +60,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 个微发送记录
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public WxSopLogs selectWxSopLogsById(Long id)
     {
         return baseMapper.selectWxSopLogsById(id);
@@ -73,7 +73,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 个微发送记录
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs)
     {
         return baseMapper.selectWxSopLogsList(wxSopLogs);
@@ -86,7 +86,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int insertWxSopLogs(WxSopLogs wxSopLogs)
     {
         wxSopLogs.setCreateTime(DateUtils.getNowDate());
@@ -100,7 +100,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int updateWxSopLogs(WxSopLogs wxSopLogs)
     {
         wxSopLogs.setUpdateTime(DateUtils.getNowDate());
@@ -114,7 +114,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopLogsByIds(Long[] ids)
     {
         return baseMapper.deleteWxSopLogsByIds(ids);
@@ -127,7 +127,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopLogsById(Long id)
     {
         return baseMapper.deleteWxSopLogsById(id);
@@ -142,6 +142,9 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
     @Override
     public List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param){
         List<WxSopLogsListVO> list = baseMapper.selectWxSopLogsListBySopId(param);
+        if(list.isEmpty()){
+            return Collections.emptyList();
+        }
         List<Long> longs = PubFun.listToNewList(list, WxSopLogsListVO::getAccountId);
         List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectBatchIds(longs);
         Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
@@ -151,7 +154,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
         return list;
     }
 
-    @DataSource(DataSourceType.SOP)
+    
     public void batchInsertQwSopLogs(List<WxSopLogs> logsToInsert) {
         if(logsToInsert == null || logsToInsert.isEmpty()) return;
         wxSopLogsMapper.batchInsertWxSopLogs(logsToInsert);
@@ -159,8 +162,8 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
 
     /**
      * 个微SOP一键群发
-     * 注意:不加 @DataSource(DataSourceType.SOP),因为需要跨库查询
-     * SOP相关Mapper方法自带 @DataSource(DataSourceType.SOP) 注解
+     * 注意:不加 ,因为需要跨库查询
+     * SOP相关Mapper方法自带  注解
      * wx_contact、crm_customer 在主库,使用默认数据源
      */
     @Override
@@ -189,7 +192,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
         for (Long sopId : param.getSopIds()) {
             WxSopUser query = new WxSopUser();
             query.setSopId(sopId);
-            // wxSopUserMapper.selectWxSopUserList 自带 @DataSource(DataSourceType.SOP)
+            // wxSopUserMapper.selectWxSopUserList 自带 
             List<WxSopUser> sopUsers = wxSopUserMapper.selectWxSopUserList(query);
 
             if (sopUsers == null || sopUsers.isEmpty()) {
@@ -199,7 +202,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
             for (WxSopUser sopUser : sopUsers) {
                 WxSopUserInfo userInfoQuery = new WxSopUserInfo();
                 userInfoQuery.setSopUserId(sopUser.getId());
-                // wxSopUserInfoMapper.selectWxSopUserInfoList 自带 @DataSource(DataSourceType.SOP)
+                // wxSopUserInfoMapper.selectWxSopUserInfoList 自带 
                 List<WxSopUserInfo> userInfos = wxSopUserInfoMapper.selectWxSopUserInfoList(userInfoQuery);
 
                 if (userInfos == null || userInfos.isEmpty()) {
@@ -310,7 +313,7 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
     }
 
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public boolean updateMapper(WxSopLogs updateQwSop) {
         return updateById(updateQwSop);
     }

+ 7 - 7
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java

@@ -43,7 +43,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 个微SOP
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public WxSop selectWxSopById(Long id)
     {
         return baseMapper.selectWxSopById(id);
@@ -97,7 +97,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 个微SOP
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public List<WxSop> selectWxSopList(WxSop wxSop)
     {
         return baseMapper.selectWxSopList(wxSop);
@@ -116,7 +116,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int insertWxSop(WxSop wxSop)
     {
         if (wxSop.getStatus() == null) {
@@ -133,7 +133,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int updateWxSop(WxSop wxSop)
     {
         wxSop.setUpdateTime(DateUtils.getNowDate());
@@ -147,7 +147,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopByIds(Long[] ids)
     {
         return baseMapper.deleteWxSopByIds(ids);
@@ -160,7 +160,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopById(Long id)
     {
         return baseMapper.deleteWxSopById(id);
@@ -173,7 +173,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public R updateStatusWxSopByIds(Long[] ids) {
         if (ids == null || ids.length == 0) {
             return R.error("参数不能为空");

+ 6 - 6
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserInfoServiceImpl.java

@@ -27,7 +27,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 个微营期详情
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public WxSopUserInfo selectWxSopUserInfoById(Long id)
     {
         return baseMapper.selectWxSopUserInfoById(id);
@@ -40,7 +40,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 个微营期详情
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public List<WxSopUserInfo> selectWxSopUserInfoList(WxSopUserInfo wxSopUserInfo)
     {
         return baseMapper.selectWxSopUserInfoList(wxSopUserInfo);
@@ -53,7 +53,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int insertWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
     {
         wxSopUserInfo.setCreateTime(DateUtils.getNowDate());
@@ -67,7 +67,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int updateWxSopUserInfo(WxSopUserInfo wxSopUserInfo)
     {
         wxSopUserInfo.setUpdateTime(DateUtils.getNowDate());
@@ -81,7 +81,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopUserInfoByIds(Long[] ids)
     {
         return baseMapper.deleteWxSopUserInfoByIds(ids);
@@ -94,7 +94,7 @@ public class WxSopUserInfoServiceImpl extends ServiceImpl<WxSopUserInfoMapper, W
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopUserInfoById(Long id)
     {
         return baseMapper.deleteWxSopUserInfoById(id);

+ 23 - 5
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopUserServiceImpl.java

@@ -6,14 +6,17 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
+import com.fs.common.core.domain.R;
 import com.fs.company.domain.CompanyWxAccount;
 import com.fs.company.mapper.CompanyWxAccountMapper;
 import com.fs.wx.sop.domain.WxSopUser;
 import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.params.UpdateWxSopUserLogDateVo;
 import com.fs.wx.sop.service.IWxSopUserService;
 import lombok.AllArgsConstructor;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -35,7 +38,7 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 个微营期
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public WxSopUser selectWxSopUserById(Long id)
     {
         return baseMapper.selectWxSopUserById(id);
@@ -50,6 +53,9 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
     @Override
     public List<WxSopUser> selectWxSopUserList(WxSopUser wxSopUser){
         List<WxSopUser> wxSopUsers = baseMapper.selectWxSopUserList(wxSopUser);
+        if(wxSopUsers.isEmpty()){
+            return Collections.emptyList();
+        }
         List<CompanyWxAccount> companyWxAccounts = companyWxAccountMapper.selectList(new QueryWrapper<CompanyWxAccount>().in("id", PubFun.listToNewList(wxSopUsers, WxSopUser::getAccountId)));
         Map<Long, CompanyWxAccount> accountMap = PubFun.listToMapByGroupObject(companyWxAccounts, CompanyWxAccount::getId);
         wxSopUsers.parallelStream().filter(e -> accountMap.containsKey(e.getAccountId())).forEach(e -> {
@@ -66,7 +72,7 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int insertWxSopUser(WxSopUser wxSopUser)
     {
         wxSopUser.setCreateTime(DateUtils.getNowDate());
@@ -80,7 +86,7 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int updateWxSopUser(WxSopUser wxSopUser)
     {
         wxSopUser.setUpdateTime(DateUtils.getNowDate());
@@ -94,7 +100,7 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopUserByIds(Long[] ids)
     {
         return baseMapper.deleteWxSopUserByIds(ids);
@@ -107,9 +113,21 @@ public class WxSopUserServiceImpl extends ServiceImpl<WxSopUserMapper, WxSopUser
      * @return 结果
      */
     @Override
-    @DataSource(DataSourceType.SOP)
+    
     public int deleteWxSopUserById(Long id)
     {
         return baseMapper.deleteWxSopUserById(id);
     }
+
+    @Override
+    public R updateLogDate(UpdateWxSopUserLogDateVo vo) {
+        if (vo.getNewStartTime() == null) {
+            return R.error("修改时间不能为空");
+        }
+        if (vo.getIds() == null || vo.getIds().isEmpty()) {
+            return R.error("营期ID不能为空");
+        }
+        int rows = baseMapper.updateWxSopUserDateById(vo);
+        return R.ok(String.valueOf(rows));
+    }
 }

+ 6 - 0
fs-service/src/main/java/com/fs/wxcid/service/impl/WxContactServiceImpl.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.CompanyWxAccount;
 import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.dto.friend.ContactItem;
 import com.fs.wxcid.mapper.WxContactMapper;
@@ -24,6 +26,7 @@ import java.util.List;
 public class WxContactServiceImpl extends ServiceImpl<WxContactMapper, WxContact> implements IWxContactService {
 
     private final CompanyWxAccountMapper companyWxAccountMapper;
+    private final CrmCustomerMapper crmCustomerMapper;
 
     /**
      * 查询个微联系人
@@ -118,5 +121,8 @@ public class WxContactServiceImpl extends ServiceImpl<WxContactMapper, WxContact
         wxContact.setCompanyUserId(account.getCompanyUserId());
         wxContact.setCustomerId(customerId);
         save(wxContact);
+        CrmCustomer crmCustomer = crmCustomerMapper.selectById(customerId);
+        crmCustomer.setWxContactId(wxContact.getId());
+        crmCustomerMapper.updateById(crmCustomer);
     }
 }

+ 1 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -266,6 +266,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="thirdAccount != null">third_account = #{thirdAccount},</if>
             <if test="clueId != null">clue_id = #{clueId},</if>
             <if test="qwName != null">qw_name = #{qwName},</if>
+            <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
             <if test="historicalCommunication != null">historical_communication = #{historicalCommunication},</if>
         </trim>
         where customer_id = #{customerId}

+ 237 - 0
fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml

@@ -0,0 +1,237 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopLogsMapper">
+
+    <resultMap type="WxSopLogs" id="WxSopLogsResult">
+        <result property="id"    column="id"    />
+        <result property="type"    column="type"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="generateType"    column="generate_type"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="wxContactName"    column="wx_contact_name"    />
+        <result property="wxRoomId"    column="wx_room_id"    />
+        <result property="wxRoomName"    column="wx_room_name"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="sendStatus"    column="send_status"    />
+        <result property="sendRemark"    column="send_remark"    />
+        <result property="sendSort"    column="send_sort"    />
+        <result property="expirationTime"    column="expiration_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopLogsVo">
+        select id, type, sop_id, sop_user_id, send_type, generate_type, account_id, wx_contact_id, wx_contact_name, wx_room_id, wx_room_name, fs_user_id, send_status, send_remark, send_sort, expiration_time, create_time, create_by, update_time, update_by, remark from wx_sop_logs
+    </sql>
+
+    <select id="selectWxSopLogsList" parameterType="WxSopLogs" resultMap="WxSopLogsResult">
+        <include refid="selectWxSopLogsVo"/>
+        <where>
+            <if test="type != null "> and type = #{type}</if>
+            <if test="sopId != null "> and sop_id = #{sopId}</if>
+            <if test="sopUserId != null "> and sop_user_id = #{sopUserId}</if>
+            <if test="sendType != null "> and send_type = #{sendType}</if>
+            <if test="generateType != null "> and generate_type = #{generateType}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="wxContactId != null "> and wx_contact_id = #{wxContactId}</if>
+            <if test="wxContactName != null  and wxContactName != ''"> and wx_contact_name like concat('%', #{wxContactName}, '%')</if>
+            <if test="wxRoomId != null "> and wx_room_id = #{wxRoomId}</if>
+            <if test="wxRoomName != null  and wxRoomName != ''"> and wx_room_name like concat('%', #{wxRoomName}, '%')</if>
+            <if test="fsUserId != null "> and fs_user_id = #{fsUserId}</if>
+            <if test="sendStatus != null "> and send_status = #{sendStatus}</if>
+            <if test="sendRemark != null  and sendRemark != ''"> and send_remark = #{sendRemark}</if>
+            <if test="sendSort != null "> and send_sort = #{sendSort}</if>
+            <if test="expirationTime != null "> and expiration_time = #{expirationTime}</if>
+        </where>
+    </select>
+
+    <select id="selectWxSopLogsById" parameterType="Long" resultMap="WxSopLogsResult">
+        <include refid="selectWxSopLogsVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertWxSopLogs" parameterType="WxSopLogs" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_logs
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="sopId != null">sop_id,</if>
+            <if test="sopUserId != null">sop_user_id,</if>
+            <if test="sendType != null">send_type,</if>
+            <if test="generateType != null">generate_type,</if>
+            <if test="accountId != null">account_id,</if>
+            <if test="wxContactId != null">wx_contact_id,</if>
+            <if test="wxContactName != null">wx_contact_name,</if>
+            <if test="wxRoomId != null">wx_room_id,</if>
+            <if test="wxRoomName != null">wx_room_name,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="sendStatus != null">send_status,</if>
+            <if test="sendRemark != null">send_remark,</if>
+            <if test="sendSort != null">send_sort,</if>
+            <if test="expirationTime != null">expiration_time,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="sopId != null">#{sopId},</if>
+            <if test="sopUserId != null">#{sopUserId},</if>
+            <if test="sendType != null">#{sendType},</if>
+            <if test="generateType != null">#{generateType},</if>
+            <if test="accountId != null">#{accountId},</if>
+            <if test="wxContactId != null">#{wxContactId},</if>
+            <if test="wxContactName != null">#{wxContactName},</if>
+            <if test="wxRoomId != null">#{wxRoomId},</if>
+            <if test="wxRoomName != null">#{wxRoomName},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="sendStatus != null">#{sendStatus},</if>
+            <if test="sendRemark != null">#{sendRemark},</if>
+            <if test="sendSort != null">#{sendSort},</if>
+            <if test="expirationTime != null">#{expirationTime},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+    <insert id="batchInsertWxSopLogs" parameterType="java.util.List">
+        INSERT INTO wx_sop_logs
+        (
+        type, sop_id, sop_user_id, send_type, generate_type, account_id,
+        wx_contact_id, wx_contact_name, wx_room_id, wx_room_name, fs_user_id,
+        send_status, send_remark, send_sort, expiration_time, create_time,
+        create_by, update_time, update_by, remark, content_json, send_time
+        )
+        VALUES
+        <foreach collection="list" item="log" separator=",">
+            (
+            #{log.type}, #{log.sopId}, #{log.sopUserId}, #{log.sendType}, #{log.generateType},
+            #{log.accountId}, #{log.wxContactId}, #{log.wxContactName}, #{log.wxRoomId},
+            #{log.wxRoomName}, #{log.fsUserId}, #{log.sendStatus}, #{log.sendRemark},
+            #{log.sendSort}, #{log.expirationTime}, #{log.createTime}, #{log.createBy},
+            #{log.updateTime}, #{log.updateBy}, #{log.remark}, #{log.contentJson}, #{log.sendTime}
+            )
+        </foreach>
+    </insert>
+
+    <update id="updateWxSopLogs" parameterType="WxSopLogs">
+        update wx_sop_logs
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="type != null">type = #{type},</if>
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="sopUserId != null">sop_user_id = #{sopUserId},</if>
+            <if test="sendType != null">send_type = #{sendType},</if>
+            <if test="generateType != null">generate_type = #{generateType},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
+            <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
+            <if test="wxContactName != null">wx_contact_name = #{wxContactName},</if>
+            <if test="wxRoomId != null">wx_room_id = #{wxRoomId},</if>
+            <if test="wxRoomName != null">wx_room_name = #{wxRoomName},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="sendStatus != null">send_status = #{sendStatus},</if>
+            <if test="sendRemark != null">send_remark = #{sendRemark},</if>
+            <if test="sendSort != null">send_sort = #{sendSort},</if>
+            <if test="expirationTime != null">expiration_time = #{expirationTime},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopLogsById" parameterType="Long">
+        delete from wx_sop_logs where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopLogsByIds" parameterType="String">
+        delete from wx_sop_logs where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <resultMap type="com.fs.wx.sop.vo.WxSopLogsListVO" id="WxSopLogsListVOResult">
+        <result property="id"    column="id"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="accountName"    column="account_name"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="wxContactName"    column="wx_contact_name"    />
+        <result property="tagNames"    column="tag_names"    />
+        <result property="wxRoomId"    column="wx_room_id"    />
+        <result property="wxRoomName"    column="wx_room_name"    />
+        <result property="type"    column="type"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="generateType"    column="generate_type"    />
+        <result property="sendStatus"    column="send_status"    />
+        <result property="sendRemark"    column="send_remark"    />
+        <result property="sendSort"    column="send_sort"    />
+        <result property="expirationTime"    column="expiration_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="realSendTime"    column="real_send_time"    />
+        <result property="remark"    column="remark"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="sendTime"    column="send_time"    />
+        <result property="contentJson"    column="content_json"    />
+    </resultMap>
+
+    <select id="selectWxSopLogsListBySopId" parameterType="com.fs.wx.sop.params.WxSopLogsParam" resultMap="WxSopLogsListVOResult">
+        SELECT
+            wsl.id, wsl.sop_id, wsl.sop_user_id, wsl.account_id,
+            wsl.wx_contact_id, wsl.wx_contact_name, wsui.tag_names,
+            wsl.wx_room_id, wsl.wx_room_name, wsl.type, wsl.send_type,
+            wsl.generate_type, wsl.send_status, wsl.send_remark, wsl.send_sort,
+            wsl.expiration_time, wsl.create_time, wsl.update_time as real_send_time,
+            wsl.remark, wsl.fs_user_id, wsl.send_time, wsl.content_json
+        FROM wx_sop_logs wsl
+        LEFT JOIN wx_sop_user_info wsui ON wsl.sop_id = wsui.sop_id AND wsl.wx_contact_id = wsui.wx_contact_id
+        <where>
+            <if test="sopId != null">AND wsl.sop_id = #{sopId}</if>
+            <if test="sopUserId != null">AND wsl.sop_user_id = #{sopUserId}</if>
+            <if test="accountId != null">AND wsl.account_id = #{accountId}</if>
+            <if test="accountIdList != null and accountIdList.size() > 0">
+                AND wsl.account_id IN
+                <foreach collection="accountIdList" item="accId" open="(" separator="," close=")">
+                    #{accId}
+                </foreach>
+            </if>
+            <if test="wxContactName != null and wxContactName != ''">AND wsl.wx_contact_name LIKE CONCAT('%', #{wxContactName}, '%')</if>
+            <if test="wxContactId != null">AND wsl.wx_contact_id = #{wxContactId}</if>
+            <if test="sendStatus != null">AND wsl.send_status = #{sendStatus}</if>
+            <if test="sendType != null">AND wsl.send_type = #{sendType}</if>
+            <if test="type != null">AND wsl.type = #{type}</if>
+            <if test="scheduleStartTime != null and scheduleStartTime != ''">AND wsl.create_time >= #{scheduleStartTime}</if>
+            <if test="scheduleEndTime != null and scheduleEndTime != ''">AND wsl.create_time &lt;= #{scheduleEndTime}</if>
+        </where>
+        ORDER BY wsl.create_time DESC
+    </select>
+
+    <select id="selectByWxId" resultType="com.fs.wx.sop.domain.WxSopLogs">
+        select ql.*,
+               qs.name,
+               qs.expiry_time as expiryTime
+        from wx_sop_logs ql
+                 left join wx_sop qs on qs.id = ql.sop_id
+        where ql.account_id = #{id}
+          and ql.send_status = 0
+        <![CDATA[
+          and ql.send_time <= now()
+        ]]>
+        order by ql.send_time limit 30
+    </select>
+</mapper>

+ 180 - 0
fs-service/src/main/resources/mapper/wx/WxSopMapper.xml

@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopMapper">
+
+    <resultMap type="WxSop" id="WxSopResult">
+        <result property="id"    column="id"    />
+        <result property="name"    column="name"    />
+        <result property="filterType"    column="filter_type"    />
+        <result property="selectTags"    column="select_tags"    />
+        <result property="excludeTags"    column="exclude_tags"    />
+        <result property="tempId"    column="temp_id"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="isFixed"    column="is_fixed"    />
+        <result property="expiryTime"    column="expiry_time"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="accountIds"    column="account_ids"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="status"    column="status"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopVo">
+        select id, name, filter_type, select_tags, exclude_tags, temp_id, company_id, is_fixed, expiry_time, start_time, account_ids, create_time, create_by, update_time, update_by, status, remark from wx_sop
+    </sql>
+
+    <select id="selectWxSopList" parameterType="WxSop" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        <where>
+            <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
+            <if test="filterType != null "> and filter_type = #{filterType}</if>
+            <if test="selectTags != null  and selectTags != ''"> and select_tags = #{selectTags}</if>
+            <if test="excludeTags != null  and excludeTags != ''"> and exclude_tags = #{excludeTags}</if>
+            <if test="tempId != null  and tempId != ''"> and temp_id = #{tempId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="isFixed != null "> and is_fixed = #{isFixed}</if>
+            <if test="expiryTime != null "> and expiry_time = #{expiryTime}</if>
+            <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="accountIds != null  and accountIds != ''"> and account_ids = #{accountIds}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+
+    <select id="selectWxSopById" parameterType="Long" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectWxSopByIds" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <insert id="insertWxSop" parameterType="WxSop" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="name != null">name,</if>
+            <if test="filterType != null">filter_type,</if>
+            <if test="selectTags != null">select_tags,</if>
+            <if test="excludeTags != null">exclude_tags,</if>
+            <if test="tempId != null">temp_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="isFixed != null">is_fixed,</if>
+            <if test="expiryTime != null">expiry_time,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="accountIds != null">account_ids,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="status != null">status,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="name != null">#{name},</if>
+            <if test="filterType != null">#{filterType},</if>
+            <if test="selectTags != null">#{selectTags},</if>
+            <if test="excludeTags != null">#{excludeTags},</if>
+            <if test="tempId != null">#{tempId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="isFixed != null">#{isFixed},</if>
+            <if test="expiryTime != null">#{expiryTime},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="accountIds != null">#{accountIds},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="status != null">#{status},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSop" parameterType="WxSop">
+        update wx_sop
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="name != null">name = #{name},</if>
+            <if test="filterType != null">filter_type = #{filterType},</if>
+            <if test="selectTags != null">select_tags = #{selectTags},</if>
+            <if test="excludeTags != null">exclude_tags = #{excludeTags},</if>
+            <if test="tempId != null">temp_id = #{tempId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="isFixed != null">is_fixed = #{isFixed},</if>
+            <if test="expiryTime != null">expiry_time = #{expiryTime},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="accountIds != null">account_ids = #{accountIds},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <update id="updateStatusWxSopByIds" parameterType="map">
+        update wx_sop
+        SET status = #{arg1}
+        where id in
+        <foreach collection="arg0" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
+    <delete id="deleteWxSopById" parameterType="Long">
+        delete from wx_sop where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopByIds">
+        delete from wx_sop where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectFilterWxSopCustomers" parameterType="com.fs.sop.params.WxSopTagsParam" resultType="com.fs.sop.vo.WxFilterSopCustomersResult">
+        SELECT DISTINCT
+            wc.id,
+            wc.nick_name AS name,
+            wc.account_id AS accountId,
+            wc.company_id AS cuCompanyId,
+            wc.company_user_id AS cuCompanyUserId,
+            cc.customer_id AS customerId
+        FROM
+            wx_contact wc
+        INNER JOIN crm_customer cc ON wc.customer_id = cc.customer_id
+        WHERE
+            1 = 1
+            <if test="accountIdsSelectList != null and accountIdsSelectList.size() > 0">
+                AND wc.account_id IN
+                <foreach collection="accountIdsSelectList" item="accountId" open="(" separator="," close=")">
+                    #{accountId}
+                </foreach>
+            </if>
+            <if test="tagsIdsSelectList != null and tagsIdsSelectList.size() > 0">
+                <foreach collection="tagsIdsSelectList" item="tagId">
+                    AND FIND_IN_SET(
+                        (SELECT dict_label FROM sys_dict_data WHERE dict_type = 'crm_customer_tag' AND dict_value = #{tagId} LIMIT 1),
+                        cc.tags
+                    )
+                </foreach>
+            </if>
+            <if test="outTagsIdsSelectList != null and outTagsIdsSelectList.size() > 0">
+                <foreach collection="outTagsIdsSelectList" item="excludeTagId">
+                    AND NOT FIND_IN_SET(
+                        (SELECT dict_label FROM sys_dict_data WHERE dict_type = 'crm_customer_tag' AND dict_value = #{excludeTagId} LIMIT 1),
+                        cc.tags
+                    )
+                </foreach>
+            </if>
+    </select>
+</mapper>

+ 144 - 0
fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml

@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopUserInfoMapper">
+
+    <resultMap type="WxSopUserInfo" id="WxSopUserInfoResult">
+        <result property="id"    column="id"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="customerId"    column="customer_id"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="isDaysNotStudy"    column="is_days_not_study"    />
+        <result property="finishCout"    column="finish_cout"    />
+        <result property="finishTime"    column="finish_time"    />
+        <result property="finishCourseDays"    column="finish_course_days"    />
+        <result property="grade"    column="grade"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+        <result property="tagNames"    column="tag_names"    />
+    </resultMap>
+
+    <sql id="selectWxSopUserInfoVo">
+        select wsui.id, wsui.sop_id, wsui.sop_user_id, wsui.wx_contact_id, wsui.customer_id, wsui.fs_user_id, wsui.is_days_not_study,
+               wsui.finish_cout, wsui.finish_time, wsui.finish_course_days, wsui.grade, wsui.status,
+               wsui.create_time, wsui.create_by, wsui.update_time, wsui.update_by, wsui.remark
+        from wx_sop_user_info wsui
+    </sql>
+
+    <select id="selectWxSopUserInfoList" parameterType="WxSopUserInfo" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        <where>
+            <if test="sopId != null "> and wsui.sop_id = #{sopId}</if>
+            <if test="sopUserId != null "> and wsui.sop_user_id = #{sopUserId}</if>
+            <if test="wxContactId != null "> and wsui.wx_contact_id = #{wxContactId}</if>
+            <if test="customerId != null "> and wsui.customer_id = #{customerId}</if>
+            <if test="fsUserId != null "> and wsui.fs_user_id = #{fsUserId}</if>
+            <if test="isDaysNotStudy != null "> and wsui.is_days_not_study = #{isDaysNotStudy}</if>
+            <if test="finishCout != null "> and wsui.finish_cout = #{finishCout}</if>
+            <if test="finishTime != null "> and wsui.finish_time = #{finishTime}</if>
+            <if test="finishCourseDays != null "> and wsui.finish_course_days = #{finishCourseDays}</if>
+            <if test="grade != null "> and wsui.grade = #{grade}</if>
+            <if test="status != null "> and wsui.status = #{status}</if>
+        </where>
+    </select>
+
+    <select id="selectWxSopUserInfoById" parameterType="Long" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        where wsui.id = #{id}
+    </select>
+
+    <insert id="insertWxSopUserInfo" parameterType="WxSopUserInfo" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_user_info
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">sop_id,</if>
+            <if test="sopUserId != null">sop_user_id,</if>
+            <if test="wxContactId != null">wx_contact_id,</if>
+            <if test="customerId != null">customer_id,</if>
+            <if test="fsUserId != null">fs_user_id,</if>
+            <if test="isDaysNotStudy != null">is_days_not_study,</if>
+            <if test="finishCout != null">finish_cout,</if>
+            <if test="finishTime != null">finish_time,</if>
+            <if test="finishCourseDays != null">finish_course_days,</if>
+            <if test="grade != null">grade,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+            <if test="tagNames != null">tag_names,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="sopId != null">#{sopId},</if>
+            <if test="sopUserId != null">#{sopUserId},</if>
+            <if test="wxContactId != null">#{wxContactId},</if>
+            <if test="customerId != null">#{customerId},</if>
+            <if test="fsUserId != null">#{fsUserId},</if>
+            <if test="isDaysNotStudy != null">#{isDaysNotStudy},</if>
+            <if test="finishCout != null">#{finishCout},</if>
+            <if test="finishTime != null">#{finishTime},</if>
+            <if test="finishCourseDays != null">#{finishCourseDays},</if>
+            <if test="grade != null">#{grade},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="tagNames != null">#{tagNames},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSopUserInfo" parameterType="WxSopUserInfo">
+        update wx_sop_user_info
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="sopUserId != null">sop_user_id = #{sopUserId},</if>
+            <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
+            <if test="customerId != null">customer_id = #{customerId},</if>
+            <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
+            <if test="isDaysNotStudy != null">is_days_not_study = #{isDaysNotStudy},</if>
+            <if test="finishCout != null">finish_cout = #{finishCout},</if>
+            <if test="finishTime != null">finish_time = #{finishTime},</if>
+            <if test="finishCourseDays != null">finish_course_days = #{finishCourseDays},</if>
+            <if test="grade != null">grade = #{grade},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="tagNames != null">tag_names = #{tagNames},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopUserInfoById" parameterType="Long">
+        delete from wx_sop_user_info where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopUserInfoByIds" parameterType="String">
+        delete from wx_sop_user_info where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectWxSopUserInfoByCondition" parameterType="WxSopUserInfo" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        <where>
+            <if test="sopId != null">and wsui.sop_id = #{sopId}</if>
+            <if test="sopUserId != null">and wsui.sop_user_id = #{sopUserId}</if>
+            <if test="wxContactId != null">and wsui.wx_contact_id = #{wxContactId}</if>
+            <if test="customerId != null">and wsui.customer_id = #{customerId}</if>
+        </where>
+        limit 1
+    </select>
+</mapper>

+ 150 - 0
fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml

@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.wx.sop.mapper.WxSopUserMapper">
+
+    <resultMap type="WxSopUser" id="WxSopUserResult">
+        <result property="id"    column="id"    />
+        <result property="type"    column="type"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="startTime"    column="start_time"    />
+        <result property="chatId"    column="chat_id"    />
+        <result property="status"    column="status"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="updateBy"    column="update_by"    />
+        <result property="remark"    column="remark"    />
+    </resultMap>
+
+    <sql id="selectWxSopUserVo">
+        select id, type, sop_id, account_id, start_time, chat_id, status, create_time, create_by, update_time, update_by, remark from wx_sop_user
+    </sql>
+
+    <select id="selectWxSopUserList" parameterType="WxSopUser" resultMap="WxSopUserResult">
+        SELECT
+        w.id,w.type, w.sop_id, w.account_id, w.start_time, w.chat_id, w.status,
+        w.create_time, w.create_by, w.update_time, w.update_by, w.remark
+        FROM wx_sop_user w
+        <where>
+            <if test="type != null "> and type = #{type}</if>
+            <if test="sopId != null "> and sop_id = #{sopId}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="accountName != null "> and account_name = #{accountName}</if>
+            <if test="startTime != null "> and start_time = #{startTime}</if>
+            <if test="chatId != null  and chatId != ''"> and chat_id = #{chatId}</if>
+            <if test="status != null "> and status = #{status}</if>
+        </where>
+    </select>
+
+    <select id="selectWxSopUserById" parameterType="Long" resultMap="WxSopUserResult">
+        <include refid="selectWxSopUserVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertWxSopUser" parameterType="WxSopUser" useGeneratedKeys="true" keyProperty="id">
+        insert into wx_sop_user
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="type != null">type,</if>
+            <if test="sopId != null">sop_id,</if>
+            <if test="accountId != null">account_id,</if>
+            <if test="startTime != null">start_time,</if>
+            <if test="chatId != null">chat_id,</if>
+            <if test="status != null">status,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="type != null">#{type},</if>
+            <if test="sopId != null">#{sopId},</if>
+            <if test="accountId != null">#{accountId},</if>
+            <if test="startTime != null">#{startTime},</if>
+            <if test="chatId != null">#{chatId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+         </trim>
+    </insert>
+
+    <update id="updateWxSopUser" parameterType="WxSopUser">
+        update wx_sop_user
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="type != null">type = #{type},</if>
+            <if test="sopId != null">sop_id = #{sopId},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
+            <if test="startTime != null">start_time = #{startTime},</if>
+            <if test="chatId != null">chat_id = #{chatId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteWxSopUserById" parameterType="Long">
+        delete from wx_sop_user where id = #{id}
+    </delete>
+
+    <delete id="deleteWxSopUserByIds" parameterType="String">
+        delete from wx_sop_user where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <delete id="deleteBySopId" parameterType="Long">
+        delete from wx_sop_user where sop_id = #{sopId}
+    </delete>
+
+    <select id="selectwxSopUser" parameterType="WxSopUser" resultMap="WxSopUserResult">
+        <include refid="selectWxSopUserVo"/>
+        <where>
+            <if test="sopId != null">and sop_id = #{sopId}</if>
+            <if test="type != null">and type = #{type}</if>
+            <if test="accountId != null">and account_id = #{accountId}</if>
+            <if test="startTime != null">and start_time = #{startTime}</if>
+        </where>
+        LIMIT 1
+    </select>
+
+    <select id="selectActiveWxSopUserForMsgGen" resultType="com.fs.wx.sop.vo.WxSopUserMsgGenVO">
+        SELECT
+            wsu.id AS sopUserId,
+            wsu.type,
+            wsu.sop_id AS sopId,
+            wsu.account_id AS accountId,
+            wsu.start_time AS startTime,
+            wsu.chat_id AS chatId,
+            ws.temp_id AS tempId,
+            ws.company_id AS companyId,
+            wsui.id AS infoId,
+            wsui.wx_contact_id AS wxContactId,
+            wsui.customer_id AS customerId,
+            wsui.fs_user_id AS fsUserId
+        FROM wx_sop_user wsu
+        INNER JOIN wx_sop ws ON wsu.sop_id = ws.id AND ws.status IN (1, 2)
+        INNER JOIN wx_sop_user_info wsui ON wsu.id = wsui.sop_user_id AND wsui.status = 0
+        WHERE wsu.start_time &lt;= CURDATE()
+          AND wsu.status = 0
+        ORDER BY wsu.id ASC, wsui.id ASC
+    </select>
+
+    <update id="updateWxSopUserDateById" parameterType="com.fs.wx.sop.params.UpdateWxSopUserLogDateVo">
+        update wx_sop_user set start_time = #{newStartTime}, update_time = NOW()
+        where id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+</mapper>

+ 2 - 1
fs-wx-api/src/main/java/com/fs/app/controller/CommonController.java

@@ -8,6 +8,7 @@ import com.fs.app.websocket.bean.ResultMsgVo;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.company.domain.CompanyWxAccount;
 import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.his.utils.PhoneUtil;
 import com.fs.wxcid.domain.WxContact;
 import com.fs.wxcid.mapper.WxContactMapper;
 import com.fs.wxcid.vo.wxvo.AddWxVo;
@@ -61,7 +62,7 @@ public class CommonController {
         WxContact wxContact = new WxContact();
         wxContact.setRemark(remark);
         wxContact.setNickName("测试1");
-        wxContact.setPhone(phone);
+        wxContact.setPhone(PhoneUtil.decryptPhone(phone));
         wxContact.setAccountId(companyWxAccount.getId());
         wxContact.setCompanyId(companyWxAccount.getCompanyId());
         wxContact.setCompanyUserId(companyWxAccount.getCompanyUserId());

+ 11 - 6
fs-wx-api/src/main/java/com/fs/app/websocket/service/WebSocketServer.java

@@ -52,7 +52,7 @@ import java.util.concurrent.Executor;
 
 @Slf4j
 @Component
-@ServerEndpoint("/app/webSocket/{wxId}")
+@ServerEndpoint("/app/webSocket/{wxId}/{tenantCode}")
 public class WebSocketServer {
 
     //concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
@@ -137,6 +137,10 @@ public class WebSocketServer {
     @OnMessage
     public void onMessage(String message, @PathParam(value = "wxId") String wxId, @PathParam(value = "tenantCode") String tenantCode) {
         Boolean switchBool = switchDataBaseByTenantCode(tenantCode);
+        if(!switchBool){
+            log.error("{} 微信连接解决,微检测到租户信息!!", wxId);
+            return;
+        }
         try {
             SendMsgVo msg = JSONObject.parseObject(message, SendMsgVo.class);
             if (msg.getType() == 0) {
@@ -160,11 +164,11 @@ public class WebSocketServer {
                         break;
                     case SYNC_CONTACT_PERSON:
                         ContactInfoVo contactInfoVo = JSON.parseObject(msg.getDataJson(), ContactInfoVo.class);
-                        if (contactInfoVo == null || StringUtils.isEmpty(contactInfoVo.getRemark())) {
+                        if (contactInfoVo == null || StringUtils.isEmpty(contactInfoVo.getWxNo())) {
                             log.error("{}同步数据失败,数据缺失:{}", wxId, contactInfoVo);
                             return;
                         }
-                        WxContact contact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", contactInfoVo.getRemark()));
+                        WxContact contact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("user_name", contactInfoVo.getWxNo()).eq("user_name", contactInfoVo.getWxNo()));
                         if (contact != null) {
                             contact.setNickName(contactInfoVo.getNickName());
                             contact.setCity(contactInfoVo.getAddress());
@@ -202,10 +206,11 @@ public class WebSocketServer {
                     case ADD_WX_RESULT:
                         com.fs.wxcid.vo.wxvo.AddResultWxVo addResultWxVo = JSON.parseObject(msg.getDataJson(), com.fs.wxcid.vo.wxvo.AddResultWxVo.class);
                         log.info("接收到加好友回调:{}", addResultWxVo);
-                        WxContact wxContact = wxContactMapper.selectOne(new QueryWrapper<WxContact>().eq("remark", addResultWxVo.getRemark()).eq("friends", 0));
+                        JSONObject jsonObject = JSONObject.parseObject(addResultWxVo.getBizJson());
+                        WxContact wxContact = wxContactMapper.selectById(jsonObject.getLong("wxContactId"));
                         log.info("更新联系人:{}", wxContact);
                         wxContact.setFriends(1);
-                        wxContact.setAlias(addResultWxVo.getWxid());
+                        wxContact.setUserName(addResultWxVo.getWxid());
                         wxContactMapper.updateById(wxContact);
                         List<CompanyWxClient> clients = companyWxClientMapper.selectWxV2(companyWxAccount.getId(), wxContact.getPhone());
                         log.info("更新联系人2:{}", clients);
@@ -215,7 +220,7 @@ public class WebSocketServer {
                                 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                                     try {
                                         e.setIsAdd(1);
-                                        e.setRemark(addResultWxVo.getRemark());
+                                        e.setWxNo(addResultWxVo.getWxid());
                                         e.setWxName(addResultWxVo.getUserName());
                                         e.setSuccessAddTime(LocalDateTime.now());
                                         companyWxClientMapper.updateById(e);

+ 146 - 0
fs-wx-ipad-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-ipad-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-starter-test</artifactId>
+        </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-ipad-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(FsWxIpadTaskApplication.class);
+    }
+}

+ 25 - 0
fs-wx-ipad-task/src/main/java/com/fs/FsWxIpadTaskApplication.java

@@ -0,0 +1,25 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+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 FsWxIpadTaskApplication
+{
+    public static void main(String[] args){
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(FsWxIpadTaskApplication.class, args);
+        System.out.println("IpadTask启动成功");
+    }
+}

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

+ 82 - 0
fs-wx-ipad-task/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,82 @@
+package com.fs.app.exception;
+
+
+
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import lombok.extern.slf4j.Slf4j;
+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;
+
+
+/**
+ * 异常处理器
+ */
+@Slf4j
+@RestControllerAdvice
+public class FSExceptionHandler {
+
+	/**
+	 * 处理自定义异常
+	 */
+	@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) {
+		log.error(e.getMessage(), e);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		log.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+
+
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		log.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		log.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());
+	}
+}

+ 34 - 0
fs-wx-ipad-task/src/main/java/com/fs/app/service/CustomThreadPoolConfig.java

@@ -0,0 +1,34 @@
+package com.fs.app.service;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * @author MixLiu
+ * @date 2025/7/11 上午11:04)
+ */
+@Configuration
+public class CustomThreadPoolConfig {
+    @Bean(name = "customThreadPool", destroyMethod = "shutdown")
+    public ThreadPoolTaskExecutor customThreadPool() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 核心线程数
+        executor.setCorePoolSize(300);
+        // 最大线程数
+        executor.setMaxPoolSize(300);
+        // 线程名前缀
+        executor.setThreadNamePrefix("custom-pool-");
+        // 拒绝策略:直接丢弃新任务
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
+        // 非核心线程空闲存活时间(秒)
+        executor.setKeepAliveSeconds(60);
+        // 等待所有任务完成后关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        // 初始化
+        executor.initialize();
+        return executor;
+    }
+}

+ 65 - 0
fs-wx-ipad-task/src/main/java/com/fs/app/service/WxIpadSendServer.java

@@ -0,0 +1,65 @@
+package com.fs.app.service;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.ipad.WxIpadSendUtils;
+import com.fs.ipad.vo.*;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.vo.WxSopMsgVo;
+import com.fs.wxcid.domain.WxContact;
+import com.fs.wxcid.mapper.WxContactMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class WxIpadSendServer {
+
+    private final CompanyWxAccountMapper companyWxAccountMapper;
+    private final WxIpadSendUtils wxIpadSendUtils;
+    private final WxContactMapper wxContactMapper;
+
+
+    public void send(WxSopMsgVo content, CompanyWxAccount account, WxSopLogs logs) {
+        WxBaseVo vo = new WxBaseVo();
+        vo.setId(logs.getId());
+        vo.setServerId(account.getServerId());
+        vo.setRemark(account.getWxNo());
+        WxContact wxContact = wxContactMapper.selectById(logs.getWxContactId());
+        vo.setUserRemark(wxContact.getNickName());
+        try {
+            content.setSendStatus(1);
+            switch (content.getContentType()) {
+                case 1:
+                    sendTxt(vo, content);
+                    break;
+                default:
+                    log.error("SOP_LOG_ID:{}错误的发送类型: {}", logs.getId(), content.getContentType());
+                    break;
+            }
+        } catch (Exception e) {
+            log.error("发送失败QW_SOP_ID:{},content:{},vo:{}", logs.getId(), JSON.toJSONString(content), JSON.toJSONString(vo), e);
+            content.setSendStatus(2);
+            content.setSendRemarks("发送失败:" + e.getMessage());
+        }
+    }
+
+    public void sendTxt(WxBaseVo vo, WxSopMsgVo content){
+        wxIpadSendUtils.sendTxt(vo, content.getValue());
+    }
+
+    public boolean isSend(CompanyWxAccount account) {
+        CompanyWxAccount wxAccount = companyWxAccountMapper.selectById(account.getId());
+        if(wxAccount.getServerId() == null || wxAccount.getLoginStatus() == 0){
+            log.info("微信:{},离线", wxAccount.getWxNickName());
+            return false;
+        }
+        return true;
+    }
+}

+ 256 - 0
fs-wx-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -0,0 +1,256 @@
+package com.fs.app.task;
+
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.app.service.WxIpadSendServer;
+import com.fs.common.annotation.TenantDataScope;
+import com.fs.common.core.redis.RedisCacheTenant;
+import com.fs.common.utils.PubFun;
+import com.fs.company.domain.CompanyWxAccount;
+import com.fs.company.mapper.CompanyWxAccountMapper;
+import com.fs.framework.aspectj.SopTenantDataSourceAspect;
+import com.fs.qw.vo.QwSopCourseFinishTempSetting;
+import com.fs.wxcid.domain.CidIpadServer;
+import com.fs.wxcid.mapper.CidIpadServerMapper;
+import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.mapper.WxSopLogsMapper;
+import com.fs.wx.sop.service.IWxSopLogsService;
+import com.fs.wx.sop.vo.WxSopMsgVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Component
+@Slf4j
+public class SendMsg {
+
+    private final CompanyWxAccountMapper companyWxAccountMapper;
+    private final CidIpadServerMapper ipadServerMapper;
+    private final WxSopLogsMapper wxSopLogsMapper;
+    private final IWxSopLogsService wxSopLogsService;
+    private final WxIpadSendServer sendServer;
+    private final RedisCacheTenant<Long> redisCache;
+
+    private final SopTenantDataSourceAspect sopTenantDataSourceAspect;
+
+    @Value("${group-no}")
+    private String groupNo;
+    @Value("${tenant-id}")
+    private Long tenantId;
+
+    private final List<CompanyWxAccount> qwUserList = Collections.synchronizedList(new ArrayList<>());
+    private final Map<Long, TaskContext> qwMap = new ConcurrentHashMap<>();
+    private static final long TASK_TIMEOUT_MS = 3 * 60 * 1000L;
+
+    public SendMsg(CompanyWxAccountMapper companyWxAccountMapper, CidIpadServerMapper ipadServerMapper, WxSopLogsMapper wxSopLogsMapper, IWxSopLogsService wxSopLogsService, WxIpadSendServer sendServer, RedisCacheTenant<Long> redisCache, SopTenantDataSourceAspect sopTenantDataSourceAspect) {
+        this.companyWxAccountMapper = companyWxAccountMapper;
+        this.ipadServerMapper = ipadServerMapper;
+        this.wxSopLogsMapper = wxSopLogsMapper;
+        this.wxSopLogsService = wxSopLogsService;
+        this.sendServer = sendServer;
+        this.redisCache = redisCache;
+        this.sopTenantDataSourceAspect = sopTenantDataSourceAspect;
+    }
+
+    private static class TaskContext {
+        final long startTime;
+        final AtomicBoolean cancelled;
+
+        TaskContext() {
+            this.startTime = System.currentTimeMillis();
+            this.cancelled = new AtomicBoolean(false);
+        }
+
+        boolean isTimeout() {
+            return System.currentTimeMillis() - startTime > TASK_TIMEOUT_MS;
+        }
+
+        void cancel() {
+            cancelled.set(true);
+        }
+
+        boolean isCancelled() {
+            return cancelled.get();
+        }
+    }
+
+    @Autowired
+    @Qualifier("customThreadPool")
+    private ThreadPoolTaskExecutor customThreadPool;
+
+    private List<CompanyWxAccount> getUserList() {
+        if (qwUserList.isEmpty()) {
+            List<CidIpadServer> serverList = ipadServerMapper.selectList(new QueryWrapper<CidIpadServer>().eq("group_no", groupNo));
+            if (serverList.isEmpty()) {
+                log.info("没找到可用的服务器 {} ", serverList);
+                return new ArrayList<>();
+            }
+            List<Long> serverIds = PubFun.listToNewList(serverList, CidIpadServer::getId);
+            List<CompanyWxAccount> qwUsers = companyWxAccountMapper.selectList(new QueryWrapper<CompanyWxAccount>().eq("server_status", 1).eq("login_status", 1).in("server_id", serverIds));
+            qwUserList.addAll(qwUsers);
+        }
+        log.info("getQwUserList {}", JSON.toJSONString(qwUserList));
+        return qwUserList;
+    }
+
+    @Scheduled(fixedRate = 50000) // 每50秒执行一次
+    public void refulsQwUserList() {
+        qwUserList.clear();
+    }
+
+    @Scheduled(fixedDelay = 20000) // 每20秒执行一次
+    public void sendMsg2() {
+        sopTenantDataSourceAspect.switchTenant(tenantId);
+        log.info("执行日志:{}", LocalDateTime.now());
+        if (StringUtils.isEmpty(groupNo)) {
+            log.error("corpId为空不执行");
+            return;
+        }
+        int delayStart = 1000;
+        int delayEnd = 2000;
+        getUserList().forEach(e -> {
+            TaskContext ctx = qwMap.get(e.getId());
+            if (ctx != null) {
+                if (ctx.isTimeout()) {
+                    log.warn("任务超时,标记取消:{}, 已运行: {}ms", e.getWxNickName(), System.currentTimeMillis() - ctx.startTime);
+                    ctx.cancel();
+                } else {
+                    log.debug("任务正在执行中,跳过:{}", e.getWxNickName());
+                    return;
+                }
+            }
+            if (customThreadPool.getActiveCount() >= customThreadPool.getMaxPoolSize()) {
+                log.warn("线程池已满,跳过任务:{}, 活跃线程: {}/{}", e.getWxNickName(), customThreadPool.getActiveCount(), customThreadPool.getMaxPoolSize());
+                return;
+            }
+            TaskContext newCtx = new TaskContext();
+            qwMap.put(e.getId(), newCtx);
+            CompletableFuture.runAsync(() -> {
+                try {
+                    log.info("开始任务:{}", e.getWxNickName());
+                    sopTenantDataSourceAspect.switchTenant(tenantId);
+                    processUser(e, delayStart, delayEnd, newCtx);
+                } catch (Exception exception) {
+                    log.error("发送错误:", exception);
+                } finally {
+                    log.info("删除任务:{}", e.getWxNickName());
+                    sopTenantDataSourceAspect.clear();
+                    qwMap.remove(e.getId());
+                }
+            }, customThreadPool).exceptionally(ex -> {
+                log.error("任务提交失败:{}, 错误: {}", e.getWxNickName(), ex.getMessage());
+                qwMap.remove(e.getId());
+                return null;
+            });
+        });
+    }
+
+    /**
+     * 发送任务执行
+     *
+     * @param account    发送企微
+     * @param delayStart 随机延迟 最小值
+     * @param delayEnd   随机延迟 最大值
+     * @param ctx        任务上下文(用于取消检查)
+     */
+    private void processUser(CompanyWxAccount account, int delayStart, int delayEnd, TaskContext ctx) {
+        long start1 = System.currentTimeMillis();
+        List<WxSopLogs> qwSopLogList = wxSopLogsMapper.selectByWxId(account.getId());
+        if (qwSopLogList.isEmpty()) {
+            log.info("获取当前企微待发送记录为空");
+            return;
+        }
+        long end1 = System.currentTimeMillis();
+        if (ctx.isCancelled()) {
+            log.info("任务被取消,退出:{}", account.getWxNickName());
+            return;
+        }
+        if (!sendServer.isSend(account)) {
+            log.info("当前这个微信不需要发送 数据{}", account);
+            return;
+        }
+        log.info("销售:{}, 消息:{}, 耗时: {}, 时间:{}", account.getWxNickName(), qwSopLogList.size(), end1 - start1, ctx.startTime);
+        long start3 = System.currentTimeMillis();
+        for (WxSopLogs qwSopLogs : qwSopLogList) {
+            if (ctx.isCancelled()) {
+                log.info("任务被取消,中断发送:{}, 已发送部分消息", account.getWxNickName());
+                return;
+            }
+            long start2 = System.currentTimeMillis();
+            log.info("进入发送消息状态:{}", qwSopLogs.getId());
+            String key = "qw:logs:pad:send:id:" + qwSopLogs.getId();
+            Long time = redisCache.getCacheObject(key);
+            redisCache.setCacheObject(key, System.currentTimeMillis(), 24, TimeUnit.HOURS);
+            List<WxSopMsgVo> setting = JSON.parseArray(qwSopLogs.getContentJson(), WxSopMsgVo.class);
+            for (WxSopMsgVo content : setting) {
+                if (ctx.isCancelled()) {
+                    log.info("任务被取消,中断发送:{}", account.getWxNickName());
+                    return;
+                }
+                long start4 = System.currentTimeMillis();
+                sendServer.send(content, account, qwSopLogs);
+                long end4 = System.currentTimeMillis();
+                log.info("请求pad发送完成:{}, {}, 时长4:{}", account.getWxNickName(), qwSopLogs.getId(), end4 - start4);
+                try {
+                    if (ctx.isCancelled()) {
+                        return;
+                    }
+                    int delay = ThreadLocalRandom.current().nextInt(300, 1000);
+                    log.debug("pad发送消息等待:{}ms", delay);
+                    Thread.sleep(delay);
+                } catch (InterruptedException e) {
+                    log.error("线程等待错误!");
+                    Thread.currentThread().interrupt();
+                    return;
+                }
+            }
+            qwSopLogs.setSend(true);
+            WxSopLogs updateQwSop = new WxSopLogs();
+            updateQwSop.setId(qwSopLogs.getId());
+            updateQwSop.setSendTime(LocalDateTime.now());
+            if (setting.stream().allMatch(e -> e.getSendStatus() == 2)) {
+                updateQwSop.setSendStatus(2);
+                updateQwSop.setRemark("全部发送失败");
+            } else if (setting.stream().anyMatch(e -> e.getSendStatus() == 2)) {
+                updateQwSop.setSendStatus(1);
+                updateQwSop.setRemark("部分发送失败");
+            } else {
+                updateQwSop.setSendStatus(1);
+                updateQwSop.setRemark("全部发送成功");
+            }
+            updateQwSop.setContentJson(JSON.toJSONString(setting));
+            long end2 = System.currentTimeMillis();
+            boolean i = wxSopLogsService.updateMapper(updateQwSop);
+            log.info("销售:{}, 修改条数{}, 发送方消息完成:{}, 耗时: {}", account.getWxNickName(), i, qwSopLogs.getId(), end2 - start2);
+            try {
+                if (ctx.isCancelled()) {
+                    return;
+                }
+                int delay = ThreadLocalRandom.current().nextInt(delayStart, delayEnd);
+                log.debug("企微发送消息等待:{}ms", delay);
+                Thread.sleep(delay);
+            } catch (InterruptedException e) {
+                log.error("线程等待错误!");
+                Thread.currentThread().interrupt();
+                return;
+            }
+        }
+        long end3 = System.currentTimeMillis();
+        log.info("销售执行完成:{}, 耗时:{}", account.getWxNickName(), end3 - start3);
+    }
+}

+ 171 - 0
fs-wx-ipad-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-ipad-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-ipad-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-ipad-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();
+    }
+}

+ 175 - 0
fs-wx-ipad-task/src/main/java/com/fs/framework/aspectj/SopTenantDataSourceAspect.java

@@ -0,0 +1,175 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.common.annotation.TenantDataScope;
+import com.fs.common.core.redis.RedisCacheT;
+import com.fs.common.enums.TenantIdType;
+import com.fs.common.exception.CustomException;
+import com.fs.framework.datasource.DynamicDataSource;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import lombok.extern.slf4j.Slf4j;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多数据源处理
+ *
+ */
+@Slf4j
+@Aspect
+@Order(1)
+@Component
+public class SopTenantDataSourceAspect {
+    private static final String TENANT_KEY = "tenant:info:";
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+    @Value("${tenant-id}")
+    private Long ymlTenantId;
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+    @Autowired
+    private RedisCacheT<TenantInfo> redis;
+    /**
+     * 租户数据源缓存
+     */
+    private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
+
+    @Pointcut("@annotation(com.fs.common.annotation.TenantDataScope)"
+            + "|| @within(com.fs.common.annotation.TenantDataScope)")
+    public void dsPointCut() {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method targetMethod = signature.getMethod(); // 拿到目标方法对象
+        log.info("执行方法:{}", targetMethod.getName());
+        TenantDataScope dataSource = getDataSource(point);
+        TenantIdType type = dataSource.type();
+        Long tenantId = 0L;
+        if(type.equals(TenantIdType.YML)){
+            tenantId = ymlTenantId;
+        }
+        if(type.equals(TenantIdType.REQUEST)){
+
+        }
+        switchTenant(tenantId);
+        try {
+            return point.proceed();
+        } finally {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    public void switchTenant(Long id) {
+        TenantInfo tenantInfo = redis.getCacheObject(TENANT_KEY + id);
+        if(tenantInfo == null){
+            tenantInfo = tenantInfoMapper.selectById(id);
+            if(tenantInfo == null){
+                throw new CustomException("租户不存在请检查");
+            }
+            redis.setCacheObject(TENANT_KEY + id, tenantInfo);
+        }
+        // 用租户主键作为唯一标识
+        String tenantKey = buildTenantKey(tenantInfo.getId());
+
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            synchronized (this) {
+                if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+
+                    javax.sql.DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+
+                    // 动态追加到已解析的数据源
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                }
+            }
+        }
+
+        // ThreadLocal 切库
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+
+
+    /**
+     * 清理 ThreadLocal
+     */
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    /**
+     * 创建租户数据源(MySQL + Druid)
+     */
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+
+        // 统一 MySQL
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+
+        ds.setInitialSize(5);
+        ds.setMinIdle(10);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+
+        return ds;
+    }
+
+    /**
+     * 反射获取 AbstractRoutingDataSource.resolvedDataSources
+     */
+    @SuppressWarnings("unchecked")
+    private Map<Object, DataSource> getResolvedDataSources() {
+        try {
+            Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
+                    .class.getDeclaredField("resolvedDataSources");
+            field.setAccessible(true);
+            return (Map<Object, DataSource>) field.get(dynamicDataSource);
+        } catch (Exception e) {
+            throw new IllegalStateException("获取 resolvedDataSources 失败", e);
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public TenantDataScope getDataSource(ProceedingJoinPoint point) {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        TenantDataScope dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), TenantDataScope.class);
+        if (Objects.nonNull(dataSource)) {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), TenantDataScope.class);
+    }
+}

+ 31 - 0
fs-wx-ipad-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-ipad-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-ipad-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;
+    }
+}

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

@@ -0,0 +1,115 @@
+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.read")
+    public DataSource sopReadDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.clickhouse")
+    public DataSource clickhouseDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.slave")
+    public DataSource slaveDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("clickhouseDataSource") DataSource clickhouseDataSource,
+                                        @Qualifier("masterDataSource") DataSource masterDataSource,
+                                        
+                                        @Qualifier("slaveDataSource") DataSource slaveDataSource,
+                                        @Qualifier("sopReadDataSource") DataSource sopReadDataSource
+                                        ) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
+
+        targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
+        
+        targetDataSources.put(DataSourceType.SopREAD.name(), sopReadDataSource);
+        targetDataSources.put(DataSourceType.CLICKHOUSE.name(), clickhouseDataSource); // Ensure matching key
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 123 - 0
fs-wx-ipad-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.druid.master")
+//    public DataSource masterDataSource(DruidProperties druidProperties)
+//    {
+//        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+//        return druidProperties.dataSource(dataSource);
+//    }
+//
+//    @Bean
+//    @ConfigurationProperties("spring.datasource.druid.slave")
+//    @ConditionalOnProperty(prefix = "spring.datasource.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.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-ipad-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-ipad-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-ipad-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-ipad-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();
+    }
+}

+ 76 - 0
fs-wx-ipad-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-ipad-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 {
+
+}

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

@@ -0,0 +1,158 @@
+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("**/errorLogUpload").anonymous()
+                .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-ipad-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-ipad-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();
+    }
+}

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

@@ -0,0 +1,102 @@
+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.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+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(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-ipad-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-ipad-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-ipad-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();
+    }
+}

+ 102 - 0
fs-wx-ipad-task/src/main/java/com/fs/framework/datasource/TenantDataSourceManager.java

@@ -0,0 +1,102 @@
+package com.fs.framework.datasource;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.mapper.TenantInfoMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class TenantDataSourceManager {
+
+    @Resource
+    private DynamicDataSource dynamicDataSource;
+    @Autowired
+    private TenantInfoMapper tenantInfoMapper;
+
+    /**
+     * 租户数据源缓存
+     */
+    private static final Map<String, DataSource> TENANT_DS_CACHE = new ConcurrentHashMap<>();
+
+    /**
+     * 切换到租户数据源(不存在则创建)
+     */
+    public void switchTenant(Long id) {
+        TenantInfo tenantInfo = tenantInfoMapper.selectById(id);
+        // 用租户主键作为唯一标识
+        String tenantKey = buildTenantKey(tenantInfo.getId());
+
+        if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+            synchronized (this) {
+                if (!TENANT_DS_CACHE.containsKey(tenantKey)) {
+
+                    DataSource tenantDs = createTenantDataSource(tenantInfo);
+                    TENANT_DS_CACHE.put(tenantKey, tenantDs);
+
+                    // 动态追加到已解析的数据源
+                    Map<Object, DataSource> resolvedMap = getResolvedDataSources();
+                    resolvedMap.put(tenantKey, tenantDs);
+                }
+            }
+        }
+
+        // ThreadLocal 切库
+        DynamicDataSourceContextHolder.setDataSourceType(tenantKey);
+    }
+
+    private String buildTenantKey(Long tenantId) {
+        return "tenant:" + tenantId;
+    }
+
+
+
+    /**
+     * 清理 ThreadLocal
+     */
+    public void clear() {
+        DynamicDataSourceContextHolder.clearDataSourceType();
+    }
+
+    /**
+     * 创建租户数据源(MySQL + Druid)
+     */
+    private DataSource createTenantDataSource(TenantInfo tenant) {
+
+        DruidDataSource ds = new DruidDataSource();
+        ds.setUrl(tenant.getDbUrl());
+        ds.setUsername(tenant.getDbAccount());
+        ds.setPassword(tenant.getDbPwd());
+
+        // 统一 MySQL
+        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
+
+        ds.setInitialSize(5);
+        ds.setMinIdle(10);
+        ds.setMaxActive(20);
+        ds.setMaxWait(60000);
+
+        return ds;
+    }
+
+    /**
+     * 反射获取 AbstractRoutingDataSource.resolvedDataSources
+     */
+    @SuppressWarnings("unchecked")
+    private Map<Object, DataSource> getResolvedDataSources() {
+        try {
+            Field field = org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
+                    .class.getDeclaredField("resolvedDataSources");
+            field.setAccessible(true);
+            return (Map<Object, DataSource>) field.get(dynamicDataSource);
+        } catch (Exception e) {
+            throw new IllegalStateException("获取 resolvedDataSources 失败", e);
+        }
+    }
+}

+ 115 - 0
fs-wx-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-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-ipad-task/src/main/resources/META-INF/spring-devtools.properties

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

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

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

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/1.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 1
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=1 --server.port=9001

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/10.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 10
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=9 --server.port=9010

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/11.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 11
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=9 --server.port=9011

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/2.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 2
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=2 --server.port=9002

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/3.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 3
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=3 --server.port=9003

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/4.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 4
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=4 --server.port=9004

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/5.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 5
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=5 --server.port=9005

+ 6 - 0
fs-wx-ipad-task/src/main/resources/bat/6.bat

@@ -0,0 +1,6 @@
+@echo off
+CLS 
+color 0a 
+TITLE 6
+
+java -jar C:\Tools\jar\send\fs-ipad-task.jar --spring.profiles.active=druid-yjf --spring.redis.password=Yzx19860213 --group-no=6 --server.port=9006

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است