Преглед на файлове

Merge remote-tracking branch 'origin/Payment-Configuration' into Payment-Configuration

yys преди 3 седмици
родител
ревизия
427244bbb2

+ 68 - 0
fs-company/src/main/java/com/fs/company/controller/delete/DataCleanController.java

@@ -0,0 +1,68 @@
+package com.fs.company.controller.delete;
+
+import com.fs.delete.service.DataCleanService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/api/data-clean")
+public class DataCleanController {
+
+    @Autowired
+    private DataCleanService dataCleanService;
+
+//    /**
+//     * 统计需要删除的数据量
+//     */
+//    @GetMapping("/statistics")
+//    public Map<String, Object> statistics() {
+//        log.info("统计需要删除的数据");
+//        Map<String, Object> result = new HashMap<>();
+//
+//        try {
+//            dataCleanService.printStatistics();
+//            result.put("code", 200);
+//            result.put("message", "统计完成,请查看日志");
+//        } catch (Exception e) {
+//            log.error("统计失败", e);
+//            result.put("code", 500);
+//            result.put("message", "统计失败: " + e.getMessage());
+//        }
+//
+//        return result;
+//    }
+//
+//    /**
+//     * 执行数据清理
+//     */
+//    @GetMapping("/execute")
+//    public Map<String, Object> execute() {
+//        log.info("收到数据清理请求");
+//        Map<String, Object> result = new HashMap<>();
+//
+//        try {
+//            // 异步执行
+//            new Thread(() -> {
+//                try {
+//                    dataCleanService.cleanData();
+//                } catch (Exception e) {
+//                    log.error("数据清理执行失败", e);
+//                }
+//            }).start();
+//
+//            result.put("code", 200);
+//            result.put("message", "数据清理任务已启动,请查看日志监控进度");
+//        } catch (Exception e) {
+//            log.error("启动数据清理任务失败", e);
+//            result.put("code", 500);
+//            result.put("message", "启动失败: " + e.getMessage());
+//        }
+//
+//        return result;
+//    }
+}

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRedPacketLog.java

@@ -76,4 +76,6 @@ public class FsCourseRedPacketLog extends BaseEntity
     //商户号
     private String mchId;
 
+    private Integer watchType;
+
 }

+ 165 - 7
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -10,6 +10,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.base.BusinessException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DictUtils;
 import com.fs.common.utils.date.DateUtil;
