|
|
@@ -29,10 +29,17 @@ import com.fs.course.service.IFsUserCoursePeriodDaysService;
|
|
|
import com.fs.course.service.IFsUserCoursePeriodService;
|
|
|
import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
|
|
|
import com.fs.course.vo.*;
|
|
|
+import com.fs.his.config.AppConfig;
|
|
|
import com.fs.his.config.FsSysConfig;
|
|
|
import com.fs.his.domain.FsUser;
|
|
|
+import com.fs.his.dto.AppUserCompanyDTO;
|
|
|
+import com.fs.his.mapper.FsUserMapper;
|
|
|
import com.fs.his.service.IFsUserService;
|
|
|
import com.fs.his.utils.ConfigUtil;
|
|
|
+import com.fs.his.utils.PhoneUtil;
|
|
|
+import com.fs.his.vo.AppCourseReportVO;
|
|
|
+import com.fs.his.vo.AppWatchLogReportVO;
|
|
|
+import com.fs.his.vo.WatchLogReportVO;
|
|
|
import com.fs.qw.Bean.MsgBean;
|
|
|
import com.fs.qw.cache.IQwExternalContactCacheService;
|
|
|
import com.fs.qw.cache.IQwUserCacheService;
|
|
|
@@ -51,9 +58,11 @@ import com.fs.sop.domain.QwSopLogs;
|
|
|
import com.fs.sop.mapper.SopUserLogsMapper;
|
|
|
import com.fs.store.service.cache.IFsUserCacheService;
|
|
|
import com.fs.store.service.cache.IFsUserCourseCacheService;
|
|
|
+import com.fs.system.domain.SysConfig;
|
|
|
import com.fs.system.service.ISysConfigService;
|
|
|
import com.fs.tag.service.FsTagUpdateService;
|
|
|
import com.github.pagehelper.PageHelper;
|
|
|
+import com.google.gson.Gson;
|
|
|
import com.hc.openapi.tool.util.StringUtils;
|
|
|
import org.apache.commons.collections4.CollectionUtils;
|
|
|
import org.slf4j.Logger;
|
|
|
@@ -155,6 +164,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
@Autowired
|
|
|
private FinishCourseStatisticsSyncMapper finishCourseStatisticsSyncMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FsUserMapper userMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 查询短链课程看课记录
|
|
|
*
|
|
|
@@ -1692,6 +1704,315 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
return fsCourseReportVOS;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * @Description: app 看课统计
|
|
|
+ * @Param:
|
|
|
+ * @Return:
|
|
|
+ * @Author xgb
|
|
|
+ * @Date 2026/3/16 16:39
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<AppCourseReportVO> selectAppCourseReportVO(FsCourseWatchLogStatisticsListParam param) {
|
|
|
+ // 时间转字符串
|
|
|
+ if (param.getSTime() != null && param.getETime() != null) {
|
|
|
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ param.setStartDate(simpleDateFormat.format(param.getSTime()));
|
|
|
+ param.setEndDate(simpleDateFormat.format(param.getETime()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 查询所有公司列表
|
|
|
+ List<AppCourseReportVO> allCompanies = companyMapper.selectAllCompanies(param.getCompanyId());
|
|
|
+ if (CollectionUtils.isEmpty(allCompanies)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询指定公司和时间范围内的 APP 会员
|
|
|
+// List<AppUserCompanyDTO> appUserList = userMapper.selectAppUserListForActiveCount(param);
|
|
|
+
|
|
|
+ // 3. 如果有APP会员数据,则统计活跃用户数
|
|
|
+// Map<Long, int[]> companyStatsMap = null;
|
|
|
+// if (!CollectionUtils.isEmpty(appUserList)) {
|
|
|
+// // 提取所有唯一的 userId 并查询活跃用户
|
|
|
+// Set<Long> allUserIds = appUserList.stream()
|
|
|
+// .map(AppUserCompanyDTO::getUserId)
|
|
|
+// .collect(Collectors.toSet());
|
|
|
+//
|
|
|
+// Set<Long> activeUserSet = new HashSet<>(
|
|
|
+// fsCourseWatchLogMapper.selectActiveUserIds(new ArrayList<>(allUserIds))
|
|
|
+// );
|
|
|
+//
|
|
|
+// // 按公司分组统计 APP 会员数据 [总数, 活跃数]
|
|
|
+// companyStatsMap = new HashMap<>();
|
|
|
+// for (AppUserCompanyDTO dto : appUserList) {
|
|
|
+// Long cid = dto.getCompanyId();
|
|
|
+// int[] stats = companyStatsMap.computeIfAbsent(cid, k -> new int[]{0, 0});
|
|
|
+// stats[0]++;
|
|
|
+// if (activeUserSet.contains(dto.getUserId())) {
|
|
|
+// stats[1]++;
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }
|
|
|
+
|
|
|
+ // 查询用户注册数
|
|
|
+ List<Map<String, Object>> registerCountMap = userMapper.selectRegisterCount(param.getCompanyId(), param.getStartDate(), param.getEndDate());
|
|
|
+ Map<Long, Integer> countMap = registerCountMap.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ map -> map.get("companyId")==null?0L:Long.parseLong(map.get("companyId").toString()),
|
|
|
+ map -> Integer.parseInt(map.get("registerCount").toString())
|
|
|
+ ));
|
|
|
+
|
|
|
+ // 4. 批量查询看课、答题、红包统计数据
|
|
|
+ List<Long> companyIds = allCompanies.stream()
|
|
|
+ .map(AppCourseReportVO::getCompanyId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ param.setCompanyIds(companyIds);
|
|
|
+
|
|
|
+ // 并行查询三个统计数据源
|
|
|
+ List<AppCourseReportVO> watchStatsList = fsCourseWatchLogMapper.selectAppWatchStatistics(param);
|
|
|
+ List<AppCourseReportVO> answerList = fsCourseWatchLogMapper.selectAppAnswerStatistics(param);
|
|
|
+ SysConfig sysConfig = configService.selectConfigByConfigKey("app.config");
|
|
|
+ AppConfig config = new Gson().fromJson(sysConfig.getConfigValue(), AppConfig.class);
|
|
|
+ param.setAppId(config.getAppId());
|
|
|
+ List<AppCourseReportVO> redpackList = fsCourseWatchLogMapper.selectAppRedPacketStatistics(param);
|
|
|
+
|
|
|
+ // 5. 转换为 Map 便于查找
|
|
|
+ Map<Long, AppCourseReportVO> watchStatsMap = watchStatsList.stream()
|
|
|
+ .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity()));
|
|
|
+ Map<Long, AppCourseReportVO> answerStatsMap = answerList.stream()
|
|
|
+ .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity(), (e, r) -> e));
|
|
|
+ Map<Long, AppCourseReportVO> redPacketStatsMap = redpackList.stream()
|
|
|
+ .collect(Collectors.toMap(AppCourseReportVO::getCompanyId, Function.identity(), (e, r) -> e));
|
|
|
+
|
|
|
+ // 6. 一次性组装所有数据,减少遍历次数
|
|
|
+ for (AppCourseReportVO vo : allCompanies) {
|
|
|
+ Long companyId = vo.getCompanyId();
|
|
|
+
|
|
|
+ // APP会员统计
|
|
|
+// if (companyStatsMap != null) {
|
|
|
+// int[] stats = companyStatsMap.getOrDefault(companyId, new int[]{0, 0});
|
|
|
+// vo.setAppUserCount(stats[0]);
|
|
|
+// vo.setActiveAppUserCount(stats[1]);
|
|
|
+// } else {
|
|
|
+// vo.setAppUserCount(0);
|
|
|
+// vo.setActiveAppUserCount(0);
|
|
|
+// }
|
|
|
+ vo.setAppUserCount(countMap.getOrDefault(companyId, 0));
|
|
|
+
|
|
|
+ // 看课统计 - 如果查不到数据就用0填充
|
|
|
+ AppCourseReportVO watchStats = watchStatsMap.get(companyId);
|
|
|
+ if (watchStats != null) {
|
|
|
+ vo.setPendingCount(watchStats.getPendingCount() != null ? watchStats.getPendingCount() : 0);
|
|
|
+ vo.setWatchingCount(watchStats.getWatchingCount() != null ? watchStats.getWatchingCount() : 0);
|
|
|
+ vo.setFinishedCount(watchStats.getFinishedCount() != null ? watchStats.getFinishedCount() : 0);
|
|
|
+ vo.setAccessCount(watchStats.getAccessCount() != null ? watchStats.getAccessCount() : 0);
|
|
|
+ vo.setStopCount(watchStats.getStopCount() != null ? watchStats.getStopCount() : 0);
|
|
|
+ vo.setWatchRate(calculateWatchingRate(
|
|
|
+ vo.getWatchingCount(),
|
|
|
+ vo.getFinishedCount(),
|
|
|
+ vo.getAccessCount()));
|
|
|
+ } else {
|
|
|
+ // 查不到看课数据时设置默认值0
|
|
|
+ vo.setPendingCount(0);
|
|
|
+ vo.setWatchingCount(0);
|
|
|
+ vo.setFinishedCount(0);
|
|
|
+ vo.setAccessCount(0);
|
|
|
+ vo.setStopCount(0);
|
|
|
+ vo.setWatchRate(BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 答题统计 - 如果查不到数据就用0填充
|
|
|
+ AppCourseReportVO answerStats = answerStatsMap.getOrDefault(companyId, new AppCourseReportVO());
|
|
|
+ vo.setAnswerUserCount(answerStats.getAnswerUserCount() != null ? answerStats.getAnswerUserCount() : 0);
|
|
|
+
|
|
|
+ // 红包统计 - 如果查不到数据就用0填充
|
|
|
+ AppCourseReportVO redPacketStats = redPacketStatsMap.getOrDefault(companyId, new AppCourseReportVO());
|
|
|
+ vo.setPacketUserCount(redPacketStats.getPacketUserCount() != null ? redPacketStats.getPacketUserCount() : 0);
|
|
|
+ vo.setPacketAmount(redPacketStats.getPacketAmount() != null ? redPacketStats.getPacketAmount() : BigDecimal.ZERO);
|
|
|
+ }
|
|
|
+
|
|
|
+ return allCompanies;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<AppWatchLogReportVO> selectUserAppWatchLogReportVO(FsCourseWatchLogStatisticsListParam param) {
|
|
|
+ if (StringUtils.isNotEmpty(param.getUserPhone())) {
|
|
|
+ //加密手机号
|
|
|
+ param.setUserPhone(PhoneUtil.encryptPhone(param.getUserPhone()));
|
|
|
+ }
|
|
|
+ // 时间转字符串
|
|
|
+ if (param.getSTime() != null && param.getETime() != null) {
|
|
|
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ param.setStartDate(simpleDateFormat.format(param.getSTime()));
|
|
|
+ param.setEndDate(simpleDateFormat.format(param.getETime()));
|
|
|
+ }
|
|
|
+ // 获取基础数据
|
|
|
+ List<AppWatchLogReportVO> baseData = fsCourseWatchLogMapper.selectAppUserBaseData(param);
|
|
|
+ if (CollectionUtils.isEmpty(baseData)) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ // 获取统计数据和组装结果
|
|
|
+ return assembleAppStatisticsData(baseData, param);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组装APP统计数据
|
|
|
+ */
|
|
|
+ private List<AppWatchLogReportVO> assembleAppStatisticsData(List<AppWatchLogReportVO> baseData, FsCourseWatchLogStatisticsListParam param) {
|
|
|
+ // 准备查询条件
|
|
|
+ List<Long> periods = baseData.stream().map(AppWatchLogReportVO::getPeriodId).collect(Collectors.toList());
|
|
|
+ List<Long> logIds = baseData.stream().map(AppWatchLogReportVO::getLogId).collect(Collectors.toList());
|
|
|
+ List<Long> userIds = baseData.stream().map(AppWatchLogReportVO::getUserId).collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 批量查询统计数据
|
|
|
+ // 营期数据
|
|
|
+ Map<Long, WatchLogReportVO> perMap = convertCampPeriodToMap(fsCourseWatchLogMapper.selectCampPeriodByPeriod(periods));
|
|
|
+
|
|
|
+ // 红包数据
|
|
|
+ Map<Long, WatchLogReportVO> redPacketMap = convertRedPacketToMap(
|
|
|
+ fsCourseWatchLogMapper.selectRedPacketStats(logIds)
|
|
|
+ );
|
|
|
+
|
|
|
+// // 订单数据
|
|
|
+// Map<Long, WatchLogReportVO> orderMap = convertOrderToMap(
|
|
|
+// fsCourseWatchLogMapper.selectOrderStats(userIds, param)
|
|
|
+// );
|
|
|
+
|
|
|
+ // 答题数据
|
|
|
+ Map<Long, WatchLogReportVO> answerMap = convertAnswerToMap(
|
|
|
+ fsCourseWatchLogMapper.selectAnswerStats(logIds)
|
|
|
+ );
|
|
|
+
|
|
|
+ // 学习时长数据(来自fs_user_course_study_log表)- 使用字符串时间
|
|
|
+// Map<String, AppWatchLogReportVO> studyDurationMap = fsUserCourseStudyLogMapper.selectStudyDurationByUserIds(userIds, param.getStartDate(), param.getEndDate())
|
|
|
+// .stream()
|
|
|
+// .collect(Collectors.toMap(
|
|
|
+// item -> item.getUserId() + "_" + item.getVideoId(),
|
|
|
+// Function.identity()
|
|
|
+// ));
|
|
|
+
|
|
|
+ // 组装数据
|
|
|
+ for (AppWatchLogReportVO item : baseData) {
|
|
|
+ // 营期数据
|
|
|
+ WatchLogReportVO watchStats = perMap.getOrDefault(item.getPeriodId(), null);
|
|
|
+ if (watchStats != null) {
|
|
|
+ item.setPeriodName(watchStats.getPeriodName());
|
|
|
+ item.setTrainingCampName(watchStats.getTrainingCampName());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 红包数据
|
|
|
+ WatchLogReportVO redPacketStats = redPacketMap.getOrDefault(item.getLogId(), null);
|
|
|
+ if (redPacketStats != null) {
|
|
|
+ item.setRedPacketAmount(redPacketStats.getRedPacketAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+// // 订单数据
|
|
|
+// WatchLogReportVO order = orderMap.getOrDefault(item.getUserId(), null);
|
|
|
+// if (order != null) {
|
|
|
+// item.setHistoryOrderCount(order.getHistoryOrderCount());
|
|
|
+// }
|
|
|
+
|
|
|
+ // 答题数据
|
|
|
+ WatchLogReportVO answer = answerMap.getOrDefault(item.getLogId(), null);
|
|
|
+ if (answer != null) {
|
|
|
+ item.setAnswerStatus(answer.getAnswerStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 学习时长数据
|
|
|
+// AppWatchLogReportVO studyDuration = studyDurationMap.get(item.getUserId() + "_" + item.getVideoId());
|
|
|
+// if (studyDuration != null && studyDuration.getPublicCourseDuration() != null) {
|
|
|
+// // 将秒转换为时分秒格式
|
|
|
+// item.setPublicCourseDuration(formatDuration(Long.valueOf(studyDuration.getPublicCourseDuration())));
|
|
|
+// }
|
|
|
+ }
|
|
|
+ return baseData;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 红包数据转Map
|
|
|
+ */
|
|
|
+ public Map<Long, WatchLogReportVO> convertRedPacketToMap(List<WatchLogReportVO> list) {
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ return list.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ WatchLogReportVO::getLogId,
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 营期数据转Map
|
|
|
+ */
|
|
|
+ public Map<Long, WatchLogReportVO> convertCampPeriodToMap(List<WatchLogReportVO> list) {
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ return list.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ WatchLogReportVO::getPeriodId,
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 订单数据转Map
|
|
|
+ */
|
|
|
+ public Map<Long, WatchLogReportVO> convertOrderToMap(List<WatchLogReportVO> list) {
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ return list.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ WatchLogReportVO::getUserId,
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 答题数据转Map
|
|
|
+ */
|
|
|
+ public Map<Long, WatchLogReportVO> convertAnswerToMap(List<WatchLogReportVO> list) {
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ return new HashMap<>();
|
|
|
+ }
|
|
|
+ return list.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ WatchLogReportVO::getLogId,
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * app 看课率((看课中人次+完课人次)/ 私域课总人次)
|
|
|
+ * @param watchingCount 私域看课中人次
|
|
|
+ * @param finishedCount 私域完课人次
|
|
|
+ * @param totalCount 私域课总人次
|
|
|
+ * @return 看课率(百分比,保留 2 位小数)
|
|
|
+ */
|
|
|
+ private BigDecimal calculateWatchingRate(Integer watchingCount, Integer finishedCount, Integer totalCount) {
|
|
|
+ // 防止除以 0
|
|
|
+ if (totalCount == null || totalCount == 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 防止空指针
|
|
|
+ int watching = watchingCount != null ? watchingCount : 0;
|
|
|
+ int finished = finishedCount != null ? finishedCount : 0;
|
|
|
+
|
|
|
+ // 看课率 = (看课中人次 + 完课人次) / 总人次 * 100%
|
|
|
+ return BigDecimal.valueOf(watching + finished)
|
|
|
+ .divide(BigDecimal.valueOf(totalCount), 4, RoundingMode.HALF_UP)
|
|
|
+ .multiply(BigDecimal.valueOf(100))
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
* 批量设置课程和视频名称
|
|
|
*
|