瀏覽代碼

Merge branch 'refs/heads/master' into openImAndLive

caoliqin 6 月之前
父節點
當前提交
1b2c37a89e
共有 100 個文件被更改,包括 3110 次插入164 次删除
  1. 26 0
      README.md
  2. 3 1
      fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java
  3. 37 0
      fs-admin/src/main/java/com/fs/api/controller/StatisticManageController.java
  4. 11 1
      fs-admin/src/main/java/com/fs/company/controller/CompanyRechargeController.java
  5. 96 1
      fs-admin/src/main/java/com/fs/his/task/Task.java
  6. 99 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsUserPromoterApplyController.java
  7. 28 0
      fs-admin/src/main/java/com/fs/task/ComprehensiveStatisticsTask.java
  8. 54 2
      fs-admin/src/main/java/com/fs/task/FsCompanyTask.java
  9. 52 0
      fs-common/src/main/java/com/fs/common/enums/DimensionEnum.java
  10. 5 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  11. 1 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseFinishTempController.java
  12. 118 5
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseFinishTempParentController.java
  13. 10 0
      fs-company/src/main/java/com/fs/company/controller/course/FsUserCourseVideoController.java
  14. 19 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwDeptController.java
  15. 47 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwUserController.java
  16. 5 0
      fs-ipad-task/pom.xml
  17. 1 1
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  18. 4 0
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  19. 52 0
      fs-ipad-task/src/test/java/com/fs/app/task/SendMsgTest.java
  20. 2 2
      fs-qw-api-msg/src/main/java/com/fs/app/controller/QwMsgController.java
  21. 39 6
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  22. 18 12
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  23. 48 0
      fs-service/src/main/java/com/fs/company/domain/CompanyDeptUserInfo.java
  24. 50 0
      fs-service/src/main/java/com/fs/company/domain/ComprehensiveDailyStats.java
  25. 57 0
      fs-service/src/main/java/com/fs/company/dto/CompanyDeptUserInfoDTO.java
  26. 42 0
      fs-service/src/main/java/com/fs/company/dto/ComprehensiveStatisticsDTO.java
  27. 7 1
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  28. 12 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  29. 38 0
      fs-service/src/main/java/com/fs/company/mapper/StatisticManageMapper.java
  30. 8 0
      fs-service/src/main/java/com/fs/company/service/ICompanyRechargeService.java
  31. 6 0
      fs-service/src/main/java/com/fs/company/service/ICompanyService.java
  32. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  33. 21 0
      fs-service/src/main/java/com/fs/company/service/IStatisticManageService.java
  34. 62 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyRechargeServiceImpl.java
  35. 7 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  36. 5 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  37. 151 0
      fs-service/src/main/java/com/fs/company/service/impl/StatisticManageServiceImpl.java
  38. 5 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseFinishTempParent.java
  39. 26 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java
  40. 13 2
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  41. 6 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  42. 2 0
      fs-service/src/main/java/com/fs/course/mapper/FsVideoResourceMapper.java
  43. 3 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempParentServiceImpl.java
  44. 41 2
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  45. 34 12
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  46. 39 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java
  47. 2 0
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  48. 68 17
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  49. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsPackageOrderMapper.java
  50. 2 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderMapper.java
  51. 5 1
      fs-service/src/main/java/com/fs/his/param/FsPackageCateUParam.java
  52. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsPackageOrderService.java
  53. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsStoreOrderService.java
  54. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsStorePaymentService.java
  55. 7 2
      fs-service/src/main/java/com/fs/his/service/impl/FsPackageOrderServiceImpl.java
  56. 2 2
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreAfterSalesServiceImpl.java
  57. 5 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  58. 21 0
      fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java
  59. 1 1
      fs-service/src/main/java/com/fs/hisStore/config/StoreConfig.java
  60. 78 53
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  61. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStorePaymentScrmServiceImpl.java
  62. 2 0
      fs-service/src/main/java/com/fs/qw/cache/IQwUserCacheService.java
  63. 15 0
      fs-service/src/main/java/com/fs/qw/cache/impl/QwUserCacheServiceImpl.java
  64. 47 0
      fs-service/src/main/java/com/fs/qw/domain/QwDeptTreeSelect.java
  65. 3 1
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  66. 1 0
      fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java
  67. 13 0
      fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java
  68. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java
  69. 5 0
      fs-service/src/main/java/com/fs/qw/param/QwUserListParam.java
  70. 18 2
      fs-service/src/main/java/com/fs/qw/service/AsyncQwAiChatSopService.java
  71. 3 0
      fs-service/src/main/java/com/fs/qw/service/IQwDeptService.java
  72. 1 0
      fs-service/src/main/java/com/fs/qw/service/IQwUserService.java
  73. 69 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwDeptServiceImpl.java
  74. 14 13
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  75. 5 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwGroupChatServiceImpl.java
  76. 12 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java
  77. 1 1
      fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java
  78. 2 2
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  79. 40 0
      fs-service/src/main/java/com/fs/statis/param/ComprehensiveStatisticsParam.java
  80. 115 0
      fs-service/src/main/java/com/fs/tag/domain/FsTagUpdateQueue.java
  81. 162 0
      fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java
  82. 29 0
      fs-service/src/main/java/com/fs/tag/service/FsTagUpdateService.java
  83. 318 0
      fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java
  84. 6 0
      fs-service/src/main/resources/application-config-druid-cqtyt.yml
  85. 6 0
      fs-service/src/main/resources/application-config-druid-fby.yml
  86. 3 3
      fs-service/src/main/resources/application-config-druid-heyantang.yml
  87. 10 3
      fs-service/src/main/resources/application-config-druid-jnlzjk.yml
  88. 2 2
      fs-service/src/main/resources/application-dev.yml
  89. 159 0
      fs-service/src/main/resources/application-druid-heyantang-test.yml
  90. 7 0
      fs-service/src/main/resources/mapper/company/CompanyMapper.xml
  91. 4 2
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  92. 366 0
      fs-service/src/main/resources/mapper/company/StatisticManageMapper.xml
  93. 13 6
      fs-service/src/main/resources/mapper/course/FsCourseFinishTempParentMapper.xml
  94. 6 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  95. 14 0
      fs-service/src/main/resources/mapper/course/FsVideoResourceMapper.xml
  96. 3 0
      fs-service/src/main/resources/mapper/his/FsPackageOrderMapper.xml
  97. 4 0
      fs-service/src/main/resources/mapper/his/FsStoreOrderMapper.xml
  98. 2 0
      fs-service/src/main/resources/mapper/his/FsStorePaymentMapper.xml
  99. 3 0
      fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml
  100. 14 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

+ 26 - 0
README.md

@@ -50,3 +50,29 @@ COALESCE(JSON_UNQUOTE(JSON_EXTRACT(remark_mobiles, '$[0]')), ''),
 -- 创建索引
 
 CREATE INDEX idx_search_mobile ON qw_external_contact(search_mobile);
+
+
+-- 新加统计表
+
+CREATE TABLE IF NOT EXISTS user_daily_stats (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
+    company_id BIGINT NOT NULL COMMENT '公司ID',
+    company_name VARCHAR(255) NOT NULL COMMENT '公司名称',
+    dept_id BIGINT NOT NULL COMMENT '部门ID',
+    dept_name VARCHAR(255) NOT NULL COMMENT '部门名称',
+    user_id BIGINT COMMENT '用户ID',
+    user_name VARCHAR(100) COMMENT '用户名',
+    nick_name VARCHAR(100) COMMENT '用户昵称',
+    statistics_time DATE NOT NULL COMMENT '统计日期',
+    line_num INT NOT NULL DEFAULT 0 COMMENT 't1统计数(线路数)',
+    active_num INT NOT NULL DEFAULT 0 COMMENT 't2统计数(活跃数)',
+    complete_num INT NOT NULL DEFAULT 0 COMMENT '完成数(t4)',
+    answer_num INT NOT NULL DEFAULT 0 COMMENT '答题数(t5)',
+    red_packet_num INT NOT NULL DEFAULT 0 COMMENT '红包数量(t6)',
+    red_packet_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT '红包金额',
+    create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
+    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
+    UNIQUE KEY uk_user_date (user_id, statistics_time) COMMENT '用户+日期唯一约束',
+    KEY idx_company_date (company_id, statistics_time) COMMENT '公司+日期索引',
+    KEY idx_dept_date (dept_id, create_time) COMMENT '部门+日期索引'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用户每日统计数据表';

+ 3 - 1
fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java

@@ -174,8 +174,10 @@ public class IndexStatisticsController {
         if(ObjectUtils.isNull(redPacketCompanyMoney)){
             redPacketCompanyMoney = BigDecimal.ZERO;
         }
-        consumptionBalanceDataDTO.setRunTianBalance(redPacketCompanyMoney);
+        if (consumptionBalanceDataDTO != null){
+            consumptionBalanceDataDTO.setRunTianBalance(redPacketCompanyMoney);
 
+        }
         return R.ok().put("data", consumptionBalanceDataDTO);
     }
 

+ 37 - 0
fs-admin/src/main/java/com/fs/api/controller/StatisticManageController.java

@@ -0,0 +1,37 @@
+package com.fs.api.controller;
+
+
+import com.fs.common.core.domain.R;
+import com.fs.company.service.IStatisticManageService;
+import com.fs.statis.param.ComprehensiveStatisticsParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.Assert;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * @description: 统计管理
+ * @author: Guos
+ * @time: 2025/10/30 上午9:16
+ */
+@Slf4j
+@RestController
+@RequestMapping("/statistic/manage")
+public class StatisticManageController {
+
+    @Resource
+    private IStatisticManageService statisticManageService;
+
+    @PostMapping("/statisticMain")
+    public R statisticMain(@RequestBody ComprehensiveStatisticsParam param) {
+        Assert.notNull(param.getDimension(), "请选择统计维度");
+        statisticManageService.executeTask();
+//        return R.ok().put("data", statisticManageService.statisticMain(param));
+        return R.ok("success");
+
+    }
+}

+ 11 - 1
fs-admin/src/main/java/com/fs/company/controller/CompanyRechargeController.java

@@ -24,6 +24,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
 import java.util.Date;
 import java.util.List;
 
@@ -121,7 +122,16 @@ public class CompanyRechargeController extends BaseController
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         if(companyRecharge.getIsAudit()==1){
             Company company=companyService.selectCompanyById(companyRecharge.getCompanyId());
-            company.setMoney(company.getMoney().add(companyRecharge.getMoney()));
+
+            // 同步redis缓存
+            // 注意:在进行充值审核之前,需要先执行一下定时任务同步缓存数据到数据库,再进行后续操作,否则金额不正确
+            R r = companyRechargeService.syncUpdateRedisCompanyRecharge(company, companyRecharge.getMoney());
+            if(!"200".equals(r.get("code").toString())){
+                return r;
+            }
+            // 充值后,需要同步更新余额到数据库,否则余额与缓存中的不一致
+            String newMoney = r.get("newMoney").toString();
+            company.setMoney(new BigDecimal(newMoney));
             companyService.updateCompany(company);
             CompanyMoneyLogs log=new CompanyMoneyLogs();
             log.setCompanyId(companyRecharge.getCompanyId());

+ 96 - 1
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -21,6 +21,7 @@ import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.dto.BatchSendCourseAllDTO;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import com.fs.course.service.IFsCourseWatchLogService;
+import com.fs.course.service.IFsUserCourseOrderService;
 import com.fs.course.service.ITencentCloudCosService;
 import com.fs.erp.domain.ErpDeliverys;
 import com.fs.erp.domain.ErpOrder;
@@ -49,6 +50,7 @@ import com.fs.his.enums.FsStoreOrderLogEnum;
 import com.fs.his.enums.FsStoreOrderStatusEnum;
 import com.fs.his.mapper.*;
 import com.fs.his.param.FsInquiryOrderFinishParam;
+import com.fs.his.param.FsPackageOrderCancelParam;
 import com.fs.his.service.*;
 import com.fs.his.service.impl.FsPackageOrderServiceImpl;
 import com.fs.his.utils.ConfigUtil;
@@ -70,6 +72,7 @@ import com.fs.sop.domain.QwSopTempVoice;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
+import com.fs.system.service.ISysConfigService;
 import com.google.gson.Gson;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -78,12 +81,14 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
@@ -146,7 +151,7 @@ public class Task {
     @Autowired
     private CompanyVoiceLogsMapper companyVoiceLogsMapper;
     @Autowired
-    FsPackageOrderServiceImpl packageOrderService;
+    IFsPackageOrderService packageOrderService;
     @Autowired
     private IFsStoreOrderLogsService fsStoreOrderLogsService;
     org.slf4j.Logger logger = LoggerFactory.getLogger(getClass());
@@ -216,6 +221,16 @@ public class Task {
     private FsUserOperationLogMapper fsUserOperationLogMapper;
 
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
+    @Autowired
+    private IFsStorePaymentService fsStorePaymentService;
+
+    @Autowired
+    private IFsStoreOrderService orderService;
+    @Autowired
+    private ISysConfigService sysConfigService;
+
+    @Autowired
+    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
 
     /**
      * sop任务token消耗统计
@@ -1563,5 +1578,85 @@ public class Task {
         log.info("定时删除行为轨迹记录 {} 条", deleteCount);
     }
 
+    //同步支付状态
+    public void synchronizePayStatus(){
+        fsStorePaymentService.synchronizePayStatus();
+    }
+
+    /**
+     * 超时取消订单
+     */
+    public void cancelOrder(){
+        //查询超时订单
+        SysConfig sysConfig= sysConfigService.selectConfigByConfigKey("his.store");
+        StoreConfig config= JSONUtil.toBean(sysConfig.getConfigValue(),StoreConfig.class);
+        Integer unPayTime = config.getUnPayTime(); //分钟
+        if (unPayTime == null){
+            return ;
+        }
+        //1.处方订单
+        //查询超时未支付订单
+        List<FsStoreOrder> orderList = orderService.selectOutTimeOrderList(unPayTime);
+        //取消订单
+        List<CompletableFuture<Void>> orderFutures = cancelOrdersAsync(orderList, order -> {
+            orderService.cancelOrder(order.getOrderId());
+        });
+
+//        //2.课程订单
+//        //查询超时未支付订单
+//        List<FsUserCourseOrder> courseOrderlist = userCourseOrderService.selectOutTimeOrderList(unPayTime);
+//        //取消订单
+//        courseOrderlist.forEach(order->{
+//            userCourseOrderService.cancelOrder(order.getOrderId());
+//        });
+        //3.服务包订单
+        //查询超时未支付订单
+        List<FsPackageOrder> packageOrderList = packageOrderService.selectOutTimeOrderList(unPayTime);
+        //取消订单
+        List<CompletableFuture<Void>> packageOrderFutures = cancelOrdersAsync(packageOrderList, order -> {
+            FsPackageOrderCancelParam param = new FsPackageOrderCancelParam();
+            param.setOrderId(order.getOrderId());
+            packageOrderService.cancel(param);
+        });
+
+        // 等待所有任务完成
+        waitForAllTasksToComplete(orderFutures);
+        waitForAllTasksToComplete(packageOrderFutures);
+    }
+
+    /**
+     * 异步取消订单
+     * @param orders 订单列表
+     * @param cancelAction 取消订单的逻辑
+     * @param <T> 订单类型
+     * @return CompletableFuture列表
+     */
+    private <T> List<CompletableFuture<Void>> cancelOrdersAsync(List<T> orders, Consumer<T> cancelAction) {
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (T order : orders) {
+            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                try {
+                    cancelAction.accept(order);
+                } catch (Exception e) {
+                    // 记录异常日志
+                    System.err.println("Failed to cancel order: " + order + ", Error: " + e.getMessage());
+                }
+            }, threadPoolTaskExecutor);
+            futures.add(future);
+        }
+        return futures;
+    }
+
+    /**
+     * 等待所有任务完成
+     * @param futures CompletableFuture列表
+     */
+    private void waitForAllTasksToComplete(List<CompletableFuture<Void>> futures) {
+        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
+                futures.toArray(new CompletableFuture[0])
+        );
+        allFutures.join(); // 等待所有任务完成
+    }
+
 
 }

+ 99 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsUserPromoterApplyController.java

@@ -0,0 +1,99 @@
+package com.fs.hisStore.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.hisStore.domain.FsUserPromoterApplyScrm;
+import com.fs.hisStore.param.FsUsePromoterApplyParam;
+import com.fs.hisStore.service.IFsUserPromoterApplyScrmService;
+import com.fs.hisStore.vo.FsUserPromoterApplyVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 推广员申请Controller
+ *
+ * @author fs
+ * @date 2023-02-28
+ */
+@RestController
+@RequestMapping("/store/userPromoterApply")
+public class FsUserPromoterApplyController extends BaseController
+{
+    @Autowired
+    private IFsUserPromoterApplyScrmService fsUserPromoterApplyService;
+
+    /**
+     * 查询推广员申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUsePromoterApplyParam fsUserPromoterApply)
+    {
+        startPage();
+        List<FsUserPromoterApplyVO> list = fsUserPromoterApplyService.selectFsUserPromoterApplyListVO(fsUserPromoterApply);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出推广员申请列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:export')")
+    @Log(title = "推广员申请", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserPromoterApplyScrm fsUserPromoterApply)
+    {
+        List<FsUserPromoterApplyScrm> list = fsUserPromoterApplyService.selectFsUserPromoterApplyList(fsUserPromoterApply);
+        ExcelUtil<FsUserPromoterApplyScrm> util = new ExcelUtil<FsUserPromoterApplyScrm>(FsUserPromoterApplyScrm.class);
+        return util.exportExcel(list, "userPromoterApply");
+    }
+
+    /**
+     * 获取推广员申请详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:query')")
+    @GetMapping(value = "/{applyId}")
+    public AjaxResult getInfo(@PathVariable("applyId") Long applyId)
+    {
+        return AjaxResult.success(fsUserPromoterApplyService.selectFsUserPromoterApplyById(applyId));
+    }
+
+    /**
+     * 新增推广员申请
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:add')")
+    @Log(title = "推广员申请", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserPromoterApplyScrm fsUserPromoterApply)
+    {
+        return toAjax(fsUserPromoterApplyService.insertFsUserPromoterApply(fsUserPromoterApply));
+    }
+
+    /**
+     * 修改推广员申请
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:edit')")
+    @Log(title = "推广员申请", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserPromoterApplyScrm fsUserPromoterApply)
+    {
+        return toAjax(fsUserPromoterApplyService.updateFsUserPromoterApply(fsUserPromoterApply));
+    }
+
+    /**
+     * 删除推广员申请
+     */
+    @PreAuthorize("@ss.hasPermi('store:userPromoterApply:remove')")
+    @Log(title = "推广员申请", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{applyIds}")
+    public AjaxResult remove(@PathVariable Long[] applyIds)
+    {
+        return toAjax(fsUserPromoterApplyService.deleteFsUserPromoterApplyByIds(applyIds));
+    }
+}

+ 28 - 0
fs-admin/src/main/java/com/fs/task/ComprehensiveStatisticsTask.java

@@ -0,0 +1,28 @@
+package com.fs.task;
+
+import com.fs.company.service.IStatisticManageService;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/11/3 下午4:12
+ */
+@AllArgsConstructor
+@Component("comprehensiveStatisticsTask")
+public class ComprehensiveStatisticsTask {
+
+    @Resource
+    private final IStatisticManageService iStatisticManageService;
+
+    /**
+     * 每隔一小时执行一次
+     */
+    public void execute() {
+        iStatisticManageService.executeTask();
+    }
+
+}

+ 54 - 2
fs-admin/src/main/java/com/fs/task/FsCompanyTask.java

@@ -1,21 +1,34 @@
 package com.fs.task;
 
+import com.fs.common.constant.FsConstants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.company.domain.Company;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.vo.RedPacketMoneyVO;
 import com.fs.course.mapper.FsCourseRedPacketLogMapper;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Component;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @AllArgsConstructor
 @Component("companyTask")
+@Slf4j
 public class FsCompanyTask {
 
+    private final RedisCache redisCache;
     private FsCourseRedPacketLogMapper fsCourseRedPacketLogMapper;
     private ICompanyService companyService;
 
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
     public void refreshCompanyMoney() {
         LocalDateTime now = LocalDateTime.now();
         // 获取上一个小时的开始时间
@@ -31,4 +44,43 @@ public class FsCompanyTask {
             companyService.subtractCompanyMoneyHourse(redPacketMoneyVO.getMoney(), redPacketMoneyVO.getCompanyId(), startTime.toLocalTime(), endTime.toLocalTime());
         }
     }
-}
+
+    /**
+     * 同步公司缓存余额到公司数据表
+     */
+    public void syncRedisCompanyMoneyToDB(){
+            // 获取所有的公司余额key
+            String companyMoneyKeyAll = FsConstants.COMPANY_MONEY_KEY + "*";
+            Collection<String> keys = redisCache.keys(companyMoneyKeyAll);
+
+            if (keys != null && !keys.isEmpty()) {
+                log.info("同步缓存余额到公司表,keys:{}", keys);
+                List<Object> values = redisTemplate.opsForValue().multiGet(keys);
+                Iterator<String> keyIterator = keys.iterator();
+                if(values != null && !values.isEmpty()){
+                    Iterator<Object> valueIterator = values.iterator();
+                    List<Company> companies = companyService.selectCompanyList(new Company());
+
+                    Map<Long, BigDecimal> moneyMap = new HashMap<>();
+                    while (keyIterator.hasNext() && valueIterator.hasNext()) {
+                        String next = keyIterator.next();
+                        String[] keySplit = next.split(":");
+                        Long companyId = Long.parseLong(keySplit[2]);
+                        String value = valueIterator.next().toString();
+                        moneyMap.put(companyId, new BigDecimal(value));
+                    }
+
+                    // 使用Stream进行匹配赋值
+                    List<Company> collect = companies.stream()
+                            .filter(company -> moneyMap.containsKey(company.getCompanyId()))
+                            .peek(company -> company.setMoney(moneyMap.get(company.getCompanyId()))).collect(Collectors.toList());
+
+                    // 保存公司余额
+                    if(!collect.isEmpty()){
+                        companyService.batchUpdateCompany(collect);
+                    }
+                }
+            }
+
+    }
+}

