yfh vor 1 Monat
Ursprung
Commit
2409d9303e
100 geänderte Dateien mit 7305 neuen und 280 gelöschten Zeilen
  1. 1 1
      fs-admin/Dockerfile
  2. 5 5
      fs-admin/src/main/java/com/fs/company/controller/CompanyController.java
  3. 13 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyDeptController.java
  4. 46 7
      fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java
  5. 4 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java
  6. 40 0
      fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java
  7. 60 0
      fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java
  8. 41 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  9. 28 18
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  10. 35 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java
  11. 8 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  12. 18 1
      fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java
  13. 27 1
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java
  14. 133 0
      fs-admin/src/main/java/com/fs/fastGpt/FastGptChatMsgController.java
  15. 116 0
      fs-admin/src/main/java/com/fs/fastGpt/FastGptChatMsgLogsController.java
  16. 121 0
      fs-admin/src/main/java/com/fs/fastGpt/FastGptChatSessionController.java
  17. 2 2
      fs-admin/src/main/java/com/fs/fastGpt/FastgptEventLogTotalController.java
  18. 20 0
      fs-admin/src/main/java/com/fs/fastGpt/GptRoleController.java
  19. 163 0
      fs-admin/src/main/java/com/fs/his/controller/FsAiWorkflowController.java
  20. 105 3
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java
  21. 15 0
      fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java
  22. 11 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderItemScrmController.java
  23. 46 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  24. 9 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java
  25. 2 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java
  26. 16 6
      fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java
  27. 39 4
      fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java
  28. 74 0
      fs-admin/src/main/java/com/fs/live/controller/FsWxExpressTaskController.java
  29. 28 0
      fs-admin/src/main/java/com/fs/live/controller/LiveMsgController.java
  30. 67 22
      fs-admin/src/main/java/com/fs/live/controller/OrderController.java
  31. 53 0
      fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java
  32. 155 0
      fs-admin/src/main/java/com/fs/qw/controller/QwGroupChatController.java
  33. 71 0
      fs-admin/src/main/java/com/fs/qw/controller/QwGroupChatUserController.java
  34. 43 0
      fs-admin/src/main/java/com/fs/qw/controller/QwPushCountController.java
  35. 9 8
      fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java
  36. 83 0
      fs-admin/src/main/java/com/fs/qw/controller/QwTagGroupController.java
  37. 713 83
      fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java
  38. 241 0
      fs-admin/src/main/java/com/fs/qw/controller/QwWorkTaskNewController.java
  39. 186 0
      fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java
  40. 5 0
      fs-common/src/main/java/com/fs/common/annotation/RateLimiter.java
  41. 16 0
      fs-common/src/main/java/com/fs/common/constant/RedisConstant.java
  42. 172 0
      fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java
  43. 367 0
      fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java
  44. 154 0
      fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java
  45. 23 0
      fs-common/src/main/java/com/fs/common/utils/poi/ExcelUtil.java
  46. 6 0
      fs-company-app/Dockerfile
  47. 1 1
      fs-company/Dockerfile
  48. 117 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowController.java
  49. 1 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyRedPacketBalanceLogsController.java
  50. 38 2
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  51. 11 3
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java
  52. 9 0
      fs-company/src/main/java/com/fs/company/controller/fastGpt/FastGptRoleController.java
  53. 19 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java
  54. 6 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java
  55. 1 1
      fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java
  56. 6 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  57. 108 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderItemScrmController.java
  58. 28 4
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  59. 151 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreScrmController.java
  60. 12 4
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  61. 25 20
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  62. 59 0
      fs-live-app/src/main/java/com/fs/live/utils/WebSocketRateLimiter.java
  63. 20 18
      fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java
  64. 63 0
      fs-live-app/src/main/java/com/fs/live/websocket/config/WebSocketSessionManager.java
  65. 94 60
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  66. 138 0
      fs-live-mq/pom.xml
  67. 14 0
      fs-live-mq/src/main/java/com/fs/FSServletInitializer.java
  68. 27 0
      fs-live-mq/src/main/java/com/fs/FsLiveMqApplication.java
  69. 12 0
      fs-live-mq/src/main/java/com/fs/app/annotation/Login.java
  70. 15 0
      fs-live-mq/src/main/java/com/fs/app/annotation/LoginUser.java
  71. 125 0
      fs-live-mq/src/main/java/com/fs/app/controller/AdCallbackController.java
  72. 37 0
      fs-live-mq/src/main/java/com/fs/app/controller/CommonController.java
  73. 215 0
      fs-live-mq/src/main/java/com/fs/app/controller/MockAppController.java
  74. 51 0
      fs-live-mq/src/main/java/com/fs/app/exception/FSException.java
  75. 81 0
      fs-live-mq/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  76. 49 0
      fs-live-mq/src/main/java/com/fs/app/mq/RocketMQConsumerService.java
  77. 182 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  78. 73 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  79. 245 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/LogAspect.java
  80. 117 0
      fs-live-mq/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  81. 31 0
      fs-live-mq/src/main/java/com/fs/framework/config/ApplicationConfig.java
  82. 85 0
      fs-live-mq/src/main/java/com/fs/framework/config/CaptchaConfig.java
  83. 92 0
      fs-live-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java
  84. 123 0
      fs-live-mq/src/main/java/com/fs/framework/config/DruidConfig.java
  85. 72 0
      fs-live-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  86. 59 0
      fs-live-mq/src/main/java/com/fs/framework/config/FilterConfig.java
  87. 76 0
      fs-live-mq/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  88. 150 0
      fs-live-mq/src/main/java/com/fs/framework/config/MyBatisConfig.java
  89. 158 0
      fs-live-mq/src/main/java/com/fs/framework/config/RedisConfig.java
  90. 65 0
      fs-live-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java
  91. 50 0
      fs-live-mq/src/main/java/com/fs/framework/config/SecurityConfig.java
  92. 33 0
      fs-live-mq/src/main/java/com/fs/framework/config/ServerConfig.java
  93. 121 0
      fs-live-mq/src/main/java/com/fs/framework/config/SwaggerConfig.java
  94. 63 0
      fs-live-mq/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  95. 77 0
      fs-live-mq/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  96. 27 0
      fs-live-mq/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  97. 45 0
      fs-live-mq/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  98. 56 0
      fs-live-mq/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  99. 126 0
      fs-live-mq/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  100. 56 0
      fs-live-mq/src/main/java/com/fs/framework/manager/AsyncManager.java

+ 1 - 1
fs-admin/Dockerfile

@@ -1,4 +1,4 @@
-FROM openjdk:8-jre
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
 # java版本,最好使用openjdk,而不是类似于Java:1.8
 COPY ./target/fs-admin.jar fs-admin.jar
 # 向外暴露的接口,最好与项目yml文件中的端口一致

+ 5 - 5
fs-admin/src/main/java/com/fs/company/controller/CompanyController.java

@@ -134,7 +134,7 @@ public class CompanyController extends BaseController
      * 新增企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:add')")
-    @Log(title = "企业", businessType = BusinessType.INSERT)
+    @Log(title = "企业", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping
     public R add(@RequestBody Company company)
     {
@@ -153,7 +153,7 @@ public class CompanyController extends BaseController
      * 修改企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:edit')")
-    @Log(title = "企业", businessType = BusinessType.UPDATE)
+    @Log(title = "企业", businessType = BusinessType.UPDATE, isStoreLog = true)
     @PutMapping
     public AjaxResult edit(@RequestBody Company company)
     {
@@ -186,7 +186,7 @@ public class CompanyController extends BaseController
      * 删除企业
      */
     @PreAuthorize("@ss.hasPermi('company:company:remove')")
-    @Log(title = "企业", businessType = BusinessType.DELETE)
+    @Log(title = "企业", businessType = BusinessType.DELETE, isStoreLog = true)
 	@DeleteMapping("/{companyIds}")
     public AjaxResult remove(@PathVariable Long[] companyIds)
     {
@@ -244,7 +244,7 @@ public class CompanyController extends BaseController
 
 
     @PreAuthorize("@ss.hasPermi('company:company:recharge')")
-    @Log(title = "企业转账", businessType = BusinessType.INSERT)
+    @Log(title = "企业转账", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping(value = "/recharge")
     @Transactional
     @RepeatSubmit
@@ -270,7 +270,7 @@ public class CompanyController extends BaseController
     }
 
     @PreAuthorize("@ss.hasPermi('company:company:deduct')")
-    @Log(title = "企业扣款", businessType = BusinessType.INSERT)
+    @Log(title = "企业扣款", businessType = BusinessType.INSERT, isStoreLog = true)
     @PostMapping(value = "/deduct")
     @Transactional
     @RepeatSubmit

+ 13 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyDeptController.java

@@ -100,4 +100,17 @@ public class CompanyDeptController extends BaseController
         List<CompanyDept> depts = companyDeptService.selectCompanyDeptList(dept);
         return AjaxResult.success(companyDeptService.buildDeptTreeSelect(depts));
     }
+
+    /**
+     * 获取部门下拉树列表
+     */
+    @GetMapping("/treeselectByCompanyId/{companyId}")
+    public AjaxResult treeselectByCompanyId(@PathVariable("companyId") Long companyId)
+    {
+        CompanyDept dept = new CompanyDept();
+        dept.setStatus("0");
+        dept.setCompanyId(companyId);
+        List<CompanyDept> depts = companyDeptService.selectCompanyDeptList(dept);
+        return AjaxResult.success(companyDeptService.buildDeptTreeSelect(depts));
+    }
 }

+ 46 - 7
fs-admin/src/main/java/com/fs/company/controller/CompanyStatisticsController.java

@@ -1,9 +1,12 @@
 package com.fs.company.controller;
 
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.Excel;
 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.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.TimeUtils;
 import com.fs.common.utils.poi.ExcelUtil;
@@ -14,6 +17,10 @@ import com.fs.company.service.ICompanySmsLogsService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.company.service.ICompanyVoiceLogsService;
 import com.fs.company.vo.*;
+import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFinishCourseStatisticsSyncService;
+import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.vo.FsCourseReportVO;
 import com.fs.crm.param.CrmCustomerStatisticsParam;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerVisitService;
@@ -29,15 +36,11 @@ import com.fs.his.vo.FsStoreOrderAmountStatsVo;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
+import java.lang.reflect.Field;
 import java.math.BigDecimal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -76,6 +79,12 @@ public class CompanyStatisticsController extends BaseController
     //app商城订单接口Service
     @Autowired
     private IFsStoreOrderScrmService fsStoreOrderScrmService;
+
+    @Autowired
+    private IFsCourseWatchLogService fsCourseWatchLogService;
+
+    @Autowired
+    private IFinishCourseStatisticsSyncService finishCourseStatisticsSyncService;
     @GetMapping("/storeOrder")
     public R storeOrder(FsStoreStatisticsParam param)
     {
@@ -751,4 +760,34 @@ public class CompanyStatisticsController extends BaseController
         return AjaxResult.success(scrmStatsVo);
     }
 
+    /**
+     * 木易华康特殊处理 课程完课统计数据
+     */
+    @GetMapping("/courseReport")
+    public TableDataInfo selectFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        startPage();
+        List<FsCourseReportVO> fsCourseReportVOS = fsCourseWatchLogService.selectFsCourseReportVO(param);
+        return getDataTable(fsCourseReportVOS);
+    }
+
+    @GetMapping("/exportFsCourseReportVO")
+    public AjaxResult exportFsCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
+        List<FsCourseReportVO> list = fsCourseWatchLogService.selectFsCourseReportVO(param);
+        List<String> allFields = Arrays.stream(FsCourseReportVO.class.getDeclaredFields())
+                .filter(field -> field.isAnnotationPresent(Excel.class))
+                .map(Field::getName)
+                .collect(Collectors.toList());
+        ExcelUtil<FsCourseReportVO> util = new ExcelUtil<FsCourseReportVO>(FsCourseReportVO.class);
+        return util.exportExcelSelectedColumns(list, "完课统计报表", allFields);
+    }
+
+    @PostMapping("/syncYesterday")
+    public AjaxResult syncYesterday() {
+        try {
+            finishCourseStatisticsSyncService.syncMultiDimensionStatistics();
+            return AjaxResult.success("同步昨天数据成功");
+        } catch (Exception e) {
+            return AjaxResult.error("同步失败:" + e.getMessage());
+        }
+    }
 }

+ 4 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyUserAllController.java

@@ -415,6 +415,8 @@ public class CompanyUserAllController extends BaseController {
     /**
      * 批量修改 销售的所属区域(临时的)
      */
+    @PreAuthorize("@ss.hasPermi('company:user:updateCompanyUserAreaList')")
+    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
     @PostMapping("/updateCompanyUserAreaList")
     public R updateCompanyUserAreaList(@RequestBody CompanyUserAreaParam param)
     {
@@ -432,6 +434,7 @@ public class CompanyUserAllController extends BaseController {
         return  R.ok().put("data",subDomain);
     }
 
+    @PreAuthorize("@ss.hasPermi('company:user:setRegister')")
     @Log(title = "设置是否需要单独注册会员", businessType = BusinessType.UPDATE)
     @PutMapping("/setRegister")
     public AjaxResult setIsRegisterMember(@RequestParam Boolean status, @RequestBody List<Long> userIds) {
@@ -443,6 +446,7 @@ public class CompanyUserAllController extends BaseController {
         }
     }
 
+    @PreAuthorize("@ss.hasPermi('company:user:allowedAllRegister')")
     @Log(title = "是否允许所有方式注册会员", businessType = BusinessType.UPDATE)
     @PutMapping("/allowedAllRegister")
     public AjaxResult isAllowedAllRegister(@RequestParam Boolean status, @RequestBody List<Long> userIds) {

+ 40 - 0
fs-admin/src/main/java/com/fs/company/controller/CompanyUserController.java

@@ -9,14 +9,21 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.his.vo.OptionsVO;
 import com.fs.qw.dto.UserProjectDTO;
+import com.fs.qw.vo.QwUserVO;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企业员工信息Controller
@@ -173,5 +180,38 @@ public class CompanyUserController extends BaseController
         List<CompanyUser> list = companyUserService.getCompanyUserList(user);
         return getDataTable(list);
     }
+    /**
+     * 根据销售名称模糊查询
+     * @param name  名称
+     * @return  list
+     */
+    @GetMapping("/getCompanyUserListLikeName")
+    public R getCompanyUserListLikeName(@RequestParam(required = false) String name,
+                                        @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                        @RequestParam(required = false, defaultValue = "10") Integer pageSize,
+                                        @RequestParam(required = false) Long companyId) {
+        Map<String,Object> params = new HashMap<>();
+        params.put("nickName", name);
+        //查询多条数据传入公司
+//        if (pageSize>=200){
+        params.put("companyId", companyId);
+//        }
+        PageHelper.startPage(pageNum, pageSize);
+        List<OptionsVO> companyUserList = companyUserService.selectCompanyUserListByMap(params);
+        return R.ok().put("data", new PageInfo<>(companyUserList));
+    }
+    @GetMapping("/getQwAllUserList/{id}/{companyId}")
+    public R getQwAllUserList(@PathVariable("id") String corpId,@PathVariable("companyId") Long companyId)
+    {
+
+        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId,companyId);
+        return  R.ok().put("data",list);
+    }
+    @GetMapping("/getCompanyList/{id}")
+    public R getCompanyList(@PathVariable("id") String corpId)
+    {
 
+        List<Company> list = companyUserService.getCompanyList(corpId);
+        return  R.ok().put("data",list);
+    }
 }

+ 60 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java

@@ -1,6 +1,7 @@
 package com.fs.course.controller;
 
 import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -19,11 +20,15 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.bean.BeanUtils;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
+import com.fs.course.enums.MiniProgramAgreementEnum;
 import com.fs.course.param.FsCoursePlaySourceConfigCreateParam;
 import com.fs.course.param.FsCoursePlaySourceConfigEditParam;
+import com.fs.course.param.MiniProgramAgreementParam;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.vo.FsCoursePlaySourceConfigVO;
 import com.fs.framework.web.service.TokenService;
+import com.fs.his.domain.MiniProgramAgreement;
+import com.fs.his.service.MiniProgramAgreementService;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import lombok.AllArgsConstructor;
@@ -42,6 +47,7 @@ public class FsCoursePlaySourceConfigController extends BaseController {
     private final IFsCoursePlaySourceConfigService fsCoursePlaySourceConfigService;
     private final TokenService tokenService;
     private final ISysConfigService configService;
+    private final MiniProgramAgreementService miniProgramAgreementService;
 
 //    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:list')")
     @GetMapping("/list")
@@ -183,4 +189,58 @@ public class FsCoursePlaySourceConfigController extends BaseController {
         }
         return R.ok().put("data", fsCoursePlaySourceConfigService.list(queryWrapper));
     }
+
+
+    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:agreement')")
+    @Log(title = "小程序协议配置", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateAgreementConfig")
+    public AjaxResult updateAgreementConfig(@RequestBody MiniProgramAgreementParam agreement) {
+        Map<String, String> map = (Map<String, String>) JSON.parse(agreement.getAgreementData());
+
+        List<MiniProgramAgreement> list = new ArrayList<>();
+        for (MiniProgramAgreementEnum agreementEnum : MiniProgramAgreementEnum.values()) {
+            String content = map.get(agreementEnum.getCode());
+            if (content != null) {
+                MiniProgramAgreement update = new MiniProgramAgreement();
+                update.setAppId(agreement.getAppId());
+                update.setAgreementType(agreementEnum.getCode());
+                update.setAgreementContent(content);
+
+                list.add(update);
+            }
+        }
+
+        if (!list.isEmpty()) {
+            miniProgramAgreementService.batchUpsertAgreements(list);
+        }
+
+        return AjaxResult.success();
+    }
+
+
+
+    @PreAuthorize("@ss.hasPermi('course:playSourceConfig:agreement')")
+    @GetMapping("/queryAgreementConfig")
+    public AjaxResult queryAgreementConfig(@RequestParam(required = true) String appid) {
+        // 查询协议数据
+        QueryWrapper<MiniProgramAgreement> queryWrapper = new QueryWrapper<MiniProgramAgreement>()
+                .eq("app_id", appid);
+        List<MiniProgramAgreement> agreements = miniProgramAgreementService.list(queryWrapper);
+
+        // 按agreementType分组
+        Map<String, String> agreementMap = new HashMap<>();
+        for (MiniProgramAgreement agreement : agreements) {
+            agreementMap.put(agreement.getAgreementType(), agreement.getAgreementContent());
+        }
+
+        // 转换为JSON串返回
+        String agreementData = JSON.toJSONString(agreementMap);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("agreementData", agreementData);
+        result.put("appId", appid);
+
+        return AjaxResult.success(result);
+    }
+
 }

+ 41 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -13,6 +13,7 @@ import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.FsCourseRedPacketLogParam;
 import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseRedPacketLogListPVO;
+import com.fs.course.vo.FsCourseRedPacketLogListVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.OptionsVO;
@@ -196,10 +197,50 @@ public class FsCourseRedPacketLogController extends BaseController
         return R.ok().put("list", optionsVOS);
     }
 
+    @GetMapping("/courseListByCompanyId/{companyId}")
+    public R courseListByCompanyId(@PathVariable("companyId") Long companyId)
+    {
+        List<OptionsVO> optionsVOS = fsUserCourseMapper.selectFsUserCourseByCompany(companyId);
+        return R.ok().put("list", optionsVOS);
+    }
+
     @GetMapping(value = "/videoList/{id}")
     public R videoList(@PathVariable("id") Long id)
     {
         List<OptionsVO> optionsVOS = fsUserCourseVideoMapper.selectFsUserCourseVodeAllList(id);
         return R.ok().put("list", optionsVOS);
     }
+
+
+    /**
+    * 红包消耗统计
+    */
+    @PreAuthorize("@ss.hasPermi('course:courseRedPacketLog:countList')")
+    @PostMapping("/getRedPacketLogCount")
+    public R getRedPacketLogCount(@RequestBody FsCourseRedPacketLogParam fsCourseRedPacketLog){
+
+        PageHelper.startPage(fsCourseRedPacketLog.getPageNum(), fsCourseRedPacketLog.getPageSize());
+
+        List<FsCourseRedPacketLogListVO> list = fsCourseRedPacketLogService.selectFsCourseRedPacketLogListCountVO(fsCourseRedPacketLog);
+
+
+        return R.ok().put("data", new PageInfo<>(list));
+    }
+
+
+    /**
+     * 导出短链课程看课记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseRedPacketLog:countExport')")
+    @Log(title = "红包消耗统计导出", businessType = BusinessType.EXPORT)
+    @PostMapping("/countExport")
+    public AjaxResult countExport(@RequestBody FsCourseRedPacketLogParam fsCourseRedPacketLog)
+    {
+
+        List<FsCourseRedPacketLogListVO> list = fsCourseRedPacketLogService.selectFsCourseRedPacketLogListCountVO(fsCourseRedPacketLog);
+
+        ExcelUtil<FsCourseRedPacketLogListVO> util = new ExcelUtil<FsCourseRedPacketLogListVO>(FsCourseRedPacketLogListVO.class);
+        return util.exportExcel(list, "红包消耗统计导出");
+    }
+
 }

+ 28 - 18
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -2,17 +2,13 @@ package com.fs.course.controller;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import cn.hutool.core.collection.CollectionUtil;
-import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
-import com.fs.common.utils.ServletUtils;
 import com.fs.course.param.FsCourseOverParam;
 import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
+import com.fs.course.service.IFsUserCoursePeriodDaysService;
+import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
 import com.fs.course.vo.FsCourseWatchLogStatisticsListVO;
@@ -54,25 +50,39 @@ public class FsCourseWatchLogController extends BaseController
 
     @Autowired
     private IQwWatchLogService qwWatchLogService;
+    @Autowired
+    private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
+    @Autowired
+    private IFsUserCoursePeriodService userCoursePeriodService;
     /**
      * 查询短链课程看课记录列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseWatchLog:list')")
-    @GetMapping("/list")
-    public TableDataInfo list(FsCourseWatchLogListParam param)
+    @PostMapping("/list")
+    public R list(@RequestBody FsCourseWatchLogListParam param)
     {
-        startPage();
-        if(CollectionUtil.isNotEmpty(param.getUserIds())){
-            param.setUserIds(param.getUserIds().stream()
-                    .filter(userId -> userId != null && userId.startsWith("user_"))
-                    .map(userId -> userId.substring(5))
-                    .collect(Collectors.toList())
-            );
+
+        if (param.getSendType()==1&& param.getPeriodETime()!=null && param.getPeriodSTime()!=null) {
+            List<Long> periodIds = userCoursePeriodDaysService.selectFsUserCoursePeriodDaysByTime(param.getPeriodSTime(), param.getPeriodETime());
+
+            if (!periodIds.isEmpty()){
+                List<Long> longs = userCoursePeriodService.selectFsUserCoursePeriodListByPeriodId(periodIds, param.getCompanyId());
+                if (!longs.isEmpty()){
+                    param.setPeriodIds(longs);
+                }else {
+                    return R.ok().put("data", new PageInfo<>());
+                }
+            }else {
+                return R.ok().put("data", new PageInfo<>());
+            }
+
         }
-        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
-        return getDataTable(list);
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.selectFsCourseWatchLogListVO(param)));
     }
 
+
     /**
      * 查询短链课程看课记录列表
      */
@@ -90,7 +100,7 @@ public class FsCourseWatchLogController extends BaseController
     {
         logger.info("会员课程数据汇总 参数: {}",param);
 
-        if(param.getCompanyId() == null){
+        if(param.getCompanyId() == null && (param.getUserIds() == null || param.getUserIds().isEmpty())){
             throw new CustomException("必须选择公司!");
         }
         if (param.getSTime()==null||param.getETime()==null){

+ 35 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java

@@ -5,6 +5,7 @@ import java.util.List;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.utils.ServletUtils;
+import com.fs.course.dto.FsCourseCategoryImportDTO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.domain.FsStoreProductCategory;
 import com.fs.his.vo.FsStoreProductCategoryVO;
@@ -33,6 +34,7 @@ import com.fs.course.domain.FsUserCourseCategory;
 import com.fs.course.service.IFsUserCourseCategoryService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 课堂分类Controller
@@ -174,4 +176,37 @@ public class FsUserCourseCategoryController extends BaseController
         List<OptionsVO> list = fsUserCourseCategoryService.selectCateListByPid(pid);
         return R.ok().put("data", list);
     }
+
+    // 下载模板
+    @GetMapping("/importTemplate")
+    public AjaxResult importTemplate() {
+        ExcelUtil<FsCourseCategoryImportDTO> util = new ExcelUtil<>(FsCourseCategoryImportDTO.class);
+        return util.importTemplateExcel("课堂分类导入模板");
+    }
+
+    @Log(title = "导入", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('course:userCourseCategory:importData')")
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file) throws Exception {
+        ExcelUtil<FsCourseCategoryImportDTO> util = new ExcelUtil<>(FsCourseCategoryImportDTO.class);
+        List<FsCourseCategoryImportDTO> list = util.importExcel(file.getInputStream());
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        if (ObjectUtil.isEmpty(config.getIsBound()) || !config.getIsBound()){
+            userId = null;
+        }
+
+        return AjaxResult.success(fsUserCourseCategoryService.importData(list, userId));
+    }
+
+    @PreAuthorize("@ss.hasPermi('course:userCourseCategory:exportFail')")
+    @Log(title = "课堂分类", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportFail")
+    public AjaxResult exportFail(@RequestBody List<FsCourseCategoryImportDTO> list) {
+        ExcelUtil<FsCourseCategoryImportDTO> util = new ExcelUtil<>(FsCourseCategoryImportDTO.class);
+        return util.exportExcel(list, "课堂分类错误数据");
+    }
 }

+ 8 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -346,4 +346,12 @@ public class FsUserCourseController extends BaseController {
         sopTempService.syncTemplate(courseId);
         return toAjax(1);
     }
+
+    /**
+     * 课程下拉列表
+     */
+    @GetMapping("/selectCourseOptionsList")
+    public R getCourseList() {
+        return R.ok().put("data",fsUserCourseService.selectCourseOptionsList());
+    }
 }

+ 18 - 1
fs-admin/src/main/java/com/fs/course/controller/FsUserCoursePeriodController.java

@@ -5,8 +5,10 @@ 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.domain.entity.SysDictData;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.domain.FsUserCoursePeriod;
 import com.fs.course.domain.FsUserCoursePeriodDays;
@@ -63,7 +65,22 @@ public class FsUserCoursePeriodController extends BaseController {
         List<FsUserCoursePeriod> list = fsUserCoursePeriodService.selectFsUserCoursePeriodList(fsUserCoursePeriod);
         return getDataTable(list);
     }
-
+    /**
+     * @Description: 营期key value 值
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/11/18 14:59
+     */
+    @GetMapping("/listLabel/{companyId}")
+    public TableDataInfo listLabel(@PathVariable("companyId") Long companyId)
+    {
+        FsUserCoursePeriod fsUserCoursePeriod = new FsUserCoursePeriod();
+        fsUserCoursePeriod.setCompanyId(companyId + "");
+        startPage();
+        List<SysDictData> list = fsUserCoursePeriodService.selectFsUserCoursePeriodListLabel(fsUserCoursePeriod);
+        return getDataTable(list);
+    }
     @PreAuthorize("@ss.hasPermi('course:period:list')")
     @PostMapping("/page")
     @ApiOperation("自定义查询主列表分页")

+ 27 - 1
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java

@@ -15,6 +15,7 @@ import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsUserCourse;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.param.BatchEditCoverParam;
 import com.fs.course.param.BatchRedUpdate;
 import com.fs.course.param.BatchVideoSvae;
 import com.fs.course.param.CourseVideoUpdates;
@@ -23,12 +24,13 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseVideoChooseVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.vo.OptionsVO;
-import com.fs.qw.vo.SortDayVo;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.HashMap;
@@ -262,4 +264,28 @@ public class FsUserCourseVideoController extends BaseController
         List<FsUserCourseVideoChooseVO> list = fsUserCourseVideoService.getChooseCourseVideoListByMap(params);
         return R.ok().put("data", new PageInfo<>(list));
     }
