yuhongqi 5 дней назад
Родитель
Сommit
3f8fa85d5c

+ 1 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -590,6 +590,7 @@ public class FsStoreHealthOrderScrmController extends BaseController {
         //通过商品ID获取关键字
         String firstKeyword = storeOrderDeliveryNoteExportVOList.stream()
                 .map(FsStoreOrderDeliveryNoteExportVO::getKeyword)
+                .filter(StringUtils::isNotEmpty)
                 .findFirst()
                 .orElse("无订单");
         String fileName="077AC"+firstKeyword+new SimpleDateFormat("yyyyMMdd").format(new Date());

+ 38 - 4
fs-admin/src/main/java/com/fs/live/controller/LiveDataController.java

@@ -5,14 +5,15 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.constant.HttpStatus;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.poi.ExcelUtil;
-import com.fs.company.domain.CompanyUser;
-import com.fs.framework.web.service.TokenService;
 import com.fs.live.domain.LiveData;
+import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
+import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveUserFirstVo;
 import com.fs.live.vo.LiveUserDetailExportVO;
 import com.github.pagehelper.PageHelper;
@@ -21,6 +22,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -31,8 +33,6 @@ public class LiveDataController extends BaseController {
 
     @Autowired
     private ILiveDataService liveDataService;
-    @Autowired
-    private TokenService tokenService;
 
     /**
      * 直播数据页面卡片数据
@@ -190,4 +190,38 @@ public class LiveDataController extends BaseController {
         return util.exportExcel(list, "直播间用户详情数据");
     }
 
+    /**
+     * 查询分公司直播数据统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:list')")
+    @PostMapping("/listLiveDataCompany")
+    public TableDataInfo listLiveDataCompany(@RequestBody LiveDataCompanyParam param) {
+        List<LiveDataCompanyVO> list = liveDataService.listLiveDataCompany(param);
+        int total = list.size();
+        int pageNum = param.getPageNum() == null || param.getPageNum() < 1 ? 1 : param.getPageNum();
+        int pageSize = param.getPageSize() == null || param.getPageSize() < 1 ? total : param.getPageSize();
+        int fromIndex = Math.min((pageNum - 1) * pageSize, total);
+        int toIndex = Math.min(fromIndex + pageSize, total);
+        List<LiveDataCompanyVO> pageList = fromIndex >= toIndex ? Collections.emptyList() : list.subList(fromIndex, toIndex);
+
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(pageList);
+        rspData.setTotal(total);
+        return rspData;
+    }
+
+    /**
+     * 导出分公司直播数据统计
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:export')")
+    @Log(title = "分公司直播数据统计", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportLiveDataCompany")
+    public AjaxResult exportLiveDataCompany(@RequestBody LiveDataCompanyParam param) {
+        List<LiveDataCompanyVO> list = liveDataService.listLiveDataCompany(param);
+        ExcelUtil<LiveDataCompanyVO> util = new ExcelUtil<>(LiveDataCompanyVO.class);
+        return util.exportExcel(list, "分公司直播数据统计");
+    }
+
 }

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java

@@ -1529,6 +1529,9 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         String orderType = "store";
         // 开票冲红
         fsStoreOrderBillLogService.billBackByOrderId(fsStoreOrder.getId());
+        if (fsStoreOrder.getOrderType() == 2) {
+            orderType = "live";
+        }
 
 
         if (fsStoreOrder.getPackageOrderId() != null) {

+ 64 - 2
fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java

@@ -4,7 +4,8 @@ package com.fs.live.mapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.LiveData;
-import com.fs.live.vo.LiveDashBoardDataVo;
+import com.fs.live.param.LiveDataCompanyParam;
+import com.fs.live.vo.LiveDataCompanyVO;
 import com.fs.live.vo.LiveDataDetailVo;
 import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
@@ -15,7 +16,6 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.springframework.stereotype.Repository;
 
-import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -176,4 +176,66 @@ public interface LiveDataMapper {
      */
     @DataSource(DataSourceType.SLAVE)
     List<LiveUserDetailVo> selectLiveUserDetailListBySql(@Param("liveId") Long liveId,@Param("companyId") Long companyId,@Param("companyUserId") Long companyUserId);
+
+    /**
+     * 查询满足条件的直播间ID列表
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<Long> selectLiveIdsByCompanyParam(LiveDataCompanyParam param);
+
+    /**
+     * 分公司总到课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectAttendanceCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                           @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司总完课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectCompleteCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                         @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司直播课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectLiveAttendCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                           @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司直播完课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectLiveCompleteCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                             @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司回放课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectReplayAttendCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                             @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司回放完课人数统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectReplayCompleteCountByCompany(@Param("liveIds") List<Long> liveIds,
+                                                               @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司订单、GMV统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectCompanyOrderAndGmv(@Param("liveIds") List<Long> liveIds,
+                                                     @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 分公司员工数量统计
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveDataCompanyVO> selectCompanyEmployeeCountByLiveIds(@Param("liveIds") List<Long> liveIds,
+                                                                @Param("companyIds") List<Long> companyIds);
 }

+ 10 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -11,6 +11,7 @@ import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 
 import java.util.List;
+import java.util.Date;
 
 /**
  * 直播Mapper接口
@@ -171,6 +172,15 @@ public interface LiveMapper
             " </script>"})
     List<Live> listLiveData(@Param("param") LiveDataParam param);
 
+    /**
+     * 根据查询条件获取直播间ID集合
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<Long> selectLiveIdsByCompanyParam(@Param("companyName") String companyName,
+                                           @Param("startDate") Date startDate,
+                                           @Param("endDate") Date endDate,
+                                           @Param("companyIds") List<Long> companyIds);
+
     @Select({"<script>" +
             "select count(1) from ( " +
             "select * from live where 1=1 " +

+ 29 - 0
fs-service/src/main/java/com/fs/live/param/LiveDataCompanyParam.java

@@ -0,0 +1,29 @@
+package com.fs.live.param;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 分公司直播数据统计查询参数
+ */
+@Data
+public class LiveDataCompanyParam {
+
+    /** 分公司名称(模糊搜索) */
+    private String companyName;
+    private List<Long> companyIds;
+
+    /** 开始日期(年月日) */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date startDate;
+
+    /** 结束日期(年月日) */
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date endDate;
+
+    private Integer pageNum = 1;
+    private Integer pageSize = 10;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/service/ILiveDataService.java

@@ -3,6 +3,7 @@ package com.fs.live.service;
 
 import com.fs.common.core.domain.R;
 import com.fs.live.domain.LiveData;
+import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.vo.*;
 
@@ -169,4 +170,11 @@ public interface ILiveDataService {
     List<LiveUserDetailExportVO> exportLiveUserDetail(Long liveId, Long companyId, Long companyUserId);
 
     List<LiveDataListVo> exportLiveData(LiveDataParam param);
+
+    /**
+     * 查询分公司直播数据统计列表
+     * @param param 查询参数
+     * @return 分公司统计数据
+     */
+    List<LiveDataCompanyVO> listLiveDataCompany(LiveDataCompanyParam param);
 }

+ 268 - 12
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -3,12 +3,14 @@ package com.fs.live.service.impl;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.hisStore.domain.FsUserScrm;
 import com.fs.hisStore.mapper.FsUserScrmMapper;
 import com.fs.live.domain.*;
 import com.fs.live.mapper.*;
+import com.fs.live.param.LiveDataCompanyParam;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.service.ILiveDataService;
 import com.fs.live.service.ILiveUserFavoriteService;
@@ -21,14 +23,12 @@ import com.fs.company.mapper.CompanyMapper;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.course.domain.FsUserCompanyUser;
 import com.fs.course.mapper.FsUserCompanyUserMapper;
-import com.fs.his.domain.FsUser;
-import com.fs.his.mapper.FsUserMapper;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import java.util.stream.Collectors;
+import java.util.function.BiConsumer;
 
 import com.github.pagehelper.PageInfo;
-import io.swagger.models.auth.In;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -39,9 +39,11 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.DayOfWeek;
 import java.time.LocalDate;
+import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 
 import static com.fs.common.constant.LiveKeysConstant.*;
 
@@ -195,10 +197,7 @@ public class LiveDataServiceImpl implements ILiveDataService {
     @Override
     public List<LiveDataListVo> exportLiveData(LiveDataParam param){
         List<Live> lives = liveMapper.listLiveData(param);
-        int total = liveMapper.listLiveDataCount(param);
-
         if (lives == null || lives.isEmpty()) {
-            LiveDataStatisticsVo statistics = new LiveDataStatisticsVo();
             return Collections.emptyList();
         }
 
@@ -210,11 +209,6 @@ public class LiveDataServiceImpl implements ILiveDataService {
         // 查询统计数据(根据live_watch_user表查询用户的在线时长,计算平均时长
         // 根据live_video的文件时长,判断用户的完课情况
         // 根据live_order查询直播间的销量额和订单数)
-        LiveDataStatisticsVo statistics = baseMapper.selectLiveDataStatistics(liveIds);
-        if (statistics == null) {
-            statistics = new LiveDataStatisticsVo();
-        }
-
         // 查询列表数据(每个直播间的详细统计数据)
         List<LiveDataListVo> liveDataList = baseMapper.selectLiveDataListByLiveIds(liveIds);
         if (liveDataList == null) {
@@ -222,6 +216,268 @@ public class LiveDataServiceImpl implements ILiveDataService {
         }
         return liveDataList;
     }
+
+    /** 时间范围最大天数(一个月) */
+    private static final int MAX_DAYS = 31;
+    /** 分段查询步长(天) */
+    private static final int SEGMENT_STEP_DAYS = 7;
+
+    @Override
+    public List<LiveDataCompanyVO> listLiveDataCompany(LiveDataCompanyParam param) {
+        Date startDate = param.getStartDate();
+        Date endDate = param.getEndDate();
+        if (startDate == null || endDate == null) {
+            return Collections.emptyList();
+        }
+
+        LocalDate start = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        LocalDate end = endDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        long daysBetween = ChronoUnit.DAYS.between(start, end) + 1;
+
+        // 超过31天直接返回报错,时间范围最大一个月
+        if (daysBetween > MAX_DAYS) {
+            throw new ServiceException("查询时间范围不能超过31天,当前为" + daysBetween + "天");
+        }
+
+        // 超过7天使用多线程分段查询
+        if (daysBetween > SEGMENT_STEP_DAYS) {
+            return listLiveDataCompanyBySegment(param, start, end, daysBetween);
+        }
+
+        return queryLiveDataCompanyByDateRange(param);
+    }
+
+    /**
+     * 多线程分段查询:步长7天,等待各段数据返回后合并
+     */
+    private List<LiveDataCompanyVO> listLiveDataCompanyBySegment(LiveDataCompanyParam param,
+                                                                  LocalDate start, LocalDate end, long totalDays) {
+        List<LocalDate[]> segments = new ArrayList<>();
+        LocalDate segmentStart = start;
+        while (segmentStart.isBefore(end) || segmentStart.isEqual(end)) {
+            LocalDate segmentEnd = segmentStart.plusDays(SEGMENT_STEP_DAYS - 1);
+            if (segmentEnd.isAfter(end)) {
+                segmentEnd = end;
+            }
+            segments.add(new LocalDate[]{segmentStart, segmentEnd});
+            segmentStart = segmentEnd.plusDays(1);
+        }
+
+        ExecutorService executor = Executors.newFixedThreadPool(Math.min(segments.size(), 8));
+        List<Future<List<LiveDataCompanyVO>>> futures = new ArrayList<>();
+        try {
+            for (LocalDate[] seg : segments) {
+                LiveDataCompanyParam segmentParam = new LiveDataCompanyParam();
+                segmentParam.setCompanyName(param.getCompanyName());
+                segmentParam.setCompanyIds(param.getCompanyIds());
+                segmentParam.setStartDate(Date.from(seg[0].atStartOfDay(ZoneId.systemDefault()).toInstant()));
+                segmentParam.setEndDate(Date.from(seg[1].atStartOfDay(ZoneId.systemDefault()).toInstant()));
+
+                Future<List<LiveDataCompanyVO>> future = executor.submit(() -> queryLiveDataCompanyByDateRange(segmentParam));
+                futures.add(future);
+            }
+
+            List<List<LiveDataCompanyVO>> segmentResults = new ArrayList<>();
+            for (Future<List<LiveDataCompanyVO>> future : futures) {
+                segmentResults.add(future.get(60, TimeUnit.SECONDS));
+            }
+
+            return mergeSegmentResults(segmentResults);
+        } catch (ExecutionException e) {
+            log.error("分公司直播数据分段查询异常", e);
+            throw new RuntimeException("分公司直播数据查询失败:" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()));
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.error("分公司直播数据分段查询被中断", e);
+            throw new RuntimeException("分公司直播数据查询被中断");
+        } catch (TimeoutException e) {
+            log.error("分公司直播数据分段查询超时", e);
+            throw new ServiceException("查询超时,请缩小时间范围后重试");
+        } finally {
+            executor.shutdown();
+        }
+    }
+
+    /**
+     * 合并分段查询结果:按分公司汇总各分段统计数据
+     */
+    private List<LiveDataCompanyVO> mergeSegmentResults(List<List<LiveDataCompanyVO>> segmentResults) {
+        Map<Long, LiveDataCompanyVO> resultMap = new HashMap<>();
+        for (List<LiveDataCompanyVO> list : segmentResults) {
+            if (list == null) continue;
+            for (LiveDataCompanyVO source : list) {
+                if (source == null || source.getCompanyId() == null) continue;
+                LiveDataCompanyVO target = resultMap.computeIfAbsent(source.getCompanyId(), k -> {
+                    LiveDataCompanyVO vo = new LiveDataCompanyVO();
+                    vo.setCompanyId(source.getCompanyId());
+                    vo.setCompanyName(source.getCompanyName());
+                    vo.setTotalAttendanceCount(0L);
+                    vo.setTotalCompleteCount(0L);
+                    vo.setLiveAttendanceCount(0L);
+                    vo.setLiveCompleteCount(0L);
+                    vo.setReplayAttendanceCount(0L);
+                    vo.setReplayCompleteCount(0L);
+                    vo.setOrderCount(0L);
+                    vo.setOrderUserCount(0L);
+                    vo.setEmployeeCount(0L);
+                    vo.setGmv(BigDecimal.ZERO);
+                    vo.setTotalCompleteRate(0.0);
+                    vo.setLiveCompleteRate(0.0);
+                    vo.setReplayCompleteRate(0.0);
+                    return vo;
+                });
+                // 累加各分段数据
+                target.setTotalAttendanceCount((target.getTotalAttendanceCount() == null ? 0L : target.getTotalAttendanceCount())
+                        + (source.getTotalAttendanceCount() == null ? 0L : source.getTotalAttendanceCount()));
+                target.setTotalCompleteCount((target.getTotalCompleteCount() == null ? 0L : target.getTotalCompleteCount())
+                        + (source.getTotalCompleteCount() == null ? 0L : source.getTotalCompleteCount()));
+                target.setLiveAttendanceCount((target.getLiveAttendanceCount() == null ? 0L : target.getLiveAttendanceCount())
+                        + (source.getLiveAttendanceCount() == null ? 0L : source.getLiveAttendanceCount()));
+                target.setLiveCompleteCount((target.getLiveCompleteCount() == null ? 0L : target.getLiveCompleteCount())
+                        + (source.getLiveCompleteCount() == null ? 0L : source.getLiveCompleteCount()));
+                target.setReplayAttendanceCount((target.getReplayAttendanceCount() == null ? 0L : target.getReplayAttendanceCount())
+                        + (source.getReplayAttendanceCount() == null ? 0L : source.getReplayAttendanceCount()));
+                target.setReplayCompleteCount((target.getReplayCompleteCount() == null ? 0L : target.getReplayCompleteCount())
+                        + (source.getReplayCompleteCount() == null ? 0L : source.getReplayCompleteCount()));
+                target.setOrderCount((target.getOrderCount() == null ? 0L : target.getOrderCount())
+                        + (source.getOrderCount() == null ? 0L : source.getOrderCount()));
+                target.setOrderUserCount((target.getOrderUserCount() == null ? 0L : target.getOrderUserCount())
+                        + (source.getOrderUserCount() == null ? 0L : source.getOrderUserCount()));
+                target.setGmv(roundGmv((target.getGmv() == null ? BigDecimal.ZERO : target.getGmv())
+                        .add(source.getGmv() == null ? BigDecimal.ZERO : source.getGmv())));
+                if (target.getEmployeeCount() == null && source.getEmployeeCount() != null) {
+                    target.setEmployeeCount(source.getEmployeeCount());
+                }
+            }
+        }
+        for (LiveDataCompanyVO vo : resultMap.values()) {
+            long totalAttend = vo.getTotalAttendanceCount() == null ? 0L : vo.getTotalAttendanceCount();
+            long totalComplete = vo.getTotalCompleteCount() == null ? 0L : vo.getTotalCompleteCount();
+            long liveAttend = vo.getLiveAttendanceCount() == null ? 0L : vo.getLiveAttendanceCount();
+            long liveComplete = vo.getLiveCompleteCount() == null ? 0L : vo.getLiveCompleteCount();
+            long replayAttend = vo.getReplayAttendanceCount() == null ? 0L : vo.getReplayAttendanceCount();
+            long replayComplete = vo.getReplayCompleteCount() == null ? 0L : vo.getReplayCompleteCount();
+            vo.setTotalCompleteRate(roundRate(totalAttend > 0 ? totalComplete * 100.0 / totalAttend : 0.0));
+            vo.setLiveCompleteRate(roundRate(liveAttend > 0 ? liveComplete * 100.0 / liveAttend : 0.0));
+            vo.setReplayCompleteRate(roundRate(replayAttend > 0 ? replayComplete * 100.0 / replayAttend : 0.0));
+            vo.setGmv(roundGmv(vo.getGmv()));
+        }
+        return resultMap.values().stream()
+                .sorted(Comparator.comparing(vo -> Optional.ofNullable(vo.getCompanyName()).orElse("")))
+                .collect(Collectors.toList());
+    }
+
+    /** 百分比四舍五入保留2位小数 */
+    private static double roundRate(double rate) {
+        return Math.round(rate * 100.0) / 100.0;
+    }
+
+    /** GMV四舍五入保留2位小数 */
+    private static BigDecimal roundGmv(BigDecimal gmv) {
+        return gmv == null ? BigDecimal.ZERO : gmv.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 从到课/完课等查询结果中提取公司ID集合(去重)
+     */
+    @SafeVarargs
+    private final List<Long> buildCompanyIdsFromResults(List<LiveDataCompanyVO>... lists) {
+        Set<Long> companyIds = new HashSet<>();
+        for (List<LiveDataCompanyVO> list : lists) {
+            if (list == null) continue;
+            for (LiveDataCompanyVO vo : list) {
+                if (vo != null && vo.getCompanyId() != null) {
+                    companyIds.add(vo.getCompanyId());
+                }
+            }
+        }
+        return new ArrayList<>(companyIds);
+    }
+
+    /**
+     * 按日期范围查询分公司直播数据(单段,供分段或直接调用)
+     */
+    private List<LiveDataCompanyVO> queryLiveDataCompanyByDateRange(LiveDataCompanyParam param) {
+        List<Long> liveIds = baseMapper.selectLiveIdsByCompanyParam(param);
+        if (liveIds == null || liveIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<LiveDataCompanyVO> attendanceList = baseMapper.selectAttendanceCountByCompany(liveIds, param.getCompanyIds());
+        List<LiveDataCompanyVO> completeList = baseMapper.selectCompleteCountByCompany(liveIds, param.getCompanyIds());
+        List<LiveDataCompanyVO> liveAttendList = baseMapper.selectLiveAttendCountByCompany(liveIds, param.getCompanyIds());
+        List<LiveDataCompanyVO> liveCompleteList = baseMapper.selectLiveCompleteCountByCompany(liveIds, param.getCompanyIds());
+        List<LiveDataCompanyVO> replayAttendList = baseMapper.selectReplayAttendCountByCompany(liveIds, param.getCompanyIds());
+        List<LiveDataCompanyVO> replayCompleteList = baseMapper.selectReplayCompleteCountByCompany(liveIds, param.getCompanyIds());
+        // 从上面查询结果中提取公司ID集合,供后续GMV和员工数查询使用
+        List<Long> companyIdsFromResult = buildCompanyIdsFromResults(
+                attendanceList, completeList, liveAttendList, liveCompleteList, replayAttendList, replayCompleteList);
+        List<LiveDataCompanyVO> gmvAndOrderList = baseMapper.selectCompanyOrderAndGmv(liveIds, companyIdsFromResult);
+        List<LiveDataCompanyVO> empCountList = baseMapper.selectCompanyEmployeeCountByLiveIds(liveIds, companyIdsFromResult);
+
+        Map<Long, LiveDataCompanyVO> resultMap = new HashMap<>();
+        BiConsumer<List<LiveDataCompanyVO>, BiConsumer<LiveDataCompanyVO, LiveDataCompanyVO>> merge =
+                (vos, setter) -> {
+                    if (vos == null) return;
+                    for (LiveDataCompanyVO source : vos) {
+                        if (source == null || source.getCompanyId() == null) continue;
+                        LiveDataCompanyVO target = resultMap.computeIfAbsent(source.getCompanyId(), k -> {
+                            LiveDataCompanyVO vo = new LiveDataCompanyVO();
+                            vo.setCompanyId(source.getCompanyId());
+                            vo.setCompanyName(source.getCompanyName());
+                            vo.setTotalAttendanceCount(0L);
+                            vo.setTotalCompleteCount(0L);
+                            vo.setLiveAttendanceCount(0L);
+                            vo.setLiveCompleteCount(0L);
+                            vo.setReplayAttendanceCount(0L);
+                            vo.setReplayCompleteCount(0L);
+                            vo.setOrderCount(0L);
+                            vo.setOrderUserCount(0L);
+                            vo.setEmployeeCount(0L);
+                            vo.setGmv(BigDecimal.ZERO);
+                            vo.setTotalCompleteRate(0.0);
+                            vo.setLiveCompleteRate(0.0);
+                            vo.setReplayCompleteRate(0.0);
+                            return vo;
+                        });
+                        setter.accept(target, source);
+                    }
+                };
+
+        merge.accept(attendanceList, (t, s) -> t.setTotalAttendanceCount(s.getTotalAttendanceCount() == null ? 0L : s.getTotalAttendanceCount()));
+        merge.accept(completeList, (t, s) -> t.setTotalCompleteCount(s.getTotalCompleteCount() == null ? 0L : s.getTotalCompleteCount()));
+        merge.accept(liveAttendList, (t, s) -> t.setLiveAttendanceCount(s.getLiveAttendanceCount() == null ? 0L : s.getLiveAttendanceCount()));
+        merge.accept(liveCompleteList, (t, s) -> t.setLiveCompleteCount(s.getLiveCompleteCount() == null ? 0L : s.getLiveCompleteCount()));
+        merge.accept(replayAttendList, (t, s) -> t.setReplayAttendanceCount(s.getReplayAttendanceCount() == null ? 0L : s.getReplayAttendanceCount()));
+        merge.accept(replayCompleteList, (t, s) -> t.setReplayCompleteCount(s.getReplayCompleteCount() == null ? 0L : s.getReplayCompleteCount()));
+        merge.accept(gmvAndOrderList, (t, s) -> {
+            t.setGmv(roundGmv(s.getGmv() == null ? BigDecimal.ZERO : s.getGmv()));
+            t.setOrderCount(s.getOrderCount() == null ? 0L : s.getOrderCount());
+            t.setOrderUserCount(s.getOrderUserCount() == null ? 0L : s.getOrderUserCount());
+        });
+        merge.accept(empCountList, (t, s) -> t.setEmployeeCount(s.getEmployeeCount() == null ? 0L : s.getEmployeeCount()));
+
+        for (LiveDataCompanyVO vo : resultMap.values()) {
+            long totalAttend = vo.getTotalAttendanceCount() == null ? 0L : vo.getTotalAttendanceCount();
+            long totalComplete = vo.getTotalCompleteCount() == null ? 0L : vo.getTotalCompleteCount();
+            long liveAttend = vo.getLiveAttendanceCount() == null ? 0L : vo.getLiveAttendanceCount();
+            long liveComplete = vo.getLiveCompleteCount() == null ? 0L : vo.getLiveCompleteCount();
+            long replayAttend = vo.getReplayAttendanceCount() == null ? 0L : vo.getReplayAttendanceCount();
+            long replayComplete = vo.getReplayCompleteCount() == null ? 0L : vo.getReplayCompleteCount();
+            vo.setTotalCompleteRate(roundRate(totalAttend > 0 ? totalComplete * 100.0 / totalAttend : 0.0));
+            vo.setLiveCompleteRate(roundRate(liveAttend > 0 ? liveComplete * 100.0 / liveAttend : 0.0));
+            vo.setReplayCompleteRate(roundRate(replayAttend > 0 ? replayComplete * 100.0 / replayAttend : 0.0));
+            vo.setGmv(roundGmv(vo.getGmv()));
+        }
+
+        return resultMap.values().stream()
+                .sorted(Comparator.comparing(vo -> Optional.ofNullable(vo.getCompanyName()).orElse("")))
+                .collect(Collectors.toList());
+    }
+
+
+
+
     /**
      * 查询直播数据
      *

+ 62 - 0
fs-service/src/main/java/com/fs/live/vo/LiveDataCompanyVO.java

@@ -0,0 +1,62 @@
+package com.fs.live.vo;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 分公司直播数据统计VO
+ */
+@Data
+public class LiveDataCompanyVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 分公司ID,可能为空(总台) */
+    private Long companyId;
+
+    @Excel(name = "分公司名称")
+    private String companyName;
+
+    @Excel(name = "总到课人数(去重)")
+    private Long totalAttendanceCount;
+
+    @Excel(name = "总完课人数")
+    private Long totalCompleteCount;
+
+    /** 总完课率(百分比 0-100) */
+    @Excel(name = "总完课率")
+    private Double totalCompleteRate;
+
+    @Excel(name = "直播课人数(去重)")
+    private Long liveAttendanceCount;
+
+    @Excel(name = "直播完课人数")
+    private Long liveCompleteCount;
+
+    @Excel(name = "直播完课率")
+    private Double liveCompleteRate;
+
+    @Excel(name = "回放课人数(去重)")
+    private Long replayAttendanceCount;
+
+    @Excel(name = "回放完课人数(去重)")
+    private Long replayCompleteCount;
+
+    @Excel(name = "回放完课率")
+    private Double replayCompleteRate;
+
+    @Excel(name = "GMV")
+    private BigDecimal gmv = BigDecimal.ZERO;
+
+    @Excel(name = "订单数")
+    private Long orderCount;
+
+    @Excel(name = "下单人数")
+    private Long orderUserCount;
+
+    @Excel(name = "现存员工人数(去重)")
+    private Long employeeCount;
+}

+ 223 - 0
fs-service/src/main/resources/mapper/live/LiveDataMapper.xml

@@ -565,4 +565,227 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         GROUP BY u.user_id, u.nick_name, u.nickname, order_info.orderCount, order_info.orderAmount, c.company_name, cu.user_name
         ORDER BY order_info.orderAmount DESC, liveWatchDuration DESC
     </select>
+
+    <!-- 查询满足条件的直播间ID -->
+    <select id="selectLiveIdsByCompanyParam" parameterType="com.fs.live.param.LiveDataCompanyParam" resultType="java.lang.Long">
+        SELECT distinct l.live_id
+        FROM live l
+                 LEFT JOIN company c ON l.company_id = c.company_id
+        WHERE l.is_del = 0
+          AND l.is_audit = 1
+          AND l.is_show = 1
+        <if test="startDate != null">
+            AND l.start_time &gt;= #{startDate}
+        </if>
+        <if test="endDate != null">
+            AND l.start_time &lt;= #{endDate}
+        </if>
+        ORDER BY l.live_id
+    </select>
+
+    <!-- 分公司总到课人数 -->
+    <select id="selectAttendanceCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT lwu.user_id), 0) AS totalAttendanceCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司总完课人数 -->
+    <select id="selectCompleteCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT CASE
+                WHEN lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                     AND COALESCE(vd.total_duration, 0) > 0
+                THEN lwu.user_id END), 0) AS totalCompleteCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+                 LEFT JOIN (
+                    SELECT live_id, SUM(COALESCE(duration, 0)) AS total_duration
+                    FROM live_video
+                    WHERE video_type IN (1, 2)
+                    GROUP BY live_id
+                 ) vd ON lwu.live_id = vd.live_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司直播课人数 -->
+    <select id="selectLiveAttendCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT CASE WHEN lwu.live_flag = 1 AND lwu.replay_flag = 0 THEN lwu.user_id END), 0) AS liveAttendanceCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司直播完课人数 -->
+    <select id="selectLiveCompleteCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT CASE
+                WHEN lwu.live_flag = 1
+                     AND lwu.replay_flag = 0
+                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                     AND COALESCE(vd.total_duration, 0) > 0
+                THEN lwu.user_id END), 0) AS liveCompleteCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+                 LEFT JOIN (
+                    SELECT live_id, SUM(COALESCE(duration, 0)) AS total_duration
+                    FROM live_video
+                    WHERE video_type IN (1, 2)
+                    GROUP BY live_id
+                 ) vd ON lwu.live_id = vd.live_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司回放课人数 -->
+    <select id="selectReplayAttendCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT CASE WHEN lwu.live_flag = 0 AND lwu.replay_flag = 1 THEN lwu.user_id END), 0) AS replayAttendanceCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司回放完课人数 -->
+    <select id="selectReplayCompleteCountByCompany" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lufe.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT CASE
+                WHEN lwu.live_flag = 0
+                     AND lwu.replay_flag = 1
+                     AND lwu.online_seconds >= COALESCE(vd.total_duration, 0)
+                     AND COALESCE(vd.total_duration, 0) > 0
+                THEN lwu.user_id END), 0) AS replayCompleteCount
+        FROM live_watch_user lwu
+                 LEFT JOIN live_user_first_entry lufe ON lwu.live_id = lufe.live_id AND lwu.user_id = lufe.user_id
+                 LEFT JOIN company c ON lufe.company_id = c.company_id
+                 LEFT JOIN (
+                    SELECT live_id, SUM(COALESCE(duration, 0)) AS total_duration
+                    FROM live_video
+                    WHERE video_type IN (1, 2)
+                    GROUP BY live_id
+                 ) vd ON lwu.live_id = vd.live_id
+        WHERE lwu.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lufe.company_id, c.company_name
+    </select>
+
+    <!-- 分公司订单/GMV统计 -->
+    <select id="selectCompanyOrderAndGmv" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            lo.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(SUM(CASE WHEN lo.is_pay = '1' THEN lo.pay_price ELSE 0 END), 0) AS gmv,
+            COALESCE(COUNT(DISTINCT lo.order_id), 0) AS orderCount,
+            COALESCE(COUNT(DISTINCT lo.user_id), 0) AS orderUserCount
+        FROM live_order lo
+                 LEFT JOIN company c ON lo.company_id = c.company_id
+        WHERE lo.live_id IN
+        <foreach collection="liveIds" item="liveId" open="(" separator="," close=")">
+            #{liveId}
+        </foreach>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lo.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY lo.company_id, c.company_name
+    </select>
+
+    <!-- 分公司员工数量统计 -->
+    <select id="selectCompanyEmployeeCountByLiveIds" resultType="com.fs.live.vo.LiveDataCompanyVO">
+        SELECT
+            cu.company_id AS companyId,
+            COALESCE(c.company_name, '总台') AS companyName,
+            COALESCE(COUNT(DISTINCT cu.user_id), 0) AS employeeCount
+        FROM company_user cu
+            LEFT JOIN company c ON cu.company_id = c.company_id
+        WHERE 
+        cu.status = 0
+        AND cu.del_flag = 0
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND cu.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        GROUP BY cu.company_id, c.company_name
+    </select>
 </mapper>