+ 52 - 0
fs-common/src/main/java/com/fs/common/enums/DimensionEnum.java

@@ -0,0 +1,52 @@
+package com.fs.common.enums;
+
+/**
+ * @description: 统计维度枚举
+ * @author: Guos
+ * @time: 2025/11/3 上午11:26
+ */
+public enum DimensionEnum {
+
+    /**
+     * 个人维度
+     */
+    PERSONAL(1, "个人"),
+
+    /**
+     * 公司维度
+     */
+    COMPANY(2, "公司"),
+
+    /**
+     * 部门维度
+     */
+    DEPARTMENT(3, "部门");
+
+    private final Integer value;
+    private final String description;
+
+    DimensionEnum(Integer value, String description) {
+        this.value = value;
+        this.description = description;
+    }
+
+    public Integer getValue() {
+        return value;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * 根据值获取枚举
+     */
+    public static DimensionEnum fromValue(Integer value) {
+        for (DimensionEnum dimension : DimensionEnum.values()) {
+            if (dimension.getValue().equals(value)) {
+                return dimension;
+            }
+        }
+        return null;
+    }
+}

+ 5 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -532,7 +532,11 @@ public class CompanyUserController extends BaseController {
                                         @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
         Map<String,Object> params = new HashMap<>();
         params.put("nickName", name);
-
+        //查询多条数据传入公司
+        if (pageSize>=200){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            params.put("companyId", loginUser.getCompany().getCompanyId());
+        }
         PageHelper.startPage(pageNum, pageSize);
         List<OptionsVO> companyUserList = companyUserService.selectCompanyUserListByMap(params);
         return R.ok().put("data", new PageInfo<>(companyUserList));

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

@@ -82,6 +82,7 @@ public class FsCourseFinishTempController extends BaseController
     {
         LoginUser loginUser = SecurityUtils.getLoginUser();
         fsCourseFinishTemp.setCompanyId(loginUser.getCompany().getCompanyId());
+        fsCourseFinishTemp.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
         return toAjax(fsCourseFinishTempService.insertFsCourseFinishTemp(fsCourseFinishTemp));
     }
 

+ 118 - 5
fs-company/src/main/java/com/fs/company/controller/course/FsCourseFinishTempParentController.java

@@ -1,7 +1,11 @@
 package com.fs.company.controller.course;
 
+import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 
+import com.fs.company.service.impl.CompanyDeptServiceImpl;
+import com.fs.company.service.impl.CompanyUserServiceImpl;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.security.SecurityUtils;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -25,7 +29,7 @@ import com.fs.common.core.page.TableDataInfo;
 
 /**
  * 完课模板Controller
- * 
+ *
  * @author 吴树波
  * @date 2025-05-22
  */
@@ -36,6 +40,12 @@ public class FsCourseFinishTempParentController extends BaseController
     @Autowired
     private IFsCourseFinishTempParentService fsCourseFinishTempParentService;
 
+    @Autowired
+    private CompanyDeptServiceImpl companyDeptService;
+
+    @Autowired
+    private CompanyUserServiceImpl companyUserService;
+
     /**
      * 查询完课模板列表
      */
@@ -50,6 +60,55 @@ public class FsCourseFinishTempParentController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询我创建的完课模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:myList')")
+    @GetMapping("/myList")
+    public TableDataInfo myList(FsCourseFinishTempParent fsCourseFinishTempParent)
+    {
+        startPage();
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsCourseFinishTempParent.setCompanyId(loginUser.getCompany().getCompanyId());
+        fsCourseFinishTempParent.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
+        List<FsCourseFinishTempParent> list = fsCourseFinishTempParentService.selectFsCourseFinishTempParentList(fsCourseFinishTempParent);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询部门的创建的完课模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:deptList')")
+    @GetMapping("/deptList")
+    public TableDataInfo deptList(FsCourseFinishTempParent fsCourseFinishTempParent)
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsCourseFinishTempParent.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        List<Long> combinedList = new ArrayList<>();
+        //本部门
+        Long deptId = loginUser.getUser().getDeptId();
+        if (deptId!=null){
+            combinedList.add(deptId);
+        }
+        //本部门的下级部门
+        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+        if (!deptList.isEmpty()){
+            combinedList.addAll(deptList);
+        }
+
+        List<Long> userIds = companyUserService.selectCompanyQwUserByDept(deptList, loginUser.getUser().getUserType());
+        if (userIds.isEmpty()){
+            return getDataTable(new ArrayList<>());
+        }
+
+        fsCourseFinishTempParent.setUserIds(userIds);
+
+        startPage();
+        List<FsCourseFinishTempParent> list = fsCourseFinishTempParentService.selectFsCourseFinishTempParentList(fsCourseFinishTempParent);
+        return getDataTable(list);
+    }
+
     /**
      * 导出完课模板列表
      */
@@ -58,15 +117,67 @@ public class FsCourseFinishTempParentController extends BaseController
     @GetMapping("/export")
     public AjaxResult export(FsCourseFinishTempParent fsCourseFinishTempParent)
     {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsCourseFinishTempParent.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<FsCourseFinishTempParent> list = fsCourseFinishTempParentService.selectFsCourseFinishTempParentList(fsCourseFinishTempParent);
+        ExcelUtil<FsCourseFinishTempParent> util = new ExcelUtil<FsCourseFinishTempParent>(FsCourseFinishTempParent.class);
+        return util.exportExcel(list, "完课模板数据");
+    }
+
+    /**
+     * 导出我的完课模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:myExport')")
+    @Log(title = "完课模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/myExport")
+    public AjaxResult myExport(FsCourseFinishTempParent fsCourseFinishTempParent)
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsCourseFinishTempParent.setCompanyId(loginUser.getCompany().getCompanyId());
+        fsCourseFinishTempParent.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
         List<FsCourseFinishTempParent> list = fsCourseFinishTempParentService.selectFsCourseFinishTempParentList(fsCourseFinishTempParent);
         ExcelUtil<FsCourseFinishTempParent> util = new ExcelUtil<FsCourseFinishTempParent>(FsCourseFinishTempParent.class);
         return util.exportExcel(list, "完课模板数据");
     }
 
+    /**
+     * 导出部门完课模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:myExport')")
+    @Log(title = "完课模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/deptExport")
+    public AjaxResult deptExport(FsCourseFinishTempParent fsCourseFinishTempParent)
+    {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+
+        List<Long> combinedList = new ArrayList<>();
+        //本部门
+        Long deptId = loginUser.getUser().getDeptId();
+        if (deptId!=null){
+            combinedList.add(deptId);
+        }
+        //本部门的下级部门
+        List<Long> deptList = companyDeptService.selectCompanyDeptByParentId(deptId);
+        if (!deptList.isEmpty()){
+            combinedList.addAll(deptList);
+        }
+
+        List<Long> userIds = companyUserService.selectCompanyQwUserByDept(deptList, loginUser.getUser().getUserType());
+        if (userIds.isEmpty()){
+            return AjaxResult.error();
+        }
+
+        fsCourseFinishTempParent.setUserIds(userIds);
+        List<FsCourseFinishTempParent> list = fsCourseFinishTempParentService.selectFsCourseFinishTempParentList(fsCourseFinishTempParent);
+        ExcelUtil<FsCourseFinishTempParent> util = new ExcelUtil<FsCourseFinishTempParent>(FsCourseFinishTempParent.class);
+        return util.exportExcel(list, "完课模板数据");
+    }
+
+
     /**
      * 获取完课模板详细信息
      */
-    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:query')")
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:query') || @ss.hasPermi('course:courseFinishTempParent:myQuery') || @ss.hasPermi('course:courseFinishTempParent:deptQuery')")
     @GetMapping(value = "/{id}")
     public AjaxResult getInfo(@PathVariable("id") Long id)
     {
@@ -76,20 +187,22 @@ public class FsCourseFinishTempParentController extends BaseController
     /**
      * 新增完课模板
      */
-    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:add')")
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:add') || @ss.hasPermi('course:courseFinishTempParent:myAdd') || @ss.hasPermi('course:courseFinishTempParent:deptAdd')")
     @Log(title = "完课模板", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody FsCourseFinishTempParent fsCourseFinishTempParent){
 
         LoginUser loginUser = SecurityUtils.getLoginUser();
         fsCourseFinishTempParent.setCompanyId(loginUser.getCompany().getCompanyId());
+        fsCourseFinishTempParent.setCreateTime(new Date());
+        fsCourseFinishTempParent.setCreateBy(String.valueOf(loginUser.getUser().getUserId()));
         return toAjax(fsCourseFinishTempParentService.insertFsCourseFinishTempParent(fsCourseFinishTempParent));
     }
 
     /**
      * 修改完课模板
      */
-    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:edit')")
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:edit') || @ss.hasPermi('course:courseFinishTempParent:myEdit') || @ss.hasPermi('course:courseFinishTempParent:deptEdit')")
     @Log(title = "完课模板", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody FsCourseFinishTempParent fsCourseFinishTempParent)
@@ -102,7 +215,7 @@ public class FsCourseFinishTempParentController extends BaseController
     /**
      * 删除完课模板
      */
-    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:remove')")
+    @PreAuthorize("@ss.hasPermi('course:courseFinishTempParent:remove') || @ss.hasPermi('course:courseFinishTempParent:myRemove') || @ss.hasPermi('course:courseFinishTempParent:deptRemove')")
     @Log(title = "完课模板", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)

+ 10 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsUserCourseVideoController.java

@@ -86,7 +86,17 @@ public class FsUserCourseVideoController extends BaseController
         }
         return toAjax(fsUserCourseVideoService.insertFsUserCourseVideo(fsUserCourseVideo));
     }
+    /**
+     * 更新课堂视频
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseVideo:update')")
+    @Log(title = "更新课堂视频", businessType = BusinessType.UPDATE)
+    @PostMapping("/update")
+    public AjaxResult update(@RequestBody FsUserCourseVideo fsUserCourseVideo)
+    {
 
+        return toAjax(fsUserCourseVideoService.updateFsUserCourseVideo(fsUserCourseVideo));
+    }
     /**
      * 修改课堂视频
      */

+ 19 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwDeptController.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
@@ -120,4 +121,22 @@ public class QwDeptController extends BaseController
     {
         return toAjax(qwDeptService.deleteQwDeptByIds(ids));
     }
+
+    /**
+     * @Description: 获取企微部门 按Treeselect返回 每一个企微主体有自己的部门,按企微主体查询
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/30 9:33
+     */
+    @GetMapping("/treeselect")
+    public AjaxResult treeselect(QwDept qwDept)
+    {
+        if(StringUtils.isEmpty(qwDept.getCorpId())){
+            return AjaxResult.error("请选择企微主体");
+        }
+        List<QwDept> depts = qwDeptService.selectQwDeptList(qwDept);
+        return AjaxResult.success(qwDeptService.buildDeptTreeSelect(depts));
+    }
+
 }

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

@@ -113,13 +113,43 @@ public class QwUserController extends BaseController
     @PreAuthorize("@ss.hasPermi('qw:user:staffList')")
     @GetMapping("/staffList")
     public TableDataInfo staffList(QwUserListParam qwUser) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+