@@ -1889,23 +1890,180 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
     @Override
     public List<AppSalesCourseStatisticsVO> selectAppSalesCourseStatisticsVO(FsCourseWatchLogStatisticsListParam param) {
 
-        // 课程统计
-        List<AppSalesCourseStatisticsVO> list = fsCourseWatchLogMapper.selectAppSalesCourseStatisticsVO(param);
+        // 校验时间为必输字段而且不能大于一个月
+        if (StringUtils.isEmpty(param.getStartDate()) || StringUtils.isEmpty(param.getEndDate())) {
+            throw new BusinessException("请选择时间");
+        }
+
+        // 看课记录
+        CompletableFuture<List<AppSalesCourseStatisticsVO>> watchFuture = CompletableFuture.supplyAsync(() -> {
+            return fsCourseWatchLogMapper.selectAppSalesCourseStatisticsVO(param);
+        });
+
+        // 答题记录
+        CompletableFuture<List<AppSalesCourseStatisticsVO>> answerFuture = CompletableFuture.supplyAsync(() -> {
+            return fsCourseAnswerLogsMapper.selectAppSalesAnswerStatisticsVO(param);
+        });
+
+        // 红包记录
+        CompletableFuture<List<AppSalesCourseStatisticsVO>> redPacketFuture = CompletableFuture.supplyAsync(() -> {
+            return fsCourseRedPacketLogMapper.selectAppSalesRedPacketStatisticsVO(param);
+        });
+
+        CompletableFuture.allOf(watchFuture, answerFuture, redPacketFuture).join();
+
+        List<AppSalesCourseStatisticsVO> list = watchFuture.join();
+        if (CollectionUtils.isEmpty(list)) {
+            return Collections.emptyList();
+        }
+
+        // 整合数据
+        List<AppSalesCourseStatisticsVO> answerList = answerFuture.join();
+        List<AppSalesCourseStatisticsVO> redPacketList = redPacketFuture.join();
+
+        Map<String, AppSalesCourseStatisticsVO> dataMap = list.stream()
+                .collect(Collectors.toMap(
+                        item -> item.getCompanyUserId() + "_" + item.getCourseId() + "_" + item.getVideoId(),
+                        Function.identity(),
+                        (e, r) -> e
+                ));
+
+        if (CollectionUtils.isNotEmpty(answerList)) {
+            Map<String, AppSalesCourseStatisticsVO> answerMap = answerList.stream()
+                    .collect(Collectors.toMap(
+                            item -> item.getCompanyUserId() + "_" + item.getCourseId() + "_" + item.getVideoId(),
+                            Function.identity(),
+                            (e, r) -> e
+                    ));
+
+            for (Map.Entry<String, AppSalesCourseStatisticsVO> entry : dataMap.entrySet()) {
+                AppSalesCourseStatisticsVO vo = entry.getValue();
+                AppSalesCourseStatisticsVO answerVO = answerMap.get(entry.getKey());
+                if (answerVO != null) {
+                    vo.setAnsweredCount(answerVO.getAnsweredCount());
+                    vo.setCorrectCount(answerVO.getCorrectCount());
+                }
+            }
+        }
+
+
+        if (CollectionUtils.isNotEmpty(redPacketList)) {
+            Map<String, AppSalesCourseStatisticsVO> redPacketMap = redPacketList.stream()
+                    .collect(Collectors.toMap(
+                            item -> item.getCompanyUserId() + "_" + item.getCourseId() + "_" + item.getVideoId(),
+                            Function.identity(),
+                            (e, r) -> e
+                    ));
+
+            for (Map.Entry<String, AppSalesCourseStatisticsVO> entry : dataMap.entrySet()) {
+                AppSalesCourseStatisticsVO vo = entry.getValue();
+                AppSalesCourseStatisticsVO redPacketVO = redPacketMap.get(entry.getKey());
+                if (redPacketVO != null) {
+                    vo.setRedPacketAmount(redPacketVO.getRedPacketAmount());
+                }
+            }
+        }
 
-        // 答题统计
-        List<AppSalesCourseStatisticsVO> answerList = fsCourseAnswerLogsMapper.selectAppSalesAnswerStatisticsVO(param);
+        // 获取销售的 app会员数和 新注册的会员数
+        CompletableFuture<List<AppSalesCourseStatisticsVO>> appNewUserFuture = CompletableFuture.supplyAsync(() -> {
+            return userMapper.selectAppSalesNewUserCountVO(param);
+        });
 
-        // 红包统计
-        List<AppSalesCourseStatisticsVO> redPacketList = fsCourseRedPacketLogMapper.selectAppSalesRedPacketStatisticsVO(param);
+        CompletableFuture<List<AppSalesCourseStatisticsVO>> appUserFuture = CompletableFuture.supplyAsync(() -> {
+            return userMapper.selectAppSalesUserCountVO(param);
+        });
 
+        CompletableFuture.allOf(appNewUserFuture, appUserFuture).join();
 
+        // 整合数据
+        List<AppSalesCourseStatisticsVO> appNewUserCountList = appNewUserFuture.join();
+        List<AppSalesCourseStatisticsVO> appUserCountList = appUserFuture.join();
 
+        if (CollectionUtils.isNotEmpty(appNewUserCountList)) {
+            Map<Long, Long> newUserCountMap = appNewUserCountList.stream()
+                    .collect(Collectors.toMap(
+                            AppSalesCourseStatisticsVO::getCompanyUserId,
+                            AppSalesCourseStatisticsVO::getNewAppUserCount,
+                            (e, r) -> e
+                    ));
 
+            for (AppSalesCourseStatisticsVO vo : dataMap.values()) {
+                Long newAppUserCount = newUserCountMap.get(vo.getCompanyUserId());
+                if (newAppUserCount != null) {
+                    vo.setNewAppUserCount(newAppUserCount);
+                }
+            }
+        }
+
+        if (CollectionUtils.isNotEmpty(appUserCountList)) {
+            Map<Long, Long> userCountMap = appUserCountList.stream()
+                    .collect(Collectors.toMap(
+                            AppSalesCourseStatisticsVO::getCompanyUserId,
+                            AppSalesCourseStatisticsVO::getAppUserCount,
+                            (e, r) -> e
+                    ));
+
+            for (AppSalesCourseStatisticsVO vo : dataMap.values()) {
+                Long appUserCount = userCountMap.get(vo.getCompanyUserId());
+                if (appUserCount != null) {
+                    vo.setAppUserCount(appUserCount);
+                }
+            }
+        }
+
+        List<AppSalesCourseStatisticsVO> resultList = new ArrayList<>(dataMap.values());
+        resultList.forEach(this::setDefaultValues);
+        return resultList;
+    }
+
+    private void setDefaultValues(AppSalesCourseStatisticsVO vo) {
+        vo.setFinishedCount(vo.getFinishedCount() != null ? vo.getFinishedCount() : 0);
+        vo.setNotWatchedCount(vo.getNotWatchedCount() != null ? vo.getNotWatchedCount() : 0);
+        vo.setInterruptCount(vo.getInterruptCount() != null ? vo.getInterruptCount() : 0);
+        vo.setWatchingCount(vo.getWatchingCount() != null ? vo.getWatchingCount() : 0);
+        vo.setAnsweredCount(vo.getAnsweredCount() != null ? vo.getAnsweredCount() : 0);
+        vo.setCorrectCount(vo.getCorrectCount() != null ? vo.getCorrectCount() : 0);
+        vo.setNewAppUserCount(vo.getNewAppUserCount() != null ? vo.getNewAppUserCount() : 0L);
+        vo.setAppUserCount(vo.getAppUserCount() != null ? vo.getAppUserCount() : 0L);
+        vo.setRedPacketCount(vo.getRedPacketCount() != null ? vo.getRedPacketCount() : 0);
+
+        if (vo.getRedPacketAmount() == null) {
+            vo.setRedPacketAmount(BigDecimal.ZERO);
+        }
+
+        // 计算完课率 = 完课数 / (完课数 + 未完课数 + 中断数 + 看课中数) * 100%,保留两位小数
+        int total = vo.getFinishedCount() + vo.getNotWatchedCount() + vo.getInterruptCount() + vo.getWatchingCount();
+        if (total > 0) {
+            BigDecimal finished = new BigDecimal(vo.getFinishedCount());
+            BigDecimal totalCount = new BigDecimal(total);
+            vo.setCompletionRate(finished.divide(totalCount, 4, RoundingMode.HALF_UP));
+        } else {
+            vo.setCompletionRate(BigDecimal.ZERO);
+        }
+        // 计算一下答题正确率
+        if (vo.getAnsweredCount() > 0) {
+            BigDecimal answered = new BigDecimal(vo.getAnsweredCount());
+            BigDecimal correct = new BigDecimal(vo.getCorrectCount());
+            vo.setCorrectRate(correct.divide(answered, 4, RoundingMode.HALF_UP));
+        } else {
+            vo.setCorrectRate(BigDecimal.ZERO);
+        }
 
+        if (vo.getSalesName() == null) {
+            vo.setSalesName("");
+        }
+
+        if (vo.getCourseName() == null) {
+            vo.setCourseName("");
+        }
 
-        return Collections.emptyList();
+        if (vo.getVideoTitle() == null) {
+            vo.setVideoTitle("");
+        }
     }
 