+
+    @ApiOperation("视频下架")
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:batchDown')")
+    @Log(title = "课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchDown/{videoIds}")
+    public AjaxResult batchDown(@PathVariable String[] videoIds) {
+        return toAjax(fsUserCourseVideoService.batchDown(videoIds));
+    }
+
+    @ApiOperation("批量修改视频封面图")
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:batchEditCover')")
+    @Log(title = "课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/batchEditCover")
+    public AjaxResult batchEditCover(@Validated @RequestBody BatchEditCoverParam param) {
+        return toAjax(fsUserCourseVideoService.batchEditCover(param));
+    }
+
+    /**
+     * 获取课程视频选项列表
+     */
+    @GetMapping("/getCourseVideoOptions")
+    public R getCourseVideoOptions(Long courseId) {
+        return R.ok().put("data", fsUserCourseVideoService.selectVideoOptionsByCourseId(courseId));
+    }
 }

+ 133 - 0
fs-admin/src/main/java/com/fs/fastGpt/FastGptChatMsgController.java

@@ -0,0 +1,133 @@
+package com.fs.fastGpt;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.fastGpt.domain.FastGptChatMsg;
+import com.fs.fastGpt.param.FastGptChatMsgListCParam;
+import com.fs.fastGpt.service.IFastGptChatMsgLogsService;
+import com.fs.fastGpt.service.IFastGptChatMsgService;
+import com.fs.fastGpt.vo.FastGptChatMsgListCVO;
+//import com.fs.framework.security.LoginUser;
+//import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 聊天记录Controller
+ *
+ * @author fs
+ * @date 2024-10-10
+ */
+@RestController
+@RequestMapping("/fastGpt/fastGptChatMsg")
+public class FastGptChatMsgController extends BaseController
+{
+    @Autowired
+    private IFastGptChatMsgService fastGptChatMsgService;
+
+//    @Autowired
+//    private TokenService tokenService;
+
+    @Autowired
+    private IFastGptChatMsgLogsService iFastGptChatMsgLogsService;
+
+    /**
+     * 查询聊天记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FastGptChatMsgListCParam param)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<FastGptChatMsgListCVO> list = fastGptChatMsgService.selectFastGptChatMsgListCVO(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出聊天记录列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:export')")
+    @Log(title = "聊天记录", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FastGptChatMsg fastGptChatMsg)
+    {
+        List<FastGptChatMsg> list = fastGptChatMsgService.selectFastGptChatMsgList(fastGptChatMsg);
+        ExcelUtil<FastGptChatMsg> util = new ExcelUtil<FastGptChatMsg>(FastGptChatMsg.class);
+        return util.exportExcel(list, "聊天记录数据");
+    }
+
+    /**
+     * 获取聊天记录详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:query')")
+    @GetMapping(value = "/{msgId}")
+    public AjaxResult getInfo(@PathVariable("msgId") Long msgId)
+    {
+        return AjaxResult.success(fastGptChatMsgService.selectFastGptChatMsgVOByMsgId(msgId));
+    }
+
+//    /**
+//     * 新增聊天记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:add')")
+//    @Log(title = "聊天记录", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public AjaxResult add(@RequestBody FastGptChatMsg fastGptChatMsg)
+//    {
+//        return toAjax(fastGptChatMsgService.insertFastGptChatMsg(fastGptChatMsg));
+//    }
+
+//    /**
+//     * 修改聊天记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:edit')")
+//    @Log(title = "聊天记录", businessType = BusinessType.UPDATE)
+//    @PutMapping
+//    public AjaxResult edit(@RequestBody FastGptChatMsg param)
+//    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        FastGptChatMsg fastGptChatMsg = fastGptChatMsgService.selectFastGptChatMsgByMsgId(param.getMsgId());
+//
+//        FastGptChatMsgLogs logs = new FastGptChatMsgLogs();
+//
+//        logs.setMsgId(param.getMsgId());
+//        logs.setLogsType(2);
+//        if (param.getContent()!=null&&!param.getContent().equals("")){
+//            logs.setContent(param.getContent());
+//        }
+//        if (param.getStatus()!=null){
+//            logs.setLogsType(1);
+//        }
+//        String userContent = fastGptChatMsgService.selectUserContent(fastGptChatMsg.getMsgId(), fastGptChatMsg.getUserId(),fastGptChatMsg.getRoleId());
+//        logs.setSContent(fastGptChatMsg.getContent());
+//        logs.setUserContent(userContent);
+//        logs.setCompanyUserId(loginUser.getUser().getUserId());
+//        logs.setCreateBy(loginUser.getUser().getNickName());
+//        logs.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        iFastGptChatMsgLogsService.insertFastGptChatMsgLogs(logs);
+//
+//        return toAjax(fastGptChatMsgService.updateFastGptChatMsg(param));
+//    }
+
+//    /**
+//     * 删除聊天记录
+//     */
+//    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsg:remove')")
+//    @Log(title = "聊天记录", businessType = BusinessType.DELETE)
+//	@DeleteMapping("/{msgIds}")
+//    public AjaxResult remove(@PathVariable Long[] msgIds)
+//    {
+//        return toAjax(fastGptChatMsgService.deleteFastGptChatMsgByMsgIds(msgIds));
+//    }
+}

+ 116 - 0
fs-admin/src/main/java/com/fs/fastGpt/FastGptChatMsgLogsController.java

@@ -0,0 +1,116 @@
+package com.fs.fastGpt;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.fastGpt.domain.FastGptChatMsgLogs;
+import com.fs.fastGpt.param.FastGptChatMsgLogsListParam;
+import com.fs.fastGpt.service.IFastGptChatMsgLogsService;
+import com.fs.fastGpt.vo.FastGptChatMsgLogsListCVO;
+import com.fs.fastGpt.vo.FastGptChatMsgLogsVO;
+//import com.fs.framework.security.LoginUser;
+//import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 聊天记录日志Controller
+ *
+ * @author fs
+ * @date 2024-10-10
+ */
+@RestController
+@RequestMapping("/fastGpt/fastGptChatMsgLogs")
+public class FastGptChatMsgLogsController extends BaseController
+{
+    @Autowired
+    private IFastGptChatMsgLogsService fastGptChatMsgLogsService;
+
+//    @Autowired
+//    private TokenService tokenService;
+
+    /**
+     * 查询聊天记录日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FastGptChatMsgLogs fastGptChatMsgLogs)
+    {
+        startPage();
+        List<FastGptChatMsgLogsVO> list = fastGptChatMsgLogsService.selectFastGptChatMsgLogsListVO(fastGptChatMsgLogs);
+        return getDataTable(list);
+    }
+
+//    @GetMapping("/logsList")
+//    public R list(FastGptChatMsgLogsListParam param)
+//    {
+//        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        List<FastGptChatMsgLogsListCVO> list= fastGptChatMsgLogsService.selectFastGptChatMsgLogsListCVO(param);
+//        PageInfo<FastGptChatMsgLogsListCVO> listPageInfo=new PageInfo<>(list);
+//        return R.ok().put("data",listPageInfo);
+//    }
+    /**
+     * 导出聊天记录日志列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:export')")
+    @Log(title = "聊天记录日志", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FastGptChatMsgLogsListParam param)
+    {
+        List<FastGptChatMsgLogsListCVO> list = fastGptChatMsgLogsService.selectFastGptChatMsgLogsListCVO(param);
+        ExcelUtil<FastGptChatMsgLogsListCVO> util = new ExcelUtil<FastGptChatMsgLogsListCVO>(FastGptChatMsgLogsListCVO.class);
+        return util.exportExcel(list, "聊天记录日志数据");
+    }
+
+    /**
+     * 获取聊天记录日志详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:query')")
+    @GetMapping(value = "/{logsId}")
+    public AjaxResult getInfo(@PathVariable("logsId") Long logsId)
+    {
+        return AjaxResult.success(fastGptChatMsgLogsService.selectFastGptChatMsgLogsByLogsId(logsId));
+    }
+
+    /**
+     * 新增聊天记录日志
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:add')")
+    @Log(title = "聊天记录日志", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FastGptChatMsgLogs fastGptChatMsgLogs)
+    {
+        return toAjax(fastGptChatMsgLogsService.insertFastGptChatMsgLogs(fastGptChatMsgLogs));
+    }
+
+    /**
+     * 修改聊天记录日志
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:edit')")
+    @Log(title = "聊天记录日志", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FastGptChatMsgLogs fastGptChatMsgLogs)
+    {
+        return toAjax(fastGptChatMsgLogsService.updateFastGptChatMsgLogs(fastGptChatMsgLogs));
+    }
+
+    /**
+     * 删除聊天记录日志
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatMsgLogs:remove')")
+    @Log(title = "聊天记录日志", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{logsIds}")
+    public AjaxResult remove(@PathVariable Long[] logsIds)
+    {
+        return toAjax(fastGptChatMsgLogsService.deleteFastGptChatMsgLogsByLogsIds(logsIds));
+    }
+}

+ 121 - 0
fs-admin/src/main/java/com/fs/fastGpt/FastGptChatSessionController.java

@@ -0,0 +1,121 @@
+package com.fs.fastGpt;
+
+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.fastGpt.domain.FastGptChatSession;
+import com.fs.fastGpt.param.FastGptChatSessionParam;
+import com.fs.fastGpt.service.IFastGptChatMsgService;
+import com.fs.fastGpt.service.IFastGptChatSessionService;
+import com.fs.fastGpt.vo.FastGptChatMsgCVO;
+import com.fs.fastGpt.vo.FastGptChatSessionCVO;
+import com.fs.fastGpt.vo.FastGptChatSessionListCVO;
+//import com.fs.framework.security.LoginUser;
+//import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 对话关系Controller
+ *
+ * @author fs
+ * @date 2024-10-10
+ */
+@RestController
+@RequestMapping("/fastGpt/fastGptChatSession")
+public class FastGptChatSessionController extends BaseController
+{
+    @Autowired
+    private IFastGptChatSessionService fastGptChatSessionService;
+
+//    @Autowired
+//    private TokenService tokenService;
+
+    @Autowired
+    private IFastGptChatMsgService fastGptChatMsgService;
+
+
+    /**
+     * 查询对话关系列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FastGptChatSessionParam param)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<FastGptChatSessionListCVO> list = fastGptChatSessionService.selectFastGptChatSessionListCVO(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出对话关系列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:export')")
+    @Log(title = "对话关系", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FastGptChatSession fastGptChatSession)
+    {
+        List<FastGptChatSession> list = fastGptChatSessionService.selectFastGptChatSessionList(fastGptChatSession);
+        ExcelUtil<FastGptChatSession> util = new ExcelUtil<FastGptChatSession>(FastGptChatSession.class);
+        return util.exportExcel(list, "对话关系数据");
+    }
+
+    /**
+     * 获取对话关系详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:query')")
+    @GetMapping(value = "/{sessionId}")
+    public R getInfo(@PathVariable("sessionId") Long sessionId)
+    {
+        FastGptChatSessionCVO sessionCVO = fastGptChatSessionService.selectFastGptChatSessionCVOBySessionId(sessionId);
+
+        List<FastGptChatMsgCVO> list = fastGptChatMsgService.selectFastGptChatMsgCVOBySessionId(sessionId,sessionCVO.getUserId());
+
+        return R.ok().put("data",sessionCVO).put("list",list);
+    }
+
+    /**
+     * 新增对话关系
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:add')")
+    @Log(title = "对话关系", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FastGptChatSession fastGptChatSession)
+    {
+        return toAjax(fastGptChatSessionService.insertFastGptChatSession(fastGptChatSession));
+    }
+
+    /**
+     * 修改对话关系
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:edit')")
+    @Log(title = "对话关系", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FastGptChatSession fastGptChatSession)
+    {
+        FastGptChatSession session = new FastGptChatSession();
+        session.setSessionId(fastGptChatSession.getSessionId());
+        session.setIsArtificial(fastGptChatSession.getIsArtificial());
+        return toAjax(fastGptChatSessionService.updateFastGptChatSession(session));
+    }
+
+    /**
+     * 删除对话关系
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:remove')")
+    @Log(title = "对话关系", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{sessionIds}")
+    public AjaxResult remove(@PathVariable Long[] sessionIds)
+    {
+        return toAjax(fastGptChatSessionService.deleteFastGptChatSessionBySessionIds(sessionIds));
+    }
+}

+ 2 - 2
fs-admin/src/main/java/com/fs/fastGpt/FastgptEventLogTotalController.java

@@ -89,8 +89,8 @@ public class FastgptEventLogTotalController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(FastgptEventLogTotal fastgptEventLogTotal)
     {
-        List<FastgptEventLogTotal> list = fastgptEventLogTotalService.selectFastgptEventLogTotalList(fastgptEventLogTotal);
-        ExcelUtil<FastgptEventLogTotal> util = new ExcelUtil<FastgptEventLogTotal>(FastgptEventLogTotal.class);
+        List<FastgptEventLogTotalVo> list = fastgptEventLogTotalService.selectFastgptEventLogTotalExport(fastgptEventLogTotal);
+        ExcelUtil<FastgptEventLogTotalVo> util = new ExcelUtil<FastgptEventLogTotalVo>(FastgptEventLogTotalVo.class);
         return util.exportExcel(list, "ai事件埋点统计数据");
     }
 

+ 20 - 0
fs-admin/src/main/java/com/fs/fastGpt/GptRoleController.java

@@ -6,6 +6,7 @@ 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.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.IFastGptRoleService;
@@ -49,6 +50,25 @@ public class GptRoleController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询应用列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptRole:newList')")
+    @GetMapping("/newList")
+    public TableDataInfo newList(FastGptRole fastGptRole)
+    {
+        startPage();
+        fastGptRole.setCompanyId(fastGptRole.getCompanyId());
+        List<FastGptRoleVO> list = fastGptRoleService.selectFastGptRoleListVONew(fastGptRole);
+        for (FastGptRoleVO fastGptRoleVO : list) {
+            String reminderWords = fastGptRoleVO.getReminderWords();
+            if (reminderWords!=null && reminderWords.length()>110) {
+                fastGptRoleVO.setReminderWords(reminderWords.substring(0,110)+"...");
+            }
+        }
+        return getDataTable(list);
+    }
+
     /**
      * 导出应用列表
      */

+ 163 - 0
fs-admin/src/main/java/com/fs/his/controller/FsAiWorkflowController.java

@@ -0,0 +1,163 @@
+package com.fs.his.controller;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsAiWorkflow;
+import com.fs.his.domain.FsAiWorkflowNodeType;
+import com.fs.his.param.FsAiWorkflowSaveParam;
+import com.fs.his.param.FsAiWorkflowUpdateBindWCParam;
+import com.fs.his.service.IFsAiWorkflowService;
+import com.fs.his.vo.FsAiWorkflowVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AI工作流Controller
+ *
+ * @author fs
+ * @date 2026-01-06
+ */
+@RestController
+@RequestMapping("/his/aiWorkflow")
+public class FsAiWorkflowController extends BaseController {
+
+    @Autowired
+    private IFsAiWorkflowService fsAiWorkflowService;
+
+    /**
+     * 查询AI工作流列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(FsAiWorkflow fsAiWorkflow) {
+        startPage();
+        List<FsAiWorkflow> list = fsAiWorkflowService.selectFsAiWorkflowList(fsAiWorkflow);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出AI工作流列表
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsAiWorkflow fsAiWorkflow) {
+        List<FsAiWorkflow> list = fsAiWorkflowService.selectFsAiWorkflowList(fsAiWorkflow);
+        ExcelUtil<FsAiWorkflow> util = new ExcelUtil<FsAiWorkflow>(FsAiWorkflow.class);
+        return util.exportExcel(list, "AI工作流数据");
+    }
+
+    /**
+     * 获取AI工作流详细信息(包含节点和连线)
+     */
+    @GetMapping(value = "/{workflowId}")
+    public AjaxResult getInfo(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(fsAiWorkflowService.selectFsAiWorkflowById(workflowId));
+    }
+
+    /**
+     * 保存AI工作流(新增或更新)
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.INSERT)
+    @PostMapping("/save")
+    public AjaxResult save(@RequestBody FsAiWorkflowSaveParam param) {
+        Long workflowId = fsAiWorkflowService.saveFsAiWorkflow(param);
+        return AjaxResult.success(workflowId);
+    }
+
+    /**
+     * 修改AI工作流状态
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.UPDATE)
+    @PutMapping("/status/{workflowId}/{status}")
+    public AjaxResult updateStatus(@PathVariable("workflowId") Long workflowId,
+                                   @PathVariable("status") Integer status) {
+        return toAjax(fsAiWorkflowService.updateFsAiWorkflowStatus(workflowId, status));
+    }
+
+    /**
+     * 删除AI工作流
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{workflowIds}")
+    public AjaxResult remove(@PathVariable Long[] workflowIds) {
+        return toAjax(fsAiWorkflowService.deleteFsAiWorkflowByIds(workflowIds));
+    }
+
+    /**
+     * 复制AI工作流
+     */
+    @Log(title = "AI工作流", businessType = BusinessType.INSERT)
+    @PostMapping("/copy/{workflowId}")
+    public AjaxResult copy(@PathVariable("workflowId") Long workflowId) {
+        Long newWorkflowId = fsAiWorkflowService.copyFsAiWorkflow(workflowId);
+        if (newWorkflowId != null) {
+            return AjaxResult.success(newWorkflowId);
+        }
+        return AjaxResult.error("复制失败,工作流不存在");
+    }
+
+    /**
+     * 获取所有启用的节点类型
+     */
+    @GetMapping("/nodeTypes")
+    public AjaxResult getNodeTypes() {
+        List<FsAiWorkflowNodeType> list = fsAiWorkflowService.selectAllEnabledNodeTypes();
+        return AjaxResult.success(list);
+    }
+    /**
+     * 导出工作流流程图JSON
+     * 包含节点信息、连接顺序、节点类型
+     */
+//    @Log(title = "AI工作流", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportJson/{workflowId}")
+    public AjaxResult exportJson(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(fsAiWorkflowService.exportWorkflowJson(workflowId));
+    }
+    /**
+     * 分页销售
+     */
+    @GetMapping("/listCompanyUser")
+    public AjaxResult listCompanyUser() {
+        return AjaxResult.success(fsAiWorkflowService.listCompanyUser());
+    }
+
+    /**
+     * 查销售
+     */
+    @GetMapping("/getCompanyUserById/{companyUserId}")
+    public AjaxResult getCompanyUserById(@PathVariable("companyUserId") Long companyUserId) {
+        return AjaxResult.success(fsAiWorkflowService.getCompanyUserById(companyUserId));
+    }
+
+//    /**
+//     * 查销售是否已经被绑定
+//     */
+//    @GetMapping("/checkCompanyUserBeUsed/{companyUserId}")
+//    public AjaxResult checkCompanyUserBeUsed(@PathVariable("companyUserId") Long companyUserId) {
+//        return AjaxResult.success(fsAiWorkflowService.checkCompanyUserBeUsed(companyUserId));
+//    }
+
+    /**
+     * 查工作流已绑定的销售
+     */
+    @GetMapping("/getBindCompanyUserByWorkflowId/{workflowId}")
+    public AjaxResult getBindCompanyUserByWorkflowId(@PathVariable("workflowId") Long workflowId) {
+        return AjaxResult.success(fsAiWorkflowService.getBindCompanyUserByWorkflowId(workflowId));
+    }
+
+    /**
+     * 修改工作流绑定的销售
+     */
+    @PostMapping("/updateWorkflowBindCompanyUser")
+    public AjaxResult updateWorkflowBindCompanyUser(@RequestBody FsAiWorkflowUpdateBindWCParam param) {
+        return fsAiWorkflowService.updateWorkflowBindCompanyUser(param);
+    }
+
+}

+ 105 - 3
fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java

@@ -148,6 +148,7 @@ public class FsIntegralOrderController extends BaseController
     @PreAuthorize("@ss.hasPermi('his:integralOrder:export')")
     @Log(title = "积分商品订单", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
+
     public AjaxResult export(FsIntegralOrderParam fsIntegralOrder) {
         List<FsIntegralOrderListVO> fsIntegralOrderListVOS = new ArrayList<>();
         if (CloudHostUtils.hasCloudHostName("金牛明医")){
@@ -162,6 +163,101 @@ public class FsIntegralOrderController extends BaseController
         }
         SysRole sysRole = isCheckPermission();
         // 处理商品名称:解析item_json并设置goodsName
+        for (FsIntegralOrderListVO vo : fsIntegralOrderListVOS) {
+            if (StringUtils.isNotEmpty(vo.getItemJson())) {
+                try {
+                    // 尝试解析JSON格式的商品信息
+                    if (vo.getItemJson().startsWith("[")) {
+                        // 数组格式,遍历所有商品,用换行符分隔
+                        com.alibaba.fastjson.JSONArray jsonArray = com.alibaba.fastjson.JSONArray.parseArray(vo.getItemJson());
+                        if (jsonArray != null && !jsonArray.isEmpty()) {
+                            StringBuilder goodsNameBuilder = new StringBuilder();
+                            StringBuilder barCodeBuilder = new StringBuilder();
+
+                            for (int i = 0; i < jsonArray.size(); i++) {
+                                com.alibaba.fastjson.JSONObject goods = jsonArray.getJSONObject(i);
+
+                                // 处理商品名称和数量
+                                if (goods != null && goods.getString("goodsName") != null) {
+                                    String name = goods.getString("goodsName");
+                                    String num = goods.getString("num");
+                                    if (StringUtils.isEmpty(num)) {
+                                        num = "1"; // 默认数量为1
+                                    }
+                                    if (goodsNameBuilder.length() > 0) {
+                                        goodsNameBuilder.append("\r\n");
+                                    }
+                                    goodsNameBuilder.append(name).append(",数量:").append(num).append("个");
+                                }
+
+                                // 处理商品编码
+                                if (goods != null && goods.getString("barCode") != null) {
+                                    String barCode = goods.getString("barCode");
+                                    if (barCodeBuilder.length() > 0) {
+                                        barCodeBuilder.append("\r\n");
+                                    }
+                                    barCodeBuilder.append(barCode);
+                                }
+                            }
+
+                            // 设置商品名称
+                            if (goodsNameBuilder.length() > 0) {
+                                vo.setGoodsName(goodsNameBuilder.toString());
+                            }
+
+                            // 设置商品编码
+                            if (barCodeBuilder.length() > 0) {
+                                vo.setBarCode(barCodeBuilder.toString());
+                            }
+                        }
+                    } else if (vo.getItemJson().startsWith("{")) {
+                        // 对象格式
+                        com.alibaba.fastjson.JSONObject goods = com.alibaba.fastjson.JSONObject.parseObject(vo.getItemJson());
+
+                        // 处理商品名称和数量
+                        if (goods != null && goods.getString("goodsName") != null) {
+                            String name = goods.getString("goodsName");
+                            String num = goods.getString("num");
+                            if (StringUtils.isEmpty(num)) {
+                                num = "1"; // 默认数量为1
+                            }
+                            vo.setGoodsName(name + ",数量:" + num + "个");
+                        }
+
+                        // 处理商品编码
+                        if (goods != null && goods.getString("barCode") != null) {
+                            vo.setBarCode(goods.getString("barCode"));
+                        }
+                    }
+                } catch (Exception e) {
+                    // 解析失败时保持goodsName为空,避免导出出错
+                    log.warn("解析商品信息失败,订单编号:{}, 商品信息:{}", vo.getOrderCode(), vo.getItemJson());
+                }
+            }
+            if (!(sysRole.getIsCheckPhone()==1)){
+                // 加密手机号
+                vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+            }
+
+        }
+        ExcelUtil<FsIntegralOrderListVO> util = new ExcelUtil<>(FsIntegralOrderListVO.class);
+        return util.exportExcel(new ArrayList<>(fsIntegralOrderListVOS), "积分商品订单数据");
+    }
+
+    /*public AjaxResult export(FsIntegralOrderParam fsIntegralOrder) {
+        List<FsIntegralOrderListVO> fsIntegralOrderListVOS = new ArrayList<>();
+        if (CloudHostUtils.hasCloudHostName("金牛明医")){
+            *//*目前只有金牛有状态为6的查询,其他项目避免使用6状态码*//*
+            if (fsIntegralOrder.getStatus() != null && fsIntegralOrder.getStatus().equals("6")) {
+                fsIntegralOrder.setStatus("1");
+                fsIntegralOrder.setIsPush(0);
+            }
+            fsIntegralOrderListVOS = fsIntegralOrderService.selectFsIntegralOrderListByJn(fsIntegralOrder);
+        } else {
+            fsIntegralOrderListVOS = fsIntegralOrderService.selectFsIntegralOrderListVO(fsIntegralOrder);
+        }
+        SysRole sysRole = isCheckPermission();
+        // 处理商品名称:解析item_json并设置goodsName
         for (FsIntegralOrderListVO vo : fsIntegralOrderListVOS) {
             if (StringUtils.isNotEmpty(vo.getItemJson())) {
                 try {
@@ -174,6 +270,9 @@ public class FsIntegralOrderController extends BaseController
                             if (goods != null && goods.getString("goodsName") != null) {
                                 vo.setGoodsName(goods.getString("goodsName"));
                             }
+                            if (goods != null && goods.getString("barCode") != null) {
+                                vo.setBarCode(goods.getString("barCode"));
+                            }
                         }
                     } else if (vo.getItemJson().startsWith("{")) {
                         // 对象格式
@@ -181,6 +280,9 @@ public class FsIntegralOrderController extends BaseController
                         if (goods != null && goods.getString("goodsName") != null) {
                             vo.setGoodsName(goods.getString("goodsName"));
                         }
+                        if (goods != null && goods.getString("barCode") != null) {
+                            vo.setGoodsName(goods.getString("barCode"));
+                        }
                     }
                 } catch (Exception e) {
                     // 解析失败时保持goodsName为空,避免导出出错
@@ -195,7 +297,7 @@ public class FsIntegralOrderController extends BaseController
         }
         ExcelUtil<FsIntegralOrderListVO> util = new ExcelUtil<>(FsIntegralOrderListVO.class);
         return util.exportExcel(new ArrayList<>(fsIntegralOrderListVOS), "积分商品订单数据");
-    }
+    }*/
     /**
      * 发货
      */
@@ -464,8 +566,8 @@ public class FsIntegralOrderController extends BaseController
     {
         ExcelUtil<FsIntegralOrderExcelVO> util = new ExcelUtil<>(FsIntegralOrderExcelVO.class);
         List<FsIntegralOrderExcelVO> list = util.importExcel(file.getInputStream());
-        String message = fsIntegralOrderService.importOrderStatusData(list);
-        return AjaxResult.success(message);
+        FsIntegralOrderImportResultVO result = fsIntegralOrderService.importOrderStatusData(list);
+        return AjaxResult.success(result);
     }
 
     @GetMapping("/importUpdateOrderTemplate")

+ 15 - 0
fs-admin/src/main/java/com/fs/his/controller/FsStorePaymentController.java

@@ -133,6 +133,21 @@ public class FsStorePaymentController extends BaseController
 
         return AjaxResult.success( fsStorePaymentService.updateFsStorePaymentByDecryptForm(paymentId));
     }
+
+    /**
+     * 批量互医同步支付明细(临时)
+     */
+    @PostMapping(value = "/batchUpdate")
+    public AjaxResult batchUpdate(@RequestBody List<Long> paymentIds)
+    {
+        if(!paymentIds.isEmpty()){
+            for (Long paymentId : paymentIds) {
+                fsStorePaymentService.updateFsStorePaymentByDecryptForm(paymentId);
+            }
+        }
+        return AjaxResult.success("成功");
+    }
+
     @PreAuthorize("@ss.hasPermi('his:storePayment:refund')")
     @GetMapping(value = "refund/{paymentId}")
     public R refund(@PathVariable("paymentId") Long paymentId)

+ 11 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderItemScrmController.java

@@ -94,4 +94,15 @@ public class FsStoreOrderItemScrmController extends BaseController
     {
         return toAjax(fsStoreOrderItemService.deleteFsStoreOrderItemByIds(itemIds));
     }
+
+    /**
+     * 修改订单数量及总价
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:updateNum')")
+    @Log(title = "订单详情修改订单数量", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateNum")
+    public AjaxResult updateNum(@RequestBody FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        return toAjax(fsStoreOrderItemService.updateFsStoreOrderItemNum(fsStoreOrderItem));
+    }
 }

+ 46 - 2
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -22,6 +22,7 @@ import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.param.CompanyStoreOrderMoneyLogsListParam;
 import com.fs.company.service.ICompanyMoneyLogsService;
 import com.fs.company.vo.CompanyStoreOrderMoneyLogsVO;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrderQuery;
 import com.fs.erp.dto.ErpOrderQueryRequert;
@@ -101,6 +102,9 @@ public class FsStoreOrderScrmController extends BaseController {
     @Autowired
     IFsStorePaymentScrmService paymentService;
 
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     @Autowired
     private ICompanyMoneyLogsService moneyLogsService;
     @Autowired
@@ -693,7 +697,7 @@ public class FsStoreOrderScrmController extends BaseController {
         ExpressInfoDTO expressInfoDTO = null;
         if (StringUtils.isNotEmpty(order.getDeliveryId())) {
             String lastFourNumber = "";
-            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue())) {
+            if (order.getDeliverySn().equals(ShipperCodeEnum.SF.getValue()) || order.getDeliverySn().equals(ShipperCodeEnum.ZTO.getValue())) {
                 lastFourNumber = order.getUserPhone();
                 if (lastFourNumber.length() == 11) {
                     lastFourNumber = StrUtil.sub(lastFourNumber, lastFourNumber.length(), -4);
@@ -723,7 +727,14 @@ public class FsStoreOrderScrmController extends BaseController {
     public AjaxResult edit(@RequestBody FsStoreOrderScrm fsStoreOrder) {
         return toAjax(fsStoreOrderService.updateFsStoreOrder(fsStoreOrder));
     }
-
+    /**
+     * 修改订单itemJson
+     */
+    @Log(title = "修改订单itemJson", businessType = BusinessType.UPDATE)
+    @GetMapping("/updateStoreOrderItemJson/{orderId}/{backendEditProductType}")
+    public AjaxResult updateStoreOrderItemJson(@PathVariable("orderId") Long orderId,@PathVariable("backendEditProductType") Integer backendEditProductType) {
+        return toAjax(fsStoreOrderService.updateStoreOrderItemJson(orderId,backendEditProductType));
+    }
     /**
      * 修改物流
      * @param fsStoreOrder
@@ -902,6 +913,10 @@ public class FsStoreOrderScrmController extends BaseController {
      * **/
     @GetMapping("/orderDimensionStatisticsList")
     public TableDataInfo orderDimensionStatisticsList(OrderStatisticsParam param){
+        // 郑多燕需求
+        if("广州郑多燕".equals(cloudHostProper.getCompanyName())){
+            return getDataTable(fsStoreOrderService.selectZDYOrderSaleStatisticsList(param));
+        }
         return getDataTable(fsStoreOrderService.selectOrderDimensionStatisticsList(param));
     }
 
@@ -1122,6 +1137,35 @@ public class FsStoreOrderScrmController extends BaseController {
         return R.ok();
     }
 
+    @ApiOperation("批量审核订单")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:batchAudit')")
+    @PostMapping("/batchAudit")
+    public R batchAuditOrder(@Validated @RequestBody FsStoreOrderBatchAuditParam param) {
+        if (param.getOrderIds() == null || param.getOrderIds().isEmpty()) {
+            return R.error("订单ID列表不能为空");
+        }
+        if (param.getIsAudit() == null) {
+            return R.error("审核状态不能为空");
+        }
+        int count = fsStoreOrderService.batchAuditOrder(param);
+        return R.ok("成功审核 " + count + " 条订单");
+    }
+
+    @ApiOperation("订单备注")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:remark')")
+    @PostMapping("/remark")
+    public R remark(@Validated @RequestBody FsStoreOrderScrm param) {
+        if (param.getId() == null || param.getId() < 1) {
+            return R.error("订单ID错误");
+        }
+        if (StringUtils.isEmpty(param.getOrderRemark())) {
+            return R.error("订单备注不能为空");
+        }
+        return fsStoreOrderService.orderRemark(param);
+    }
+
     private FsStoreOrderDf getDFInfo(String loginAccount) {
         //查询订单账户 判断是否存在该订单账户
         List<FsDfAccount> erpAccounts = fsDfAccountService.selectFsDfAccountList(null);

+ 9 - 2
fs-admin/src/main/java/com/fs/hisStore/controller/FsStorePaymentScrmController.java

@@ -56,6 +56,7 @@ import org.springframework.transaction.interceptor.TransactionAspectSupport;
 import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.HashMap;
@@ -197,7 +198,7 @@ public class FsStorePaymentScrmController extends BaseController
     }
 
     @PreAuthorize("@ss.hasPermi('store:storePayment:refund')")
-    @PostMapping("refundStorePayment")
+    @PostMapping("/refundStorePayment")
     @Transactional
     public R refundStorePayment(@RequestBody FsStorePaymentScrm fsStorePayment)
     {
@@ -238,7 +239,13 @@ public class FsStorePaymentScrmController extends BaseController
                 }
                 V2TradePaymentScanpayRefundRequest request = new V2TradePaymentScanpayRefundRequest();
                 request.setHuifuId(huifuId);
-                request.setOrdAmt(payment.getPayMoney().toString());
+
+                if (("易行健".equals(cloudHostProper.getCompanyName()))){
+                    request.setOrdAmt(fsStorePayment.getRefundMoney().setScale(2, RoundingMode.DOWN).toString());
+                }else {
+                    request.setOrdAmt(payment.getPayMoney().toString());
+                }
+
                 request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
                 request.setReqSeqId("refund-"+payment.getPayCode());
                 request.setAppId(payment.getAppId());

+ 2 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreStatisticsScrmController.java

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -76,7 +77,7 @@ public class FsStoreStatisticsScrmController extends BaseController
             List<JSONObject> jsonObjectList = storeOrderService.selectFsStoreOrderCounts(timeEntity.toMap());
             List<String> dates = jsonObjectList.stream().map(jsonObject -> jsonObject.getString("type")).collect(Collectors.toList());
             List<Integer> orderCount = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("orderCount")).collect(Collectors.toList());
-            List<Integer> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getInteger("payPrice")).collect(Collectors.toList());
+            List<BigDecimal> payPrice = jsonObjectList.stream().map(jsonObject -> jsonObject.getBigDecimal("payPrice")).collect(Collectors.toList());
             //表格数据
             List<FsStoreOrderCountsVO> tableData = storeOrderService.selectFsStoreOrderCountsByDept(timeEntity.toMap(),param.getDeptId());
             return R.ok().put("dates",dates).put("orderCount",orderCount).put("payPrice",payPrice).put("tableData",tableData);