+ 27 - 0
fs-service/src/main/resources/mapper/live/LiveMapper.xml

@@ -364,5 +364,32 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by create_time desc
     </select>
 
+    <select id="selectLiveIdsByCompanyParam" resultType="java.lang.Long">
+        SELECT l.live_id
+        FROM live l
+                 LEFT JOIN company c ON l.company_id = c.company_id
+                 LEFT JOIN live_user_first_entry lufe ON l.live_id = lufe.live_id
+        WHERE l.is_del = 0
+          AND l.is_audit = 1
+          AND l.is_show = 1
+        <if test="companyName != null and companyName != ''">
+            AND (c.company_name LIKE CONCAT('%', #{companyName}, '%')
+                OR (#{companyName} = '总台' AND l.company_id IS NULL))
+        </if>
+        <if test="startDate != null">
+            AND DATE(l.start_time) &gt;= DATE(#{startDate})
+        </if>
+        <if test="endDate != null">
+            AND DATE(l.start_time) &lt;= DATE(#{endDate})
+        </if>
+        <if test="companyIds != null and companyIds.size() > 0">
+            AND lufe.company_id IN
+            <foreach collection="companyIds" item="cid" open="(" separator="," close=")">
+                #{cid}
+            </foreach>
+        </if>
+        ORDER BY l.live_id
+    </select>
+
 
 </mapper>