Browse Source

Merge remote-tracking branch 'origin/bly_store' into bly_store

xgb 1 ngày trước cách đây
mục cha
commit
ca6034d487
100 tập tin đã thay đổi với 4460 bổ sung188 xóa
  1. 22 3
      fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java
  2. 124 0
      fs-admin/src/main/java/com/fs/his/controller/FsComplaintTemplateController.java
  3. 2 1
      fs-admin/src/main/java/com/fs/his/controller/FsPackageOrderController.java
  4. 40 2
      fs-admin/src/main/java/com/fs/his/controller/FsStoreOrderController.java
  5. 98 0
      fs-admin/src/main/java/com/fs/his/controller/FsUserComplaintController.java
  6. 29 1
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  7. 2 0
      fs-admin/src/main/java/com/fs/his/task/Task.java
  8. 10 67
      fs-company/src/main/java/com/fs/company/controller/common/Test.java
  9. 6 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmMsgController.java
  10. 5 6
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  11. 6 5
      fs-company/src/main/java/com/fs/company/controller/store/FsInquiryOrderController.java
  12. 14 1
      fs-company/src/main/java/com/fs/company/controller/store/FsPackageOrderController.java
  13. 1 1
      fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java
  14. 12 2
      fs-framework/src/main/java/com/fs/framework/web/exception/GlobalExceptionHandler.java
  15. 5 2
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  16. 141 0
      fs-qw-mq/pom.xml
  17. 14 0
      fs-qw-mq/src/main/java/com/fs/FSServletInitializer.java
  18. 24 0
      fs-qw-mq/src/main/java/com/fs/FsQwMqApiApplication.java
  19. 58 0
      fs-qw-mq/src/main/java/com/fs/app/controller/CommonController.java
  20. 51 0
      fs-qw-mq/src/main/java/com/fs/app/exception/FSException.java
  21. 81 0
      fs-qw-mq/src/main/java/com/fs/app/exception/FSExceptionHandler.java
  22. 34 0
      fs-qw-mq/src/main/java/com/fs/app/mq/RocketMQConsumerCourseFinishService.java
  23. 188 0
      fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishRtyTaskOne.java
  24. 182 0
      fs-qw-mq/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  25. 73 0
      fs-qw-mq/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  26. 245 0
      fs-qw-mq/src/main/java/com/fs/framework/aspectj/LogAspect.java
  27. 117 0
      fs-qw-mq/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  28. 31 0
      fs-qw-mq/src/main/java/com/fs/framework/config/ApplicationConfig.java
  29. 85 0
      fs-qw-mq/src/main/java/com/fs/framework/config/CaptchaConfig.java
  30. 100 0
      fs-qw-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java
  31. 72 0
      fs-qw-mq/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  32. 59 0
      fs-qw-mq/src/main/java/com/fs/framework/config/FilterConfig.java
  33. 76 0
      fs-qw-mq/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  34. 150 0
      fs-qw-mq/src/main/java/com/fs/framework/config/MyBatisConfig.java
  35. 121 0
      fs-qw-mq/src/main/java/com/fs/framework/config/RedisConfig.java
  36. 65 0
      fs-qw-mq/src/main/java/com/fs/framework/config/ResourcesConfig.java
  37. 50 0
      fs-qw-mq/src/main/java/com/fs/framework/config/SecurityConfig.java
  38. 33 0
      fs-qw-mq/src/main/java/com/fs/framework/config/ServerConfig.java
  39. 121 0
      fs-qw-mq/src/main/java/com/fs/framework/config/SwaggerConfig.java
  40. 63 0
      fs-qw-mq/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  41. 77 0
      fs-qw-mq/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  42. 27 0
      fs-qw-mq/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  43. 45 0
      fs-qw-mq/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  44. 56 0
      fs-qw-mq/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  45. 126 0
      fs-qw-mq/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  46. 56 0
      fs-qw-mq/src/main/java/com/fs/framework/manager/AsyncManager.java
  47. 40 0
      fs-qw-mq/src/main/java/com/fs/framework/manager/ShutdownManager.java
  48. 103 0
      fs-qw-mq/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  49. 1 0
      fs-qw-mq/src/main/resources/META-INF/spring-devtools.properties
  50. 2 0
      fs-qw-mq/src/main/resources/banner.txt
  51. 37 0
      fs-qw-mq/src/main/resources/i18n/messages.properties
  52. 93 0
      fs-qw-mq/src/main/resources/logback.xml
  53. 15 0
      fs-qw-mq/src/main/resources/mybatis/mybatis-config.xml
  54. 103 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/AsyncCourseWatchFinishService.java
  55. 13 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  56. 1 1
      fs-service/src/main/java/com/fs/aiTongueApi/config/AiTongueConfig.java
  57. 7 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  58. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseFinishTempService.java
  59. 212 2
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java
  60. 1 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java
  61. 4 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java
  62. 5 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  63. 1 2
      fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchStatisticsServiceImpl.java
  64. 4 2
      fs-service/src/main/java/com/fs/erp/dto/sdk/wangdian/api/WdtClient.java
  65. 1 1
      fs-service/src/main/java/com/fs/erp/http/JstErpHttpServiceImpl.java
  66. 9 7
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  67. 2 2
      fs-service/src/main/java/com/fs/erp/service/impl/WdtErpGoodsServiceImpl.java
  68. 11 12
      fs-service/src/main/java/com/fs/erp/service/impl/WdtErpOrderServiceImpl.java
  69. 19 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  70. 4 0
      fs-service/src/main/java/com/fs/his/config/StoreConfig.java
  71. 4 3
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  72. 6 1
      fs-service/src/main/java/com/fs/his/enums/FsStoreOrderLogEnum.java
  73. 10 1
      fs-service/src/main/java/com/fs/his/mapper/FsPackageOrderMapper.java
  74. 5 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderMapper.java
  75. 1 0
      fs-service/src/main/java/com/fs/his/param/FsStoreOrderSetErpPhoneParam.java
  76. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsInquiryOrderService.java
  77. 2 1
      fs-service/src/main/java/com/fs/his/service/IFsPackageOrderService.java
  78. 5 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java
  79. 47 0
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java
  80. 46 2
      fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java
  81. 2 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreAfterSalesServiceImpl.java
  82. 18 1
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  83. 143 0
      fs-service/src/main/java/com/fs/his/utils/IdCardUtil.java
  84. 14 0
      fs-service/src/main/java/com/fs/his/vo/FsStoreOrderListAndStatisticsVo.java
  85. 1 1
      fs-service/src/main/java/com/fs/hisStore/enums/SysConfigEnum.java
  86. 2 2
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java
  87. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  88. 41 4
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  89. 2 2
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserSignScrmServiceImpl.java
  90. 48 0
      fs-service/src/main/java/com/fs/qw/domain/QwCourseFinishRemarkRty.java
  91. 1 1
      fs-service/src/main/java/com/fs/qw/domain/QwUserVoiceLog.java
  92. 74 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCourseFinishRemarkRtyMapper.java
  93. 5 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  94. 72 0
      fs-service/src/main/java/com/fs/qw/service/IQwCourseFinishRemarkRtyService.java
  95. 5 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java
  96. 1 1
      fs-service/src/main/java/com/fs/qw/service/IQwUserVoiceLogService.java
  97. 117 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCourseFinishRemarkRtyServiceImpl.java
  98. 20 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  99. 6 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  100. 68 44
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserVoiceLogServiceImpl.java

+ 22 - 3
fs-admin/src/main/java/com/fs/course/controller/FsCourseRedPacketLogController.java

@@ -1,14 +1,22 @@
 package com.fs.course.controller;
 
+import java.util.ArrayList;
 import java.util.List;
 
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
 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.config.CourseConfig;
 import com.fs.course.mapper.FsUserCourseMapper;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.FsCourseRedPacketLogParam;
 import com.fs.course.vo.FsCourseRedPacketLogListPVO;
+import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.PhoneUtil;
 import com.fs.his.vo.OptionsVO;
+import com.fs.system.service.ISysConfigService;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -46,8 +54,10 @@ public class FsCourseRedPacketLogController extends BaseController
     FsUserCourseMapper fsUserCourseMapper;
     @Autowired
     FsUserCourseVideoMapper fsUserCourseVideoMapper;
-
-
+    @Autowired
+    private TokenService tokenService;
+    @Autowired
+    private ISysConfigService configService;
     /**
      * 查询短链课程看课记录列表
      */
@@ -135,7 +145,16 @@ public class FsCourseRedPacketLogController extends BaseController
     @GetMapping("/courseList")
     public R courseList()
     {
-        List<OptionsVO> optionsVOS = fsUserCourseMapper.selectFsUserCourseAllList();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long userId = loginUser.getUser().getUserId();
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        List<OptionsVO> optionsVOS = new ArrayList<>();
+        if (ObjectUtil.isNotEmpty(config.getIsBound())&&config.getIsBound()){
+            optionsVOS = fsUserCourseMapper.selectFsUserCourseAllListByUserId(userId);
+        }else {
+            optionsVOS = fsUserCourseMapper.selectFsUserCourseAllList();
+        }
         return R.ok().put("list", optionsVOS);
     }
 

+ 124 - 0
fs-admin/src/main/java/com/fs/his/controller/FsComplaintTemplateController.java

@@ -0,0 +1,124 @@
+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.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.his.domain.FsComplaintTemplate;
+import com.fs.his.service.IFsComplaintTemplateService;
+import com.fs.his.utils.ComplaintTreeUtil;
+import com.fs.his.vo.FsComplaintTemplateVO;
+import com.google.common.collect.Lists;
+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 2025-06-09
+ */
+@RestController
+@RequestMapping("/his/template")
+public class FsComplaintTemplateController extends BaseController
+{
+    @Autowired
+    private IFsComplaintTemplateService fsComplaintTemplateService;
+
+    /**
+     * 查询投诉模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsComplaintTemplate fsComplaintTemplate)
+    {
+        startPage();
+        fsComplaintTemplate.setIsDel(0L);
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询投诉模板树形结果
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:list')")
+    @GetMapping("/treeList")
+    public R treeList(FsComplaintTemplate fsComplaintTemplate)
+    {
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        List<FsComplaintTemplateVO> templateVOS = Lists.newArrayList();
+        for (FsComplaintTemplate template : list){
+            FsComplaintTemplateVO templateVO = new FsComplaintTemplateVO();
+            templateVO.setId(template.getId());
+            templateVO.setName(template.getName());
+            templateVO.setParentId(template.getParentId());
+            templateVO.setDescription(template.getDescription());
+            templateVO.setSort(template.getSort());
+            templateVOS.add(templateVO);
+        }
+        return R.ok().put("data", ComplaintTreeUtil.list2TreeConverter(templateVOS, 0));
+    }
+
+
+    /**
+     * 导出投诉模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:export')")
+    @Log(title = "投诉模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsComplaintTemplate fsComplaintTemplate)
+    {
+        List<FsComplaintTemplate> list = fsComplaintTemplateService.selectFsComplaintTemplateList(fsComplaintTemplate);
+        ExcelUtil<FsComplaintTemplate> util = new ExcelUtil<FsComplaintTemplate>(FsComplaintTemplate.class);
+        return util.exportExcel(list, "投诉模板数据");
+    }
+
+    /**
+     * 获取投诉模板详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsComplaintTemplateService.selectFsComplaintTemplateById(id));
+    }
+
+    /**
+     * 新增投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:add')")
+    @Log(title = "投诉模板", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsComplaintTemplate fsComplaintTemplate)
+    {
+        return toAjax(fsComplaintTemplateService.insertFsComplaintTemplate(fsComplaintTemplate));
+    }
+
+    /**
+     * 修改投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:edit')")
+    @Log(title = "投诉模板", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsComplaintTemplate fsComplaintTemplate)
+    {
+        return toAjax(fsComplaintTemplateService.updateFsComplaintTemplate(fsComplaintTemplate));
+    }
+
+    /**
+     * 删除投诉模板
+     */
+    @PreAuthorize("@ss.hasPermi('his:template:remove')")
+    @Log(title = "投诉模板", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsComplaintTemplateService.deleteFsComplaintTemplateByIds(ids));
+    }
+}

+ 2 - 1
fs-admin/src/main/java/com/fs/his/controller/FsPackageOrderController.java

@@ -193,7 +193,8 @@ public class FsPackageOrderController extends BaseController
     @GetMapping("storeRefund/{orderId}")
     public AjaxResult storeRefund(@PathVariable("orderId") Long orderId)
     {
-        return AjaxResult.success(fsPackageOrderService.PackageStoreOrderRefund(orderId));
+        String nickName = getLoginUser().getUser().getNickName();
+        return AjaxResult.success(fsPackageOrderService.PackageStoreOrderRefund(orderId,nickName));
     }
 
     /**

+ 40 - 2
fs-admin/src/main/java/com/fs/his/controller/FsStoreOrderController.java

@@ -1,9 +1,11 @@
 package com.fs.his.controller;
 
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import cn.hutool.core.util.StrUtil;
@@ -35,6 +37,7 @@ import com.fs.his.domain.*;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.dto.StoreOrderExpressExportDTO;
 import com.fs.his.dto.TracesDTO;
+import com.fs.his.enums.FsStoreOrderLogEnum;
 import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.param.FsFollowMsgParam;
 import com.fs.his.param.FsStoreOrderParam;
@@ -48,6 +51,7 @@ import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysRoleService;
 import com.github.pagehelper.PageHelper;
 import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -120,11 +124,14 @@ public class FsStoreOrderController extends BaseController
 
     @Autowired
     private CloudHostProper cloudHostProper;
+
+    @Autowired
+    private IFsStoreOrderLogsService fsStoreOrderLogsService;
     /**
      * 查询订单列表
      */
     @PostMapping("/list")
-    public TableDataInfo list(@RequestBody FsStoreOrderParam fsStoreOrder)
+    public FsStoreOrderListAndStatisticsVo list(@RequestBody FsStoreOrderParam fsStoreOrder)
     {
         PageHelper.startPage(fsStoreOrder);
         if (fsStoreOrder.getUserPhoneMk()!=null&& !fsStoreOrder.getUserPhoneMk().isEmpty()){
@@ -151,7 +158,29 @@ public class FsStoreOrderController extends BaseController
             }
             dataTable.setMsg("jnmy");
         }
