|
|
@@ -19,6 +19,7 @@ import com.fs.company.cache.ICompanyCacheService;
|
|
|
import com.fs.company.cache.ICompanyUserCacheService;
|
|
|
import com.fs.company.domain.Company;
|
|
|
import com.fs.company.domain.CompanyUser;
|
|
|
+import com.fs.company.mapper.CompanyMapper;
|
|
|
import com.fs.course.config.CourseConfig;
|
|
|
import com.fs.course.constant.CourseConstant;
|
|
|
import com.fs.course.domain.*;
|
|
|
@@ -31,13 +32,13 @@ import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
|
|
|
import com.fs.course.vo.*;
|
|
|
import com.fs.his.config.FsSysConfig;
|
|
|
import com.fs.his.domain.FsUser;
|
|
|
+import com.fs.his.dto.AppUserCompanyDTO;
|
|
|
import com.fs.his.dto.UserConditionDTO;
|
|
|
+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.FsCourseReportVO;
|
|
|
-import com.fs.his.vo.FsUserReportVO;
|
|
|
-import com.fs.his.vo.WatchLogReportVO;
|
|
|
+import com.fs.his.vo.*;
|
|
|
import com.fs.qw.Bean.MsgBean;
|
|
|
import com.fs.qw.cache.IQwExternalContactCacheService;
|
|
|
import com.fs.qw.cache.IQwUserCacheService;
|
|
|
@@ -155,6 +156,15 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
@Autowired
|
|
|
private SysDictDataMapper dictDataMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private FsUserMapper userMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private CompanyMapper companyMapper;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private FsUserCourseStudyLogMapper fsUserCourseStudyLogMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 查询短链课程看课记录
|
|
|
*
|
|
|
@@ -1346,6 +1356,117 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
redPacketStatistics, param);
|
|
|
}
|
|
|
|
|
|
+ @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. 提取所有唯一的 userId
|
|
|
+ Set<Long> allUserIds = appUserList != null ?
|
|
|
+ appUserList.stream().map(AppUserCompanyDTO::getUserId).collect(Collectors.toSet()) :
|
|
|
+ new HashSet<>();
|
|
|
+
|
|
|
+ // 4. 查询有看课记录的活跃用户 ID(只查一次,避免 N+1 问题)
|
|
|
+ List<Long> activeUserIds = allUserIds.isEmpty() ?
|
|
|
+ new ArrayList<>() :
|
|
|
+ fsCourseWatchLogMapper.selectActiveUserIds(new ArrayList<>(allUserIds));
|
|
|
+ Set<Long> activeUserSet = new HashSet<>(activeUserIds);
|
|
|
+
|
|
|
+ // 5. 按公司分组统计 APP 会员数据
|
|
|
+ Map<Long, int[]> companyStatsMap = new HashMap<>();
|
|
|
+
|
|
|
+ if (appUserList != null) {
|
|
|
+ for (AppUserCompanyDTO dto : appUserList) {
|
|
|
+ Long cid = dto.getCompanyId();
|
|
|
+
|
|
|
+ if (!companyStatsMap.containsKey(cid)) {
|
|
|
+ companyStatsMap.put(cid, new int[]{0, 0}); // [总数,活跃数]
|
|
|
+ }
|
|
|
+
|
|
|
+ int[] stats = companyStatsMap.get(cid);
|
|
|
+
|
|
|
+ // 统计 APP 会员总数(每个 user_id + company_user_id 算一个)
|
|
|
+ stats[0]++;
|
|
|
+
|
|
|
+ // 如果是活跃用户,活跃数 +1
|
|
|
+ if (activeUserSet.contains(dto.getUserId())) {
|
|
|
+ stats[1]++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 组装结果:遍历所有公司,填充 APP 会员统计数据
|
|
|
+ for (AppCourseReportVO vo : allCompanies) {
|
|
|
+ int[] stats = companyStatsMap.get(vo.getCompanyId());
|
|
|
+ if (stats != null) {
|
|
|
+ vo.setAppUserCount(stats[0]);
|
|
|
+ vo.setActiveAppUserCount(stats[1]);
|
|
|
+ } else {
|
|
|
+ vo.setAppUserCount(0);
|
|
|
+ vo.setActiveAppUserCount(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //获取公域课 统计信息
|
|
|
+ List<Long> companyIds = allCompanies.stream()
|
|
|
+ .map(company -> (Long) company.getCompanyId())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (CollectionUtils.isNotEmpty(companyIds)) {
|
|
|
+ param.setCompanyIds(companyIds);
|
|
|
+ List<AppCourseReportVO> appCourseReportVOList = fsCourseWatchLogMapper.selectAppWatchStatistics(param);
|
|
|
+ Map<String, AppCourseReportVO> answerStatsMap=new HashMap<>();
|
|
|
+ Map<String, AppCourseReportVO> redPacketStatsMap=new HashMap<>();
|
|
|
+ //统计答题数据
|
|
|
+
|
|
|
+ List<AppCourseReportVO> answerList = fsCourseWatchLogMapper.selectAppAnswerStatistics(param);
|
|
|
+ //根据公司id 分组
|
|
|
+ answerStatsMap= answerList.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ stats -> String.valueOf(stats.getCompanyId()),
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ //统计红包数据
|
|
|
+ List<AppCourseReportVO> redpackList = fsCourseWatchLogMapper.selectAppRedPacketStatistics(param);
|
|
|
+ redPacketStatsMap= redpackList.stream()
|
|
|
+ .collect(Collectors.toMap(
|
|
|
+ stats -> String.valueOf(stats.getCompanyId()),
|
|
|
+ Function.identity(),
|
|
|
+ (existing, replacement) -> existing // 当出现重复键时,保留第一个值
|
|
|
+ ));
|
|
|
+ for (AppCourseReportVO vo : allCompanies) {
|
|
|
+ Long companyId = vo.getCompanyId();
|
|
|
+ AppCourseReportVO watchStats = appCourseReportVOList.stream()
|
|
|
+ .filter(w -> w.getCompanyId().equals(companyId))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ if (watchStats != null) {
|
|
|
+ vo.setPendingCount(watchStats.getPendingCount());
|
|
|
+ vo.setWatchingCount(watchStats.getWatchingCount());
|
|
|
+ vo.setFinishedCount(watchStats.getFinishedCount());
|
|
|
+ vo.setWatchRate(calculateWatchingRate(watchStats.getWatchingCount(),watchStats.getPendingCount()));
|
|
|
+ }
|
|
|
+ AppCourseReportVO anserStats = answerStatsMap.getOrDefault(companyId.toString(), new AppCourseReportVO());
|
|
|
+ vo.setAnswerUserCount(anserStats.getAnswerUserCount());
|
|
|
+ AppCourseReportVO redPacketStats = redPacketStatsMap.getOrDefault(companyId.toString(), new AppCourseReportVO());
|
|
|
+ vo.setPacketUserCount(redPacketStats.getPacketUserCount());
|
|
|
+ vo.setPacketAmount(redPacketStats.getPacketAmount());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return allCompanies;
|
|
|
+ }
|
|
|
+
|
|
|
private List<FsCourseReportVO> assembleStatisticsResult(List<FsCourseReportVO> companyList, Map<String, FsCourseReportVO> watchStatistics, Map<String, FsCourseReportVO> answerStatistics, Map<String, FsCourseReportVO> redPacketStatistics, FsCourseWatchLogStatisticsListParam param) {
|
|
|
for (FsCourseReportVO company : companyList) {
|
|
|
FsCourseReportVO watchStats;
|
|
|
@@ -1399,6 +1520,36 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
.setScale(2, RoundingMode.HALF_UP);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * app 看课率(私域看课中人次/私域课待看课人次)
|
|
|
+ */
|
|
|
+
|
|
|
+
|
|
|
+// ... existing code ...
|
|
|
+ /**
|
|
|
+ * app 看课率(私域看课中人次/私域课待看课人次)
|
|
|
+ * @param watchingCount 私域看课中人次
|
|
|
+ * @param pendingCount 私域课待看课人次
|
|
|
+ * @return 看课率(百分比,保留 2 位小数)
|
|
|
+ */
|
|
|
+ private BigDecimal calculateWatchingRate(Integer watchingCount, Integer pendingCount) {
|
|
|
+ // 防止除以 0
|
|
|
+ if (pendingCount == null || pendingCount == 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 防止空指针
|
|
|
+ if (watchingCount == null || watchingCount == 0) {
|
|
|
+ return BigDecimal.ZERO;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 看课率 = 看课中人次 / 待看课人次 * 100%
|
|
|
+ return BigDecimal.valueOf(watchingCount)
|
|
|
+ .divide(BigDecimal.valueOf(pendingCount), 4, RoundingMode.HALF_UP)
|
|
|
+ .multiply(BigDecimal.valueOf(100))
|
|
|
+ .setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+// ... existing code ...
|
|
|
/**
|
|
|
* 计算完成率
|
|
|
*/
|
|
|
@@ -1568,6 +1719,119 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
|
|
|
return assembleStatisticsData(baseData, param);
|
|
|
}
|
|
|
|
|
|
+ @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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将秒数转换为时分秒格式
|
|
|
+ */
|
|
|
+ private String formatDuration(Long totalSeconds) {
|
|
|
+ if (totalSeconds == null || totalSeconds <= 0) {
|
|
|
+ return "0秒";
|
|
|
+ }
|
|
|
+ long hours = totalSeconds / 3600;
|
|
|
+ long minutes = (totalSeconds % 3600) / 60;
|
|
|
+ long seconds = totalSeconds % 60;
|
|
|
+ if (hours > 0) {
|
|
|
+ return String.format("%d小时%d分%d秒", hours, minutes, seconds);
|
|
|
+ } else if (minutes > 0) {
|
|
|
+ return String.format("%d分%d秒", minutes, seconds);
|
|
|
+ } else {
|
|
|
+ return String.format("%d秒", seconds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
/**
|
|
|
* 根据维度获取基础数据
|