+ 16 - 6
fs-admin/src/main/java/com/fs/hisStore/task/LiveTask.java

@@ -40,6 +40,7 @@ import com.fs.live.param.LiveAfterSalesAudit1Param;
 import com.fs.live.param.LiveAfterSalesParam;
 import com.fs.live.param.LiveAfterSalesProductParam;
 import com.fs.live.service.*;
+import com.fs.live.utils.redis.RedisBatchHandler;
 import com.fs.pay.pay.dto.OrderQueryDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.store.config.StoreConfig;
@@ -51,6 +52,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -65,6 +67,7 @@ import java.util.Map;
 import java.util.concurrent.CompletableFuture;
 
 import static com.fs.hisStore.constants.StoreConstants.DELIVERY;
+import static com.fs.live.utils.redis.RedisBatchHandler.CONSUME_INTERVAL;
 
 /**
  * 定时任务调度测试
@@ -163,6 +166,8 @@ public class LiveTask {
 
     @Autowired
     private FsJstAftersalePushScrmService fsJstAftersalePushScrmService;
+    @Autowired
+    public RedisBatchHandler redisBatchHandler;
 
     /**
      * 查询被拆分的订单,然后查询拆分订单的物流信息
@@ -331,17 +336,14 @@ public class LiveTask {
     public void deliveryOp() {
         List<LiveOrder> list = liveOrderService.selectUpdateExpress();
         if(list == null || list.isEmpty()) return;
-
+        Date now = new Date();
         for (LiveOrder order : list) {
-            order.setUpdateTime(new Date());
-            liveOrderService.updateLiveOrder(order);
+            order.setUpdateTime(now);
+            liveOrderService.updateTime(order);
             ErpOrderQueryRequert request = new ErpOrderQueryRequert();
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getLiveOrder(request);
-            if(!response.getSuccess() && "429".equals(response.getCode())){
-                break;
-            }
             if (erpOrderService != dfOrderService) {
                 if (response.getOrders() != null && !response.getOrders().isEmpty()) {
                     for (ErpOrderQuery orderQuery : response.getOrders()) {
@@ -666,4 +668,12 @@ public class LiveTask {
              }
         }
     }
+    /**
+     * 定时流量入库
+     */
+    @Scheduled(fixedRate = CONSUME_INTERVAL)
+    public void insertLiveTrralog() {
+        redisBatchHandler.consumeBatchData();
+    }
+
 }

+ 39 - 4
fs-admin/src/main/java/com/fs/hisStore/task/MallStoreTask.java

@@ -34,7 +34,11 @@ import com.fs.hisStore.mapper.FsStorePaymentScrmMapper;
 import com.fs.hisStore.mapper.FsStoreProductAttrValueScrmMapper;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
+import com.fs.huifuPay.domain.HuiFuQueryOrderResult;
+import com.fs.huifuPay.sdk.opps.core.request.V2TradePaymentScanpayQueryRequest;
+import com.fs.huifuPay.service.HuiFuService;
 import com.fs.live.domain.LiveOrder;
+import com.fs.live.domain.LiveOrderPayment;
 import com.fs.pay.pay.dto.OrderQueryDTO;
 import com.fs.pay.service.IPayService;
 import com.fs.store.config.StoreConfig;
@@ -50,6 +54,7 @@ import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.text.ParseException;
+import java.text.SimpleDateFormat;
 import java.time.LocalTime;
 import java.util.ArrayList;
 import java.util.Date;
@@ -160,6 +165,39 @@ public class MallStoreTask
 
     //@Autowired
     //private IFsUserOnlineStateService fsUserOnlineStateService;