+        // 添加企微部门查询条件
+        Long deptId = qwUser.getDeptId();
+        if(deptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (deptId!=null){
+                qwDeptIdList.add(deptId);
+            }
+            // 本部门的下级部门
+            List<Long> deptList = qwUserService.selectDeptByParentId(deptId,qwUser.getCorpId());
+            if (!deptList.isEmpty()){
+                qwDeptIdList.addAll(deptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+        startPage();
+        List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询我的企微员工列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:user:myStaffList')")
+    @GetMapping("/myStaffList")
+    public TableDataInfo myStaffList(QwUserListParam qwUser) {
         startPage();
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         qwUser.setCompanyId(loginUser.getCompany().getCompanyId());
+        qwUser.setCompanyUserId(loginUser.getUser().getUserId());
         List<QwUserVO> list = qwUserService.selectQwUserListStaffVO(qwUser);
         return getDataTable(list);
     }
 
+
     /**
      * 导出企微员工列表
      * @param qwUser
@@ -173,6 +203,23 @@ public class QwUserController extends BaseController
         if (!deptList.isEmpty()){
             combinedList.addAll(deptList);
         }
+
+        // 添加企微部门查询条件
+        Long qwDeptId = qwUser.getDeptId();
+        if(qwDeptId!=null && qwUser.getCorpId()!=null){
+            List<Long> qwDeptIdList = new ArrayList<>();
+            if (qwDeptId!=null){
+                qwDeptIdList.add(qwDeptId);
+            }
+            // 本部门的下级部门
+            List<Long> qwDeptList = qwUserService.selectDeptByParentId(qwDeptId,qwUser.getCorpId());
+            if (!qwDeptList.isEmpty()){
+                qwDeptIdList.addAll(qwDeptList);
+            }
+            qwUser.setQwDeptIdList(qwDeptIdList);
+        }
+
+
         qwUser.setCuDeptIdList(combinedList);
         qwUser.setUserType(loginUser.getUser().getUserType());
 

+ 5 - 0
fs-ipad-task/pom.xml

@@ -105,6 +105,11 @@
             <groupId>com.fs</groupId>
             <artifactId>fs-service</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 1 - 1
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -334,7 +334,7 @@ public class IpadSendServer {
     public void send(QwSopCourseFinishTempSetting.Setting content, QwUser qwUser, QwSopLogs qwSopLogs, Map<String, FsCoursePlaySourceConfig> miniMap, BaseVo parentVo) {
         BaseVo vo = new BaseVo();
         vo.setId(Long.parseLong(qwSopLogs.getId()));
-        vo.setRoom(qwSopLogs.getSendType() == 12);
+        vo.setRoom(qwSopLogs.getSendType() == 6 || qwSopLogs.getSendType() == 12);
         vo.setUuid(qwUser.getUid());
         vo.setExId(qwSopLogs.getExternalUserId());
         vo.setServerId(qwUser.getServerId());

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

@@ -90,12 +90,14 @@ public class SendMsg {
         if (qwUserList.isEmpty()) {
             List<QwIpadServer> serverList = qwIpadServerMapper.selectList(new QueryWrapper<QwIpadServer>().eq("group_no", groupNo));
             if (serverList.isEmpty()) {
+                log.info("没找到可用的服务器 {} ",serverList);
                 return new ArrayList<>();
             }
             List<Long> serverIds = PubFun.listToNewList(serverList, QwIpadServer::getId);
             List<QwUser> qwUsers = qwUserMapper.selectList(new QueryWrapper<QwUser>().eq("send_msg_type", 1).eq("server_status", 1).eq("ipad_status", 1).in("server_id", serverIds));
             qwUserList.addAll(qwUsers);
         }
+        log.info("getQwUserList {}",JSON.toJSONString(qwUserList));
         return qwUserList;
     }
 
@@ -169,6 +171,7 @@ public class SendMsg {
         // 获取当前企微待发送记录
         List<QwSopLogs> qwSopLogList = qwSopLogsMapper.selectByQwUserId(qwUser.getId());
         if (qwSopLogList.isEmpty()) {
+            log.info("获取当前企微待发送记录为空");
             return;
         }
         // 获取企微用户
@@ -178,6 +181,7 @@ public class SendMsg {
         long end1 = System.currentTimeMillis();
         // 判断这个企微是否需要发送
         if (!sendServer.isSend(user, parentVo)) {
+            log.info("当前这个企微不需要发送 数据{}",user);
             return;
         }
         log.info("销售:{}, 消息:{}, 耗时: {}, 时间:{}", user.getQwUserName(), qwSopLogList.size(), end1 - start1, qwMap.get(qwUser.getId()));

+ 52 - 0
fs-ipad-task/src/test/java/com/fs/app/task/SendMsgTest.java

@@ -0,0 +1,52 @@
+package com.fs.app.task;
+
+import com.fs.FsIpadTaskApplication;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.tag.service.FsTagUpdateService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = FsIpadTaskApplication.class)
+@RequiredArgsConstructor
+@Slf4j
+public class SendMsgTest {
+    @Autowired
+    private SendMsg sendMsg;
+
+    @Autowired
+    private FsTagUpdateService fsTagUpdateService;
+
+    @Autowired
+    private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+
+    @Test
+    public void sendMsg2() {
+        sendMsg.sendMsg2();
+    }
+
+    @Test
+    public void testLogWrite(){
+        List<Long> testLogIds = Arrays.asList(177L,180L,182L,183L,184L,185L);
+
+        for(Long logId : testLogIds){
+            FsCourseWatchLog fsCourseWatchLog = fsCourseWatchLogMapper.selectFsCourseWatchLogByLogId(logId);
+            fsTagUpdateService.onCourseWatchingBatch(Collections.singletonList(fsCourseWatchLog));
+        }
+    }
+
+    @Test
+    public void handleData(){
+        fsTagUpdateService.handleData();
+    }
+}

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

@@ -376,8 +376,8 @@ public class QwMsgController {
                         log.info("id:{}, 客户发送", id);
                         aiHookService.qwHookNotifyAiReply(id,sender,content,wxWorkMsgResp.getUuid(),wxWorkMessageDTO.getMsgtype());
                     }else {
-                        log.info("id:{}, 销售发送", id);
-                        aiHookService.qwHookNotifyAddMsg(id,receiver,content,wxWorkMsgResp.getUuid());
+                        log.info("销售发送");
+                        aiHookService.qwHookNotifyAddMsgNew(id,receiver,content,wxWorkMsgResp.getUuid(),1);
                     }
 
                 }

+ 39 - 6
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -26,6 +26,8 @@ import com.google.gson.JsonParser;
 import com.tencent.wework.Finance;
 import lombok.extern.slf4j.Slf4j;
 import org.json.JSONObject;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
@@ -73,6 +75,9 @@ public class QwDataCallbackService {
     @Autowired
     IQwAutoTagsService qwAutoTagsService;
 
+    @Autowired
+    private RedissonClient redissonClient;
+
     @Autowired
     IQwAutoTagsLogsService qwAutoTagsLogsService;
 
@@ -202,13 +207,41 @@ public class QwDataCallbackService {
                             if(WelcomeCodeList.getLength() > 0) {
                                 WelcomeCode = WelcomeCodeList.item(0).getTextContent();
                             }
-
-                            String qwApiExternal=redisCache.getCacheObject("qwApiExternal:"+root.getElementsByTagName("UserID").item(0).getTextContent()+":"+corpId+":"+root.getElementsByTagName("ExternalUserID").item(0).getTextContent());
-                            if (StringUtil.strIsNullOrEmpty(qwApiExternal)){
-                                redisCache.setCacheObject("qwApiExternal:"+root.getElementsByTagName("UserID").item(0).getTextContent()+":"+corpId+":"+root.getElementsByTagName("ExternalUserID").item(0).getTextContent() ,"1",10, TimeUnit.MINUTES);
-                                qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
-
+                            String userId = root.getElementsByTagName("UserID").item(0).getTextContent();
+                            String externalUserId = root.getElementsByTagName("ExternalUserID").item(0).getTextContent();
+                            String cacheKey = "qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId;
+                            String lockKey = "lock:qwApiExternal:" + userId + ":" + corpId + ":" + externalUserId; // 锁Key(Hash类型,加前缀lock:)
+
+                            // 2. 获取 Redisson 分布式锁
+                            RLock lock = redissonClient.getLock(lockKey);
+                            boolean isLocked = false;
+                            try {
+                                // 3. 尝试加锁:最多等待 5 秒,锁自动释放时间 15 分钟
+                                isLocked = lock.tryLock(5, 15, TimeUnit.MINUTES);
+                                if (isLocked) {
+                                    // 4. 加锁成功后,再次检查缓存(避免多线程竞争时重复执行业务)
+                                    String qwApiExternal = redisCache.getCacheObject(cacheKey);
+                                    if (StringUtil.strIsNullOrEmpty(qwApiExternal)) {
+                                        try {
+                                            // 5. 新增用户
+                                            qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
+                                            // 6. 业务逻辑执行成功后,写入 Redis 缓存(有效期 10 分钟)
+                                            redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
+                                        } catch (Exception e) {
+                                            // 7. 业务逻辑失败时,删除缓存
+                                            redisCache.deleteObject(cacheKey);
+                                        }
+                                    }
+                                }
+                            } catch (InterruptedException e) {
+                                logger.error("中断异常");
+                            } finally {
+                                // 4. 确保锁最终被释放(只有加锁成功的线程才需要释放)
+                                if (isLocked && lock.isHeldByCurrentThread()) {
+                                    lock.unlock();
+                                }
                             }
+
                             break;
                         case "edit_external_contact":
                             qwExternalContactService.updateQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),corpId);

+ 18 - 12
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -714,23 +714,29 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             QwGroupChat groupChat = groupChatMap.get(logVo.getChatId());
             ruleTimeVO.setSendType(6);
             ruleTimeVO.setType(2);
-            if (content.getIndex() == 0) {
+            if (groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()) {
                 QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
                 handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
                         type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
                         null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
-            } else {
-                if(groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()){
-                    groupChat.getChatUserList().forEach(user -> {
-                        ruleTimeVO.setSendType(2);
-                        ruleTimeVO.setRemark("客户群催课");
-                        QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, user.getUserId(), user.getName(), null, isOfficial, null);
-                        handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
-                                type, qwUserId, companyUserId, companyId, user.getId().toString(), welcomeText, qwUserName,
-                                null, false, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
-                    });
-                }
             }
+//            if (content.getIndex() == 0) {
+//                QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, groupChat.getChatId(), groupChat.getName(), null, isOfficial, null);
+//                handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
+//                        type, qwUserId, companyUserId, companyId, groupChat.getChatId(), welcomeText, qwUserName,
+//                        null, true, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
+//            } else {
+//                if(groupChat.getChatUserList() != null && !groupChat.getChatUserList().isEmpty()){
+//                    groupChat.getChatUserList().forEach(user -> {
+//                        ruleTimeVO.setSendType(2);
+//                        ruleTimeVO.setRemark("客户群催课");
+//                        QwSopLogs sopLogs = createBaseLog(formattedSendTime, logVo, ruleTimeVO, user.getUserId(), user.getName(), null, isOfficial, null);
+//                        handleLogBasedOnType(sopLogs, content, logVo, sendTime, courseId, videoId,
+//                                type, qwUserId, companyUserId, companyId, user.getId().toString(), welcomeText, qwUserName,
+//                                null, false, miniAppId, groupChat,config, miniMap, null, sendMsgType,companies);
+//                    });
+//                }
+//            }
         } else {
             // 处理每个 externalContactId
             sopUserLogsInfos.forEach(contactId -> {

+ 48 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyDeptUserInfo.java

@@ -0,0 +1,48 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/30 上午9:40
+ */
+@Data
+public class CompanyDeptUserInfo {
+
+   /**
+    * 公司id
+    */
+   private Long companyId;
+
+   /**
+    * 公司名称
+    */
+   private String companyName;
+
+   /**
+    * 部门id
+    */
+   private Long deptId;
+
+   /**
+    * 部门名称
+    */
+   private String deptName;
+
+   /**
+    * 员工id
+    */
+   private Long userId;
+
+   /**
+    * 员工名称
+    */
+   private String userName;
+
+   /**
+    * 员工昵称
+    */
+   private String nickName;
+
+}

+ 50 - 0
fs-service/src/main/java/com/fs/company/domain/ComprehensiveDailyStats.java

@@ -0,0 +1,50 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/11/3 下午4:58
+ */
+@Data
+public class ComprehensiveDailyStats {
+
+    private Long id;                     // 记录ID
+
+    private Long companyId;              // 公司ID
+
+    private String companyName;          // 公司名称
+
+    private Long deptId;                 // 部门ID
+
+    private String deptName;             // 部门名称
+
+    private Long userId;                 // 用户ID
+
+    private String userName;             // 用户名
+
+    private String nickName;             // 用户昵称
+
+    private Date statisticsTime;    // 统计日期
+
+    private Integer lineNum;             // t1统计数(线路数)
+
+    private Integer activeNum;           // t2统计数(活跃数)
+
+    private Integer completeNum;         // 完成数(t4)
+
+    private Integer answerNum;           // 答题数(t5)
+
+    private Integer redPacketNum;        // 红包数量(t6)
+
+    private BigDecimal redPacketAmount;  // 红包金额
+
+    private Date createTime;    // 记录创建时间
+
+    private Date updateTime;    // 记录更新时间
+
+}

+ 57 - 0
fs-service/src/main/java/com/fs/company/dto/CompanyDeptUserInfoDTO.java

@@ -0,0 +1,57 @@
+package com.fs.company.dto;
+
+import lombok.Data;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/30 上午10:38
+ */
+@Data
+public class CompanyDeptUserInfoDTO {
+
+    /**
+     * 公司id
+     */
+    private Long companyId;
+
+    /**
+     * 公司名称
+     */
+    private String companyName;
+
+    /**
+     * 部门id
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 进线数量
+     */
+    private Integer lineNum;
+
+    /**
+     * 激活数
+     */
+    private Integer activeNum;
+
+    /**
+     * 完课数量
+     */
+    private Integer completeNum;
+
+    /**
+     * 答题数量
+     */
+    private Integer answerNum;
+
+    /**
+     * 红包数量
+     */
+    private Integer redPacketNum;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/company/dto/ComprehensiveStatisticsDTO.java

@@ -0,0 +1,42 @@
+package com.fs.company.dto;
+
+import lombok.Data;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/11/3 下午3:24
+ */
+@Data
+public class ComprehensiveStatisticsDTO {
+
+    /**
+     * 日期
+     */
+    private String dateStr;
+
+    /**
+     * 进线数量
+     */
+    private Long lineNum;
+
+    /**
+     * 激活数
+     */
+    private Long activeNum;
+
+    /**
+     * 完课数量
+     */
+    private Long completeNum;
+
+    /**
+     * 答题数量
+     */
+    private Long answerNum;
+
+    /**
+     * 红包数量
+     */
+    private Long redPacketNum;
+}

+ 7 - 1
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -155,7 +155,7 @@ public interface CompanyMapper
 
     @Select({"<script> " +
             "select c.*,cu.user_name,qu.used_num FROM company c LEFT JOIN company_user cu ON c.user_id =cu.user_id  " +
-            "LEFT JOIN (select company_id, count(id) as used_num from qw_user where server_id is not null group by company_id) qu ON qu.company_id = c.company_id " +
+            "LEFT JOIN (select company_id, count(id) as used_num from qw_user where server_id is not null and server_status = 1 group by company_id) qu ON qu.company_id = c.company_id " +
             "where c.is_del=0 " +
             "            <if test=\"companyName != null  and companyName != ''\"> and c.company_name like concat('%', #{companyName}, '%')</if>\n" +
             "            <if test=\"companyMobile != null  and companyMobile != ''\"> and c.company_mobile = #{companyMobile}</if>\n" +
@@ -207,4 +207,10 @@ public interface CompanyMapper
     List<OptionsVO> getCompanyListByCorpId(@Param("corpId") String corpId);
 
     List<Company> selectCompanyMoneyAllList();
+
+    /**
+     * 批量修改公司余额
+     * @param list
+     */
+    void batchUpdateCompany(@Param("list") List<Company> list);
 }

+ 12 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -284,4 +284,16 @@ public interface CompanyUserMapper
 
 
     List<String> selectCompanyUserNameByIdsList(@Param("companyUserIDs")List<Long> companyUserID);
+
+    @Select("<script>" +
+            "SELECT user_id FROM company_user WHERE 1=1 " +
+            "<if test=\"companyUserIDs != null and companyUserIDs.size() > 0 and userType != '00'\">" +
+            "   AND dept_id IN " +
+            "   <foreach collection='companyUserIDs' item='item' open='(' separator=',' close=')'>" +
+            "       #{item}" +
+            "   </foreach>" +
+            "</if>" +
+            "</script>")
+    List<Long> selectCompanyQwUserByDept(@Param("companyUserIDs") List<Long> companyUserIDs, @Param("userType") String userType);
+
 }

+ 38 - 0
fs-service/src/main/java/com/fs/company/mapper/StatisticManageMapper.java

@@ -0,0 +1,38 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyDeptUserInfo;
+import com.fs.company.domain.ComprehensiveDailyStats;
+import com.fs.company.dto.CompanyDeptUserInfoDTO;
+import com.fs.company.dto.ComprehensiveStatisticsDTO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/30 上午9:25
+ */
+public interface StatisticManageMapper {
+
+    //插入数据
+    Integer insert(ComprehensiveDailyStats comprehensiveDailyStats);
+
+    //获取公司、部门、员工信息
+    List<CompanyDeptUserInfo> getCompanyAndDeptAndDeptUserList(@Param("companyId") Long companyId);
+
+    List<CompanyDeptUserInfo> getCompanyInfo();
+
+
+    CompanyDeptUserInfoDTO getStatisticNum(@Param("userIds") Long userIds);
+
+    /**
+     * 按照个人统计
+     * @param dimension 维度
+     * @param startTime
+     * @param endTime
+     * @param userIds
+     */
+    List<ComprehensiveStatisticsDTO> getStatisticNumByPersonal(@Param("dimension")Integer dimension, @Param("startTime") Date startTime, @Param("endTime") Date endTime, @Param("userIds") Long... userIds);
+}

+ 8 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyRechargeService.java

@@ -4,6 +4,7 @@ import java.math.BigDecimal;
 import java.util.List;
 
 import com.fs.common.core.domain.R;
+import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyRecharge;
 import com.fs.company.domain.CompanyRechargeOrder;
 import com.fs.company.dto.RechargeDTO;
@@ -50,6 +51,13 @@ public interface ICompanyRechargeService
      */
     public int updateCompanyRecharge(CompanyRecharge companyRecharge);
 
+    /**
+     * 同步到缓存余额
+     * @param company 公司
+     * @param rechargeMoney 充值金额
+     * @return 是否成功
+     */
+    R syncUpdateRedisCompanyRecharge(Company company, BigDecimal rechargeMoney);
 
     List<CompanyRechargeVO> selectCompanyRechargeListVO(CompanyRechargeVO companyRecharge);
     /**

+ 6 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyService.java

@@ -174,4 +174,10 @@ public interface ICompanyService
     void subtractCompanyMoneyHourse(BigDecimal money, Long companyId, LocalTime start, LocalTime end);
 
     void syncCompanyBalance();
+
+    /**
+     * 批量更新公司余额
+     * @param list 公司列表
+     */
+    void batchUpdateCompany(List<Company> list);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java

@@ -244,4 +244,6 @@ public interface ICompanyUserService {
      * @param batchUserRolesVO 批量修改角色参数
      */
     R updateBatchUserRoles(BatchUserRolesVO batchUserRolesVO);
+
+    List<Long> selectCompanyQwUserByDept(List<Long> deptList,String userType);
 }

+ 21 - 0
fs-service/src/main/java/com/fs/company/service/IStatisticManageService.java

@@ -0,0 +1,21 @@
+package com.fs.company.service;
+
+import com.fs.statis.param.ComprehensiveStatisticsParam;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/30 上午9:21
+ */
+public interface IStatisticManageService {
+
+    List<Map<String, Object>> statisticMain(ComprehensiveStatisticsParam param);
+
+    /**
+     * 用来执行定时任务
+     */
+    void executeTask();
+}

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyRechargeServiceImpl.java

@@ -1,12 +1,17 @@
 package com.fs.company.service.impl;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.constant.FsConstants;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.company.constant.PaymentStatus;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyMoneyLogs;
@@ -19,7 +24,10 @@ import com.fs.company.service.CompanyRechargeOrderService;
 import com.fs.company.vo.CompanyRechargeExportVO;
 import com.fs.company.vo.CompanyRechargeVO;
 import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 import com.fs.company.mapper.CompanyRechargeMapper;
 import com.fs.company.domain.CompanyRecharge;
@@ -45,6 +53,12 @@ public class CompanyRechargeServiceImpl implements ICompanyRechargeService
     private CompanyMapper companyMapper;
     @Autowired
     private CompanyRechargeOrderService companyRechargeOrderService;
+    @Qualifier("redissonClient")
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    private RedisCache redisCache;
+
     /**
      * 查询充值
      *
@@ -93,6 +107,54 @@ public class CompanyRechargeServiceImpl implements ICompanyRechargeService
     {
         return companyRechargeMapper.updateCompanyRecharge(companyRecharge);
     }
+
+    @Override
+    public R syncUpdateRedisCompanyRecharge(Company company, BigDecimal rechargeMoney) {
+        if(company.getCompanyId() == null){
+            log.error("公司充值-审核-同步更新到缓存,参数错误,公司id:{}, 充值余额:{}", company.getCompanyId(), rechargeMoney);
+            return R.error("公司id为空");
+        }
+
+        // 公司红包真实余额key
+        String companyMoneyKey = FsConstants.COMPANY_MONEY_KEY + company.getCompanyId();
+
+        // 加锁,与看课发放红包的加锁保持一致
+        RLock lock = redissonClient.getLock(FsConstants.COMPANY_MONEY_LOCK + company.getCompanyId());
+        boolean lockAcquired = false;
+        try {
+            BigDecimal newMoney = company.getMoney();
+
+            // 尝试加锁
+            lockAcquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
+            if (lockAcquired) {
+                BigDecimal redisMoney;
+                // 获取当前余额
+                String moneyStr = redisCache.getCacheObject(companyMoneyKey);
+                if (StringUtils.isNotEmpty(moneyStr)) {
+                    redisMoney = new BigDecimal(moneyStr);
+                } else {
+                    redisMoney = company.getMoney();
+                }
+
+                newMoney = redisMoney.add(rechargeMoney);
+
+                redisCache.setCacheObject(companyMoneyKey, newMoney.toString());
+            }
+            return R.ok().put("newMoney", newMoney);
+        } catch (Exception e) {
+            log.error("公司充值-审核-同步更新到缓存,参数错误,请求异常,异常信息:{}", e.getMessage(), e);
+            return R.error("审核失败,请重试");
+        } finally {
+            if (lockAcquired && lock.isHeldByCurrentThread()) {
+                try {
+                    lock.unlock();
+                } catch (IllegalMonitorStateException e) {
+                    log.warn("尝试释放非当前线程持有的锁: companyId:{}", company.getCompanyId());
+                }
+            }
+        }
+    }
+
     @Override
     public List<CompanyRechargeVO> selectCompanyRechargeListVO(CompanyRechargeVO companyRecharge) {
         return companyRechargeMapper.selectCompanyRechargeListVO(companyRecharge);

+ 7 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -434,7 +434,8 @@ public class CompanyServiceImpl implements ICompanyService
                 orderMap.setOrderId(order.getId());
                 orderMap.setTuiMoneyStatus(1);
                 storeOrderMapper.updateFsStoreOrder(orderMap);
-                BigDecimal money = order.getPayMoney().add(order.getPayRemain());
+                // order.getPayRemain() 数据库实际没有这个字段了 直接使用 应付金额
+                BigDecimal money = order.getPayPrice();
                 company.setMoney(company.getMoney().add(money));
                 companyMapper.updateCompany(company);
                 CompanyMoneyLogs log=new CompanyMoneyLogs();
@@ -1358,4 +1359,9 @@ public class CompanyServiceImpl implements ICompanyService
                 }));
     }
 
+    @Override
+    public void batchUpdateCompany(List<Company> list) {
+        companyMapper.batchUpdateCompany(list);
+    }
+
 }

+ 5 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java

@@ -1061,4 +1061,9 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         }
         return R.ok("修改成功");
     }
+
+    @Override
+    public List<Long> selectCompanyQwUserByDept(List<Long> deptList,String userType) {
+        return companyUserMapper.selectCompanyQwUserByDept(deptList,userType);
+    }
 }

+ 151 - 0
fs-service/src/main/java/com/fs/company/service/impl/StatisticManageServiceImpl.java

@@ -0,0 +1,151 @@
+package com.fs.company.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.common.enums.DimensionEnum;
+import com.fs.common.utils.bean.BeanUtils;
+import com.fs.company.domain.CompanyDeptUserInfo;
+import com.fs.company.domain.ComprehensiveDailyStats;
+import com.fs.company.dto.CompanyDeptUserInfoDTO;
+import com.fs.company.dto.ComprehensiveStatisticsDTO;
+import com.fs.company.mapper.StatisticManageMapper;
+import com.fs.company.service.IStatisticManageService;
+import com.fs.statis.param.ComprehensiveStatisticsParam;
+import com.google.common.collect.Maps;
+import lombok.extern.slf4j.Slf4j;
+import com.google.common.collect.Lists;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/30 上午9:21
+ */
+@Slf4j
+@Service
+public class StatisticManageServiceImpl implements IStatisticManageService {
+
+    @Resource
+    private StatisticManageMapper statisticManageMapper;
+
+    /**
+     * 统计
+     * 按照部门分组情况
+     * @return
+     */
+    @Override
+    public List<Map<String, Object>> statisticMain(ComprehensiveStatisticsParam param) {
+        List<Map<String, Object>> result = Lists.newArrayList();
+
+        if (param.getDimension() == DimensionEnum.PERSONAL.getValue()){
+            Assert.notNull(param.getId(), "按个人展示查询条件不能为空!");
+            List<CompanyDeptUserInfo> companyDeptdUserList = statisticManageMapper.getCompanyAndDeptAndDeptUserList(null);
+            //从所有数据中去匹配userId = param.getId的
+            Optional<CompanyDeptUserInfo> first = companyDeptdUserList.stream()
+                    .filter(e -> e.getUserId().equals(param.getId()))
+                    .findFirst();
+            if (!first.isPresent()) {
+                return result;
+            }
+            CompanyDeptUserInfo companyDeptUserInfo = first.get();
+            Map<String, Object> map = Maps.newHashMap();
+            map.put("id", companyDeptUserInfo.getUserId());
+            map.put("name", companyDeptUserInfo.getNickName());
+            List<ComprehensiveStatisticsDTO> statisticNumByPersonal =
+                    getStatisticNumByPersonal(DimensionEnum.PERSONAL.getValue(), param.getStartTime(), param.getEndTime(), param.getId());
+            map.put("list", statisticNumByPersonal);
+            result.add(map);
+            return result;
+        }
+        if(param.getDimension() == DimensionEnum.COMPANY.getValue()){
+            //按照公司统计,如果id为空就要展示全部公司,如果不为空就展示id查询的公司
+            if(ObjectUtil.isEmpty(param.getId())){
+                //得到所有公司信息
+                List<CompanyDeptUserInfo> companyInfo = statisticManageMapper.getCompanyInfo();
+                for (CompanyDeptUserInfo companyDeptUserInfo : companyInfo){
+                    getStatisticNumByPersonal(DimensionEnum.COMPANY.getValue(), param.getStartTime(), param.getEndTime(),
+                            companyDeptUserInfo.getCompanyId());
+                }
+            }
+        }
+        if(param.getDimension() == DimensionEnum.DEPARTMENT.getValue()){
+            Assert.notNull(param.getId(), "按部门展示公司不能为空!");
+            List<CompanyDeptUserInfo> companyDeptdUserList = statisticManageMapper.getCompanyAndDeptAndDeptUserList(param.getId());
+            //按照部门分组
+            Map<Long, List<CompanyDeptUserInfo>> deptInfos = companyDeptdUserList.stream()
+                    .collect(Collectors.groupingBy(CompanyDeptUserInfo::getDeptId));
+            deptInfos.forEach((deptId, companyDeptUserInfos) -> {
+                Long[] userIds = companyDeptUserInfos.stream().map(CompanyDeptUserInfo::getUserId).toArray(Long[]::new);
+                getStatisticNumByPersonal(DimensionEnum.PERSONAL.getValue(), param.getStartTime(), param.getEndTime(),userIds);
+            });
+        }
+        return null;
+    }
+
+    @Override
+    public void executeTask() {
+        List<CompanyDeptUserInfo> companyDeptdUserList = statisticManageMapper.getCompanyAndDeptAndDeptUserList(null);
+        companyDeptdUserList.forEach(companyDeptUserInfo -> {
+            CompanyDeptUserInfoDTO statisticNum =  new CompanyDeptUserInfoDTO();
+            if(null != companyDeptUserInfo.getUserId()){
+                statisticNum = statisticManageMapper.getStatisticNum(companyDeptUserInfo.getUserId());
+            }
+            ComprehensiveDailyStats comprehensiveDailyStats = component(companyDeptUserInfo, statisticNum);
+            statisticManageMapper.insert(comprehensiveDailyStats);
+        });
+    }
+
+    /**
+     * 获取统计数据
+     * @param dimension 维度
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @param userIds 用户id
+     * @return
+     */
+    public List<ComprehensiveStatisticsDTO> getStatisticNumByPersonal(Integer dimension, Date startTime, Date endTime, Long... userIds) {
+       return statisticManageMapper.getStatisticNumByPersonal(dimension, startTime, endTime, userIds);
+    }
+
+    private ComprehensiveDailyStats component(CompanyDeptUserInfo source, CompanyDeptUserInfoDTO statisticNum){
+        ComprehensiveDailyStats target = new ComprehensiveDailyStats();
+        target.setCompanyId(source.getCompanyId());
+        target.setCompanyName(source.getCompanyName());
+        target.setDeptId(source.getDeptId());
+        target.setDeptName(source.getDeptName());
+        target.setStatisticsTime(new Date());
+        target.setCreateTime(new Date());
+        target.setUpdateTime(new Date());
+        if(null != source.getUserId()){
+            target.setUserId(source.getUserId());
+            target.setUserName(source.getUserName());
+            target.setNickName(source.getNickName());
+            target.setAnswerNum(statisticNum.getAnswerNum());
+            target.setCompleteNum(statisticNum.getCompleteNum());
+            target.setLineNum(statisticNum.getLineNum());
+            target.setActiveNum(statisticNum.getActiveNum());
+            target.setRedPacketNum(statisticNum.getRedPacketNum());
+            target.setRedPacketAmount(new BigDecimal(0.00));
+        }else{
+            target.setAnswerNum(0);
+            target.setCompleteNum(0);
+            target.setLineNum(0);
+            target.setActiveNum(0);
+            target.setRedPacketNum(0);
+            target.setRedPacketAmount(new BigDecimal(0.00));
+        }
+      return target;
+    }
+
+
+}
+
+

+ 5 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseFinishTempParent.java

@@ -8,6 +8,8 @@ import lombok.Data;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.EqualsAndHashCode;
 
+import java.util.List;
+
 /**
  * 完课模板对象 fs_course_finish_temp_parent
  *
@@ -45,4 +47,7 @@ public class FsCourseFinishTempParent extends BaseEntity{
     private String companyUserIds;
     @TableField(exist = false)
     private Integer isAllCompanyUser;
+
+    @TableField(exist = false)
+    private List<Long> userIds;
 }

+ 26 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseVideo.java

@@ -113,4 +113,30 @@ public class FsUserCourseVideo extends BaseEntity
     private Long listingEndTime;//商品结束售卖时间
 
 
+    /**
+     * 看课中标签ID
+     */
+    private String watchingTagId;
+    /**
+     * 完课标签ID
+     */
+    private String watchedTagId;
+    /**
+     * 标签组ID
+     */
+    private String tagGroupId;
+
+    /**
+     * 标签组表中的ID
+     */
+    private Long tgId;
+    /**
+     * 看课标签 表中的ID
+     */
+    private Long watchingTgId;
+    /**
+     * 完课标签 表中的ID
+     */
+    private Long watchedTgId;
+
 }

+ 13 - 2
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -477,8 +477,19 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
 
     void batchUpdateFsUserWatchLog(@Param("list") List<FsCourseWatchLog> list);
 
-    @Select("select * from fs_course_watch_log where user_id = #{userId} and video_id = #{videoId} and send_type = 1 order by log_id desc limit 1")
-    FsCourseWatchLog getCourseWatchLogByUser(@Param("userId") Long userId, @Param("videoId") Long videoId);
+    @Select("<script>" +
+            "select * from fs_course_watch_log " +
+            "<where>" +
+            "   <if test='periodId != null'>and period_id = #{periodId}</if>" +
+            "   and send_type = 1" +
+            "   and user_id = #{userId}" +
+            "   and video_id = #{videoId}" +
+            "</where>" +
+            "order by log_id desc limit 1" +
+            "</script>")
+    FsCourseWatchLog getCourseWatchLogByUser(@Param("userId") Long userId,
+                                             @Param("videoId") Long videoId,
+                                             @Param("periodId") Long periodId);
 
     /**
      * 根据条件查询条数

+ 6 - 1
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -10,6 +10,7 @@ import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.param.FsUserCourseRedPageParam;
+import org.apache.ibatis.annotations.MapKey;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -145,7 +146,7 @@ public interface FsUserCourseVideoMapper
     Long selectFsUserCourseVideoByCourseSort(@Param("courseId")Long courseId, @Param("courseSort")Long courseSort);
 
 
-    @Select("select video_id dict_value, title dict_label , thumbnail  dict_img_url from fs_user_course_video where course_id=#{id} and is_del = 0 order by course_sort")
+    @Select("select video_id dict_value, title dict_label  from fs_user_course_video where course_id=#{id} and is_del = 0 order by course_sort")
     List<OptionsVO> selectFsUserCourseVodeAllList(Long id);
 
     @Select({"<script> " +
@@ -255,4 +256,8 @@ public interface FsUserCourseVideoMapper
     List<FsUserCourseVideoAppletVO> getFsUserCourseVideoAppletVOListByIds(@Param("videoIds") List<Long> videoIds);
 
     FsUserCourseVO selectFsUserCourseVideoVoByVideoIdAndCourdeId(@Param("videoId") Long videoId,@Param("courseId") Long courseId);
+
+    @Select("select video_id,is_first,course_sort,tg_id,watching_tg_id,watched_tg_id,watching_tag_id,watched_tag_id,tag_group_id from fs_user_course_video")
+    @MapKey("videoId")
+    Map<Long, FsUserCourseVideo> selectAllMap();
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/mapper/FsVideoResourceMapper.java

@@ -27,4 +27,6 @@ public interface FsVideoResourceMapper extends BaseMapper<FsVideoResource> {
 
     @Select("select * from fs_video_resource where file_key = #{fileKey} limit 1")
     FsVideoResource selectByFileKey(String fileKey);
+
+    List<FsVideoResource> selectByIds(@Param("ids") long[] ids);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempParentServiceImpl.java

@@ -1,5 +1,6 @@
 package com.fs.course.service.impl;
 
+import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -79,6 +80,8 @@ public class FsCourseFinishTempParentServiceImpl extends ServiceImpl<FsCourseFin
             temp.setCompanyId(fsCourseFinishTempParent.getCompanyId());
             temp.setCourseId(e.getCourseId());
             temp.setVideoId(e.getVideoId());
+            temp.setCreateBy(fsCourseFinishTempParent.getCreateBy());
+            temp.setCreateTime(new Date());
             temp.setCompanyUserIds(fsCourseFinishTempParent.getCompanyUserIds());
             temp.setIsAllCompanyUser(fsCourseFinishTempParent.getIsAllCompanyUser());
             temp.setParentId(fsCourseFinishTempParent.getId());

+ 41 - 2
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -48,10 +48,13 @@ import com.fs.sop.mapper.SopUserLogsMapper;
 import com.fs.store.service.cache.IFsUserCacheService;
 import com.fs.store.service.cache.IFsUserCourseCacheService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.tag.service.FsTagUpdateService;
 import com.hc.openapi.tool.util.StringUtils;
+import org.apache.commons.collections4.CollectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
@@ -89,7 +92,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private QwExternalContactMapper qwExternalContactMapper;
     @Autowired
-    RedisCache redisCache;
+    private RedisCache redisCache;
     @Autowired
     private IQwExternalContactCacheService qwExternalContactCacheService;
     @Autowired
@@ -130,6 +133,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Autowired
     private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
 
+    @Autowired
+    private FsTagUpdateService fsTagUpdateService;
+
     /**
      * 查询短链课程看课记录
      *
@@ -367,6 +373,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             String[] parts = key.split(":");
@@ -405,12 +412,17 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
+
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
     public Long getFsUserVideoDuration(Long videoId){
         //将视频时长也存到redis
@@ -439,6 +451,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Collection<String> keys = redisCache.keys("h5wxuser:watch:heartbeat:*");
         LocalDateTime now = LocalDateTime.now();
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> watchingLogs = new ArrayList<>();
         for (String key : keys) {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
             String[] parts = key.split(":");
@@ -463,11 +476,14 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redisCache.deleteObject(key);
             }else {
                 watchLog.setLogType(1);
+                watchingLogs.add(watchLog);
             }
             logs.add(watchLog);
         }
         batchUpdateFsUserCourseWatchLog(logs,100);
-
+        if(CollectionUtils.isNotEmpty(watchingLogs)){
+            fsTagUpdateService.onCourseWatchingBatch(watchingLogs);
+        }
     }
 
     @Override
@@ -490,6 +506,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             Long videoId=null;
@@ -538,6 +555,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -545,6 +564,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLogIsOpen(logs,100);
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     public void batchUpdateFsCourseWatchLogIsOpen(List<FsCourseWatchLog> logs, int batchSize) {
@@ -766,6 +790,7 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
 
         List<FsCourseWatchLog> logs = new ArrayList<>();
+        List<FsCourseWatchLog> finishedLogs = new ArrayList<>();
         for (String key : keys) {
             //取key中数据
             Long qwUserId=null;
@@ -824,6 +849,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                     redisCache.deleteObject(heartbeatKey);
                     // 完课删除看课时长记录
                     redisCache.deleteObject(key);
+
+                    finishedLogs.add(watchLog);
                 }
             }
             //集合中增加
@@ -831,6 +858,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         }
 
         batchUpdateFsCourseWatchLog(logs,100);
+
+        // 完课打标签
+        if(CollectionUtils.isNotEmpty(finishedLogs)){
+            fsTagUpdateService.onCourseWatchFinishedBatch(finishedLogs);
+        }
     }
 
     @Override
@@ -840,6 +872,8 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         Collection<String> keys = redisCache.keys("h5user:watch:heartbeat:*");
         LocalDateTime now = LocalDateTime.now();
         List<FsCourseWatchLog> logs = new ArrayList<>();
+
+        List<FsCourseWatchLog> watchingLogs = new ArrayList<>();
         for (String key : keys) {
             FsCourseWatchLog watchLog = new FsCourseWatchLog();
             //取key中数据
@@ -874,10 +908,15 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
                 redisCache.deleteObject(key);
             }else {
                 watchLog.setLogType(1);
+                watchingLogs.add(watchLog);
             }
             logs.add(watchLog);
         }
         batchUpdateFsCourseWatchLog(logs,100);
+
+        if(CollectionUtils.isNotEmpty(watchingLogs)){
+            fsTagUpdateService.onCourseWatchingBatch(watchingLogs);
+        }
     }
 
     @Override

+ 34 - 12
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -54,10 +54,7 @@ import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.domain.*;
-import com.fs.qw.mapper.QwExternalContactMapper;
-import com.fs.qw.mapper.QwGroupChatMapper;
-import com.fs.qw.mapper.QwGroupChatUserMapper;
-import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.mapper.*;
 import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwExternalContactService;
@@ -256,7 +253,10 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     @Autowired
     private IFsUserIntegralLogsService iFsUserIntegralLogsService;
-
+    @Autowired
+    private QwTagGroupMapper qwTagGroupMapper;
+    @Autowired
+    private QwTagMapper qwTagMapper;
 
 
 
@@ -922,9 +922,33 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         return R.error(400,msg).put("qrcode",contactWay);
     }
 
+
     @Override
     public List<FsUserCourseVideoVO> selectFsUserCourseVideoListByCourseIdAndCompany(FsUserCourseVideoParam fsUserCourseVideo) {
-        return fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAndCompany(fsUserCourseVideo);
+        List<FsUserCourseVideoVO> fsUserCourseVideoVOS = fsUserCourseVideoMapper.selectFsUserCourseVideoListByCourseIdAndCompany(fsUserCourseVideo);
+        for (FsUserCourseVideoVO item : fsUserCourseVideoVOS) {
+            if(ObjectUtils.isNotNull(item.getTgId())){
+                QwTagGroup qwTagGroup = qwTagGroupMapper.selectQwTagGroupById(item.getTgId());
+                if(ObjectUtils.isNotNull(qwTagGroup)){
+                    item.setTagGroupName(qwTagGroup.getName());
+                }
+            }
+
+            if(ObjectUtils.isNotNull(item.getWatchingTgId())){
+                QwTag qwTag = qwTagMapper.selectQwTagById(item.getWatchingTgId());
+                if(ObjectUtils.isNotNull(qwTag)){
+                    item.setWatchingTagName(qwTag.getName());
+                }
+            }
+
+            if(ObjectUtils.isNotNull(item.getWatchedTgId())) {
+                QwTag qwTag = qwTagMapper.selectQwTagById(item.getWatchedTgId());
+                if(ObjectUtils.isNotNull(qwTag)){
+                    item.setWatchedTagName(qwTag.getName());
+                }
+            }
+        }
+        return fsUserCourseVideoVOS;
     }
 
     @Override
@@ -2060,7 +2084,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         }
         //公开课
         if (param.getIsOpenCourse()!=null && param.getIsOpenCourse()==1){
-            FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId());
+            FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(),null);
             //添加判断:该用户是否已经存在此课程的看课记录,并且看课记录的销售id不是传入的销售id
             if(watchCourseVideo != null){
                 FsCourseWatchLog updateLog = new FsCourseWatchLog();
@@ -2134,7 +2158,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
         //查询看课记录
 //        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getWatchCourseVideoByFsUser(param.getUserId(), param.getVideoId(), param.getCompanyUserId());
-        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId());
+        FsCourseWatchLog watchCourseVideo = courseWatchLogMapper.getCourseWatchLogByUser(param.getUserId(), param.getVideoId(),param.getPeriodId());
 
         if (!isUserCoursePeriodValid(param)) {
             return ResponseResult.fail(504, "请观看最新的课程项目");
@@ -2471,10 +2495,8 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
     @Override
     public void batchSaveVideo(BatchVideoSvae vo) {
-        List<FsVideoResource> videoResourceList = fsVideoResourceMapper.selectBatchIds(vo.getIds());
-        videoResourceList = videoResourceList.stream()
-                .sorted(Comparator.comparing(FsVideoResource::getSort).thenComparing(FsVideoResource::getId))
-                .collect(Collectors.toList());
+        long[] idArray = vo.getIds().stream().mapToLong(Long::longValue).toArray();
+        List<FsVideoResource> videoResourceList = fsVideoResourceMapper.selectByIds(idArray);
         FsUserCourseVideo param = new FsUserCourseVideo();
         param.setCourseId(vo.getCourseId());
         List<FsUserCourseVideo> videoList = selectFsUserCourseVideoList(param);

+ 39 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCourseVideoVO.java

@@ -68,4 +68,43 @@ public class FsUserCourseVideoVO extends BaseEntity {
     private String redPacketMoney;
 
     private String companyRedPacketMoney;
+    /**
+     * 标签组表中的ID
+     */
+    private Long tgId;
+    /**
+     * 看课标签 表中的ID
+     */
+    private Long watchingTgId;
+    /**
+     * 完课标签 表中的ID
+     */
+    private Long watchedTgId;
+
+    /**
+     * 看课中标签ID
+     */
+    private String watchingTagId;
+    /**
+     * 完课标签ID
+     */
+    private String watchedTagId;
+    /**
+     * 标签组ID
+     */
+    private String tagGroupId;
+
+    /**
+     * 标签组名称
+     */
+    private String tagGroupName;
+    /**
+     * 看课标签
+     */
+    private String watchingTagName;
+    /**
+     * 完课标签
+     */
+    private String watchedTagName;
+
 }

+ 2 - 0
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -20,6 +20,8 @@ public interface AiHookService {
 
     R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid);
 
+    R qwHookNotifyAddMsgNew(Long qwUserID, Long sender,String count,String uid,Integer type);
+
     void expireAiMsg();
 
     WxWorkResponseDTO<String> getFileUrl(String uuid, String fileId, String aesKey, String authKey, String fileName, Integer fileSize, Long serverId);

+ 68 - 17
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -459,9 +459,9 @@ public class AiHookServiceImpl implements AiHookService {
                 return R.ok();
             }
 
-            //对用户处理的内容做处理
-            String maskedContent = processContent(qwContent);
-            String contentEmj = replaceWxEmo(maskedContent);
+            //对用户处理的内容做处理,去除手机号替换
+            //String maskedContent = processContent(qwContent);
+            String contentEmj = replaceWxEmo(qwContent);
             if(!contentEmj.contains("表情包")){
                 if(!contentEmj.isEmpty()){
                     addSaveAiMsg(1,1,contentEmj,user,fastGptChatSession.getSessionId(),role.getRoleId(),qwExternalContacts,fastGptChatSession.getUserId(),null,null,null);
@@ -1937,8 +1937,9 @@ public class AiHookServiceImpl implements AiHookService {
         sendAIParam.setKey(user.getAppKey());
         redisTemplate.opsForList().leftPush("AiMsg:"+user.getAppKey(), JSON.toJSONString(sendAIParam));
     }
+
     @Override
-    public R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid) {
+    public R qwHookNotifyAddMsgNew(Long qwUserID, Long sender,String count,String uid,Integer type) {
         QwUser sendUser = qwUserMapper.selectQwUserById(qwUserID);
 
 
@@ -1952,6 +1953,25 @@ public class AiHookServiceImpl implements AiHookService {
             FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), sendUser.getId());
             if (fastGptChatSession!=null){
                 saveQwUserMsg(fastGptChatSession,2,count);
+                // 客服进行回复后就转人工10分钟
+                if(type == 1){
+                    Calendar calendar = Calendar.getInstance();
+                    calendar.add(Calendar.MINUTE, -5);
+                    Date lastTime = calendar.getTime();
+                    if(lastTime.after(fastGptChatSession.getCreateTime())){
+                        Calendar calendar1 = Calendar.getInstance();
+                        //定时任务会处理10分钟以内的,所以设置20分钟
+                        calendar1.add(Calendar.MINUTE, 10);
+                        Date expireTime = calendar1.getTime();
+
+                        FastGptChatSession chatSession = new FastGptChatSession();
+                        chatSession.setLastTime(expireTime);
+                        chatSession.setIsArtificial(1);
+                        chatSession.setSessionId(fastGptChatSession.getSessionId());
+
+                        fastGptChatSessionMapper.updateFastGptChatSession(chatSession);
+                    }
+                }
             }else {
 
                 if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
@@ -1978,19 +1998,50 @@ public class AiHookServiceImpl implements AiHookService {
                     }
                 }
             }
-            // 客服进行回复后就转人工10分钟
-            if(fastGptChatSession != null){
-                Calendar calendar = Calendar.getInstance();
-                //定时任务会处理10分钟以内的,所以设置20分钟
-                calendar.add(Calendar.MINUTE, 30);
-                Date expireTime = calendar.getTime();
-
-                FastGptChatSession chatSession = new FastGptChatSession();
-                chatSession.setLastTime(expireTime);
-                chatSession.setIsArtificial(1);
-                chatSession.setSessionId(fastGptChatSession.getSessionId());
-
-                fastGptChatSessionMapper.updateFastGptChatSession(chatSession);
+        }
+        return R.ok();
+    }
+
+    @Override
+    public R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid) {
+        QwUser sendUser = qwUserMapper.selectQwUserById(qwUserID);
+
+
+        if (sendUser!=null){
+
+            String extId = getExtId(sender, uid, sendUser.getServerId());
+            QwExternalContact qwExternalContacts = qwExternalContactMapper.selectQwExternalContactByExternalUserIdAndQwUserId(extId, sendUser.getCorpId(),sendUser.getQwUserId());
+            if (qwExternalContacts==null){
+                return R.ok();
+            }
+            FastGptChatSession fastGptChatSession = fastGptChatSessionMapper.selectFastGptChatSessionByQwExternalContactsAndUserId(qwExternalContacts.getId(), sendUser.getId());
+            if (fastGptChatSession!=null){
+                saveQwUserMsg(fastGptChatSession,2,count);
+            }else {
+
+                if(qwExternalContacts.getType()!=null&&qwExternalContacts.getType()==1){
+                    if(sendUser.getFastGptRoleId()!=null){
+                        fastGptChatSession = new FastGptChatSession();
+                        String chatId = UUID.randomUUID().toString();
+                        fastGptChatSession.setChatId(chatId);
+                        fastGptChatSession.setKfId(sendUser.getFastGptRoleId().toString());
+                        fastGptChatSession.setStatus(1);
+                        fastGptChatSession.setRemindCount(0);
+                        fastGptChatSession.setRemindStatus(0);
+                        fastGptChatSession.setCreateTime(new Date());
+                        fastGptChatSession.setQwExtId(qwExternalContacts.getId());
+                        fastGptChatSession.setQwUserId(sendUser.getId());
+                        fastGptChatSession.setIsArtificial(0);
+                        fastGptChatSession.setAvatar(qwExternalContacts.getAvatar());
+                        fastGptChatSession.setNickName(qwExternalContacts.getName());
+                        fastGptChatSession.setCompanyId(sendUser.getCompanyId());
+                        fastGptChatSession.setLastTime(new Date());
+                        fastGptChatSession.setIsReply(0);
+                        fastGptChatSessionMapper.insertFastGptChatSession(fastGptChatSession);
+                        addUserSex(qwExternalContacts);
+                        saveQwUserMsg(fastGptChatSession,2,count);
+                    }
+                }
             }
         }
         return R.ok();

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

@@ -239,4 +239,6 @@ public interface FsPackageOrderMapper
     FsPackage selectFsPackageByOrderId(Long packageOrderId);
 
     List<PackageOrderDTO> getNewOrder();
+
+    List<FsPackageOrder> selectOutTimeOrderList(@Param("unPayTime") Integer unPayTime);
 }

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

@@ -1194,4 +1194,6 @@ public interface FsStoreOrderMapper
     List<Report> selectOrderByCustomerIds(@Param("map") ReportParam param);
 
     FsStoreOrderAmountStatsVo selectFsStoreOrderAmountStats(FsStoreOrderAmountStatsQueryDto queryDto);
+
+    List<FsStoreOrder> selectOutTimeOrderList(@Param("unPayTime")Integer unPayTime);
 }

+ 5 - 1
fs-service/src/main/java/com/fs/his/param/FsPackageCateUParam.java

@@ -1,5 +1,7 @@
 package com.fs.his.param;
 
+import com.fs.common.param.BaseQueryParam;
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -12,7 +14,7 @@ import java.util.List;
  * @date 2024-05-08
  */
 @Data
-public class FsPackageCateUParam implements Serializable
+public class FsPackageCateUParam extends BaseQueryParam implements Serializable
 {
 
     /** ID */
@@ -43,5 +45,7 @@ public class FsPackageCateUParam implements Serializable
 
     private Long companyUserId;
 
+    private Integer pageNum;
+    private Integer pageSize;
 
 }

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

@@ -142,4 +142,6 @@ public interface IFsPackageOrderService
 
 
     R getPackageOrder(String createOrderKey);
+
+    List<FsPackageOrder> selectOutTimeOrderList(Integer unPayTime);
 }

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

@@ -276,4 +276,6 @@ public interface IFsStoreOrderService
     FsStoreOrderScrm selectFsStoreOrderScrmByOrderCode(String soId);
 
     FsStoreOrder confirmOrder(FsPackageOrder packageOrder,Long doctorId);
+
+    List<FsStoreOrder> selectOutTimeOrderList(Integer unPayTime);
 }

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

@@ -126,4 +126,6 @@ public interface IFsStorePaymentService
     R getWxaCodeByPayment(FsStorePaymentGetWxaCodeParam param);
 
     String payConfirm(String payCode,String tradeNo,String bankTransactionId,String bankSerialNo);
+
+    void synchronizePayStatus();
 }

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

@@ -358,7 +358,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
         if (param.getUserCouponId() != null && param.getUserCouponId() > 0l) {
             FsUserCoupon userCoupon = userCouponService.selectFsUserCouponById(param.getUserCouponId());
             if (userCoupon != null) {
-                if (userCoupon.getStatus() == 0) {
+                if (Objects.equals(userCoupon.getBusinessId(), param.getOrderId()) || userCoupon.getStatus() == 0) {
                     FsCoupon coupon = couponService.selectFsCouponByCouponId(userCoupon.getCouponId());
                     if (coupon.getCouponType().equals(1)) {
                         if (coupon.getMinPrice().compareTo(orderPrice) <=0) {
@@ -500,7 +500,7 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
         FsPatient patient=null;
         FsDoctor doctor=null;
         FsPackage fsPackage=fsPackageMapper.selectFsPackageByPackageId(param.getPackageId());
-        if(fsPackage.getProductType()==1 || fsPackage.getProductType()==2){
+        if(fsPackage.getProductType()!= null &&(fsPackage.getProductType()==1 || fsPackage.getProductType()==2)){
             if(param.getPatientId()!=null){
                 patient=fsPatientMapper.selectFsPatientByPatientId(param.getPatientId());
                 if (patient==null){
@@ -1830,4 +1830,9 @@ public class FsPackageOrderServiceImpl implements IFsPackageOrderService
         }
         return R.ok().put("package", fsPackage).put("money", money).put("payType", payType);
     }
+
+    @Override
+    public List<FsPackageOrder> selectOutTimeOrderList(Integer unPayTime) {
+        return fsPackageOrderMapper.selectOutTimeOrderList(unPayTime);
+    }
 }

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

@@ -388,7 +388,7 @@ public class FsStoreAfterSalesServiceImpl implements IFsStoreAfterSalesService {
         return 1;
     }
 
-    @Transactional
+    @Transactional(rollbackFor = Exception.class)
     @Override
     public int refundMoney(FsStoreAfterSales fsStoreAfterSales) {
         FsStoreAfterSales order = fsStoreAfterSalesMapper.selectFsStoreAfterSalesById(fsStoreAfterSales.getId());
@@ -473,7 +473,7 @@ public class FsStoreAfterSalesServiceImpl implements IFsStoreAfterSalesService {
         if (payments != null && payments.size() > 0) {
             FsStorePayment payment = payments.get(0);
             if (reMoney.compareTo(payment.getPayMoney()) > 0) {
-                return 0; //退款金额不能大于实际支付金额
+                throw new CustomException("退款金额不能大于实际支付金额"); //退款金额不能大于实际支付金额
             }
             String json = configService.selectConfigByKey("his.pay");
             if (payment.getPayMode().equals("wx")) {

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

@@ -4446,4 +4446,9 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
         return order;
     }
 
+    @Override
+    public List<FsStoreOrder> selectOutTimeOrderList(Integer unPayTime) {
+        return fsStoreOrderMapper.selectOutTimeOrderList(unPayTime);
+    }
+
 }

+ 21 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsStorePaymentServiceImpl.java

@@ -7,6 +7,7 @@ import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
@@ -1536,6 +1537,26 @@ public class FsStorePaymentServiceImpl implements IFsStorePaymentService {
         return "SUCCESS";
     }
 
+    @Override
+    public void synchronizePayStatus() {
+        FsStorePayment queryParam = new FsStorePayment();
+        queryParam.setStatus(0);//未支付
+        queryParam.setBeginTime(DateUtils.addDateDays(-1));
+        queryParam.setEndTime(DateUtils.getDate());
+        List<FsStorePayment> list = selectFsStorePaymentList(queryParam);
+        if (list != null && !list.isEmpty()) {
+            List<CompletableFuture<Void>> futures = new ArrayList<>();
+            for (FsStorePayment fsStorePayment : list) {
+                CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
+                    updateFsStorePaymentByDecryptForm(fsStorePayment.getPaymentId());
+                    logger.info("定时任务:同步支付状态,payment_id:{}",fsStorePayment.getPaymentId());
+                });
+                futures.add(future);
+            }
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+        }
+    }
+
     @Override
     public R paymentByWxaCode(FsStorePaymentPayParam param) {
         FsUser user = userMapper.selectFsUserById(param.getUserId());

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/config/StoreConfig.java

@@ -24,5 +24,5 @@ public class StoreConfig implements Serializable {
     private Integer auditSwitch; // 订单审核开关
     private Integer createSalesOrderType; // 订单改价方式 1 商品改价 2总价改价
     private Boolean isBrushOrders;//是否开启刷单按钮
-
+    private Integer orderAttribution; // 下单归属 1 多销售 2单销售
 }

+ 78 - 53
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -33,13 +33,11 @@ import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.ip.IpUtils;
 import com.fs.common.utils.spring.SpringUtils;
-import com.fs.company.domain.Company;
-import com.fs.company.domain.CompanyDept;
-import com.fs.company.domain.CompanyMoneyLogs;
-import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.*;
 import com.fs.company.mapper.CompanyDeptMapper;
 import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.mapper.CompanyMoneyLogsMapper;
+import com.fs.company.mapper.CompanyUserUserMapper;
 import com.fs.company.service.ICompanyDeptService;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
@@ -170,6 +168,9 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     Logger logger = LoggerFactory.getLogger(getClass());
     @Autowired
     private CompanyMoneyLogsMapper moneyLogsMapper;
+
+    @Autowired
+    private CompanyUserUserMapper companyUserUserMapper;
     @Autowired
     private IFsStoreOrderStatusScrmService orderStatusService;
     @Autowired
@@ -740,23 +741,42 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setStoreHouseCode("CK01");
             storeOrder.setCompanyId(param.getCompanyId());
             storeOrder.setCompanyUserId(param.getCompanyUserId());
+
+            String json = configService.selectConfigByKey("store.config");
+            StoreConfig config= JSONUtil.toBean(json, StoreConfig.class);
             //绑定销售
-            FsUserScrm fsuser = userService.selectFsUserById(userId);
-            if (param.getCompanyUserId() != null) {
-                if (!CloudHostUtils.hasCloudHostName("鸿森堂") && ObjectUtil.isNotEmpty(fsuser.getCompanyUserId()) && !Objects.equals(fsuser.getCompanyUserId(), param.getCompanyUserId())) {
-                    CompanyUser companyUser = companyUserService.selectCompanyUserById(fsuser.getCompanyUserId());
-                    return R.error(String.format("请联系%s销售进行购买商品!", companyUser.getNickName()));
-                } else {
-                    fsuser.setCompanyUserId(param.getCompanyUserId());
-                    userService.updateFsUser(fsuser);
+            FsUserScrm fsuser= userService.selectFsUserById(userId);
+            if(ObjectUtil.isEmpty(config.getOrderAttribution())
+                    ||!config.getOrderAttribution().equals(1)){
+                if(param.getCompanyUserId()!=null){
+                    if (ObjectUtil.isNotEmpty(fsuser.getCompanyUserId())&&fsuser.getCompanyUserId()!=param.getCompanyUserId()){
+                        CompanyUser companyUser=companyUserService.selectCompanyUserById(fsuser.getCompanyUserId());
+                        return R.error(String.format("请联系%s销售进行购买商品!",companyUser.getNickName()));
+                    }else {
+                        fsuser.setCompanyUserId(param.getCompanyUserId());
+                        userService.updateFsUser(fsuser);
+                    }
+                    CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                    if(companyUser!=null){
+                        storeOrder.setDeptId(companyUser.getDeptId());
+                    }
+                }else {
+                    storeOrder.setCompanyUserId(fsuser.getCompanyUserId());
                 }
-                CompanyUser companyUser = companyUserService.selectCompanyUserById(param.getCompanyUserId());
-                if (companyUser != null) {
-                    storeOrder.setDeptId(companyUser.getDeptId());
+            }
+
+            CompanyUserUser map=new CompanyUserUser();
+            map.setCompanyUserId(param.getCompanyUserId());
+            map.setUserId(userId);
+            List<CompanyUserUser> list= companyUserUserMapper.selectCompanyUserUserList(map);
+            if(list==null||list.size()==0){
+                CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+                if(companyUser!=null&&companyUser.getStatus().equals("0")){
+                    map.setCompanyId(companyUser.getCompanyId());
+                    companyUserUserMapper.insertCompanyUserUser(map);
                 }
-            } else {
-                storeOrder.setCompanyUserId(fsuser.getCompanyUserId());
             }
+
             storeOrder.setUserId(userId);
             storeOrder.setOrderCode(orderSn);
             storeOrder.setRealName(address.getRealName());
@@ -810,8 +830,8 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             storeOrder.setShippingType(1);
             storeOrder.setCreateTime(new Date());
 
-            String json = configService.selectConfigByKey("store.config");
-            StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
+//            String json = configService.selectConfigByKey("store.config");
+//            StoreConfig config = JSONUtil.toBean(json, StoreConfig.class);
             if (config.getServiceFee() != null) {
                 storeOrder.setServiceFee(config.getServiceFee());
             }
@@ -1728,43 +1748,44 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             if (order != null && !order.getPaid().equals(0)) {
                 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
                 return "";
-            } else if (type.equals(2)) {
-                //货到付款
-                order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
-                if (!order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
-                    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-                    return "";
-                }
             }
-            //写入公司佣金 当有归属公司时只进行公司分佣
-            if (order.getCompanyId() != null && order.getCompanyId() > 0) {
-                companyService.addCompanyTuiMoney(order);
-                //没有归属公司对个人分佣
-            } else if (order.getIsPackage() != 1 && order.getTuiUserId() != null && order.getTuiUserId() > 0) {
-                //处理佣金 套餐不分佣金
-                FsStoreOrderItemScrm orderItemMap = new FsStoreOrderItemScrm();
-                orderItemMap.setOrderId(order.getId());
-                List<FsStoreOrderItemScrm> items = storeOrderItemService.selectFsStoreOrderItemList(orderItemMap);
-                userService.addTuiMoney(order, items);
-            }
-            //增加用户购买次数
-            userService.incPayCount(order.getUserId());
-            //增加状态
-            orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
-                    OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
-            FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
-            storeOrder.setId(order.getId());
-            storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
-            storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
-            storeOrder.setPayTime(new Date());
-            fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
-            // 添加订单审核
-            if (storeOrder.getCompanyId() != null) {
-                addOrderAudit(order);
+        }else if (type.equals(2)) {
+            //货到付款
+            order = fsStoreOrderMapper.selectFsStoreOrderById(orderId);
+            if (!order.getStatus().equals(OrderInfoEnum.STATUS_0.getValue())) {
+                TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
+                return "";
             }
         }
+        //写入公司佣金 当有归属公司时只进行公司分佣
+        if (order.getCompanyId() != null && order.getCompanyId() > 0) {
+            companyService.addCompanyTuiMoney(order);
+            //没有归属公司对个人分佣
+        } else if (order.getIsPackage() != 1 && order.getTuiUserId() != null && order.getTuiUserId() > 0) {
+            //处理佣金 套餐不分佣金
+            FsStoreOrderItemScrm orderItemMap = new FsStoreOrderItemScrm();
+            orderItemMap.setOrderId(order.getId());
+            List<FsStoreOrderItemScrm> items = storeOrderItemService.selectFsStoreOrderItemList(orderItemMap);
+            userService.addTuiMoney(order, items);
+        }
+        //增加用户购买次数
+        userService.incPayCount(order.getUserId());
+        //增加状态
+        orderStatusService.create(order.getId(), OrderLogEnum.PAY_ORDER_SUCCESS.getValue(),
+                OrderLogEnum.PAY_ORDER_SUCCESS.getDesc());
+        FsStoreOrderScrm storeOrder = new FsStoreOrderScrm();
+        storeOrder.setId(order.getId());
+        storeOrder.setPaid(OrderInfoEnum.PAY_STATUS_1.getValue());
+        storeOrder.setStatus(OrderInfoEnum.STATUS_1.getValue());
+        storeOrder.setPayTime(new Date());
+        fsStoreOrderMapper.updateFsStoreOrder(storeOrder);
+        // 添加订单审核
+        if (storeOrder.getCompanyId() != null) {
+            addOrderAudit(order);
+        }
 
-        return "SUCCESS";
+
+    return "SUCCESS";
 
 
         //非处方直接提交OMS
@@ -3876,6 +3897,10 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     order.setPayType("2");
                     BigDecimal payMoney=order.getPayPrice().multiply(new BigDecimal(storeConfig.getPayRate())).divide(new BigDecimal(100));
                     payMoney=new BigDecimal(payMoney.setScale(0, BigDecimal.ROUND_HALF_UP).longValue());
+                    // 如果小程序需要支付的金额小于0.01元,不能走物流代收,让走货到付款 xgb
+                    if (payMoney.compareTo(new BigDecimal("0.01")) < 0) {
+                        return R.error("物流代收计算支付金额为0,不允许选择物流代收");
+                    }
                     order.setPayDelivery(order.getPayPrice().subtract(payMoney));
                     order.setPayMoney(payMoney);
                 }
@@ -4367,7 +4392,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
                     order.setPayDelivery(BigDecimal.ZERO);
                 }
                 else if(param.getPayType().equals(2)){
-
+                    // 物流代收
                     order.setPayType("2");
                     BigDecimal payMoney=order.getPayPrice().multiply(new BigDecimal(storeConfig.getPayRate())).divide(new BigDecimal(100));
                     payMoney=new BigDecimal(payMoney.setScale(0, BigDecimal.ROUND_HALF_UP).longValue());

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

@@ -878,7 +878,7 @@ public class FsStorePaymentScrmServiceImpl implements IFsStorePaymentScrmService
         storePayment.setPayMoney(param.getPayMoney());
         storePayment.setCreateTime(new Date());
         storePayment.setPayTypeCode("weixin");
-        storePayment.setBusinessType(7);//微信收款
+        storePayment.setBusinessType(1);//微信收款
         storePayment.setRemark("商城收款订单支付");
         storePayment.setOpenId(user.getMaOpenId());
         storePayment.setUserId(user.getUserId());

+ 2 - 0
fs-service/src/main/java/com/fs/qw/cache/IQwUserCacheService.java

@@ -2,4 +2,6 @@ package com.fs.qw.cache;
 
 public interface IQwUserCacheService {
     String queryQwUserNameByUserId(String userId);
+    String queryCorpIdByQwUserId(Long qwUserId);
+
 }

+ 15 - 0
fs-service/src/main/java/com/fs/qw/cache/impl/QwUserCacheServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.qw.cache.impl;
 
 import com.fs.qw.cache.IQwUserCacheService;
 import com.fs.qw.domain.QwUser;
+import com.fs.qw.mapper.QwUserMapper;
 import com.fs.qw.service.IQwUserService;
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
@@ -14,6 +15,9 @@ import java.util.concurrent.TimeUnit;
 public class QwUserCacheServiceImpl implements IQwUserCacheService {
     @Autowired
     private IQwUserService qwUserService;
+
+    @Autowired
+    private QwUserMapper qwUserMapper;
     /**
      * 企微用户名昵称缓存类
      */
@@ -21,6 +25,12 @@ public class QwUserCacheServiceImpl implements IQwUserCacheService {
             .maximumSize(5000)
             .expireAfterWrite(12, TimeUnit.HOURS)
             .build();
+
+    private static final Cache<Long, String> QW_USER_ID_CACHE = Caffeine.newBuilder()
+            .maximumSize(5000)
+            .expireAfterWrite(5, TimeUnit.HOURS)
+            .build();
+
     @Override
     public String queryQwUserNameByUserId(String userId) {
         return QW_USER_CACHE.get(userId,e-> {
@@ -31,4 +41,9 @@ public class QwUserCacheServiceImpl implements IQwUserCacheService {
             return "-";
         });
     }
+
+    @Override
+    public String queryCorpIdByQwUserId(Long qwUserId) {
+        return QW_USER_ID_CACHE.get(qwUserId,e-> qwUserMapper.selectCorpIdById(qwUserId));
+    }
 }

+ 47 - 0
fs-service/src/main/java/com/fs/qw/domain/QwDeptTreeSelect.java

@@ -0,0 +1,47 @@
+package com.fs.qw.domain;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class QwDeptTreeSelect implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+    private String label;
+    private List<QwDeptTreeSelect> children;
+
+    // 构造方法
+    public QwDeptTreeSelect() {
+    }
+
+    public QwDeptTreeSelect(Long id, String label) {
+        this.id = id;
+        this.label = label;
+    }
+
+    // getter和setter
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public List<QwDeptTreeSelect> getChildren() {
+        return children;
+    }
+
+    public void setChildren(List<QwDeptTreeSelect> children) {
+        this.children = children;
+    }
+}

+ 3 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -400,8 +400,10 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
 
     @Select("SELECT id,external_user_id,name,avatar,remark,description,fs_user_id FROM  qw_external_contact " +
             " WHERE user_id = #{map.userId}   " +
-            "AND external_user_id = #{map.externalUserId} " +
             "AND corp_id =#{map.corpId} " +
+            "AND external_user_id = #{map.externalUserId}" +
+            "AND `status` != 4 " +
+            "ORDER BY id desc " +
             "limit 1 ")
     QwExternalContact getQwExternalContactDetails(@Param("map")QwExternalContactHParam param);
 

+ 1 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwTagGroupMapper.java

@@ -83,4 +83,5 @@ public interface QwTagGroupMapper
 
     List<QwTagGroupListVO> selectQwTagGroups(QwTagGroup qwTagGroup);
 
+    QwTagGroup selectQwTagGroupByName(@Param("tagGroup") String tagGroup, @Param("corpId") String corpId);
 }

+ 13 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwUserMapper.java

@@ -19,6 +19,7 @@ import org.apache.ibatis.annotations.Update;
 import org.springframework.stereotype.Repository;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 企微用户Mapper接口
@@ -163,6 +164,12 @@ public interface QwUserMapper extends BaseMapper<QwUser>
             "                       #{item} " +
             "                   </foreach> " +
             "            </if>" +
+            "            <if test=\"qwDeptIdList != null and !qwDeptIdList.isEmpty() \">" +
+            "               AND qd.dept_id IN " +
+            "                   <foreach collection='qwDeptIdList' item='item' open='(' separator=',' close=')'> " +
+            "                       #{item} " +
+            "                   </foreach> " +
+            "            </if>" +
             "ORDER BY  qu.login_status asc,qu.tool_status desc " +
             "</script>"})
     List<QwUserVO> selectQwUserListStaffVO(QwUserListParam qwUser);
@@ -442,4 +449,10 @@ public interface QwUserMapper extends BaseMapper<QwUser>
 
     List<QwOptionsVO> selectQwCompanyListOptionsVOBySys();
 
+    @Select("select corp_id from qw_user where id=#{id} limit 1")
+    String selectCorpIdById(@Param("id") Long id);
+    List<Long> selectDeptByParentId(@Param("deptId")Long deptId,@Param("corpId") String corpId);
+
+    @Select("  select company_id from qw_user where qw_user_id = #{owner} and corp_id = #{corpId}  limit 1")
+    Long getCompanyIdByCorpIdAndOwner(@Param("corpId")String corpId, @Param("owner")String owner);
 }

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

@@ -142,7 +142,7 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "ORDER BY\n" +
             "    DATE(qec.create_time) "+
             "</script>"})
-    List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAnd(FsCourseWatchLogListParam param);
+    List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAndOther(FsCourseWatchLogListParam param);
     @Select("select \n" +
             "COUNT(CASE WHEN day = 0 and status in (1,2) THEN 1 END) AS firstOnline,\n" +
             "COUNT(CASE WHEN day = 0 and status=2 THEN 1 END) AS firstOver,\n" +

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

@@ -57,6 +57,11 @@ public class QwUserListParam {
      * 销售部门
      */
     private List<Long> cuDeptIdList;
+    /**
+     * 企微部门
+     */
+    private List<Long> qwDeptIdList;
+
 
     /**
      * 部门类型 00 管理员 01 员工

+ 18 - 2
fs-service/src/main/java/com/fs/qw/service/AsyncQwAiChatSopService.java

@@ -1,6 +1,7 @@
 package com.fs.qw.service;
 
 import com.alibaba.fastjson.JSON;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.company.service.ICompanyMiniappService;
 import com.fs.course.config.CourseConfig;
@@ -12,9 +13,11 @@ import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.fastGpt.domain.FastGptChatReplaceWords;
 import com.fs.fastGpt.mapper.FastGptChatReplaceWordsMapper;
 import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwExternalContactInfo;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwExternalContactInfoMapper;
+import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.vo.QwSopRuleTimeVO;
 import com.fs.qw.vo.QwSopTempSetting;
 import com.fs.sop.domain.QwSopLogs;
@@ -67,6 +70,8 @@ public class AsyncQwAiChatSopService {
 
     @Autowired
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
 
     @Autowired
     private QwSopLogsMapper qwSopLogsMapper;
@@ -92,6 +97,13 @@ public class AsyncQwAiChatSopService {
                                    QwUser qwUser, String externalUserID, String externalContactName,
                                    Long externalId, Long fsUserId, LocalDate currentDate, LocalTime localTime) {
 
+
+        QwExternalContact contact;
+        if(externalId != null){
+            contact = qwExternalContactMapper.selectById(externalId);
+        } else {
+            contact = null;
+        }
         //新客对话任务
         List<QwSopRuleTimeVO> qwSopAiRuleTimeVOS = qwSopMapper.selectQwAiSopAutoByTagsByForeach(qwSopAutoByTags);
         List<FastGptChatReplaceWords> words = fastGptChatReplaceWordsMapper.selectAllFastGptChatReplaceWords();
@@ -120,7 +132,6 @@ public class AsyncQwAiChatSopService {
                 List<QwSopTempContent> tempContentList = qwSopTempContentMapper.selectQwSopTempContentByTempIdAndRules(item.getTempId());
 
                 tempContentList.forEach(content->{
-
                     QwSopLogs sopLogs = new QwSopLogs();
                     sopLogs.setQwUserKey(qwUser.getId());
                     sopLogs.setQwUserid(userID);
@@ -162,8 +173,13 @@ public class AsyncQwAiChatSopService {
                         case "3":
 
                             if ("1".equals(setting.getContentType())) {
+                                String defaultName = "同学";
+                                if(contact != null && StringUtils.isNotEmpty(contact.getName()) && !"待同步客户".equals(contact.getName())){
+                                    defaultName = contact.getName();
+                                }
                                 setting.setValue(setting.getValue()
-                                        .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText()));
+                                        .replaceAll("#销售称呼#", StringUtil.strIsNullOrEmpty(qwUser.getWelcomeText()) ? "" : qwUser.getWelcomeText())
+                                        .replaceAll("#客户称呼#", contact == null || StringUtil.strIsNullOrEmpty(contact.getStageStatus())|| "0".equals(contact.getStageStatus())?defaultName:contact.getStageStatus()));
                             }
 
 

+ 3 - 0
fs-service/src/main/java/com/fs/qw/service/IQwDeptService.java

@@ -2,6 +2,7 @@ package com.fs.qw.service;
 
 import com.fs.common.core.domain.R;
 import com.fs.qw.domain.QwDept;
+import com.fs.qw.domain.QwDeptTreeSelect;
 
 import java.util.List;
 
@@ -69,4 +70,6 @@ public interface IQwDeptService
      * @return 结果
      */
     public int deleteQwDeptById(Long id);
+
+    List<QwDeptTreeSelect> buildDeptTreeSelect(List<QwDept> depts);
 }

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

@@ -199,4 +199,5 @@ public interface IQwUserService
 
     List<QwOptionsVO> selectQwCompanyListOptionsVOBySys();
 
+    List<Long> selectDeptByParentId(Long deptId,String cropId);
 }

+ 69 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwDeptServiceImpl.java

@@ -2,6 +2,7 @@ package com.fs.qw.service.impl;
 
 import com.fs.common.core.domain.R;
 import com.fs.qw.domain.QwDept;
+import com.fs.qw.domain.QwDeptTreeSelect;
 import com.fs.qw.mapper.QwDeptMapper;
 import com.fs.qw.service.IQwDeptService;
 import com.fs.qwApi.domain.QwDeptResult;
@@ -10,7 +11,11 @@ import com.fs.qwApi.service.QwApiService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 企业微信部门Service业务层处理
@@ -129,4 +134,68 @@ public class QwDeptServiceImpl implements IQwDeptService
     {
         return qwDeptMapper.deleteQwDeptById(id);
     }
+
+    /**
+     * @Description: 按TreeSelect结构返回部门
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/30 9:37
+     */
+    @Override
+    /**
+     * 将部门列表转换为TreeSelect树形结构
+     */
+    public List<QwDeptTreeSelect> buildDeptTreeSelect(List<QwDept> deptList) {
+        if (deptList == null || deptList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 按父部门ID分组
+        Map<Long, List<QwDept>> deptMap = deptList.stream()
+                .collect(Collectors.groupingBy(QwDept::getParentid));
+
+        // 获取所有根部门(parentId = 0)
+        List<QwDept> rootDepts = deptMap.getOrDefault(0L, new ArrayList<>());
+
+        // 构建树形结构
+        return rootDepts.stream()
+                .map(dept -> buildTreeSelect(dept, deptMap))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 递归构建TreeSelect节点
+     */
+    private QwDeptTreeSelect buildTreeSelect(QwDept dept, Map<Long, List<QwDept>> deptMap) {
+        return buildTreeSelect(dept, deptMap, 0);
+    }
+
+    /**
+     * 递归构建TreeSelect节点,最多5级
+     */
+    private QwDeptTreeSelect buildTreeSelect(QwDept dept, Map<Long, List<QwDept>> deptMap, int depth) {
+        // 限制最多5级,避免死循环
+        if (depth > 5) {
+            return new QwDeptTreeSelect(dept.getDeptId(), dept.getDeptName());
+        }
+
+        QwDeptTreeSelect treeSelect = new QwDeptTreeSelect(dept.getDeptId(), dept.getDeptName());
+
+        // 获取当前部门的子部门
+        List<QwDept> children = deptMap.get(dept.getDeptId());
+        if (children != null && !children.isEmpty()) {
+            // 递归构建子节点
+            List<QwDeptTreeSelect> childNodes = children.stream()
+                    .map(child -> buildTreeSelect(child, deptMap, depth + 1))
+                    .collect(Collectors.toList());
+            treeSelect.setChildren(childNodes);
+        }
+
+        return treeSelect;
+    }
+
+
+
+
 }

+ 14 - 13
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -2230,7 +2230,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
         QwContactWay wayId = null;
         //先入客户
         QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
-        boolean isNewQwExternalContact = qwExternalContact == null ? true : false;
+        boolean isNewQwExternalContact = qwExternalContact == null;
         qwExternalContact = qwExternalContact == null ? new QwExternalContact() : qwExternalContact;
         qwExternalContact.setUserId(userID); // 设置属于用户ID
         qwExternalContact.setExternalUserId(externalUserID); // 设置外部联系人ID
@@ -2259,18 +2259,18 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
 //        }
 //        iAdHtmlClickLogService.upload(state, AdUploadType.ADD_WX, e -> finalQwExternalContact.setUploadAddWxStatus(1));
         //重粉问题
-        try {
-            new Thread(() -> {
-                try {
-                    Thread.sleep(3000);
-                } catch (InterruptedException e) {
-                    logger.error("添加等待时长错误", e);
-                }
-                rocketMQTemplate.syncSend("repeat-upload", JSON.toJSONString(RepeatUploadVo.builder().type(0).externalUserId(externalUserID).build()));
-            }).start();
-        }catch (Exception e){
-            logger.error("重粉提交mq失败", e);
-        }
+//        try {
+//            new Thread(() -> {
+//                try {
+//                    Thread.sleep(3000);
+//                } catch (InterruptedException e) {
+//                    logger.error("添加等待时长错误", e);
+//                }
+//                rocketMQTemplate.syncSend("repeat-upload", JSON.toJSONString(RepeatUploadVo.builder().type(0).externalUserId(externalUserID).build()));
+//            }).start();
+//        }catch (Exception e){
+//            logger.error("重粉提交mq失败", e);
+//        }
 
 
 
@@ -2526,6 +2526,7 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                 qwExternalContact.setType(externalContact.getType()); // 设置外部联系人类型(1微信用户,2企业微信用户)
                 qwExternalContact.setGender(externalContact.getGender()); // 设置性别 (0-未知, 1-男性, 2-女性)
                 qwExternalContact.setDescription(followUser.getDescription()); // 设置描述信息
+                qwExternalContact.setUnionid(externalContact.getUnionid());
                 List<Tag> tags = followUser.getTags();
                 Set<String> combinedTagsSet = new HashSet<>();
 

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

@@ -311,6 +311,11 @@ public class QwGroupChatServiceImpl implements IQwGroupChatService
                     qwGroupChat.setCorpId(corpId);
                     qwGroupChat.setNotice(notice);
                     qwGroupChat.setOwner(owner);
+                    //2025-10-31 新增维护 qw_group_chat的company_id
+                    Long findCompanyId = qwUserMapper.getCompanyIdByCorpIdAndOwner(corpId, owner);
+                    if(null != findCompanyId){
+                        qwGroupChat.setCompanyId(findCompanyId);
+                    }
                     qwGroupChat.setCreateAt(String.valueOf(createTime));
                     qwGroupChat.setMemberVersion(groupChat.getMemberVersion());
                     qwGroupChat.setChatId(chatId);

+ 12 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwUserServiceImpl.java

@@ -1559,6 +1559,18 @@ public class QwUserServiceImpl implements IQwUserService
         return qwUserMapper.selectQwCompanyListOptionsVOBySys();
     }
 
+    /**
+     * @Description: 根据企微部门查询下级部门id
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/30 14:27
+     */
+    @Override
+    public List<Long> selectDeptByParentId(Long deptId,String cropId) {
+        return qwUserMapper.selectDeptByParentId(deptId,cropId);
+    }
+
 
     /**
      * 构建查询条件

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

@@ -155,7 +155,7 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
         if (param.getCompanyUserId()!=null){
             param.setIds(companyUserMapper.selectQwUserIdsByCompany(param.getCompanyUserId()));
         }
-        List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper.selectQwExtCountByDayAnd(param);
+        List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper.selectQwExtCountByDayAndOther(param);
         for (QwWatchLogStatisticsListVO vo : vos) {
             Long id = vo.getId();
             Date createTime = vo.getCreateTime();

+ 2 - 2
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -553,7 +553,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     sopLogs.setSopId(param.getSopId());
                     sopLogs.setCorpId(qwGroupChat.getCorpId());
                     sopLogs.setSort(30000001);
-                    sopLogs.setSendType(6);
+                    sopLogs.setSendType(2);
                     sopLogs.setExternalUserName(groupUser.getName());
                     sopLogs.setQwUserKey(qwUser.getId());
 
@@ -692,7 +692,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     sopLogs.setSopId(param.getSopId());
                     sopLogs.setCorpId(groupChat.getCorpId());
                     sopLogs.setSort(2);
-                    sopLogs.setSendType(12);
+                    sopLogs.setSendType(6);
                     sopLogs.setExternalUserName(groupChat.getName());
                     sopLogs.setQwUserKey(qwUser.getId());
                     // 设置实际发送人

+ 40 - 0
fs-service/src/main/java/com/fs/statis/param/ComprehensiveStatisticsParam.java

@@ -0,0 +1,40 @@
+package com.fs.statis.param;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @description: 综合统计入参
+ * @author: Guos
+ * @time: 2025/11/3 上午11:22
+ */
+@Data
+public class ComprehensiveStatisticsParam {
+
+    /**
+     * 统计维度
+     */
+    private Integer dimension;
+
+    /**
+     * 开始时间
+     */
+    private Date startTime;
+
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * id 在不同的维度下,id代表的意义不同
+     */
+    private Long id;
+}
+

+ 115 - 0
fs-service/src/main/java/com/fs/tag/domain/FsTagUpdateQueue.java

@@ -0,0 +1,115 @@
+package com.fs.tag.domain;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 标签更新对列表
+ */
+@Data
+public class FsTagUpdateQueue {
+    /** id */
+    private Long id;
+
+    /** 看课日志id */
+    private Long courseLogId;
+
+    /** 是否是先导课*/
+    private Integer isFirst;
+
+    /**
+     * 课程排序
+     */
+    private Long sort;
+
+    /** 课程ID */
+    private Long courseId;
+
+    /** 标签id */
+    private String tagId;
+
+    /** 标签名称 */
+    private String tagName;
+
+    /** 操作类型(0 ADD 1 REMOVE) 默认0 */
+    private Integer operationType;
+
+    /** 视频ID */
+    private Long videoId;
+
+    /** 0未处理 1处理中 2成功 3失败 */
+    private Integer status;
+
+    /** 重试次数 */
+    private Integer retryCount;
+
+    /** 企微主体id */
+    private String corpId;
+
+    /** 企微user_id */
+    private Long qwUserId;
+
+    /** 企微外部联系人id */
+    private Long qwExternalContactId;
+    /**
+     * 记录类型
+     * 0 正在看课
+     * 1 已完课
+     */
+    private Integer logType;
+
+    /** 失败原因 */
+    private String failMsg;
+
+    /** 请求参数 */
+    private String payload;
+
+    /** 返回结果 */
+    private String response;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 更新人 */
+    private Long updateBy;
+
+    /** 创建人 */
+    private Long createBy;
+
+    /**
+     * 下次执行时间
+     */
+    private LocalDateTime nextExecuteTime;
+
+
+    /**
+     * 看课中标签ID
+     */
+    private String watchingTagId;
+    /**
+     * 完课标签ID
+     */
+    private String watchedTagId;
+    /**
+     * 标签组ID
+     */
+    private String tagGroupId;
+
+    /**
+     * 标签组表中的ID
+     */
+    private Long tgId;
+    /**
+     * 看课标签 表中的ID
+     */
+    private Long watchingTgId;
+    /**
+     * 完课标签 表中的ID
+     */
+    private Long watchedTgId;
+}

+ 162 - 0
fs-service/src/main/java/com/fs/tag/mapper/FsTagUpdateQueueMapper.java

@@ -0,0 +1,162 @@
+package com.fs.tag.mapper;
+
+import com.fs.tag.domain.FsTagUpdateQueue;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+/**
+ * 标签更新对列表 Mapper
+ */
+@Mapper
+public interface FsTagUpdateQueueMapper {
+
+    @Select("select * from fs_tag_update_queue where retry_count < 3 and status in (0,3) and (next_execute_time < now() or next_execute_time is null) limit 500")
+    List<FsTagUpdateQueue> selectPending();
+
+    @Select("<script>" +
+            "SELECT * FROM fs_tag_update_queue " +
+            "<where>" +
+            "<if test='id != null'> AND id = #{id} </if>" +
+            "<if test='courseLogId != null'> AND course_log_id = #{courseLogId} </if>" +
+            "<if test='courseId != null'> AND course_id = #{courseId} </if>" +
+            "<if test='tagId != null'> AND tag_id = #{tagId} </if>" +
+            "<if test='tagName != null'> AND tag_name = #{tagName} </if>" +
+            "<if test='operationType != null'> AND operation_type = #{operationType} </if>" +
+            "<if test='videoId != null'> AND video_id = #{videoId} </if>" +
+            "<if test='status != null'> AND status = #{status} </if>" +
+            "<if test='retryCount != null'> AND retry_count = #{retryCount} </if>" +
+            "<if test='corpId != null'> AND corp_id = #{corpId} </if>" +
+            "<if test='qwUserId != null'> AND qw_user_id = #{qwUserId} </if>" +
+            "</where>" +
+            "</script>")
+    List<FsTagUpdateQueue> selectByConditions(FsTagUpdateQueue condition);
+
+    @Insert("<script>" +
+            "INSERT INTO fs_tag_update_queue " +
+            "(course_log_id, course_id, tag_id, tag_name, operation_type, video_id, status, retry_count, corp_id, qw_user_id, fail_msg, payload, response, create_time, update_time, update_by, create_by,log_type) " +
+            "VALUES " +
+            "<trim prefix='(' suffix=')' suffixOverrides=','>" +
+            "<if test='courseLogId != null'>course_log_id,</if>" +
+            "<if test='courseId != null'>course_id,</if>" +
+            "<if test='tagId != null'>tag_id,</if>" +
+            "<if test='tagName != null'>tag_name,</if>" +
+            "<if test='operationType != null'>operation_type,</if>" +
+            "<if test='videoId != null'>video_id,</if>" +
+            "<if test='status != null'>status,</if>" +
+            "<if test='retryCount != null'>retry_count,</if>" +
+            "<if test='corpId != null'>corp_id,</if>" +
+            "<if test='qwUserId != null'>qw_user_id,</if>" +
+            "<if test='qwExternalContactId != null'>qw_external_contact_id,</if>" +
+            "<if test='failMsg != null'>fail_msg,</if>" +
+            "<if test='payload != null'>payload,</if>" +
+            "<if test='response != null'>response,</if>" +
+            "<if test='createTime != null'>create_time,</if>" +
+            "<if test='updateTime != null'>update_time,</if>" +
+            "<if test='updateBy != null'>update_by,</if>" +
+            "<if test='createBy != null'>create_by,</if>" +
+            "<if test='logType != null'>log_type,</if>" +
+            "</trim>" +
+            "<trim prefix='VALUES (' suffix=')' suffixOverrides=','>" +
+            "<if test='courseLogId != null'>#{courseLogId},</if>" +
+            "<if test='courseId != null'>#{courseId},</if>" +
+            "<if test='tagId != null'>#{tagId},</if>" +
+            "<if test='tagName != null'>#{tagName},</if>" +
+            "<if test='operationType != null'>#{operationType},</if>" +
+            "<if test='videoId != null'>#{videoId},</if>" +
+            "<if test='status != null'>#{status},</if>" +
+            "<if test='retryCount != null'>#{retryCount},</if>" +
+            "<if test='corpId != null'>#{corpId},</if>" +
+            "<if test='qwExternalContactId != null'>#{qwExternalContactId},</if>" +
+            "<if test='qwUserId != null'>#{qwUserId},</if>" +
+            "<if test='failMsg != null'>#{failMsg},</if>" +
+            "<if test='payload != null'>#{payload},</if>" +
+            "<if test='response != null'>#{response},</if>" +
+            "<if test='createTime != null'>#{createTime},</if>" +
+            "<if test='updateTime != null'>#{updateTime},</if>" +
+            "<if test='updateBy != null'>#{updateBy},</if>" +
+            "<if test='createBy != null'>#{createBy},</if>" +
+            "<if test='log_type != null'>#{logType},</if>" +
+            "</trim>" +
+            "</script>")
+    @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id")
+    int insertSelective(FsTagUpdateQueue record);
+
+
+
+    @Insert("<script>" +
+            "INSERT IGNORE INTO fs_tag_update_queue (" +
+            "course_log_id, is_first, course_id, tag_id, tag_name, operation_type, video_id, status, retry_count, " +
+            "corp_id, qw_user_id, qw_external_contact_id, fail_msg, payload, response, create_time, update_time, update_by, create_by, log_type,tg_id,watching_tg_id,watched_tg_id,watching_tag_id,watched_tag_id,tag_group_id" +
+            ") VALUES " +
+            "<foreach collection='list' item='item' separator=','>" +
+            "(" +
+            "#{item.courseLogId}, #{item.isFirst}, #{item.courseId}, #{item.tagId}, #{item.tagName}, #{item.operationType}, #{item.videoId}, #{item.status}, #{item.retryCount}, " +
+            "#{item.corpId}, #{item.qwUserId}, #{item.qwExternalContactId}, #{item.failMsg}, #{item.payload}, #{item.response}, #{item.createTime}," +
+            " #{item.updateTime}, #{item.updateBy}, #{item.createBy}, #{item.logType},#{item.tgId},#{item.watchingTgId},#{item.watchedTgId},#{item.watchingTagId},#{item.watchedTagId},#{item.tagGroupId}" +
+            ")" +
+            "</foreach>" +
+            "</script>")
+    int batchInsert(@Param("list") List<FsTagUpdateQueue> list);
+
+
+    @Update("<script>" +
+            "UPDATE fs_tag_update_queue " +
+            "<set>" +
+            "<if test='courseLogId != null'>course_log_id = #{courseLogId},</if>" +
+            "<if test='courseId != null'>course_id = #{courseId},</if>" +
+            "<if test='tagId != null'>tag_id = #{tagId},</if>" +
+            "<if test='tagName != null'>tag_name = #{tagName},</if>" +
+            "<if test='operationType != null'>operation_type = #{operationType},</if>" +
+            "<if test='videoId != null'>video_id = #{videoId},</if>" +
+            "<if test='status != null'>status = #{status},</if>" +
+            "<if test='retryCount != null'>retry_count = #{retryCount},</if>" +
+            "<if test='corpId != null'>corp_id = #{corpId},</if>" +
+            "<if test='qwUserId != null'>qw_user_id = #{qwUserId},</if>" +
+            "<if test='failMsg != null'>fail_msg = #{failMsg},</if>" +
+            "<if test='payload != null'>payload = #{payload},</if>" +
+            "<if test='response != null'>response = #{response},</if>" +
+            "<if test='createTime != null'>create_time = #{createTime},</if>" +
+            "<if test='updateTime != null'>update_time = #{updateTime},</if>" +
+            "<if test='updateBy != null'>update_by = #{updateBy},</if>" +
+            "<if test='createBy != null'>create_by = #{createBy},</if>" +
+            "<if test='logType != null'>log_type = #{logType},</if>" +
+            "</set> " +
+            "WHERE id = #{id}" +
+            "</script>")
+    int updateSelective(FsTagUpdateQueue record);
+
+
+
+    @Update("<script>" +
+            "<foreach collection='list' item='item' separator=';'>" +
+            "UPDATE fs_tag_update_queue" +
+            "<set>" +
+            "<if test='item.courseLogId != null'>course_log_id = #{item.courseLogId},</if>" +
+            "<if test='item.isFirst != null'>is_first = #{item.isFirst},</if>" +
+            "<if test='item.courseId != null'>course_id = #{item.courseId},</if>" +
+            "<if test='item.tagId != null'>tag_id = #{item.tagId},</if>" +
+            "<if test='item.tagName != null'>tag_name = #{item.tagName},</if>" +
+            "<if test='item.operationType != null'>operation_type = #{item.operationType},</if>" +
+            "<if test='item.videoId != null'>video_id = #{item.videoId},</if>" +
+            "<if test='item.status != null'>status = #{item.status},</if>" +
+            "<if test='item.retryCount != null'>retry_count = #{item.retryCount},</if>" +
+            "<if test='item.corpId != null'>corp_id = #{item.corpId},</if>" +
+            "<if test='item.qwUserId != null'>qw_user_id = #{item.qwUserId},</if>" +
+            "<if test='item.qwExternalContactId != null'>qw_external_contact_id = #{item.qwExternalContactId},</if>" +
+            "<if test='item.failMsg != null'>fail_msg = #{item.failMsg},</if>" +
+            "<if test='item.payload != null'>payload = #{item.payload},</if>" +
+            "<if test='item.response != null'>response = #{item.response},</if>" +
+            "<if test='item.createTime != null'>create_time = #{item.createTime},</if>" +
+            "<if test='item.updateTime != null'>update_time = #{item.updateTime},</if>" +
+            "<if test='item.updateBy != null'>update_by = #{item.updateBy},</if>" +
+            "<if test='item.createBy != null'>create_by = #{item.createBy},</if>" +
+            "<if test='item.logType != null'>log_type = #{item.logType},</if>" +
+            "<if test='item.nextExecuteTime != null'>next_execute_time = #{item.nextExecuteTime},</if>" +
+            "</set>" +
+            " WHERE id = #{item.id}" +
+            "</foreach>" +
+            "</script>")
+    int batchUpdateSelective(@Param("list") List<FsTagUpdateQueue> list);
+
+}

+ 29 - 0
fs-service/src/main/java/com/fs/tag/service/FsTagUpdateService.java

@@ -0,0 +1,29 @@
+package com.fs.tag.service;
+
+import com.fs.course.domain.FsCourseWatchLog;
+
+import java.util.List;
+
+/**
+ * 完课打备注服务类
+ */
+public interface FsTagUpdateService {
+
+    /**
+     * 正课正在看课
+     * @param logs 日志
+     */
+    void onCourseWatchingBatch(List<FsCourseWatchLog> logs);
+
+    /**
+     * 正课完成看课
+     * @param logs 日志
+     */
+    void onCourseWatchFinishedBatch(List<FsCourseWatchLog> logs);
+
+
+    /**
+     * 处理数据
+     */
+    void handleData();
+}

+ 318 - 0
fs-service/src/main/java/com/fs/tag/service/impl/FsTagUpdateServiceImpl.java

@@ -0,0 +1,318 @@
+package com.fs.tag.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.domain.FsCourseWatchLog;
+import com.fs.course.domain.FsUserCourse;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.mapper.FsUserCourseMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.qw.cache.IQwUserCacheService;
+import com.fs.qw.domain.QwExternalContact;
+import com.fs.qw.domain.QwTag;
+import com.fs.qw.domain.QwTagGroup;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.qw.mapper.QwTagGroupMapper;
+import com.fs.qw.mapper.QwTagMapper;
+import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.IQwTagGroupService;
+import com.fs.qw.vo.QwTagGroupAddParam;
+import com.fs.qw.vo.QwTagVO;
+import com.fs.qwApi.domain.QwAddTagResult;
+import com.fs.qwApi.domain.QwResult;
+import com.fs.qwApi.domain.inner.InTag;
+import com.fs.qwApi.domain.inner.TagData;
+import com.fs.qwApi.param.QwAddTagParam;
+import com.fs.qwApi.param.QwEditUserTagParam;
+import com.fs.qwApi.service.QwApiService;
+import com.fs.tag.domain.FsTagUpdateQueue;
+import com.fs.tag.mapper.FsTagUpdateQueueMapper;
+import com.fs.tag.service.FsTagUpdateService;
+import com.google.common.util.concurrent.RateLimiter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.apache.http.util.Asserts;
+import org.checkerframework.checker.signature.qual.PolySignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Slf4j
+@Service("fsTagUpdateService")
+public class FsTagUpdateServiceImpl implements FsTagUpdateService {
+
+
+    @Autowired
+    private FsTagUpdateQueueMapper fsTagUpdateQueueMapper;
+    @Autowired
+    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    private IQwUserCacheService qwUserCacheService;
+    @Autowired
+    private QwApiService qwApiService;
+
+    @Autowired
+    private QwTagMapper qwTagMapper;
+
+    @Autowired
+    private QwTagGroupMapper qwTagGroupMapper;
+
+    @Autowired
+    private IQwTagGroupService qwTagGroupService;
+
+    @Autowired
+    private FsUserCourseMapper fsUserCourseMapper;
+
+    @Autowired
+    private QwExternalContactMapper qwExternalContactMapper;
+
+    @Value("${tag.thread.num:5}")
+    private Integer TAG_THREAD_NUM;
+
+    @Value("${tag.rate.limit:30}")
+    private Integer RATE_LIMIT_NUM;
+    /**
+     * 标签组最大数量
+     */
+    private static final Integer TAG_MAX_NUM = 100;
+
+    /**
+     * 接口限流
+     */
+    private RateLimiter rateLimiter;
+
+    /**
+     * 看课自动打标签开关
+     */
+    @Value("${qw.enableAutoTag:0}")
+    private Integer enableAutoTag;
+
+    @PostConstruct
+    public void init(){
+        this.rateLimiter = RateLimiter.create(RATE_LIMIT_NUM);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
+    public void onCourseWatchingBatch(List<FsCourseWatchLog> logs) {
+
+        if(ObjectUtil.equal(enableAutoTag,0)){
+            return;
+        }
+        Map<Long, FsUserCourseVideo> courseVideoMap = fsUserCourseVideoMapper.selectAllMap();
+        // 用户(这里用户用的是企微外部联系人ID)+videoId+status 唯一
+
+        // 先导课看课记录
+        List<FsTagUpdateQueue> batchData = new ArrayList<>();
+        for (FsCourseWatchLog item : logs) {
+            FsTagUpdateQueue task = new FsTagUpdateQueue();
+            task.setCourseId(item.getCourseId());
+            task.setVideoId(item.getVideoId());
+            task.setCourseLogId(item.getLogId());
+            task.setTagId(null);
+            task.setTagName(null);
+
+            task.setLogType(0);
+            task.setOperationType(0);
+            task.setStatus(0);
+            task.setRetryCount(0);
+            task.setQwExternalContactId(item.getQwExternalContactId());
+            task.setQwUserId(item.getQwUserId());
+
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
+            }
+            if(ObjectUtils.isNull(fsUserCourseVideo)) {
+                String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
+                log.error(errorMsg);
+                task.setStatus(3);
+                task.setRetryCount(3);
+                task.setFailMsg(errorMsg);
+                batchData.add(task);
+                continue;
+            }
+            task.setTagGroupId(fsUserCourseVideo.getTagGroupId());
+            task.setTgId(fsUserCourseVideo.getTgId());
+            task.setWatchingTagId(fsUserCourseVideo.getWatchingTagId());
+            task.setWatchedTagId(fsUserCourseVideo.getWatchedTagId());
+            task.setWatchingTgId(fsUserCourseVideo.getWatchingTgId());
+            task.setWatchedTgId(fsUserCourseVideo.getWatchedTgId());
+
+            if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
+                task.setIsFirst(1);
+            } else {
+                task.setIsFirst(0);
+            }
+            task.setSort(fsUserCourseVideo.getCourseSort());
+            batchData.add(task);
+        }
+
+
+        fsTagUpdateQueueMapper.batchInsert(batchData);
+    }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
+    public void onCourseWatchFinishedBatch(List<FsCourseWatchLog> logs) {
+        if(ObjectUtil.equal(enableAutoTag,0)){
+            return;
+        }
+        Map<Long, FsUserCourseVideo> courseVideoMap = fsUserCourseVideoMapper.selectAllMap();
+
+        // 先导课看课记录
+        List<FsTagUpdateQueue> batchData = new ArrayList<>();
+        for (FsCourseWatchLog item : logs) {
+            FsTagUpdateQueue task = new FsTagUpdateQueue();
+            task.setCourseId(item.getCourseId());
+            task.setVideoId(item.getVideoId());
+            task.setCourseLogId(item.getLogId());
+            task.setTagId(null);
+            task.setTagName(null);
+            task.setOperationType(0);
+            task.setStatus(0);
+            task.setRetryCount(0);
+            task.setQwExternalContactId(item.getQwExternalContactId());
+            task.setQwUserId(item.getQwUserId());
+            task.setLogType(1);
+            String corpId = qwUserCacheService.queryCorpIdByQwUserId(item.getQwUserId());
+            if(StringUtils.isNotNull(corpId)){
+                task.setCorpId(corpId);
+            }
+            FsUserCourseVideo fsUserCourseVideo = courseVideoMap.get(task.getVideoId());
+            if(ObjectUtils.isNull(fsUserCourseVideo)) {
+                String errorMsg = String.format("该条记录 %d 找不到对应的课堂视频", task.getVideoId());
+                log.error(errorMsg);
+                task.setStatus(3);
+                task.setRetryCount(3);
+                task.setFailMsg(errorMsg);
+                batchData.add(task);
+                continue;
+            }
+            task.setTagGroupId(fsUserCourseVideo.getTagGroupId());
+            task.setTgId(fsUserCourseVideo.getTgId());
+            task.setWatchingTagId(fsUserCourseVideo.getWatchingTagId());
+            task.setWatchedTagId(fsUserCourseVideo.getWatchedTagId());
+            task.setWatchingTgId(fsUserCourseVideo.getWatchingTgId());
+            task.setWatchedTgId(fsUserCourseVideo.getWatchedTgId());
+
+            if(ObjectUtil.equal(fsUserCourseVideo.getIsFirst(),1)) {
+                task.setIsFirst(1);
+            } else {
+                task.setIsFirst(0);
+            }
+            task.setSort(fsUserCourseVideo.getCourseSort());
+            batchData.add(task);
+        }
+
+
+        fsTagUpdateQueueMapper.batchInsert(batchData);
+    }
+
+    @Override
+    public void handleData() {
+        List<FsTagUpdateQueue> tasks = fsTagUpdateQueueMapper.selectPending();
+        if(CollectionUtils.isEmpty(tasks)){
+            log.info("找不到可处理的任务,已跳过!");
+            return;
+        }
+        ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
+        ExecutorService executor = Executors.newFixedThreadPool(TAG_THREAD_NUM);
+        CountDownLatch latch = new CountDownLatch(tasks.size());
+
+        for (FsTagUpdateQueue task : tasks) {
+            executor.submit(() -> {
+                String lockKey = task.getCourseId() + "_" + task.getVideoId();
+                ReentrantLock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
+                lock.lock();
+                try {
+                    processSingleTask(task);
+                } finally {
+                    lock.unlock();
+                    latch.countDown();
+                }
+            });
+        }
+        try {
+            latch.await();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        try {
+            executor.shutdown();
+            if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
+                executor.shutdownNow();
+                // 再次等待确保已经关闭
+                if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
+                    log.warn("线程池未能在预期时间内关闭");
+                }
+            }
+        } catch (InterruptedException e) {
+            executor.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+
+        if(CollectionUtils.isNotEmpty(tasks)){
+            fsTagUpdateQueueMapper.batchUpdateSelective(tasks);
+        }
+
+    }
+
+    private void processSingleTask(FsTagUpdateQueue fsTagUpdateQueue) {
+        try {
+            // 调用企微API更新标签
+            QwEditUserTagParam qwEditUserTagParam = new QwEditUserTagParam();
+            QwExternalContact qwExternalContact = qwExternalContactMapper
+                    .selectQwExternalContactById(fsTagUpdateQueue.getQwExternalContactId());
+            if(qwExternalContact == null) {
+                throw new IllegalArgumentException(String.format("企微外部联系人 %s 未找到!", fsTagUpdateQueue.getQwExternalContactId()));
+            }
+            qwEditUserTagParam.setUserid(qwExternalContact.getUserId());
+            qwEditUserTagParam.setExternal_userid(qwExternalContact.getExternalUserId());
+
+            rateLimiter.acquire();
+
+            // 如果是看课中
+            if(ObjectUtil.equal(fsTagUpdateQueue.getLogType(),0)){
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(fsTagUpdateQueue.getWatchingTagId()));
+            } else {
+                // 已完课
+                qwEditUserTagParam.setAdd_tag(Collections.singletonList(fsTagUpdateQueue.getWatchedTagId()));
+                qwEditUserTagParam.setRemove_tag(Collections.singletonList(fsTagUpdateQueue.getWatchingTagId()));
+            }
+
+            QwResult qwResult = qwApiService.editUserTag(qwEditUserTagParam, fsTagUpdateQueue.getCorpId());
+            fsTagUpdateQueue.setPayload(JSON.toJSONString(qwEditUserTagParam));
+            fsTagUpdateQueue.setResponse(JSON.toJSONString(qwResult));
+            // 打标签成功
+            if(ObjectUtil.equal(qwResult.getErrcode(),0)) {
+                fsTagUpdateQueue.setStatus(2);
+                fsTagUpdateQueue.setRetryCount(0);
+            } else {
+                throw new RuntimeException(String.format("打标签失败 原因: %s", JSON.toJSONString(qwResult)));
+            }
+        } catch (Exception e){
+            fsTagUpdateQueue.setStatus(3);
+            fsTagUpdateQueue.setRetryCount(fsTagUpdateQueue.getRetryCount()+1);
+            fsTagUpdateQueue.setFailMsg(ExceptionUtils.getFullStackTrace(e));
+            fsTagUpdateQueue.setNextExecuteTime(LocalDateTime.now().plusHours(1));
+        }
+    }
+
+
+}

+ 6 - 0
fs-service/src/main/resources/application-config-druid-cqtyt.yml

@@ -65,6 +65,12 @@ watch:
 fs :
   commonApi: http://172.16.0.9:8010
   h5CommonApi: http://192.168.0.18:7771
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb1
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
 nuonuo:
   key: 10924508
   secret: A2EB20764D304D16

+ 6 - 0
fs-service/src/main/resources/application-config-druid-fby.yml

@@ -87,6 +87,12 @@ fs :
 nuonuo:
   key: 10924508
   secret: A2EB20764D304D16
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb5
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
 
 # 存储捅配置
 tencent_cloud_config:

+ 3 - 3
fs-service/src/main/resources/application-config-druid-heyantang.yml

@@ -29,7 +29,7 @@ wx:
     subAppId:  #服务商模式下的子商户公众账号ID
     subMchId:  #服务商模式下的子商户号
     keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
-    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
+    notifyUrl: https://userapp.yytcdta.com/app/wxpay/wxPayNotify
   mp:
     useRedis: false
     redisConfig:
@@ -37,8 +37,8 @@ wx:
       port: 6379
       timeout: 2000
     configs:
-      - appId: wx9df02b2236d4fbf2 # 第一个公众号的appid
-        secret: 4f685750d40ab334b6f2f54b7d121389 # 公众号的appsecret
+      - appId: wx375d93939326f0b5 # 第一个公众号的appid
+        secret: 4059109327ec47d0b468c12aa76e5f78 # 公众号的appsecret
         token: PPKOdAlCoMO # 接口配置里的Token值
         aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
 aifabu:  #爱链接

+ 10 - 3
fs-service/src/main/resources/application-config-druid-jnlzjk.yml

@@ -57,8 +57,8 @@ watch:
   password3: v9xsKuqn_$d2y
 
 fs :
-  commonApi: http://127.0.0.1:7771
-  h5CommonApi: http://127.0.0.1:7771
+  commonApi: http://10.206.0.12:7771
+  h5CommonApi: http://10.206.0.12:7771
   jwt:
     # 加密秘钥
     secret: 3e6d9c0b4a7f1e2d5c4e0d3c6b9a2f5e
@@ -91,12 +91,19 @@ headerImg:
   imgUrl:
 
 ipad:
-  ipadUrl: http://ipadjnlzjk.ylrztop.com
+  ipadUrl: http://ipad.ljhehualu.com
   aiApi: http://49.232.181.28:3000/api
   voiceApi: http://129.28.187.88:8667
   commonApi: http://129.28.187.88:7771
 wx_miniapp_temp:
   pay_order_temp_id:
   inquiry_temp_id:
+qw:
+  enableAutoTag: 1
+tag:
+  thread:
+    num: 5
+  rate:
+    limit: 30
 
 

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

@@ -43,7 +43,7 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&allowMultiQueries=true
                     username: Rtroot
                     password: Rtroot
                 # 初始连接数
@@ -176,7 +176,7 @@ cloud_host:
 headerImg:
     imgUrl: https://hzyy.obs.cn-north-4.myhuaweicloud.com/fs/20250616/1750067609692.png
 ipad:
-    ipadUrl: http://139.159.133.223:8667
+    ipadUrl: http://admin.test.ylrztop.com/ipad
     aiApi: http://1.95.196.10:3000/api
 wx_miniapp_temp:
     pay_order_temp_id:

+ 159 - 0
fs-service/src/main/resources/application-druid-heyantang-test.yml

@@ -0,0 +1,159 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-druid-heyantang,common
+    # redis 配置
+    redis:
+        # 地址
+        host: 127.0.0.1
+        # 端口,默认为6379
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 20s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        #        clickhouse:
+        #            type: com.alibaba.druid.pool.DruidDataSource
+        #            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
+        #            url: jdbc:clickhouse://cc-2vc8zzo26w0l7m2l6.public.clickhouse.ads.aliyuncs.com/sop?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
+        #            username: rt_2024
+        #            password: Yzx_19860213
+        #            initialSize: 10
+        #            maxActive: 100
+        #            minIdle: 10
+        #            maxWait: 6000
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://118.24.172.242:2345/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_c123232014^$
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    enabled: false
+                    url:
+                    username:
+                    password:
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+        sop:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://118.24.172.242:2345/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_c123232014^$
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+rocketmq:
+    name-server: rmq-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    consumer:
+        group: test-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+openIM:
+    secret: openIM123
+    userID: imAdmin
+    url: https://web.im.fbylive.com/api
+#是否使用新im
+im:
+    type: NONE
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: false

+ 7 - 0
fs-service/src/main/resources/mapper/company/CompanyMapper.xml

@@ -280,4 +280,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectCompanyMoneyAllList" resultType="com.fs.company.domain.Company">
         select company_id,company_name,money from company where is_del= 0
     </select>
+
+    <update id="batchUpdateCompany" parameterType="java.util.List">
+        <foreach collection="list" item="company" separator=";">
+            update company set money = #{company.money}
+            WHERE company_id = #{company.companyId}
+        </foreach>
+    </update>
 </mapper>

+ 4 - 2
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -93,7 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         u.qw_status, u.phonenumber, u.create_time, u.dept_id,
         u.qr_code_weixin, u.user_type, u.qr_code_wecom, u.jpush_id,
         u.is_need_register_member,u.is_allowed_all_register,
-        u.avatar,
+        u.avatar,u.address_id,
         u.qw_user_id,
         d.dept_name,
         d.leader
@@ -431,7 +431,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
 
     <sql id="selectUserVo">
-        select u.user_id,u.company_id,u.qw_user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
+        select u.user_id,u.company_id,u.qw_user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar,
+               u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by,
+               u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
                d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
                r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,
                u.is_need_register_member, u.is_allowed_all_register,u.doctor_id

+ 366 - 0
fs-service/src/main/resources/mapper/company/StatisticManageMapper.xml

@@ -0,0 +1,366 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.StatisticManageMapper">
+
+    <resultMap id="CompanyDeptUserListMap" type="com.fs.company.domain.CompanyDeptUserInfo">
+        <id column="company_id" property="companyId" />
+        <result column="company_name" property="companyName"/>
+        <result column="dept_id" property="deptId"/>
+        <result column="dept_name" property="deptName"/>
+        <result column="user_id" property="userId"/>
+        <result column="user_name" property="userName"/>
+        <result column="nick_name" property="nickName"/>
+    </resultMap>
+
+    <select id="getCompanyAndDeptAndDeptUserList" resultMap="CompanyDeptUserListMap">
+        SELECT
+            ci.company_id,
+            ci.company_name,
+            cd.dept_id,
+            cd.dept_name,
+            cu.user_id,
+            cu.user_name,
+            cu.nick_name
+        FROM company AS ci
+        LEFT JOIN company_dept AS cd ON ci.company_id = cd.company_id AND cd.STATUS = 0
+        LEFT JOIN company_user AS cu ON cu.dept_id = cd.dept_id AND cu.del_flag = 0
+        WHERE ci.is_del = 0 AND cd.del_flag = 0
+        <if test="companyId != null">
+            and ci.company_id = #{companyId}
+        </if>
+    </select>
+
+    <select id="getStatisticNum" resultType="com.fs.company.dto.CompanyDeptUserInfoDTO">
+        WITH t1 AS (
+            SELECT
+                count(*) AS lineNum
+            FROM
+                qw_external_contact AS qec
+            WHERE
+                date_format( qec.create_time, '%y%m%d' ) = date_format(now(), '%y%m%d' )
+              AND qec.company_user_id = 327
+        ),
+        t2 AS (
+                 SELECT
+                     count( qec.fs_user_id ) AS activeNum
+                 FROM
+                     qw_external_contact AS qec
+                 WHERE
+                     date_format( qec.create_time, '%y%m%d' ) = date_format(now(), '%y%m%d' )
+                   AND qec.fs_user_id IS NOT NULL
+                   AND qec.company_user_id = 327
+             ),
+             t3 AS ( SELECT count(*) AS completeNum FROM fs_course_watch_log AS fcwl WHERE date_format(fcwl.create_time, '%y%m%d' ) = date_format( now(), '%y%m%d') and fcwl.log_type = 2 AND fcwl.company_user_id = 327 ),
+             t4 AS ( SELECT count(*) AS answerNum FROM fs_course_answer_logs AS fcal WHERE date_format(fcal.create_time, '%y%m%d' ) = date_format( now(), '%y%m%d')  and fcal.company_user_id = 327 ),
+             t5 AS ( SELECT count(*) AS redPacketNum FROM fs_course_red_packet_log AS fcrpl WHERE date_format(fcrpl.create_time, '%y%m%d' ) = date_format( now(), '%y%m%d' )  and  fcrpl.company_user_id = 327 )
+        SELECT * FROM t1,t2,t3,t4,t5
+    </select>
+
+    <select id="getStatisticNumByPersonal" resultType="com.fs.company.dto.ComprehensiveStatisticsDTO">
+        WITH RECURSIVE date_range AS (
+            SELECT #{startTime} AS dt
+            UNION ALL
+            SELECT DATE_ADD(dt, INTERVAL 1 DAY)
+            FROM date_range
+            WHERE dt &lt; #{endTime}
+        ),
+        t1 AS (
+            SELECT
+            COUNT(qec.id) AS t1_count,
+            d.dt AS create_time
+            FROM date_range d
+            LEFT JOIN qw_external_contact qec
+            ON DATE(qec.create_time) = d.dt
+        <if test="userIds != null">
+            <if test="dimension == 1">
+                <choose>
+                    <when test="userIds.length > 1 ">
+                        AND qec.company_user_id IN (
+                        <foreach collection="userIds" item="i" separator=",">
+                            #{i}
+                        </foreach>
+                        )
+                    </when>
+                    <otherwise>
+                        AND qec.company_user_id = #{userIds[0]}
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dimension == 2">
+                AND qec.company_id = #{userIds[0]}
+            </if>
+        </if>
+            GROUP BY d.dt
+        ),
+        t2 AS (
+            SELECT
+            COUNT(qec.id) AS t2_count,
+            d.dt AS create_time
+            FROM date_range d
+            LEFT JOIN qw_external_contact qec
+            ON DATE(qec.create_time) = d.dt
+        <if test="userIds != null">
+            <if test="dimension == 1">
+                <choose>
+                    <when test="userIds.length > 1 ">
+                        AND qec.company_user_id IN (
+                        <foreach collection="userIds" item="i" separator=",">
+                            #{i}
+                        </foreach>
+                        )
+                    </when>
+                    <otherwise>
+                        AND qec.company_user_id = #{userIds[0]}
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dimension == 2">
+                AND qec.company_id = #{userIds[0]}
+            </if>
+        </if>
+            AND qec.fs_user_id IS NOT NULL
+            GROUP BY d.dt
+        ),
+        t4 AS (
+            SELECT
+            d.dt AS create_time,
+            COUNT(fcwl.qw_external_contact_id) AS completeNum
+            FROM
+            date_range d
+            LEFT JOIN fs_course_watch_log AS fcwl
+            ON DATE(fcwl.create_time) = d.dt
+            AND fcwl.log_type = 2
+        <if test="userIds != null">
+            <if test="dimension == 1">
+                <choose>
+                    <when test="userIds.length > 1 ">
+                        AND fcwl.company_user_id IN (
+                        <foreach collection="userIds" item="i" separator=",">
+                            #{i}
+                        </foreach>
+                        )
+                    </when>
+                    <otherwise>
+                        AND fcwl.company_user_id = #{userIds[0]}
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dimension == 2">
+                AND fcwl.company_id = #{userIds[0]}
+            </if>
+        </if>
+            GROUP BY d.dt
+        ),
+        t5 AS (
+            SELECT
+            d.dt AS create_time,
+            COUNT(fcal.log_id) AS answerNum
+            FROM
+            date_range d
+            LEFT JOIN fs_course_answer_logs AS fcal
+            ON DATE(fcal.create_time) = d.dt
+        <if test="userIds != null">
+            <if test="dimension == 1">
+                <choose>
+                    <when test="userIds.length > 1 ">
+                        AND fcal.company_user_id IN (
+                        <foreach collection="userIds" item="i" separator=",">
+                            #{i}
+                        </foreach>
+                        )
+                    </when>
+                    <otherwise>
+                        AND fcal.company_user_id = #{userIds[0]}
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dimension == 2">
+                AND fcal.company_id = #{userIds[0]}
+            </if>
+        </if>
+            GROUP BY d.dt
+        ),
+        t6 AS (
+            SELECT
+            d.dt AS create_time,
+            COUNT(fcrpl.log_id) AS redPacketNum
+            FROM
+            date_range d
+            LEFT JOIN fs_course_red_packet_log AS fcrpl
+            ON DATE(fcrpl.create_time) = d.dt
+        <if test="userIds != null">
+            <if test="dimension == 1">
+                <choose>
+                    <when test="userIds.length > 1 ">
+                        AND fcrpl.company_user_id IN (
+                        <foreach collection="userIds" item="i" separator=",">
+                            #{i}
+                        </foreach>
+                        )
+                    </when>
+                    <otherwise>
+                        AND fcrpl.company_user_id = #{userIds[0]}
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="dimension == 2">
+                AND fcrpl.company_id = #{userIds[0]}
+            </if>
+        </if>
+            GROUP BY d.dt
+        )
+        SELECT
+            t1.create_time as dateStr,
+            t1.t1_count as lineNum,
+            t2.t2_count as activeNum,
+            t4.completeNum,
+            t5.answerNum,
+            t6.redPacketNum
+        FROM t1
+        INNER JOIN t2 ON t1.create_time = t2.create_time
+        INNER JOIN t4 ON t1.create_time = t4.create_time
+        INNER JOIN t5 ON t1.create_time = t5.create_time
+        INNER JOIN t6 ON t1.create_time = t6.create_time
+        ORDER BY t1.create_time
+    </select>
+
+    <select id="getCompanyInfo" resultType="com.fs.company.domain.CompanyDeptUserInfo">
+        SELECT
+            ci.company_id,
+            ci.company_name
+        FROM
+            company AS ci
+        WHERE
+            ci.is_del = 0
+    </select>
+
+    <!-- 基础字段映射(复用) -->
+    <sql id="Base_Column_List">
+        id, company_id, company_name, dept_id, dept_name, 
+        user_id, user_name, nick_name, statistics_time, 
+        line_num, active_num, complete_num, answer_num, 
+        red_packet_num, red_packet_amount, create_time, update_time
+    </sql>
+
+    <!-- 1. 插入数据(全字段插入) -->
+    <insert id="insert" parameterType="com.fs.company.domain.ComprehensiveDailyStats">
+        INSERT INTO user_daily_stats (
+        company_id, company_name, dept_id, dept_name,
+        user_id, user_name, nick_name, statistics_time,
+        line_num, active_num, complete_num, answer_num,
+        red_packet_num, red_packet_amount, create_time, update_time
+        ) VALUES (
+        #{companyId}, #{companyName}, #{deptId}, #{deptName},
+        #{userId}, #{userName}, #{nickName}, #{statisticsTime},
+        #{lineNum}, #{activeNum}, #{completeNum}, #{answerNum},
+        #{redPacketNum}, #{redPacketAmount}, #{createTime}, #{updateTime}
+        )
+    </insert>
+
+    <!-- 2. 插入或更新(根据唯一索引uk_user_date,存在则更新,不存在则插入) -->
+    <insert id="insertOrUpdate" parameterType="com.fs.company.domain.ComprehensiveDailyStats">
+        INSERT INTO user_daily_stats (
+            company_id, company_name, dept_id, dept_name,
+            user_id, user_name, nick_name, statistics_time,
+            line_num, active_num, complete_num, answer_num,
+            red_packet_num, red_packet_amount, create_time, update_time
+        ) VALUES (
+                     #{companyId}, #{companyName}, #{deptId}, #{deptName},
+                     #{userId}, #{userName}, #{nickName}, #{statisticsTime},
+                     #{lineNum}, #{activeNum}, #{completeNum}, #{answerNum},
+                     #{redPacketNum}, #{redPacketAmount}, NOW(), NOW()
+                 ) ON DUPLICATE KEY UPDATE
+            company_id = VALUES(company_id),
+            company_name = VALUES(company_name),
+            dept_id = VALUES(dept_id),
+            dept_name = VALUES(dept_name),
+            user_name = VALUES(user_name),
+            nick_name = VALUES(nick_name),
+            line_num = VALUES(line_num),
+            active_num = VALUES(active_num),
+            complete_num = VALUES(complete_num),
+            answer_num = VALUES(answer_num),
+            red_packet_num = VALUES(red_packet_num),
+            red_packet_amount = VALUES(red_packet_amount),
+            update_time = NOW()
+    </insert>
+
+    <!-- 3. 根据ID更新数据(全字段更新) -->
+    <update id="updateById" parameterType="com.fs.company.domain.ComprehensiveDailyStats">
+        UPDATE user_daily_stats
+        SET
+            company_id = #{companyId},
+            company_name = #{companyName},
+            dept_id = #{deptId},
+            dept_name = #{deptName},
+            user_id = #{userId},
+            user_name = #{userName},
+            nick_name = #{nickName},
+            statistics_time = #{statisticsTime},
+            line_num = #{lineNum},
+            active_num = #{activeNum},
+            complete_num = #{completeNum},
+            answer_num = #{answerNum},
+            red_packet_num = #{redPacketNum},
+            red_packet_amount = #{redPacketAmount},
+            update_time = NOW()
+        WHERE id = #{id}
+    </update>
+
+    <!-- 4. 根据用户ID和统计日期更新(部分字段更新,按需调整) -->
+    <update id="updateByUserAndDate" parameterType="com.fs.company.domain.ComprehensiveDailyStats">
+        UPDATE user_daily_stats
+        SET
+            line_num = #{lineNum},
+            active_num = #{activeNum},
+            complete_num = #{completeNum},
+            answer_num = #{answerNum},
+            red_packet_num = #{redPacketNum},
+            red_packet_amount = #{redPacketAmount},
+            update_time = NOW()
+        WHERE user_id = #{userId} AND statistics_time = #{statisticsTime}
+    </update>
+
+    <!-- 5. 根据ID查询 -->
+    <select id="selectById" resultType="com.fs.company.domain.ComprehensiveDailyStats" parameterType="java.lang.Long">
+        SELECT <include refid="Base_Column_List"/> FROM user_daily_stats WHERE id = #{id}
+    </select>
+
+    <!-- 6. 根据用户ID和统计日期查询 -->
+    <select id="selectByUserAndDate" resultType="com.fs.company.domain.ComprehensiveDailyStats">
+        SELECT <include refid="Base_Column_List"/>
+        FROM user_daily_stats
+        WHERE user_id = #{userId} AND statistics_time = #{statisticsTime}
+    </select>
+
+    <!-- 7. 根据公司ID和日期范围查询 -->
+    <select id="selectByCompanyAndDateRange" resultType="com.fs.company.domain.ComprehensiveDailyStats">
+        SELECT <include refid="Base_Column_List"/>
+        FROM user_daily_stats
+        WHERE company_id = #{companyId}
+        AND statistics_time BETWEEN #{startTime} AND #{endTime}
+        ORDER BY statistics_time ASC
+    </select>
+
+    <!-- 8. 根据部门ID和日期范围查询 -->
+    <select id="selectByDeptAndDateRange" resultType="com.fs.company.domain.ComprehensiveDailyStats">
+        SELECT <include refid="Base_Column_List"/>
+        FROM user_daily_stats
+        WHERE dept_id = #{deptId}
+        AND statistics_time BETWEEN #{startTime} AND #{endTime}
+        ORDER BY statistics_time ASC
+    </select>
+
+    <!-- 9. 删除数据(根据ID) -->
+    <delete id="deleteById" parameterType="java.lang.Long">
+        DELETE FROM user_daily_stats WHERE id = #{id}
+    </delete>
+
+    <!-- 10. 删除数据(根据用户ID和统计日期) -->
+    <delete id="deleteByUserAndDate">
+        DELETE FROM user_daily_stats WHERE user_id = #{userId} AND statistics_time = #{statisticsTime}
+    </delete>
+
+</mapper>

+ 13 - 6
fs-service/src/main/resources/mapper/course/FsCourseFinishTempParentMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.course.mapper.FsCourseFinishTempParentMapper">
-    
+
     <resultMap type="FsCourseFinishTempParent" id="FsCourseFinishTempParentResult">
         <result property="id"    column="id"    />
         <result property="name"    column="name"    />
@@ -22,18 +22,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectFsCourseFinishTempParentList" parameterType="FsCourseFinishTempParent" resultMap="FsCourseFinishTempParentResult">
         <include refid="selectFsCourseFinishTempParentVo"/>
-        <where>  
+        <where>
             <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
             <if test="courseId != null "> and course_id = #{courseId}</if>
             <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="createBy != null "> and create_by = #{createBy}</if>
+            <if test="userIds != null and !userIds.isEmpty() ">
+                AND create_by IN
+                 <foreach collection='userIds' item='item' open='(' separator=',' close=')'>
+                 #{item}
+                </foreach>
+             </if>
         </where>
     </select>
-    
+
     <select id="selectFsCourseFinishTempParentById" parameterType="Long" resultMap="FsCourseFinishTempParentResult">
         <include refid="selectFsCourseFinishTempParentVo"/>
         where id = #{id}
     </select>
-        
+
     <insert id="insertFsCourseFinishTempParent" parameterType="FsCourseFinishTempParent" useGeneratedKeys="true" keyProperty="id">
         insert into fs_course_finish_temp_parent
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -75,9 +82,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteFsCourseFinishTempParentByIds" parameterType="String">
-        delete from fs_course_finish_temp_parent where id in 
+        delete from fs_course_finish_temp_parent where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+</mapper>

+ 6 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml

@@ -233,6 +233,12 @@
             <if test="listingEndTime != null">listing_end_time = #{listingEndTime},</if>
             <if test="projectId != null">project_id = #{projectId},</if>
             <if test="isFirst != null">is_first = #{isFirst},</if>
+            <if test="tagGroupId != null">tag_group_id = #{tagGroupId},</if>
+            <if test="watchingTagId != null">watching_tag_id = #{watchingTagId},</if>
+            <if test="watchedTagId != null">watched_tag_id = #{watchedTagId},</if>
+            <if test="tgId != null">tg_id = #{tgId},</if>
+            <if test="watchingTgId != null">watching_tg_id = #{watchingTgId},</if>
+            <if test="watchedTgId != null">watched_tg_id = #{watchedTgId},</if>
         </trim>
         where video_id = #{videoId}
     </update>

+ 14 - 0
fs-service/src/main/resources/mapper/course/FsVideoResourceMapper.xml

@@ -26,4 +26,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </if>
         order by rr.sort,rr.id desc
     </select>
+
+    <select id="selectByIds" parameterType="String" resultType="com.fs.course.domain.FsVideoResource">
+        SELECT *
+        FROM fs_video_resource
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" close=")" separator=",">
+            #{id}
+        </foreach>
+        ORDER BY FIELD(id,
+        <foreach collection="ids" item="id" separator=",">
+            #{id}
+        </foreach>
+        )
+    </select>
 </mapper>

+ 3 - 0
fs-service/src/main/resources/mapper/his/FsPackageOrderMapper.xml

@@ -317,4 +317,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             LEFT JOIN fs_package P ON O.package_id = P.package_id
         where O.is_pay = 1 ORDER BY RAND() desc LIMIT 20
     </select>
+    <select id="selectOutTimeOrderList" resultType="com.fs.his.domain.FsPackageOrder">
+        select * from fs_package_order  where status = 1 AND NOW() &gt; DATE_ADD(create_time, INTERVAL ${unPayTime} MINUTE)
+    </select>
 </mapper>

+ 4 - 0
fs-service/src/main/resources/mapper/his/FsStoreOrderMapper.xml

@@ -2133,5 +2133,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             AND company_user_id = #{companyUserId}
         </if>
     </select>
+    <select id="selectOutTimeOrderList" resultType="com.fs.his.domain.FsStoreOrder">
+        <include refid="selectFsStoreOrderVo"/>
+        where is_del = 0 and status = 1 and NOW() &gt; DATE_ADD(create_time, INTERVAL ${unPayTime} MINUTE)
+    </select>
 
 </mapper>

+ 2 - 0
fs-service/src/main/resources/mapper/his/FsStorePaymentMapper.xml

@@ -58,6 +58,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="storeId != null "> and store_id = #{storeId}</if>
             <if test="businessCode != null "> and business_code = #{businessCode}</if>
             <if test="appId != null and appId !=''"> and app_id = #{appId}</if>
+            <if test="beginTime != null and beginTime !=''"> and create_time >= #{beginTime}</if>
+            <if test="endTime != null and endTime !=''"> and create_time &lt; #{endTime}</if>
         </where>
     </select>
 

+ 3 - 0
fs-service/src/main/resources/mapper/qw/QwTagGroupMapper.xml

@@ -108,4 +108,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      </where>
         )
     </select>
+    <select id="selectQwTagGroupByName" resultType="com.fs.qw.domain.QwTagGroup">
+        select * from qw_tag_group where name=#{tagGroup} and corp_id=#{corpId} limit 1
+    </select>
 </mapper>

+ 14 - 0
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -289,5 +289,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and qu.qw_user_name like concat('%', #{nickName}, '%')
         </if>
     </select>
+    <!--  递归查询子部门,不包括自己,深度5   -->
+    <select id="selectDeptByParentId" resultType="java.lang.Long">
+        WITH RECURSIVE sub_dept AS (
+        SELECT dept_id, parentid, 1 as depth
+        FROM qw_dept
+        WHERE parentid = #{deptId} and corp_id=#{corpId}
+        UNION ALL
+        SELECT qd.dept_id, qd.parentid, sd.depth + 1
+        FROM qw_dept qd
+        INNER JOIN sub_dept sd ON qd.parentid = sd.dept_id
+        WHERE sd.depth &lt; 5 and qd.corp_id=#{corpId}
+        )
+        SELECT dept_id FROM sub_dept
+    </select>
 
 </mapper>

部分文件因文件數量過多而無法顯示