-        return dataTable;
+        FsStoreOrderListAndStatisticsVo vo = new FsStoreOrderListAndStatisticsVo();
+        BeanUtils.copyProperties(dataTable, vo);
+        if (dataTable.getTotal()>0){
+            Map<String,BigDecimal> statistics= fsStoreOrderService.selectFsStoreOrderStatistics(fsStoreOrder);
+            if (statistics != null && statistics.size() >= 3){
+                vo.setPayPriceTotal(statistics.get("pay_price").toString());
+                vo.setPayMoneyTotal(statistics.get("pay_money").toString());
+                vo.setPayRemainTotal(statistics.get("pay_remain").toString());
+            }else {
+                vo.setPayPriceTotal("0");
+                vo.setPayMoneyTotal("0");
+                vo.setPayRemainTotal("0");
+            }
+            //商品数量合计
+            String productStatistics= fsStoreOrderService.selectFsStoreOrderProductStatistics(fsStoreOrder);
+            if (StringUtils.isNotBlank(productStatistics)){
+                vo.setProductInfo(productStatistics);
+            } else {
+                vo.setProductInfo("");
+            }
+
+        }
+        return vo;
     }
 
     /**
@@ -598,6 +627,7 @@ public class FsStoreOrderController extends BaseController
     @PostMapping(value = "/batchCreateErpOrder")
     public R batchCreateErpOrder(@RequestBody FsStoreOrderSetErpPhoneParam param)
     {
+        String nickName = getLoginUser().getUser().getNickName();
         String loginAccount = param.getLoginAccount();
         if (StringUtils.isBlank(loginAccount)){
             return R.error("未选择推送erp账户");
@@ -629,8 +659,12 @@ public class FsStoreOrderController extends BaseController
                 FsStoreOrderDf temp = fsStoreOrderDfService.selectFsStoreOrderDfByOrderId(df.getOrderId());
                 if (temp == null){
                     fsStoreOrderDfService.insertFsStoreOrderDf(df);
+                    fsStoreOrderLogsService.create(orderId, FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getValue(),
+                            nickName + " " +FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getDesc() + ":" + df.getLoginAccount());
                 }
                 fsStoreOrderService.createOmsOrder(orderId);
+                fsStoreOrderLogsService.create(orderId, FsStoreOrderLogEnum.PUSH_ORDER_ERP.getValue(),
+                        nickName + " " +FsStoreOrderLogEnum.PUSH_ORDER_ERP.getDesc() + ":" + df.getLoginAccount());
             } catch (ParseException e) {
                 throw new RuntimeException(e);
             }
@@ -645,6 +679,7 @@ public class FsStoreOrderController extends BaseController
     @PostMapping(value = "/batchSetErpOrder")
     public R batchSetErpOrder(@RequestBody FsStoreOrderSetErpPhoneParam param)
     {
+        String nickName = getLoginUser().getUser().getNickName();
         String loginAccount = param.getLoginAccount();
         if (StringUtils.isBlank(loginAccount)){
             return R.error("未选择erp账户");
@@ -673,6 +708,8 @@ public class FsStoreOrderController extends BaseController
             } else {
                 fsStoreOrderDfService.insertFsStoreOrderDf(df);
             }
+            fsStoreOrderLogsService.create(orderId, FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getValue(),
+                    nickName + " " +FsStoreOrderLogEnum.SET_PUSH_ACCOUNT.getDesc() + ":" + df.getLoginAccount());
         });
         return R.ok();
     }
@@ -855,6 +892,7 @@ public class FsStoreOrderController extends BaseController
     @PostMapping("/editErpPhone")
     public AjaxResult editErpPhone(@RequestBody FsStoreOrderSetErpPhoneParam param)
     {
+        param.setOpeName(getLoginUser().getUser().getNickName());
         List<String> erpPhone = param.getErpPhone();
         if (erpPhone == null || erpPhone.isEmpty()) {
             return AjaxResult.error("请选择手机号");

+ 98 - 0
fs-admin/src/main/java/com/fs/his/controller/FsUserComplaintController.java

@@ -0,0 +1,98 @@
+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.FsUserComplaint;
+import com.fs.his.service.IFsUserComplaintService;
+import com.fs.his.vo.FsUserComplaintVo;
+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 2025-06-09
+ */
+@RestController
+@RequestMapping("/his/complaint")
+public class FsUserComplaintController extends BaseController
+{
+    @Autowired
+    private IFsUserComplaintService fsUserComplaintService;
+
+    /**
+     * 查询用户投诉列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserComplaint fsUserComplaint)
+    {
+        startPage();
+        List<FsUserComplaintVo> list = fsUserComplaintService.selectFsUserComplaintList(fsUserComplaint);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出用户投诉列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:export')")
+    @Log(title = "用户投诉", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserComplaint fsUserComplaint)
+    {
+        List<FsUserComplaintVo> list = fsUserComplaintService.selectFsUserComplaintList(fsUserComplaint);
+        ExcelUtil<FsUserComplaintVo> util = new ExcelUtil<FsUserComplaintVo>(FsUserComplaintVo.class);
+        return util.exportExcel(list, "用户投诉数据");
+    }
+
+    /**
+     * 获取用户投诉详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserComplaintService.selectFsUserComplaintById(id));
+    }
+
+    /**
+     * 新增用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:add')")
+    @Log(title = "用户投诉", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserComplaint fsUserComplaint)
+    {
+        return toAjax(fsUserComplaintService.insertFsUserComplaint(fsUserComplaint));
+    }
+
+    /**
+     * 修改用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:edit')")
+    @Log(title = "用户投诉", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserComplaint fsUserComplaint)
+    {
+        return toAjax(fsUserComplaintService.updateFsUserComplaint(fsUserComplaint));
+    }
+
+    /**
+     * 删除用户投诉
+     */
+    @PreAuthorize("@ss.hasPermi('his:complaint:remove')")
+    @Log(title = "用户投诉", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserComplaintService.deleteFsUserComplaintByIds(ids));
+    }
+}

+ 29 - 1
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -93,7 +93,7 @@ public class FsUserController extends BaseController
         if (fsUser.getPhoneMk()!=null&&fsUser.getPhoneMk()!=""){
             fsUser.setPhone(encryptPhone(fsUser.getPhoneMk()));
         }
-        if(StringUtils.isNotEmpty(fsUser.getPhone())){
+        if(StringUtils.isNotEmpty(fsUser.getPhone())&&fsUser.getPhone().length()>11){
             fsUser.setPhone(encryptPhone(fsUser.getPhone()));
         }
         List<FsUserVO> list = fsUserService.selectFsUserListVO(fsUser);
@@ -162,6 +162,34 @@ public class FsUserController extends BaseController
         return getDataTable(list);
     }
 
+    @PreAuthorize("@ss.hasPermi('his:user:export')")
+    @GetMapping("/exportListProject")
+    public AjaxResult exportListProject(FsUser fsUser)
+    {
+        if(StringUtils.isNotEmpty(fsUser.getPhone())){
+            fsUser.setPhone(encryptPhone(fsUser.getPhone()));
+        }
+        List<FsUserVO> list = fsUserService.selectFsUserVOListByProject(fsUser);
+        boolean checkPhone = isCheckPhone();
+        for (FsUserVO fsUserVO : list) {
+            if(fsUserVO.getPhone() != null&&fsUserVO.getPhone()!=""){
+                if (!checkPhone){
+                    if (fsUserVO.getPhone().length()>11){
+                        fsUserVO.setPhone(decryptPhoneMk(fsUserVO.getPhone()));
+                    }else {
+                        fsUserVO.setPhone(fsUserVO.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    }
+                } else {
+                    if (fsUserVO.getPhone().length()>11) {
+                        fsUserVO.setPhone(decryptPhone(fsUserVO.getPhone()));
+                    }
+                }
+            }
+        }
+        ExcelUtil<FsUserVO> util = new ExcelUtil<FsUserVO>(FsUserVO.class);
+        return util.exportExcel(list, "项目会员数据");
+    }
+
     /**
      * 导出用户列表
      */

+ 2 - 0
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -565,10 +565,12 @@ public class Task {
     public void CreateOmsAndHis()
     {
         List<Long> omsList = fsStoreOrderMapper.selectFsStoreOrderNoCreateOms();
+        logger.info("推送订单id====>{}",omsList);
         for (Long l : omsList) {
             try {
                 fsStoreOrderService.createOmsOrder(l);
             } catch (Exception e) {
+                logger.error("推送订单异常:",e);
             }
         }
 //        List<Long> tuiOrderList = fsStoreOrderMapper.selectFsStoreOrderNoTuiOrder();

+ 10 - 67
fs-company/src/main/java/com/fs/company/controller/common/Test.java

@@ -3,17 +3,26 @@ package com.fs.company.controller.common;
 import com.alibaba.fastjson.JSON;
 import com.fs.ad.enums.AdUploadType;
 import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
 import com.fs.common.enums.DataSourceType;
+import com.fs.his.utils.qrcode.QRCodeUtils;
 import com.fs.qw.vo.AdUploadVo;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempDayService;
 import com.fs.sop.service.IQwSopTempRulesService;
 import com.fs.sop.service.IQwSopTempService;
+import com.google.zxing.WriterException;
 import lombok.AllArgsConstructor;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 @RestController
 @AllArgsConstructor
 public class Test {
@@ -23,76 +32,10 @@ public class Test {
     private final IQwSopTempDayService qwSopTempDayService;
     private final IQwSopTempContentService qwSopTempContentService;
     private final RocketMQTemplate rocketMQTemplate;
-
-//    @GetMapping("test")
-//    public void fileDownload(){
-////        List<QwSopTempRules> rulesList = qwSopTempRulesService.listByTempId("3922c166-a539-4a64-a535-d4feafc096c3");
-//        List<QwSopTemp> tempList = qwSopTempService.listTemp().stream().filter(e -> StringUtils.isNotEmpty(e.getSetting())).collect(Collectors.toList());
-//        Map<String, QwSopTemp> tempMap = PubFun.listToMapByGroupObject(tempList, QwSopTemp::getId);
-//        Map<String, List<QwSopTempSetting2>> collect = tempList.stream().collect(Collectors.toMap(QwSopTemp::getId, e -> JSONArray.parseArray(e.getSetting(), QwSopTempSetting2.class)));
-//        collect.forEach((k, rules) -> {
-//            QwSopTemp qwSopTemp = tempMap.get(k);
-//            for (int i = 0; i < rules.size(); i++) {
-//                QwSopTempSetting2 e = rules.get(i);
-//                QwSopTempDay qwSopTempDay = new QwSopTempDay();
-//                qwSopTempDay.setSorts(i);
-//                qwSopTempDay.setName(e.getName());
-//                qwSopTempDay.setDayNum((qwSopTempDay.getSorts() * qwSopTemp.getGap()) + 1);
-//                qwSopTempDay.setTempId(k);
-//                qwSopTemp.getList().add(qwSopTempDay);
-//                for (int i1 = 0; i1 < e.getContent().size(); i1++) {
-//                    QwSopTempRules qwSopTempRules = new QwSopTempRules();
-//                    qwSopTempRules.setTempId(k);
-//                    qwSopTempRules.setName(e.getName());
-//                    qwSopTempRules.setSorts(i1);
-//                    QwSopTempSetting2.Content vo = e.getContent().get(i1);
-//                    qwSopTempRules.setTime(vo.getTime());
-//                    qwSopTempRules.setType(vo.getType());
-//                    qwSopTempRules.setContentType(StringUtils.isNotEmpty(vo.getContentType()) ? Integer.parseInt(vo.getContentType()) : null);
-//                    qwSopTempRules.setCourseType(vo.getCourseType());
-//                    qwSopTempRules.setCourseId(vo.getCourseId());
-//                    qwSopTempRules.setVideoId(vo.getVideoId());
-//                    qwSopTempRules.setAiTouch(vo.getAiTouch());
-//                    qwSopTempDay.getList().add(qwSopTempRules);
-//                    for (int i2 = 0; i2 < vo.getSetting().size(); i2++) {
-//                        QwSopTempSetting2.Content.Setting setting = vo.getSetting().get(i2);
-//                        QwSopTempContent qwSopTempContent = new QwSopTempContent();
-//                        qwSopTempContent.setTempId(k);
-//                        qwSopTempContent.setContentType(StringUtils.isNotEmpty(setting.getContentType()) ? Integer.parseInt(setting.getContentType()) : null);
-//                        qwSopTempContent.setContent(JSON.toJSONString(setting));
-//                        qwSopTempContent.setIsBindUrl(setting.getIsBindUrl());
-//                        qwSopTempContent.setExpiresDays(setting.getExpiresDays());
-//                        qwSopTempRules.getList().add(qwSopTempContent);
-//                    }
-//                }
-//            }
-//        });
-////        System.out.println(1);
-//        tempList.parallelStream().forEach(qwSopTemp -> {
-////            new Thread(() -> {
-//                qwSopTempDayService.saveList(qwSopTemp.getList());
-//                List<QwSopTempRules> ruleList = qwSopTemp.getList().stream().flatMap(e -> e.getList().stream().peek(s -> s.setDayId(e.getId()))).collect(Collectors.toList());
-//                qwSopTempRulesService.saveList(ruleList);
-//                List<QwSopTempContent> contentList = ruleList.stream().flatMap(s -> s.getList().stream().peek(c -> c.setRulesId(s.getId()))).collect(Collectors.toList());
-//                qwSopTempContentService.saveList(contentList);
-////            }).start();
-//        });
-//    }
-//    @GetMapping("test")
-//    @DataSource(DataSourceType.SOP)
-//    public void fileDownload(){
-//        List<QwSopTempContent> contentList = qwSopTempContentService.list(new QueryWrapper<QwSopTempContent>().isNull("day_id"));
-//        List<Long> longs = PubFun.listToNewList(contentList, QwSopTempContent::getRulesId);
-//        List<QwSopTempRules> ruleList = qwSopTempRulesService.list(new QueryWrapper<QwSopTempRules>().in("id", longs));
-//        Map<Long, QwSopTempRules> rulesMap = PubFun.listToMapByGroupObject(ruleList, QwSopTempRules::getId);
-//        contentList.stream().filter(e ->rulesMap.containsKey(e.getRulesId())).forEach(qwSopTempContent -> {
-//            qwSopTempContent.setDayId(rulesMap.get(qwSopTempContent.getRulesId()).getDayId());
-//        });
-//        contentList.forEach(qwSopTempContentService::updateDay);
-//    }
     @GetMapping("test")
     @DataSource(DataSourceType.SOP)
     public void fileDownload(){
         rocketMQTemplate.syncSend("ad-upload", JSON.toJSONString(AdUploadVo.builder().state("测试").type(AdUploadType.ADD_WX).build()));
     }
+
 }

+ 6 - 1
fs-company/src/main/java/com/fs/company/controller/crm/CrmMsgController.java

@@ -64,7 +64,12 @@ public class CrmMsgController extends BaseController
     }
 
 
-
+    /**
+     * 获取站内销售消息列表
+     * @param request
+     * @param type
+     * @return
+     */
     @GetMapping("/getMsgList")
     public R getMsgList(
             HttpServletRequest request,

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

@@ -659,12 +659,11 @@ public class QwUserController extends BaseController
                     companyUserMapper.updateCompanyUser(user);
 
 
-                    QwUser qw = new QwUser();
-                    qw.setCompanyUserId(qwUserParam.getCompanyUserId());
-                    qw.setId(Long.parseLong(paramId));
-                    qw.setStatus(1);
-                    qw.setCompanyId(companyUser.getCompanyId());
-                    qwUserService.updateQwUser(qw);
+
+                    qu.setCompanyUserId(qwUserParam.getCompanyUserId());
+                    qu.setStatus(1);
+                    qu.setCompanyId(companyUser.getCompanyId());
+                    qwUserService.updateQwUser(qu);
 
                 }
 

+ 6 - 5
fs-company/src/main/java/com/fs/company/controller/store/FsInquiryOrderController.java

@@ -364,16 +364,17 @@ public class FsInquiryOrderController extends BaseController
 
 
     /**
-    * 生成订单二维码
+    * 生成问诊付款二维码
     */
     @PreAuthorize("@ss.hasPermi('store:inquiryOrder:wxaCodeInquiryOrder')")
     @GetMapping("/getWxaCodeInquiryOrderUnLimit/{orderId}")
-    public AjaxResult getWxaCodeInquiryOrderUnLimit(@PathVariable("orderId") Long orderId)
+    public R getWxaCodeInquiryOrderUnLimit(@PathVariable("orderId") Long orderId)
     {
 
-        byte[] bytes = fsInquiryOrderService.getWxaCodeInquiryOrderUnLimit(orderId);
-        String base64 = Base64.getEncoder().encodeToString(bytes);
-        return AjaxResult.success("成功",base64);
+        //        byte[] bytes = fsInquiryOrderService.getWxaCodeInquiryOrderUnLimit(orderId);
+//        String base64 = Base64.getEncoder().encodeToString(bytes);
+        return fsInquiryOrderService.getWxaCodeInquiryOrderUnLimitR(orderId);
 
     }
+
 }

+ 14 - 1
fs-company/src/main/java/com/fs/company/controller/store/FsPackageOrderController.java

@@ -235,7 +235,8 @@ public class FsPackageOrderController extends BaseController
     @GetMapping("storeRefund/{orderId}")
     public AjaxResult storeRefund(@PathVariable("orderId") Long orderId)
     {
-        return AjaxResult.success(fsPackageOrderService.PackageStoreOrderRefund(orderId));
+        String nickName = getLoginUser().getUser().getNickName();
+        return AjaxResult.success(fsPackageOrderService.PackageStoreOrderRefund(orderId,nickName));
     }
 
 
@@ -247,4 +248,16 @@ public class FsPackageOrderController extends BaseController
         String base64 = Base64.getEncoder().encodeToString(bytes);
         return AjaxResult.success("成功",base64);
     }
+
+    /**
+     * 生成套餐包付款二维码
+     */
+    @PreAuthorize("@ss.hasPermi('store:packageOrder:wxaCodePackageOrder')")
+    @GetMapping("/getWxaCodePackageOrderUnLimit/{orderId}")
+    public R getWxaCodePackageOrderUnLimit(@PathVariable("orderId") Long orderId)
+    {
+
+        return fsPackageOrderService.getWxaCodePackageOrderUnLimit(orderId);
+
+    }
 }