+    @Autowired
+    private HuiFuService huiFuService;
+
+    // 订单银行回调数据丢失补偿
+    public void recoveryBankOrder() {
+        // 查询出来最近30分钟的订单 待支付 未退款
+        List<FsStoreOrderScrm> list = fsStoreOrderMapper.selectBankOrder();
+        if(list == null || list.isEmpty()) return;
+        for (FsStoreOrderScrm order : list) {
+            List<FsStorePaymentScrm> orderPayments = fsStorePaymentMapper.selectNoPayByOrderId(order.getId());
+            if(orderPayments == null || orderPayments.isEmpty()) continue;
+            for (FsStorePaymentScrm payment : orderPayments) {
+                V2TradePaymentScanpayQueryRequest request = new V2TradePaymentScanpayQueryRequest();
+                request.setOrgReqDate(new SimpleDateFormat("yyyyMMdd").format(payment.getCreateTime()));
+                request.setOrgHfSeqId(payment.getTradeNo());
+                request.setAppId(payment.getAppId());
+                HuiFuQueryOrderResult o = null;
+                try {
+                    o = huiFuService.queryOrder(request);
+                } catch (Exception e) {
+                    log.error("查询失败:"+e.getMessage());
+                    continue;
+                }
+                log.info("汇付返回"+o);
+                if ("00000000".equals(o.getResp_code()) && "S".equals(o.getTrans_stat())) {
+                    String[] orderSpilt=o.getOrg_req_seq_id().split("-");
+                    if ("store".equals(orderSpilt[0])) {
+                        orderService.payConfirm(1, null, orderSpilt[1], o.getOrg_hf_seq_id(), o.getOut_trans_id(), o.getParty_order_id());
+                    }
+                }
+            }
+        }
+    }
 
     public void PushErp() throws ParseException {
         List<Long> ids;
@@ -229,9 +267,6 @@ public class MallStoreTask
             request.setCode(order.getExtendOrderId());
             IErpOrderService erpOrderService = getErpOrderService();
             ErpOrderQueryResponse response = erpOrderService.getScrmOrder(request);
-            if(!response.getSuccess() && "429".equals(response.getCode())){
-                break;
-            }
             if (erpOrderService != dfOrderService) {
                 if(response.getOrders()!=null && !response.getOrders().isEmpty()){
                     for(ErpOrderQuery orderQuery : response.getOrders()){
@@ -449,7 +484,7 @@ public class MallStoreTask
     //每天执行一次
     public void syncExpress()
     {
-        List<Long> ids =fsStoreOrderMapper.selectSyncExpressIds();
+        List<Long> ids =fsStoreOrderMapper.selectSyncExpressIdsNoDate();
 
         for (Long id : ids) {
             FsStoreOrderExpressEditParam param =new FsStoreOrderExpressEditParam();

+ 74 - 0
fs-admin/src/main/java/com/fs/live/controller/FsWxExpressTaskController.java

@@ -0,0 +1,74 @@
+package com.fs.live.controller;
+
+import com.alibaba.fastjson.JSONObject;
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.system.service.ISysConfigService;
+import com.fs.wx.order.domain.FsWxExpressTask;
+import com.fs.wx.order.service.IFsWxExpressTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+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;
+
+/**
+ * 微信快递任务Controller
+ *
+ * @author fs
+ * @date 2025-01-XX
+ */
+@RestController
+@RequestMapping("/live/wxExpressTask")
+public class FsWxExpressTaskController extends BaseController {
+
+    @Autowired
+    private IFsWxExpressTaskService fsWxExpressTaskService;
+
+    @Autowired
+    private ISysConfigService configService;
+
+    /**
+     * 查询微信快递任务列表
+     */
+    @PreAuthorize("@ss.hasPermi('live:wxExpressTask:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsWxExpressTask fsWxExpressTask) {
+        startPage();
+        List<FsWxExpressTask> list = fsWxExpressTaskService.selectFsWxExpressTaskList(fsWxExpressTask);
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取当前使用小程序的微信上传失败订单数量
+     */
+    @GetMapping("/getFailedCount")
+    public AjaxResult getFailedCount() {
+        try {
+            // 获取his.store配置
+            String configJson = configService.selectConfigByKey("his.config");
+            if (configJson == null || configJson.isEmpty()) {
+                return AjaxResult.success(0);
+            }
+            
+            // 解析JSON获取appId
+            JSONObject config = JSONObject.parseObject(configJson);
+            String appId = config.getString("appid");
+            
+            if (appId == null || appId.isEmpty()) {
+                return AjaxResult.success(0);
+            }
+            
+            // 查询失败订单数量(status=3)
+            int count = fsWxExpressTaskService.countFailedTasksByAppId(appId);
+            return AjaxResult.success(count);
+        } catch (Exception e) {
+            return AjaxResult.error("查询失败订单数量失败:" + e.getMessage());
+        }
+    }
+}

+ 28 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveMsgController.java

@@ -3,11 +3,16 @@ package com.fs.live.controller;
 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.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.web.service.TokenService;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.service.ILiveMsgService;
+import com.fs.live.vo.LiveMsgExportVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -26,6 +31,9 @@ public class LiveMsgController extends BaseController
 {
     @Autowired
     private ILiveMsgService liveMsgService;
+    
+    @Autowired
+    private TokenService tokenService;
 
     /**
      * 查询直播讨论列表
@@ -102,4 +110,24 @@ public class LiveMsgController extends BaseController
     {
         return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
     }
+
+    /**
+     * 导出直播评论
+     */
+    @PreAuthorize("@ss.hasPermi('live:liveMsg:export')")
+    @Log(title = "直播评论导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportComments/{liveId}")
+    public AjaxResult exportComments(@PathVariable("liveId") Long liveId)
+    {
+        try {
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            Long userId = loginUser.getUser().getUserId();
+            
+            List<LiveMsgExportVO> list = liveMsgService.exportLiveMsgComments(liveId, userId);
+            ExcelUtil<LiveMsgExportVO> util = new ExcelUtil<LiveMsgExportVO>(LiveMsgExportVO.class);
+            return util.exportExcel(list, "直播评论数据");
+        } catch (Exception e) {
+            return AjaxResult.error("导出失败:" + e.getMessage());
+        }
+    }
 }

+ 67 - 22
fs-admin/src/main/java/com/fs/live/controller/OrderController.java

@@ -7,10 +7,13 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableSupport;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
+import com.fs.common.constant.HttpStatus;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.hisStore.dto.StoreOrderProductDTO;
@@ -64,7 +67,33 @@ public class OrderController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(MergedOrderQueryParam param)
     {
-        startPage();
+        // 从请求参数中获取分页信息
+        PageDomain pageDomain = TableSupport.buildPageRequest();
+        Integer pageNum = pageDomain.getPageNum();
+        Integer pageSize = pageDomain.getPageSize();
+        
+        // 设置分页参数到 param
+        if (pageNum == null || pageNum < 1) {
+            pageNum = 1;
+            param.setPageNum(1);
+        } else {
+            param.setPageNum(pageNum);
+        }
+        if (pageSize == null || pageSize < 1) {
+            pageSize = 10;
+            param.setPageSize(10);
+        } else {
+            param.setPageSize(pageSize);
+        }
+        
+        // 在外面计算 offset,然后放在请求参数中
+        Integer offset = (pageNum - 1) * pageSize;
+        param.setOffset(offset);
+        
+        // 先查询总数(不分页)
+        long total = mergedOrderService.countMergedOrderList(param);
+        
+        // 查询分页数据
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         for (MergedOrderVO vo : list) {
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
@@ -73,7 +102,13 @@ public class OrderController extends BaseController
             vo.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
             vo.setCost(BigDecimal.ZERO);
         }
-        return getDataTable(list);
+        
+        TableDataInfo dataTable = new TableDataInfo();
+        dataTable.setCode(HttpStatus.SUCCESS);
+        dataTable.setMsg("查询成功");
+        dataTable.setRows(list);
+        dataTable.setTotal(total);
+        return dataTable;
     }
 
     /**
@@ -86,14 +121,16 @@ public class OrderController extends BaseController
     public AjaxResult export(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
-        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
-        
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
             return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
         }
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
+
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
         for (MergedOrderVO vo : list) {
@@ -109,7 +146,7 @@ public class OrderController extends BaseController
 
         // 转换为导出VO
         List<MergedOrderExportVO> exportList = convertToExportVO(list, false,loginUser);
-        
+
         // 如果数据量在限制范围内,正常导出
         ExcelUtil<MergedOrderExportVO> util = new ExcelUtil<>(MergedOrderExportVO.class);
         return util.exportExcel(exportList, "合并订单数据");
@@ -125,14 +162,16 @@ public class OrderController extends BaseController
     public AjaxResult exportDetails(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
-        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
-
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
             return AjaxResult.error("导出数据量超过限制,最多只能导出" + maxExportCount + "条数据,请缩小查询范围后重试");
         }
+        list = list.stream().filter(item -> StringUtils.isNotEmpty(item.getBankTransactionId())).collect(Collectors.toList());
+
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
 
         for (MergedOrderVO vo : list) {
@@ -165,7 +204,9 @@ public class OrderController extends BaseController
     public AjaxResult exportItems(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
 
         // 如果查询结果超过20000条,返回错误提示
@@ -190,7 +231,9 @@ public class OrderController extends BaseController
     public AjaxResult exportItemsDetails(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
 
         // 如果查询结果超过20000条,返回错误提示
@@ -211,7 +254,9 @@ public class OrderController extends BaseController
     public AjaxResult exportShipping(MergedOrderQueryParam param)
     {
         // 先查询数据,限制查询20001条,用于判断是否超过限制
-        PageHelper.startPage(1, maxExportCount + 1);
+        param.setExportFlag(1);
+        param.setPageNum(1);
+        param.setPageSize(maxExportCount + 1);
         List<MergedOrderVO> list = mergedOrderService.selectMergedOrderList(param);
         // 如果查询结果超过20000条,返回错误提示
         if (list != null && list.size() > maxExportCount) {
@@ -235,13 +280,13 @@ public class OrderController extends BaseController
 
         return list.stream().map(vo -> {
             MergedOrderExportVO exportVO = new MergedOrderExportVO();
-            
+
             // 订单基本信息(参考 FsStoreOrderItemExportVO 的顺序)
             exportVO.setOrderTypeName(vo.getOrderTypeName());
             exportVO.setOrderCode(vo.getOrderCode());
             exportVO.setStatus(vo.getStatus() != null ? String.valueOf(vo.getStatus()) : null);
             exportVO.setUserId(vo.getUserId());
-            
+
             // 产品信息
             exportVO.setProductName(StringUtils.isEmpty(vo.getProductName()) ? "产品被删除" : vo.getProductName());
             exportVO.setBarCode(vo.getBarCode());
@@ -261,7 +306,7 @@ public class OrderController extends BaseController
                 exportVO.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
                 exportVO.setUserAddress(ParseUtils.parseAddress(vo.getUserAddress()));
             }
-            
+
             // 时间信息
             exportVO.setCreateTime(vo.getCreateTime());
             exportVO.setPayTime(vo.getPayTime());
@@ -271,25 +316,25 @@ public class OrderController extends BaseController
             exportVO.setDeliverySn(vo.getDeliveryCode()); // 快递公司编号,合并订单暂无此字段
             exportVO.setDeliveryName(vo.getDeliveryName()); // 快递公司,合并订单暂无此字段
             exportVO.setDeliveryId(vo.getDeliveryId());
-            
+
             // 公司和销售信息
             exportVO.setCompanyName(vo.getCompanyName());
             exportVO.setCompanyUserNickName(vo.getCompanyUserNickName());
-            
+
             // 套餐信息
             exportVO.setPackageName(null); // 套餐名称,合并订单暂无此字段
             exportVO.setGroupBarCode(null); // 组合码,合并订单暂无此字段
-            
+
             // 凭证信息
             exportVO.setIsUpload(null); // 是否上传凭证,合并订单暂无此字段
             exportVO.setUploadTime(null); // 上传时间,合并订单暂无此字段
-            
+
             // 档期信息
             exportVO.setScheduleName(null); // 归属档期,合并订单暂无此字段
-            
+
             // 银行交易流水号
             exportVO.setBankTransactionId(vo.getBankTransactionId());
-            
+
             // 金额信息
             exportVO.setTotalPrice(vo.getTotalPrice());
             exportVO.setPayPrice(vo.getPayPrice());
@@ -304,7 +349,7 @@ public class OrderController extends BaseController
                 vo.setFPrice(BigDecimal.ZERO);
                 vo.setBankTransactionId("");
             }
-            
+
             return exportVO;
         }).collect(Collectors.toList());
     }

+ 53 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwExternalContactController.java

@@ -2,19 +2,24 @@ package com.fs.qw.controller;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.ServletUtils;
 import com.fs.qw.param.QwExternalContactParam;
 import com.fs.qw.param.QwTagSearchParam;
+import com.fs.qw.param.ResignedTransferParam;
+import com.fs.qw.param.TransferParam;
 import com.fs.qw.service.IQwExternalContactInfoService;
 import com.fs.qw.service.IQwTagService;
 import com.fs.qw.vo.QwExternalContactUnionIdExportVO;
 import com.fs.qw.vo.QwExternalContactVO;
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
+import org.apache.commons.collections.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -181,4 +186,52 @@ public class QwExternalContactController extends BaseController
         return R.ok().put("data",qwExternalContactInfoService.selectQwExternalContactInfoByExternalContactId(id));
     }
 
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:addUnassigned')")
+    @Log(title = "同步待转接", businessType = BusinessType.INSERT)
+    @PostMapping("/addUnassigned")
+    public R addUnassigned(@RequestBody QwExternalContact qwExternalContact)
+    {
+        return qwExternalContactService.syncQwExternalContactUnassigned(qwExternalContact.getCorpId());
+    }
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:transfer')")
+    @Log(title = "企业微信客户", businessType = BusinessType.UPDATE)
+    @PutMapping("/resignedTransfer")
+    public R resignedTransfer(@RequestBody ResignedTransferParam param)
+    {
+        if (ObjectUtil.isNotEmpty(param.getQwUserName())&&ObjectUtil.isNotEmpty(param.getType())&&param.getType().equals("1")){
+            QwExternalContactParam qwExternalContact =new QwExternalContactParam();
+            qwExternalContact.setQwUserName(param.getQwUserName());
+
+//            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//            qwExternalContact.setCompanyId(loginUser.getCompany().getCompanyId());
+            List<QwExternalContactVO> list = qwExternalContactService.selectQwExternalContactListVO(qwExternalContact);
+            if (!CollectionUtils.isEmpty(list)){
+                List<Long> ids = list.stream().map(QwExternalContactVO::getId).collect(Collectors.toList());
+                param.setIds(ids);
+            }
+        }
+
+        return qwExternalContactService.resignedTransfer(param);
+    }
+    /**
+     * 在职转接 分配客户
+     */
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:transfer')")
+    @Log(title = "企业微信客户", businessType = BusinessType.UPDATE)
+    @PutMapping("/transfer")
+    public R transfer(@RequestBody TransferParam param)
+    {
+        if (ObjectUtil.isNotEmpty(param.getQwUserName())&&ObjectUtil.isNotEmpty(param.getType())&&param.getType().equals("1")){
+            QwExternalContactParam qwExternalContact =new QwExternalContactParam();
+            qwExternalContact.setQwUserName(param.getQwUserName());
+//            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//            qwExternalContact.setCompanyId(loginUser.getCompany().getCompanyId());
+            List<QwExternalContactVO> list = qwExternalContactService.selectQwExternalContactListVO(qwExternalContact);
+            if (!CollectionUtils.isEmpty(list)){
+                List<Long> ids = list.stream().map(QwExternalContactVO::getId).collect(Collectors.toList());
+                param.setIds(ids);
+            }
+        }
+        return qwExternalContactService.transfer(param);
+    }
 }

+ 155 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwGroupChatController.java

@@ -0,0 +1,155 @@
+package com.fs.qw.controller;
+
+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.utils.ServletUtils;
+//import com.fs.company.service.impl.CompanyDeptServiceImpl;
+//import com.fs.framework.security.LoginUser;
+//import com.fs.framework.service.TokenService;
+import com.fs.qw.param.QwGroupChatParam;
+import com.fs.qw.service.IQwGroupChatService;
+//import com.fs.qw.service.IQwUserService;
+//import com.fs.qw.vo.QwGroupChatOptionsVO;
+import com.fs.qw.vo.QwGroupChatVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 客户群详情Controller
+ *
+ * @author fs
+ * @date 2024-06-20
+ */
+@RestController
+@RequestMapping("/qw/groupChat")
+public class QwGroupChatController extends BaseController
+{
+    @Autowired
+    private IQwGroupChatService qwGroupChatService;
+
+//    @Autowired
+//    private TokenService tokenService;
+
+//    @Autowired
+//    private CompanyDeptServiceImpl companyDeptService;
+//
+//
+//    @Autowired
+//    private IQwUserService iQwUserService;
+
+    /**
+     * 查询客户群详情列表总后台查看
+     */
+    @PreAuthorize("@ss.hasPermi('qw:groupChat:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwGroupChatParam qwGroupChat)
+    {
+        startPage();
+        List<QwGroupChatVO> list = qwGroupChatService.selectQwGroupChatListAdmin(qwGroupChat);
+        return getDataTable(list);
+    }
+
+//    /**
+//     * 查询我的部门客户群详情列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:groupChat:deptList')")
+//    @GetMapping("/deptList")
+//    public TableDataInfo deptList(QwGroupChatParam qwGroupChat)
+//    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//
+//        List<Long> combinedList = new ArrayList<>();
+//        //本部门
+//        Long deptId = loginUser.getUser().getDeptId();
+//        if (deptId!=null){
+//            combinedList.add(deptId);
+//        }
+//        //本部门的下级部门
+//        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+//        if (!deptList.isEmpty()){
+//            combinedList.addAll(deptList);
+//        }
+//        qwGroupChat.setCuDeptIdList(combinedList);
+//        qwGroupChat.setUserType(loginUser.getUser().getUserType());
+//
+//
+//        startPage();
+//        List<QwGroupChatVO> list = qwGroupChatService.selectQwGroupChatList(qwGroupChat);
+//        return getDataTable(list);
+//    }
+
+
+//    /**
+//     * 我的-查询客户群详情列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:groupChat:myList')")
+//    @GetMapping("/myList")
+//    public TableDataInfo myList(QwGroupChatParam qwGroupChat)
+//    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwGroupChat.setCompanyId(loginUser.getCompany().getCompanyId());
+//        qwGroupChat.setCompanyUserId(loginUser.getUser().getUserId());
+//        startPage();
+//        List<QwGroupChatVO> list = qwGroupChatService.selectQwGroupChatList(qwGroupChat);
+//        return getDataTable(list);
+//    }
+
+
+//    /**
+//     * 获取客户群详情详细信息
+//     */
+////    @PreAuthorize("@ss.hasPermi('qw:groupChat:query')")
+//    @GetMapping(value = "/{chatId}")
+//    public AjaxResult getInfo(@PathVariable("chatId") String chatId)
+//    {
+//        return AjaxResult.success(qwGroupChatService.selectQwGroupChatByChatId(chatId));
+//    }
+
+    /**
+     *  同步 插入客户群信息
+     */
+    @GetMapping("/cogradientGroupChat/{corpId}")
+    public R cogradientGroupChat(@PathVariable("corpId") String corpId) throws Exception {
+
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        Long companyId = loginUser.getCompany().getCompanyId();
+        List<String> qwUserIdList=new ArrayList<>();
+        return qwGroupChatService.cogradientGroupChat(corpId,qwUserIdList);
+    }
+
+//    /**
+//     *  同步 我的客户群信息
+//     */
+//    @GetMapping("/cogradientMyGroupChat/{corpId}")
+//    public R cogradientMyGroupChat(@PathVariable("corpId") String corpId) throws Exception {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//
+//        List<String> qwUserIdList = iQwUserService.selectQwUserListByCompanyUserId(loginUser.getUser().getUserId(), corpId);
+//
+//        return qwGroupChatService.cogradientGroupChat(corpId,qwUserIdList);
+//    }
+
+//    @GetMapping("/allList/{corpId}")
+//    public AjaxResult allList(@PathVariable("corpId") String corpId)
+//    {
+////        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+////        Long companyId = loginUser.getCompany().getCompanyId();
+//        List<QwGroupChatOptionsVO> list = qwGroupChatService.selectGroupChatOptionsVOList(corpId);
+//        return AjaxResult.success(list);
+//    }
+//    @GetMapping("/listAll")
+//    public AjaxResult listAll(String qwUserIds, String corpId, String sopId){
+//        List<QwGroupChatOptionsVO> list = qwGroupChatService.listAllByQwUserList(qwUserIds, corpId, sopId);
+//        return AjaxResult.success(list);
+//    }
+}

+ 71 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwGroupChatUserController.java

@@ -0,0 +1,71 @@
+package com.fs.qw.controller;
+
+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.framework.service.TokenService;
+import com.fs.qw.domain.QwGroupChatUser;
+import com.fs.qw.param.QwGroupChatUserDataType;
+import com.fs.qw.param.QwInsertGroupChatUserParam;
+import com.fs.qw.service.IQwGroupChatUserService;
+import com.fs.qw.vo.QwGroupChatUserVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 客户群成员列Controller
+ *
+ * @author fs
+ * @date 2024-06-20
+ */
+@RestController
+@RequestMapping("/qw/group_chat_user")
+public class QwGroupChatUserController extends BaseController
+{
+    @Autowired
+    private IQwGroupChatUserService qwGroupChatUserService;
+
+//    @Autowired
+//    private TokenService tokenService;
+
+    /**
+     * 查询客户群成员列列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(QwGroupChatUser qwGroupChatUser)
+    {
+        startPage();
+        List<QwGroupChatUserVO> list = qwGroupChatUserService.selectQwGroupChatUserList(qwGroupChatUser);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 获取客户群成员列详细信息
+     */
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwGroupChatUserService.selectQwGroupChatUserById(id));
+    }
+
+
+    /** 获取日星月的入群退群统计 */
+    @GetMapping("/getDataWeekMonthCount")
+    public R getDataWeekMonthCount(QwGroupChatUserDataType qwGroupChatUserDataType){
+
+        return qwGroupChatUserService.getDataWeekMonthCount(qwGroupChatUserDataType);
+    }
+
+    /**
+     *  同步 插入某个客户群信息
+     */
+    @PutMapping("/cogradientGroupChatUser")
+    public R cogradientGroupChatUser(@RequestBody QwInsertGroupChatUserParam param) {
+
+        return qwGroupChatUserService.cogradientGroupChatUser(param.getCorpId(), param.getChatId());
+    }
+}

+ 43 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwPushCountController.java

@@ -10,6 +10,8 @@ import com.fs.common.core.page.TableSupport;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.fastGpt.domain.FastGptPushTokenTotal;
+import com.fs.fastGpt.param.FastGptPushTokenDeptTotalParam;
+import com.fs.fastGpt.vo.FastGptPushTokenDeptTotalVO;
 import com.fs.qw.domain.QwPushCount;
 import com.fs.qw.service.IQwPushCountService;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -189,6 +191,47 @@ public class QwPushCountController extends BaseController {
 
 
 
+        list.add(sumTotal); // 将合计行添加到列表末尾
+
+        // 构造返回结果
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(total);
+        return rspData;
+    }
+    @PreAuthorize("@ss.hasPermi('qw:qwPushCount:tokenListDept')")
+    @GetMapping("/tokenListDept")
+    public TableDataInfo tokenListDept(FastGptPushTokenDeptTotalParam pushTokenInfo) {
+        List<FastGptPushTokenDeptTotalVO> list = qwPushCountService.selectFastGptPushTokenTotalDeptList(pushTokenInfo);
+
+
+        // 计算总和
+        FastGptPushTokenDeptTotalVO sumTotal = new FastGptPushTokenDeptTotalVO();
+        sumTotal.setCompanyName("合计"); // 假设有一个字段用于显示“合计”标签,具体字段名根据实际情况替换
+        Long sum = list.stream().mapToLong(FastGptPushTokenDeptTotalVO::getCount).sum(); // 假设有一个数字字段需要求和,具体字段名根据实际情况替换
+        sumTotal.setCount(sum); // 设置合计值,具体字段名根据实际情况替换
+
+        // 获取分页参数
+        PageDomain pageDomain = TableSupport.buildPageRequest();
+        Integer pageNum = pageDomain.getPageNum();
+        Integer pageSize = pageDomain.getPageSize();
+
+        int total = list.size();
+        // 在内存中进行分页处理
+        if (pageNum != null && pageSize != null) {
+            int fromIndex = (pageNum - 1) * pageSize;
+            int toIndex = Math.min(fromIndex + pageSize, total);
+
+            // 确保索引不越界
+            if (fromIndex < total) {
+                list = list.subList(fromIndex, toIndex);
+            } else {
+                list = new ArrayList<>(); // 返回空列表
+            }
+        }
+
         list.add(sumTotal); // 将合计行添加到列表末尾
 
         // 构造返回结果

+ 9 - 8
fs-admin/src/main/java/com/fs/qw/controller/QwSopTempController.java

@@ -58,6 +58,15 @@ public class QwSopTempController extends BaseController
     {
 
 //        List<QwSopTemp> list = qwSopTempService.selectQwSopTempList(qwSopTemp);
+
+        if(qwSopTemp.getPageNum() == null) {
+            qwSopTemp.setPageNum(1);
+        }
+        if(qwSopTemp.getPageSize() == null) {
+            qwSopTemp.setPageSize(10);
+        }
+
+        PageHelper.startPage(qwSopTemp.getPageNum(), qwSopTemp.getPageSize());
         List<QwSopTemp> list = qwSopTempService.selectQwSopTempListNew(qwSopTemp);
         // 收集所有需要查询的用户ID
         Set<Long> userIds = list.stream()
@@ -87,14 +96,6 @@ public class QwSopTempController extends BaseController
             });
         }
 
-        if(qwSopTemp.getPageNum() == null) {
-            qwSopTemp.setPageNum(1);
-        }
-        if(qwSopTemp.getPageSize() == null) {
-            qwSopTemp.setPageSize(10);
-        }
-
-        PageHelper.startPage(qwSopTemp.getPageNum(), qwSopTemp.getPageSize());
 
         return R.ok().put("data",new PageInfo<>(list));
     }

+ 83 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwTagGroupController.java

@@ -1,12 +1,19 @@
 package com.fs.qw.controller;
 
+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.qw.domain.QwTagGroup;
 import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.vo.QwTagGroupAddParam;
 import com.fs.qw.vo.QwTagGroupListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -50,4 +57,80 @@ public class QwTagGroupController extends BaseController
         List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(qwTagGroup);
         return getDataTable(list);
     }
+
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:sync')")
+    @Log(title = "同步标签", businessType = BusinessType.INSERT)
+    @PostMapping("/syncTag/{corpId}")
+    public R syncTag(@PathVariable("corpId") String corpId)
+    {
+
+        return qwTagGroupService.syncQwTagGroup(corpId);
+    }
+    /**
+     * 查询企微客户标签组列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwTagGroup qwTagGroup)
+    {
+        startPage();
+
+        List<QwTagGroupListVO> list = qwTagGroupService.selectQwTagGroupListVO(qwTagGroup);
+        return getDataTable(list);
+    }
+    /**
+     * 获取企微客户标签组详细信息
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:tagGroup:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwTagGroupService.selectQwTagGroupByIdVO(id));
+    }
+    /**
+     * 删除企微客户标签组
+     */
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:remove')")
+    @Log(title = "企微客户标签组", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R remove(@PathVariable Long[] ids)
+    {
+
+        return qwTagGroupService.deleteQwTagGroupByIds(ids);
+    }
+    /**
+     * 新增企微客户标签组
+     */
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:add')")
+    @Log(title = "企微客户标签组", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody QwTagGroupAddParam qwTagGroup)
+    {
+
+        return toAjax(qwTagGroupService.insertQwTagGroupParam(qwTagGroup));
+    }
+    /**
+     * 修改企微客户标签组
+     */
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:edit')")
+    @Log(title = "企微客户标签组", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody QwTagGroupAddParam qwTagGroup)
+    {
+
+        return toAjax(qwTagGroupService.updateQwTagGroupParam(qwTagGroup));
+    }
+    /**
+     * 导出企微客户标签组列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:tagGroup:export')")
+    @Log(title = "企微客户标签组", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(QwTagGroup qwTagGroup)
+    {
+
+        List<QwTagGroup> list = qwTagGroupService.selectQwTagGroupList(qwTagGroup);
+        ExcelUtil<QwTagGroup> util = new ExcelUtil<QwTagGroup>(QwTagGroup.class);
+        return util.exportExcel(list, "企微客户标签组数据");
+    }
 }

+ 713 - 83
fs-admin/src/main/java/com/fs/qw/controller/QwUserController.java

@@ -1,32 +1,39 @@
 package com.fs.qw.controller;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.constant.Constants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
-import com.fs.common.utils.ServletUtils;
-import com.fs.company.domain.Company;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.exception.user.UserPasswordNotMatchException;
+import com.fs.common.utils.MessageUtils;
+import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.impl.CompanyDeptServiceImpl;
+import com.fs.fastGpt.domain.FastGptRole;
+import com.fs.fastGpt.mapper.FastGptRoleMapper;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
 import com.fs.qw.domain.QwExternalContact;
-import com.fs.qw.domain.QwExternalContactTransferCompanyAudit;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.param.QwFsUserParam;
-import com.fs.qw.param.QwUserBingParam;
-import com.fs.qw.param.QwUserListParam;
+import com.fs.qw.param.*;
 import com.fs.qw.service.IQwDeptService;
 import com.fs.qw.service.IQwExternalContactTransferCompanyAuditService;
 import com.fs.qw.service.IQwUserService;
 import com.fs.qw.vo.QwOptionsVO;
 import com.fs.qw.vo.QwUserVO;
+import com.fs.qw.vo.UpdateSendTypeVo;
 import com.fs.qwApi.domain.QwExternalContactAllListResult;
 import com.fs.qwApi.domain.inner.ExternalContact;
 import com.fs.qwApi.domain.inner.ExternalContactInfo;
@@ -34,14 +41,18 @@ import com.fs.qwApi.domain.inner.FollowInfo;
 import com.fs.qwApi.param.QwExternalListParam;
 import com.fs.qwApi.service.QwApiService;
 import com.fs.voice.utils.StringUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
+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.web.bind.annotation.*;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import javax.annotation.Resource;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -77,26 +88,639 @@ public class QwUserController extends BaseController {
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
 
-    @GetMapping("/getQwUserAll")
-    public AjaxResult getQwUserAll(){
-        return AjaxResult.success(qwUserService.getQwUserAll());
+    @Autowired
+    private CompanyDeptServiceImpl companyDeptService;
+
+    @Resource
+    private AuthenticationManager authenticationManager;
+    @Autowired
+    private FastGptRoleMapper fastGptRoleMapper;
+
+    /**
+     * 查询企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:staffList')")
+    @GetMapping("/staffList")
+    public TableDataInfo staffList(QwUserListParam qwUser) {
+
+        // 添加企微部门查询条件
+        Long deptId = qwUser.getDeptId();
+        if(deptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (deptId!=null){
+                qwDeptIdList.add(deptId);
+            }
+            // 本部门的下级部门
+            List<Long> deptList = qwUserService.selectDeptByParentId(deptId,qwUser.getCorpId());
+            if (!deptList.isEmpty()){
+                qwDeptIdList.addAll(deptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
     }
 
     /**
-     * 获取企微信息
-     * **/
-    @GetMapping("/getQwUserInfo")
-    public R getQwUserInfo(QwFsUserParam param){
-        return R.ok().put("data",qwUserService.getQwUserInfo(param));
+     * 查询企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:staffListPost')")
+    @GetMapping("/staffListPost")
+    public TableDataInfo staffListPost(QwUserListParam qwUser) {
+        // 添加企微部门查询条件
+        Long deptId = qwUser.getDeptId();
+        if(deptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            qwDeptIdList.add(deptId);
+            // 本部门的下级部门
+            List<Long> deptList = qwUserService.selectDeptByParentId(deptId,qwUser.getCorpId());
+            if (!deptList.isEmpty()){
+                qwDeptIdList.addAll(deptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询我的企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:myStaffList')")
+    @GetMapping("/myStaffList")
+    public TableDataInfo myStaffList(QwUserListParam qwUser) {
+        startPage();
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+
+    /**
+     * 导出企微员工列表
+     * @param qwUser
+     * @return AjaxResult
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:export')")
+    @Log(title = "企微员工", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportStaff")
+    public AjaxResult export(QwUserListParam qwUser) {
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        ExcelUtil<QwUserVO> util = new ExcelUtil<QwUserVO>(QwUserVO.class);
+        return util.exportExcel(list, "企微员工数据");
+    }
+
+    /**
+     * 导出企微用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:export')")
+    @Log(title = "企微用户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(QwUser qwUser) {
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        ExcelUtil<QwUser> util = new ExcelUtil<QwUser>(QwUser.class);
+        return util.exportExcel(list, "企微用户数据");
+    }
+
+
+    /**
+     * 查询我的部门 企业微信员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:myDepartList')")
+    @GetMapping("/myDepartList")
+    public TableDataInfo myDepartList(QwUserListParam qwUser)
+    {
+
+        qwUser.setCompanyId(qwUser.getCompanyId());
+//        qwUser.setUserType(loginUser.getUser().getUserType());
+        List<Long> combinedList = new ArrayList<>();
+        //本部门
+        Long deptId = getLoginUser().getUser().getDeptId();
+        if (deptId!=null){
+            combinedList.add(deptId);
+        }
+        //本部门的下级部门
+        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+        if (!deptList.isEmpty()){
+            combinedList.addAll(deptList);
+        }
+
+        // 添加企微部门查询条件
+        Long qwDeptId = qwUser.getDeptId();
+        if(qwDeptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (qwDeptId!=null){
+                qwDeptIdList.add(qwDeptId);
+            }
+            // 本部门的下级部门
+            List<Long> qwDeptList = qwUserService.selectDeptByParentId(qwDeptId,qwUser.getCorpId());
+            if (!qwDeptList.isEmpty()){
+                qwDeptIdList.addAll(qwDeptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+
+
+        qwUser.setCuDeptIdList(combinedList);
+//        qwUser.setUserType(loginUser.getUser().getUserType());
+
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwIpad")
+    public R loginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.loginQwIpad(loginParam);
+    }
+
+
+    /**
+     * 查询部门下的 企业微信账号
+     */
+    @PostMapping("/getQwUserByDept")
+    public R getQwUserByDept(@RequestBody QwUserByDeptParam deptParam){
+        deptParam.setCompanyId(deptParam.getCompanyId());
+        return R.ok().put("data",qwUserService.getQwUserByDept(deptParam)) ;
+    }
+
+
+
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getQwIpad")
+    @RepeatSubmit
+    public R getQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/delQwIpad")
+    @RepeatSubmit
+    public R delQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.delQwIpad(loginParam);
+    }
+
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/qrCodeStatus")
+    public R qrCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeStatus(loginParam);
+    }
+    //输入验证码
+    @PostMapping("/qrCodeVerify")
+    public R qrCodeVerify(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.qrCodeVerify(loginParam);
+    }
+
+    @PostMapping("/outLoginQwIpad")
+    public R outLoginQwIpad(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.outLoginQwIpad(loginParam);
+    }
+
+    @PostMapping("/twoCode")
+    public R twoCode(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCode(loginParam);
+    }
+    @PostMapping("/twoCodeStatus")
+    public R TwoCodeStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getTwoCodeStatus(loginParam);
+    }
+
+    @PostMapping("/getQwIpadStatus")
+    public R getQwIpadStatus(@RequestBody QwLoginHookParam loginParam){
+        return qwUserService.getLoginQwIpadStatus(loginParam);
+    }
+    /**
+     * 直接授权key
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:authAppKey')")
+    @PostMapping("/authAppKey")
+    public R authAppKey(@RequestBody QwUser param){
+        return qwUserService.authAppKey(param);
+    }
+
+    /**
+     * 输入授权key
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:authAppKey')")
+    @PostMapping("/handleInputAuthAppKey")
+    public R handleInputAuthAppKey(@RequestBody QwUser param){
+        return qwUserService.handleInputAuthAppKey(param);
+    }
+
+
+    /**
+     * 登录企业微信(发起登录)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCode")
+    public R loginQwCode(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCode(loginParam);
+    }
+
+    /**
+     * 登录请求-刷新获取二维码
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCodeUrl")
+    public R loginQwCodeUrl(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCodeUrl(loginParam);
+    }
+    /**
+     * 取redis里的登录二维码
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getQwCodeUrl")
+    public R getQwCodeUrl(@RequestBody QwLoginParam loginParam) throws InterruptedException {
+        return qwUserService.getQwCodeUrl(loginParam);
+    }
+
+    /**
+     * 登录企业微信(传输验证信息)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/loginQwCodeMsg")
+    public R loginQwCodeMsg(@RequestBody QwLoginParam loginParam){
+        return qwUserService.loginQwCodeMsg(loginParam);
+    }
+
+    /**
+     * 退出企业微信(退出插件)
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/logoutQwLogout")
+    public R logoutQwLogout(@RequestBody QwLoginParam loginParam){
+        return qwUserService.logoutQwLogout(loginParam);
+    }
+
+//    /**
+//     * 企业微信(修改登录状态)
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+//    @PostMapping("/modifyLoginQwStatus")
+//    public R modifyLoginQwStatus(@RequestBody QwLoginParam loginParam){
+//        return qwUserService.modifyLoginQwStatus(loginParam);
+//    }
+//
+    /**
+     * 查询企业微信登录状态
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:login')")
+    @PostMapping("/getLoginQwStatus")
+    public R getLoginQwStatus(@RequestBody QwLoginParam loginParam){
+        return qwUserService.getLoginQwStatus(loginParam);
+    }
+
+    @PutMapping
+    public AjaxResult updateUser(@RequestBody QwUser qwUser){
+        return toAjax(qwUserService.updateQwUser(qwUser));
+    }
+
+    /**
+     * 自动发课启用禁用
+     * @param qwUser
+     * @return
+     */
+    @PostMapping("/updateIsAuto")
+    @PreAuthorize("@ss.hasPermi('qw:user:isauto')")
+    public AjaxResult updateIsAuto(@RequestBody QwUser qwUser){
+        return toAjax(qwUserService.updateQwUser(qwUser));
+    }
+
+    /**
+     * 企业微信员工账号 绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:loginIp')")
+    @Log(title = "绑定 云主机", businessType = BusinessType.INSERT)
+    @GetMapping("/qwBindCloudHost/{appKey}")
+    public R qwBindCloudHost(@PathVariable("appKey") String appKey){
+        return qwUserService.qwBindCloudHost(appKey);
+    }
+
+    /**
+     * 获取云主机的账密
+     *
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:cloudAP')")
+    @PostMapping("/selectCloudAP")
+    public R selectCloudAP(@RequestBody QwCloudAPParam param) throws Exception {
+        return qwUserService.selectCloudAP(param);
+    }
+
+    /**
+     * 根据销售账号密码 获取 他的所有企业微信账号以及云主机和账号密码
+     */
+    @PostMapping("/selectCloudByCompany")
+    public R selectCloudByCompany(@RequestBody QwCloudIPByCompanyParam param) throws Exception {
+
+        // 用户验证
+        Authentication authentication = null;
+        try
+        {
+            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+            authentication = authenticationManager
+                    .authenticate(new UsernamePasswordAuthenticationToken(param.getCompanyAdmin(), param.getCompanyPassWord()));
+        }
+        catch (Exception e)
+        {
+            if (e instanceof BadCredentialsException)
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(param.getCompanyAdmin(), Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+                throw new UserPasswordNotMatchException();
+            }
+            else
+            {
+                AsyncManager.me().execute(AsyncFactory.recordLogininfor(param.getCompanyAdmin(), Constants.LOGIN_FAIL, e.getMessage()));
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        LoginUser loginUser=(LoginUser) authentication.getPrincipal();
+
+        return qwUserService.selectCloudByCompany(loginUser.getUser().getCompanyId(),loginUser.getUser().getUserId());
+    }
+
+
+    /**
+     * 企业微信员工账号 绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:bindIp')")
+    @GetMapping("/qwBindCloudHostByIp/{appKey}/{IP}")
+    public R qwBindCloudHostByIp(@PathVariable("appKey") String appKey,@PathVariable("IP") String IP){
+        return qwUserService.qwBindCloudHostByIp(appKey,IP);
+    }
+
+    /**
+     * 企业微信员工账号 解除绑定 云主机
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:loginIpOut')")
+    @Log(title = "解除绑定 云主机", businessType = BusinessType.UPDATE)
+    @GetMapping("/qwUnbindCloudHost/{appKey}")
+    public R qwUnbindCloudHost(@PathVariable("appKey") String appKey){
+        return qwUserService.qwUnbindCloudHost(appKey);
+    }
+
+    /**
+     * 根据销售名称模糊查询
+     * @param qwUserName  名称
+     * @return  list
+     */
+    @GetMapping("/getQwUserListLikeName")
+    public R getQwUserListLikeName(@RequestParam(required = false) String qwUserName,
+                                   @RequestParam(required = false, defaultValue = "1") Integer pageNum,
+                                   @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+        Map<String,Object> params = new HashMap<>();
+        params.put("qwUserName", qwUserName);
+
+        PageHelper.startPage(pageNum, pageSize);
+        List<QwOptionsVO> qwUserList = companyUserService.selectQwUserListLikeName(params);
+        return R.ok().put("data", new PageInfo<>(qwUserList));
+    }
+
+    /**
+     * 查询企微用户列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwUserListParam qwUser)
+    {
+        startPage();
+        qwUser.setCompanyId(qwUser.getCompanyId());
+        if (ObjectUtil.isNotEmpty(qwUser.getIsRemark())&&qwUser.getIsRemark().equals("1")){
+            qwUser.setCompanyUserId(getLoginUser().getUser().getUserId());
+        }else if (ObjectUtil.isNotEmpty(qwUser.getIsRemark())&&qwUser.getIsRemark().equals("2")){
+            qwUser.setDeptId(getLoginUser().getDeptId());
+            qwUser.setCorpId(null);
+        }
+
+        List<QwUserVO> list = qwUserService.selectQwUserListVO(qwUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询企微用户列表
+     */
+
+    @GetMapping("/userList")
+    public TableDataInfo userList(QwUserListParam qwUser)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<QwUserVO> list = qwUserService.selectAllQwUserListVO(qwUser);
+        return getDataTable(list);
+    }
+    //    /**
+//     * 查询我的企微用户列表
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:myList')")
+//    @GetMapping("/myList")
+//    public TableDataInfo myList(QwUserParam qwUser)
+//    {
+//        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+//        qwUser.setCompanyUserId(loginUser.getCompany().getUserId());
+//        List<QwUserVO> list = qwUserService.selectQwUserListVO(qwUser);
+//        return getDataTable(list);
+//    }
+    @GetMapping("/getQwAllUserList")
+    public R getQwAllUserList(@RequestParam String corpId, @RequestParam Long companyId)
+    {
+        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId, companyId);
+        return  R.ok().put("data",list);
+    }
+    /**
+     * 查询企微用户列表-下拉框
+     */
+    @GetMapping("/qwList")
+    public TableDataInfo getQwList(QwUser qwUser)
+    {
+        startPage();
+
+        if(ObjectUtil.notEqual(qwUser.getDisableCompanyId(),1)){
+            qwUser.setCompanyId(qwUser.getCompanyId());
+        }
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        return getDataTable(list);
+    }
+    /**
+     * 查询企微用户列表-下拉框
+     */
+    @PostMapping("/qwCompanyList")
+    public TableDataInfo qwCompanyList(@RequestBody QwUser qwUser)
+    {
+        startPage();
+        List<QwUser> list = qwUserService.selectQwUserList(qwUser);
+        return getDataTable(list);
+    }
+    /**
+     * 查询企微员工列表-用于员工管理绑定
+     */
+    @GetMapping("/getQwUserList")
+    public R getQwUserList()
+    {
+        QwUserParam qwUser = new QwUserParam();
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        qwUser.setCorpId(strings);
+        if (strings==null||strings.size()==0){
+            return  R.ok().put("data",null);
+        }
+        qwUser.setIsDel(0);
+        List<QwUserVO> list = qwUserService.selectQwUserListBindVO(qwUser);
+        return  R.ok().put("data",list);
+    }
+
+    @GetMapping("/getMyQwUserList")
+    public R getMyQwUserList()
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwUserListOptionsVOByCompanyUserId(getLoginUser().getUser().getUserId());
+        return  R.ok().put("data",list);
+    }
+    @GetMapping("/getMyQwCompanyList/{companyId}")
+    public R getMyQwCompanyList(@PathVariable Long companyId)
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOByCompanyId(companyId);
+        return  R.ok().put("data",list);
     }
 
-   @GetMapping("/getMyQwCompanyList")
-    public R getMyQwCompanyList()
+    @GetMapping("/getMyQwCompanyListAll")
+    public R getMyQwCompanyListAll()
+    {
+        List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOAll();
+        return  R.ok().put("data",list);
+    }
+
+
+    @GetMapping("/getQwCompanyList")
+    public R getQwCompanyList()
     {
         List<QwOptionsVO> list = qwUserService.selectQwCompanyListOptionsVOAll();
         return  R.ok().put("data",list);
     }
 
+    /**
+     * 获取企微用户详细信息
+     */
+
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwUserService.selectQwUserVOById(id));
+    }
+    /**
+     * 批量查询 企微用户详细信息
+     */
+    @GetMapping(value = "/getInfo/{ids}")
+    public AjaxResult getInfoByIds(@PathVariable("ids") Long[] ids)
+    {
+        return AjaxResult.success(qwUserService.selectQwUserVOByIds(ids));
+    }
+
+
+//    /**
+//     * 新增企微用户
+//     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:add')")
+//    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+//    @PostMapping
+//    public R add()
+//    {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//
+//        return R.ok(qwUserService.syncQwUser(loginUser.getCompany().getCompanyId()));
+//    }
+
+
+
+    /**
+     * 同步企微用户
+     */
+    @RepeatSubmit
+    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
+    @Log(title = "企微用户", businessType = BusinessType.INSERT)
+    @PostMapping("sync/{corpId}")
+    public R sync(@PathVariable String corpId)
+    {
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        for (String string : strings) {
+
+            if (string.equals(corpId)){
+                qwUserService.syncQwUser(string);
+                qwDeptService.insertOrUpdateQwDept(string);
+                logger.info("同步完成");
+            }
+        }
+        return R.ok();
+    }
+    @RepeatSubmit
+    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
+    @Log(title = "同步企微用户名称", businessType = BusinessType.INSERT)
+    @PostMapping("syncName/{corpId}")
+    public R syncName(@PathVariable String corpId)
+    {
+        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByCompanyId(getLoginUser().getUser().getCompanyId());
+        for (String string : strings) {
+            if (string.equals(corpId)){
+                qwUserService.syncQwUserName(string);
+            }
+        }
+        return R.ok();
+    }
+    /**
+     * 绑定AI客服
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:user:bindAi')")
+    @Log(title = "企微用户", businessType = BusinessType.UPDATE)
+    @PutMapping("/bindAi")
+    @RepeatSubmit
+    public R edit(@RequestBody QwUserBindAi param)
+    {
+        QwUser qwUser=new QwUser();
+        qwUser.setId(param.getId());
+        qwUser.setFastGptRoleId(param.getFastGptRoleId());
+        FastGptRole role = fastGptRoleMapper.selectFastGptRoleByRoleId(param.getFastGptRoleId());
+
+        if (role.getBindCorpId()!=null){
+            if (role.getBindCorpId().equals(param.getCorpId())){
+                qwUserService.updateQwUser(qwUser);
+            }else {
+                return R.error("该角色已绑定其他企业");
+            }
+        }else {
+            int i = qwUserService.updateQwUser(qwUser);
+
+            FastGptRole fastGptRole=new FastGptRole();
+            fastGptRole.setRoleId(param.getFastGptRoleId());
+            fastGptRole.setBindCorpId(param.getCorpId());
+            fastGptRoleMapper.updateFastGptRole(fastGptRole);
+
+            if (i>0){
+                return R.ok();
+            }else {
+                return R.error("绑定失败");
+            }
+        }
+
+        return R.ok();
+    }
+
+    /**
+     * 解除应用绑定
+     */
+//    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptRole:relieve')")
+    @Log(title = "解除应用", businessType = BusinessType.UPDATE)
+    @GetMapping("/relieveFastGptRoleById/{id}")
+    public R relieveFastGptRoleById(@PathVariable("id") Long id)
+    {
+        return qwUserService.relieveFastGptRoleById(id);
+    }
+
     /**
      * 绑定企微用户
      */
@@ -188,6 +812,7 @@ public class QwUserController extends BaseController {
         }
         return R.error("绑定失败");
 
+
     }
 
 
@@ -201,6 +826,52 @@ public class QwUserController extends BaseController {
         syncMyQwExternalContact(qu,null);
     }
 
+    /** 修改企微用户的欢迎语 */
+    @PostMapping("/weclomeQwUser")
+    public R weclomeQwUser(@RequestBody QwUser qwUser) throws Exception {
+
+
+        return  qwUserService.weclomeQwUser(qwUser);
+    }
+
+    /**
+     * 删除企微用户
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:remove')")
+    @Log(title = "企微用户", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(qwUserService.deleteQwUserByIds(ids));
+    }
+
+
+    /**
+     * 获取企业微信用户列表
+     */
+    @GetMapping("/qwUserList/{corpId}")
+    public TableDataInfo qwUserList(@PathVariable String corpId)
+    {
+
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        Long companyId = loginUser.getCompany().getCompanyId();
+
+        List<QwOptionsVO> list = qwUserService.selectQwUserListOptionsVO(corpId);
+        return getDataTable(list);
+    }
+    @GetMapping("/getQwUserAll")
+    public AjaxResult getQwUserAll(){
+        return AjaxResult.success(qwUserService.getQwUserAll());
+    }
+
+    /**
+     * 获取企微信息
+     * **/
+    @GetMapping("/getQwUserInfo")
+    public R getQwUserInfo(QwFsUserParam param){
+        return R.ok().put("data",qwUserService.getQwUserInfo(param));
+    }
+
     public R  syncMyQwExternalContact(QwUser qwUser,String getNextCursor )  {
 
         String qwUserId = qwUser.getQwUserId();
@@ -286,79 +957,38 @@ public class QwUserController extends BaseController {
         return R.ok();
     }
 
-
-    /**
-     * 同步企微用户
-     */
-    @RepeatSubmit
-    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
-    @Log(title = "企微用户", businessType = BusinessType.INSERT)
-    @PostMapping("sync/{corpId}")
-    public R sync(@PathVariable("corpId") String corpId)
-    {
-
-        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
-        for (String string : strings) {
-
-            if (string.equals(corpId)){
-                qwUserService.syncQwUser(string);
-                qwDeptService.insertOrUpdateQwDept(string);
-                logger.info("同步完成");
-            }
-        }
-        return R.ok();
-    }
-
-    /**
-     * 获取企微用户详细信息
-     */
-
-    @GetMapping(value = "/{id}")
-    public AjaxResult getInfo(@PathVariable("id") Long id)
-    {
-        return AjaxResult.success(qwUserService.selectQwUserVOById(id));
-    }
-    /**
-     * 批量查询 企微用户详细信息
-     */
-    @GetMapping(value = "/getInfo/{ids}")
-    public AjaxResult getInfoByIds(@PathVariable("ids") Long[] ids)
-    {
-        return AjaxResult.success(qwUserService.selectQwUserVOByIds(ids));
+    //    /**
+//     * 重启云主机
+//     * @return
+//     */
+//    @PutMapping("/restartHost")
+//    public R restartCloudHost(@RequestParam String serverIp) {
+//        return qwUserService.restartCloudHost(serverIp);
+//    }
+    @PostMapping("/updateSendType")
+    public R updateSendType(@RequestBody UpdateSendTypeVo vo) {
+        return qwUserService.updateSendType(vo);
     }
 
-    @RepeatSubmit
-    @PreAuthorize("@ss.hasPermi('qw:user:sync')")
-    @Log(title = "同步企微用户名称", businessType = BusinessType.INSERT)
-    @PostMapping("syncName/{corpId}")
-    public R syncName(@PathVariable("corpId") String corpId)
-    {
-        List<String> strings = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
-        for (String string : strings) {
-            if (string.equals(corpId)){
-                qwUserService.syncQwUserName(string);
-            }
-        }
+    @GetMapping("/changeVideoStatus")
+    public R changeVideoStatus(Long id) {
+        qwUserService.changeVideoStatus(id);
         return R.ok();
     }
 
-
-    /**
-     * 查询企微用户列表
-     */
-
-    @GetMapping("/userList")
-    public TableDataInfo userList(QwUserListParam qwUser)
+    @GetMapping("/companyQwUserlist")
+    public TableDataInfo companyQwUserlist(@RequestParam Long companyId,
+                                           @RequestParam String corpId,
+                                           @RequestParam(required = false) String nickName)
     {
         startPage();
-        List<QwUserVO> list = qwUserService.selectAllQwUserListVO(qwUser);
+        List<QwUserVO> list = qwUserService.selectQwUserListByCompanyIdAndCorpIdAndNickName(companyId, corpId, nickName);
         return getDataTable(list);
     }
 
-    @GetMapping("/getQwAllUserList")
-    public R getQwAllUserList(@RequestParam String corpId, @RequestParam Long companyId)
+    @GetMapping("/updateFastGptRoleStatusById/{id}")
+    public R updateFastGptRoleStatusById(@PathVariable Long id)
     {
-        List<QwUserVO> list = companyUserService.selectCompanyQwUserList(corpId, companyId);
-        return  R.ok().put("data",list);
+        return qwUserService.updateQwUserFastGptRoleStatusById(id);
     }
 }

+ 241 - 0
fs-admin/src/main/java/com/fs/qw/controller/QwWorkTaskNewController.java

@@ -0,0 +1,241 @@
+package com.fs.qw.controller;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.service.impl.CompanyDeptServiceImpl;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+//import com.fs.framework.security.LoginUser;
+//import com.fs.framework.service.TokenService;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwWorkTask;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.param.QwWorkTaskListParam;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.IQwWorkTaskService;
+import com.fs.qw.vo.QwWorkTaskAllListVO;
+import com.fs.qw.vo.QwWorkTaskListVO;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 企微任务看板Controller
+ *
+ * @author fs
+ * @date 2025-03-25
+ */
+@RestController
+@RequestMapping("/qw/QwWorkTaskNew")
+public class QwWorkTaskNewController extends BaseController
+{
+    @Autowired
+    private IQwWorkTaskService qwWorkTaskService;
+//    @Autowired
+//    private TokenService tokenService;
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Autowired
+    private CompanyDeptServiceImpl companyDeptService;
+
+    /**
+     * 查询企微任务看板列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwWorkTaskListParam qwWorkTask)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwWorkTask.setCompanyId(loginUser.getCompany().getCompanyId());
+//        qwWorkTask.setCompanyUserId(loginUser.getUser().getUserId());
+        List<QwWorkTaskListVO> list = qwWorkTaskService.selectQwWorkTaskListVO(qwWorkTask);
+
+        for (QwWorkTaskListVO qwWorkTaskListVO : list) {
+            qwWorkTaskListVO.setLogs(fsCourseWatchLogMapper.selectFsCourseWatchLog7DayByExtId(qwWorkTaskListVO.getExtId()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询企微任务部门催课看板列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:deptList')")
+//    @GetMapping("/deptList")
+//    public TableDataInfo deptList(QwWorkTaskListParam qwWorkTask)
+//    {
+//
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwWorkTask.setCompanyId(loginUser.getCompany().getCompanyId());
+//
+//        List<Long> combinedList = new ArrayList<>();
+//        //本部门
+//        Long deptId = loginUser.getUser().getDeptId();
+//        if (deptId!=null){
+//            combinedList.add(deptId);
+//        }
+//        //本部门的下级部门
+//        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+//        if (!deptList.isEmpty()){
+//            combinedList.addAll(deptList);
+//        }
+//
+//        qwWorkTask.setCuDeptIdList(combinedList);
+//        qwWorkTask.setUserType(loginUser.getUser().getUserType());
+//
+//
+//        startPage();
+//        List<QwWorkTaskListVO> list = qwWorkTaskService.selectQwWorkTaskListVO(qwWorkTask);
+//        for (QwWorkTaskListVO qwWorkTaskListVO : list) {
+//            qwWorkTaskListVO.setLogs(fsCourseWatchLogMapper.selectFsCourseWatchLog7DayByExtId(qwWorkTaskListVO.getExtId()));
+//        }
+//        return getDataTable(list);
+//    }
+
+    @GetMapping("/glList")
+    public TableDataInfo glList(QwWorkTaskListParam qwWorkTask)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwWorkTask.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<QwWorkTaskListVO> list = qwWorkTaskService.selectQwWorkTaskListVO(qwWorkTask);
+        for (QwWorkTaskListVO qwWorkTaskListVO : list) {
+            qwWorkTaskListVO.setLogs(fsCourseWatchLogMapper.selectFsCourseWatchLog7DayByExtId(qwWorkTaskListVO.getExtId()));
+        }
+        return getDataTable(list);
+    }
+
+    @GetMapping("/allList")
+    public TableDataInfo allList(QwWorkTaskListParam qwWorkTask)
+    {
+        startPage();
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        qwWorkTask.setCompanyId(loginUser.getCompany().getCompanyId());
+        if (qwWorkTask.getSTime()==null||qwWorkTask.getETime()==null){
+            return new TableDataInfo();
+        }
+        List<QwWorkTaskAllListVO> list = qwWorkTaskService.selectQwWorkTaskAllListVO(qwWorkTask);
+
+        return getDataTable(list);
+    }
+    /**
+     * 导出企微任务看板列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:export')")
+    @Log(title = "企微任务看板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(QwWorkTask qwWorkTask)
+    {
+        List<QwWorkTask> list = qwWorkTaskService.selectQwWorkTaskList(qwWorkTask);
+        ExcelUtil<QwWorkTask> util = new ExcelUtil<QwWorkTask>(QwWorkTask.class);
+        return util.exportExcel(list, "企微任务看板数据");
+    }
+
+    /**
+     * 获取企微任务看板详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(qwWorkTaskService.selectQwWorkTaskById(id));
+    }
+
+    /**
+     * 新增企微任务看板
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:add')")
+    @Log(title = "企微任务看板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody QwWorkTask qwWorkTask)
+    {
+        return toAjax(qwWorkTaskService.insertQwWorkTask(qwWorkTask));
+    }
+    @Autowired
+    QwApiService qwApiService;
+    @Autowired
+    IQwExternalContactService qwExternalContactService;
+    @Autowired
+    QwExternalContactMapper qwExternalContactMapper;
+    /**
+     * 修改企微任务看板
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:edit')")
+    @Log(title = "企微任务看板处理", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody QwWorkTask qwWorkTask)
+    {
+        QwWorkTask task = new QwWorkTask();
+        task.setId(qwWorkTask.getId());
+        task.setStatus(1);
+        task.setTrackType(qwWorkTask.getTrackType());
+        task.setDescription(qwWorkTask.getDescription());
+        task.setUpdateTime(new Date());
+        if (task.getDescription()!=null&& !task.getDescription().isEmpty()){
+
+            QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactById(qwWorkTask.getExtId());
+            if (qwExternalContact!=null){
+                QwExternalContactRemarkParam param = new QwExternalContactRemarkParam();
+                param.setUserid(qwExternalContact.getUserId());
+                param.setExternal_userid(qwExternalContact.getExternalUserId());
+                param.setDescription(task.getDescription());
+
+                QwExternalContactRemarkResult qwExternalContactRemarkResult = qwApiService.externalcontactRemark(param, qwExternalContact.getCorpId());
+                logger.info("QwExternalContactRemarkResult206:" + qwExternalContactRemarkResult);
+                if (qwExternalContactRemarkResult.getErrcode() == 0) {
+                    QwExternalContact ext = new QwExternalContact();
+                    ext.setId(qwExternalContact.getId());
+                    ext.setDescription(task.getDescription());
+                    qwExternalContactMapper.updateQwExternalContact(ext);
+                }
+            }
+        }
+        return toAjax(qwWorkTaskService.updateQwWorkTask(task));
+    }
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:edit')")
+    @Log(title = "企微任务看板处理", businessType = BusinessType.UPDATE)
+    @PutMapping("/edit2")
+    public AjaxResult edit2(@RequestBody QwWorkTask qwWorkTask)
+    {
+        QwWorkTask task = new QwWorkTask();
+        task.setId(qwWorkTask.getId());
+        task.setStatus(1);
+        task.setUpdateTime(new Date());
+        task.setTrackType(2);
+        return toAjax(qwWorkTaskService.updateQwWorkTask(task));
+    }
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:edit')")
+    @Log(title = "企微任务看板处理", businessType = BusinessType.UPDATE)
+    @PutMapping("/edit3")
+    public AjaxResult edit3(@RequestBody QwWorkTask qwWorkTask)
+    {
+        QwWorkTask task = new QwWorkTask();
+        task.setId(qwWorkTask.getId());
+        task.setStatus(1);
+        task.setUpdateTime(new Date());
+        task.setTrackType(3);
+        return toAjax(qwWorkTaskService.updateQwWorkTask(task));
+    }
+    /**
+     * 删除企微任务看板
+     */
+    @PreAuthorize("@ss.hasPermi('qw:QwWorkTaskNew:remove')")
+    @Log(title = "企微任务看板", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(qwWorkTaskService.deleteQwWorkTaskByIds(ids));
+    }
+}

+ 186 - 0
fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java

@@ -0,0 +1,186 @@
+package com.fs.user.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.his.service.IFsUserIntegralLogsService;
+import com.fs.his.service.IFsUserService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 添加积分参数
+ */
+class AddIntegralParam {
+    private Long userId;
+    private Integer logType;
+    private Long integral;
+
+    public Long getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Long userId) {
+        this.userId = userId;
+    }
+
+    public Integer getLogType() {
+        return logType;
+    }
+
+    public void setLogType(Integer logType) {
+        this.logType = logType;
+    }
+
+    public Long getIntegral() {
+        return integral;
+    }
+
+    public void setIntegral(Long integral) {
+        this.integral = integral;
+    }
+}
+
+/**
+ * 用户积分管理Controller 提供给卓美 完成直播积分发放
+ *
+ * @author fs
+ * @date 2025-01-01
+ */
+@Api("用户积分管理")
+@RestController
+@RequestMapping("/user/integral")
+public class FsUserIntegralController {
+
+    @Autowired
+    private IFsUserService userService;
+
+    @Autowired
+    private IFsUserIntegralLogsService integralLogsService;
+
+    @Autowired
+    private FsUserMapper userMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper integralLogsMapper;
+
+    /**
+     * 查询用户列表(支持手机号和昵称筛选)
+     */
+    @ApiOperation("查询用户列表")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam(required = false) String phone,
+                              @RequestParam(required = false) String nickName,
+                              @RequestParam(defaultValue = "1") Integer pageNum,
+                              @RequestParam(defaultValue = "10") Integer pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        
+        // 构建查询条件
+        FsUser queryUser = new FsUser();
+        if (StringUtils.isNotEmpty(phone)) {
+            queryUser.setPhone(phone);
+        }
+        if (StringUtils.isNotEmpty(nickName)) {
+            queryUser.setNickName(nickName);
+            queryUser.setNickname(nickName);
+        }
+        
+        // 查询用户列表(只返回需要的字段)
+        List<FsUser> userList = userMapper.selectFsUserListForIntegral(queryUser);
+        
+        PageInfo<FsUser> pageInfo = new PageInfo<>(userList);
+        TableDataInfo dataTable = new TableDataInfo();
+        dataTable.setCode(200);
+        dataTable.setMsg("查询成功");
+        dataTable.setRows(pageInfo.getList());
+        dataTable.setTotal(pageInfo.getTotal());
+        return dataTable;
+    }
+
+    /**
+     * 查询用户积分明细
+     */
+    @ApiOperation("查询用户积分明细")
+    @GetMapping("/logs/{userId}")
+    public R getIntegralLogs(@PathVariable Long userId,
+                             @RequestParam(defaultValue = "1") Integer pageNum,
+                             @RequestParam(defaultValue = "10") Integer pageSize,
+                             @RequestParam(required = false) Integer logType) {
+        PageHelper.startPage(pageNum, pageSize);
+        FsUserIntegralLogs queryLogs = new FsUserIntegralLogs();
+        queryLogs.setUserId(userId);
+        if (logType != null) {
+            queryLogs.setLogType(logType);
+        }
+        // 按创建时间倒序查询(Mapper XML 中已添加排序)
+        List<FsUserIntegralLogs> logsList = integralLogsService.selectFsUserIntegralLogsList(queryLogs);
+        PageInfo<FsUserIntegralLogs> pageInfo = new PageInfo<>(logsList);
+        return R.ok().put("data", pageInfo.getList()).put("total", pageInfo.getTotal());
+    }
+
+    /**
+     * 添加积分
+     */
+    @ApiOperation("添加积分")
+    @PostMapping("/add")
+    public R addIntegral(@RequestBody AddIntegralParam param) {
+        Long userId = param.getUserId();
+        Integer logType = param.getLogType();
+        Long integral = param.getIntegral();
+        // 查询用户信息
+        FsUser user = userService.selectFsUserByUserId(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+
+        // 查询用户最新的积分记录
+        FsUserIntegralLogs latestLog = integralLogsMapper.selectLatestIntegralLogByUserId(userId);
+        
+        // 计算新的积分余额
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + integral;
+        
+        // 创建时间默认往后移动2个小时,使用整点的创建时间和更新时间
+        Date now = new Date();
+        LocalDateTime localDateTime = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());
+        // 往后移动2小时
+        localDateTime = localDateTime.plusHours(2);
+        // 设置为整点(分钟和秒都设为0)
+        localDateTime = localDateTime.withMinute(0).withSecond(0).withNano(0);
+        Date createTime = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        Date updateTime = createTime;
+
+        // 更新用户积分
+        user.setIntegral(newIntegral);
+
+        userService.increaseIntegral(Collections.singletonList(user.getUserId()),integral);
+
+        // 创建积分记录
+        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+        integralLogs.setUserId(userId);
+        integralLogs.setLogType(logType);
+        integralLogs.setIntegral(integral);
+        integralLogs.setBalance(newIntegral);
+        integralLogs.setCreateTime(createTime);
+        integralLogs.setUpdateTime(updateTime);
+        integralLogsService.insertFsUserIntegralLogs(integralLogs);
+
+        return R.ok("添加积分成功");
+    }
+}

+ 5 - 0
fs-common/src/main/java/com/fs/common/annotation/RateLimiter.java

@@ -37,4 +37,9 @@ public @interface RateLimiter
      * 限流类型
      */
     public LimitType limitType() default LimitType.DEFAULT;
+
+    /**
+     * 消息提示
+     */
+    public String msg() default "访问过于频繁,请稍后再试";
 }

+ 16 - 0
fs-common/src/main/java/com/fs/common/constant/RedisConstant.java

@@ -0,0 +1,16 @@
+package com.fs.common.constant;
+/**
+ * 库存与锁相关常量(Java 8 静态常量优化)
+ */
+public class RedisConstant {
+    // 库存Key前缀
+    public static final String STOCK_KEY_PREFIX = "product:stock:";
+    // 分布式锁Key前缀
+    public static final String LOCK_KEY_PREFIX = "product:lock:";
+    // 锁过期时间(30秒,避免死锁,大于业务执行时间)
+    public static final long LOCK_EXPIRE_SECONDS = 3L;
+    // 锁重试间隔(50毫秒,非阻塞重试,避免线程阻塞)
+    public static final long LOCK_RETRY_INTERVAL = 100L;
+    // 锁最大重试次数(3次,避免无限重试)
+    public static final int LOCK_MAX_RETRY = 20;
+}

+ 172 - 0
fs-common/src/main/java/com/fs/common/core/redis/service/StockDeductService.java

@@ -0,0 +1,172 @@
+package com.fs.common.core.redis.service;
+
+import com.fs.common.constant.RedisConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.IntStream;
+
+/**
+ * 高并发库存扣减服务(Java 8 + Redis分布式锁)
+ */
+@Slf4j
+@Service
+public class StockDeductService {
+
+    // 注入RedisTemplate
+    public final RedisTemplate<String, Object> redisTemplate;
+
+    // 构造器注入(Spring 推荐,Java 8 支持)
+    public StockDeductService(RedisTemplate<String, Object> redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    // 库存扣减Lua脚本(预编译,提升高并发性能)
+    private static final DefaultRedisScript<Long> STOCK_DEDUCT_SCRIPT;
+
+    // 库存扣减Lua脚本(优化后,增强健壮性)
+    static {
+        // 初始化库存扣减脚本
+        STOCK_DEDUCT_SCRIPT = new DefaultRedisScript<>();
+        STOCK_DEDUCT_SCRIPT.setScriptText("if redis.call('exists', KEYS[1]) ~= 1 then " + "return -2; " + "end " + "local stock_str = redis.call('get', KEYS[1]); " + "local stock = tonumber(stock_str); " + "if stock == nil then " + "return -3; " + "end " + "local deductNum_str = ARGV[1]; " + "local deductNum = tonumber(deductNum_str); " + "if deductNum == nil or deductNum <= 0 then " + "return -4; " + "end " + "if stock >= deductNum then " + "return redis.call('decrby', KEYS[1], deductNum); " + "else " + "return -1; " + "end");
+        STOCK_DEDUCT_SCRIPT.setResultType(Long.class);
+    }
+
+    /**
+     * 初始化商品库存(Redis)
+     *
+     * @param productId 商品ID
+     * @param initStock 初始库存
+     */
+    public void initStock(Long productId, Long liveId, Integer initStock) {
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+        redisTemplate.opsForValue().set(stockKey, initStock);
+        log.info("商品" + productId + "库存初始化完成,初始库存:" + initStock);
+    }
+
+    /**
+     * 同步获取商品库存(基础版,适用于低并发/实时性要求高的场景)
+     *
+     * @param productId 商品ID
+     * @param liveId    直播间ID(贴合原有库存Key的拆分粒度)
+     * @return 库存数量(库存不存在/非数字时返回0)
+     */
+    public Integer getStock(Long productId, Long liveId) {
+        // 1. 参数合法性校验(避免空ID导致Redis Key异常)
+        if (productId == null || liveId == null) {
+            log.warn("获取库存失败:商品ID/直播间ID为空");
+            return 0;
+        }
+
+        // 2. 拼接库存Key(与初始化/扣减逻辑保持一致)
+        String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+
+        try {
+            // 3. 读取Redis库存值(Java 8 Optional处理空值)
+            Object stockObj = redisTemplate.opsForValue().get(stockKey);
+            Integer stock = Optional.ofNullable(stockObj)
+                    // 处理Redis值非数字的情况(如脏数据)
+                    .map(val -> {
+                        if (val instanceof Integer) {
+                            return (Integer) val;
+                        } else if (val instanceof String) {
+                            try {
+                                return Integer.parseInt((String) val);
+                            } catch (NumberFormatException e) {
+                                log.error("库存值格式异常,Key:{},值:{}", stockKey, val, e);
+                                return 0;
+                            }
+                        } else {
+                            log.error("库存值类型不支持,Key:{},类型:{}", stockKey, val.getClass().getName());
+                            return 0;
+                        }
+                    })
+                    // 库存Key不存在时返回0
+                    .orElse(0);
+
+            log.info("获取商品{}库存成功,直播间{},当前库存:{}", productId, liveId, stock);
+            return stock;
+        } catch (Exception e) {
+            // 4. 异常兜底(Redis连接异常/超时等场景)
+            log.error("获取商品{}库存异常,直播间{}", productId, liveId, e);
+            return 0;
+        }
+    }
+
+    /**
+     * 异步获取商品库存(高并发版,适配原有异步扣减逻辑)
+     *
+     * @param productId 商品ID
+     * @param liveId    直播间ID
+     * @return 异步结果:库存数量(异常/空值返回0)
+     */
+    public CompletableFuture<Integer> getStockAsync(Long productId, Long liveId) {
+        // 复用CompletableFuture异步化,避免主线程阻塞(Java 8特性)
+        return CompletableFuture.supplyAsync(() -> getStock(productId, liveId));
+    }
+
+    /**
+     * 高并发库存扣减(核心方法,落地Java 8特性)
+     *
+     * @param productId 商品ID
+     * @param deductNum 扣减数量(默认1)
+     * @return 扣减结果:true=成功,false=失败
+     */
+    public CompletableFuture<Boolean> deductStockAsync(Long productId, Long liveId, Integer deductNum, Long userId) {
+        // Java 8 CompletableFuture 异步处理,提升高并发吞吐量
+        return CompletableFuture.supplyAsync(() -> {
+            // 1. 参数校验(Java 8 Optional 空值处理)
+            Integer num = Optional.ofNullable(deductNum).orElse(1);
+            String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + productId;
+            try {
+                // 5. 执行库存扣减Lua脚本(原子操作,防超卖)
+                // 新增日志:打印当前库存值和扣减数量
+                Integer currentStockStr = (Integer) redisTemplate.opsForValue().get(stockKey);
+                log.info("拿到锁成功 → 库存Key:{},当前库存值:{},扣减数量:{}", stockKey, currentStockStr, num);
+
+                // 执行库存扣减Lua脚本
+                Long remainingStock = redisTemplate.execute(STOCK_DEDUCT_SCRIPT, Collections.singletonList(stockKey), 1);
+
+                // 新增日志:打印Lua返回结果
+                log.info("Lua脚本返回值:{}", remainingStock);
+
+                // 6. 判断扣减结果
+                if (remainingStock >= 0) {
+                    log.info("商品{}库存扣减成功,剩余库存:{}", productId, remainingStock);
+                    return true;
+                } else {
+                    String errorMsg = "";
+                    switch (remainingStock.intValue()) {
+                        case -1:
+                            errorMsg = "库存不足";
+                            break;
+                        case -2:
+                            errorMsg = "库存Key不存在";
+                            break;
+                        case -3:
+                            errorMsg = "库存值非数字";
+                            break;
+                        case -4:
+                            errorMsg = "扣减数量无效";
+                            break;
+                        default:
+                            errorMsg = "未知错误,错误码:" + remainingStock;
+                    }
+                    log.info("商品{}扣减失败:{}", productId, errorMsg);
+                    return false;
+                }
+            }catch (Exception e) {
+                log.error("支付失败获取失败", e);
+                return false;
+            }
+        });
+    }
+}

+ 367 - 0
fs-common/src/main/java/com/fs/common/utils/HsCryptoUtil.java

@@ -0,0 +1,367 @@
+package com.fs.common.utils;
+
+import cn.hutool.json.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * 红杉健康平台加解密工具类
+ */
+public class HsCryptoUtil {
+
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+    private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
+    private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
+
+    /**
+     * 加密方法
+     * @param data 待加密的数据对象
+     * @param appSecret 应用密钥,用于AES加密的IV
+     * @param publicKeyStr RSA公钥(Base64编码)
+     * @return 加密结果Map,包含content、key、sign三个字段
+     */
+    public static Map<String, String> encrypt(JSONObject data, String appSecret, String publicKeyStr) {
+        try {
+//            公钥解码base64
+            String publicKey = new String( Base64.decodeBase64(publicKeyStr), StandardCharsets.UTF_8);
+            publicKey = publicKey.replace("-----BEGIN PUBLIC KEY-----\n", "").replace(
+                    "\n" +
+                            "-----END PUBLIC KEY-----\n", ""
+            );
+            // 1. 将对象转换为按ASCII码排序的Map
+
+            Map<String, Object> sortedParams = new TreeMap<>();
+            /*处理掉空值,否则转jsonString要报错*/
+            for (Map.Entry<String, Object> entry : data.entrySet()) {
+                if ( entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    Object processedValue = processNestedObjects(entry.getValue());
+                    sortedParams.put(entry.getKey(), processedValue);
+                }
+            }
+            String contentStr = objectMapper.writeValueAsString(sortedParams);
+//            // 直接转换 JSONObject 为 Map
+//            Map<String, Object> sortedParams = convertToSortedMap(data);
+//
+//            // 2. 转换为JSON字符串(contentStr)
+//            String contentStr = objectMapper.writeValueAsString(sortedParams);
+
+            // 3. 生成32位随机密钥(randStr)
+            String randStr = generateRandStr(32);
+
+            // 4. AES-256-CBC加密contentStr
+            String encryptedContent = aesEncrypt(contentStr, randStr, appSecret);
+
+            // 5. 使用RSA公钥加密randStr
+            String encryptedKey = rsaEncrypt(randStr, publicKey);
+
+            // 6. 对contentStr进行MD5签名
+            String sign = DigestUtils.md5Hex(contentStr);
+
+            // 7. 构造返回结果
+            Map<String, String> result = new TreeMap<>();
+            result.put("content", encryptedContent);
+            result.put("key", encryptedKey);
+            result.put("sign", sign);
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("加密失败", e);
+        }
+    }
+
+    /**
+     * 递归处理嵌套的JSON对象 的jsonNull和null
+     */
+    private static Object processNestedObjects(Object value) {
+        if (value instanceof cn.hutool.json.JSONObject) {
+            // 处理嵌套的JSONObject
+            cn.hutool.json.JSONObject jsonObj = (cn.hutool.json.JSONObject) value;
+            Map<String, Object> nestedMap = new TreeMap<>();
+            for (Map.Entry<String, Object> entry : jsonObj.entrySet()) {
+                if (entry.getValue() != null && !(entry.getValue() instanceof cn.hutool.json.JSONNull)) {
+                    nestedMap.put(entry.getKey(), processNestedObjects(entry.getValue()));
+                }
+            }
+            return nestedMap;
+        } else if (value instanceof cn.hutool.json.JSONArray) {
+            // 处理JSONArray
+            cn.hutool.json.JSONArray jsonArray = (cn.hutool.json.JSONArray) value;
+            List<Object> list = new ArrayList<>();
+            for (Object item : jsonArray) {
+                list.add(processNestedObjects(item));
+            }
+            return list;
+        } else {
+            // 基本类型直接返回
+            return value;
+        }
+    }
+
+    /**
+     * 将对象转换为按ASCII码排序的Map
+     */
+    private static Map<String, Object> convertToSortedMap(Object obj) throws Exception {
+        Map<String, Object> map = objectMapper.convertValue(obj, Map.class);
+        return new TreeMap<>(map);
+    }
+
+    /**
+     * 生成指定长度的随机字符串
+     */
+    private static String generateRandStr(int length) {
+        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+        SecureRandom random = new SecureRandom();
+        StringBuilder sb = new StringBuilder(length);
+
+        for (int i = 0; i < length; i++) {
+            sb.append(chars.charAt(random.nextInt(chars.length())));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * AES-256-CBC加密
+     */
+    private static String aesEncrypt(String content, String randStr, String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+        // 使用appSecret的前16位作为IV
+        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+        byte[] encrypted = cipher.doFinal(content.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * RSA加密
+     */
+    private static String rsaEncrypt(String randStr, String publicKey) throws Exception {
+        byte[] decodedPublicKey = Base64.decodeBase64(publicKey);
+        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedPublicKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PublicKey pubKey = keyFactory.generatePublic(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.ENCRYPT_MODE, pubKey);
+        byte[] encrypted = cipher.doFinal(randStr.getBytes());
+        return Base64.encodeBase64String(encrypted);
+    }
+
+    /**
+     * 解密方法
+     * @param encryptedContent AES加密的内容(Base64编码)
+     * @param encryptedKey RSA加密的密钥(Base64编码)
+     * @param sign 签名(MD5)
+     * @param appSecret 应用密钥,用于AES解密的IV
+     * @param privateKeyStr RSA私钥(PEM格式,Base64编码)
+     * @return 解密后的原始数据对象
+     */
+    public static Map<String, Object> decrypt(String encryptedContent,
+                                              String encryptedKey,
+                                              String sign,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        try {
+            // 1. Base64解码私钥字符串,得到PEM格式的私钥
+            String privateKeyPEM = new String(
+                    Base64.decodeBase64(privateKeyStr),
+                    StandardCharsets.UTF_8
+            );
+            // 1. RSA解密获取AES密钥(randStr)
+            String randStr = rsaDecrypt(encryptedKey, privateKeyPEM);
+
+            // 2. AES解密获取contentStr
+            String contentStr = aesDecrypt(encryptedContent, randStr, appSecret);
+
+            // 3. 验证签名
+            String calculatedSign = DigestUtils.md5Hex(contentStr);
+            if (!calculatedSign.equals(sign)) {
+                throw new RuntimeException("签名验证失败,数据可能被篡改");
+            }
+
+            // 4. 将contentStr解析为Map
+            return objectMapper.readValue(contentStr, Map.class);
+
+        } catch (Exception e) {
+            throw new RuntimeException("解密失败", e);
+        }
+    }
+
+    /**
+     * 解密并验证的完整方法
+     * @param encryptedData 加密数据Map,包含content、key、sign三个字段
+     * @param appSecret 应用密钥
+     * @param privateKeyStr RSA私钥
+     * @return 解密后的原始数据Map
+     */
+    public static Map<String, Object> decrypt(JsonNode encryptedData,
+                                              String appSecret,
+                                              String privateKeyStr) {
+        return decrypt(
+                encryptedData.get("content").asText(),
+                encryptedData.get("key").asText(),
+                encryptedData.get("sign").asText(),
+                appSecret,
+                privateKeyStr
+        );
+    }
+
+    /**
+     * RSA解密 - 使用私钥解密AES密钥
+     */
+    private static String rsaDecrypt(String encryptedKey, String privateKeyStr) throws Exception {
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+        // 处理PEM格式的私钥
+        String privateKeyPEM = privateKeyStr
+                .replace("-----BEGIN PRIVATE KEY-----", "")
+                .replace("-----END PRIVATE KEY-----", "")
+                .replaceAll("\\s+", "");
+
+        byte[] decodedPrivateKey = Base64.decodeBase64(privateKeyPEM);
+        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+
+        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+        cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedKey);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * AES-256-CBC解密
+     */
+    private static String aesDecrypt(String encryptedContent,
+                                     String randStr,
+                                     String appSecret) throws Exception {
+        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(StandardCharsets.UTF_8), "AES");
+
+        // 使用appSecret的前16位作为IV(与加密时一致)
+        String iv = appSecret.length() >= 16 ? appSecret.substring(0, 16) : appSecret;
+        IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
+
+        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+
+        byte[] encryptedBytes = Base64.decodeBase64(encryptedContent);
+        byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+
+        return new String(decryptedBytes, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * 验证签名(不进行解密)
+     */
+    public static boolean verifySignature(String contentStr, String sign) {
+        String calculatedSign = DigestUtils.md5Hex(contentStr);
+        return calculatedSign.equals(sign);
+    }
+
+    /**
+     * 单独解密AES密钥(用于调试)
+     */
+    public static String decryptKeyOnly(String encryptedKey, String privateKeyStr) throws Exception {
+        return rsaDecrypt(encryptedKey, privateKeyStr);
+    }
+
+    /**
+     * 单独解密内容(需要提供AES密钥)
+     */
+    public static String decryptContentOnly(String encryptedContent,
+                                            String randStr,
+                                            String appSecret) throws Exception {
+        return aesDecrypt(encryptedContent, randStr, appSecret);
+    }
+
+
+//    /**
+//     * 解密方法
+//     * @param encryptedData 加密的数据Map,包含content、key、sign三个字段
+//     * @param appSecret 应用密钥,用于AES解密的IV
+//     * @param privateKeyStr RSA私钥(Base64编码)
+//     * @return 解密后的原始数据对象
+//     */
+//    public static Map<String, Object> decrypt(JsonNode encryptedData, String appSecret, String privateKeyStr) {
+//        //            私钥解码base64
+//    String privateKey = new String( Base64.decodeBase64(privateKeyStr), StandardCharsets.UTF_8);
+
+//        privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----\n", "").replace(
+//                "\n" +
+//                        "-----END PRIVATE KEY-----", ""
+//        );
+//        try {
+//            String content = String.valueOf(encryptedData.get("content"));
+//            String key = String.valueOf(encryptedData.get("key"));
+//            String sign = String.valueOf(encryptedData.get("sign"));
+//
+//            // 1. 使用RSA私钥解密key字段,得到randStr
+//            String randStr = rsaDecrypt(key, privateKey);
+//
+//            // 2. 使用AES-256-CBC解密content字段
+//            String decryptedContent = aesDecrypt(content, randStr, appSecret);
+//
+//            // 3. 验证签名
+//            String calculatedSign = DigestUtils.md5Hex(decryptedContent);
+//            if (!calculatedSign.equals(sign)) {
+//                throw new RuntimeException("签名验证失败");
+//            }
+//
+//            // 4. 将解密后的JSON字符串转换为Map对象
+//            @SuppressWarnings("unchecked")
+//            Map<String, Object> result = objectMapper.readValue(decryptedContent, Map.class);
+//
+//            return result;
+//        } catch (Exception e) {
+//            throw new RuntimeException("解密失败", e);
+//        }
+//    }
+//
+//    /**
+//     * AES-256-CBC解密
+//     */
+//    private static String aesDecrypt(String content, String randStr, String appSecret) throws Exception {
+//        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+//        SecretKeySpec keySpec = new SecretKeySpec(randStr.getBytes(), "AES");
+//        // 使用appSecret的前16位作为IV
+//        IvParameterSpec ivSpec = new IvParameterSpec(appSecret.substring(0, 16).getBytes());
+//        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(content));
+//        return new String(decrypted);
+//    }
+//
+//    /**
+//     * RSA解密
+//     */
+//    private static String rsaDecrypt(String encryptedKey, String privateKey) throws Exception {
+//        byte[] decodedPrivateKey = Base64.decodeBase64(privateKey);
+//        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedPrivateKey);
+//        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+//        PrivateKey privKey = keyFactory.generatePrivate(keySpec);
+//
+//        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
+//        cipher.init(Cipher.DECRYPT_MODE, privKey);
+//        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(encryptedKey));
+//        return new String(decrypted);
+//    }
+}

+ 154 - 0
fs-common/src/main/java/com/fs/common/utils/StringToMapUtil.java

@@ -0,0 +1,154 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.lang.TypeReference;
+import cn.hutool.json.JSONUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class StringToMapUtil {
+    public static Map<String, Object> toMap(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            return new HashMap<>();
+        }
+
+        try {
+            // 尝试JSON解析
+            return JSONUtil.toBean(input, new TypeReference<Map<String, Object>>() {}, false);
+        } catch (Exception e) {
+            // 如果失败,尝试Map.toString()格式解析
+            try {
+                // 检查是否是Map.toString()格式
+                if (input.trim().startsWith("{") && input.trim().endsWith("}") &&
+                        input.contains("=")) {
+                    return parseMapToString(input);
+                }
+            } catch (Exception ex) {
+                // 忽略,抛出原始异常
+            }
+            throw e;
+        }
+    }
+    /**
+     * 将Map.toString()格式的字符串转换为Map
+     * @param mapString 类似 {data={book_no=10381008622, ...}, msg=success, state=0} 的字符串
+     * @return 转换后的Map对象
+     */
+    private static Map<String, Object> parseMapToString(String mapString) {
+        Map<String, Object> resultMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapString.trim();
+        if (content.startsWith("{") && content.endsWith("}")) {
+            content = content.substring(1, content.length() - 1);
+        }
+
+        // 分割顶层键值对
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                if (value.startsWith("{") && value.endsWith("}")) {
+                    // 嵌套Map对象
+                    resultMap.put(key, parseNestedMap(value));
+                } else {
+                    // 简单值
+                    resultMap.put(key, value);
+                }
+            }
+        }
+
+        return resultMap;
+    }
+
+    /**
+     * 分割顶层键值对,避免分割嵌套对象内的逗号
+     */
+    private static List<String> splitTopLevelPairs(String content) {
+        List<String> pairs = new ArrayList<>();
+        int level = 0;
+        int start = 0;
+
+        for (int i = 0; i < content.length(); i++) {
+            char c = content.charAt(i);
+            if (c == '{') {
+                level++;
+            } else if (c == '}') {
+                level--;
+            } else if (c == ',' && level == 0) {
+                pairs.add(content.substring(start, i));
+                start = i + 1;
+            }
+        }
+
+        // 添加最后一个元素
+        if (start < content.length()) {
+            pairs.add(content.substring(start));
+        }
+
+        return pairs;
+    }
+
+    /**
+     * 解析嵌套的Map对象
+     */
+    private static Map<String, Object> parseNestedMap(String mapStr) {
+        Map<String, Object> nestedMap = new HashMap<>();
+
+        // 移除外层大括号
+        String content = mapStr.substring(1, mapStr.length() - 1);
+
+        List<String> pairs = splitTopLevelPairs(content);
+
+        for (String pair : pairs) {
+            int eqIndex = pair.indexOf('=');
+            if (eqIndex > 0) {
+                String key = pair.substring(0, eqIndex).trim();
+                String value = pair.substring(eqIndex + 1).trim();
+
+                // 尝试转换为合适的类型
+                nestedMap.put(key, convertValueType(value));
+            }
+        }
+
+        return nestedMap;
+    }
+
+    /**
+     * 根据值的特点转换为合适的类型
+     */
+    private static Object convertValueType(String value) {
+        // 数字
+        if (value.matches("-?\\d+")) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                // 忽略,继续处理
+            }
+        }
+
+        // 布尔值
+        if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+            return Boolean.parseBoolean(value);
+        }
+
+        // 嵌套对象
+        if (value.startsWith("{") && value.endsWith("}")) {
+            return parseNestedMap(value);
+        }
+
+        // 数组或列表(简化处理)
+        if (value.startsWith("[") && value.endsWith("]")) {
+            return value.substring(1, value.length() - 1).split(",");
+        }
+
+        // 默认作为字符串
+        return value;
+    }
+}

+ 23 - 0
fs-common/src/main/java/com/fs/common/utils/poi/ExcelUtil.java

@@ -19,6 +19,8 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletResponse;
+
+import com.fs.common.core.domain.entity.SysDictData;
 import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
 import org.apache.poi.hssf.usermodel.HSSFPicture;
 import org.apache.poi.hssf.usermodel.HSSFPictureData;
@@ -705,6 +707,27 @@ public class ExcelUtil<T>
             // 这里默认设了2-101列只能选择不能输入.
             setXSSFValidation(sheet, attr.combo(), 1, 100, column, column);
         }
+        // 如果设置了dictType属性,则从字典中获取选项并设置下拉列表
+        if (StringUtils.isNotEmpty(attr.dictType()))
+        {
+            try
+            {
+                List<SysDictData> dictDatas = DictUtils.getDictCache(attr.dictType());
+                if (dictDatas != null && !dictDatas.isEmpty())
+                {
+                    // 提取字典标签列表
+                    String[] dictLabels = dictDatas.stream()
+                            .map(SysDictData::getDictLabel)
+                            .toArray(String[]::new);
+                    // 这里默认设了2-101列只能选择不能输入.
+                    setXSSFValidation(sheet, dictLabels, 1, 100, column, column);
+                }
+            }
+            catch (Exception e)
+            {
+                log.warn("设置字典下拉选项失败,dictType: {}, error: {}", attr.dictType(), e.getMessage());
+            }
+        }
     }
 
     /**

+ 6 - 0
fs-company-app/Dockerfile

@@ -0,0 +1,6 @@
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-company-app.jar fs-company-app.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-company-app.jar"]
+# 执行启动命令java -jar

+ 1 - 1
fs-company/Dockerfile

@@ -1,4 +1,4 @@
-FROM openjdk:8-jre
+FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:8-8.6
 # java版本,最好使用openjdk,而不是类似于Java:1.8
 COPY ./target/fs-company.jar fs-company.jar
 # 向外暴露的接口,最好与项目yml文件中的端口一致

+ 117 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyAiWorkflowController.java

@@ -0,0 +1,117 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.param.CompanyUserAiWorkflowTtsVoiceParam;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsAiWorkflow;
+import com.fs.his.domain.FsAiWorkflowNode;
+import com.fs.his.service.IFsAiWorkflowService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 企业AI工作流Controller
+ *
+ * @author fs
+ * @date 2026-01-09
+ */
+@RestController
+@RequestMapping("/company/aiWorkflow")
+public class CompanyAiWorkflowController extends BaseController {
+
+    @Autowired
+    private IFsAiWorkflowService fsAiWorkflowService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 分页查询AI工作流列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list(FsAiWorkflow fsAiWorkflow) {
+        startPage();
+        List<FsAiWorkflow> list = fsAiWorkflowService.selectFsAiWorkflowList(fsAiWorkflow);
+        return getDataTable(list);
+    }
+
+    /**
+     * 根据企业用户ID获取工作流的关键节点
+     * 节点类型: start(开始), end(结束), ai_chat(AI对话)
+     */
+    @GetMapping("/nodes/{companyUserId}")
+    public R getWorkflowNodes(@PathVariable("companyUserId") Long companyUserId) {
+        // 定义要查询的节点类型
+        List<String> nodeTypes = Arrays.asList("start", "end", "ai_chat");
+        List<FsAiWorkflowNode> nodes = fsAiWorkflowService.selectWorkflowNodesByCompanyUserId(companyUserId, nodeTypes);
+        return R.ok().put("data", nodes);
+    }
+
+    /**
+     * 获取当前登录企业用户的工作流关键节点
+     * 节点类型: start(开始), end(结束), ai_chat(AI对话)
+     */
+//    @GetMapping("/myNodes")
+    public R getMyWorkflowNode() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser == null || loginUser.getUser() == null) {
+            return R.error("用户信息错误");
+        }
+        Long companyUserId = loginUser.getUser().getUserId();
+        List<String> nodeTypes = Arrays.asList("start", "end", "ai_chat");
+        List<FsAiWorkflowNode> nodes = fsAiWorkflowService.selectWorkflowNodesByCompanyUserId(companyUserId, nodeTypes);
+        return R.ok().put("data", nodes);
+    }
+
+    /**
+     * 获取当前登录企业用户的工作流及需要录音的节点
+     */
+    @GetMapping("/myNodes")
+    public R getMyWorkflowNodes() {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (loginUser == null || loginUser.getUser() == null) {
+            return R.error("用户信息错误");
+        }
+        Long companyUserId = loginUser.getUser().getUserId();
+        return R.ok().put("data", fsAiWorkflowService.getMyWorkflowNodes(companyUserId));
+    }
+
+//    @PostMapping("/ttsVoice")
+//    public R ttsVoice(@RequestBody CompanyUserAiWorkflowTtsVoiceParam param) {
+//        return R.ok().put("data", fsAiWorkflowService.ttsVoice(param));
+//    }
+
+//    /**
+//     * 更新节点的语音URL
+//     * @param nodeKey 节点ID
+//     * @param voiceUrl 语音URL
+//     */
+//    @PutMapping("/node/voiceUrl")
+//    public R updateNodeVoiceUrl(@RequestParam("nodeKey") String nodeKey,
+//                                @RequestParam("voiceUrl") String voiceUrl,
+//                                @RequestParam("workflowId") Long workflowId) {
+//        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        if (loginUser == null || loginUser.getUser() == null) {
+//            return R.error("用户信息错误");
+//        }
+//        Long companyUserId = loginUser.getUser().getUserId();
+//
+//        int result = fsAiWorkflowService.updateNodeVoiceUrl(nodeKey, voiceUrl, companyUserId,workflowId);
+//        if (result == -1) {
+//            return R.error("节点不存在");
+//        } else if (result == -2) {
+//            return R.error("无权限修改该节点");
+//        } else if (result > 0) {
+//            return R.ok("更新成功");
+//        }
+//        return R.error("更新失败");
+//    }
+}

+ 1 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyRedPacketBalanceLogsController.java

@@ -108,6 +108,7 @@ public class CompanyRedPacketBalanceLogsController extends BaseController {
         redRecharge.setRemark(param.getRemark());
         redRecharge.setPayType(3);
         redRecharge.setBusinessType(1);// 红包充值
+        redRecharge.setImgs(param.getImgs());// 红包充值
         rechargeService.insertCompanyRecharge(redRecharge);
         return R.ok("提交成功,等待审核");
 

+ 38 - 2
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -160,7 +160,6 @@ public class CompanyUserController extends BaseController {
         return getDataTable(list);
     }
 
-
     @GetMapping("/getUserList")
     public R getUserList()
     {
@@ -199,7 +198,44 @@ public class CompanyUserController extends BaseController {
                     }
                     //是否绑定
                     if(QwStatusEnum.BOUND.getCode() == companyUserQwListVO.getQwStatus()){
-                        if(!companyUserQwListVO.getQwUserId().isEmpty()){
+                        if(!StringUtil.strIsNullOrEmpty(companyUserQwListVO.getQwUserId())){
+                            Long[] ids = Arrays.stream(companyUserQwListVO.getQwUserId().split(","))
+                                    .map(Long::parseLong)
+                                    .toArray(Long[]::new);
+                            List<QwUserVO> qwUserVOS = qwUserService.selectQwUserVOByIds(ids);
+                            companyUserQwListVO.setQwUsers(qwUserVOS);
+                        }
+                    }
+                });
+                futures.add(future);
+            }
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+        return getDataTable(list);
+    }
+
+    @GetMapping("/myQwList")
+    public TableDataInfo myQwList(CompanyUserQwParam user) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+//        user.setCompanyId(loginUser.getCompany().getCompanyId());
+        user.setUserId(loginUser.getUser().getUserId());
+        List<CompanyUserQwListVO> list = companyUserService.selectCompanyUserQwListVO(user);
+        if (!list.isEmpty()){
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+            Long sendDelayTime = config.getSendDelayTime();
+            List<CompletableFuture<Void>> futures = new ArrayList<>();
+            for (CompanyUserQwListVO companyUserQwListVO : list) {
+                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                    CompanyUserDelayTime companyUserDelayTime = companyUserDelayTimeService.selectCompanyUserDelayTimeByCompanyUser(companyUserQwListVO.getCompanyId(), companyUserQwListVO.getUserId());
+                    if (ObjectUtil.isEmpty(companyUserDelayTime)) {
+                        companyUserQwListVO.setSendDelayTime(sendDelayTime);
+                    } else {
+                        companyUserQwListVO.setSendDelayTime(companyUserDelayTime.getSendDelayTime());
+                    }
+                    //是否绑定
+                    if(QwStatusEnum.BOUND.getCode() == companyUserQwListVO.getQwStatus()){
+                        if(!StringUtil.strIsNullOrEmpty(companyUserQwListVO.getQwUserId())){
                             Long[] ids = Arrays.stream(companyUserQwListVO.getQwUserId().split(","))
                                     .map(Long::parseLong)
                                     .toArray(Long[]::new);

+ 11 - 3
fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java

@@ -104,11 +104,19 @@ public class FsCourseWatchLogController extends BaseController
 
 
         List<Long> combinedList = new ArrayList<>();
-        //本部门
-        Long deptId = loginUser.getUser().getDeptId();
-        if (deptId!=null){
+        Long deptId;
+
+        if(param.getDeptId() != null){
+            deptId = param.getDeptId();
             combinedList.add(deptId);
+        }else{
+            //本部门
+            deptId = loginUser.getUser().getDeptId();
+            if (deptId!=null){
+                combinedList.add(deptId);
+            }
         }
+
         //本部门的下级部门
         List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
         if (!deptList.isEmpty()){

+ 9 - 0
fs-company/src/main/java/com/fs/company/controller/fastGpt/FastGptRoleController.java

@@ -8,6 +8,7 @@ import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.fastGpt.domain.FastGptRole;
 import com.fs.fastGpt.service.IFastGptRoleService;
 import com.fs.fastGpt.vo.FastGptRoleVO;
@@ -78,6 +79,14 @@ public class FastGptRoleController extends BaseController
         return util.exportExcel(list, "应用数据");
     }
 
+    @GetMapping("/getAllCourseList")
+    public R getAllCourseList()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<FsUserCourseVideo> list = fastGptRoleService.selectAllCourseList(loginUser.getCompany().getCompanyId());
+        return R.ok().put("data",list);
+    }
+
     /**
      * 获取应用详细信息
      */

+ 19 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveMsgController.java

@@ -10,6 +10,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.framework.security.SecurityUtils;
 import com.fs.live.domain.LiveMsg;
 import com.fs.live.service.ILiveMsgService;
+import com.fs.live.vo.LiveMsgExportVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -105,4 +106,22 @@ public class LiveMsgController extends BaseController
         return toAjax(liveMsgService.deleteLiveMsgByMsgIds(msgIds));
     }
 
+    /**
+     * 导出直播评论
+     */
+    @Log(title = "直播评论导出", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportComments/{liveId}")
+    public AjaxResult exportComments(@PathVariable("liveId") Long liveId)
+    {
+        try {
+            CompanyUser user = SecurityUtils.getLoginUser().getUser();
+            Long userId = user.getUserId();
+            
+            List<LiveMsgExportVO> list = liveMsgService.exportLiveMsgComments(liveId, userId);
+            ExcelUtil<LiveMsgExportVO> util = new ExcelUtil<LiveMsgExportVO>(LiveMsgExportVO.class);
+            return util.exportExcel(list, "直播评论数据");
+        } catch (Exception e) {
+            return AjaxResult.error("导出失败:" + e.getMessage());
+        }
+    }
 }

+ 6 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java

@@ -1,5 +1,6 @@
 package com.fs.company.controller.qw;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -39,10 +40,14 @@ public class QwAssignRuleController {
     public Result<IPage<QwAssignRule>> page(
             @RequestParam(defaultValue = "1") Long pageNum,
             @RequestParam(defaultValue = "10") Long pageSize,
-            @RequestParam(required = false) String ruleName
+            @RequestParam(required = false) String ruleName,
+            @RequestParam(required = false) Integer  status
     ) {
         Page<QwAssignRule> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<QwAssignRule> wrapper = new LambdaQueryWrapper<>();
+        if (ObjectUtil.isNotEmpty(status)){
+            wrapper.eq(QwAssignRule::getStatus,status);
+        }
         wrapper.like(StrUtil.isNotBlank(ruleName), QwAssignRule::getRuleName, ruleName);
         wrapper.orderByDesc(QwAssignRule::getCreateTime);
         IPage<QwAssignRule> result = qwAssignRuleService.page(page, wrapper);

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/qw/QwExternalContactController.java

@@ -722,7 +722,7 @@ public class QwExternalContactController extends BaseController
 
         return  qwExternalContactService.setCustomerCourseSopList(param);
     }
-    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit')")
+    @PreAuthorize("@ss.hasPermi('qw:externalContact:edit') or @ss.hasPermi('qw:externalContact:deptEdit') or @ss.hasPermi('qw:externalContact:myEdit')")
     @Log(title = "批量修改备注", businessType = BusinessType.UPDATE)
     @PostMapping("/batchUpdateExternalContactNotes")
     public R batchUpdateExternalContactNotes(@RequestBody QwExternalContactUpdateNoteParam param) throws JSONException {

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

@@ -952,4 +952,10 @@ public class QwUserController extends BaseController
         List<QwUserVO> list = qwUserService.selectQwUserListByCompanyIdAndCorpIdAndNickName(companyId, corpId, nickName);
         return getDataTable(list);
     }
+
+    @GetMapping("/updateFastGptRoleStatusById/{id}")
+    public R updateFastGptRoleStatusById(@PathVariable Long id)
+    {
+        return qwUserService.updateQwUserFastGptRoleStatusById(id);
+    }
 }

+ 108 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderItemScrmController.java

@@ -0,0 +1,108 @@
+package com.fs.hisStore.controller;
+
+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.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.hisStore.domain.FsStoreOrderItemScrm;
+import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 订单详情Controller
+ *
+ * @author fs
+ * @date 2022-03-21
+ */
+@RestController
+@RequestMapping("/store/store/storeOrderItem")
+public class FsStoreOrderItemScrmController extends BaseController
+{
+    @Autowired
+    private IFsStoreOrderItemScrmService fsStoreOrderItemService;
+
+    /**
+     * 查询订单详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        startPage();
+        List<FsStoreOrderItemScrm> list = fsStoreOrderItemService.selectFsStoreOrderItemList(fsStoreOrderItem);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出订单详情列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:export')")
+    @Log(title = "订单详情", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        List<FsStoreOrderItemScrm> list = fsStoreOrderItemService.selectFsStoreOrderItemList(fsStoreOrderItem);
+        ExcelUtil<FsStoreOrderItemScrm> util = new ExcelUtil<FsStoreOrderItemScrm>(FsStoreOrderItemScrm.class);
+        return util.exportExcel(list, "storeOrderItem");
+    }
+
+    /**
+     * 获取订单详情详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:query')")
+    @GetMapping(value = "/{itemId}")
+    public AjaxResult getInfo(@PathVariable("itemId") Long itemId)
+    {
+        return AjaxResult.success(fsStoreOrderItemService.selectFsStoreOrderItemById(itemId));
+    }
+
+    /**
+     * 新增订单详情
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:add')")
+    @Log(title = "订单详情", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        return toAjax(fsStoreOrderItemService.insertFsStoreOrderItem(fsStoreOrderItem));
+    }
+
+    /**
+     * 修改订单详情
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:edit')")
+    @Log(title = "订单详情", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        return toAjax(fsStoreOrderItemService.updateFsStoreOrderItem(fsStoreOrderItem));
+    }
+
+    /**
+     * 删除订单详情
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:remove')")
+    @Log(title = "订单详情", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{itemIds}")
+    public AjaxResult remove(@PathVariable Long[] itemIds)
+    {
+        return toAjax(fsStoreOrderItemService.deleteFsStoreOrderItemByIds(itemIds));
+    }
+
+    /**
+     * 修改订单数量及总价
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeOrderItem:updateNum')")
+    @Log(title = "订单详情修改订单数量", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateNum")
+    public AjaxResult updateNum(@RequestBody FsStoreOrderItemScrm fsStoreOrderItem)
+    {
+        return toAjax(fsStoreOrderItemService.updateFsStoreOrderItemNum(fsStoreOrderItem));
+    }
+}

+ 28 - 4
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -15,6 +15,9 @@ import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.framework.security.LoginUser;
@@ -30,13 +33,11 @@ import com.fs.hisStore.dto.ExpressInfoDTO;
 import com.fs.hisStore.dto.StoreOrderProductDTO;
 import com.fs.hisStore.enums.OrderLogEnum;
 import com.fs.hisStore.enums.ShipperCodeEnum;
-import com.fs.hisStore.param.FsStoreOrderBindCustomerParam;
-import com.fs.hisStore.param.FsStoreOrderCreateUserParam;
-import com.fs.hisStore.param.FsStoreOrderFinishParam;
-import com.fs.hisStore.param.FsStoreOrderParam;
+import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
 import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -473,4 +474,27 @@ public class FsStoreOrderScrmController extends BaseController
         Integer createSalesOrderType = config.getCreateSalesOrderType();
         return R.ok().put("createSalesOrderType",createSalesOrderType);
     }
+
+    @ApiOperation("批量审核订单")
+    @Log(title = "订单管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:batchAudit')")
+    @PostMapping("/batchAudit")
+    public R batchAuditOrder(@Validated @RequestBody FsStoreOrderBatchAuditParam param) {
+        if (param.getOrderIds() == null || param.getOrderIds().isEmpty()) {
+            return R.error("订单ID列表不能为空");
+        }
+        if (param.getIsAudit() == null) {
+            return R.error("审核状态不能为空");
+        }
+        int count = fsStoreOrderService.batchAuditOrder(param);
+        return R.ok("成功审核 " + count + " 条订单");
+    }
+    /**
+     * 修改订单itemJson
+     */
+    @Log(title = "修改订单itemJson", businessType = BusinessType.UPDATE)
+    @GetMapping("/updateStoreOrderItemJson/{orderId}/{backendEditProductType}")
+    public AjaxResult updateStoreOrderItemJson(@PathVariable("orderId") Long orderId,@PathVariable("backendEditProductType") Integer backendEditProductType) {
+        return toAjax(fsStoreOrderService.updateStoreOrderItemJson(orderId,backendEditProductType));
+    }
 }

+ 151 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreScrmController.java

@@ -0,0 +1,151 @@
+package com.fs.hisStore.controller;
+
+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.ParseUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.param.FsStoreAuditParam;
+import com.fs.hisStore.domain.FsStoreScrm;
+import com.fs.hisStore.service.IFsStoreScrmService;
+import com.fs.hisStore.utils.StoreAuditLogUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 店铺管理Controller
+ *
+ * @author fs
+ * @date 2023-06-15
+ */
+@RestController
+@RequestMapping("/store/his/store")
+public class FsStoreScrmController extends BaseController
+{
+    @Autowired
+    private IFsStoreScrmService fsStoreService;
+
+    @Autowired
+    private StoreAuditLogUtil storeAuditLogUtil;
+
+    /**
+     * 查询店铺管理列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreScrm fsStore)
+    {
+        startPage();
+        List<FsStoreScrm> list = fsStoreService.selectFsStoreList(fsStore);
+        for (FsStoreScrm store : list) {
+            store.setPhone(ParseUtils.parsePhone(store.getPhone()));
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出店铺管理列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:export')")
+    @Log(title = "店铺管理", businessType = BusinessType.EXPORT,logParam = {"店铺","导出店铺信息"},isStoreLog = true)
+    @GetMapping("/export")
+    public AjaxResult export(FsStoreScrm fsStore)
+    {
+        storeAuditLogUtil.generateOperId();
+        List<FsStoreScrm> list = fsStoreService.selectFsStoreList(fsStore);
+        for (FsStoreScrm store : list) {
+            store.setPhone(ParseUtils.parsePhone(store.getPhone()));
+        }
+        ExcelUtil<FsStoreScrm> util = new ExcelUtil<FsStoreScrm>(FsStoreScrm.class);
+        return util.exportExcel(list, "店铺管理数据");
+    }
+
+    /**
+     * 获取店铺管理详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:query')")
+    @GetMapping(value = "/{storeId}")
+    public AjaxResult getInfo(@PathVariable("storeId") Long storeId)
+    {
+        FsStoreScrm fsStore = fsStoreService.selectFsStoreByStoreId(storeId);
+        fsStore.setPhone(ParseUtils.parsePhone(fsStore.getPhone()));
+        return AjaxResult.success(fsStore);
+    }
+
+    /**
+     * 新增店铺管理
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:add')")
+    @Log(title = "店铺管理", businessType = BusinessType.INSERT,logParam = {"店铺","新增店铺信息"},isStoreLog = true)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreScrm fsStore)
+    {
+        storeAuditLogUtil.addOperLog(fsStoreService.insertFsStore(fsStore));
+        return AjaxResult.success();
+    }
+
+    /**
+     * 修改店铺管理
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:edit')")
+    @Log(title = "店铺管理", businessType = BusinessType.UPDATE,logParam = {"店铺","修改店铺信息"},isStoreLog = true)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreScrm fsStore)
+    {
+
+        if (fsStore.getPhone()!=null&&fsStore.getPhone().contains("*")){
+            fsStore.setPhone(null);
+        }
+        return toAjax(fsStoreService.updateFsStore(fsStore));
+    }
+
+    /**
+     * 删除店铺管理
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:remove')")
+    @Log(title = "店铺管理", businessType = BusinessType.DELETE,logParam = {"店铺","删除店铺信息"},isStoreLog = true)
+	@DeleteMapping("/{storeIds}")
+    public AjaxResult remove(@PathVariable Long[] storeIds)
+    {
+        return toAjax(fsStoreService.deleteFsStoreByStoreIds(storeIds));
+    }
+
+    /**
+     * 店铺审核
+     */
+    @PreAuthorize("@ss.hasPermi('store:his:store:audit')")
+    @Log(title = "店铺审核", businessType = BusinessType.AUDIT,logParam = {"店铺","店铺审核"},isStoreLog = true
+    ,logParamExpression = "#p0.getIsAudit()==1?new String[]{'店铺','店铺审核通过'}: new String[]{'店铺','店铺审核退回'}")
+    @PutMapping("/audit")
+    public AjaxResult audit(@RequestBody FsStoreAuditParam fsStore)
+    {
+        return toAjax(fsStoreService.updateFsStoreAudit(fsStore));
+    }
+
+    /**
+     * 重置店铺密码
+     * */
+    @PreAuthorize("@ss.hasPermi('store:his:store:refresh')")
+    @Log(title = "店铺管理", businessType = BusinessType.UPDATE,logParam = {"店铺","重置店铺密码"},isStoreLog = true)
+    @PutMapping("/refresh/{storeId}")
+    public AjaxResult refresh(@PathVariable Long storeId)
+    {
+        return toAjax(fsStoreService.refreshFsStore(storeId));
+    }
+
+
+    /**
+     * 店铺审核日志
+     * */
+    @PreAuthorize("@ss.hasPermi('store:his:store:auditLog')")
+    @GetMapping("/auditLog/{storeId}")
+    public R auditLog(@PathVariable Long storeId){
+        return R.ok().put("auditLog",storeAuditLogUtil.selectOperLogByMainId(storeId));
+    }
+}

+ 12 - 4
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -36,6 +36,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Date;
+import java.util.List;
 
 @Api(tags = "会员管理接口")
 @RestController
@@ -47,9 +48,6 @@ public class FsUserAdminController extends BaseController {
     @Autowired
     private IFsUserService fsUserService;
 
-    @Autowired
-    private ICompanyUserCacheService companyUserCacheService;
-
     @Autowired
     private TokenService tokenService;
 
@@ -65,6 +63,7 @@ public class FsUserAdminController extends BaseController {
     @Autowired
     private IFsUserCompanyUserService fsUserCompanyUserService;
 
+
     @PreAuthorize("@ss.hasPermi('user:fsUser:list')")
     @PostMapping("/list")
     @ApiOperation("会员列表(与移动端使用的相同查询)")
@@ -189,8 +188,17 @@ public class FsUserAdminController extends BaseController {
         String url = courseSortLink.get("url").toString();
         batchSendCourseDTO.setUrl(url);
         batchSendCourseDTO.setIsUrgeCourse(false);
-        batchSendCourseDTO.setLinkId(Long.parseLong(courseSortLink.get("linkId").toString()));
         return openIMService.batchSendCourse(batchSendCourseDTO);
     }
 
+    @PreAuthorize("@ss.hasPermi('his:user:unbind')")
+    @Log(title = "批量解绑会员(删除销售和会员的关系)", businessType = BusinessType.DELETE)
+    @DeleteMapping("/batchUnbind")
+    public AjaxResult batchUnbind(@RequestBody List<Long> userIds){
+        if(!userIds.isEmpty()){
+            return toAjax(fsUserCompanyUserService.batchUnbind(userIds));
+        } else {
+            return error("请先选择会员");
+        }
+    }
 }

+ 25 - 20
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -40,6 +40,7 @@ import java.util.stream.Collectors;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 import static com.fs.common.constant.LiveKeysConstant.LIVE_COUPON_NUM;
+import static com.fs.live.websocket.service.WebSocketServer.USER_ENTRY_TIME_KEY;
 
 @Component
 @AllArgsConstructor
@@ -185,17 +186,17 @@ public class Task {
                                 .mapToLong(LiveVideo::getDuration)
                                 .sum();
                     }
-                    
+
                     // 如果视频时长大于0,将直播间信息存入Redis
                     if (videoDuration > 0 && live.getStartTime() != null) {
                         Map<String, Object> tagMarkInfo = new HashMap<>();
                         tagMarkInfo.put("liveId", live.getLiveId());
                         tagMarkInfo.put("startTime", live.getStartTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli());
                         tagMarkInfo.put("videoDuration", videoDuration);
-                        
+
                         String tagMarkKey = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, live.getLiveId());
                         redisCache.setCacheObject(tagMarkKey, JSON.toJSONString(tagMarkInfo), 24, TimeUnit.HOURS);
-                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}", 
+                        log.info("直播间开启,已加入打标签缓存: liveId={}, startTime={}, videoDuration={}",
                                 live.getLiveId(), live.getStartTime(), videoDuration);
                     }
                 } catch (Exception e) {
@@ -536,19 +537,19 @@ public class Task {
                 // 回放:使用 Redis 中的数据减去直播的数据,得到回放的数据
                 String likeKey = "live:like:" + liveData.getLiveId();
                 String totalViewsKey = TOTAL_VIEWS_KEY + liveData.getLiveId();
-                
+
                 // 从 Redis 获取总数据(直播+回放)
                 Long totalLikeCount = getAsLong(redisCache, likeKey);
                 Long totalViewCount = getAsLong(redisCache, totalViewsKey);
-                
+
                 // 获取数据库中直播的数据
                 Long liveLikeCount = liveData.getLikes() != null ? liveData.getLikes() : 0L;
                 Long liveViewCount = liveData.getTotalViews() != null ? liveData.getTotalViews() : 0L;
-                
+
                 // 回放数据 = Redis总数据 - 直播数据
                 Long replayLikeNum = totalLikeCount - liveLikeCount;
                 Long replayViewNum = totalViewCount - liveViewCount;
-                
+
                 // 确保回放数据不为负数
                 if (replayLikeNum < 0L) {
                     replayLikeNum = 0L;
@@ -556,7 +557,7 @@ public class Task {
                 if (replayViewNum < 0L) {
                     replayViewNum = 0L;
                 }
-                
+
                 // 更新回放数据
                 liveData.setReplayLikeNum(replayLikeNum);
                 liveData.setReplayViewNum(replayViewNum);
@@ -645,11 +646,11 @@ public class Task {
             // 获取所有打标签缓存的key
             String pattern = String.format(LiveKeysConstant.LIVE_TAG_MARK_CACHE, "*");
             Set<String> keys = redisCache.redisTemplate.keys(pattern);
-            
+
             if (keys == null || keys.isEmpty()) {
                 return;
             }
-            
+
             long currentTimeMillis = System.currentTimeMillis();
             LocalDateTime now = LocalDateTime.now();
             List<Long> processedLiveIds = new ArrayList<>();
@@ -661,19 +662,19 @@ public class Task {
                     if (cacheValue == null) {
                         continue;
                     }
-                    
+
                     String jsonStr = cacheValue.toString();
                     JSONObject tagMarkInfo = JSON.parseObject(jsonStr);
                     Long liveId = tagMarkInfo.getLong("liveId");
                     Long startTimeMillis = tagMarkInfo.getLong("startTime");
                     Long videoDuration = tagMarkInfo.getLong("videoDuration");
-                    
+
                     if (liveId == null || startTimeMillis == null || videoDuration == null || videoDuration <= 0) {
-                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}", 
+                        log.warn("直播间打标签缓存信息不完整: key={}, liveId={}, startTime={}, videoDuration={}",
                                 key, liveId, startTimeMillis, videoDuration);
                         continue;
                     }
-                    
+
                     // 查询直播间信息
                     Live live = liveService.selectLiveDbByLiveId(liveId);
                     if (live == null || live.getStartTime() == null) {
@@ -790,7 +791,7 @@ public class Task {
                     log.error("处理直播间打标签缓存异常: key={}, error={}", key, e.getMessage(), e);
                 }
             }
-            
+
             // 删除已处理的直播间缓存
             for (Long liveId : processedLiveIds) {
                 try {
@@ -819,7 +820,6 @@ public class Task {
             if (activeLives == null || activeLives.isEmpty()) {
                 return;
             }
-
             for (Live live : activeLives) {
                 try {
                     Long liveId = live.getLiveId();
@@ -862,7 +862,12 @@ public class Task {
                             }
 
                             // 获取用户的在线观看时长
-                            Long onlineSeconds = user.getOnlineSeconds();
+                            String entryTimeKey = String.format(USER_ENTRY_TIME_KEY, liveId, userId);
+                            Long existingEntryTime = redisCache.getCacheObject(entryTimeKey);
+                            Long onlineSeconds = user.getOnlineSeconds() ==null ? 0L : user.getOnlineSeconds();
+                            if(null != existingEntryTime){
+                                onlineSeconds = onlineSeconds + ((System.currentTimeMillis() - existingEntryTime)/1000);
+                            }
                             if (onlineSeconds == null || onlineSeconds <= 0) {
                                 continue;
                             }
@@ -873,7 +878,7 @@ public class Task {
                             if (liveUserFirstEntry == null) {
                                 continue;
                             }
-                            
+
                             Long qwUserId = liveUserFirstEntry.getQwUserId();
                             Long externalContactId = liveUserFirstEntry.getExternalContactId();
 
@@ -887,7 +892,7 @@ public class Task {
                             // 使用 updateLiveWatchLogTypeByDuration 的逻辑更新观看记录状态
                             updateLiveWatchLogTypeByDuration(liveId, userId, qwUserId, externalContactId,
                                     onlineSeconds, totalVideoDuration, updateLog);
-                            
+
                         } catch (Exception e) {
                             log.error("处理用户观看记录状态异常: liveId={}, userId={}, error={}",
                                     liveId, user.getUserId(), e.getMessage(), e);
@@ -905,7 +910,7 @@ public class Task {
                             redisCache.setCacheObject("live:watch:log:cache:" + liveWatchLog.getLogId(), liveWatchLog, 1, TimeUnit.HOURS);
                         }
                     }
-                    
+
                 } catch (Exception e) {
                     log.error("处理直播间观看记录状态异常: liveId={}, error={}",
                             live.getLiveId(), e.getMessage(), e);

+ 59 - 0
fs-live-app/src/main/java/com/fs/live/utils/WebSocketRateLimiter.java

@@ -0,0 +1,59 @@
+package com.fs.live.utils;
+
+import com.google.common.util.concurrent.RateLimiter;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * WebSocket限流工具类
+ */
+public class WebSocketRateLimiter {
+
+    // 用户级别限流:每个用户每秒最多发送5条消息
+    private static final double USER_RATE_LIMIT = 5.0;
+    private static final ConcurrentHashMap<String, RateLimiter> userLimiters = new ConcurrentHashMap<>();
+
+    // 直播间级别限流:每个直播间每秒最多处理100条消息
+    private static final double ROOM_RATE_LIMIT = 100.0;
+    private static final ConcurrentHashMap<Long, RateLimiter> roomLimiters = new ConcurrentHashMap<>();
+
+    /**
+     * 检查用户是否被限流
+     * @param userId 用户ID
+     * @param liveId 直播间ID
+     * @return true-允许通过,false-被限流
+     */
+    public static boolean tryAcquire(Long userId, Long liveId) {
+        // 检查用户级别限流
+        String userKey = userId + "_" + liveId;
+        RateLimiter userLimiter = userLimiters.computeIfAbsent(
+                userKey,
+                k -> RateLimiter.create(USER_RATE_LIMIT)
+        );
+
+        if (!userLimiter.tryAcquire()) {
+            return false;
+        }
+
+        // 检查直播间级别限流
+        RateLimiter roomLimiter = roomLimiters.computeIfAbsent(
+                liveId,
+                k -> RateLimiter.create(ROOM_RATE_LIMIT)
+        );
+
+        return roomLimiter.tryAcquire();
+    }
+
+    /**
+     * 清理用户限流器
+     */
+    public static void removeUserLimiter(Long userId, Long liveId) {
+        userLimiters.remove(userId + "_" + liveId);
+    }
+
+    /**
+     * 清理直播间限流器
+     */
+    public static void removeRoomLimiter(Long liveId) {
+        roomLimiters.remove(liveId);
+    }
+}

+ 20 - 18
fs-live-app/src/main/java/com/fs/live/websocket/auth/WebSocketConfigurator.java

@@ -72,26 +72,28 @@ public class WebSocketConfigurator extends ServerEndpointConfig.Configurator {
 
             userProperties.put(AttrConstant.USER_TYPE, 0L);
         }
+        String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
+        userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
 
         // 验证签名
-        if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
-            String liveIdStr = parameterMap.get(AttrConstant.LIVE_ID).get(0);
-            String userIdStr = parameterMap.get(AttrConstant.USER_ID).get(0);
-            String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
-            String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
-            String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
-
-            try {
-                if (!VerifyUtils.verifySignature(liveIdStr, userIdStr, userTypeStr, timestampStr, signatureStr)) {
-                    throw new BaseException("缺少必要的参数");
-                }
-
-                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
-            } catch (Exception e) {
-                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
-                throw new BaseException("缺少必要的参数");
-            }
-        }
+//        if (parameterMap.containsKey(AttrConstant.SIGNATURE)) {
+//            String liveIdStr = parameterMap.get(AttrConstant.LIVE_ID).get(0);
+//            String userIdStr = parameterMap.get(AttrConstant.USER_ID).get(0);
+//            String userTypeStr = parameterMap.get(AttrConstant.USER_TYPE).get(0);
+//            String timestampStr = parameterMap.get(AttrConstant.TIMESTAMP).get(0);
+//            String signatureStr = parameterMap.get(AttrConstant.SIGNATURE).get(0);
+//
+//            try {
+//                if (!VerifyUtils.verifySignature(liveIdStr, userIdStr, userTypeStr, timestampStr, signatureStr)) {
+//                    throw new BaseException("缺少必要的参数");
+//                }
+//
+//                userProperties.put(AttrConstant.USER_TYPE, Long.parseLong(userTypeStr));
+//            } catch (Exception e) {
+//                log.warn("webSocket连接验签失败 msg: {}", e.getMessage(), e);
+//                throw new BaseException("缺少必要的参数");
+//            }
+//        }
     }
 
 }

+ 63 - 0
fs-live-app/src/main/java/com/fs/live/websocket/config/WebSocketSessionManager.java

@@ -0,0 +1,63 @@
+package com.fs.live.websocket.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * 直播间WebSocket连接管理器
+ * 核心:按直播间ID分组存储Session,线程安全,低锁竞争
+ */
+@Slf4j
+@Component
+public class WebSocketSessionManager {
+
+    /**
+     * 存储结构:直播间ID -> 该直播间的所有连接Session
+     * CopyOnWriteArraySet:读多写少场景,线程安全且遍历无锁
+     */
+    private static final Map<String, Set<Session>> ROOM_SESSIONS = new ConcurrentHashMap<>();
+
+    /**
+     * 添加连接(绑定直播间)
+     */
+    public void addSession(String roomId, Session session) {
+        // 不存在则创建空集合(ConcurrentHashMap的原子操作)
+        ROOM_SESSIONS.computeIfAbsent(roomId, k -> new CopyOnWriteArraySet<>()).add(session);
+        log.info("直播间[{}]新增连接,当前连接数:{}", roomId, ROOM_SESSIONS.get(roomId).size());
+    }
+
+    /**
+     * 移除连接(清理无效连接)
+     */
+    public void removeSession(String roomId, Session session) {
+        if (ROOM_SESSIONS.containsKey(roomId)) {
+            Set<Session> sessions = ROOM_SESSIONS.get(roomId);
+            sessions.remove(session);
+            // 直播间无连接时清空,释放内存
+            if (sessions.isEmpty()) {
+                ROOM_SESSIONS.remove(roomId);
+            }
+            log.info("直播间[{}]移除连接,当前连接数:{}", roomId, sessions.size());
+        }
+    }
+
+    /**
+     * 获取直播间所有连接
+     */
+    public Set<Session> getRoomSessions(String roomId) {
+        return ROOM_SESSIONS.getOrDefault(roomId, new CopyOnWriteArraySet<>());
+    }
+
+    /**
+     * 获取所有直播间ID
+     */
+    public Set<String> getAllRoomIds() {
+        return ROOM_SESSIONS.keySet();
+    }
+}

+ 94 - 60
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -88,7 +88,7 @@ public class WebSocketServer {
     private static Random random = new Random();
 
     // Redis key 前缀:用户进入直播间时间
-    private static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
+    public static final String USER_ENTRY_TIME_KEY = "live:user:entry:time:%s:%s"; // liveId:userId
 
     // 直播间在线用户缓存
 //    private static final ConcurrentHashMap<Long, Integer> liveOnlineUsers = new ConcurrentHashMap<>();
@@ -110,7 +110,7 @@ public class WebSocketServer {
         if (live == null) {
             throw new BaseException("未找到直播间");
         }
-        long companyId = live.getCompanyId() == null ? -1L : live.getCompanyId();
+        long companyId = -1L;
         long companyUserId = -1L;
         if (!Objects.isNull(userProperties.get("companyId"))) {
             companyId = (long) userProperties.get("companyId");
@@ -131,7 +131,15 @@ public class WebSocketServer {
 
         // 记录连接信息 管理员不记录
         if (userType == 0) {
-            FsUserScrm fsUser = fsUserService.selectFsUserById(userId);
+            // 缓存用户信息,过期时间4小时
+            String userCacheKey = "fs:user:" + userId;
+            FsUserScrm fsUser = redisCache.getCacheObject(userCacheKey);
+            if (fsUser == null) {
+                fsUser = fsUserService.selectFsUserById(userId);
+                if (fsUser != null) {
+                    redisCache.setCacheObject(userCacheKey, fsUser, 4, TimeUnit.HOURS);
+                }
+            }
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -195,7 +203,15 @@ public class WebSocketServer {
                 broadcastWebMessage(liveId, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             }
 
-            LiveUserFirstEntry liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+            // 缓存用户首次进入记录,过期时间4小时
+            String liveUserFirstEntryCacheKey = "live:userFirstEntry:" + liveId + ":" + userId;
+            LiveUserFirstEntry liveUserFirstEntry = redisCache.getCacheObject(liveUserFirstEntryCacheKey);
+            if (liveUserFirstEntry == null) {
+                liveUserFirstEntry = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, userId);
+                if (liveUserFirstEntry != null) {
+                    redisCache.setCacheObject(liveUserFirstEntryCacheKey, liveUserFirstEntry, 4, TimeUnit.HOURS);
+                }
+            }
             // 如果用户连上了 socket,并且公司ID和销售ID大于0,更新 LiveWatchLog 的 logType
 
             if ((qwUserId > 0 && externalContactId > 0) || (liveUserFirstEntry != null && liveUserFirstEntry.getCompanyId() > 0 && liveUserFirstEntry.getCompanyUserId() > 0 )) {
@@ -225,10 +241,20 @@ public class WebSocketServer {
                 }
             } else {
                 // 这个用户A邀请用户b,b的业绩算a的销售的
-                if (companyUserId == -2L) {
-                    LiveUserFirstEntry clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
-                    companyId = clientB.getCompanyId();
-                    companyUserId = clientB.getCompanyUserId();
+                if (companyId == -2L) {
+                    // 缓存用户首次进入记录,过期时间4小时
+                    String clientBCacheKey = "live:userFirstEntry:" + liveId + ":" + companyUserId;
+                    LiveUserFirstEntry clientB = redisCache.getCacheObject(clientBCacheKey);
+                    if (clientB == null) {
+                        clientB = liveUserFirstEntryService.selectEntityByLiveIdUserId(liveId, companyUserId);
+                        if (clientB != null) {
+                            redisCache.setCacheObject(clientBCacheKey, clientB, 4, TimeUnit.HOURS);
+                        }
+                    }
+                    if (clientB != null) {
+                        companyId = clientB.getCompanyId();
+                        companyUserId = clientB.getCompanyUserId();
+                    }
                 }
                 Date date = new Date();
                 liveUserFirstEntry = new LiveUserFirstEntry();
@@ -247,10 +273,10 @@ public class WebSocketServer {
                 }
                 liveUserFirstEntryService.insertLiveUserFirstEntry(liveUserFirstEntry);
             }
-            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry,1, TimeUnit.HOURS);
+            redisCache.setCacheObject( "live:user:first:entry:" + liveId + ":" + userId, liveUserFirstEntry, 4, TimeUnit.HOURS);
 
             // 推送完课积分倒计时配置信息给前端
-            sendCompletionPointsConfigToUser(session, liveId, userId, live);
+//            sendCompletionPointsConfigToUser(session, liveId, userId, live);
 
 
         } else {
@@ -287,7 +313,15 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
         if (userType == 0) {
-            FsUserScrm fsUser = fsUserService.selectFsUserById(userId);
+            // 缓存用户信息,过期时间4小时
+            String userCacheKey = "fs:user:" + userId;
+            FsUserScrm fsUser = redisCache.getCacheObject(userCacheKey);
+            if (fsUser == null) {
+                fsUser = fsUserService.selectFsUserById(userId);
+                if (fsUser != null) {
+                    redisCache.setCacheObject(userCacheKey, fsUser, 4, TimeUnit.HOURS);
+                }
+            }
             if (Objects.isNull(fsUser)) {
                 throw new BaseException("用户信息错误");
             }
@@ -360,55 +394,55 @@ public class WebSocketServer {
 
 
 
-                    if (msg.getData() != null && !msg.getData().isEmpty()) {
-                        try {
-                            Long currentDuration = Long.parseLong(msg.getData());
-
-                            Live currentLive = liveService.selectLiveByLiveId(liveId);
-                            if (currentLive == null) {
-                                break;
-                            }
-
-
-                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
-                            boolean isLiveStarted = false;
-                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
-                                // status=2 表示直播中
-                                isLiveStarted = true;
-                            } else if (currentLive.getStartTime() != null) {
-                                // 判断当前时间是否已超过开播时间
-                                LocalDateTime now = LocalDateTime.now();
-                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
-                            }
-
-                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
-                            String hashKey = "live:watch:duration:hash:" + liveId;
-                            String userIdField = String.valueOf(watchUserId);
-
-                            if (!isLiveStarted) {
-                                redisCache.hashDelete(hashKey, userIdField);
-                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
-                                break;
-                            }
-
-                            // 直播已开始,记录观看时长
-                            // 获取现有时长
-                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
-                            // 只有当新的时长更大时才更新
-                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
-                                // 更新Hash中的用户时长
-                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
-                                // 设置过期时间(2小时)
-                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
-
-                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
-
-                            }
-                        } catch (Exception e) {
-                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
-                                    liveId, watchUserId, msg.getData(), e);
-                        }
-                    }
+//                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+//                        try {
+//                            Long currentDuration = Long.parseLong(msg.getData());
+//
+//                            Live currentLive = liveService.selectLiveByLiveId(liveId);
+//                            if (currentLive == null) {
+//                                break;
+//                            }
+//
+//
+//                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
+//                            boolean isLiveStarted = false;
+//                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
+//                                // status=2 表示直播中
+//                                isLiveStarted = true;
+//                            } else if (currentLive.getStartTime() != null) {
+//                                // 判断当前时间是否已超过开播时间
+//                                LocalDateTime now = LocalDateTime.now();
+//                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
+//                            }
+//
+//                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
+//                            String hashKey = "live:watch:duration:hash:" + liveId;
+//                            String userIdField = String.valueOf(watchUserId);
+//
+//                            if (!isLiveStarted) {
+//                                redisCache.hashDelete(hashKey, userIdField);
+//                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
+//                                break;
+//                            }
+//
+//                            // 直播已开始,记录观看时长
+//                            // 获取现有时长
+//                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
+//                            // 只有当新的时长更大时才更新
+//                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+//                                // 更新Hash中的用户时长
+//                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
+//                                // 设置过期时间(2小时)
+//                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
+//
+//                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
+//
+//                            }
+//                        } catch (Exception e) {
+//                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
+//                                    liveId, watchUserId, msg.getData(), e);
+//                        }
+//                    }
 
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;

+ 138 - 0
fs-live-mq/pom.xml

@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>fs-live-mq</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>
+
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 验证码 -->
+        <dependency>
+            <groupId>com.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 获取系统信息 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.tencentyun</groupId>
+            <artifactId>tls-sig-api-v2</artifactId>
+            <version>2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+            <version>5.1.10.RELEASE</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+
+
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 14 - 0
fs-live-mq/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(FsLiveMqApplication.class);
+    }
+}

+ 27 - 0
fs-live-mq/src/main/java/com/fs/FsLiveMqApplication.java

@@ -0,0 +1,27 @@
+package com.fs;
+
+import org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.context.annotation.Import;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@Import({ RocketMQAutoConfiguration.class })
+@EnableTransactionManagement
+@EnableAsync
+@EnableScheduling
+public class FsLiveMqApplication
+{
+    public static void main(String[] args)
+    {
+        SpringApplication.run(FsLiveMqApplication.class, args);
+        System.out.println("live-mq启动成功");
+    }
+}

+ 12 - 0
fs-live-mq/src/main/java/com/fs/app/annotation/Login.java

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

+ 15 - 0
fs-live-mq/src/main/java/com/fs/app/annotation/LoginUser.java

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

+ 125 - 0
fs-live-mq/src/main/java/com/fs/app/controller/AdCallbackController.java

@@ -0,0 +1,125 @@
+package com.fs.app.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.ad.domain.AdHtmlTemplate;
+import com.fs.ad.domain.AdSite;
+import com.fs.ad.mapper.AdHtmlTemplateMapper;
+import com.fs.ad.service.IAdAccountService;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.ad.service.IAdSiteService;
+import com.fs.baidu.api.BaiduApis;
+import com.fs.baidu.service.IBdAccountService;
+import com.fs.baidu.vo.ad.AdBaiduClickCallbackVo;
+import com.fs.baidu.vo.ad.AdDyClickCallbackVo;
+import com.fs.baidu.vo.ad.AdIqiyiClickCallbackVo;
+import com.fs.baidu.vo.ad.AdYouKuClickCallbackVo;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.qw.domain.QwWorkLink;
+import com.fs.qw.service.IQwWorkLinkService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 知识库Controller
+ *
+ * @author fs
+ * @date 2024-04-21
+ */
+@Slf4j
+@AllArgsConstructor
+@RestController
+@RequestMapping("/baidu")
+public class AdCallbackController extends BaseController {
+
+    private final AdHtmlTemplateMapper adHtmlTemplateMapper;
+    private final IAdHtmlClickLogService adHtmlClickLogService;
+    private final BaiduApis baiduApis;
+    private final IBdAccountService bdAccountService;
+    private final IAdSiteService adSiteService;
+    private final IAdAccountService adAccountService;
+    private final IQwWorkLinkService qwWorkLinkService;
+
+    //百度-页面点击接口
+//    @GetMapping("/callback")
+//    public R callback(String url, String bdVid, String t, Long id) {
+//        if(id == null || StringUtils.isEmpty(bdVid)) {
+//            return R.ok();
+//        }
+//        adHtmlClickLogService.addLog(url, id, bdVid, t);
+//        return R.ok();
+//    }
+    // 百度点击监听返回接口
+//    @GetMapping("/clickCallback")
+//    public R clickCallback(AdClickCallbackVo vo){
+//        log.info("百度监听地址返回数据:{}", JSON.toJSONString(vo));
+//        adHtmlClickLogService.setLogBaiDu(vo, "67", 0);
+//        return R.ok();
+//    }
+    @GetMapping("/getTemplateByNo")
+    public R getTemplateByNo(String no){
+        AdHtmlTemplate htmlUrl = adHtmlTemplateMapper.selectOne(new QueryWrapper<AdHtmlTemplate>().eq("no", no));
+        if(htmlUrl == null){
+            return R.error("错误编号");
+        }
+        return R.ok().put("data",  htmlUrl);
+    }
+    @GetMapping("/getTemplateById")
+    public R getTemplateById(Long id){
+        AdSite site = adSiteService.getById(id);
+        if(site.getWorkId() != null){
+            QwWorkLink byId = qwWorkLinkService.getById(site.getWorkId());
+            if(byId != null){
+                site.setWorkUrl(byId.getUrl());
+            }
+        }
+        AdHtmlTemplate htmlUrl = adHtmlTemplateMapper.selectById(site.getTemplateId());
+        if(htmlUrl == null){
+            return R.error("错误编号");
+        }
+        return R.ok().put("site", site).put("data",  htmlUrl);
+    }
+//    @GetMapping("/syncPlan")
+//    public R syncPlan(){
+//        baiduApis.listAccount(1L);
+//        return R.ok();
+//    }
+
+
+    @GetMapping("/baiduClickCallbackApi")
+    public R baiduClickCallbackApi(AdBaiduClickCallbackVo vo){
+        log.info("百度点击监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogBaiDuApi(vo);
+        return R.ok();
+    }
+    @GetMapping("/baiduClickCallback")
+    public R baiduClickCallback(AdBaiduClickCallbackVo vo){
+        log.info("百度监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogBaiDu(vo);
+        return R.ok();
+    }
+    @GetMapping("/youkuClickCallback")
+    public R youkuClickCallback(AdYouKuClickCallbackVo vo){
+        log.info("优酷监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogYouKu(vo);
+        return R.ok();
+    }
+    @GetMapping("/iqiyiClickCallback")
+    public R iqiyiClickCallback(AdIqiyiClickCallbackVo vo){
+        log.info("爱奇艺监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogIqiyi(vo);
+        return R.ok();
+    }
+    @GetMapping("/dyClickCallback")
+    public R dyClickCallback(AdDyClickCallbackVo vo){
+        log.info("抖音监听地址返回数据:{}", JSON.toJSONString(vo));
+        adHtmlClickLogService.setLogDy(vo);
+        return R.ok();
+    }
+
+
+}

+ 37 - 0
fs-live-mq/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,37 @@
+package com.fs.app.controller;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.ad.enums.AdUploadType;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.common.core.domain.R;
+import com.fs.qw.vo.AdUploadVo;
+import io.swagger.annotations.Api;
+import jdk.nashorn.internal.ir.annotations.Ignore;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@Slf4j
+@Api("公共接口")
+@RestController
+@AllArgsConstructor
+@Ignore
+@RequestMapping(value="/app/common")
+public class CommonController {
+    private IAdHtmlClickLogService adHtmlClickLogService;
+    private RocketMQTemplate rocketMQTemplate;
+
+    @GetMapping("/testSend")
+    public R testSend(String id){
+        AdUploadVo build = AdUploadVo.builder().state(id).type(AdUploadType.ADD_WX).build();
+        rocketMQTemplate.syncSend("ad-upload", JSON.toJSONString(build));
+//        adHtmlClickLogService.upload(id, AdUploadType.ADD_WX, e -> {});
+        return R.ok();
+    }
+
+}

+ 215 - 0
fs-live-mq/src/main/java/com/fs/app/controller/MockAppController.java

@@ -0,0 +1,215 @@
+package com.fs.app.controller;
+
+import com.fs.baidu.domain.BdApi;
+import com.fs.baidu.service.IBdApiService;
+import com.fs.baidu.utils.SignService;
+import com.fs.huifuPay.sdk.opps.core.exception.BasePayException;
+import com.fs.huifuPay.sdk.opps.core.utils.HttpClientUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONObject;
+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 javax.servlet.http.HttpServletRequest;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+@Slf4j
+@RestController
+@RequestMapping("/baiduBack")
+public class MockAppController {
+    private static final String APPID = "appId";
+    private static final String AUTH_CODE = "authCode";
+    private static final String USERID = "userId";
+    private static final String TIMESTAMP = "timestamp";
+    private static final String SIGNATURE = "signature";
+    private static final String STATE = "state";
+    private static final String ACCESSTOKEN_URL = "https://u.baidu.com/oauth/accessToken";
+
+    @Autowired
+    private IBdApiService bdApiService; // 开发者自定义
+    @Autowired
+    private SignService signService; // 开发者可自定义
+
+    @GetMapping("/callback")
+    public String mockApp(HttpServletRequest request) throws BasePayException {
+
+        // 获取请求参数
+        Map<String, String> params = new HashMap<>();
+        this.fillParams(params, request);
+        log.info("callback: params = {}", JSONObject.valueToString(params));
+        int userIdInt = 0;
+        try {
+            userIdInt = Integer.parseInt(params.get(USERID));
+        } catch (Exception e) {
+            return this.getResponseJson(600011, "参数错误", new Object());
+        }
+        // 对签名内容进行判空
+        if (StringUtils.isBlank(params.get(SIGNATURE))) {
+            return this.getResponseJson(600011, "参数错误", new Object());
+        }
+
+        // 获取应用对应的密钥
+        BdApi app = bdApiService.getAppByAppId(params.get(APPID));
+        String sk = app.getAppSecretKey();
+        // 开发者进行验签
+        // 检查状态码
+//        if (!this.checkState(params.get(APPID), app.getAppUserId(), params.get(STATE))) {
+//            log.info("callback: state check fail");
+//            return this.getResponseJson(600011, "状态码错误", new Object());
+//        }
+
+        // 签名验证
+        if (!this.checkSignature(params, sk)) {
+            log.info("callback: signature check fail");
+            return this.getResponseJson(600011, "签名错误", new Object());
+        }
+
+        // 调用接口换取授权令牌
+        String response = this.getAccessToken(params, sk, userIdInt);
+        log.info("callback:getAccesstoken, response={}", response);
+        String accessToken = null;
+        String refreshToken = null;
+        int uid;
+
+        if (response.length() > 0) {
+            JSONObject res = new JSONObject(response);
+            int code = (int) res.get("code");
+            if (code == 0) {
+                JSONObject data = res.getJSONObject("data");
+                accessToken = (String) data.get("accessToken");
+                // 注释部分为 accessToken 其他信息,各应用开发者酌情使用即可
+                 refreshToken = (String) data.get("refreshToken");
+                String openId = data.getString("openId");
+                int expiresIn = data.getInt("expiresIn");
+                int refreshExpiresIn = data.getInt("refreshExpiresIn");
+                app.setAccessToken(accessToken);
+                app.setRefreshToken(refreshToken);
+                app.setExpiresIn(expiresIn);
+                app.setOpenId(openId);
+                app.setRefreshExpiresIn(refreshExpiresIn);
+                app.setAuthCode(params.get(AUTH_CODE));
+                bdApiService.updateById(app);
+
+                // TODO 相关信息落库处理
+            } else {
+                return this.getResponseJson(600011, "未获取到 access_token", new Object());
+            }
+        }
+        Map<String, String> data = new HashMap<>();
+        data.put("accessToken", accessToken);
+        return this.getResponseJson(0, "success", data);
+    }
+
+    /**
+     * 填充请求参数,开发者可用实体代替map
+     *
+     * @param params
+     * @param request
+     */
+    private void fillParams(Map<String, String> params, HttpServletRequest request) {
+        params.put(APPID, request.getParameter(APPID));
+        params.put(AUTH_CODE, request.getParameter(AUTH_CODE));
+        params.put(USERID, request.getParameter(USERID));
+        params.put(TIMESTAMP, request.getParameter(TIMESTAMP));
+        params.put(SIGNATURE, request.getParameter(SIGNATURE));
+        params.put(STATE, request.getParameter(STATE));
+    }
+
+    /**
+     * 校验状态码是否符合预期
+     *
+     * @param appId  应用ID
+     * @param userId 创建应用的ID
+     * @param state  请求参数中的状态码
+     * @return
+     */
+    private boolean checkState(String appId, Long userId, String state) {
+        // md5util 开发者自行开发即可
+        String newState = md5(appId.concat("_").concat(String.valueOf(userId)));
+        return newState.equals(state);
+    }
+    public static String md5(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] messageDigest = md.digest(input.getBytes());
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : messageDigest) {
+                String hex = Integer.toHexString(0xff & b);
+                if (hex.length() == 1) {
+                    hexString.append('0');
+                }
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    /**
+     * 签名验证方法
+     *
+     * @param params
+     * @param sk
+     * @return
+     */
+    private boolean checkSignature(Map<String, String> params, String sk) {
+        // 获取验签字符串:按key自然排序,拼接成 json 字符串,
+        // 示例:str1 = {"appId": xxxxx, "authCode": xxx, "state": xxx,"timestamp": xxx}
+        TreeMap<String, Object> map = new TreeMap<>();
+        map.put(APPID, params.get(APPID));
+        map.put(AUTH_CODE, params.get(AUTH_CODE));
+        map.put(USERID, params.get(USERID));
+        map.put(STATE, params.get(STATE));
+        map.put(TIMESTAMP, params.get(TIMESTAMP));
+        // 根据上述签名算法对接收到的参数签名
+        String sign = signService.paramsSign(sk, map);
+        log.info("callback: signature = {}", sign);
+        // 判断签名和URL传参签名是否一致
+        return params.get(SIGNATURE).equals(sign);
+    }
+
+    /**
+     * 换取授权令牌
+     *
+     * @param params
+     * @param sk
+     * @param userIdInt 授权账户ID
+     * @return
+     */
+    private String getAccessToken(Map<String, String> params, String sk, int userIdInt) throws BasePayException {
+        Map<String, Object> requestMap = new HashMap<>();
+        requestMap.put(APPID, params.get(APPID));
+        requestMap.put("secretKey", sk);
+        requestMap.put(AUTH_CODE, params.get(AUTH_CODE));
+        requestMap.put("grantType", "access_token");
+        requestMap.put("userId", userIdInt);
+
+        String paramsJson = JSONObject.valueToString(requestMap);
+        // 开发者自行增加异常判断
+        return HttpClientUtils.httpPostJson(ACCESSTOKEN_URL, new HashMap<>(), paramsJson);
+    }
+
+    /**
+     * 封装返回json串
+     *
+     * @param code
+     * @param message
+     * @param data
+     * @return
+     */
+    private String getResponseJson(int code, String message, Object data) {
+        // 封装返回字段的map
+        Map<String, Object> responseMap = new HashMap<>();
+        responseMap.put("code", code);
+        responseMap.put("message", message);
+        responseMap.put("data", data);
+        return JSONObject.valueToString(responseMap);
+    }
+}

+ 51 - 0
fs-live-mq/src/main/java/com/fs/app/exception/FSException.java

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

+ 81 - 0
fs-live-mq/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -0,0 +1,81 @@
+package com.fs.app.exception;
+
+
+
+
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+
+/**
+ * 异常处理器
+ */
+@RestControllerAdvice
+public class FSExceptionHandler {
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	/**
+	 * 处理自定义异常
+	 */
+	@ExceptionHandler(FSException.class)
+	public R handleRRException(FSException e){
+		R r = new R();
+		r.put("code", e.getCode());
+		r.put("msg", e.getMessage());
+
+		return r;
+	}
+
+	@ExceptionHandler(NoHandlerFoundException.class)
+	public R handlerNoFoundException(Exception e) {
+		logger.error(e.getMessage(), e);
+		return R.error(404, "路径不存在,请检查路径是否正确");
+	}
+
+	@ExceptionHandler(DuplicateKeyException.class)
+	public R handleDuplicateKeyException(DuplicateKeyException e){
+		logger.error(e.getMessage(), e);
+		return R.error("数据库中已存在该记录");
+	}
+
+
+	@ExceptionHandler(Exception.class)
+	public R handleException(Exception e){
+		logger.error(e.getMessage(), e);
+		return R.error();
+	}
+	@ExceptionHandler(AccessDeniedException.class)
+	public R handleAccessDeniedException(AccessDeniedException e){
+		logger.error(e.getMessage(), e);
+		return R.error("没有权限");
+	}
+
+	@ExceptionHandler(BindException.class)
+	public R bindExceptionHandler(BindException e) {
+		FieldError error = e.getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public R exceptionHandler(MethodArgumentNotValidException e) {
+		FieldError error = e.getBindingResult().getFieldError();
+		String message = String.format("%s",  error.getDefaultMessage());
+		return R.error(message);
+	}
+	@ExceptionHandler(CustomException.class)
+	public R handleException(CustomException e){
+
+		return R.error(e.getMessage());
+	}
+}

+ 49 - 0
fs-live-mq/src/main/java/com/fs/app/mq/RocketMQConsumerService.java

@@ -0,0 +1,49 @@
+package com.fs.app.mq;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.mapper.LiveGoodsMapper;
+import com.fs.live.vo.LiveGoodsUploadMqVo;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.vo.AdUploadVo;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+@RocketMQMessageListener(topic = "live-goods-upload", consumerGroup = "${rocketmq.consumer.group}")
+public class RocketMQConsumerService implements RocketMQListener<String> {
+
+    private final SqlSessionFactory sqlSessionFactory;
+
+    @Override
+    public void onMessage(String message) {
+        LiveGoodsUploadMqVo vo = JSON.parseObject(message, LiveGoodsUploadMqVo.class);
+        if(vo == null || vo.getGoodsId() == null || vo.getGoodsNum() == null){
+            return;
+        }
+        // 2. 数据库事务:更新库存+销量(原子操作)
+        try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE)) {
+            LiveGoodsMapper mapper = session.getMapper(LiveGoodsMapper.class);
+            // 事务内更新:扣减可用库存 + 增加销量
+            int affected = mapper.updateStock(vo.getGoodsId(),vo.getGoodsNum());
+            if (affected > 0) {
+                session.commit();
+                // 标记消息已消费(过期时间=1天)
+                log.info("库存销量入库成功,skuId={}, deductNum={}", vo.getGoodsId(), vo.getGoodsNum());
+            } else {
+                log.error("库存销量入库失败(无匹配SKU),msg={}", vo);
+            }
+        } catch (Exception e) {
+            log.error("库存销量入库异常,msg={}", vo, e);
+        }
+    }
+}

+ 182 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

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

+ 73 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

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

+ 245 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/LogAspect.java

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

+ 117 - 0
fs-live-mq/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

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

+ 31 - 0
fs-live-mq/src/main/java/com/fs/framework/config/ApplicationConfig.java

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

+ 85 - 0
fs-live-mq/src/main/java/com/fs/framework/config/CaptchaConfig.java

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

+ 92 - 0
fs-live-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java

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

+ 123 - 0
fs-live-mq/src/main/java/com/fs/framework/config/DruidConfig.java

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

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

+ 158 - 0
fs-live-mq/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,158 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.math.BigDecimal;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 65 - 0
fs-live-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java

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

+ 50 - 0
fs-live-mq/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * spring security配置
+ * 
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

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

+ 63 - 0
fs-live-mq/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    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);
+            }
+        };
+    }
+}

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

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

+ 56 - 0
fs-live-mq/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-live-mq/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-live-mq/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);
+    }
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.