+
+
     /**
      * 销售维度APP看课统计报表
      */

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

@@ -5407,6 +5407,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                     redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
                     redPacketLog.setPeriodId(log.getPeriodId());
                     redPacketLog.setAppId(packetParam.getAppId());
+                    redPacketLog.setWatchType(1);
 
                     redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
 

+ 87 - 0
fs-service/src/main/java/com/fs/delete/mapper/DataCleanMapper.java

@@ -0,0 +1,87 @@
+package com.fs.delete.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+@Mapper
+public interface DataCleanMapper {
+
+    /**
+     * 查询拉黑用户ID
+     */
+    List<Long> selectBlackUserIds(@Param("projectId") Long projectId,
+                                  @Param("statusList") List<Integer> statusList,
+                                  @Param("limit") int limit);
+
+    /**
+     * 查询0观看用户ID
+     */
+    List<Long> selectZeroWatchUserIds(@Param("projectId") Long projectId,
+                                      @Param("dateLimit") String dateLimit,
+                                      @Param("limit") int limit);
+
+    /**
+     * 统计拉黑用户数量
+     */
+    int countBlackUsers(@Param("projectId") Long projectId,
+                        @Param("statusList") List<Integer> statusList);
+
+    /**
+     * 统计0观看用户数量
+     */
+    int countZeroWatchUsers(@Param("projectId") Long projectId,
+                            @Param("dateLimit") String dateLimit);
+
+    /**
+     * 备份用户关系表
+     */
+    int backupUserCompanyUser(@Param("tableSuffix") String tableSuffix,
+                              @Param("projectId") Long projectId,
+                              @Param("userIds") List<Long> userIds);
+
+    /**
+     * 备份看课记录表
+     */
+    int backupCourseWatchLog(@Param("tableSuffix") String tableSuffix,
+                             @Param("projectId") Long projectId,
+                             @Param("userIds") List<Long> userIds);
+
+    /**
+     * 备份答题记录表(关联看课记录)
+     */
+    int backupCourseAnswerLogs(@Param("tableSuffix") String tableSuffix,
+                               @Param("projectId") Long projectId,
+                               @Param("userIds") List<Long> userIds);
+
+    /**
+     * 备份红包记录表(关联看课记录)
+     */
+    int backupCourseRedPacketLog(@Param("tableSuffix") String tableSuffix,
+                                 @Param("projectId") Long projectId,
+                                 @Param("userIds") List<Long> userIds);
+
+    /**
+     * 删除答题记录(关联看课记录)
+     */
+    int deleteAnswerLogs(@Param("projectId") Long projectId,
+                         @Param("userIds") List<Long> userIds);
+
+    /**
+     * 删除红包记录(关联看课记录)
+     */
+    int deleteRedPacketLogs(@Param("projectId") Long projectId,
+                            @Param("userIds") List<Long> userIds);
+
+    /**
+     * 删除看课记录
+     */
+    int deleteWatchLogs(@Param("projectId") Long projectId,
+                        @Param("userIds") List<Long> userIds);
+
+    /**
+     * 删除用户关系
+     */
+    int deleteUserCompanyUser(@Param("projectId") Long projectId,
+                              @Param("userIds") List<Long> userIds);
+}

+ 216 - 0
fs-service/src/main/java/com/fs/delete/service/DataCleanService.java

@@ -0,0 +1,216 @@
+package com.fs.delete.service;
+
+import com.fs.delete.mapper.DataCleanMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+@Service
+public class DataCleanService {
+
+    @Autowired
+    private DataCleanMapper dataCleanMapper;
+
+    @Autowired
+    private DataSourceTransactionManager transactionManager;
+
+    // 每批处理用户数量
+//    private static final int BATCH_SIZE = 1000;
+    private static final int BATCH_SIZE = 200;
+    // 项目ID
+    private static final Long PROJECT_ID = 60L;
+    // 日期限制
+    private static final String DATE_LIMIT = "2026-03-15";
+    // 备份表后缀
+    private static final String BACKUP_SUFFIX = "_20260324_bak";
+    // 拉黑状态列表
+    private static final List<Integer> BLACK_STATUS_LIST = Arrays.asList(0, 2);
+
+    /**
+     * 执行数据清理
+     */
+    public void cleanData() {
+        log.info("========== 开始数据清理任务 ==========");
+        long startTime = System.currentTimeMillis();
+
+        try {
+            // 1. 处理拉黑用户
+            processBlackUsers();
+
+            // 2. 处理0观看用户
+            processZeroWatchUsers();
+
+            long endTime = System.currentTimeMillis();
+            log.info("========== 数据清理任务完成,总耗时: {} 秒 ==========", (endTime - startTime) / 1000);
+
+        } catch (Exception e) {
+            log.error("数据清理任务失败", e);
+            throw new RuntimeException("数据清理任务失败", e);
+        }
+    }
+
+    /**
+     * 处理拉黑用户
+     */
+    private void processBlackUsers() {
+        log.info("开始处理拉黑用户");
+
+        int batchNo = 0;
+        int totalDeleted = 0;
+        int totalCount = dataCleanMapper.countBlackUsers(PROJECT_ID, BLACK_STATUS_LIST);
+        log.info("拉黑用户总数: {}", totalCount);
+
+        while (true) {
+            // 1. 查询一批需要删除的用户ID(1000个)
+            List<Long> userIds = dataCleanMapper.selectBlackUserIds(PROJECT_ID, BLACK_STATUS_LIST, BATCH_SIZE);
+
+            if (userIds.isEmpty()) {
+                log.info("拉黑用户处理完成,共处理 {} 批", batchNo);
+                break;
+            }
+
+            batchNo++;
+            log.info("========== 处理第 {} 批拉黑用户,数量: {} ==========", batchNo, userIds.size());
+
+            // 2. 处理这一批用户(备份+删除)
+            processUserBatch(userIds);
+
+            totalDeleted += userIds.size();
+            log.info("第 {} 批处理完成,累计删除: {}/{}", batchNo, totalDeleted, totalCount);
+            log.info("========================================\n");
+
+//            break;
+        }
+
+        log.info("拉黑用户处理完成,共删除: {} 条", totalDeleted);
+    }
+
+    /**
+     * 处理0观看用户
+     */
+    private void processZeroWatchUsers() {
+        log.info("开始处理0观看用户");
+
+        int batchNo = 0;
+        int totalDeleted = 0;
+//        int totalCount = dataCleanMapper.countZeroWatchUsers(PROJECT_ID, DATE_LIMIT);
+//        log.info("0观看用户总数: {}", totalCount);
+
+        while (true) {
+            // 1. 查询一批需要删除的用户ID(1000个)
+            List<Long> userIds = dataCleanMapper.selectZeroWatchUserIds(PROJECT_ID, DATE_LIMIT, BATCH_SIZE);
+
+            if (userIds.isEmpty()) {
+                log.info("0观看用户处理完成,共处理 {} 批", batchNo);
+                break;
+            }
+
+            batchNo++;
+            log.info("========== 处理第 {} 批0观看用户,数量: {} ==========", batchNo, userIds.size());
+
+            // 2. 处理这一批用户(备份+删除)
+            processUserBatch(userIds);
+
+            totalDeleted += userIds.size();
+            log.info("第 {} 批处理完成,累计删除: {}/{}", batchNo, totalDeleted);
+            log.info("==========================================\n");
+        }
+
+        log.info("0观看用户处理完成,共删除: {} 条", totalDeleted);
+    }
+
+    /**
+     * 处理单批用户(备份+删除)
+     * 删除顺序:先删除答题记录 -> 再删除红包记录 -> 再删除看课记录 -> 最后删除用户关系
+     */
+    private void processUserBatch(List<Long> userIds) {
+        if (userIds == null || userIds.isEmpty()) {
+            return;
+        }
+
+        // 开启新事务,确保这一批用户的数据要么全部成功,要么全部回滚
+        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
+        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+        TransactionStatus transactionStatus = transactionManager.getTransaction(def);
+
+        try {
+            log.info("开始处理 {} 个用户的数据备份和删除", userIds.size());
+
+            // 1. 备份用户相关数据
+            backupUserData(userIds);
+
+            // 2. 删除答题记录(通过关联看课记录表)
+            int answerDeleted = dataCleanMapper.deleteAnswerLogs(PROJECT_ID, userIds);
+            log.info("删除答题记录: {} 条", answerDeleted);
+
+            // 3. 删除红包记录(通过关联看课记录表)
+            int redPacketDeleted = dataCleanMapper.deleteRedPacketLogs(PROJECT_ID, userIds);
+            log.info("删除红包记录: {} 条", redPacketDeleted);
+
+            // 4. 删除看课记录
+            int watchDeleted = dataCleanMapper.deleteWatchLogs(PROJECT_ID, userIds);
+            log.info("删除看课记录: {} 条", watchDeleted);
+
+            // 5. 删除用户关系
+            int userDeleted = dataCleanMapper.deleteUserCompanyUser(PROJECT_ID, userIds);
+            log.info("删除用户关系: {} 条", userDeleted);
+
+            // 提交事务,这一批用户的所有操作完成
+            transactionManager.commit(transactionStatus);
+            log.info("{} 个用户处理完成", userIds.size());
+
+        } catch (Exception e) {
+            // 如果这一批有任何失败,全部回滚
+            transactionManager.rollback(transactionStatus);
+            log.error("处理用户批次失败,将回滚所有操作,用户数量: {}", userIds.size(), e);
+            throw new RuntimeException("处理用户批次失败", e);
+        }
+    }
+
+    /**
+     * 备份用户相关数据
+     */
+    private void backupUserData(List<Long> userIds) {
+        // 1. 备份用户关系表
+        int userCount = dataCleanMapper.backupUserCompanyUser(BACKUP_SUFFIX, PROJECT_ID, userIds);
+        log.debug("备份用户关系: {} 条", userCount);
+
+        // 2. 备份看课记录表
+        int watchCount = dataCleanMapper.backupCourseWatchLog(BACKUP_SUFFIX, PROJECT_ID, userIds);
+        log.debug("备份看课记录: {} 条", watchCount);
+
+        // 3. 备份答题记录表(关联看课记录)
+        int answerCount = dataCleanMapper.backupCourseAnswerLogs(BACKUP_SUFFIX, PROJECT_ID, userIds);
+        log.debug("备份答题记录: {} 条", answerCount);
+
+        // 4. 备份红包记录表(关联看课记录)
+        int redPacketCount = dataCleanMapper.backupCourseRedPacketLog(BACKUP_SUFFIX, PROJECT_ID, userIds);
+        log.debug("备份红包记录: {} 条", redPacketCount);
+
+        log.info("备份完成 - 用户关系: {}, 看课: {}, 答题: {}, 红包: {}",
+                userCount, watchCount, answerCount, redPacketCount);
+    }
+
+    /**
+     * 打印统计数据
+     */
+    public void printStatistics() {
+        log.info("========== 统计数据 ==========");
+
+        int blackCount = dataCleanMapper.countBlackUsers(PROJECT_ID, BLACK_STATUS_LIST);
+        int zeroCount = dataCleanMapper.countZeroWatchUsers(PROJECT_ID, DATE_LIMIT);
+
+        log.info("拉黑用户数量: {}", blackCount);
+        log.info("0观看用户数量: {}", zeroCount);
+        log.info("总计需要删除: {}", (blackCount + zeroCount));
+        log.info("==============================");
+    }
+}

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

@@ -14,6 +14,7 @@ import com.fs.his.dto.AppUserCompanyDTO;
 import com.fs.his.dto.FindUsersByDTO;
 import com.fs.his.param.FindUserByParam;
 import com.fs.his.param.FsUserParam;
+import com.fs.his.vo.AppSalesCourseStatisticsVO;
 import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.OptionsVO;
@@ -488,4 +489,8 @@ public interface FsUserMapper
     List<Map<String, Object>>  selectRegisterCount(@Param("companyId") Long companyId,@Param("startDate") String startDate,@Param("endDate") String endDate);
 
     UserDetailsVO selectCountWatchCourse(@Param("userId") Long userId, @Param("fsUserId") Long fsUserId, @Param("dateTag") String dateTag,@Param("userCompanyId")  Long userCompanyId);
+
+    List<AppSalesCourseStatisticsVO> selectAppSalesUserCountVO(FsCourseWatchLogStatisticsListParam param);
+
+    List<AppSalesCourseStatisticsVO> selectAppSalesNewUserCountVO(FsCourseWatchLogStatisticsListParam param);
 }

+ 11 - 0
fs-service/src/main/java/com/fs/his/vo/AppSalesCourseStatisticsVO.java

@@ -1,6 +1,7 @@
 package com.fs.his.vo;
 
 import com.fs.common.annotation.Excel;
+import lombok.Data;
 
 import java.math.BigDecimal;
 
@@ -10,6 +11,7 @@ import java.math.BigDecimal;
  * @createDate: 2026/3/23
  * @version: 1.0
  */
+@Data
 public class AppSalesCourseStatisticsVO {
         /** 销售名称 */
         @Excel(name = "销售名称")
@@ -67,9 +69,18 @@ public class AppSalesCourseStatisticsVO {
         @Excel(name = "完课率")
         private BigDecimal correctRate;
 
+        @Excel(name = "红包个数")
+        private Integer redPacketCount;
+
         /** 红包金额 */
         @Excel(name = "红包金额")
         private BigDecimal redPacketAmount;
 
+        private Long companyUserId;
+
+        private Long courseId;
+
+        private Long videoId;
+
     }
 

+ 5 - 5
fs-service/src/main/resources/mapper/course/FsCourseAnswerLogsMapper.xml

@@ -262,11 +262,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
 
     <select id="selectAppSalesAnswerStatisticsVO" resultType="com.fs.his.vo.AppSalesCourseStatisticsVO">
-        select cal.company_user_id,cal.course_id,cal.video_id
-        count(cal.log_id) AS correctCount,
+        select cal.company_user_id,cal.course_id,cal.video_id,
+        count(cal.log_id) AS answeredCount,
         SUM(CASE WHEN cal.is_right = 1 THEN 1 ELSE 0 END)  AS  correctCount
         from fs_course_answer_logs cal
-         <where>
+        where cal.watch_type = 1
             <if test="companyId != null ">
                 and cal.company_id = #{companyId}
             </if>
@@ -282,7 +282,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
                 and cal.create_time &gt;= #{startDate} and cal.create_time &lt;= #{endDate}
             </if>
-         </where>
-        group by company_user_id,course_id,vodio_id
+
+        group by cal.company_user_id,cal.course_id,cal.video_id
     </select>
 </mapper>

+ 23 - 0
fs-service/src/main/resources/mapper/course/FsCourseRedPacketLogMapper.xml

@@ -116,6 +116,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="accBalanceBefore != null">acc_balance_before,</if>
             <if test="accBalanceAfter != null">acc_balance_after,</if>
             <if test="mchId != null">mch_id,</if>
+            <if test="watchType != null">watch_type,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="courseId != null">#{courseId},</if>
@@ -138,6 +139,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="accBalanceBefore != null">#{accBalanceBefore},</if>
             <if test="accBalanceAfter != null">#{accBalanceAfter},</if>
             <if test="mchId != null">#{mchId},</if>
+            <if test="watchType != null">#{watchType},</if>
         </trim>
     </insert>
 
@@ -287,7 +289,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         and create_time &lt;= CONCAT(CURDATE(), ' 23:59:59')
     </select>
     <select id="selectAppSalesRedPacketStatisticsVO" resultType="com.fs.his.vo.AppSalesCourseStatisticsVO">
+        select  rpl.company_user_id,rpl.course_id,rpl.video_id,
+                sum(rpl.log_id) as redPacketCount,
+                sum(rpl.amount) as redPacketAmount
+        from fs_course_red_packet_log rpl
+        where rpl.watch_type = 1
+            <if test="companyId != null ">
+                and rpl.company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null ">
+                and rpl.company_user_id = #{companyUserId}
+            </if>
+            <if test="courseId != null ">
+                and rpl.course_id = #{courseId}
+            </if>
+            <if test="videoId != null ">
+                and rpl.video_id = #{videoId}
+            </if>
+            <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                and rpl.create_time &gt;= #{startDate} and rpl.create_time &lt;= #{endDate}
+            </if>
 
+        group by rpl.company_user_id,rpl.course_id,rpl.video_id
     </select>
 
 

+ 7 - 3
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1747,12 +1747,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      <!-- 记录类型 1看课中 2完课 3待看课 4看课中断   -->
     <select id="selectAppSalesCourseStatisticsVO" resultType="com.fs.his.vo.AppSalesCourseStatisticsVO">
         SELECT l.company_user_id,l.course_id,l.video_id,
+        cu.nick_name as salesName,fuc.course_name as courseName,fuv.title as videoTitle,
         SUM(CASE WHEN l.log_type = 2 THEN 1 ELSE 0 END) AS finishedCount,
         SUM(CASE WHEN l.log_type = 3 THEN 1 ELSE 0 END) AS notWatchedCount,
         SUM(CASE WHEN l.log_type = 4 THEN 1 ELSE 0 END) AS interruptCount,
-        SUM(CASE WHEN l.log_type = 1 THEN 1 ELSE 0 END) AS watchingCount,
+        SUM(CASE WHEN l.log_type = 1 THEN 1 ELSE 0 END) AS watchingCount
         from fs_course_watch_log l
-        where send_type = 1
+        left join company_user cu on l.company_user_id=cu.user_id
+        left join fs_user_course fuc on l.course_id=fuc.course_id
+        left join fs_user_course_video fuv on l.video_id=fuv.video_id
+        where send_type = 1 and watch_type =1
             <if test="companyId != null ">
                 and l.company_id = #{companyId}
             </if>
@@ -1768,7 +1772,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
                 and l.create_time &gt;= #{startDate} and l.create_time &lt;= #{endDate}
             </if>
-        group by company_user_id,course_id,vodio_id
+        group by l.company_user_id,l.course_id,l.video_id
     </select>
 
 

+ 150 - 0
fs-service/src/main/resources/mapper/delete/DataCleanMapper.xml

@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.delete.mapper.DataCleanMapper">
+
+    <!-- 查询拉黑用户ID -->
+    <select id="selectBlackUserIds" resultType="java.lang.Long">
+        SELECT user_id
+        FROM fs_user_company_user
+        WHERE project_id = #{projectId}
+        AND status IN
+        <foreach collection="statusList" item="status" open="(" close=")" separator=",">
+            #{status}
+        </foreach>
+        and create_time &lt; CURDATE()
+        ORDER BY user_id
+        LIMIT #{limit}
+    </select>
+
+    <!-- 查询0观看用户ID -->
+    <select id="selectZeroWatchUserIds" resultType="java.lang.Long">
+        SELECT DISTINCT fucu.user_id
+        FROM fs_user_company_user fucu
+                 LEFT JOIN fs_course_watch_log fcwl
+                           ON fcwl.user_id = fucu.user_id
+                               AND fcwl.project = #{projectId}
+                               AND fcwl.log_type = 2
+        WHERE fucu.project_id = #{projectId}
+          AND fucu.create_time &lt; #{dateLimit}
+          AND fcwl.log_id IS NULL
+        ORDER BY fucu.user_id
+            LIMIT #{limit}
+    </select>
+
+    <!-- 统计拉黑用户数量 -->
+    <select id="countBlackUsers" resultType="int">
+        SELECT COUNT(*)
+        FROM fs_user_company_user
+        WHERE project_id = #{projectId}
+        AND status IN
+        <foreach collection="statusList" item="status" open="(" close=")" separator=",">
+            #{status}
+        </foreach>
+        and create_time &lt; CURDATE()
+    </select>
+
+    <!-- 统计0观看用户数量 -->
+    <select id="countZeroWatchUsers" resultType="int">
+        SELECT COUNT(DISTINCT fucu.user_id)
+        FROM fs_user_company_user fucu
+                 LEFT JOIN fs_course_watch_log fcwl
+                           ON fcwl.user_id = fucu.user_id
+                               AND fcwl.project = #{projectId}
+        WHERE fucu.project_id = #{projectId}
+          AND fucu.create_time &lt; #{dateLimit}
+          AND fcwl.log_id IS NULL
+    </select>
+
+    <!-- 备份用户关系表 -->
+    <insert id="backupUserCompanyUser">
+        INSERT INTO fs_user_company_user${tableSuffix}
+        SELECT * FROM fs_user_company_user
+        WHERE project_id = #{projectId}
+        AND user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </insert>
+
+    <!-- 备份看课记录表 -->
+    <insert id="backupCourseWatchLog">
+        INSERT INTO fs_course_watch_log${tableSuffix}
+        SELECT * FROM fs_course_watch_log
+        WHERE project = #{projectId}
+        AND user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </insert>
+
+    <!-- 备份答题记录表(关联看课记录) -->
+    <insert id="backupCourseAnswerLogs">
+        INSERT INTO fs_course_answer_logs${tableSuffix}
+        SELECT fcal.*
+        FROM fs_course_answer_logs fcal
+        INNER JOIN fs_course_watch_log fcwl ON fcwl.log_id = fcal.watch_log_id
+        WHERE fcwl.project = #{projectId}
+        AND fcwl.user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </insert>
+
+    <!-- 备份红包记录表(关联看课记录) -->
+    <insert id="backupCourseRedPacketLog">
+        INSERT INTO fs_course_red_packet_log${tableSuffix}
+        SELECT fcrpl.*
+        FROM fs_course_red_packet_log fcrpl
+        INNER JOIN fs_course_watch_log fcwl ON fcwl.log_id = fcrpl.watch_log_id
+        WHERE fcwl.project = #{projectId}
+        AND fcwl.user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </insert>
+
+    <!-- 删除答题记录(关联看课记录) -->
+    <delete id="deleteAnswerLogs">
+        DELETE fcal
+        FROM fs_course_answer_logs fcal
+        INNER JOIN fs_course_watch_log fcwl ON fcwl.log_id = fcal.watch_log_id
+        WHERE fcwl.project = #{projectId}
+        AND fcwl.user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </delete>
+
+    <!-- 删除红包记录(关联看课记录) -->
+    <delete id="deleteRedPacketLogs">
+        DELETE fcrpl
+        FROM fs_course_red_packet_log fcrpl
+        INNER JOIN fs_course_watch_log fcwl ON fcwl.log_id = fcrpl.watch_log_id
+        WHERE fcwl.project = #{projectId}
+        AND fcwl.user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </delete>
+
+    <!-- 删除看课记录 -->
+    <delete id="deleteWatchLogs">
+        DELETE FROM fs_course_watch_log
+        WHERE project = #{projectId}
+        AND user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </delete>
+
+    <!-- 删除用户关系 -->
+    <delete id="deleteUserCompanyUser">
+        DELETE FROM fs_user_company_user
+        WHERE project_id = #{projectId}
+        AND user_id IN
+        <foreach collection="userIds" item="userId" open="(" close=")" separator=",">
+            #{userId}
+        </foreach>
+    </delete>
+
+</mapper>

+ 28 - 0
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -2522,5 +2522,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         GROUP BY
         fs_course_watch_log.user_id
     </select>
+    <select id="selectAppSalesNewUserCountVO" resultType="com.fs.his.vo.AppSalesCourseStatisticsVO">
+        select count(distinct u.user_id) as newAppUserCount, u.company_user_id from fs_user u
+        left join fs_user_company_user ucu on u.user_id = ucu.user_id and ucu.status=1
+        where u.is_del = 0
+            <if test="companyId != null ">
+                and ucu.company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null ">
+                and ucu.company_user_id = #{companyUserId}
+            </if>
+            <if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
+                and u.app_create_time &gt;= #{startDate} and u.app_create_time &lt;= #{endDate}
+            </if>
+        group by ucu.company_user_id
+    </select>
+
+    <select id="selectAppSalesUserCountVO" resultType="com.fs.his.vo.AppSalesCourseStatisticsVO">
+        select count(distinct u.user_id) as appUserCount, u.company_user_id from fs_user u
+        left join fs_user_company_user ucu on u.user_id = ucu.user_id and ucu.status=1
+        where u.is_del = 0 and u.source is not null
+            <if test="companyId != null ">
+                and ucu.company_id = #{companyId}
+            </if>
+            <if test="companyUserId != null ">
+                and ucu.company_user_id = #{companyUserId}
+            </if>
+        group by ucu.company_user_id
+    </select>
 
 </mapper>