+ 1 - 1
fs-company/src/main/java/com/fs/company/controller/store/FsStoreOrderController.java

@@ -390,7 +390,7 @@ public class FsStoreOrderController extends BaseController
         }
         LoginUser loginUser = SecurityUtils.getLoginUser();
         fsStoreOrder.setCompanyId(loginUser.getCompany().getCompanyId());
-        fsStoreOrder.setOperator(loginUser.getUser().getNickName());
+        fsStoreOrder.setOperator("销售端:" + loginUser.getUser().getNickName());
         return toAjax(fsStoreOrderService.afterSales(fsStoreOrder));
     }
 

+ 12 - 2
fs-framework/src/main/java/com/fs/framework/web/exception/GlobalExceptionHandler.java

@@ -1,6 +1,8 @@
 package com.fs.framework.web.exception;
 
 import javax.servlet.http.HttpServletRequest;
+
+import com.fs.common.exception.CustomException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.security.access.AccessDeniedException;
@@ -17,7 +19,7 @@ import com.fs.common.utils.StringUtils;
 
 /**
  * 全局异常处理器
- * 
+ *
 
  */
 @RestControllerAdvice
@@ -59,6 +61,15 @@ public class GlobalExceptionHandler
         return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
     }
 
+
+    @ExceptionHandler(CustomException.class)
+    public AjaxResult handleCustomException(CustomException e, HttpServletRequest request)
+    {
+        log.error(e.getMessage(), e);
+        Integer code = e.getCode();
+        return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
+    }
+
     /**
      * 拦截未知的运行时异常
      */
@@ -69,7 +80,6 @@ public class GlobalExceptionHandler
         log.error("请求地址'{}',发生未知异常.", requestURI, e);
         return AjaxResult.error(e.getMessage());
     }
-
     /**
      * 系统异常
      */

+ 5 - 2
fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java

@@ -330,6 +330,9 @@ public class QwMsgController {
                         WxwSpeechToTextEntityRespDTO data = dto.getData();
                         content = data.getText();
                         System.out.println("语音消息"+content);
+                        if(content == null || content.isEmpty()){
+                            content = "==语音转换失败==";
+                        }
                     }
                     else if (wxWorkMessageDTO.getMsgtype() == 101){
                         content = processImageMessage(serverId, wxWorkMessageDTO, wxWorkMsgResp);
@@ -357,7 +360,7 @@ public class QwMsgController {
                     }
                     Long receiver = wxWorkMessageDTO.getReceiver();
                     Long extId=null;
-                    Integer totalSeconds=0;
+                    Long totalSeconds=null;
                     if (2000000000000000L-receiver>0){
                          extId = wxWorkMessageDTO.getSender();
                         System.out.println("客户发起");
@@ -388,7 +391,7 @@ public class QwMsgController {
                                     minutes = Integer.parseInt(matcher.group(1));
                                     seconds = Integer.parseInt(matcher.group(2));
                                 }
-                                totalSeconds = hours * 3600 + minutes * 60 + seconds;
+                                totalSeconds = hours * 3600L + minutes * 60L + seconds;
                                 System.out.println("总通话秒数: " + totalSeconds);
                             }
                         }

+ 141 - 0
fs-qw-mq/pom.xml

@@ -0,0 +1,141 @@
+<?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-qw-mq</artifactId>
+    <description>
+       企业微信mq
+    </description>
+    <dependencies>
+
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 验证码 -->
+        <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-qw-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(FsQwMqApiApplication.class);
+    }
+}

+ 24 - 0
fs-qw-mq/src/main/java/com/fs/FsQwMqApiApplication.java

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

+ 58 - 0
fs-qw-mq/src/main/java/com/fs/app/controller/CommonController.java

@@ -0,0 +1,58 @@
+package com.fs.app.controller;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.ad.service.IAdHtmlClickLogService;
+import com.fs.common.core.domain.R;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.voice.utils.StringUtil;
+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.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+@Slf4j
+@Api("公共接口")
+@RestController
+@AllArgsConstructor
+@Ignore
+@RequestMapping(value="/app/common")
+public class CommonController {
+    private IAdHtmlClickLogService adHtmlClickLogService;
+    private RocketMQTemplate rocketMQTemplate;
+
+
+    @PostMapping("/testSend")
+    public R testSend(@RequestBody FsCourseWatchLog watchLog){
+
+
+        try {
+            rocketMQTemplate.syncSend("course-finish-notes", JSON.toJSONString(watchLog));
+        }catch (Exception e){
+            log.error("添加完课打备注失败",e);
+        }
+
+        return R.ok();
+    }
+
+}

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

+ 34 - 0
fs-qw-mq/src/main/java/com/fs/app/mq/RocketMQConsumerCourseFinishService.java

@@ -0,0 +1,34 @@
+package com.fs.app.mq;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.service.IFsCourseFinishTempService;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+@RocketMQMessageListener(topic = "course-finish-notes", consumerGroup = "course-finish-group")
+public class RocketMQConsumerCourseFinishService implements RocketMQListener<String> {
+
+    private final IFsCourseFinishTempService courseFinishTempService;
+
+    @Override
+    public void onMessage(String message) {
+
+
+        log.info("收到消息1:" + message);
+
+        FsCourseWatchLog watchLog = JSON.parseObject(message, FsCourseWatchLog.class);
+        if (watchLog == null || watchLog.getQwExternalContactId() == null) {
+            return;
+        }
+        courseFinishTempService.finishCourseExtContactIdByRemark(watchLog);
+
+    }
+}

+ 188 - 0
fs-qw-mq/src/main/java/com/fs/app/mq/courseFinishRtyTaskOne.java

@@ -0,0 +1,188 @@
+package com.fs.app.mq;
+
+
+import com.fs.common.core.redis.RedisCache;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.service.IQwCourseFinishRemarkRtyService;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.*;
+
+@Component
+@Slf4j
+public class courseFinishRtyTaskOne {
+
+    @Autowired
+    private IQwCourseFinishRemarkRtyService finishRemarkRtyService;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private QwApiService qwApiService;
+
+    /**
+     * 完课打备注失败的 重复打 一直到失败 (这个 是销售1的 启动项)
+     */
+
+    @Scheduled(cron = "0 0/15 * * * ?") // 每15分钟
+//    @Scheduled(cron = "0/1 * * * * ? ") // 每15分钟
+    public void selectSopUserLogsListByTimeOne() {
+
+        System.out.println("schedule.task1-enabled111111111");
+
+        // 获取当前时间
+        LocalDateTime now = LocalDateTime.now();
+        // 计算10分钟前
+        LocalDateTime tenMinutesBefore = now.minusMinutes(10);
+
+        // 创建线程池
+        int threadCount = Math.min(8, Runtime.getRuntime().availableProcessors() * 2);
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+        try {
+            // 使用线程安全的List来收集需要批量更新的数据
+            List<Long> batchUpdateList = Collections.synchronizedList(new ArrayList<>());
+
+            // 存储Future对象以便检查所有任务完成情况
+            List<Future<?>> futures = new ArrayList<>();
+
+            // 1. 批量查询所有用户数据
+            List<QwCourseFinishRemarkRty> remarkRtyList=new ArrayList<>();
+            try {
+                remarkRtyList = finishRemarkRtyService.selectQwCourseFinishRemarkRtyListByTime(tenMinutesBefore,1);
+                if (remarkRtyList == null || remarkRtyList.isEmpty()) {
+                     return;
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+                log.info("批量查询用户数据失败:" + e.getMessage());
+            }
+
+            // 直接遍历contacts而不是userIds
+            for (QwCourseFinishRemarkRty remarkRty : remarkRtyList) {
+                futures.add(executor.submit(() -> {
+                    try {
+
+                        QwExternalContactRemarkParam remarkParam = new QwExternalContactRemarkParam();
+                        remarkParam.setRemark(remarkRty.getRemark().trim());
+                        remarkParam.setUserid(remarkRty.getQwUserId().trim());
+                        remarkParam.setExternal_userid(remarkRty.getExternalUserId().trim());
+
+                        QwExternalContactRemarkResult qwResult = qwApiService.externalcontactRemark(remarkParam, remarkRty.getCorpId());
+                        if (qwResult.getErrcode() == 0) {
+                            // 添加到批量更新列表
+                            batchUpdateList.add(remarkRty.getId());
+                        }else {
+                            log.error("客户添加备注失败task,userId: {}{}{}", remarkRty.getQwUserId(), remarkRty.getExternalUserId(), qwResult.getErrmsg());
+                        }
+                    } catch (Exception e) {
+                        log.error("客户添加备注失败task,userId: " + remarkRty.getQwUserId() +
+                                ", externalUserId: " + remarkRty.getExternalUserId() +
+                                ", 错误信息: " + e.getMessage());
+                    }
+                }));
+            }
+
+
+            // 等待所有任务完成
+            for (Future<?> future : futures) {
+                try {
+                    future.get();
+                } catch (InterruptedException | ExecutionException e) {
+                    log.error("任务执行异常: " + e.getMessage());
+                    Thread.currentThread().interrupt();
+                }
+            }
+
+            // 批量更新数据库
+            if (!batchUpdateList.isEmpty()) {
+                try {
+                    // 分批处理,避免单次批量过大
+                    int batchSize = 500; // 根据数据库性能调整
+                    for (int i = 0; i < batchUpdateList.size(); i += batchSize) {
+                        int end = Math.min(i + batchSize, batchUpdateList.size());
+                        List<Long> subList = batchUpdateList.subList(i, end);
+
+                        finishRemarkRtyService.deleteQwCourseFinishRemarkRtyByIds(subList.toArray(new Long[0]));
+                    }
+                } catch (Exception e) {
+                    log.error("批量更新失败: " + e.getMessage());
+                }
+            }
+
+            // 关闭线程池
+            executor.shutdown();
+
+        } catch (Exception e) {
+            log.error("打备注任务执行异常: " + e.getMessage());
+        } finally {
+            // 7. 确保线程池关闭
+            try {
+                // 先尝试正常关闭
+                executor.shutdown();
+                if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
+                    // 超时后强制关闭
+                    executor.shutdownNow();
+                    log.warn("线程池强制关闭");
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                executor.shutdownNow();
+            }
+        }
+        try {
+            Thread.sleep(3000);
+        } catch (InterruptedException ex) {
+            log.info("线程被中断");
+        }
+
+
+
+    }
+
+
+
+
+    /**
+     * 批量删除 掉 前一天的没打上标签的
+     */
+    @Scheduled(cron = "0 30 0 * * ?")
+    public void deleteFinishCourseRemark() {
+        long startTimeMillis = System.currentTimeMillis();
+        log.info("====== 批量删除 掉 前一天的没打上标签的 ======");
+
+        List<Long> batchUpdateList = finishRemarkRtyService.selectQwCourseFinishRemarkRtyListByOutTime();
+
+        // 批量更新数据库
+        if (!batchUpdateList.isEmpty()) {
+            try {
+                // 分批处理,避免单次批量过大
+                int batchSize = 500; // 根据数据库性能调整
+                for (int i = 0; i < batchUpdateList.size(); i += batchSize) {
+                    int end = Math.min(i + batchSize, batchUpdateList.size());
+                    List<Long> subList = batchUpdateList.subList(i, end);
+
+                    finishRemarkRtyService.deleteQwCourseFinishRemarkRtyByIds(subList.toArray(new Long[0]));
+                }
+            } catch (Exception e) {
+                log.error("批量更新失败: " + e.getMessage());
+            }
+        }
+
+        long endTimeMillis = System.currentTimeMillis();
+        log.info("====== 更批量删除 掉 前一天的没打上标签的,耗时 {} 毫秒 ======", (endTimeMillis - startTimeMillis));
+    }
+
+}
+

+ 182 - 0
fs-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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;
+    }
+}

+ 100 - 0
fs-qw-mq/src/main/java/com/fs/framework/config/DataSourceConfig.java

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

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

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

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

+ 65 - 0
fs-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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-qw-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);
+    }
+}

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

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

+ 103 - 0
fs-qw-mq/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

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

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

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

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

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

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

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

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

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

+ 15 - 0
fs-qw-mq/src/main/resources/mybatis/mybatis-config.xml

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

+ 103 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/AsyncCourseWatchFinishService.java

@@ -0,0 +1,103 @@
+package com.fs.app.taskService.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.service.IQwCompanyService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.client.producer.SendCallback;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Slf4j
+@Service
+@AllArgsConstructor
+public class AsyncCourseWatchFinishService {
+
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+
+    @Autowired
+    private IQwCompanyService iQwCompanyService;
+
+    @Autowired
+    private QwExternalContactServiceImpl qwExternalContactService;
+
+    @Autowired
+    RedisCache redisCache;
+
+    /**
+    * 异步处理完课打备注的
+    */
+    @Async("scheduledExecutorService")
+    public void executeCourseWatchFinish(FsCourseWatchLog finishLog) {
+
+        FsCourseWatchLog watchLog = new FsCourseWatchLog();
+        watchLog.setQwExternalContactId(finishLog.getQwExternalContactId());
+        watchLog.setFinishTime(finishLog.getFinishTime());
+        watchLog.setQwUserId(finishLog.getQwUserId());
+
+
+        QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(finishLog.getQwUserId()));
+        if (qwUserByRedis == null) {
+            log.error("无企微员工信息 {} 跳过处理。", finishLog.getQwUserId());
+            return;
+        }
+
+        QwCompany qwCompany = iQwCompanyService.getQwCompanyByRedis(qwUserByRedis.getCorpId());
+
+        if (qwCompany == null) {
+            log.error("企业微信主体为空 {} 跳过处理。", qwUserByRedis.getCorpId());
+            return;
+        }
+
+        rocketMQTemplate.asyncSend("course-finish-notes", JSON.toJSONString(finishLog),     new SendCallback() {
+            @Override public void onSuccess(SendResult sendResult) {}  // 空实现
+            @Override public void onException(Throwable e) {log.error("推送完课打备注失败1:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
+        });
+
+
+        // 定义默认值
+         final Integer DEFAULT_SERVER_NUM = 99;
+
+        // 使用
+        Integer companyServerNum = Optional.ofNullable(qwCompany.getCompanyServerNum())
+                .orElse(DEFAULT_SERVER_NUM);
+        switch (companyServerNum){
+            case 1:
+                rocketMQTemplate.asyncSend("course-finish-notes", JSON.toJSONString(finishLog),     new SendCallback() {
+                    @Override public void onSuccess(SendResult sendResult) {}  // 空实现
+                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败1:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
+                });
+                break;
+            case 2:
+
+                rocketMQTemplate.asyncSend("course-finish-notesTwo", JSON.toJSONString(finishLog),     new SendCallback() {
+                    @Override public void onSuccess(SendResult sendResult) {}  // 空实现
+                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败2:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
+                });
+                break;
+            case 3:
+                rocketMQTemplate.asyncSend("course-finish-notesThree", JSON.toJSONString(finishLog),     new SendCallback() {
+                    @Override public void onSuccess(SendResult sendResult) {}  // 空实现
+                    @Override public void onException(Throwable e) {log.error("推送完课打备注失败3:{},{}",JSON.toJSONString(finishLog),e.getMessage());}          // 空实现
+                });
+                break;
+            default:
+                break;
+        }
+
+
+    }
+
+}

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

@@ -173,6 +173,10 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     @Autowired
     private CompanyMapper companyMapper;
 
+    @Autowired
+    private AsyncCourseWatchFinishService asyncCourseWatchFinishService;
+
+
     @PostConstruct
     public void init() {
         loadCourseConfig();
@@ -1814,6 +1818,15 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         log.info("开始执行处理批次方法-数量:{}",batch.size());
         for (FsCourseWatchLog finishLog : batch) {
             try {
+
+                try {
+
+                    asyncCourseWatchFinishService.executeCourseWatchFinish(finishLog);
+
+                }catch (Exception e){
+                    log.error("添加完课打备注失败",e);
+                }
+
                 // 查询外部联系人信息
                 QwExternalContact externalContact = qwExternalContactMapper.selectQwExternalContactById(finishLog.getQwExternalContactId());
                 if (externalContact == null) {

+ 1 - 1
fs-service/src/main/java/com/fs/aiTongueApi/config/AiTongueConfig.java

@@ -4,7 +4,7 @@ public interface AiTongueConfig {
     String getFaceHistoryByIDUrl="https://api.aikanshe.com/agency/getHistoryByID";
     String quanxiUrl="https://api.aikanshe.com/agency/quanxi";
     String checkTongue="https://api.aikanshe.com/agency/checkTongue";
-    String appKey="";
+    String appKey="i5h5u6g59dw9x0o6yymd3tf5ea6gcdqi";
 
     String newCheckTongue="http://132.232.234.246:5056/api/detect";
 }

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

@@ -290,4 +290,11 @@ public interface FsUserCourseMapper
             " from fs_user_course_video where course_id = #{courseId} and is_del = 0 order by course_sort DESC,video_id")
     List<FsUserCourseVideoAppletVO.FsUserCourseVideo> selectFsUserCourseVideoAppletByCourseId(@Param("courseId") Long courseId);
 
+    @Select("SELECT course_id dict_value, course_name dict_label, img_url dict_imgUrl  \n" +
+            "         FROM fs_user_course \n" +
+            "         WHERE is_del = 0 \n" +
+            "         AND is_private = 1 \n" +
+            "         AND user_id =#{userId}" +
+            "         ORDER BY create_time DESC")
+    List<OptionsVO> selectFsUserCourseAllListByUserId(@Param("userId") Long userId);
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseFinishTempService.java

@@ -1,6 +1,7 @@
 package com.fs.course.service;
 
 import com.fs.course.domain.FsCourseFinishTemp;
+import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.vo.FsCourseFinishTempListVO;
 import com.fs.course.vo.FsCourseFinishTempVO;
 
@@ -71,4 +72,9 @@ public interface IFsCourseFinishTempService
      * 将所有的CompanyUserId更新为 企业微信账号
      */
 //    public void updateFsCourseFinishTempByCompanyUserId();
+
+    /**
+     * 完课用户打备注
+     */
+    public void finishCourseExtContactIdByRemark(FsCourseWatchLog watchLog);
 }

+ 212 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java

@@ -5,21 +5,38 @@ import com.alibaba.fastjson.JSONArray;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.course.domain.FsCourseFinishTemp;
+import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.mapper.FsCourseFinishTempMapper;
 import com.fs.course.service.IFsCourseFinishTempService;
 import com.fs.course.vo.FsCourseFinishTempListVO;
 import com.fs.course.vo.FsCourseFinishTempVO;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwCourseFinishRemarkRtyService;
+import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwUserVO;
+import com.fs.qwApi.domain.QwExternalContactRemarkResult;
+import com.fs.qwApi.param.QwExternalContactRemarkParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.voice.utils.StringUtil;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Arrays;
-import java.util.List;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
 import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * 完课模板Service业务层处理
@@ -27,6 +44,7 @@ import java.util.function.Consumer;
  * @author fs
  * @date 2024-12-19
  */
+@Slf4j
 @Service
 public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
 {
@@ -42,6 +60,19 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
     @Autowired
     private CompanyUserMapper companyUserMapper;
 
+    @Autowired
+    private IQwExternalContactService iQwExternalContactService;
+
+    @Autowired
+    private QwApiService qwApiService;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+
+    @Autowired
+    private IQwCourseFinishRemarkRtyService finishRemarkRtyService;
+
     /**
      * 查询完课模板
      *
@@ -175,6 +206,185 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
         fsCourseFinishTempMapper.deleteByParentIds(ids);
     }
 
+    /**
+     * 完课用户 打备注
+     */
+    @Override
+    public void finishCourseExtContactIdByRemark(FsCourseWatchLog watchLog) {
+
+        Long qwExternalContactId = watchLog.getQwExternalContactId();
+
+        Date finishTime = watchLog.getFinishTime();
+        LocalDate localDate = finishTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMdd");
+        String monthDay = localDate.format(formatter);
+
+        DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("dd");
+        String dayOfMonth = localDate.format(dayFormatter);
+
+        QwUser qwUserByRedisForId = iQwExternalContactService.getQwUserByRedisForId(String.valueOf(watchLog.getQwUserId()));
+        if (qwUserByRedisForId == null || qwUserByRedisForId.getIsSendMsg() == null) {
+            log.error("无企微员工信息 {} 跳过处理。", watchLog.getQwUserId());
+            return;
+        }
+
+        //不用完课备注
+        if (qwUserByRedisForId.getIsSendMsg() == 5) {
+            log.error("员工不用完课备注 {} 跳过处理。", watchLog.getQwUserId());
+            return;
+        }
+
+        int isSendMsg = qwUserByRedisForId.getIsSendMsg();
+
+        QwExternalContact externalContact = iQwExternalContactService.selectQwExternalContactByRemark(qwExternalContactId);
+
+        if (externalContact != null) {
+
+            String oldRemark = externalContact.getRemark();
+            String newRemark = "";
+            String newNotes = "*" + monthDay + "完";
+            String newNotesDay = "*" + dayOfMonth + "完";
+
+            if (StringUtil.strIsNullOrEmpty(oldRemark)) {
+                oldRemark = externalContact.getName();
+            }
+
+            // 2. 提取所有旧标记(无论类型)
+            List<String> allOldMarks = new ArrayList<>();
+            Pattern markPattern = Pattern.compile("\\*(\\d{1,4})完");
+            Matcher markMatcher = markPattern.matcher(oldRemark);
+            while (markMatcher.find()) {
+                allOldMarks.add(markMatcher.group());
+            }
+
+            // 3. 检查是否需要更新
+            boolean shouldUpdate = true;
+            int currentYear = Calendar.getInstance().get(Calendar.YEAR);
+
+            // Convert input dates to full YYYYMMDD format
+            String fullMonthDay = String.valueOf(currentYear) + monthDay; // becomes YYYYMMDD
+            String fullDayOfMonth = String.valueOf(currentYear) +
+                    String.format("%02d", Calendar.getInstance().get(Calendar.MONTH) + 1) +
+                    String.format("%02d", Integer.parseInt(dayOfMonth));
+
+            for (String mark : allOldMarks) {
+                Matcher numMatcher = Pattern.compile("\\*(\\d{1,4})完").matcher(mark);
+                if (numMatcher.find()) {
+                    String numStr = numMatcher.group(1);
+                    String fullOldDate;
+
+                    if (numStr.length() <= 2) { // 处理 "*D完" 格式
+
+                        int day = Integer.parseInt(numStr);
+                        Calendar cal = Calendar.getInstance();
+                        int currentMonth = cal.get(Calendar.MONTH) + 1;
+                        int currentDay = cal.get(Calendar.DAY_OF_MONTH);
+                        int year = cal.get(Calendar.YEAR);
+
+                        // 获取上个月的最大天数
+                        int prevMonth = (currentMonth == 1) ? 12 : currentMonth - 1;
+                        int prevYear = (currentMonth == 1) ? year - 1 : year;
+                        cal.set(prevYear, prevMonth - 1, 1); // Calendar 月份是 0-based
+                        int maxDaysInPrevMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+
+                        // 如果 day > 上个月的天数,说明是上个月的日期
+                        if (day > maxDaysInPrevMonth || day > currentDay + 7) {
+                            currentMonth = prevMonth;
+                            year = prevYear;
+                        }
+
+                        fullOldDate = String.format("%04d%02d%02d", year, currentMonth, day);
+
+                        // 比较日期
+                        if (Integer.parseInt(fullOldDate) >= Integer.parseInt(fullMonthDay) ||
+                                Integer.parseInt(fullOldDate) >= Integer.parseInt(fullDayOfMonth)) {
+                            shouldUpdate = false;
+                            break;
+                        }
+                    }
+
+                }
+            }
+
+            if (!shouldUpdate) {
+                return; // 不更新
+            }
+
+            // 根据 isSendMsg 决定标记格式
+            String markToAdd = (isSendMsg == 3 || isSendMsg == 4) ? newNotesDay : newNotes;
+
+            // 先移除现有标记
+            String remarkWithoutMark = oldRemark.replaceAll("\\*\\d{2,4}完", "").trim();
+
+            // 添加新标记(考虑长度限制)
+            int keepLength = 20 - markToAdd.length();
+            if (keepLength < 0) keepLength = 0;
+
+            if (isSendMsg == 1 || isSendMsg == 3) {
+                // 添加到前面
+                newRemark = markToAdd + (remarkWithoutMark.length() > keepLength ?
+                        remarkWithoutMark.substring(0, keepLength) : remarkWithoutMark);
+            } else { // isSendMsg == 2 或 4
+                // 添加到后面
+                newRemark = (remarkWithoutMark.length() > keepLength ?
+                        remarkWithoutMark.substring(0, keepLength) : remarkWithoutMark) + markToAdd;
+            }
+
+            QwExternalContactRemarkParam remarkParam = new QwExternalContactRemarkParam();
+            remarkParam.setRemark(newRemark);
+            remarkParam.setUserid(externalContact.getUserId());
+            remarkParam.setExternal_userid(externalContact.getExternalUserId());
+
+            for (int attempt = 1; attempt <= 2; attempt++) {
+                try {
+                    QwExternalContactRemarkResult qwResult = qwApiService.externalcontactRemark(remarkParam, externalContact.getCorpId());
+                    if (qwResult.getErrcode() == 0) {
+
+                        QwExternalContact contactNew = new QwExternalContact();
+                        contactNew.setId(externalContact.getId());
+                        contactNew.setRemark(newRemark);
+                        qwExternalContactMapper.updateQwExternalContact(contactNew);
+
+                        log.info("完课成功添加备注:" + externalContact.getName() + "|" + externalContact.getExternalUserId() + "|" + externalContact.getCorpId() + "|" + externalContact.getUserId() + "|" + newRemark);
+
+                        break;
+                    } else {
+                        if (attempt==2 && (qwResult.getErrcode() == 45033 || qwResult.getErrcode()== -1 || qwResult.getErrcode()== 60020)) {
+                            QwCourseFinishRemarkRty remarkRty=new QwCourseFinishRemarkRty();
+                            remarkRty.setQwUserId(externalContact.getUserId());
+                            remarkRty.setCorpId(externalContact.getCorpId());
+                            remarkRty.setExternalUserId(externalContact.getExternalUserId());
+                            remarkRty.setExternalId(externalContact.getId());
+                            remarkRty.setRemark(newRemark);
+                            remarkRty.setCreateTime(new Date());
+                            finishRemarkRtyService.insertOrUpdateQwCourseFinishRemarkRty(remarkRty);
+
+                        }
+
+                        log.error("完课加备注失败:" + externalContact.getName() + "|" + externalContact.getExternalUserId() + "|" + externalContact.getCorpId() + "|" + externalContact.getUserId() + "|" + newRemark + "|原因" + qwResult.getErrmsg());
+
+                    }
+                } catch (Exception e) {
+                    log.error("添加备注异常 [尝试第 " + attempt + " 次]:" + externalContact.getName() + "|" + externalContact.getExternalUserId() + "|" + externalContact.getCorpId() + "|" + externalContact.getUserId() + "|" + newRemark + "|" + e.getMessage());
+                }
+
+                // 若不是最后一次尝试,则等待3秒再试
+                if (attempt < 2) {
+                    try {
+                        Thread.sleep(3000);
+                    } catch (InterruptedException e) {
+                        log.warn("线程等待被中断", e);
+                        break;
+                    }
+                }
+            }
+        } else {
+            log.error("完课加备注失败无客户信息:" + watchLog);
+        }
+
+
+    }
+
 //    @Override
 //    public void updateFsCourseFinishTempByCompanyUserId() {
 //        List<FsCourseFinishTemp> fsCourseFinishTemps = fsCourseFinishTempMapper.selectFsCourseFinishTempByCompanyList();

+ 1 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseLinkServiceImpl.java

@@ -389,6 +389,7 @@ public class FsCourseLinkServiceImpl implements IFsCourseLinkService
         }else {
             link.setLink(randomString);
         }
+        link.setProjectCode(cloudHostProper.getProjectCode());
 
         link.setCreateTime(sendTime);
 

+ 4 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCoursePeriodServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
 import com.fs.course.domain.FsUserCoursePeriod;
 import com.fs.course.domain.FsUserCoursePeriodDays;
@@ -114,6 +115,9 @@ public class FsUserCoursePeriodServiceImpl implements IFsUserCoursePeriodService
         }
 
         FsUserCoursePeriod fsUserCoursePeriod1 = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(fsUserCoursePeriod.getPeriodId());
+        if (!fsUserCoursePeriod1.getTrainingCampId().equals(fsUserCoursePeriod.getTrainingCampId())){
+            throw new CustomException("参数错误,请刷新后重试!");
+        }
         int flag = fsUserCoursePeriodMapper.updateFsUserCoursePeriod(fsUserCoursePeriod);
 
         // 2. 判定是否变更过开始时间(periodStartingTime)

+ 5 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -792,6 +792,11 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             BigDecimal longAsBigDecimal = BigDecimal.valueOf(video.getFileSize());
             long roundedResult = result.multiply(longAsBigDecimal).setScale(0, RoundingMode.HALF_UP).longValue();
             trafficLog.setInternetTraffic(roundedResult);
+            // 获取课程所属项目id
+            FsUserCourse fsUserCourse = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
+            if(fsUserCourse != null){
+                trafficLog.setProject(fsUserCourse.getProject());
+            }
 
             // 处理 UUID 为空的情况
             if (StringUtils.isNotEmpty(trafficLog.getUuId())) {

+ 1 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchStatisticsServiceImpl.java

@@ -141,9 +141,8 @@ public class FsUserWatchStatisticsServiceImpl extends ServiceImpl<FsUserWatchSta
         //获取公司的会员数量和今日新增会员数量
         List<FsUserWatchStatistics> userTotal = fsUserMapper.selectFsUserTotal();
         Map<String, FsUserWatchStatistics> userTotalMap;
-        if (userTotal != null && userTotal.size() > 0) {
+        if (userTotal != null && !userTotal.isEmpty()) {
             userTotalMap = userTotal.stream().collect(Collectors.toMap(FsUserWatchStatistics::getCompanyId, Function.identity()));
-
         } else {
             userTotalMap = null;
         }

+ 4 - 2
fs-service/src/main/java/com/fs/erp/dto/sdk/wangdian/api/WdtClient.java

@@ -26,7 +26,9 @@ public class WdtClient {
 	ConfigUtil configUtil;
 
 
-
+	private String getWarehouseNo() {
+		return configUtil.getErpConfig().getErpWarehouseCode();
+	}
 	private String getAppKey() {
 		return configUtil.getSysConfig().getErpWdAppKey();
 	}
@@ -134,7 +136,7 @@ public class WdtClient {
 
 		log.info("开始执行请求,相对路径:{},请求参数:{}", relativeUrl, params);
 
-
+		params.put("warehouse_no", getWarehouseNo());
 		params.put("appkey", appkey);
 		params.put("sid", sid);
 		params.put("timestamp", Long.toString(System.currentTimeMillis()/1000));

+ 1 - 1
fs-service/src/main/java/com/fs/erp/http/JstErpHttpServiceImpl.java

@@ -110,7 +110,7 @@ public class JstErpHttpServiceImpl implements JstErpHttpService {
 
     @Override
     public OrderQueryResponseDTO query(OrderQueryRequestDTO dto) {
-        String url = BASE_URL + "/open/orders/single/query";
+        String url = BASE_URL + "open/orders/single/query";
         log.info("查询订单信息 - URL: {}, 请求体: {}", url, JSON.toJSONString(dto));
 
         HttpResponse response = executeJsonPost(url, dto);

+ 9 - 7
fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -77,7 +77,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
     @Override
     public ErpOrderResponse addOrder(ErpOrder order) {
         FsStoreOrder fsStoreOrder = fsStoreOrderService.selectFsStoreOrderByOrderCode(order.getPlatform_code());
-
+        log.info("ERP订单号: {}, 订单信息: {}",order.getPlatform_code(), JSON.toJSONString(fsStoreOrder));
         ErpOrderPayment erpOrderPayment = order.getPayments().get(0);
 
         ShopOrderDTO shopOrderDTO = new ShopOrderDTO();
@@ -182,16 +182,17 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
 
 
         // 如果是货到付款
-        if("2".equals(fsStoreOrder.getPayType()) || "3".equals(fsStoreOrder.getPayType())){
+        log.info("订单支付方式: {},\"2\".equals(fsStoreOrder.getPayType()) || \"3\".equals(fsStoreOrder.getPayType()):{}",fsStoreOrder.getPayType(),fsStoreOrder.getPayType() == 2 || fsStoreOrder.getPayType() == 3);
+        if(fsStoreOrder.getPayType() == 2 || fsStoreOrder.getPayType() == 3){
             shopOrderDTO.setIsCod(true);
             // 货到付款金额 = 物流代收金额-优惠金额
             shopOrderDTO.setBuyerPaidAmount(fsStoreOrder.getPayDelivery());
-            shopOrderDTO.setPayAmount(fsStoreOrder.getPayPrice().doubleValue());
+            shopOrderDTO.setPayAmount(fsStoreOrder.getPayRemain().doubleValue());
 
             // 货到付款要推两次
             PaymentDTO paymentDTO2 = new PaymentDTO();
             // 物流代收金额
-            paymentDTO2.setAmount(fsStoreOrder.getPayDelivery().doubleValue());
+            paymentDTO2.setAmount(fsStoreOrder.getPayRemain().doubleValue());
             paymentDTO2.setOuterPayId(String.format("%s%d",order.getPlatform_code(),1));
             paymentDTO2.setPayDate(order.getDeal_datetime());
             paymentDTO2.setPayment("货到付款");
@@ -207,7 +208,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
             fsJstCodPush.setTaskStatus(TaskStatusEnum.PENDING.getCode());
             fsJstCodPushMapper.insert(fsJstCodPush);
 
-            shopOrderDTO.setPay(paymentDTO);
+            //shopOrderDTO.setPay(paymentDTO);
         }
 
         ErpOrderResponseDTO upload = jstErpHttpService.upload(shopOrderDTO);
@@ -227,6 +228,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
     @Override
     public ErpOrderResponse addOrderScrm(ErpOrder order) {
         FsStoreOrderScrm fsStoreOrder = fsStoreOrderScrmService.selectFsStoreOrderByOrderCode(order.getPlatform_code());
+        log.info("ERP订单号: {}, 订单信息: {}",order.getPlatform_code(), JSON.toJSONString(fsStoreOrder));
 
         ErpOrderPayment erpOrderPayment = order.getPayments().get(0);
 
@@ -294,6 +296,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
 //        }
 
         List<FsStoreOrderItemVO> fsStoreOrderItemVOS = fsStoreOrderItemScrmService.selectFsStoreOrderItemListByOrderId(fsStoreOrder.getId());
+        log.info("fsStoreOrderItemVOS==========>{}",fsStoreOrderItemVOS);
         for (FsStoreOrderItemVO item : fsStoreOrderItemVOS) {
             OrderItemDTO orderItemDTO = new OrderItemDTO();
             JSONObject jsonObject = JSON.parseObject(item.getJsonInfo());
@@ -328,7 +331,6 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         paymentDTO.setSellerAccount("平台销售");
         shopOrderDTO.setPay(paymentDTO);
 
-
         // 如果是货到付款
         if("2".equals(fsStoreOrder.getPayType()) || "3".equals(fsStoreOrder.getPayType())){
             shopOrderDTO.setIsCod(true);
@@ -355,7 +357,7 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
             fsJstCodPush.setTaskStatus(TaskStatusEnum.PENDING.getCode());
             fsJstCodPushMapper.insert(fsJstCodPush);
 
-            shopOrderDTO.setPay(paymentDTO);
+            //shopOrderDTO.setPay(paymentDTO);
         }
 
         ErpOrderResponseDTO upload = jstErpHttpService.upload(shopOrderDTO);

+ 2 - 2
fs-service/src/main/java/com/fs/erp/service/impl/WdtErpGoodsServiceImpl.java

@@ -99,8 +99,8 @@ public class WdtErpGoodsServiceImpl implements IErpGoodsService {
                 for (ErpWdtStockDTO stock : stocks) {
                     ErpGoodsStock erpGoodsStock = new ErpGoodsStock();
                     erpGoodsStock.setBarcode(barcode);
-                    erpGoodsStock.setQty(stock.getStock_num());
-                    erpGoodsStock.setSalable_qty(stock.getAvaliable_num());
+                    erpGoodsStock.setQty(stock.getStock_num().split("\\.")[0]); //999979.0000
+                    erpGoodsStock.setSalable_qty(stock.getAvaliable_num().split("\\.")[0]); //999976.0000
                     list.add(erpGoodsStock);
                 }
             } else {

+ 11 - 12
fs-service/src/main/java/com/fs/erp/service/impl/WdtErpOrderServiceImpl.java

@@ -25,13 +25,14 @@ import com.fs.his.service.IFsStoreOrderItemService;
 import com.fs.his.service.IFsStoreOrderService;
 import com.fs.his.service.IFsStoreProductService;
 import com.fs.his.utils.ConfigUtil;
-import com.fs.his.vo.FsStoreOrderItemVO;
+import com.fs.hisStore.config.FsErpConfig;
 import com.fs.hisStore.domain.FsStoreOrderItemScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
+import com.fs.hisStore.vo.FsStoreOrderItemVO;
 import com.hc.openapi.tool.util.StringUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
@@ -284,21 +285,16 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
     }
 
     private ErpOrderResponse getErpOrderResponseScrm(ErpOrder order) {
-        FsSysConfig sysConfig = configUtil.getSysConfig();
-        String shopCode = sysConfig.getErpWdShopCode();
-
         //测试环境sid、appkey、密钥请到旺店通开放平台-自助对接-申请测试环境内查看,测试环境url=https://sandbox.wangdian.cn/openapi2/
         //调用正式环境时请将sid、appkey、appsecret切换为实际参数,参数在旺店通开放平台-自助对接-应用管理内应用状态为已上线的应用中查看,调用正式环境注意切换正式环境url=https://api.wangdian.cn/openapi2/
 
+        FsErpConfig erpconfig = configUtil.getErpConfig();
         ErpWdtBusinessRequestParams erpWdtBusinessRequestParams = new ErpWdtBusinessRequestParams();
-        erpWdtBusinessRequestParams.setShopNo(shopCode);
+        erpWdtBusinessRequestParams.setShopNo(erpconfig.getErpWdShopCode());
         erpWdtBusinessRequestParams.setSwitchMode(0);
         ErpWdtTrade erpWdtTrade = new ErpWdtTrade();
         FsStoreOrderScrm fsStoreOrder = fsStoreOrderScrmService.selectFsStoreOrderByOrderCode(order.getPlatform_code());
         BigDecimal couponPrice = fsStoreOrder.getCouponPrice();
-        if (couponPrice == null) {
-            couponPrice = BigDecimal.ZERO;
-        }
 
         // 平台状态
         erpWdtTrade.setTradeStatus(TradeStatus.PAID_WAITING_FOR_SHIPMENT.getValue());
@@ -365,7 +361,7 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
             erpWdtTrade.setPaid(fsStoreOrder.getPayMoney());
         }
 
-        List<com.fs.hisStore.vo.FsStoreOrderItemVO> fsStoreOrderItemVOS = fsStoreOrderItemScrmService.selectFsStoreOrderItemListByOrderId(fsStoreOrder.getId());
+        List<FsStoreOrderItemVO> fsStoreOrderItemVOS = fsStoreOrderItemScrmService.selectFsStoreOrderItemListByOrderId(fsStoreOrder.getId());
         List<ErpWdtOrder> erpWdtOrderList = new ArrayList<>();
         // 商品总价
         BigDecimal totalPrice = fsStoreOrder.getTotalPrice();
@@ -381,15 +377,16 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
             totalPrice = BigDecimal.ONE;
         }
 
-        for (com.fs.hisStore.vo.FsStoreOrderItemVO fsStoreOrderItem : fsStoreOrderItemVOS) {
+        for (FsStoreOrderItemVO fsStoreOrderItem : fsStoreOrderItemVOS) {
             ErpWdtOrder erpWdtOrder = new ErpWdtOrder();
             //平台订单货品表主键
-            erpWdtOrder.setOid(String.format("%s%s",fsStoreOrder.getOrderCode(),fsStoreOrderItem.getItemId()));
+            erpWdtOrder.setOid(String.format("%s%s",fsStoreOrderItem.getOrderCode(),fsStoreOrderItem.getItemId()));
             erpWdtOrder.setNum(BigDecimal.valueOf(fsStoreOrderItem.getNum()));
             FsStoreProductScrm fsStoreProduct = fsStoreProductScrmService.selectFsStoreProductById(fsStoreOrderItem.getProductId());
             Asserts.check(ObjectUtils.isNotNull(fsStoreProduct),"该产品不存在! 产品id: {} ",fsStoreOrderItem.getProductId());
+
             // 单价
-            erpWdtOrder.setPrice(fsStoreProduct.getPrice());
+//            erpWdtOrder.setPrice(fsStoreProduct.getPrice());
             // 状态
             erpWdtOrder.setStatus(TradeStatus.PAID_WAITING_FOR_SHIPMENT.getValue());
             // 退款状态
@@ -401,6 +398,8 @@ public class WdtErpOrderServiceImpl implements IErpOrderService {
             erpWdtOrder.setSpecId(jsonObject.getString("sku"));
             erpWdtOrder.setGoodsNo(jsonObject.getString("barCode"));
             erpWdtOrder.setSpecNo(jsonObject.getString("sku"));
+            //由于商品改价 用item里面的单价
+            erpWdtOrder.setPrice(jsonObject.getBigDecimal("price"));
             // 货品名称
             erpWdtOrder.setGoodsName(fsStoreProduct.getProductName());
             // 调整

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

@@ -17,6 +17,8 @@ import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.FsCourseLinkCreateParam;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.vo.FsCourseWatchLogVO;
+import com.fs.crm.domain.CrmMsg;
+import com.fs.crm.service.ICrmMsgService;
 import com.fs.fastGpt.config.ModeConfig;
 import com.fs.fastGpt.domain.*;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
@@ -161,6 +163,8 @@ public class AiHookServiceImpl implements AiHookService {
     private CompanyConfigMapper companyConfigMapper;
     @Autowired
     private IFastGptChatReplaceTextService fastGptChatReplaceTextService;
+    @Autowired
+    private ICrmMsgService crmMsgService;
 
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
@@ -449,6 +453,12 @@ public class AiHookServiceImpl implements AiHookService {
                 log.info("不是图片"+type);
             }
 
+            if("==语音转换失败==".equals(qwContent)){
+                log.error("语音转换失败转人工:"+role.getRoleId()+":"+qwExternalContacts.getName() + ",uid" + uid);
+                notifyArtificial(fastGptChatSession.getSessionId(),user,qwExternalContacts.getName()," 语音转换失败转人工",qwExternalContacts.getId(),sender);
+                return R.ok();
+            }
+
             //对用户处理的内容做处理
             String maskedContent = processContent(qwContent);
             String contentEmj = replaceWxEmo(maskedContent);
@@ -1123,6 +1133,15 @@ public class AiHookServiceImpl implements AiHookService {
         sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),"您的客户:"+name+"因"+reason+"  需要转人工,请及时回复");
         sendQwAppMsg(user.getCorpId(),Integer.parseInt(qwCompany.getServerAgentId().trim()),user.getQwUserId(),name);
         qwWorkTaskService.addQwWorkByAiNotifyArtificial(user,extId,"Ai转人工");
+        FastGptChatSession chatSession = fastGptChatSessionMapper.selectFastGptChatSessionBySessionId(sessionId);
+        CrmMsg crmMsg = new CrmMsg();
+        crmMsg.setMsgType(3);
+        crmMsg.setTitle("Ai转人工");
+        crmMsg.setContent("你的客户:" + chatSession.getNickName() + ", 因  \"" + reason + "\"  需要转人工,请及时回复");
+        crmMsg.setCompanyId(user.getCompanyId());
+        crmMsg.setCompanyUserId(user.getCompanyUserId());
+        crmMsgService.insertCrmMsg(crmMsg);
+
     }
 
     void sendQwAppMsg(String corpId, Integer agentId,String qwUserId,String content){

+ 4 - 0
fs-service/src/main/java/com/fs/his/config/StoreConfig.java

@@ -18,5 +18,9 @@ public class StoreConfig implements Serializable {
     private String refundPhoneNumber;
     private String refundAddress;
     private Integer storeCall;
+    private Integer isIdVerification; //1开启三方身份认证 0不开启
+    private String API_URL;
+    private String HASHCODE;
+    private String SECRET_KEY;
 
 }

+ 4 - 3
fs-service/src/main/java/com/fs/his/domain/FsUser.java

@@ -22,6 +22,7 @@ public class FsUser extends BaseEntity
     private static final long serialVersionUID = 1L;
 
     /** 用户id */
+    @Excel(name = "ID", sort = 0)
     private Long userId;
     private String username;
     private String password;
@@ -35,7 +36,7 @@ public class FsUser extends BaseEntity
     private String nickName;
 
     /** 用户头像 */
-    @Excel(name = "用户头像")
+//    @Excel(name = "用户头像")
     private String avatar;
     private String remark;
 
@@ -73,11 +74,11 @@ public class FsUser extends BaseEntity
     private Long tuiUserCount;
 
     /** 微信小程序OPENID */
-    @Excel(name = "微信小程序OPENID")
+//    @Excel(name = "微信小程序OPENID")
     private String maOpenId;
 
     /** 微信公众号OPENID */
-    @Excel(name = "微信公众号OPENID")
+//    @Excel(name = "微信公众号OPENID")
     private String mpOpenId;
 
     /** 关联ID */

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

@@ -13,6 +13,7 @@ public enum FsStoreOrderLogEnum {
     REMOVE_ORDER("remove_order","删除订单"),
     EVAL_ORDER("order_eval","用户评价"),
     REFUND_ORDER_APPLY("apply_refund","用户申请退款"),
+    REFUND_ORDER_PLATFORM("REFUND_ORDER_PLATFORM","平台申请退款"),
     REFUND_ORDER_DF("refund_order_df","代服取消订单,申请退款"),
     TAKE_ORDER_DELIVERY("user_take_delivery","用户已收货"),
     PAY_ORDER_SUCCESS("pay_success","用户付款成功"),
@@ -30,7 +31,11 @@ public enum FsStoreOrderLogEnum {
     PLATFORM_REVIEW_SALES("PLATFORM_REVIEW_SALES","平台已审核"),
     WAREHOUSE_REVIEW_SALES("WAREHOUSE_REVIEW_SALES","仓库已审核"),
     FINANCE_REVIEW_SALES("FINANCE_REVIEW_SALES","财务已审核"),
-    PLATFORM_REVIEW_CANCEL("PLATFORM_REVIEW_CANCEL","平台已取消售后");
+    PLATFORM_REVIEW_CANCEL("PLATFORM_REVIEW_CANCEL","平台已取消售后"),
+
+    SET_PUSH_MOBILE("SET_PUSH_MOBILE","设置推送手机号"),
+    SET_PUSH_ACCOUNT("SET_PUSH_ACCOUNT","数据分拣"),
+    PUSH_ORDER_ERP("PUSH_ORDER_ERP","创建ERP");
     private String value;
     private String desc;
 

+ 10 - 1
fs-service/src/main/java/com/fs/his/mapper/FsPackageOrderMapper.java

@@ -76,7 +76,16 @@ public interface FsPackageOrderMapper
     public int deleteFsPackageOrderByOrderIds(Long[] orderIds);
 
     @Select({"<script> " +
-            "select o.order_id,o.package_second_name,o.source,o.order_sn,o.package_name,o.pay_time, o.days,o.pay_price,o.pay_money,o.pay_type,o.`status`,o.package_sub_type,o.create_time,o.start_time,o.finish_time      ,d.doctor_name,u.nick_name,u.phone,c.company_name,cu.nick_name company_user_name,patient_json->>'$.patientName' as patientName,fso.delivery_status,fso.delivery_pay_status  from fs_package_order o LEFT JOIN fs_doctor d ON d.doctor_id=o.doctor_id LEFT JOIN fs_user u ON u.user_id=o.user_id LEFT JOIN company c on c.company_id =o.company_id LEFT JOIN company_user cu on cu.user_id=o.company_user_id LEFT JOIN fs_store_order fso ON fso.order_id= o.store_order_id"+
+            "select o.order_id,o.package_second_name,o.source,o.order_sn,o.package_name,o.pay_time,o.is_pay, o.days,o.pay_price, " +
+            "o.pay_money,o.pay_type,o.`status`,o.package_sub_type,o.create_time,o.start_time,o.finish_time      " +
+            ",d.doctor_name,u.nick_name,u.phone,c.company_name,cu.nick_name company_user_name, " +
+            "patient_json->>'$.patientName' as patientName,fso.delivery_status,fso.delivery_pay_status  " +
+            "from fs_package_order o " +
+            "LEFT JOIN fs_doctor d ON d.doctor_id=o.doctor_id " +
+            "LEFT JOIN fs_user u ON u.user_id=o.user_id " +
+            "LEFT JOIN company c on c.company_id =o.company_id " +
+            "LEFT JOIN company_user cu on cu.user_id=o.company_user_id " +
+            "LEFT JOIN fs_store_order fso ON fso.order_id= o.store_order_id "+
             " where 1 = 1 \n" +
             "            <if test=\"maps.orderSn != null  and maps.orderSn != ''\"> and o.order_sn = #{maps.orderSn}</if>\n" +
             "            <if test=\"maps.phone != null and maps.phone != '' \">and u.phone = #{maps.phone} </if>\n" +

+ 5 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderMapper.java

@@ -1,5 +1,6 @@
 package com.fs.his.mapper;
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -1166,6 +1167,10 @@ public interface FsStoreOrderMapper
 
     List<FsStoreOrderErpExportVO> selectFsStoreOrderListErpVOByExport(@Param("maps")FsStoreOrderParam fsStoreOrder);
 
+    Map<String,BigDecimal> selectFsStoreOrderStatistics(@Param("maps") FsStoreOrderParam fsStoreOrder);
+
+    String selectFsStoreOrderProductStatistics(@Param("maps")FsStoreOrderParam fsStoreOrder);
+
 
     List<Report> selectOrderByCustomerIds(@Param("map") ReportParam param);
 }

+ 1 - 0
fs-service/src/main/java/com/fs/his/param/FsStoreOrderSetErpPhoneParam.java

@@ -9,4 +9,5 @@ public class FsStoreOrderSetErpPhoneParam extends FsStoreOrderParam{
     private List<String> erpPhone;
     private List<Long> orderIds;
     private String loginAccount;
+    private String opeName; //操作人
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/service/IFsInquiryOrderService.java

@@ -129,4 +129,6 @@ public interface IFsInquiryOrderService
 
     byte[] getWxaCodeInquiryOrderUnLimit(Long orderId);
 
+    R getWxaCodeInquiryOrderUnLimitR(Long orderId);
+
 }

+ 2 - 1
fs-service/src/main/java/com/fs/his/service/IFsPackageOrderService.java

@@ -94,7 +94,7 @@ public interface IFsPackageOrderService
 
     Integer PackageInquiryOrderRefund(Long orderId);
 
-    Integer PackageStoreOrderRefund(Long orderId);
+    Integer PackageStoreOrderRefund(Long orderId,String opeName);
 
     R payment(FsPackageOrderDoPayParam param);
 
@@ -122,5 +122,6 @@ public interface IFsPackageOrderService
 
     List<PackageOrderDTO> getNewOrder();
 
+    R getWxaCodePackageOrderUnLimit(Long orderId);
 
 }

+ 5 - 0
fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java

@@ -1,5 +1,6 @@
 package com.fs.his.service;
 
+import java.math.BigDecimal;
 import java.text.ParseException;
 import java.util.List;
 import java.util.Map;
@@ -260,4 +261,8 @@ public interface IFsStoreOrderService
     String importOrderStatusData(List<FsStoreOrderStatusExcelVO> list);
 
     Long selectFsStoreOrderListVOByErpAccountByExportCount(FsStoreOrderParam param);
+
+    Map<String,BigDecimal> selectFsStoreOrderStatistics(FsStoreOrderParam fsStoreOrder);
+
+    String selectFsStoreOrderProductStatistics(FsStoreOrderParam fsStoreOrder);
 }

+ 47 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.his.service.impl;
 
+import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
@@ -23,6 +24,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyMoneyLogsMapper;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
+import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.config.WxPayProperties;
 import com.fs.core.utils.OrderCodeUtils;
 import com.fs.event.TemplateBean;
@@ -48,6 +50,8 @@ import com.fs.jpush.service.JpushService;
 import com.fs.repeat.vo.RepeatUploadVo;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
 import com.fs.tzBankPay.doman.*;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.ybPay.domain.RefundResult;
@@ -67,6 +71,7 @@ import com.github.binarywang.wxpay.service.WxPayService;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
 import lombok.Synchronized;
+import me.chanjar.weixin.common.error.WxErrorException;
 import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -1807,4 +1812,46 @@ public class FsInquiryOrderServiceImpl implements IFsInquiryOrderService
         byte[] bytes = HttpUtil.getWechatQrcodeByHttpURL(codeUrl, jsonMap);
         return bytes;
     }
+
+    @Override
+    public R getWxaCodeInquiryOrderUnLimitR(Long orderId) {
+
+        FsSysConfig sysConfig = configUtil.getSysConfig();
+
+        final WxMaService wxMaService = WxMaConfiguration.getMaService(sysConfig.getAppid());
+        String scene="orderId="+ orderId ;
+        byte[] file;
+        try {
+            file = wxMaService.getQrcodeService().createWxaCodeUnlimitBytes(
+                    scene,
+                    "pages_order/inquiryPay",
+                    true,
+                    "release",
+                    430,
+                    true,
+                    null,
+                    false);
+
+            // 上传图片到存储桶
+            String suffix = ".png";
+            CloudStorageService storage = OSSFactory.build();
+            String url;
+            try {
+                url = storage.uploadSuffix(file, suffix);
+            }  catch (Exception e) {
+                // 记录错误日志
+                logger.error("生成图片失败:{}",e.getMessage(),e);
+                return R.error("生成图片失败");
+            }
+            // 返回成功信息
+            return R.ok().put("url",url);
+
+        } catch (WxErrorException e) {
+            logger.error(e.getMessage(), e);
+            return R.error("微信接口调用失败: " + e.getMessage());
+        }
+
+    }
+
+
 }

+ 46 - 2
fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java

@@ -10,6 +10,7 @@ import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 
+import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
@@ -27,6 +28,7 @@ import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.core.config.WxMaConfiguration;
 import com.fs.core.config.WxPayProperties;
 import com.fs.core.utils.OrderCodeUtils;
 import com.fs.his.config.FsSysConfig;
@@ -50,6 +52,8 @@ import com.fs.huifuPay.domain.HuiFuCreateOrder;
 import com.fs.huifuPay.domain.HuifuCreateOrderResult;
 import com.fs.huifuPay.service.HuiFuService;
 import com.fs.system.domain.SysConfig;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
 import com.fs.ybPay.domain.CreateWxOrderResult;
 import com.fs.ybPay.domain.OrderResult;
 import com.fs.ybPay.dto.OrderQueryDTO;
@@ -70,6 +74,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
 import com.google.common.reflect.TypeToken;
 import com.google.gson.Gson;
+import me.chanjar.weixin.common.error.WxErrorException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -846,7 +851,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
     }
 
     @Override
-    public Integer PackageStoreOrderRefund(Long orderId) {
+    public Integer PackageStoreOrderRefund(Long orderId,String opeName) {
         FsPackageOrder fsPackageOrder = fsPackageOrderMapper.selectFsPackageOrderByOrderId(orderId);
         if (fsPackageOrder.getStatus()!=2&&fsPackageOrder.getStatus()!=3){
             throw  new CustomException("非法操作");
@@ -857,6 +862,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
         }
         FsStoreOrderSalesParam fsStoreOrderSalesParam = new FsStoreOrderSalesParam();
         fsStoreOrderSalesParam.setOrderId(fsPackageOrder.getStoreOrderId());
+        fsStoreOrderSalesParam.setOperator(opeName);
         return fsStoreOrderService.afterSales(fsStoreOrderSalesParam);
     }
 
@@ -1029,7 +1035,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
             }
         }
         else{
-            return R.error("用户OPENID不存在");
+            return R.error(401,"用户OPENID不存在"); //触发前端重新登录
         }
         return R.error();
     }
@@ -1535,4 +1541,42 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
             return R.error("创建失败");
         }
     }
+
+    @Override
+    public R getWxaCodePackageOrderUnLimit(Long orderId) {
+        FsSysConfig sysConfig = configUtil.getSysConfig();
+
+        final WxMaService wxMaService = WxMaConfiguration.getMaService(sysConfig.getAppid());
+        String scene="orderId="+ orderId ;
+        byte[] file;
+        try {
+            file = wxMaService.getQrcodeService().createWxaCodeUnlimitBytes(
+                    scene,
+                    "pages_order/packagePayment",
+                    true,
+                    "release",
+                    430,
+                    true,
+                    null,
+                    false);
+
+            // 上传图片到存储桶
+            String suffix = ".png";
+            CloudStorageService storage = OSSFactory.build();
+            String url;
+            try {
+                url = storage.uploadSuffix(file, suffix);
+            }  catch (Exception e) {
+                // 记录错误日志
+                logger.error("生成图片失败:{}",e.getMessage(),e);
+                return R.error("生成图片失败");
+            }
+            // 返回成功信息
+            return R.ok().put("url",url);
+
+        } catch (WxErrorException e) {
+            logger.error(e.getMessage(), e);
+            return R.error("微信接口调用失败: " + e.getMessage());
+        }
+    }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStoreAfterSalesServiceImpl.java

@@ -719,6 +719,8 @@ public class FsStoreAfterSalesServiceImpl implements IFsStoreAfterSalesService {
         logs.setStoreAfterSalesId(storeAfterSales.getId());
         logs.setChangeMessage(FsStoreAfterSalesStatusEnum.STATUS_0.getDesc());
         fsStoreAfterSalesLogsMapper.insertFsStoreAfterSalesLogs(logs);
+        fsStoreOrderLogsService.create(order.getOrderId(), FsStoreOrderLogEnum.REFUND_ORDER_APPLY.getValue(),
+                FsStoreOrderLogEnum.REFUND_ORDER_APPLY.getDesc());
         if (order.getExtendOrderId() != null) {
             ErpRefundUpdateRequest request = new ErpRefundUpdateRequest();
             request.setTid(order.getOrderCode());

+ 18 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -1606,6 +1606,9 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         logs.setCompanyId(fsStoreOrderSalesParam.getCompanyId());
         logs.setChangeMessage("平台提交售后");
         fsStoreAfterSalesLogsMapper.insertFsStoreAfterSalesLogs(logs);
+        //添加订单日志
+        fsStoreOrderLogsService.create(order.getOrderId(), FsStoreOrderLogEnum.REFUND_ORDER_PLATFORM.getValue(),
+                fsStoreOrderSalesParam.getOperator() + " " +FsStoreOrderLogEnum.REFUND_ORDER_PLATFORM.getDesc());
         //判断是否开启erp
         FsSysConfig sysConfig = configUtil.getSysConfig();
         Integer erpOpen = sysConfig.getErpOpen();
@@ -2311,7 +2314,7 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
             order.setStatus(4);
             fsStoreOrderMapper.updateFsStoreOrder(order);
             fsStoreOrderLogsService.create(order.getOrderId(), FsStoreOrderLogEnum.FINISH_ORDER.getValue(),
-                    FsStoreOrderLogEnum.FINISH_ORDER.getDesc());
+                    "用户"+FsStoreOrderLogEnum.FINISH_ORDER.getDesc());
             return R.ok("操作成功");
         } else {
             return R.error("非法操作");
@@ -3290,6 +3293,8 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
                 map.put("orderId", orderIds.get(i).toString());
                 map.put("erpPhone", erpPhones.get(i));
                 maps.add(map);
+                fsStoreOrderLogsService.create(orderIds.get(i), FsStoreOrderLogEnum.SET_PUSH_MOBILE.getValue(),
+                        param.getOpeName() + " " +FsStoreOrderLogEnum.SET_PUSH_MOBILE.getDesc() + ":" + erpPhones.get(i));
             }
         } else {
             //2.手机号小于orderIds长度
@@ -3309,6 +3314,8 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
                     map.put("erpPhone", erpPhone);
                     maps.add(map);
                     orderIndex++;
+                    fsStoreOrderLogsService.create(orderIds.get(i), FsStoreOrderLogEnum.SET_PUSH_MOBILE.getValue(),
+                            param.getOpeName() + " " +FsStoreOrderLogEnum.SET_PUSH_MOBILE.getDesc() + ":" + erpPhone);
                 }
             }
         }
@@ -3688,4 +3695,14 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         return fsStoreOrderMapper.selectFsStoreOrderListVOByErpAccountByExportCount(param);
     }
 
+    @Override
+    public Map<String,BigDecimal> selectFsStoreOrderStatistics(FsStoreOrderParam fsStoreOrder) {
+        return fsStoreOrderMapper.selectFsStoreOrderStatistics(fsStoreOrder);
+    }
+
+    @Override
+    public String selectFsStoreOrderProductStatistics(FsStoreOrderParam fsStoreOrder) {
+        return fsStoreOrderMapper.selectFsStoreOrderProductStatistics(fsStoreOrder);
+    }
+
 }

+ 143 - 0
fs-service/src/main/java/com/fs/his/utils/IdCardUtil.java

@@ -0,0 +1,143 @@
+package com.fs.his.utils;
+
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.his.config.StoreConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 三方身份验证
+ */
+@Service
+public class IdCardUtil {
+    @Autowired
+    private static ISysConfigService configService;
+
+    /**
+     * 身份证信息隐藏 中间
+     * @param idCard   身份证信息
+     * @return  隐藏后信息
+     */
+    public static String hiddenIdCard(String idCard) {
+        if (StringUtils.isBlank(idCard)) {
+            return idCard;
+        }
+
+        return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2");
+    }
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+
+    private static String generateSign(String hashcode, String passname, String pid, String mobile,String secretKey) {
+        String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
+        String raw = null;
+        if (StringUtils.isNotBlank(mobile)){
+            raw = hashcode + mobile + passname + secretKey + date;
+        } else {
+            raw = hashcode + passname + pid + secretKey + date;
+        }
+        return DigestUtil.md5Hex(raw.getBytes(StandardCharsets.UTF_8));
+    }
+
+    /**
+     * 检查身份证与姓名是否匹配
+     *
+     * @param passname 姓名
+     * @param pid      身份证号
+     * @return true 表示一致,false 表示不一致
+     */
+    public static boolean isMatchById(StoreConfig storeConfig,String passname, String pid) {
+        if (storeConfig == null) {
+            return false;
+        }
+        String API_URL = storeConfig.getAPI_URL() + "/HrmApi/Idverify";
+        String HASHCODE = storeConfig.getHASHCODE();
+        String SECRET_KEY = storeConfig.getSECRET_KEY();
+        try {
+            String sign = generateSign(HASHCODE, passname, pid,null, SECRET_KEY);
+            String bodyJson = String.format(
+                    "{\"passname\":\"%s\",\"pid\":\"%s\",\"hashcode\":\"%s\",\"sign\":\"%s\"}",
+                    passname, pid, HASHCODE, sign
+            );
+
+            HttpResponse response = HttpRequest.post(API_URL)
+                    .body(bodyJson)
+                    .header("Content-Type", "application/json;charset=UTF-8")
+                    .timeout(5000)
+                    .execute();
+
+            if (response.getStatus() != 200) {
+                throw new RuntimeException("HTTP请求失败,状态码:" + response.getStatus());
+            }
+
+            JsonNode root = OBJECT_MAPPER.readTree(response.body());
+            JsonNode errorRes = root.get("errorRes");
+            String errTransCode = root.get("err_TransCode").asText();
+            System.out.println(DateUtils.getNowDate());
+            System.out.println("流水号"+errTransCode);
+            if (errorRes != null && "200".equals(errorRes.get("err_code").asText())) {
+                return true; // 一致
+            }
+            return false; // 不一致或接口异常
+        } catch (Exception e) {
+            throw new RuntimeException("匹配请求异常", e);
+        }
+    }
+
+    /**
+     * 检查身份证与姓名是否匹配
+     *
+     * @param passname 姓名
+     * @param mobile      身份证号
+     * @return true 表示一致,false 表示不一致
+     */
+    public static boolean isMatchByMobile(StoreConfig storeConfig,String passname, String mobile) {
+        if (storeConfig == null) {
+            return false;
+        }
+        String API_URL = storeConfig.getAPI_URL() + "/HrmApi/mobile2everify";
+        String HASHCODE = storeConfig.getHASHCODE();
+        String SECRET_KEY = storeConfig.getSECRET_KEY();
+        try {
+            String sign = generateSign(HASHCODE, passname, null,mobile, SECRET_KEY);
+            String bodyJson = String.format(
+                    "{\"passname\":\"%s\",\"mobile\":\"%s\",\"hashcode\":\"%s\",\"sign\":\"%s\"}",
+                    passname, mobile, HASHCODE, sign
+            );
+
+            HttpResponse response = HttpRequest.post(API_URL)
+                    .body(bodyJson)
+                    .header("Content-Type", "application/json;charset=UTF-8")
+                    .timeout(5000)
+                    .execute();
+
+            if (response.getStatus() != 200) {
+                throw new RuntimeException("HTTP请求失败,状态码:" + response.getStatus());
+            }
+
+            JsonNode root = OBJECT_MAPPER.readTree(response.body());
+            JsonNode errorRes = root.get("errorRes");
+            String errTransCode = root.get("err_TransCode").asText();
+            System.out.println(DateUtils.getNowDate());
+            System.out.println("流水号"+errTransCode);
+            if (errorRes != null && "200".equals(errorRes.get("err_code").asText())) {
+                return true; // 一致
+            }
+            return false; // 不一致或接口异常
+        } catch (Exception e) {
+            throw new RuntimeException("匹配请求异常", e);
+        }
+    }
+}

+ 14 - 0
fs-service/src/main/java/com/fs/his/vo/FsStoreOrderListAndStatisticsVo.java

@@ -0,0 +1,14 @@
+package com.fs.his.vo;
+
+import com.fs.common.core.page.TableDataInfo;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsStoreOrderListAndStatisticsVo extends TableDataInfo{
+    private String payPriceTotal;
+    private String payMoneyTotal;
+    private String payRemainTotal;
+    private String productInfo;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/enums/SysConfigEnum.java

@@ -85,7 +85,7 @@ public enum SysConfigEnum {
     //红包流量,joinTime.switch.config
     JOIN_TIME_SWITCH_CONFIG("joinTime.switch.config", "红包流量"),
     //签到配置,store.sign
-    SIGN_CONFIG("store.sign", "签到配置"),
+    SIGN_CONFIG("his.sign", "签到配置"),
     //商城支付配置,store.pay
     STORE_PAY_CONFIG("store.pay", "商城支付配置");
 

+ 2 - 2
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreCartScrmMapper.java

@@ -81,7 +81,7 @@ public interface FsStoreCartScrmMapper
 
     @Select("select ifnull(sum(c.cart_num),0) from fs_store_cart_scrm c inner join fs_store_product_scrm p on p.product_id=c.product_id inner join fs_store_product_attr_value_scrm v on v.id=c.product_attr_value_id where c.is_pay=0 and c.is_del=0 and c.is_buy=0 and p.is_show=1 and p.is_del=0 and c.user_id= #{userId}")
     Integer selectFsStoreCartCountByUserId(long userId);
-    @Select({"<script> " +
+    /*@Select({"<script> " +
             "select ifnull(sum(c.cart_num),0) from fs_store_cart_scrm c  " +
             "where c.is_pay=0 and c.is_del=0 and c.is_buy=0 " +
             "<if test = 'maps.userId != null     '> " +
@@ -90,7 +90,7 @@ public interface FsStoreCartScrmMapper
             "<if test = 'maps.productId != null    '> " +
             "and c.product_id =#{maps.productId} " +
             "</if>" +
-            "</script>"})
+            "</script>"})*/
     Integer selectFsStoreCartCount(@Param("maps") FsStoreCartCountParam param);
     @Select("select c.*,p.cate_id,p.product_name,p.image as product_image,p.temp_id,p.product_type,v.price,v.sku as product_attr_name,v.image as product_attr_image,v.stock,v.cost,v.integral,v.weight,v.volume,v.bar_code,v.group_bar_code,v.brokerage,v.brokerage_two,v.brokerage_three from fs_store_cart_scrm c left join fs_store_product_scrm p on p.product_id=c.product_id left join fs_store_product_attr_value_scrm v on v.id=c.product_attr_value_id where c.is_gift = 1 AND user_id = #{userId}")
     List<FsStoreCartQueryVO> selectFsStoreGiftCartList(@Param("userId") Long userId);

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java

@@ -238,7 +238,7 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
                 ErpGoodsStockQueryResponse goodsStock = goodsService.getGoodsStock(queryRequert);
                 if(goodsStock != null && goodsStock.getStocks() != null && !goodsStock.getStocks().isEmpty()){
 
-                    int stocks = Integer.valueOf(goodsStock.getStocks().get(0).getSalable_qty());
+                    int stocks = Double.valueOf(goodsStock.getStocks().get(0).getSalable_qty()).intValue();
 
                     if(stocks<=0){
                         productAttrValue.setStock(0);

+ 41 - 4
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -2347,11 +2347,32 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService
             cartMapper.insertFsStoreCart(storeCart);
             ids.add(storeCart.getId());
         }
+        List<FsStoreCartQueryVO> giftCarts = redisCache.getCacheObject("orderGiftCarts:" + createOrderKey);
+        List<Long> giftIds=new ArrayList<>();
+        if(giftCarts != null){
+            for(FsStoreCartQueryVO vo : giftCarts){
+                FsStoreCartScrm storeCart = FsStoreCartScrm.builder()
+                        .cartNum(vo.getCartNum())
+                        .productAttrValueId(vo.getProductAttrValueId())
+                        .productId(vo.getProductId())
+                        .userId(userId)
+                        .isPay(0)
+                        .isDel(0)
+                        .isBuy(1)
+                        .changePrice(vo.getPrice())
+                        .isGift(1)
+                        .build();
+                storeCart.setCreateTime(new Date());
+                cartMapper.insertFsStoreCart(storeCart);
+                giftIds.add(storeCart.getId());
+            }
+        }
+
         //删除REDIS
 //        redisCache.deleteObject("createOrderKey:"+createOrderKey);
 //        redisCache.deleteObject("orderCarts:" + createOrderKey);
         String[] idArr=key.split("-");
-        return R.ok().put("cartIds",ids).put("companyId",idArr[0]).put("companyUserId",idArr[1]);
+        return R.ok().put("cartIds",ids).put("companyId",idArr[0]).put("companyUserId",idArr[1]).put("giftCartIds",giftIds);
 
     }
 
@@ -3066,8 +3087,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService
         for(FsStoreCartQueryVO vo:carts){
             //单个价格不能为0
             BigDecimal price = vo.getPrice();
-            if (price==null || price.compareTo(BigDecimal.ZERO)<=0){
-                return R.error("商品单价不能0");
+            if (price==null || price.compareTo(BigDecimal.ZERO)<0){
+                return R.error("商品单价不能小于0");
             }
             //更新购物车表
             FsStoreCartScrm storeCart = new FsStoreCartScrm();
@@ -3077,11 +3098,27 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService
             //计算总价
             totalMoney=totalMoney.add(price.multiply(new BigDecimal(vo.getCartNum().toString())));
         }
+        List<FsStoreCartQueryVO> giftCarts = param.getGiftCarts();
+        if (giftCarts != null && !giftCarts.isEmpty()) {
+            for(FsStoreCartQueryVO vo:giftCarts){
+                BigDecimal price = vo.getPrice();
+                if (price==null || price.compareTo(BigDecimal.ZERO)<0){
+                    return R.error("商品单价不能小于0");
+                }
+                //更新购物车表
+                FsStoreCartScrm storeCart = new FsStoreCartScrm();
+                storeCart.setId(vo.getId());
+                storeCart.setChangePrice(price);
+                cartMapper.updateFsStoreCart(storeCart);
+                //计算总价
+                totalMoney=totalMoney.add(price.multiply(new BigDecimal(vo.getCartNum().toString())));
+            }
+        }
 
         redisCache.setCacheObject("createOrderMoney:"+createOrderKey, totalMoney, 24, TimeUnit.HOURS);
         //更新redis的购物车明细
         redisCache.setCacheObject("orderCarts:" + createOrderKey,carts,24, TimeUnit.HOURS);
-
+        redisCache.setCacheObject("orderGiftCarts:" + createOrderKey,giftCarts,24, TimeUnit.HOURS);
         return R.ok();
     }
 

+ 2 - 2
fs-service/src/main/java/com/fs/hisStore/service/impl/FsUserSignScrmServiceImpl.java

@@ -122,7 +122,7 @@ public class FsUserSignScrmServiceImpl implements IFsUserSignScrmService
     @Override
     @Transactional
     public Long sign(FsUserScrm user) {
-        String json=configService.selectConfigByKey("store.sign");
+        String json=configService.selectConfigByKey("his.sign");
         if(StringUtils.isEmpty(json)) {
             throw new CustomException("请先配置签到天数");
         }
@@ -194,7 +194,7 @@ public class FsUserSignScrmServiceImpl implements IFsUserSignScrmService
 
     @Override
     public Long getSign(FsUserScrm user) {
-        String json=configService.selectConfigByKey("store.sign");
+        String json=configService.selectConfigByKey("his.sign");
         if(StringUtils.isEmpty(json)) {
             throw new CustomException("请先配置签到天数");
         }

+ 48 - 0
fs-service/src/main/java/com/fs/qw/domain/QwCourseFinishRemarkRty.java

@@ -0,0 +1,48 @@
+package com.fs.qw.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信完课打备注失败重试对象 qw_course_finish_remark_rty
+ *
+ * @author fs
+ * @date 2025-07-23
+ */
+@Data
+public class QwCourseFinishRemarkRty extends BaseEntity{
+
+    /** $column.columnComment */
+    private Long id;
+
+    /** 企业微信账号 */
+    @Excel(name = "企业微信账号")
+    private String qwUserId;
+
+    /** 外部联系人主键 */
+    @Excel(name = "外部联系人主键")
+    private Long externalId;
+
+    /** 外部联系人长字符串编号 */
+    @Excel(name = "外部联系人长字符串编号")
+    private String externalUserId;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private String corpId;
+
+
+    /** 创建时间 */
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 备注 */
+    @Excel(name = "remark")
+    private String remark;
+
+}

+ 1 - 1
fs-service/src/main/java/com/fs/qw/domain/QwUserVoiceLog.java

@@ -36,7 +36,7 @@ public class QwUserVoiceLog extends BaseEntity{
 
     /** 1接听 2未接 */
     @Excel(name = "1接听 2未接")
-    private Long status;
+    private Integer status;
 
     /** 企微id */
     @Excel(name = "企微id")

+ 74 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwCourseFinishRemarkRtyMapper.java

@@ -0,0 +1,74 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 企业微信完课打备注失败重试Mapper接口
+ *
+ * @author fs
+ * @date 2025-07-23
+ */
+public interface QwCourseFinishRemarkRtyMapper extends BaseMapper<QwCourseFinishRemarkRty>{
+    /**
+     * 查询企业微信完课打备注失败重试
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 企业微信完课打备注失败重试
+     */
+    QwCourseFinishRemarkRty selectQwCourseFinishRemarkRtyById(Long id);
+
+    /**
+     * 查询企业微信完课打备注失败重试列表
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 企业微信完课打备注失败重试集合
+     */
+    List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyList(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+
+    List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyListByTime(@Param("time") LocalDateTime tenMinutesBefore,@Param("num") Integer num);
+
+
+    List<Long> selectQwCourseFinishRemarkRtyListByOutTime();
+
+    /**
+     * 新增企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    int insertQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    /**
+     * 修改企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    int updateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    int insertOrUpdateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    /**
+     * 删除企业微信完课打备注失败重试
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 结果
+     */
+    int deleteQwCourseFinishRemarkRtyById(Long id);
+
+    /**
+     * 批量删除企业微信完课打备注失败重试
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwCourseFinishRemarkRtyByIds(Long[] ids);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -1,6 +1,8 @@
 package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.fastGpt.domain.FastgptChatArtificialWords;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUserDelLossLog;
@@ -443,6 +445,9 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     @Select("select id,first_time,qw_user_id,create_time from qw_external_contact where  id=#{id} ")
     QwExternalContact selectQwExternalContactTimeById(Long id);
 
+    @Select("select id,user_id,corp_id,external_user_id,name,remark,status from qw_external_contact where  id=#{id} ")
+    QwExternalContact selectQwExternalContactByRemark(Long id);
+
     List<QwExternalContactVOTime> selectQwExternalContactListVOByUserIds(@Param("ids") List<String> ids);
 
     List<GroupUserExternalVo> selectByGroupUser(@Param("ids") List<String> ids);

+ 72 - 0
fs-service/src/main/java/com/fs/qw/service/IQwCourseFinishRemarkRtyService.java

@@ -0,0 +1,72 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 企业微信完课打备注失败重试Service接口
+ *
+ * @author fs
+ * @date 2025-07-23
+ */
+public interface IQwCourseFinishRemarkRtyService extends IService<QwCourseFinishRemarkRty>{
+    /**
+     * 查询企业微信完课打备注失败重试
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 企业微信完课打备注失败重试
+     */
+    QwCourseFinishRemarkRty selectQwCourseFinishRemarkRtyById(Long id);
+
+    /**
+     * 查询企业微信完课打备注失败重试列表
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 企业微信完课打备注失败重试集合
+     */
+    List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyList(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    /**
+    * 获取10分钟以前的所有打标签失败的
+    */
+    List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyListByTime(LocalDateTime tenMinutesBefore,Integer num);
+
+    List<Long> selectQwCourseFinishRemarkRtyListByOutTime();
+
+    /**
+     * 新增企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    int insertQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    /**
+     * 修改企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    int updateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty);
+
+    /**
+     * 批量删除企业微信完课打备注失败重试
+     *
+     * @param ids 需要删除的企业微信完课打备注失败重试主键集合
+     * @return 结果
+     */
+    int deleteQwCourseFinishRemarkRtyByIds(Long[] ids);
+
+    /**
+     * 删除企业微信完课打备注失败重试信息
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 结果
+     */
+    int deleteQwCourseFinishRemarkRtyById(Long id);
+
+    void insertOrUpdateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty remarkRty);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java

@@ -117,6 +117,7 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
 
     QwUser getQwUserByRedis(String corpId, String userID);
 
+    QwUser getQwUserByRedisForId(String qwUserId);
 
     void insertQwExternalContactByExternalUserId(String externalUserID, String userID, Long companyId, String corpId, String state, String welcomeCode) throws ParseException;
 
@@ -236,6 +237,10 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
 
     List<QwExternalContact> selectQwExternalContactByFsUserIdAndCompany(Long userId,Long companyUserId);
 
+
+    QwExternalContact selectQwExternalContactByRemark(Long qwExternalContactId);
+
+
     /**
      * 企微用户流失删除统计
      * @param param 查询参数

+ 1 - 1
fs-service/src/main/java/com/fs/qw/service/IQwUserVoiceLogService.java

@@ -64,5 +64,5 @@ public interface IQwUserVoiceLogService extends IService<QwUserVoiceLog>{
 
     List<QwUserVoiceLogTotalVo> selectQwUserVoiceLogTotalList(QwUserVoiceLogTotalVo qwUserVoiceLog);
 
-    void addQuUserVoiceByIpadCallback(Long id, Long extUserId, Integer recordType, Integer totalSeconds, String uuid);
+    void addQuUserVoiceByIpadCallback(Long id, Long extUserId, Integer recordType, Long totalSeconds, String uuid);
 }

+ 117 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwCourseFinishRemarkRtyServiceImpl.java

@@ -0,0 +1,117 @@
+package com.fs.qw.service.impl;
+
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.qw.domain.QwCourseFinishRemarkRty;
+import com.fs.qw.mapper.QwCourseFinishRemarkRtyMapper;
+import com.fs.qw.service.IQwCourseFinishRemarkRtyService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 企业微信完课打备注失败重试Service业务层处理
+ *
+ * @author fs
+ * @date 2025-07-23
+ */
+@Service
+public class QwCourseFinishRemarkRtyServiceImpl extends ServiceImpl<QwCourseFinishRemarkRtyMapper, QwCourseFinishRemarkRty> implements IQwCourseFinishRemarkRtyService {
+
+
+    @Autowired
+    private QwCourseFinishRemarkRtyMapper finishRemarkRtyMapper;
+
+    /**
+     * 查询企业微信完课打备注失败重试
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 企业微信完课打备注失败重试
+     */
+    @Override
+    public QwCourseFinishRemarkRty selectQwCourseFinishRemarkRtyById(Long id)
+    {
+        return baseMapper.selectQwCourseFinishRemarkRtyById(id);
+    }
+
+    /**
+     * 查询企业微信完课打备注失败重试列表
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 企业微信完课打备注失败重试
+     */
+    @Override
+    public List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyList(QwCourseFinishRemarkRty qwCourseFinishRemarkRty)
+    {
+        return baseMapper.selectQwCourseFinishRemarkRtyList(qwCourseFinishRemarkRty);
+    }
+
+    @Override
+    public List<QwCourseFinishRemarkRty> selectQwCourseFinishRemarkRtyListByTime(LocalDateTime tenMinutesBefore, Integer num) {
+        return finishRemarkRtyMapper.selectQwCourseFinishRemarkRtyListByTime(tenMinutesBefore,num);
+    }
+
+    @Override
+    public List<Long> selectQwCourseFinishRemarkRtyListByOutTime() {
+        return finishRemarkRtyMapper.selectQwCourseFinishRemarkRtyListByOutTime();
+    }
+
+
+    /**
+     * 新增企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    @Override
+    public int insertQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty)
+    {
+        qwCourseFinishRemarkRty.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertQwCourseFinishRemarkRty(qwCourseFinishRemarkRty);
+    }
+
+    /**
+     * 修改企业微信完课打备注失败重试
+     *
+     * @param qwCourseFinishRemarkRty 企业微信完课打备注失败重试
+     * @return 结果
+     */
+    @Override
+    public int updateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty qwCourseFinishRemarkRty)
+    {
+        return baseMapper.updateQwCourseFinishRemarkRty(qwCourseFinishRemarkRty);
+    }
+
+    /**
+     * 批量删除企业微信完课打备注失败重试
+     *
+     * @param ids 需要删除的企业微信完课打备注失败重试主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwCourseFinishRemarkRtyByIds(Long[] ids)
+    {
+        return baseMapper.deleteQwCourseFinishRemarkRtyByIds(ids);
+    }
+
+    /**
+     * 删除企业微信完课打备注失败重试信息
+     *
+     * @param id 企业微信完课打备注失败重试主键
+     * @return 结果
+     */
+    @Override
+    public int deleteQwCourseFinishRemarkRtyById(Long id)
+    {
+        return baseMapper.deleteQwCourseFinishRemarkRtyById(id);
+    }
+
+    @Override
+    public void insertOrUpdateQwCourseFinishRemarkRty(QwCourseFinishRemarkRty remarkRty) {
+        finishRemarkRtyMapper.insertOrUpdateQwCourseFinishRemarkRty(remarkRty);
+    }
+
+}

+ 20 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -2165,7 +2165,22 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
 
         redisCache.setCacheObject("qwUserRd:"+qwUser.getCorpId()+":"+qwUser.getQwUserId() ,JSON.toJSONString(qwUser),1, TimeUnit.HOURS);
         return qwUser;
-    };
+    }
+
+    @Override
+    public QwUser getQwUserByRedisForId(String qwUserId) {
+        String key =(String)redisCache.getCacheObject("qwUserRdById:"+qwUserId);
+        if (!StringUtil.strIsNullOrEmpty(key)){
+            return JSON.parseObject(key, QwUser.class);
+        }
+        QwUser qwUser = qwUserMapper.selectQwUserById(Long.valueOf(qwUserId));
+        if (qwUser==null){
+            return null;
+        }
+        redisCache.setCacheObject("qwUserRdById:"+qwUser.getId() ,JSON.toJSONString(qwUser),1, TimeUnit.HOURS);
+        return qwUser;
+    }
+    ;
 
     @Override
     public void insertQwExternalContactByExternalUserId(String externalUserID, String userID, Long companyId, String corpId, String state, String welcomeCode) throws ParseException {
@@ -5543,6 +5558,10 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         return qwExternalContactMapper.selectQwExternalContactByFsUserIdAndCompany(userId,companyUserId);
     }
 
+    @Override
+    public QwExternalContact selectQwExternalContactByRemark(Long qwExternalContactId) {
+        return qwExternalContactMapper.selectQwExternalContactByRemark(qwExternalContactId);
+    }
 
     //发送好友欢迎语
        void   SyncAddSendWelcome(QwExternalContact qwExternalContact, QwUser qwUser, String corpId){

+ 6 - 1
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -850,7 +850,12 @@ public class QwUserServiceImpl implements IQwUserService
     @Override
     public int updateQwUser(QwUser qwUser)
     {
-        return qwUserMapper.updateQwUser(qwUser);
+        int i = qwUserMapper.updateQwUser(qwUser);
+        if (i>0){
+            redisCache.setCacheObject("qwUserRdById:"+qwUser.getId() ,JSON.toJSONString(qwUser),1, TimeUnit.HOURS);
+            redisCache.setCacheObject("qwUserRd:"+qwUser.getCorpId()+":"+qwUser.getQwUserId() ,JSON.toJSONString(qwUser),1, TimeUnit.HOURS);
+        }
+        return i;
     }
 
     /**

+ 68 - 44
fs-service/src/main/java/com/fs/qw/service/impl/QwUserVoiceLogServiceImpl.java

@@ -155,57 +155,81 @@ public class QwUserVoiceLogServiceImpl extends ServiceImpl<QwUserVoiceLogMapper,
     }
 
     @Override
-    public void addQuUserVoiceByIpadCallback(Long id, Long extUserId, Integer recordType, Integer totalSeconds, String uuid) {
-        QwUser sendUser = qwUserMapper.selectQwUserById(id);
-        if (sendUser!=null){
-            String extId = getExtId(extUserId, uuid, sendUser.getServerId());
-            QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(extId, sendUser.getCorpId(),sendUser.getQwUserId());
-            if (qwExternalContacts==null){
-                log.error("未找到外部联系人,qwUserId:" + id);
-                return ;
-            }
+    public void addQuUserVoiceByIpadCallback(Long id, Long extUserId, Integer recordType, Long totalSeconds, String uuid) {
+        // 参数校验
+        if (id == null || (extUserId == null && uuid == null)) {
+            log.error("参数校验失败: id={}, extUserId={}, uuid={}", id, extUserId, uuid);
+            return;
+        }
 
-            QwUserVoiceLog qwUserVoiceLog = new QwUserVoiceLog();
-            qwUserVoiceLog.setCorpId(sendUser.getCorpId());
-            qwUserVoiceLog.setExtId(qwExternalContacts.getId());
-            qwUserVoiceLog.setQwUserId(sendUser.getId());
-            qwUserVoiceLog.setCompanyId(qwExternalContacts.getCompanyId());
-            if(qwExternalContacts.getCompanyUserId() != null){
-                qwUserVoiceLog.setCompanyUserId(qwExternalContacts.getCompanyUserId());
-            }
+        try {
+            QwUser sendUser = qwUserMapper.selectQwUserById(id);
+            if (sendUser != null) {
+                String extId = getExtId(extUserId, uuid, sendUser.getServerId());
+                // 检查extId是否有效
+                if (extId == null || extId.isEmpty()) {
+                    log.error("获取外部联系人ID失败,qwUserId:" + id + ",extUserId:" + extUserId + ",uuid:" + uuid);
+                    return;
+                }
 
-            if(recordType == 2){//2是未接通的处理
-                qwUserVoiceLog.setStatus(Long.valueOf(recordType));
-                qwUserVoiceLog.setTitle("通话拒接");
-            }else{
-                qwUserVoiceLog.setStatus(1L);
-                if(totalSeconds != null){
-                    qwUserVoiceLog.setDuration(Long.valueOf(totalSeconds));
-                }else{
-                    qwUserVoiceLog.setDuration(0L);
+                QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(extId, sendUser.getCorpId(), sendUser.getQwUserId());
+                if (qwExternalContacts == null) {
+                    log.error("未找到外部联系人,qwUserId:" + id + ",extId:" + extId);
+                    return;
                 }
-                qwUserVoiceLog.setTitle("通话已接听");
-            }
-            qwUserVoiceLog.setCreateTime(new Date());
-
-            int insert = baseMapper.insertQwUserVoiceLog(qwUserVoiceLog);
-            //插入成功的情况下就处理定时任务
-            if(insert == 1){
-                List<QwWorkTask> qwWorkTaskList = qwWorkTaskMapper.selectQwWorkTaskByExtIdAndQwUserId(qwUserVoiceLog);
-                if(qwWorkTaskList != null && !qwWorkTaskList.isEmpty()){
-                    for (QwWorkTask qwWorkTask : qwWorkTaskList) {
-                        if(qwUserVoiceLog.getStatus() == 1L){//接通电话
-                            qwWorkTask.setTrackType(3);//1正常处理 2 未接听 3 已接听
-                            qwWorkTask.setDuration(qwUserVoiceLog.getDuration());
-                        }else{
-                            qwWorkTask.setTrackType(2);//1正常处理 2 未接听 3 已接听
+
+                QwUserVoiceLog qwUserVoiceLog = new QwUserVoiceLog();
+                qwUserVoiceLog.setCorpId(sendUser.getCorpId());
+                qwUserVoiceLog.setExtId(qwExternalContacts.getId());
+                qwUserVoiceLog.setQwUserId(sendUser.getId());
+                qwUserVoiceLog.setCompanyId(qwExternalContacts.getCompanyId());
+                if (qwExternalContacts.getCompanyUserId() != null) {
+                    qwUserVoiceLog.setCompanyUserId(qwExternalContacts.getCompanyUserId());
+                }
+
+                if (recordType == 2) {//2是未接通的处理
+                    qwUserVoiceLog.setStatus(recordType);
+                    qwUserVoiceLog.setTitle("通话拒接");
+                } else {
+                    qwUserVoiceLog.setStatus(1);
+                    if (totalSeconds != null) {
+                        qwUserVoiceLog.setDuration(totalSeconds);
+                    } else {
+                        qwUserVoiceLog.setDuration(0L);
+                    }
+                    qwUserVoiceLog.setTitle("通话已接听");
+                }
+                qwUserVoiceLog.setCreateTime(new Date());
+
+                int insert = baseMapper.insertQwUserVoiceLog(qwUserVoiceLog);
+                // 插入成功的情况下就处理定时任务
+                if (insert == 1) {
+                    List<QwWorkTask> qwWorkTaskList = qwWorkTaskMapper.selectQwWorkTaskByExtIdAndQwUserId(qwUserVoiceLog);
+                    if (qwWorkTaskList != null && !qwWorkTaskList.isEmpty()) {
+                        for (QwWorkTask qwWorkTask : qwWorkTaskList) {
+                            try {
+                                if (qwUserVoiceLog.getStatus() == 1L) {//接通电话
+                                    qwWorkTask.setTrackType(3);//1正常处理 2 未接听 3 已接听
+                                    qwWorkTask.setDuration(qwUserVoiceLog.getDuration());
+                                } else {
+                                    qwWorkTask.setTrackType(2);//1正常处理 2 未接听 3 已接听
+                                }
+                                qwWorkTask.setStatus(1);//0 待处理 1 已处理 3 过期
+                                qwWorkTask.setUpdateTime(new Date());
+                                qwWorkTaskMapper.updateQwWorkTask(qwWorkTask);
+                            } catch (Exception e) {
+                                log.error("更新任务状态失败, taskId: " + qwWorkTask.getId(), e);
+                            }
                         }
-                        qwWorkTask.setStatus(1);//0 待处理 1 已处理 3 过期
-                        qwWorkTask.setUpdateTime(new Date());
-                        qwWorkTaskMapper.updateQwWorkTask(qwWorkTask);
                     }
+                } else {
+                    log.error("插入失败,qwUserId:" + id + ",extId:" + extUserId + ",uuid:" + uuid + ",totalSeconds" + totalSeconds);
                 }
+            } else {
+                log.error("未找到企微用户,id:" + id + ",extId:" + extUserId + ",uuid:" + uuid + ",totalSeconds" + totalSeconds);
             }
+        } catch (Exception e) {
+            log.error("处理通话回调异常, id:" + id + ",extUserId:" + extUserId + ",recordType:" + recordType + ",totalSeconds:" + totalSeconds + ",uuid:" + uuid, e);
         }
     }
 

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác