Browse Source

Merge remote-tracking branch 'origin/master'

zx 1 week ago
parent
commit
fb75fa6d8e
60 changed files with 3516 additions and 200 deletions
  1. 145 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserWatchCourseStatisticsController.java
  2. 118 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserWatchStatisticsController.java
  3. 103 0
      fs-admin/src/main/java/com/fs/his/controller/FsUserOnlineStateController.java
  4. 56 0
      fs-admin/src/main/java/com/fs/his/task/WatchCourseTask.java
  5. 9 0
      fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java
  6. 12 0
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  7. 35 21
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseAnswerLogsController.java
  8. 1 2
      fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java
  9. 118 0
      fs-company/src/main/java/com/fs/company/controller/store/FsUserOnlineStateController.java
  10. 15 3
      fs-company/src/main/java/com/fs/user/FsUserAdminController.java
  11. 5 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java
  12. 3 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  13. 9 1
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  14. 11 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  15. 4 0
      fs-service/src/main/java/com/fs/company/vo/CompanyUserQwListVO.java
  16. 6 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java
  17. 139 0
      fs-service/src/main/java/com/fs/course/domain/FsUserWatchCourseStatistics.java
  18. 77 0
      fs-service/src/main/java/com/fs/course/domain/FsUserWatchStatistics.java
  19. 4 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java
  20. 7 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java
  21. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoRedPackageMapper.java
  22. 75 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserWatchCourseStatisticsMapper.java
  23. 74 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserWatchStatisticsMapper.java
  24. 17 0
      fs-service/src/main/java/com/fs/course/param/FsCourseAnswerLogsParam.java
  25. 4 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseAnswerLogsService.java
  26. 75 0
      fs-service/src/main/java/com/fs/course/service/IFsUserWatchCourseStatisticsService.java
  27. 67 0
      fs-service/src/main/java/com/fs/course/service/IFsUserWatchStatisticsService.java
  28. 92 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java
  29. 19 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  30. 242 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchCourseStatisticsServiceImpl.java
  31. 216 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchStatisticsServiceImpl.java
  32. 46 32
      fs-service/src/main/java/com/fs/course/vo/FsCourseAnswerLogsListVO.java
  33. 8 2
      fs-service/src/main/java/com/fs/course/vo/FsUserCourseListPVO.java
  34. 101 0
      fs-service/src/main/java/com/fs/course/vo/FsUserWatchCourseStatisticsExportVO.java
  35. 1 1
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  36. 96 0
      fs-service/src/main/java/com/fs/his/domain/FsUserOnlineState.java
  37. 24 1
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  38. 76 0
      fs-service/src/main/java/com/fs/his/mapper/FsUserOnlineStateMapper.java
  39. 68 0
      fs-service/src/main/java/com/fs/his/service/IFsUserOnlineStateService.java
  40. 10 1
      fs-service/src/main/java/com/fs/his/service/IFsUserService.java
  41. 136 0
      fs-service/src/main/java/com/fs/his/service/impl/FsUserOnlineStateServiceImpl.java
  42. 33 8
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  43. 7 94
      fs-service/src/main/java/com/fs/his/vo/FsUserVO.java
  44. 8 0
      fs-service/src/main/java/com/fs/store/mapper/FsUserCourseCountMapper.java
  45. 36 0
      fs-service/src/main/java/com/fs/store/vo/FsUserLastCount.java
  46. 27 15
      fs-service/src/main/resources/application-config-druid-fby.yml
  47. 8 0
      fs-service/src/main/resources/mapper/company/CompanyMapper.xml
  48. 16 9
      fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml
  49. 79 0
      fs-service/src/main/resources/mapper/course/FsCourseAnswerLogsMapper.xml
  50. 2 2
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  51. 22 1
      fs-service/src/main/resources/mapper/course/FsUserCoursePeriodDaysMapper.xml
  52. 1 1
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  53. 9 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoRedPackageMapper.xml
  54. 308 0
      fs-service/src/main/resources/mapper/course/FsUserWatchCourseStatisticsMapper.xml
  55. 176 0
      fs-service/src/main/resources/mapper/course/FsUserWatchStatisticsMapper.xml
  56. 224 3
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  57. 202 0
      fs-service/src/main/resources/mapper/his/FsUserOnlineStateMapper.xml
  58. 28 1
      fs-service/src/main/resources/mapper/store/FsUserCourseCountMapper.xml
  59. 4 0
      fs-user-app/src/main/java/com/fs/app/controller/UserController.java
  60. 1 0
      fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

+ 145 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserWatchCourseStatisticsController.java

@@ -0,0 +1,145 @@
+package com.fs.course.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.FsUserWatchCourseStatistics;
+import com.fs.course.service.IFsUserWatchCourseStatisticsService;
+import com.fs.course.vo.FsUserWatchCourseStatisticsExportVO;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 会员看课统计-按课程统计Controller
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+@RestController
+@RequestMapping("/course/userWatchCourseStatistics")
+public class FsUserWatchCourseStatisticsController extends BaseController
+{
+    @Autowired
+    private IFsUserWatchCourseStatisticsService fsUserWatchCourseStatisticsService;
+
+    /**
+     * 查询会员看课统计-按课程统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        startPage();
+        List<FsUserWatchCourseStatistics> list = fsUserWatchCourseStatisticsService.selectFsUserWatchCourseStatisticsList(fsUserWatchCourseStatistics);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员看课统计-按课程统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:export')")
+    @Log(title = "会员看课统计-按课程统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        List<FsUserWatchCourseStatistics> list = fsUserWatchCourseStatisticsService.selectFsUserWatchCourseStatisticsList(fsUserWatchCourseStatistics);
+        ExcelUtil<FsUserWatchCourseStatistics> util = new ExcelUtil<FsUserWatchCourseStatistics>(FsUserWatchCourseStatistics.class);
+        return util.exportExcel(list, "会员观看数据明细");
+    }
+
+    /**
+     * 获取会员看课统计-按课程统计详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserWatchCourseStatisticsService.selectFsUserWatchCourseStatisticsById(id));
+    }
+
+    /**
+     * 新增会员看课统计-按课程统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:add')")
+    @Log(title = "会员看课统计-按课程统计", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        return toAjax(fsUserWatchCourseStatisticsService.insertFsUserWatchCourseStatistics(fsUserWatchCourseStatistics));
+    }
+
+    /**
+     * 修改会员看课统计-按课程统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:edit')")
+    @Log(title = "会员看课统计-按课程统计", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        return toAjax(fsUserWatchCourseStatisticsService.updateFsUserWatchCourseStatistics(fsUserWatchCourseStatistics));
+    }
+
+    /**
+     * 删除会员看课统计-按课程统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:remove')")
+    @Log(title = "会员看课统计-按课程统计", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserWatchCourseStatisticsService.deleteFsUserWatchCourseStatisticsByIds(ids));
+    }
+
+    /**
+     * 查询会员观看数据明细汇总
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchCourseStatistics:listTotal')")
+    @GetMapping("/listTotal")
+    public TableDataInfo listTotal(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        startPage();
+        List<FsUserWatchCourseStatistics> list = fsUserWatchCourseStatisticsService.selectFsUserWatchCourseStatisticsListTotal(fsUserWatchCourseStatistics);
+        if(!list.isEmpty()){
+            for (FsUserWatchCourseStatistics userWatchCourseStatistics : list) {
+                userWatchCourseStatistics.setCompleteWatchRatePercent(userWatchCourseStatistics.getCompleteWatchRate() + "%");
+                userWatchCourseStatistics.setOnlineRatePercent(userWatchCourseStatistics.getOnlineRate() + "%");
+            }
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员看课统计-按课程汇总统计列表
+     */
+    @Log(title = "会员看课统计-按课程汇总统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/exportTotal")
+    public AjaxResult exportTotal(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        List<FsUserWatchCourseStatistics> list = fsUserWatchCourseStatisticsService.selectFsUserWatchCourseStatisticsListTotal(fsUserWatchCourseStatistics);
+        List<FsUserWatchCourseStatisticsExportVO> listVO = list.stream().map(v -> {
+            FsUserWatchCourseStatisticsExportVO vo = new FsUserWatchCourseStatisticsExportVO();
+            BeanUtils.copyProperties(v, vo);
+            vo.setCompleteWatchRatePercent(v.getCompleteWatchRate() + "%");
+            vo.setOnlineRatePercent(v.getOnlineRate() + "%");
+            return vo;
+        }).collect(Collectors.toList());
+        ExcelUtil<FsUserWatchCourseStatisticsExportVO> util = new ExcelUtil<FsUserWatchCourseStatisticsExportVO>(FsUserWatchCourseStatisticsExportVO.class);
+        return util.exportExcel(listVO, "会员观看数据明细汇总");
+    }
+
+
+    @PostMapping("/test")
+    @ApiOperation("测试看课统计明细定时任务")
+    public void userCourseCountTask() {
+        fsUserWatchCourseStatisticsService.insertWatchCourseStatistics();
+    }
+}

+ 118 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserWatchStatisticsController.java

@@ -0,0 +1,118 @@
+package com.fs.course.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.course.domain.FsUserWatchStatistics;
+import com.fs.course.service.IFsUserWatchStatisticsService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 会员看课统计-按营期统计Controller
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+@RestController
+@RequestMapping("/course/userWatchStatistics")
+public class FsUserWatchStatisticsController extends BaseController
+{
+    @Autowired
+    private IFsUserWatchStatisticsService fsUserWatchStatisticsService;
+
+    /**
+     * 查询会员看课统计-按营期统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        startPage();
+        List<FsUserWatchStatistics> list = fsUserWatchStatisticsService.selectFsUserWatchStatisticsList(fsUserWatchStatistics);
+        if(!list.isEmpty()){
+            for (FsUserWatchStatistics userWatchStatistics : list) {
+                userWatchStatistics.setCompleteWatchRatePercent(userWatchStatistics.getCompleteWatchRate() + "%");
+                userWatchStatistics.setOnlineRatePercent(userWatchStatistics.getOnlineRate() + "%");
+            }
+        }
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员看课统计-按营期统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:export')")
+    @Log(title = "会员看课统计-按营期统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        List<FsUserWatchStatistics> list = fsUserWatchStatisticsService.selectFsUserWatchStatisticsList(fsUserWatchStatistics);
+        if(!list.isEmpty()){
+            for (FsUserWatchStatistics userWatchStatistics : list) {
+                userWatchStatistics.setCompleteWatchRatePercent(userWatchStatistics.getCompleteWatchRate() + "%");
+                userWatchStatistics.setOnlineRatePercent(userWatchStatistics.getOnlineRate() + "%");
+            }
+        }
+        ExcelUtil<FsUserWatchStatistics> util = new ExcelUtil<FsUserWatchStatistics>(FsUserWatchStatistics.class);
+        return util.exportExcel(list, "会员看课统计-按营期统计数据");
+    }
+
+    /**
+     * 获取会员看课统计-按营期统计详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserWatchStatisticsService.selectFsUserWatchStatisticsById(id));
+    }
+
+    /**
+     * 新增会员看课统计-按营期统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:add')")
+    @Log(title = "会员看课统计-按营期统计", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        return toAjax(fsUserWatchStatisticsService.insertFsUserWatchStatistics(fsUserWatchStatistics));
+    }
+
+    /**
+     * 修改会员看课统计-按营期统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:edit')")
+    @Log(title = "会员看课统计-按营期统计", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        return toAjax(fsUserWatchStatisticsService.updateFsUserWatchStatistics(fsUserWatchStatistics));
+    }
+
+    /**
+     * 删除会员看课统计-按营期统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:userWatchStatistics:remove')")
+    @Log(title = "会员看课统计-按营期统计", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserWatchStatisticsService.deleteFsUserWatchStatisticsByIds(ids));
+    }
+
+
+    @PostMapping("/test")
+    @ApiOperation("测试营期看课统计定时任务")
+    public void userCourseCountTask() {
+        fsUserWatchStatisticsService.insertStatistics();
+    }
+
+}

+ 103 - 0
fs-admin/src/main/java/com/fs/his/controller/FsUserOnlineStateController.java

@@ -0,0 +1,103 @@
+package com.fs.his.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.his.domain.FsUserOnlineState;
+import com.fs.his.service.IFsUserOnlineStateService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+
+/**
+ * 用户上线情况Controller
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+@RestController
+@RequestMapping("/store/userOnlineState")
+public class FsUserOnlineStateController extends BaseController
+{
+    @Autowired
+    private IFsUserOnlineStateService fsUserOnlineStateService;
+
+    /**
+     * 查询用户上线情况列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserOnlineState fsUserOnlineState)
+    {
+        startPage();
+        List<FsUserOnlineState> list = fsUserOnlineStateService.selectFsUserOnlineStateList(fsUserOnlineState);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出用户上线情况列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:export')")
+    @Log(title = "用户上线情况", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserOnlineState fsUserOnlineState)
+    {
+        List<FsUserOnlineState> list = fsUserOnlineStateService.selectFsUserOnlineStateList(fsUserOnlineState);
+        ExcelUtil<FsUserOnlineState> util = new ExcelUtil<FsUserOnlineState>(FsUserOnlineState.class);
+        return util.exportExcel(list, "未上线会员");
+    }
+
+    /**
+     * 获取用户上线情况详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:query')")
+    @GetMapping(value = "/{userId}")
+    public AjaxResult getInfo(@PathVariable("userId") Long userId)
+    {
+        return AjaxResult.success(fsUserOnlineStateService.selectFsUserOnlineStateById(userId));
+    }
+
+    /**
+     * 新增用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:add')")
+    @Log(title = "用户上线情况", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserOnlineState fsUserOnlineState)
+    {
+        return toAjax(fsUserOnlineStateService.insertFsUserOnlineState(fsUserOnlineState));
+    }
+
+    /**
+     * 修改用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:edit')")
+    @Log(title = "用户上线情况", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserOnlineState fsUserOnlineState)
+    {
+        return toAjax(fsUserOnlineStateService.updateFsUserOnlineState(fsUserOnlineState));
+    }
+
+    /**
+     * 删除用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:remove')")
+    @Log(title = "用户上线情况", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        return toAjax(fsUserOnlineStateService.deleteFsUserOnlineStateByIds(userIds));
+    }
+
+    @ApiOperation("新增/更新未上线人员-定时任务")
+    @PostMapping("/testTask")
+    public void test(){
+        fsUserOnlineStateService.insertUserNotOnline();
+    }
+}

+ 56 - 0
fs-admin/src/main/java/com/fs/his/task/WatchCourseTask.java

@@ -0,0 +1,56 @@
+package com.fs.his.task;
+
+import com.fs.course.service.IFsUserWatchCourseStatisticsService;
+import com.fs.course.service.IFsUserWatchStatisticsService;
+import com.fs.erp.service.IErpOrderService;
+import com.fs.his.service.IFsUserOnlineStateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 定时任务调度测试
+ *
+ * @author fs
+ */
+@Component("storeTask")
+public class WatchCourseTask
+{
+    @Autowired
+    IErpOrderService erpOrderService;
+
+    @Autowired
+    private IFsUserWatchCourseStatisticsService fsUserWatchCourseStatisticsService;
+
+    @Autowired
+    private IFsUserWatchStatisticsService fsUserWatchStatisticsService;
+
+    @Autowired
+    private IFsUserOnlineStateService fsUserOnlineStateService;
+
+
+    /**
+     * 添加看课汇总统计
+     */
+    public void insertWatchStatistics(){
+        /***************************************进入营期会员看课汇总统计定时任务****************************************/
+        fsUserWatchStatisticsService.insertStatistics();
+        /***************************************营期会员看课汇总统计定时任务结束***************************************/
+    }
+
+    /**
+     * 添加看课明细统计
+     */
+    public void insertWatchCourseStatistics(){
+        /***************************************进入营期会员看课明细统计定时任务*******************************/
+        fsUserWatchCourseStatisticsService.insertWatchCourseStatistics();
+        /***************************************进入营期会员看课明细统计定时任务结束**********************************************/
+    }
+
+    /**
+     * 定时查询未上线的用户
+     */
+    public void insertUserNotOnline(){
+        fsUserOnlineStateService.insertUserNotOnline();
+    }
+
+}

+ 9 - 0
fs-company-app/src/main/java/com/fs/app/controller/FsUserController.java

@@ -50,6 +50,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
 @Slf4j
 @Api(tags = "用户会员相关接口")
 @RestController
@@ -377,4 +379,11 @@ public class FsUserController extends AppBaseController {
         userCourseCountService.insertFsUserCourseCountTask();
     }
 
+    @GetMapping("/test1")
+    @ApiOperation("1")
+    public void aa(String phone) {
+        String s = encryptPhone(phone);
+        System.out.println(s);
+    }
+
 }

+ 12 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -416,4 +416,16 @@ public class CompanyUserController extends BaseController
             return AjaxResult.error("操作失败");
         }
     }
+
+    @Log(title = "是否允许所有方式注册会员", businessType = BusinessType.UPDATE)
+    @PutMapping("/allowedAllRegister")
+    public AjaxResult isAllowedAllRegister(@RequestParam Boolean status, @RequestBody List<Long> userIds) {
+        Boolean r = companyUserService.isAllowedAllRegister(status, userIds);
+        if (r) {
+            return AjaxResult.success();
+        } else {
+            return AjaxResult.error("操作失败");
+        }
+    }
+
 }

+ 35 - 21
fs-company/src/main/java/com/fs/company/controller/course/FsCourseAnswerLogsController.java

@@ -1,6 +1,7 @@
 package com.fs.company.controller.course;
 
 import com.fs.common.annotation.Log;
+import com.fs.common.constant.HttpStatus;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
@@ -44,16 +45,21 @@ public class FsCourseAnswerLogsController extends BaseController
     @GetMapping("/list")
     public TableDataInfo list(FsCourseAnswerLogsParam param)
     {
-
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
-            param.setPhone(encryptPhone(param.getPhoneMk()));
+            param.setPhone(param.getPhoneMk());
         }
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId( loginUser.getCompany().getCompanyId());
 
-        startPage();
-        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVO(param);
-        return getDataTable(list);
+        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONew(param);
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONewCount(param));
+
+        return rspData;
     }
 
     /**
@@ -64,17 +70,22 @@ public class FsCourseAnswerLogsController extends BaseController
     public TableDataInfo myList(FsCourseAnswerLogsParam param)
     {
 
-
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId( loginUser.getCompany().getCompanyId());
-
+        param.setCompanyUserId(loginUser.getUser().getUserId());
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
-            param.setPhone(encryptPhone(param.getPhoneMk()));
+            param.setPhone(param.getPhoneMk());
         }
-        startPage();
 
-        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVO(param);
-        return getDataTable(list);
+        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONew(param);
+
+        TableDataInfo rspData = new TableDataInfo();
+        rspData.setCode(HttpStatus.SUCCESS);
+        rspData.setMsg("查询成功");
+        rspData.setRows(list);
+        rspData.setTotal(fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONewCount(param));
+
+        return rspData;
     }
     /**
      * 导出答题日志列表
@@ -85,31 +96,34 @@ public class FsCourseAnswerLogsController extends BaseController
     public AjaxResult export(FsCourseAnswerLogsParam param)
     {
 
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId( loginUser.getCompany().getCompanyId());
+
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
-            param.setPhone(encryptPhone(param.getPhoneMk()));
+            param.setPhone(param.getPhoneMk());
         }
-        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVO(param);
+        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONew(param);
         ExcelUtil<FsCourseAnswerLogsListVO> util = new ExcelUtil<FsCourseAnswerLogsListVO>(FsCourseAnswerLogsListVO.class);
-        return util.exportExcel(list, "答题日志数据");
+        return util.exportExcel(list, "答题记录数据");
     }
 
     /**
-     * 导出答题日志列表
+     * 导出我的答题日志列表
      */
     @PreAuthorize("@ss.hasPermi('course:courseAnswerLog:myExport')")
-    @Log(title = "答题日志", businessType = BusinessType.EXPORT)
+    @Log(title = "我的答题日志", businessType = BusinessType.EXPORT)
     @GetMapping("/myExport")
     public AjaxResult myExport(FsCourseAnswerLogsParam param)
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId( loginUser.getCompany().getCompanyId());
-
+        param.setCompanyUserId(loginUser.getUser().getUserId());
         if (param.getPhoneMk() != null && param.getPhoneMk() != "") {
-            param.setPhone(encryptPhone(param.getPhoneMk()));
+            param.setPhone(param.getPhoneMk());
         }
-        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVO(param);
+        List<FsCourseAnswerLogsListVO> list = fsCourseAnswerLogsService.selectFsCourseAnswerLogsListVONew(param);
         ExcelUtil<FsCourseAnswerLogsListVO> util = new ExcelUtil<FsCourseAnswerLogsListVO>(FsCourseAnswerLogsListVO.class);
-        return util.exportExcel(list, "答题日志数据");
+        return util.exportExcel(list, "我的答题记录数据");
     }
 
 //    /**

+ 1 - 2
fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java

@@ -24,7 +24,6 @@ import com.fs.his.vo.UserVo;
 import com.fs.qw.dto.UserProjectDTO;
 import com.fs.store.param.h5.FsUserPageListParam;
 import com.fs.store.vo.h5.FsUserPageListVO;
-import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.ApiOperation;
 import lombok.extern.slf4j.Slf4j;
@@ -247,7 +246,7 @@ public class FsUserController extends BaseController
         PageInfo<FsUserPageListVO> fsUserPageListVOPageInfo = fsUserService.selectFsUserPageList(param);
         Map<String, Object> map = new HashMap<String, Object>();
         map.put("rows", fsUserPageListVOPageInfo.getList());
-        map.put("total", fsUserPageListVOPageInfo.getList().size());
+        map.put("total", fsUserPageListVOPageInfo.getTotal());
         return R.ok(map);
     }
 

+ 118 - 0
fs-company/src/main/java/com/fs/company/controller/store/FsUserOnlineStateController.java

@@ -0,0 +1,118 @@
+package com.fs.company.controller.store;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.his.domain.FsUserOnlineState;
+import com.fs.his.service.IFsUserOnlineStateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 用户上线情况Controller
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+@RestController
+@RequestMapping("/store/userOnlineState")
+public class FsUserOnlineStateController extends BaseController
+{
+    @Autowired
+    private IFsUserOnlineStateService fsUserOnlineStateService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询用户上线情况列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserOnlineState fsUserOnlineState)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        // 如果是管理员,可以查询当前公司所有数据
+        if(loginUser.getUser().isAdmin()){
+            fsUserOnlineState.setCompanyId( loginUser.getCompany().getCompanyId());
+        } else{
+            fsUserOnlineState.setCompanyUserId( loginUser.getUser().getUserId());
+        }
+        List<FsUserOnlineState> list = fsUserOnlineStateService.selectFsUserOnlineStateList(fsUserOnlineState);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出用户上线情况列表
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:export')")
+    @Log(title = "用户上线情况", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserOnlineState fsUserOnlineState)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        // 如果是管理员,可以查询当前公司所有数据
+        if(loginUser.getUser().isAdmin()){
+            fsUserOnlineState.setCompanyId( loginUser.getCompany().getCompanyId());
+        } else{
+            fsUserOnlineState.setCompanyUserId( loginUser.getUser().getUserId());
+        }
+        List<FsUserOnlineState> list = fsUserOnlineStateService.selectFsUserOnlineStateList(fsUserOnlineState);
+        ExcelUtil<FsUserOnlineState> util = new ExcelUtil<FsUserOnlineState>(FsUserOnlineState.class);
+        return util.exportExcel(list, "未上线会员");
+    }
+
+    /**
+     * 获取用户上线情况详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:query')")
+    @GetMapping(value = "/{userId}")
+    public AjaxResult getInfo(@PathVariable("userId") Long userId)
+    {
+        return AjaxResult.success(fsUserOnlineStateService.selectFsUserOnlineStateById(userId));
+    }
+
+    /**
+     * 新增用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:add')")
+    @Log(title = "用户上线情况", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserOnlineState fsUserOnlineState)
+    {
+        return toAjax(fsUserOnlineStateService.insertFsUserOnlineState(fsUserOnlineState));
+    }
+
+    /**
+     * 修改用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:edit')")
+    @Log(title = "用户上线情况", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserOnlineState fsUserOnlineState)
+    {
+        return toAjax(fsUserOnlineStateService.updateFsUserOnlineState(fsUserOnlineState));
+    }
+
+    /**
+     * 删除用户上线情况
+     */
+    @PreAuthorize("@ss.hasPermi('store:userOnlineState:remove')")
+    @Log(title = "用户上线情况", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{userIds}")
+    public AjaxResult remove(@PathVariable Long[] userIds)
+    {
+        return toAjax(fsUserOnlineStateService.deleteFsUserOnlineStateByIds(userIds));
+    }
+
+}

+ 15 - 3
fs-company/src/main/java/com/fs/user/FsUserAdminController.java

@@ -18,7 +18,6 @@ import com.fs.qw.domain.CustomerTransferApproval;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.service.ICustomerTransferApprovalService;
 import com.fs.store.param.h5.FsUserPageListParam;
-import com.fs.store.vo.h5.FsUserPageListVO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.AllArgsConstructor;
@@ -29,8 +28,6 @@ import org.springframework.web.bind.annotation.*;
 
 import java.util.Date;
 
-import static com.fs.his.utils.PhoneUtil.decryptAutoPhoneMk;
-
 @Api(tags = "会员管理接口")
 @RestController
 @Slf4j
@@ -69,6 +66,21 @@ public class FsUserAdminController extends BaseController {
         return fsUserService.selectFsUserPageListNew(param);
     }
 
+    @PreAuthorize("@ss.hasPermi('user:fsUser:myList')")
+    @PostMapping("/myList")
+    @ApiOperation("我的会员列表(与移动端使用的相同查询)")
+    public TableDataInfo pageMyList(@RequestBody FsUserPageListParam param) {
+
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+        param.setCompanyUserId(String.valueOf(loginUser.getUser().getUserId()));
+
+        if(param.getCompanyUserId() == null) {
+            throw new IllegalArgumentException("当前销售不存在!");
+        }
+        return fsUserService.selectFsUserPageListNew(param);
+    }
+
     @PostMapping("/auditUser")
     @ApiOperation("审核用户(移除小黑屋)")
     public R auditUser(@RequestBody String[] userIds) {

+ 5 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyMapper.java

@@ -2,6 +2,8 @@ package com.fs.company.mapper;
 
 import java.math.BigDecimal;
 import java.util.List;
+import java.util.Set;
+
 import com.fs.company.domain.Company;
 import com.fs.company.param.CompanyParam;
 import com.fs.company.vo.CompanyCrmVO;
@@ -184,4 +186,7 @@ public interface CompanyMapper
      * 通过企业id批量查询
      * **/
     List<Company> selectCompanyByIds(@Param("ids") List<Long> ids);
+
+    List<Company> selectCompanyByIds2(@Param("companyIds") Set<Long> companyIds);
+
 }

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

@@ -305,4 +305,7 @@ public interface CompanyUserMapper
     CompanyUser selectCompanyUserByCompanyUserId(Long companyUserId);
 
     String selectCompanyUserNameByIds(@Param("companyUserIds")String companyUserIds);
+
+    int updateAllowedAllRegister(@Param("status") boolean status, @Param("userIds")List<Long> userIds);
+
 }

+ 9 - 1
fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java

@@ -14,7 +14,6 @@ import com.fs.qw.dto.UserProjectDTO;
 import com.fs.qw.vo.CompanyUserQwVO;
 import com.fs.qw.vo.QwOptionsVO;
 import com.fs.qw.vo.QwUserVO;
-import com.fs.store.vo.h5.UserListPageVO;
 import com.fs.wxUser.domain.CompanyWxUser;
 
 import java.util.List;
@@ -212,4 +211,13 @@ public interface ICompanyUserService {
     List<QwOptionsVO> selectQwUserListLikeName(Map<String, Object> params);
 
     String selectCompanyUserByStrIds(String companyUserIds);
+
+    /**
+     * 批量设置是否允许所有方式注册会员
+     * @param status 开/关
+     * @param userIds 员工id
+     * @return true/false
+     */
+    Boolean isAllowedAllRegister(boolean status,  List<Long> userIds);
+
 }

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

@@ -613,4 +613,15 @@ public class CompanyUserServiceImpl implements ICompanyUserService
     public String selectCompanyUserByStrIds(String companyUserIds) {
         return companyUserMapper.selectCompanyUserNameByIds(companyUserIds);
     }
+
+    @Override
+    public Boolean isAllowedAllRegister(boolean status, List<Long> userIds) {
+        try {
+            companyUserMapper.updateAllowedAllRegister(status, userIds);
+        } catch (RuntimeException e) {
+            throw new ServiceException("操作异常");
+        }
+        return true;
+    }
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyUserQwListVO.java

@@ -124,4 +124,8 @@ public class CompanyUserQwListVO extends BaseEntity {
 
     /** 是否需要单独注册会员,1-是,0-否*/
     private Integer isNeedRegisterMember;
+
+    /** 是否允许所有方式注册会员,1-是,0-否,默认1(用于个微注册会员) */
+    private Integer isAllowedAllRegister;
+
 }

+ 6 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCoursePeriod.java

@@ -9,6 +9,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.Date;
 import java.util.List;
 
 /**
@@ -112,4 +113,9 @@ public class FsUserCoursePeriod
      */
     private Integer delFlag;
 
+    /**
+     * 营期线,即营期首次播放课程的日期
+     */
+    private Date periodLine;
+
 }

+ 139 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserWatchCourseStatistics.java

@@ -0,0 +1,139 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 会员看课统计-按课程统计对象 fs_user_watch_course_statistics
+ *
+ * @author fs
+ * @date 2025-06-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserWatchCourseStatistics extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 营期id */
+//    @Excel(name = "营期id")
+    private Long periodId;
+
+    /** 营期名称 */
+    @Excel(name = "营期名称")
+    private String periodName;
+
+    /** 营期开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期线", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date periodStartingTime;
+
+    /** 课程开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "播出时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date courseStartDateTime;
+
+    /** 课程id */
+//    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 课程名称 */
+    @Excel(name = "课程名称")
+    private String courseName;
+
+    /** 视频id */
+//    @Excel(name = "视频id")
+    private Long videoId;
+
+    /** 视频标题 */
+    @Excel(name = "视频小节")
+    private String videoTitle;
+
+    /** 销售公司id */
+//    @Excel(name = "销售公司id")
+    private Long companyId;
+
+    /** 销售公司名称 */
+    @Excel(name = "销售公司")
+    private String companyName;
+
+    /** 销售id */
+//    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 销售名称 */
+    @Excel(name = "所属销售")
+    private String companyUserName;
+
+    /** 新增会员数量 */
+    @Excel(name = "新增会员")
+    private Integer newUserNum;
+
+    /** 会员数量 */
+    @Excel(name = "会员数量")
+    private Integer userNum;
+
+    /** 观看人数 */
+    @Excel(name = "观看人数")
+    private Integer watchNum;
+
+    /** 上线率+% */
+    @Excel(name = "上线率")
+    private String onlineRatePercent;
+
+    /** 完播人数 */
+    @Excel(name = "完播人数")
+    private Integer completeWatchNum;
+
+    /** 上线率 */
+    private BigDecimal onlineRate;
+
+    /** 完播率 */
+//    @Excel(name = "完播率")
+    private BigDecimal completeWatchRate;
+
+    /** 完播率+% */
+    @Excel(name = "完播率")
+    private String completeWatchRatePercent;
+
+    /** 答题人数 */
+    @Excel(name = "答题人数")
+    private Integer answerNum;
+
+    /** 答题正确人数 */
+    @Excel(name = "正确人数")
+    private Integer answerRightNum;
+
+    /** 答题正确率 */
+//    @Excel(name = "正确率")
+    private BigDecimal answerRightRate;
+
+    /** 答题正确率+% */
+    @Excel(name = "正确率")
+    private String answerRightRatePercent;
+
+    /** 红包领取数量 */
+    @Excel(name = "红包领取个数")
+    private Integer redPacketNum;
+
+    /** 红包领取总额 */
+    @Excel(name = "红包领取总额")
+    private BigDecimal redPacketAmount;
+
+    /** 用户的创建日期 */
+    private Date userCreateDate;
+
+    /** 营期日期 */
+    @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date periodLine;
+
+}

+ 77 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserWatchStatistics.java

@@ -0,0 +1,77 @@
+package com.fs.course.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 会员看课统计-按营期统计对象 fs_user_watch_statistics
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserWatchStatistics extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 营期id */
+//    @Excel(name = "营期id")
+    private Long periodId;
+
+    /** 营期名称 */
+    @Excel(name = "营期名称")
+    private String periodName;
+
+    /** 营期开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期线", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date periodStartingTime;
+
+    /** 销售公司id */
+//    @Excel(name = "销售公司id")
+    private String companyId;
+
+    /** 销售公司名称 */
+    @Excel(name = "销售公司")
+    private String companyName;
+
+    /** 新增会员数量 */
+    @Excel(name = "新增会员数量")
+    private Integer newUserNum;
+
+    /** 会员数量 */
+    @Excel(name = "会员数量")
+    private Integer userNum;
+
+    /** 观看人数 */
+    @Excel(name = "观看人数")
+    private Integer watchNum;
+
+    /** 上线率+% */
+    @Excel(name = "上线率")
+    private String onlineRatePercent;
+
+    /** 完播人数 */
+    @Excel(name = "完播人数")
+    private Integer completeWatchNum;
+
+    /** 上线率 */
+    private BigDecimal onlineRate;
+
+    /** 完播率 */
+//    @Excel(name = "完播率")
+    private BigDecimal completeWatchRate;
+
+    /** 完播率+% */
+    @Excel(name = "完播率")
+    private String completeWatchRatePercent;
+
+}

+ 4 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseAnswerLogsMapper.java

@@ -123,4 +123,8 @@ public interface FsCourseAnswerLogsMapper
     @Select("select count(log_id) from fs_course_red_packet_log where user_id = #{userId} and video_id = #{videoId}")
     Long selectRedStatus(@Param("userId") Long userId, @Param("videoId") Long videoId);
 
+    List<FsCourseAnswerLogsListVO> selectFsCourseAnswerLogsListVONew(FsCourseAnswerLogsParam param);
+
+    Long selectFsCourseAnswerLogsListVONewCount(FsCourseAnswerLogsParam param);
+
 }

+ 7 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java

@@ -2,6 +2,7 @@ package com.fs.course.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsUserCoursePeriodDays;
+import com.fs.course.domain.FsUserWatchCourseStatistics;
 import com.fs.his.vo.OptionsVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Update;
@@ -97,6 +98,12 @@ public interface FsUserCoursePeriodDaysMapper extends BaseMapper<FsUserCoursePer
      */
     void endPeriodCourse(@Param("now") LocalDateTime now);
 
+    /**
+     * 查询视频相关信息
+     * @return
+     */
+    List<FsUserWatchCourseStatistics> selectDaysCountList();
+
     /**
      * 批量删除
      * @param ids

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

@@ -71,8 +71,8 @@ public interface FsUserCourseVideoRedPackageMapper
             "ON DUPLICATE KEY UPDATE red_packet_money = VALUES(red_packet_money);")
     void insertOrUpdateFsUserCourseVideoRedPackage(FsUserCourseVideoParam fsUserCourseVideo);
 
-    @Select("select * from fs_user_course_video_red_package where video_id =#{videoId} and company_id = #{companyId} and period_id = #{periodId}")
     FsUserCourseVideoRedPackage selectRedPacketByCompanyId(@Param("videoId") Long videoId,@Param("companyId") Long companyId, @Param("periodId") Long periodId);
+
     /**
      * 批量查询匹配的红包数据
      *

+ 75 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserWatchCourseStatisticsMapper.java

@@ -0,0 +1,75 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsUserWatchCourseStatistics;
+
+import java.util.List;
+
+/**
+ * 会员看课统计-按课程统计Mapper接口
+ *
+ * @author fs
+ * @date 2025-06-17
+ */
+public interface FsUserWatchCourseStatisticsMapper extends BaseMapper<FsUserWatchCourseStatistics>{
+    /**
+     * 查询会员看课统计-按课程统计
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 会员看课统计-按课程统计
+     */
+    FsUserWatchCourseStatistics selectFsUserWatchCourseStatisticsById(Long id);
+
+    /**
+     * 查询会员看课统计-按课程统计列表
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 会员看课统计-按课程统计集合
+     */
+    List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsList(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 查询会员看课明细汇总
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 会员看课统计-按课程统计集合
+     */
+    List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsListTotal(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 新增会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    int insertFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 修改会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    int updateFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 删除会员看课统计-按课程统计
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 结果
+     */
+    int deleteFsUserWatchCourseStatisticsById(Long id);
+
+    /**
+     * 批量删除会员看课统计-按课程统计
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserWatchCourseStatisticsByIds(Long[] ids);
+
+    /**
+     * 插入看课统计数据
+     */
+    void insertFsUserWatchCourseStatisticsTask(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+}

+ 74 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserWatchStatisticsMapper.java

@@ -0,0 +1,74 @@
+package com.fs.course.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsUserWatchStatistics;
+
+import java.util.List;
+
+/**
+ * 会员看课统计-按营期统计Mapper接口
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+public interface FsUserWatchStatisticsMapper extends BaseMapper<FsUserWatchStatistics>{
+    /**
+     * 查询会员看课统计-按营期统计
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 会员看课统计-按营期统计
+     */
+    FsUserWatchStatistics selectFsUserWatchStatisticsById(Long id);
+
+    /**
+     * 查询会员看课统计-按营期统计列表
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 会员看课统计-按营期统计集合
+     */
+    List<FsUserWatchStatistics> selectFsUserWatchStatisticsList(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 新增会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    int insertFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 修改会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    int updateFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 删除会员看课统计-按营期统计
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 结果
+     */
+    int deleteFsUserWatchStatisticsById(Long id);
+
+    /**
+     * 批量删除会员看课统计-按营期统计
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserWatchStatisticsByIds(Long[] ids);
+
+    /**
+     * 获取看课统计-按营期统计
+     * @return
+     */
+    List<FsUserWatchStatistics> getCourseWatchStatistics();
+
+    /**
+     * 插入看课统计数据
+     */
+    void insertFsUserWatchStatisticsTask(FsUserWatchStatistics fsUserWatchStatistics);
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/course/param/FsCourseAnswerLogsParam.java

@@ -5,6 +5,7 @@ import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 
 import java.util.Date;
+import java.util.Set;
 
 @Data
 public class FsCourseAnswerLogsParam  extends BaseEntity  {
@@ -13,6 +14,10 @@ public class FsCourseAnswerLogsParam  extends BaseEntity  {
     private String courseId;
     private String videoId;
     private String companyUserName;
+    /**
+     * 销售id
+     */
+    private Long companyUserId;
     private Long companyId;
     private Long isRight;
     private Long watchLogId;
@@ -26,4 +31,16 @@ public class FsCourseAnswerLogsParam  extends BaseEntity  {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date sTime;
 
+    private Long pageNum;
+    private Long pageSize;
+
+    /**
+     * 用户(昵称_id组合)
+     */
+    private String userName;
+    /**
+     * 用户id
+     */
+    private Set<Long> userIds;
+
 }

+ 4 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseAnswerLogsService.java

@@ -62,4 +62,8 @@ public interface IFsCourseAnswerLogsService
      * @return 结果
      */
     public int deleteFsCourseAnswerLogsByLogId(Long logId);
+
+    public List<FsCourseAnswerLogsListVO> selectFsCourseAnswerLogsListVONew(FsCourseAnswerLogsParam param);
+
+    public Long selectFsCourseAnswerLogsListVONewCount(FsCourseAnswerLogsParam param);
 }

+ 75 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserWatchCourseStatisticsService.java

@@ -0,0 +1,75 @@
+package com.fs.course.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.course.domain.FsUserWatchCourseStatistics;
+
+import java.util.List;
+
+/**
+ * 会员看课统计-按课程统计Service接口
+ *
+ * @author fs
+ * @date 2025-06-17
+ */
+public interface IFsUserWatchCourseStatisticsService extends IService<FsUserWatchCourseStatistics>{
+    /**
+     * 查询会员看课统计-按课程统计
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 会员看课统计-按课程统计
+     */
+    FsUserWatchCourseStatistics selectFsUserWatchCourseStatisticsById(Long id);
+
+    /**
+     * 查询会员看课统计-按课程统计列表
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 会员看课统计-按课程统计集合
+     */
+    List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsList(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 查询会员看课明细汇总
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 会员看课统计-按课程统计集合
+     */
+    List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsListTotal(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 新增会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    int insertFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 修改会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    int updateFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics);
+
+    /**
+     * 批量删除会员看课统计-按课程统计
+     *
+     * @param ids 需要删除的会员看课统计-按课程统计主键集合
+     * @return 结果
+     */
+    int deleteFsUserWatchCourseStatisticsByIds(Long[] ids);
+
+    /**
+     * 删除会员看课统计-按课程统计信息
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 结果
+     */
+    int deleteFsUserWatchCourseStatisticsById(Long id);
+
+    /**
+     * 新增按营期课程统计会员看课表数据
+     */
+    void insertWatchCourseStatistics();
+}

+ 67 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserWatchStatisticsService.java

@@ -0,0 +1,67 @@
+package com.fs.course.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.course.domain.FsUserWatchStatistics;
+
+import java.util.List;
+
+/**
+ * 会员看课统计-按营期统计Service接口
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+public interface IFsUserWatchStatisticsService extends IService<FsUserWatchStatistics>{
+    /**
+     * 查询会员看课统计-按营期统计
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 会员看课统计-按营期统计
+     */
+    FsUserWatchStatistics selectFsUserWatchStatisticsById(Long id);
+
+    /**
+     * 查询会员看课统计-按营期统计列表
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 会员看课统计-按营期统计集合
+     */
+    List<FsUserWatchStatistics> selectFsUserWatchStatisticsList(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 新增会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    int insertFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 修改会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    int updateFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics);
+
+    /**
+     * 批量删除会员看课统计-按营期统计
+     *
+     * @param ids 需要删除的会员看课统计-按营期统计主键集合
+     * @return 结果
+     */
+    int deleteFsUserWatchStatisticsByIds(Long[] ids);
+
+    /**
+     * 删除会员看课统计-按营期统计信息
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 结果
+     */
+    int deleteFsUserWatchStatisticsById(Long id);
+
+    /**
+     * 新增按营期统计会员看课表数据
+     */
+    void insertStatistics();
+}

+ 92 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseAnswerLogsServiceImpl.java

@@ -1,16 +1,31 @@
 package com.fs.course.service.impl;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.DictUtils;
+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.course.domain.FsCourseAnswerLogs;
+import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseAnswerLogsMapper;
 import com.fs.course.param.FsCourseAnswerLogsParam;
 import com.fs.course.service.IFsCourseAnswerLogsService;
+import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
 import com.fs.course.vo.FsCourseAnswerLogsListVO;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.store.service.cache.IFsUserCacheService;
+import com.hc.openapi.tool.util.StringUtils;
+import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * 答题日志Service业务层处理
@@ -19,11 +34,22 @@ import java.util.List;
  * @date 2024-10-26
  */
 @Service
+@RequiredArgsConstructor
 public class FsCourseAnswerLogsServiceImpl implements IFsCourseAnswerLogsService
 {
+    private final IFsUserCourseVideoCacheService fsUserCourseVideoCacheService;
+
     @Autowired
     private FsCourseAnswerLogsMapper fsCourseAnswerLogsMapper;
 
+    private final IFsUserCacheService fsUserCacheService;
+
+    private final ICompanyCacheService companyCacheService;
+
+    private final ICompanyUserCacheService companyUserCacheService;
+
+    private final IFsUserService fsUserService;
+
     /**
      * 查询答题日志
      *
@@ -101,4 +127,70 @@ public class FsCourseAnswerLogsServiceImpl implements IFsCourseAnswerLogsService
     {
         return fsCourseAnswerLogsMapper.deleteFsCourseAnswerLogsByLogId(logId);
     }
+
+    @Override
+    public List<FsCourseAnswerLogsListVO> selectFsCourseAnswerLogsListVONew(FsCourseAnswerLogsParam param) {
+
+        if(StringUtils.isNotEmpty(param.getUserName())){
+            List<FsUser> fsUsers = fsUserService.selectFsUserListByJointUserNameKey(param.getUserName());
+            if(fsUsers.isEmpty()) {
+                return Collections.emptyList();
+            }
+            Set<Long> userIds = fsUsers.stream().map(FsUser::getUserId).collect(Collectors.toSet());
+            param.setUserIds(userIds);
+        }
+
+        List<FsCourseAnswerLogsListVO> data = fsCourseAnswerLogsMapper.selectFsCourseAnswerLogsListVONew(param);
+        for (FsCourseAnswerLogsListVO datum : data) {
+            FsUserCourseVideo fsUserCourseVideo = fsUserCourseVideoCacheService.selectFsUserCourseVideoByVideoId(datum.getVideoId());
+            if (fsUserCourseVideo != null) {
+                datum.setVideoName(fsUserCourseVideo.getTitle());
+                datum.setCourseId(fsUserCourseVideo.getCourseId());
+            }
+            // 用户
+            FsUser fsUser = fsUserCacheService.selectFsUserById(datum.getUserId());
+            if(fsUser != null) {
+                datum.setUserName(String.format("%s_%d",fsUser.getNickName(),fsUser.getUserId()));
+                datum.setFsAvatar(fsUser.getAvatar());
+            }
+
+            // 公司名称
+            if(datum.getCompanyId() != null) {
+                Company company = companyCacheService.selectCompanyById(datum.getCompanyId());
+                if(company != null) {
+                    datum.setCompanyName(String.format("%s_%d", company.getCompanyName(), company.getCompanyId()));
+                }
+            }
+            // 销售
+            if(datum.getCompanyUserId() != null) {
+                CompanyUser companyUser = companyUserCacheService.selectCompanyUserById(datum.getCompanyUserId());
+                if(companyUser != null) {
+                    datum.setCompanyUserName(String.format("%s_%d",companyUser.getUserName(),companyUser.getUserId()));
+                }
+            }
+            // 项目名
+            if(datum.getProject() != null) {
+                String sysCourseProject = DictUtils.getDictLabel("sys_course_project", datum.getProject());
+                if(StringUtils.isNotBlank(sysCourseProject)){
+                    datum.setProjectName(sysCourseProject);
+                }
+            }
+
+            // 是否全部正确
+            if(ObjectUtil.isNotNull(datum.getIsRight())){
+                String sysCompanyOr = DictUtils.getDictLabel("sys_company_or", String.valueOf(datum.getIsRight()));
+                if(StringUtils.isNotBlank(sysCompanyOr)){
+                    datum.setIsRightText(sysCompanyOr);
+                }
+            }
+        }
+
+        return data;
+    }
+
+    @Override
+    public Long selectFsCourseAnswerLogsListVONewCount(FsCourseAnswerLogsParam param) {
+        return fsCourseAnswerLogsMapper.selectFsCourseAnswerLogsListVONewCount(param);
+    }
+
 }

+ 19 - 1
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -9,6 +9,7 @@ import java.net.URL;
 import java.net.URLConnection;
 import java.util.*;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
@@ -43,7 +44,9 @@ import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.impl.AsyncUploadQwCourseImageService;
 import com.fs.qwApi.Result.QwUploadResult;
 import com.fs.qwApi.service.QwApiService;
+import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.vo.DictVO;
 import com.fs.voice.utils.StringUtil;
 import com.google.zxing.BarcodeFormat;
 import com.google.zxing.EncodeHintType;
@@ -83,6 +86,8 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     @Autowired
     private FsCourseWatchLogMapper fsCourseWatchLogMapper;
     @Autowired
+    private SysDictDataMapper sysDictDataMapper;
+    @Autowired
     private FsUserCourseCommentMapper fsUserCourseCommentMapper;
     @Autowired
     private FsUserCourseNoteMapper fsUserCourseNoteMapper;
@@ -279,7 +284,20 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
 
     @Override
     public List<FsUserCourseListPVO> selectFsUserCourseListPVO(FsUserCourse param) {
-        return fsUserCourseMapper.selectFsUserCourseListPVO(param);
+        List<DictVO> dictVOS = sysDictDataMapper.selectDictDataListByType("sys_course_project");
+        // Create a map for faster lookup (dictValue -> dictVO)
+        Map<String, String> projectMap = dictVOS.stream()
+                .collect(Collectors.toMap(DictVO::getDictValue, DictVO::getDictLabel));
+        List<FsUserCourseListPVO> list =  fsUserCourseMapper.selectFsUserCourseListPVO(param);
+        for (FsUserCourseListPVO vo : list) {
+            if (vo.getProject() != null) {
+                String projectName = projectMap.get(vo.getProject().toString());
+                if (projectName != null) {
+                    vo.setProjectName(projectName);
+                }
+            }
+        }
+        return list;
     }
 
     @Override

+ 242 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchCourseStatisticsServiceImpl.java

@@ -0,0 +1,242 @@
+package com.fs.course.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.course.domain.FsUserWatchCourseStatistics;
+import com.fs.course.mapper.FsUserCoursePeriodDaysMapper;
+import com.fs.course.mapper.FsUserWatchCourseStatisticsMapper;
+import com.fs.course.service.IFsUserWatchCourseStatisticsService;
+import com.fs.his.mapper.FsUserMapper;
+import com.google.common.collect.Lists;
+import lombok.AllArgsConstructor;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+
+/**
+ * 会员看课统计-按课程统计Service业务层处理
+ *
+ * @author fs
+ * @date 2025-06-17
+ */
+@Service
+@AllArgsConstructor
+public class FsUserWatchCourseStatisticsServiceImpl extends ServiceImpl<FsUserWatchCourseStatisticsMapper, FsUserWatchCourseStatistics> implements IFsUserWatchCourseStatisticsService {
+
+    private FsUserCoursePeriodDaysMapper fsUserCoursePeriodDaysMapper;
+
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private SqlSessionFactory sqlSessionFactory;
+
+    /**
+     * 查询会员看课统计-按课程统计
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 会员看课统计-按课程统计
+     */
+    @Override
+    public FsUserWatchCourseStatistics selectFsUserWatchCourseStatisticsById(Long id)
+    {
+        return baseMapper.selectFsUserWatchCourseStatisticsById(id);
+    }
+
+    /**
+     * 查询会员看课统计-按课程统计列表
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 会员看课统计-按课程统计
+     */
+    @Override
+    public List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsList(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        List<FsUserWatchCourseStatistics> list = baseMapper.selectFsUserWatchCourseStatisticsList(fsUserWatchCourseStatistics);
+        if(!list.isEmpty()){
+            for (FsUserWatchCourseStatistics userWatchCourseStatistics : list) {
+                userWatchCourseStatistics.setCompleteWatchRatePercent(userWatchCourseStatistics.getCompleteWatchRate() + "%");
+                userWatchCourseStatistics.setAnswerRightRatePercent(userWatchCourseStatistics.getAnswerRightRate() + "%");
+                userWatchCourseStatistics.setOnlineRatePercent(userWatchCourseStatistics.getOnlineRate() + "%");
+            }
+        }
+        return list;
+    }
+
+    @Override
+    public List<FsUserWatchCourseStatistics> selectFsUserWatchCourseStatisticsListTotal(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        return baseMapper.selectFsUserWatchCourseStatisticsListTotal(fsUserWatchCourseStatistics);
+    }
+
+    /**
+     * 新增会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        return baseMapper.insertFsUserWatchCourseStatistics(fsUserWatchCourseStatistics);
+    }
+
+    /**
+     * 修改会员看课统计-按课程统计
+     *
+     * @param fsUserWatchCourseStatistics 会员看课统计-按课程统计
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserWatchCourseStatistics(FsUserWatchCourseStatistics fsUserWatchCourseStatistics)
+    {
+        return baseMapper.updateFsUserWatchCourseStatistics(fsUserWatchCourseStatistics);
+    }
+
+    /**
+     * 批量删除会员看课统计-按课程统计
+     *
+     * @param ids 需要删除的会员看课统计-按课程统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserWatchCourseStatisticsByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsUserWatchCourseStatisticsByIds(ids);
+    }
+
+    /**
+     * 删除会员看课统计-按课程统计信息
+     *
+     * @param id 会员看课统计-按课程统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserWatchCourseStatisticsById(Long id)
+    {
+        return baseMapper.deleteFsUserWatchCourseStatisticsById(id);
+    }
+
+    @Override
+    public void insertWatchCourseStatistics() {
+        // 1、获取统计数据
+        // 查询课程相关数据
+        List<FsUserWatchCourseStatistics> fsUserWatchCourseStatistics = fsUserCoursePeriodDaysMapper.selectDaysCountList();
+
+        // 查询统计数据
+        List<FsUserWatchCourseStatistics> watchLog = fsUserMapper.selectWatchLogCount();
+        List<FsUserWatchCourseStatistics> redPacketLog = fsUserMapper.selectRedPacketLogCount();
+        List<FsUserWatchCourseStatistics> answerLog = fsUserMapper.selectAnswerLogCount();
+        List<FsUserWatchCourseStatistics> userTotal = fsUserMapper.selectFsUserDetail();
+
+        // 转化为自定义键的map
+        Map<String, FsUserWatchCourseStatistics> watchLogMap = watchLog.stream().collect(Collectors.toMap(k -> String.format("%s-%s-%s", k.getPeriodId(), k.getVideoId(), k.getCompanyUserId()), v -> v));
+        Map<String, FsUserWatchCourseStatistics> redPacketLogMap = redPacketLog.stream().collect(Collectors.toMap(k -> String.format("%s-%s-%s", k.getPeriodId(), k.getVideoId(), k.getCompanyUserId()), v -> v));
+        Map<String, FsUserWatchCourseStatistics> answerLogMap = answerLog.stream().collect(Collectors.toMap(k -> String.format("%s-%s-%s", k.getPeriodId(), k.getVideoId(), k.getCompanyUserId()), v -> v));
+        Map<Long, List<FsUserWatchCourseStatistics>> userTotalMap = userTotal.stream()
+                .collect(Collectors.groupingBy(FsUserWatchCourseStatistics::getCompanyUserId));
+
+        // 处理数据
+        List<FsUserWatchCourseStatistics> list = new ArrayList<>();
+        for (FsUserWatchCourseStatistics data : fsUserWatchCourseStatistics) {
+            FsUserWatchCourseStatistics vo = new FsUserWatchCourseStatistics();
+            String key = String.format("%s-%s-%s", data.getPeriodId(), data.getVideoId(), data.getCompanyUserId());
+            FsUserWatchCourseStatistics watchLogData = watchLogMap.get(key);
+            FsUserWatchCourseStatistics redPacketLogData = redPacketLogMap.get(key);
+            FsUserWatchCourseStatistics answerLogData = answerLogMap.get(key);
+            List<FsUserWatchCourseStatistics> userTotalDataList = userTotalMap.get(data.getCompanyUserId());
+            BeanUtils.copyProperties(data, vo);
+            // 改成使用营期线来表示营期开始时间
+            vo.setPeriodStartingTime(data.getPeriodLine() != null ? data.getPeriodLine() : data.getPeriodStartingTime());
+
+            // 单独一个一个set,不用copy,避免copy出来的结果被前面的覆盖
+            if(userTotalDataList != null && !userTotalDataList.isEmpty()){
+                // 获取过滤时间后的销售会员数量
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
+
+                int userNum = userTotalDataList.stream()
+                        .filter(v -> v.getUserCreateDate().before(data.getCourseStartDateTime())).mapToInt(FsUserWatchCourseStatistics::getUserNum).sum();
+                int newUserNum = userTotalDataList.stream()
+                        .filter(v -> sdf.format(v.getUserCreateDate()).equals(sdf.format(data.getCourseStartDateTime()))).mapToInt(FsUserWatchCourseStatistics::getUserNum).sum();
+                vo.setUserNum(userNum);
+                vo.setNewUserNum(newUserNum);
+            } else {
+                vo.setUserNum(0);
+                vo.setNewUserNum(0);
+            }
+
+            if(watchLogData != null) {
+                vo.setWatchNum(watchLogData.getWatchNum());
+                vo.setCompleteWatchNum(watchLogData.getCompleteWatchNum());
+                vo.setCompleteWatchRate(watchLogData.getCompleteWatchRate());
+            } else {
+                vo.setWatchNum(0);
+                vo.setCompleteWatchNum(0);
+                vo.setCompleteWatchRate(BigDecimal.ZERO);
+            }
+
+            if(redPacketLogData != null) {
+                vo.setRedPacketNum(redPacketLogData.getRedPacketNum());
+                vo.setRedPacketAmount(redPacketLogData.getRedPacketAmount());
+            } else {
+                vo.setRedPacketNum(0);
+                vo.setRedPacketAmount(BigDecimal.ZERO);
+            }
+
+            if(answerLogData != null) {
+                vo.setAnswerNum(answerLogData.getAnswerNum());
+                vo.setAnswerRightNum(answerLogData.getAnswerRightNum());
+                vo.setAnswerRightRate(answerLogData.getAnswerRightRate());
+            } else {
+                vo.setAnswerNum(0);
+                vo.setAnswerRightNum(0);
+                vo.setAnswerRightRate(BigDecimal.ZERO);
+            }
+
+            // 设置上线率
+            BigDecimal watchNum = new BigDecimal(vo.getWatchNum());
+            BigDecimal userNum = new BigDecimal(vo.getUserNum());
+            if(!userNum.equals(BigDecimal.ZERO)){
+                BigDecimal onlineRate = watchNum.divide(userNum, 4, RoundingMode.HALF_UP).multiply(new BigDecimal(100));
+                vo.setOnlineRate(onlineRate);
+            } else{
+                vo.setOnlineRate(BigDecimal.ZERO);
+            }
+
+            vo.setCreateTime(new Date());
+            vo.setUpdateTime(new Date());
+            list.add(vo);
+        }
+
+        //2、分批次插入数据
+        this.batchInsert(list);
+
+    }
+
+    private void batchInsert(List<FsUserWatchCourseStatistics> list) {
+        // 分批次处理,一次提交500条
+        List<List<FsUserWatchCourseStatistics>> batches = Lists.partition(list, 500);
+        batches.forEach(batch -> {
+            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
+            try {
+                FsUserWatchCourseStatisticsMapper mapper = sqlSession.getMapper(FsUserWatchCourseStatisticsMapper.class);
+                batch.forEach(mapper::insertFsUserWatchCourseStatisticsTask);
+                sqlSession.commit();
+            } finally {
+                sqlSession.close();
+            }
+        });
+    }
+
+}

+ 216 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserWatchStatisticsServiceImpl.java

@@ -0,0 +1,216 @@
+package com.fs.course.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.course.domain.FsUserCoursePeriod;
+import com.fs.course.domain.FsUserWatchStatistics;
+import com.fs.course.mapper.FsUserCoursePeriodMapper;
+import com.fs.course.mapper.FsUserWatchStatisticsMapper;
+import com.fs.course.service.IFsUserWatchStatisticsService;
+import com.fs.his.mapper.FsUserMapper;
+import com.google.common.collect.Lists;
+import lombok.AllArgsConstructor;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 会员看课统计-按营期统计Service业务层处理
+ *
+ * @author fs
+ * @date 2025-06-16
+ */
+@Service
+@AllArgsConstructor
+public class FsUserWatchStatisticsServiceImpl extends ServiceImpl<FsUserWatchStatisticsMapper, FsUserWatchStatistics> implements IFsUserWatchStatisticsService {
+
+    private final FsUserCoursePeriodMapper fsUserCoursePeriodMapper;
+
+    private final FsUserMapper fsUserMapper;
+
+    @Autowired
+    private SqlSessionFactory sqlSessionFactory;
+
+    private final CompanyMapper companyMapper;
+
+    /**
+     * 查询会员看课统计-按营期统计
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 会员看课统计-按营期统计
+     */
+    @Override
+    public FsUserWatchStatistics selectFsUserWatchStatisticsById(Long id)
+    {
+        return baseMapper.selectFsUserWatchStatisticsById(id);
+    }
+
+    /**
+     * 查询会员看课统计-按营期统计列表
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 会员看课统计-按营期统计
+     */
+    @Override
+    public List<FsUserWatchStatistics> selectFsUserWatchStatisticsList(FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        return baseMapper.selectFsUserWatchStatisticsList(fsUserWatchStatistics);
+    }
+
+    /**
+     * 新增会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        return baseMapper.insertFsUserWatchStatistics(fsUserWatchStatistics);
+    }
+
+    /**
+     * 修改会员看课统计-按营期统计
+     *
+     * @param fsUserWatchStatistics 会员看课统计-按营期统计
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserWatchStatistics(FsUserWatchStatistics fsUserWatchStatistics)
+    {
+        return baseMapper.updateFsUserWatchStatistics(fsUserWatchStatistics);
+    }
+
+    /**
+     * 批量删除会员看课统计-按营期统计
+     *
+     * @param ids 需要删除的会员看课统计-按营期统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserWatchStatisticsByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsUserWatchStatisticsByIds(ids);
+    }
+
+    /**
+     * 删除会员看课统计-按营期统计信息
+     *
+     * @param id 会员看课统计-按营期统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserWatchStatisticsById(Long id)
+    {
+        return baseMapper.deleteFsUserWatchStatisticsById(id);
+    }
+
+    @Override
+    @Transactional
+    public void insertStatistics() {
+        // 1、获取统计结果
+        // 获取所有的营期,并拆分公司id
+        FsUserCoursePeriod periodParam = new FsUserCoursePeriod();
+        List<FsUserCoursePeriod> fsUserCoursePeriods = fsUserCoursePeriodMapper.selectFsUserCoursePeriodList(periodParam);
+        Set<Long> companyIds = fsUserCoursePeriods.stream()
+                .flatMap(item -> Arrays.stream(item.getCompanyId().split(",")))
+                .map(Long::valueOf)
+                .collect(Collectors.toSet());
+
+        // 获取公司信息
+        List<Company> companies = companyMapper.selectCompanyByIds2(companyIds);
+        Map<Long, Company> companyMap = companies.stream().collect(Collectors.toMap(Company::getCompanyId, Function.identity()));
+
+        // 获取看课统计(按营期按公司)
+        List<FsUserWatchStatistics> courseWatchStatistics = baseMapper.getCourseWatchStatistics();
+        Map<String, FsUserWatchStatistics> courseWatchStatisticsMap = courseWatchStatistics.stream().collect(Collectors.toMap(k -> String.format("%s-%s", k.getPeriodId(), k.getCompanyId()), v -> v));
+
+        //获取公司的会员数量和今日新增会员数量
+        List<FsUserWatchStatistics> userTotal = fsUserMapper.selectFsUserTotal();
+        Map<String, FsUserWatchStatistics> userTotalMap = userTotal.stream().collect(Collectors.toMap(FsUserWatchStatistics::getCompanyId, Function.identity()));
+
+        List<FsUserWatchStatistics> list = fsUserCoursePeriods.stream()
+                .flatMap(item -> Arrays.stream(item.getCompanyId().split(","))
+                        .map(companyIdStr -> {
+                            Long companyId = Long.valueOf(companyIdStr.trim());
+                            Company company = companyMap.get(companyId);
+
+                            // 赋值
+                            FsUserWatchStatistics fsUserWatchStatistics = new FsUserWatchStatistics();
+                            BeanUtils.copyProperties(item, fsUserWatchStatistics);
+                            ZonedDateTime zonedDateTime = item.getPeriodStartingTime().atStartOfDay(ZoneId.systemDefault());
+                            // 改成使用营期线来表示营期开始时间
+                            fsUserWatchStatistics.setPeriodStartingTime(item.getPeriodLine() != null ? item.getPeriodLine() : Date.from(zonedDateTime.toInstant()));
+                            fsUserWatchStatistics.setCompanyId(companyIdStr.trim());
+                            fsUserWatchStatistics.setCompanyName(company != null ? company.getCompanyName() : null);
+
+                            FsUserWatchStatistics userTotalData = userTotalMap.get(fsUserWatchStatistics.getCompanyId());
+
+                            String key = String.format("%s-%s", fsUserWatchStatistics.getPeriodId(), fsUserWatchStatistics.getCompanyId());
+                            FsUserWatchStatistics watchData = courseWatchStatisticsMap.get(key);
+
+                            if(userTotalData != null){
+                                fsUserWatchStatistics.setUserNum(userTotalData.getUserNum());
+                                fsUserWatchStatistics.setNewUserNum(userTotalData.getNewUserNum());
+                            } else {
+                                fsUserWatchStatistics.setUserNum(0);
+                                fsUserWatchStatistics.setNewUserNum(0);
+                            }
+
+                            if(watchData != null){
+                                fsUserWatchStatistics.setWatchNum(watchData.getWatchNum());
+                                fsUserWatchStatistics.setCompleteWatchNum(watchData.getCompleteWatchNum());
+                                fsUserWatchStatistics.setCompleteWatchRate(watchData.getCompleteWatchRate());
+                            } else {
+                                fsUserWatchStatistics.setWatchNum(0);
+                                fsUserWatchStatistics.setCompleteWatchNum(0);
+                                fsUserWatchStatistics.setCompleteWatchRate(BigDecimal.ZERO);
+                            }
+
+                            // 计算上线率
+                            BigDecimal watchNum = new BigDecimal(fsUserWatchStatistics.getWatchNum());
+                            BigDecimal userNum = new BigDecimal(fsUserWatchStatistics.getUserNum());
+                            if(!userNum.equals(BigDecimal.ZERO)){
+                                BigDecimal onlineRate = watchNum.divide(userNum, 2, RoundingMode.HALF_UP).multiply(new BigDecimal(100));
+                                fsUserWatchStatistics.setOnlineRate(onlineRate);
+                            } else {
+                                fsUserWatchStatistics.setOnlineRate(BigDecimal.ZERO);
+                            }
+
+                            fsUserWatchStatistics.setCreateTime(new Date());
+                            fsUserWatchStatistics.setUpdateTime(new Date());
+                            return fsUserWatchStatistics;
+                        })).collect(Collectors.toList());
+
+        //2、分批次插入数据
+        this.batchInsert(list);
+    }
+
+    private void batchInsert(List<FsUserWatchStatistics> list) {
+        // 分批次处理,一次提交500条
+        List<List<FsUserWatchStatistics>> batches = Lists.partition(list, 500);
+        batches.forEach(batch -> {
+            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
+            try {
+                FsUserWatchStatisticsMapper mapper = sqlSession.getMapper(FsUserWatchStatisticsMapper.class);
+                batch.forEach(mapper::insertFsUserWatchStatisticsTask);
+                sqlSession.commit();
+            } finally {
+                sqlSession.close();
+            }
+        });
+    }
+}

+ 46 - 32
fs-service/src/main/java/com/fs/course/vo/FsCourseAnswerLogsListVO.java

@@ -3,6 +3,8 @@ package com.fs.course.vo;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
 
+import java.util.Date;
+
 @Data
 public class FsCourseAnswerLogsListVO {
 
@@ -10,71 +12,83 @@ public class FsCourseAnswerLogsListVO {
     private Long logId;
 
     /**
-    * 小程序id
-    */
+     * 小程序id
+     */
     private Long userId;
     /**
-    * 小程序用户名
-    */
-    @Excel(name = "小程序用户")
+     * 小程序用户名
+     */
+    @Excel(name = "用户", sort = 1)
     private String userName;
     private String fsAvatar;
     /**
-    * 课程id
-    */
+     * 课程id
+     */
     private Long courseId;
     /**
-    * 课程名称
-    */
+     * 课程名称
+     */
     @Excel(name = "课程名称")
     private String courseName;
     /**
-    * 视频id
-    */
+     * 视频id
+     */
     private Long videoId;
     /**
-    * 视频名称
-    */
-    @Excel(name = "视频名称")
+     * 视频名称
+     */
+    @Excel(name = "小节名称",width = 70)
     private String videoName;
     /**
-    * 是否全部正确 0否 1是
-    */
-    @Excel(name = "是否全部正确 0否 1是")
+     * 是否全部正确 0否 1是
+     */
     private Integer isRight;
+    @Excel(name = "是否全部正确")
+    private String isRightText;
     /**
-    * 创建时间
-    */
+     * 创建时间
+     */
     @Excel(name = "创建时间")
     private String createTime;
     /**
-    * 销售id
-    */
+     * 销售id
+     */
     private Long companyUserId;
     /**
-    * 销售名称
-    */
+     * 销售名称
+     */
     @Excel(name = "销售名称")
     private String companyUserName;
     /**
-    * 公司id
-    */
+     * 公司id
+     */
     private Long companyId;
     /**
-    * 公司名称
-    */
+     * 公司名称
+     */
     @Excel(name = "公司名称")
     private String companyName;
     /**
-    * 企业微信员工id
-    */
+     * 企业微信员工id
+     */
     private Long qwUserId;
     /**
-    * 企业微信员工名称
-    */
-    @Excel(name = "企业微信员工名称")
+     * 企业微信员工名称
+     */
     private String qwUserName;
     private String answer;
     private String questionTitle;
     private String questionJson;
+    /**
+     * 项目
+     */
+    private String project;
+    /**
+     * 项目名称
+     */
+    @Excel(name = "项目", sort = 2)
+    private String projectName;
+
+    private Date sTime;
+    private Date eTime;
 }

+ 8 - 2
fs-service/src/main/java/com/fs/course/vo/FsUserCourseListPVO.java

@@ -44,6 +44,12 @@ public class FsUserCourseListPVO extends BaseEntity
     private Integer isShow;
 
     private Integer isPrivate;
-
-
+    /**
+     * 项目ID
+     */
+    private Long project;
+    /**
+     * 项目名称
+     */
+    private String projectName;
 }

+ 101 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserWatchCourseStatisticsExportVO.java

@@ -0,0 +1,101 @@
+package com.fs.course.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserWatchCourseStatisticsExportVO extends BaseEntity{
+
+    /** 主键id */
+    private Long id;
+
+    /** 营期id */
+//    @Excel(name = "营期id")
+    private Long periodId;
+
+    /** 营期名称 */
+    @Excel(name = "营期名称")
+    private String periodName;
+
+    /** 营期开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "营期线", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date periodStartingTime;
+
+    /** 课程开始日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "播出时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date courseStartDateTime;
+
+    /** 课程id */
+//    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 课程名称 */
+    @Excel(name = "课程名称")
+    private String courseName;
+
+    /** 视频id */
+//    @Excel(name = "视频id")
+    private Long videoId;
+
+    /** 视频标题 */
+    @Excel(name = "视频小节")
+    private String videoTitle;
+
+    /** 销售公司id */
+//    @Excel(name = "销售公司id")
+    private Long companyId;
+
+    /** 销售公司名称 */
+    @Excel(name = "销售公司")
+    private String companyName;
+
+    /** 销售id */
+//    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 销售名称 */
+//    @Excel(name = "所属销售")
+    private String companyUserName;
+
+    /** 新增会员数量 */
+    @Excel(name = "新增会员")
+    private Integer newUserNum;
+
+    /** 会员数量 */
+    @Excel(name = "会员数量")
+    private Integer userNum;
+
+    /** 观看人数 */
+    @Excel(name = "观看人数")
+    private Integer watchNum;
+
+    /** 上线率+% */
+    @Excel(name = "上线率")
+    private String onlineRatePercent;
+
+    /** 完播人数 */
+    @Excel(name = "完播人数")
+    private Integer completeWatchNum;
+
+    /** 完播率 */
+//    @Excel(name = "完播率")
+    private BigDecimal completeWatchRate;
+
+    /** 完播率+% */
+    @Excel(name = "完播率")
+    private String completeWatchRatePercent;
+
+    /** 红包领取数量 */
+    @Excel(name = "红包领取个数")
+    private Integer redPacketNum;
+
+}

+ 1 - 1
fs-service/src/main/java/com/fs/his/domain/FsUser.java

@@ -31,7 +31,7 @@ public class FsUser extends BaseEntity
     private String idCard;
 
     /** 用户昵称 */
-    @Excel(name = "用户昵称")
+    @Excel(name = "会员昵称", sort = 1)
     private String nickName;
 
     /** 用户头像 */

+ 96 - 0
fs-service/src/main/java/com/fs/his/domain/FsUserOnlineState.java

@@ -0,0 +1,96 @@
+package com.fs.his.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 用户上线情况对象 fs_user_online_state
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserOnlineState extends BaseEntity{
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /** 用户id */
+    private Long userId;
+
+    /** 用户昵称 */
+    @Excel(name = "用户昵称")
+    private String nickname;
+
+    /** 用户头像 */
+    @Excel(name = "用户头像")
+    private String avatar;
+
+    /** 手机号码 */
+    @Excel(name = "手机号码")
+    private String phone;
+
+    /** 微信小程序OPENID */
+    @Excel(name = "微信小程序OPENID")
+    private String maOpenId;
+
+    /** 微信公众号OPENID */
+    @Excel(name = "微信公众号OPENID")
+    private String mpOpenId;
+
+    /** 关联ID */
+    @Excel(name = "关联ID")
+    private String unionId;
+
+    /** 用户状态,1-正常,0-禁止 */
+    @Excel(name = "用户状态,1-正常,0-禁止")
+    private Integer status;
+
+    /** 公司id */
+    @Excel(name = "公司id")
+    private Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "所属公司")
+    private String companyName;
+
+    /** 销售id */
+    @Excel(name = "销售id")
+    private Long companyUserId;
+
+    /** 销售名称 */
+    @Excel(name = "所属销售")
+    private String companyUserName;
+
+    /** 上线状态,1-已上线;2-未上线 */
+    @Excel(name = "上线状态,1-已上线;2-未上线")
+    private Integer onlineStatus;
+
+    /** 上线时间(第一次看课的时间) */
+    @Excel(name = "上线时间(第一次看课的时间)")
+    private Date onlineTime;
+
+    /** 看课数量 */
+    @Excel(name = "看课数量")
+    private Integer watchCourseCount;
+
+    /** 参与营期数量 */
+    @Excel(name = "参与营期数量")
+    private Integer partCourseCount;
+
+    /** 最后一次看课时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "最后一次看课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date lastWatchDate;
+
+    /** 更新时间 */
+    @Excel(name = "更新时间", readConverterExp = "更新时间")
+    private Date updateTime;
+
+}

+ 24 - 1
fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java

@@ -3,12 +3,14 @@ package com.fs.his.mapper;
 import java.util.List;
 import java.util.Map;
 
+import com.fs.course.domain.FsUserWatchCourseStatistics;
+import com.fs.course.domain.FsUserWatchStatistics;
 import com.fs.course.param.CourseAnalysisParam;
 import com.fs.course.vo.newfs.FsCourseAnalysisCountVO;
 import com.fs.his.domain.FsUser;
 import com.fs.his.param.FsUserParam;
-import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.FsUserVO;
+import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.param.QwFsUserParam;
@@ -43,6 +45,8 @@ public interface FsUserMapper
      */
     public List<FsUser> selectFsUserList(FsUser fsUser);
 
+    public List<FsUserVO> selectFsUserVOList(@Param("maps") FsUser fsUser);
+
     /**
      * 新增用户
      *
@@ -284,6 +288,8 @@ public interface FsUserMapper
 
     int batchUpdateUserCompanyUser(@Param("userIds") List<Long> userIds, @Param("companyUserId") Long companyUserId, @Param("companyId") Long companyId);
 
+    int batchUpdateCompanyUserRelation(@Param("userIds") List<Long> userIds, @Param("companyUserId") Long companyUserId, @Param("companyId") Long companyId);
+
     /**
      * 查询会员选项列表
      * @param params    参数
@@ -357,4 +363,21 @@ public interface FsUserMapper
     FsUserAndCompanyAndDoctorVo selectCompanyAndDoctor(@Param("userId")Long userId);
 
     List<FsUser> selectUserNameByIds(@Param("userIds") String userIds);
+
+    /**
+     * 获取会员总数和新增总数-可以按营期/课程
+     * @return
+     */
+    List<FsUserWatchStatistics> selectFsUserTotal();
+
+    List<FsUserWatchCourseStatistics> selectWatchLogCount();
+
+    List<FsUserWatchCourseStatistics> selectRedPacketLogCount();
+
+    List<FsUserWatchCourseStatistics> selectAnswerLogCount();
+
+    List<FsUserWatchCourseStatistics> selectFsUserDetail();
+
+    List<FsUser> selectFsUserListByJointUserNameKey(String userNameKey);
+
 }

+ 76 - 0
fs-service/src/main/java/com/fs/his/mapper/FsUserOnlineStateMapper.java

@@ -0,0 +1,76 @@
+package com.fs.his.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserOnlineState;
+
+import java.util.List;
+
+/**
+ * 用户上线情况Mapper接口
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+public interface FsUserOnlineStateMapper extends BaseMapper<FsUserOnlineState>{
+    /**
+     * 查询用户上线情况
+     *
+     * @param userId 用户上线情况主键
+     * @return 用户上线情况
+     */
+    FsUserOnlineState selectFsUserOnlineStateById(Long userId);
+
+    /**
+     * 查询用户上线情况列表
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 用户上线情况集合
+     */
+    List<FsUserOnlineState> selectFsUserOnlineStateList(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 新增用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    int insertFsUserOnlineState(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 修改用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    int updateFsUserOnlineState(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 删除用户上线情况
+     *
+     * @param userId 用户上线情况主键
+     * @return 结果
+     */
+    int deleteFsUserOnlineStateById(Long userId);
+
+    /**
+     * 批量删除用户上线情况
+     *
+     * @param userIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserOnlineStateByIds(Long[] userIds);
+
+    /**
+     * 查询没有看课记录的用户
+     * @return
+     */
+    List<FsUserOnlineState> selectUserNotOnline();
+
+    /**
+     * 查询短时间已经有且仅有一条看课记录的用户(查询的时间需要大于等于定时任务的时间)
+     * @return
+     */
+    List<FsUser> selectExistWatchLogUser();
+
+}

+ 68 - 0
fs-service/src/main/java/com/fs/his/service/IFsUserOnlineStateService.java

@@ -0,0 +1,68 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.his.domain.FsUserOnlineState;
+
+import java.util.List;
+
+/**
+ * 用户上线情况Service接口
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+public interface IFsUserOnlineStateService extends IService<FsUserOnlineState>{
+    /**
+     * 查询用户上线情况
+     *
+     * @param userId 用户上线情况主键
+     * @return 用户上线情况
+     */
+    FsUserOnlineState selectFsUserOnlineStateById(Long userId);
+
+    /**
+     * 查询用户上线情况列表
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 用户上线情况集合
+     */
+    List<FsUserOnlineState> selectFsUserOnlineStateList(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 新增用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    int insertFsUserOnlineState(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 修改用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    int updateFsUserOnlineState(FsUserOnlineState fsUserOnlineState);
+
+    /**
+     * 批量删除用户上线情况
+     *
+     * @param userIds 需要删除的用户上线情况主键集合
+     * @return 结果
+     */
+    int deleteFsUserOnlineStateByIds(Long[] userIds);
+
+    /**
+     * 删除用户上线情况信息
+     *
+     * @param userId 用户上线情况主键
+     * @return 结果
+     */
+    int deleteFsUserOnlineStateById(Long userId);
+
+    /**
+     * 统计未上线的用户(定时更新上线状态)
+     */
+    void insertUserNotOnline();
+
+}

+ 10 - 1
fs-service/src/main/java/com/fs/his/service/IFsUserService.java

@@ -13,9 +13,9 @@ import com.fs.course.vo.newfs.FsCourseAnalysisVO;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserAddress;
 import com.fs.his.param.FsUserParam;
+import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.FsUserFollowDoctorVO;
-import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.UserVo;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.param.QwFsUserParam;
@@ -185,4 +185,13 @@ public interface IFsUserService
     FsUserAndCompanyAndDoctorVo selectCompanyAndDoctor(Long userId);
 
     List<FsUser> selectUserByIds(String userIds);
+
+
+    /**
+     * 根据组合key(用户昵称+id)查询用户信息
+     * @param userNameKey (用户昵称+id)
+     * @return
+     */
+    List<FsUser> selectFsUserListByJointUserNameKey(String userNameKey);
+
 }

+ 136 - 0
fs-service/src/main/java/com/fs/his/service/impl/FsUserOnlineStateServiceImpl.java

@@ -0,0 +1,136 @@
+package com.fs.his.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserOnlineState;
+import com.fs.his.mapper.FsUserOnlineStateMapper;
+import com.fs.his.service.IFsUserOnlineStateService;
+import com.google.common.collect.Lists;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 用户上线情况Service业务层处理
+ *
+ * @author fs
+ * @date 2025-06-30
+ */
+@Service
+public class FsUserOnlineStateServiceImpl extends ServiceImpl<FsUserOnlineStateMapper, FsUserOnlineState> implements IFsUserOnlineStateService {
+
+    @Autowired
+    private SqlSessionFactory sqlSessionFactory;
+
+    /**
+     * 查询用户上线情况
+     *
+     * @param userId 用户上线情况主键
+     * @return 用户上线情况
+     */
+    @Override
+    public FsUserOnlineState selectFsUserOnlineStateById(Long userId)
+    {
+        return baseMapper.selectFsUserOnlineStateById(userId);
+    }
+
+    /**
+     * 查询用户上线情况列表
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 用户上线情况
+     */
+    @Override
+    public List<FsUserOnlineState> selectFsUserOnlineStateList(FsUserOnlineState fsUserOnlineState)
+    {
+        return baseMapper.selectFsUserOnlineStateList(fsUserOnlineState);
+    }
+
+    /**
+     * 新增用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserOnlineState(FsUserOnlineState fsUserOnlineState)
+    {
+        fsUserOnlineState.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsUserOnlineState(fsUserOnlineState);
+    }
+
+    /**
+     * 修改用户上线情况
+     *
+     * @param fsUserOnlineState 用户上线情况
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserOnlineState(FsUserOnlineState fsUserOnlineState)
+    {
+        return baseMapper.updateFsUserOnlineState(fsUserOnlineState);
+    }
+
+    /**
+     * 批量删除用户上线情况
+     *
+     * @param userIds 需要删除的用户上线情况主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserOnlineStateByIds(Long[] userIds)
+    {
+        return baseMapper.deleteFsUserOnlineStateByIds(userIds);
+    }
+
+    /**
+     * 删除用户上线情况信息
+     *
+     * @param userId 用户上线情况主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserOnlineStateById(Long userId)
+    {
+        return baseMapper.deleteFsUserOnlineStateById(userId);
+    }
+
+    @Override
+    public void insertUserNotOnline() {
+        // 1、获取需要新增/更新的数据
+        // 获取当前所有没有看课记录的用户
+        List<FsUserOnlineState> fsUserOnlineStates = baseMapper.selectUserNotOnline();
+
+        // 2、分批次新增
+        this.batchInsertOnline(fsUserOnlineStates);
+
+        // 3、移除有看课记录的用户
+        List<FsUser> fsUsers = baseMapper.selectExistWatchLogUser();
+        List<Long> moveUserIds = fsUsers.stream().map(FsUser::getUserId).collect(Collectors.toList());
+        if(!moveUserIds.isEmpty()){
+            baseMapper.deleteFsUserOnlineStateByIds(moveUserIds.toArray(new Long[0]));
+        }
+
+    }
+
+    private void batchInsertOnline(List<FsUserOnlineState> list) {
+        // 分批次处理,一次提交500条
+        List<List<FsUserOnlineState>> batches = Lists.partition(list, 500);
+        batches.forEach(batch -> {
+            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
+            try {
+                FsUserOnlineStateMapper mapper = sqlSession.getMapper(FsUserOnlineStateMapper.class);
+                batch.forEach(mapper::insertFsUserOnlineState);
+                sqlSession.commit();
+            } finally {
+                sqlSession.close();
+            }
+        });
+    }
+}

+ 33 - 8
fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java

@@ -45,18 +45,20 @@ import com.fs.his.param.FsUserAddIntegralTemplateParam;
 import com.fs.his.param.FsUserParam;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserProjectTagService;
+import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.FsUserExportListVO;
 import com.fs.his.vo.FsUserFollowDoctorVO;
-import com.fs.his.vo.FsUserVO;
 import com.fs.his.vo.UserVo;
 import com.fs.qw.cache.IQwExternalContactCacheService;
 import com.fs.qw.dto.FsUserTransferParamDTO;
 import com.fs.qw.param.QwFsUserParam;
 import com.fs.qw.vo.QwFsUserVO;
 import com.fs.store.domain.FsUserCourseCount;
+import com.fs.store.mapper.FsUserCourseCountMapper;
 import com.fs.store.param.h5.FsUserPageListParam;
 import com.fs.store.param.h5.UserStatisticsCommonParam;
 import com.fs.store.service.cache.IFsUserCourseCountCacheService;
+import com.fs.store.vo.FsUserLastCount;
 import com.fs.store.vo.h5.*;
 import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
@@ -134,6 +136,9 @@ public class FsUserServiceImpl implements IFsUserService
     @Autowired
     private IFsUserProjectTagService userProjectTagService;
 
+    @Autowired
+    private FsUserCourseCountMapper fsUserCourseCountMapper;
+
 
     /**
      * 查询用户
@@ -568,11 +573,25 @@ public class FsUserServiceImpl implements IFsUserService
         if(companyUserId != null) {
             Long companyUser = Long.parseLong(companyUserId);
             Set<Long> userIds = companyUserCacheService.selectUserAllCompanyUserId(companyUser);
+            if (userIds != null || userIds.size() <= 1) {
+                if (param.getIsAdmin()) {
+                    List<CompanyUser> companyUsers = companyUserMapper.selectCompanyUserByCompanyId(param.getCompanyId());
+                    userIds = companyUsers.stream().map(CompanyUser::getUserId).collect(Collectors.toSet());
+                }
+            }
             param.setCompanyUserIds(userIds);
         }
 
         List<FsUserPageListVO> fsUserPageListVOS = fsUserMapper.selectFsUserPageListNew(param);
         Map<Long, CompanyTag> tagMap = companyTagCacheService.queryAllTagMap();
+        //获取会员的最新的看课状态和最后看课时间
+        Set<Long> userIds = fsUserPageListVOS.stream().map(FsUserPageListVO::getUserId).collect(Collectors.toSet());
+        List<FsUserLastCount> fsUserCourseCounts = Collections.emptyList();
+        if(!userIds.isEmpty()){
+            fsUserCourseCounts = fsUserCourseCountMapper.selectUserLastCount(userIds);
+        }
+        Map<Long, FsUserLastCount> countMap = fsUserCourseCounts.stream().collect(Collectors.toMap(FsUserLastCount::getUserId, Function.identity()));
+
         for (FsUserPageListVO item : fsUserPageListVOS) {
             if(item.getCompanyUserId() != null) {
                 String companyUserName = companyUserCacheService.selectCompanyUserNameUserById(item.getCompanyUserId());
@@ -596,18 +615,19 @@ public class FsUserServiceImpl implements IFsUserService
                     item.setMissCourseCount(byUserId.getMissCourseCount());
                     item.setMissCourseStatus(byUserId.getMissCourseStatus());
                     if(StringUtils.isNotEmpty(byUserId.getPartCourseCount())){
-                        item.setPartCourseCount(Long.valueOf(byUserId.getPartCourseCount()));
+                        item.setPartCourseCount(new BigDecimal(byUserId.getPartCourseCount()).longValue());
                     }
-                    item.setCourseCountStatus(byUserId.getStatus());
+//                    item.setCourseCountStatus(byUserId.getStatus());
                     item.setStopWatchDays(byUserId.getStopWatchDays());
                     item.setCompleteWatchDate(byUserId.getCompleteWatchDate());
+                    item.setLastWatchDate(byUserId.getLastWatchDate());
                 }
-                String userTagByUserId = null;
-                if (item.getUserId()!=null && item.getCompanyUserId() != null){
-                    userTagByUserId = companyTagCacheService
-                            .findUserTagByUserId(item.getUserId(),item.getCompanyUserId());
+                FsUserLastCount fsUserCourseCount = countMap.get(item.getUserId());
+                if(fsUserCourseCount != null){
+                    item.setCourseCountStatus(fsUserCourseCount.getStatus());
                 }
-
+                String userTagByUserId = companyTagCacheService
+                        .findUserTagByUserId(item.getUserId(),item.getCompanyUserId());
                 if(StringUtils.isNotEmpty(userTagByUserId)) {
                     String[] split = userTagByUserId.split(",");
                     Set<String> tagNames = new HashSet<>();
@@ -1026,6 +1046,11 @@ public class FsUserServiceImpl implements IFsUserService
         return fsUserMapper.selectUserNameByIds(userIds);
     }
 
+    @Override
+    public List<FsUser> selectFsUserListByJointUserNameKey(String userNameKey) {
+        return fsUserMapper.selectFsUserListByJointUserNameKey(userNameKey);
+    }
+
 
     private FsUserStatisticsVO getUserStatistics(UserStatisticsCommonParam param) {
         FsUserStatisticsVO fsUserStatisticsVO = new FsUserStatisticsVO();

+ 7 - 94
fs-service/src/main/java/com/fs/his/vo/FsUserVO.java

@@ -3,112 +3,25 @@ package com.fs.his.vo;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
+import com.fs.his.domain.FsUser;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
 import java.io.Serializable;
 import java.math.BigDecimal;
-import java.util.Base64;
 import java.util.Date;
 
 @Data
-public class FsUserVO implements Serializable {
-    /** 用户id */
-    private String userId;
-
-    /** 用户昵称 */
-    @Excel(name = "用户昵称")
-    private String nickName;
-
-    /** 用户头像 */
-    @Excel(name = "用户头像")
-    private String avatar;
-
-    /** 手机号码 */
-    @Excel(name = "手机号码")
-    private String phone;
-
-    /** 用户积分 */
-    @Excel(name = "用户积分")
-    private BigDecimal integral;
-
-    /** 1为正常,0为禁止 */
-    @Excel(name = "1为正常,0为禁止")
-    private Integer status;
-
-    /** 推广上级用户ID */
-//    @Excel(name = "推广上级用户ID")
-    private String tuiUserId;
-    /** 推广上级用户ID */
-    @Excel(name = "推广上级用户名称")
-    private String tuiName;
-    /** 推广上级用户ID */
-    @Excel(name = "推广上级电话")
-    private String tuiPhone;
-
-
-
-    /** 推广员关联时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd")
-    @Excel(name = "推广员关联时间", width = 30, dateFormat = "yyyy-MM-dd")
-    private Date tuiTime;
-
-    /** 下级人数 */
-    @Excel(name = "下级人数")
-    private Long tuiUserCount;
-
-    /** 微信小程序OPENID */
-//    @Excel(name = "微信小程序OPENID")
-    private String maOpenId;
-
-    /** 微信公众号OPENID */
-//    @Excel(name = "微信公众号OPENID")
-    private String mpOpenId;
-
-    /** 关联ID */
-//    @Excel(name = "关联ID")
-    private String unionId;
-
-    /** $column.columnComment */
-//    @Excel(name = "关联ID")
-    private Integer isDel;
-
-    /** 邀请码 */
-    @Excel(name = "邀请码")
-    private String userCode;
-
-    /** 最后一次登录ip */
-    @Excel(name = "最后一次登录ip")
-    private String lastIp;
-
-    /** 余额 */
-    @Excel(name = "余额")
-    private BigDecimal balance;
-
-    /** 创建时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private Date createTime;
-
-    /** 更新时间 */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private Date updateTime;
-
-    private Integer isBuy;
-
-    private String loginDevice;//当前登录设备
-
-    private String source;//app来源
-
-
+public class FsUserVO extends FsUser implements Serializable
+{
     @ApiModelProperty(value = "销售名称")
     @Excel(name = "所属销售", sort = 10)
     private String companyUserNickName;
 
-    @ApiModelProperty(value = "所属公司")
-    @Excel(name = "所属公司", sort = 9)
-    private String companyName;
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "会员注册时间", dateFormat = "yyyy-MM-dd HH:mm:ss" , sort = 6)
+    private Date createTime;
 
     private String username;
     /** 用户密码(跟pwd) */

+ 8 - 0
fs-service/src/main/java/com/fs/store/mapper/FsUserCourseCountMapper.java

@@ -1,9 +1,11 @@
 package com.fs.store.mapper;
 
 import com.fs.store.domain.FsUserCourseCount;
+import com.fs.store.vo.FsUserLastCount;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
+import java.util.Set;
 
 /**
  * 用户看课统计Mapper接口
@@ -80,4 +82,10 @@ public interface FsUserCourseCountMapper
      */
     void insertFsUserCourseCountTask(FsUserCourseCount fsUserCourseCount);
 
+    /**
+     * 查询会员最新的看课状态和心跳时间
+     * @return
+     */
+    List<FsUserLastCount> selectUserLastCount(@Param("userIds") Set<Long> userIds);
+
 }

+ 36 - 0
fs-service/src/main/java/com/fs/store/vo/FsUserLastCount.java

@@ -0,0 +1,36 @@
+package com.fs.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 用户看课统计对象 fs_user_course_count
+ *
+ * @author fs
+ * @date 2025-04-02
+ */
+@Data
+public class FsUserLastCount {
+    /**
+     * 用户id
+     */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /**
+     * 最后一次看课时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "最后一次看课时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date lastWatchDate;
+
+    /**
+     * 用户状态,1-正常;2-停止;3-未看
+     */
+    @Excel(name = "用户状态,1-正常;2-停止;3-未看")
+    private Long status;
+
+}

+ 27 - 15
fs-service/src/main/resources/application-config-druid-fby.yml

@@ -10,31 +10,43 @@ logging:
 wx:
   miniapp:
     configs:
-      - appid: wx4115995705bb0ea0   #中康智慧
-        secret: 58910ae743005c396012b029c7def579
+      - appid: wx11a2ce7c2bbc4521    #倍力优会员商城
+        secret: d680dc8ff20258b158c9355f8b7769ae
         token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
         aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
         msgDataFormat: JSON
-      - appid: wxedde588767b358b1   #中康未来智慧药房
-        secret: 928d2961c81610d8f64b019597212fcd
+
+      ##  云联融智优选小程序,暂时使用
+      #      - appid: wxd70f99287830cb51   #云联融智优选(暂时用于测试销售app)
+      #        secret: 6e2684b3d48e6363018d4eedb8dae3e5
+      #        token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
+      #        aesKey:
+      #        msgDataFormat: JSON
+
+      - appid: wxb9b453d37c5fad45   #福本源小程序
+        secret: 45ee94e8c48edbafdcca2131a5e9d48d
         token: Ncbnd7lJvkripVOpyTFAna6NAWCxCrvC
-        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        aesKey:
         msgDataFormat: JSON
   cp:
-    corpId: wwb2a1055fb6c9a7c2
+    corpId:
     appConfigs:
-      - agentId: 1000005
-        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
+      - agentId: 1000002
+        secret: bhj3402rPCT0YGcosffyTO3eUMs1G2MFHMspXVBNf-c
         token: PPKOdAlCoMO
         aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
   pay:
-    appId: wx73f85f8d62769119 #微信公众号或者小程序等的appid
-    mchId: 1611402045 #微信支付商户号
-    mchKey: 8cab128997a3547c1363b0898b877f38 #微信支付商户密钥
+    appId: wx961fadab9bcb792b #微信公众号或者小程序等的appid
+    mchId: 1716217886 #微信支付商户号
+    mchKey: a7Fc5B9dE2h8J3kL4mN6pQ7rS9tU2vW1 #微信支付商户密钥
     subAppId:  #服务商模式下的子商户公众账号ID
     subMchId:  #服务商模式下的子商户号
-    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
-    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
+    keyPath: c:\\Tools\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl: https://userapp.bly.ylrztop.com/app/wxpay/wxPayNotify
+    privateKeyPath: 'C:\cert\1716217886_20250509_cert\apiclient_key.pem'
+    privateCertPath: 'C:\cert\1716217886_20250509_cert\apiclient_cert.pem'
+    certSerialNo: '4E8BD68BC2BFD37CA58244D660E5FDCCE475D82E'
+    nativeNotifyUrl: 'http://company.fbylive.com/prod-api/pay/wxPay/payNotify'
   mp:
     useRedis: false
     redisConfig:
@@ -42,8 +54,8 @@ wx:
       port: 6379
       timeout: 2000
     configs:
-      - appId: wx5d3096e20e4bd8ba # 第一个公众号的appid  //公众号名称:成都九州在线互联网医院
-        secret: 1afa05f0c71beff0d52fb849c62e479a # 公众号的appsecret
+      - appId: wx961fadab9bcb792b # 第一个公众号的appid  //公众号名称:云联
+        secret: 8adb2a7533921449ef6e60814c2ff075
         token: PPKOdAlCoMO # 接口配置里的Token值
         aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
 aifabu:  #爱链接

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

@@ -199,4 +199,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <!-- 批量查询企业ID: ${ids} -->
     </select>
 
+    <select id="selectCompanyByIds2" parameterType="Long" resultType="Company">
+        <include refid="selectCompanyVo"/>
+        where company_id in
+        <foreach item="companyId" collection="companyIds" open="(" separator="," close=")">
+            #{companyId}
+        </foreach>
+    </select>
+
 </mapper>

+ 16 - 9
fs-service/src/main/resources/mapper/company/CompanyUserMapper.xml

@@ -41,6 +41,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="domain"    column="domain"    />
         <result property="isAudit"    column="is_audit"    />
         <result property="isNeedRegisterMember"    column="is_need_register_member"    />
+        <result property="isAllowedAllRegister"    column="is_allowed_all_register"    />
         <association property="dept"    column="dept_id" javaType="CompanyDept" resultMap="deptResult" />
         <collection  property="roles"   javaType="java.util.List"        resultMap="RoleResult" />
     </resultMap>
@@ -295,16 +296,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
 
     <sql id="selectUserVo">
-        select u.user_id,u.qw_user_id,u.company_id,u.voice_print_url, u.dept_id, u.user_name, u.nick_name,
-               u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip,
-               u.login_date, u.create_by, u.create_time,u.id_card, u.remark,u.user_type,u.open_id,
-               u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.address_id,u.domain,u.is_audit,u.is_need_register_member,
-        d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
-        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
+        select u.user_id,u.company_id,u.qw_user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time,u.id_card, u.remark,u.user_type,u.open_id,u.qr_code_weixin,u.qr_code_wecom,u.jpush_id,u.domain,u.is_audit,u.address_id,
+               d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,
+               r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,
+               u.is_need_register_member, u.is_allowed_all_register
         from company_user u
-		    left join company_dept d on u.dept_id = d.dept_id
-		    left join company_user_role ur on u.user_id = ur.user_id
-		    left join company_role r on r.role_id = ur.role_id
+                 left join company_dept d on u.dept_id = d.dept_id
+                 left join company_user_role ur on u.user_id = ur.user_id
+                 left join company_role r on r.role_id = ur.role_id
     </sql>
 
     <select id="selectAllCompanyUserByCompanyIdAndDeptId" resultType="com.fs.company.domain.CompanyUser">
@@ -532,4 +531,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectCompanyUserByCompanyUserId" resultMap="CompanyUserResult">
         select  * from company_user where user_id = #{companyUserId}
     </select>
+
+    <update id="updateAllowedAllRegister" parameterType="Long">
+        update company_user
+        set is_allowed_all_register = #{status} where user_id in
+        <foreach item="userId" collection="userIds" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </update>
 </mapper>

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

@@ -98,4 +98,83 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{logId}
         </foreach>
     </delete>
+
+    <select id="selectFsCourseAnswerLogsListVONew" resultType="com.fs.course.vo.FsCourseAnswerLogsListVO">
+        SELECT
+        cal.*, uc.course_name AS course_name, uc.project AS project
+        FROM
+        fs_course_answer_logs cal
+        INNER JOIN (
+        SELECT
+        cal_inner.log_id
+        FROM
+        fs_course_answer_logs cal_inner
+        LEFT JOIN fs_user_course uc_inner ON cal_inner.course_id = uc_inner.course_id
+        <where>
+            <if test="courseId != null">
+                cal_inner.course_id = #{courseId}
+            </if>
+            <if test="videoId != null and videoId != '' ">
+                AND cal_inner.video_id = #{videoId}
+            </if>
+            <if test="companyUserId != null">
+                AND cal_inner.company_user_id = #{companyUserId}
+            </if>
+            <if test="companyId != null">
+                AND cal_inner.company_id = #{companyId}
+            </if>
+            <if test="isRight != null">
+                AND cal_inner.is_right = #{isRight}
+            </if>
+            <if test="project != null">
+                AND uc_inner.project = #{project}
+            </if>
+            <if test="sTime != null and eTime != null">
+                AND cal_inner.create_time BETWEEN #{sTime} AND #{eTime}
+            </if>
+            <if test="userIds != null and userIds.size() > 0">
+                and cal_inner.user_id in
+                <foreach collection="userIds"  open="(" close=")" separator="," item="userId" index="index">
+                    #{userId}
+                </foreach>
+            </if>
+            <if test="watchLogId != null">
+                AND cal_inner.watch_log_id = #{watchLogId}
+            </if>
+        </where>
+        ORDER BY cal_inner.log_id DESC
+        LIMIT ${(pageNum-1)*pageSize}, ${pageSize}
+        ) AS paged_ids ON cal.log_id = paged_ids.log_id
+        LEFT JOIN fs_user_course uc ON cal.course_id = uc.course_id
+        ORDER BY
+        cal.log_id DESC
+
+    </select>
+
+    <select id="selectFsCourseAnswerLogsListVONewCount" resultType="java.lang.Long">
+        select count(1) from fs_course_answer_logs cal
+        left join fs_user_course uc on cal.course_id=uc.course_id
+        <where>
+            <if test="courseId != null">
+                cal.course_id = #{courseId}
+            </if>
+            <if test="companyUserId != null">
+                AND cal.company_user_id = #{companyUserId}
+            </if>
+            <if test="companyId != null">
+                AND cal.company_id = #{companyId}
+            </if>
+            <if test="isRight != null">
+                AND is_right = #{isRight}
+            </if>
+            <if test="project != null">
+                AND uc.project = #{project}
+            </if>
+            <if test="sTime != null and eTime != null">
+                AND cal.create_time BETWEEN #{sTime} AND #{eTime}
+            </if>
+
+        </where>
+    </select>
+
 </mapper>

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

@@ -440,10 +440,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 and o.company_id=#{companyId}
             </if>
             <if test= 'sTime != null '>
-                and o.create_time &gt;= #{startDate}
+                and o.create_time &gt;= #{sTime}
             </if>
             <if test='eTime != null '>
-                and o.create_time &lt;= #{endDate}
+                and o.create_time &lt;= #{eTime}
             </if>
             <if test ='courseId !=null'>
                 and o.course_id = #{courseId}

+ 22 - 1
fs-service/src/main/resources/mapper/course/FsUserCoursePeriodDaysMapper.xml

@@ -120,7 +120,7 @@
     </delete>
 
     <select id="selectCourseVideoList" resultType="FsUserCoursePeriodDays">
-        select * from fs_user_course_period_days where  del_flag ='0' and period_id in
+        select * from fs_user_course_period_days where del_flag ='0' and period_id in
         <foreach collection="periodIds" item="item" open="(" separator="," close=")">
             #{item}
         </foreach>
@@ -200,4 +200,25 @@
         where ucpd.status = 1 and ucpd.end_date_time < #{now}
         ]]>
     </update>
+
+    <select id="selectDaysCountList" resultType="FsUserWatchCourseStatistics">
+        SELECT
+            a.*,
+            a.start_date_time as courseStartDateTime
+             ,b.course_name
+             ,c.title as videoTitle
+             ,period.period_name,period.period_starting_time,period.period_line
+             ,company.company_id,company.company_name
+             ,company_user.user_id as companyUserId, company_user.nick_name as companyUserName
+        FROM
+            fs_user_course_period_days a
+                INNER JOIN fs_user_course b ON a.course_id = b.course_id
+                INNER JOIN fs_user_course_video c ON a.video_id = c.video_id
+                left join fs_user_course_period period on period.period_id = a.period_id
+                left join company on FIND_IN_SET(company.company_id, period.company_id) > 0
+                left join company_user on company_user.company_id = company.company_id
+        where period.del_flag ='0' and period.period_name is not null
+        ORDER BY
+            a.day_date
+    </select>
 </mapper>

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

@@ -317,7 +317,7 @@
         AND ccut.course_id = fcpd.course_id
         AND ccut.video_id = fcpd.video_id
         AND ccut.company_user_id = #{params.companyUserId}
-        where course.is_del = 0 AND fcpd.del_flag = 0
+        where course.is_del = 0
         <if test="params.companyId != null">
             and FIND_IN_SET(#{params.companyId}, fcp.company_id)
         </if>

+ 9 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseVideoRedPackageMapper.xml

@@ -144,4 +144,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="periodId != null "> and period_id = #{periodId}</if>
         </where>
     </select>
+
+    <select id="selectRedPacketByCompanyId" resultType="com.fs.course.domain.FsUserCourseVideoRedPackage">
+        select * from fs_user_course_video_red_package
+        <where>
+            <if test="videoId != null "> and video_id =#{videoId}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="periodId != null "> and period_id = #{periodId}</if>
+        </where>
+    </select>
 </mapper>

+ 308 - 0
fs-service/src/main/resources/mapper/course/FsUserWatchCourseStatisticsMapper.xml

@@ -0,0 +1,308 @@
+<?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.course.mapper.FsUserWatchCourseStatisticsMapper">
+
+    <resultMap type="FsUserWatchCourseStatistics" id="FsUserWatchCourseStatisticsResult">
+        <result property="id"    column="id"    />
+        <result property="periodId"    column="period_id"    />
+        <result property="periodName"    column="period_name"    />
+        <result property="courseId"    column="course_id"    />
+        <result property="courseName"    column="course_name"    />
+        <result property="videoId"    column="video_id"    />
+        <result property="videoTitle"    column="video_title"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="courseStartDateTime"    column="course_start_date_time"    />
+        <result property="companyUserName"    column="company_user_name"    />
+        <result property="periodStartingTime"    column="period_starting_time"    />
+        <result property="newUserNum"    column="new_user_num"    />
+        <result property="userNum"    column="user_num"    />
+        <result property="watchNum"    column="watch_num"    />
+        <result property="completeWatchNum"    column="complete_watch_num"    />
+        <result property="onlineRate"    column="online_rate"    />
+        <result property="completeWatchRate"    column="complete_watch_rate"    />
+        <result property="answerNum"    column="answer_num"    />
+        <result property="answerRightNum"    column="answer_right_num"    />
+        <result property="answerRightRate"    column="answer_right_rate"    />
+        <result property="redPacketNum"    column="red_packet_num"    />
+        <result property="redPacketAmount"    column="red_packet_amount"    />
+    </resultMap>
+
+    <sql id="selectFsUserWatchCourseStatisticsVo">
+        select id, period_id, period_name, course_id, course_name, video_id, video_title, company_id, company_name, company_user_id, course_start_date_time, company_user_name, period_starting_time, new_user_num, user_num, watch_num, complete_watch_num, online_rate, complete_watch_rate, answer_num, answer_right_num, answer_right_rate, red_packet_num, red_packet_amount from fs_user_watch_course_statistics
+    </sql>
+
+    <select id="selectFsUserWatchCourseStatisticsList" parameterType="FsUserWatchCourseStatistics" resultMap="FsUserWatchCourseStatisticsResult">
+        <include refid="selectFsUserWatchCourseStatisticsVo"/>
+        <where>
+            <if test="periodId != null "> and period_id = #{periodId}</if>
+            <if test="periodName != null  and periodName != ''"> and period_name like concat('%', #{periodName}, '%')</if>
+            <if test="courseId != null "> and course_id = #{courseId}</if>
+            <if test="courseName != null  and courseName != ''"> and course_name like concat('%', #{courseName}, '%')</if>
+            <if test="videoId != null "> and video_id = #{videoId}</if>
+            <if test="videoTitle != null  and videoTitle != ''"> and video_title like concat('%', #{videoTitle}, '%')</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="courseStartDateTime != null "> and course_start_date_time like concat(DATE(#{courseStartDateTime}),'%')</if>
+            <if test="companyUserName != null  and companyUserName != ''"> and company_user_name like concat('%', #{companyUserName}, '%')</if>
+            <if test="periodStartingTime != null "> and period_starting_time like concat(DATE(#{periodStartingTime}), '%')</if>
+            <if test="newUserNum != null "> and new_user_num = #{newUserNum}</if>
+            <if test="userNum != null "> and user_num = #{userNum}</if>
+            <if test="watchNum != null "> and watch_num = #{watchNum}</if>
+            <if test="completeWatchNum != null "> and complete_watch_num = #{completeWatchNum}</if>
+            <if test="completeWatchRate != null "> and complete_watch_rate = #{completeWatchRate}</if>
+            <if test="answerNum != null "> and answer_num = #{answerNum}</if>
+            <if test="answerRightNum != null "> and answer_right_num = #{answerRightNum}</if>
+            <if test="answerRightRate != null "> and answer_right_rate = #{answerRightRate}</if>
+            <if test="redPacketNum != null "> and red_packet_num = #{redPacketNum}</if>
+            <if test="redPacketAmount != null "> and red_packet_amount = #{redPacketAmount}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserWatchCourseStatisticsListTotal" parameterType="FsUserWatchCourseStatistics" resultMap="FsUserWatchCourseStatisticsResult">
+        SELECT
+        id,
+        period_id,
+        period_name,
+        course_id,
+        course_name,
+        video_id,
+        video_title,
+        company_id,
+        company_name,
+        company_user_id,
+        course_start_date_time,
+        period_starting_time,
+        sum(new_user_num) as new_user_num,
+        sum(user_num) as user_num,
+        sum(watch_num) as watch_num,
+        ifnull(
+        ROUND(
+        ( sum(watch_num) / sum(user_num) ) * 100, 2
+        ), 0
+        ) as online_rate,
+        sum(complete_watch_num) as complete_watch_num ,
+        complete_watch_rate as complete_watch_rate1,
+        ifnull(
+        ROUND(
+        ( sum(complete_watch_num) / sum(watch_num) ) * 100, 2
+        ), 0
+        ) as complete_watch_rate,
+        sum(red_packet_num) as red_packet_num
+        from fs_user_watch_course_statistics
+        <where>
+            <if test="periodId != null "> and period_id = #{periodId}</if>
+            <if test="periodName != null  and periodName != ''"> and period_name like concat('%', #{periodName}, '%')</if>
+            <if test="courseId != null "> and course_id = #{courseId}</if>
+            <if test="courseName != null  and courseName != ''"> and course_name like concat('%', #{courseName}, '%')</if>
+            <if test="videoId != null "> and video_id = #{videoId}</if>
+            <if test="videoTitle != null  and videoTitle != ''"> and video_title like concat('%', #{videoTitle}, '%')</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="courseStartDateTime != null "> and course_start_date_time like concat(DATE(#{courseStartDateTime}),'%')</if>
+            <if test="companyUserName != null  and companyUserName != ''"> and company_user_name like concat('%', #{companyUserName}, '%')</if>
+            <if test="periodStartingTime != null "> and period_starting_time like concat(DATE(#{periodStartingTime}), '%')</if>
+            <if test="newUserNum != null "> and new_user_num = #{newUserNum}</if>
+            <if test="userNum != null "> and user_num = #{userNum}</if>
+            <if test="watchNum != null "> and watch_num = #{watchNum}</if>
+            <if test="completeWatchNum != null "> and complete_watch_num = #{completeWatchNum}</if>
+            <if test="completeWatchRate != null "> and complete_watch_rate = #{completeWatchRate}</if>
+            <if test="answerNum != null "> and answer_num = #{answerNum}</if>
+            <if test="answerRightNum != null "> and answer_right_num = #{answerRightNum}</if>
+            <if test="answerRightRate != null "> and answer_right_rate = #{answerRightRate}</if>
+            <if test="redPacketNum != null "> and red_packet_num = #{redPacketNum}</if>
+            <if test="redPacketAmount != null "> and red_packet_amount = #{redPacketAmount}</if>
+        </where>
+        group by fs_user_watch_course_statistics.company_id, course_start_date_time
+    </select>
+
+    <select id="selectFsUserWatchCourseStatisticsById" parameterType="Long" resultMap="FsUserWatchCourseStatisticsResult">
+        <include refid="selectFsUserWatchCourseStatisticsVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertFsUserWatchCourseStatistics" parameterType="FsUserWatchCourseStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_watch_course_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">period_id,</if>
+            <if test="periodName != null">period_name,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="courseName != null">course_name,</if>
+            <if test="videoId != null">video_id,</if>
+            <if test="videoTitle != null">video_title,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="courseStartDateTime != null">course_start_date_time,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+            <if test="periodStartingTime != null">period_starting_time,</if>
+            <if test="newUserNum != null">new_user_num,</if>
+            <if test="userNum != null">user_num,</if>
+            <if test="watchNum != null">watch_num,</if>
+            <if test="completeWatchNum != null">complete_watch_num,</if>
+            <if test="onlineRate != null">online_rate,</if>
+            <if test="completeWatchRate != null">complete_watch_rate,</if>
+            <if test="answerNum != null">answer_num,</if>
+            <if test="answerRightNum != null">answer_right_num,</if>
+            <if test="answerRightRate != null">answer_right_rate,</if>
+            <if test="redPacketNum != null">red_packet_num,</if>
+            <if test="redPacketAmount != null">red_packet_amount,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">#{periodId},</if>
+            <if test="periodName != null">#{periodName},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="courseName != null">#{courseName},</if>
+            <if test="videoId != null">#{videoId},</if>
+            <if test="videoTitle != null">#{videoTitle},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="courseStartDateTime != null">#{courseStartDateTime},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+            <if test="periodStartingTime != null">#{periodStartingTime},</if>
+            <if test="newUserNum != null">#{newUserNum},</if>
+            <if test="userNum != null">#{userNum},</if>
+            <if test="watchNum != null">#{watchNum},</if>
+            <if test="completeWatchNum != null">#{completeWatchNum},</if>
+            <if test="onlineRate != null">#{onlineRate},</if>
+            <if test="completeWatchRate != null">#{completeWatchRate},</if>
+            <if test="answerNum != null">#{answerNum},</if>
+            <if test="answerRightNum != null">#{answerRightNum},</if>
+            <if test="answerRightRate != null">#{answerRightRate},</if>
+            <if test="redPacketNum != null">#{redPacketNum},</if>
+            <if test="redPacketAmount != null">#{redPacketAmount},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserWatchCourseStatistics" parameterType="FsUserWatchCourseStatistics">
+        update fs_user_watch_course_statistics
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="periodId != null">period_id = #{periodId},</if>
+            <if test="periodName != null">period_name = #{periodName},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="courseName != null">course_name = #{courseName},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="videoTitle != null">video_title = #{videoTitle},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="courseStartDateTime != null">course_start_date_time = #{courseStartDateTime},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="periodStartingTime != null">period_starting_time = #{periodStartingTime},</if>
+            <if test="newUserNum != null">new_user_num = #{newUserNum},</if>
+            <if test="userNum != null">user_num = #{userNum},</if>
+            <if test="watchNum != null">watch_num = #{watchNum},</if>
+            <if test="completeWatchNum != null">complete_watch_num = #{completeWatchNum},</if>
+            <if test="onlineRate != null">online_rate = #{onlineRate},</if>
+            <if test="completeWatchRate != null">complete_watch_rate = #{completeWatchRate},</if>
+            <if test="answerNum != null">answer_num = #{answerNum},</if>
+            <if test="answerRightNum != null">answer_right_num = #{answerRightNum},</if>
+            <if test="answerRightRate != null">answer_right_rate = #{answerRightRate},</if>
+            <if test="redPacketNum != null">red_packet_num = #{redPacketNum},</if>
+            <if test="redPacketAmount != null">red_packet_amount = #{redPacketAmount},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsUserWatchCourseStatisticsById" parameterType="Long">
+        delete from fs_user_watch_course_statistics where id = #{id}
+    </delete>
+
+    <delete id="deleteFsUserWatchCourseStatisticsByIds" parameterType="String">
+        delete from fs_user_watch_course_statistics where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+
+    <insert id="insertFsUserWatchCourseStatisticsTask" parameterType="FsUserWatchCourseStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_watch_course_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">period_id,</if>
+            <if test="periodName != null">period_name,</if>
+            <if test="courseId != null">course_id,</if>
+            <if test="courseName != null">course_name,</if>
+            <if test="videoId != null">video_id,</if>
+            <if test="videoTitle != null">video_title,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="courseStartDateTime != null">course_start_date_time,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+            <if test="periodStartingTime != null">period_starting_time,</if>
+            <if test="newUserNum != null">new_user_num,</if>
+            <if test="userNum != null">user_num,</if>
+            <if test="watchNum != null">watch_num,</if>
+            <if test="completeWatchNum != null">complete_watch_num,</if>
+            <if test="onlineRate != null">online_rate,</if>
+            <if test="completeWatchRate != null">complete_watch_rate,</if>
+            <if test="answerNum != null">answer_num,</if>
+            <if test="answerRightNum != null">answer_right_num,</if>
+            <if test="answerRightRate != null">answer_right_rate,</if>
+            <if test="redPacketNum != null">red_packet_num,</if>
+            <if test="redPacketAmount != null">red_packet_amount,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">#{periodId},</if>
+            <if test="periodName != null">#{periodName},</if>
+            <if test="courseId != null">#{courseId},</if>
+            <if test="courseName != null">#{courseName},</if>
+            <if test="videoId != null">#{videoId},</if>
+            <if test="videoTitle != null">#{videoTitle},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="courseStartDateTime != null">#{courseStartDateTime},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+            <if test="periodStartingTime != null">#{periodStartingTime},</if>
+            <if test="newUserNum != null">#{newUserNum},</if>
+            <if test="userNum != null">#{userNum},</if>
+            <if test="watchNum != null">#{watchNum},</if>
+            <if test="completeWatchNum != null">#{completeWatchNum},</if>
+            <if test="onlineRate != null">#{onlineRate},</if>
+            <if test="completeWatchRate != null">#{completeWatchRate},</if>
+            <if test="answerNum != null">#{answerNum},</if>
+            <if test="answerRightNum != null">#{answerRightNum},</if>
+            <if test="answerRightRate != null">#{answerRightRate},</if>
+            <if test="redPacketNum != null">#{redPacketNum},</if>
+            <if test="redPacketAmount != null">#{redPacketAmount},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+        on duplicate key update
+        <trim suffixOverrides=",">
+            <if test="periodId != null">period_id = #{periodId},</if>
+            <if test="periodName != null">period_name = #{periodName},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="courseName != null">course_name = #{courseName},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="videoTitle != null">video_title = #{videoTitle},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="courseStartDateTime != null">course_start_date_time = #{courseStartDateTime},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="periodStartingTime != null">period_starting_time = #{periodStartingTime},</if>
+            <if test="newUserNum != null">new_user_num = #{newUserNum},</if>
+            <if test="userNum != null">user_num = #{userNum},</if>
+            <if test="watchNum != null">watch_num = #{watchNum},</if>
+            <if test="completeWatchNum != null">complete_watch_num = #{completeWatchNum},</if>
+            <if test="onlineRate != null">online_rate = #{onlineRate},</if>
+            <if test="completeWatchRate != null">complete_watch_rate = #{completeWatchRate},</if>
+            <if test="answerNum != null">answer_num = #{answerNum},</if>
+            <if test="answerRightNum != null">answer_right_num = #{answerRightNum},</if>
+            <if test="answerRightRate != null">answer_right_rate = #{answerRightRate},</if>
+            <if test="redPacketNum != null">red_packet_num = #{redPacketNum},</if>
+            <if test="redPacketAmount != null">red_packet_amount = #{redPacketAmount},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+    </insert>
+
+</mapper>

+ 176 - 0
fs-service/src/main/resources/mapper/course/FsUserWatchStatisticsMapper.xml

@@ -0,0 +1,176 @@
+<?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.course.mapper.FsUserWatchStatisticsMapper">
+
+    <resultMap type="FsUserWatchStatistics" id="FsUserWatchStatisticsResult">
+        <result property="id"    column="id"    />
+        <result property="periodId"    column="period_id"    />
+        <result property="periodName"    column="period_name"    />
+        <result property="periodStartingTime"    column="period_starting_time"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="newUserNum"    column="new_user_num"    />
+        <result property="userNum"    column="user_num"    />
+        <result property="watchNum"    column="watch_num"    />
+        <result property="completeWatchNum"    column="complete_watch_num"    />
+        <result property="onlineRate"    column="online_rate"    />
+        <result property="completeWatchRate"    column="complete_watch_rate"    />
+    </resultMap>
+
+    <sql id="selectFsUserWatchStatisticsVo">
+        select id, period_id, period_name, period_starting_time, new_user_num, user_num, watch_num, complete_watch_num, online_rate, complete_watch_rate, company_id, company_name from fs_user_watch_statistics
+    </sql>
+
+    <select id="selectFsUserWatchStatisticsList" parameterType="FsUserWatchStatistics" resultMap="FsUserWatchStatisticsResult">
+        <include refid="selectFsUserWatchStatisticsVo"/>
+        <where>
+            <if test="periodId != null "> and period_id = #{periodId}</if>
+            <if test="periodName != null  and periodName != ''"> and period_name like concat('%', #{periodName}, '%')</if>
+            <if test="periodStartingTime != null "> and period_starting_time = #{periodStartingTime}</if>
+            <if test="companyId != null and companyId !='' "> and company_id = #{companyId}</if>
+            <if test="companyName != null  and companyName != ''"> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="newUserNum != null "> and new_user_num = #{newUserNum}</if>
+            <if test="userNum != null "> and user_num = #{userNum}</if>
+            <if test="watchNum != null "> and watch_num = #{watchNum}</if>
+            <if test="completeWatchNum != null "> and complete_watch_num = #{completeWatchNum}</if>
+            <if test="completeWatchRate != null "> and complete_watch_rate = #{completeWatchRate}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserWatchStatisticsById" parameterType="Long" resultMap="FsUserWatchStatisticsResult">
+        <include refid="selectFsUserWatchStatisticsVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertFsUserWatchStatistics" parameterType="FsUserWatchStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_watch_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">period_id,</if>
+            <if test="periodName != null">period_name,</if>
+            <if test="periodStartingTime != null">period_starting_time,</if>
+            <if test="companyId != null and companyId !='' ">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="newUserNum != null">new_user_num,</if>
+            <if test="userNum != null">user_num,</if>
+            <if test="watchNum != null">watch_num,</if>
+            <if test="completeWatchNum != null">complete_watch_num,</if>
+            <if test="onlineRate != null">online_rate,</if>
+            <if test="completeWatchRate != null">complete_watch_rate,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">#{periodId},</if>
+            <if test="periodName != null">#{periodName},</if>
+            <if test="periodStartingTime != null">#{periodStartingTime},</if>
+            <if test="companyId != null and companyId !='' ">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="newUserNum != null">#{newUserNum},</if>
+            <if test="userNum != null">#{userNum},</if>
+            <if test="watchNum != null">#{watchNum},</if>
+            <if test="completeWatchNum != null">#{completeWatchNum},</if>
+            <if test="onlineRate != null">#{onlineRate},</if>
+            <if test="completeWatchRate != null">#{completeWatchRate},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserWatchStatistics" parameterType="FsUserWatchStatistics">
+        update fs_user_watch_statistics
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="periodId != null">period_id = #{periodId},</if>
+            <if test="periodName != null">period_name = #{periodName},</if>
+            <if test="periodStartingTime != null">period_starting_time = #{periodStartingTime},</if>
+            <if test="companyId != null and companyId !='' ">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="newUserNum != null">new_user_num = #{newUserNum},</if>
+            <if test="userNum != null">user_num = #{userNum},</if>
+            <if test="watchNum != null">watch_num = #{watchNum},</if>
+            <if test="completeWatchNum != null">complete_watch_num = #{completeWatchNum},</if>
+            <if test="onlineRate != null">online_rate = #{onlineRate},</if>
+            <if test="completeWatchRate != null">complete_watch_rate = #{completeWatchRate},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsUserWatchStatisticsById" parameterType="Long">
+        delete from fs_user_watch_statistics where id = #{id}
+    </delete>
+
+    <delete id="deleteFsUserWatchStatisticsByIds" parameterType="String">
+        delete from fs_user_watch_statistics where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="getCourseWatchStatistics" resultType="FsUserWatchStatistics">
+        SELECT
+            count( DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END ) AS watchNum,
+            count( DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) AS completeWatchNum,
+            ifnull(
+                    ROUND(
+                            (
+                                COUNT( DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) / count( DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END )) * 100,
+                            2
+                    ),
+                    0
+            ) AS completeWatchRate,
+            fwl.period_id,
+            fwl.company_id
+        FROM
+            fs_course_watch_log fwl
+
+        WHERE
+            fwl.send_type = 1
+        GROUP BY
+            fwl.period_id, fwl.company_id
+    </select>
+
+    <insert id="insertFsUserWatchStatisticsTask" parameterType="FsUserWatchStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_watch_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">period_id,</if>
+            <if test="periodName != null">period_name,</if>
+            <if test="periodStartingTime != null">period_starting_time,</if>
+            <if test="companyId != null and companyId !='' ">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="newUserNum != null">new_user_num,</if>
+            <if test="userNum != null">user_num,</if>
+            <if test="watchNum != null">watch_num,</if>
+            <if test="completeWatchNum != null">complete_watch_num,</if>
+            <if test="completeWatchRate != null">complete_watch_rate,</if>
+            <if test="onlineRate != null">online_rate,</if>
+            <if test="createTime != null">create_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="periodId != null">#{periodId},</if>
+            <if test="periodName != null">#{periodName},</if>
+            <if test="periodStartingTime != null">#{periodStartingTime},</if>
+            <if test="companyId != null and companyId !='' ">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="newUserNum != null">#{newUserNum},</if>
+            <if test="userNum != null">#{userNum},</if>
+            <if test="watchNum != null">#{watchNum},</if>
+            <if test="completeWatchNum != null">#{completeWatchNum},</if>
+            <if test="onlineRate != null">#{onlineRate},</if>
+            <if test="completeWatchRate != null">#{completeWatchRate},</if>
+            <if test="createTime != null">#{createTime},</if>
+        </trim>
+        on duplicate key update
+        <trim suffixOverrides=",">
+            <if test="periodId != null">period_id = #{periodId},</if>
+            <if test="periodName != null">period_name = #{periodName},</if>
+            <if test="periodStartingTime != null">period_starting_time = #{periodStartingTime},</if>
+            <if test="companyId != null and companyId !='' ">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="newUserNum != null">new_user_num = #{newUserNum},</if>
+            <if test="userNum != null">user_num = #{userNum},</if>
+            <if test="watchNum != null">watch_num = #{watchNum},</if>
+            <if test="completeWatchNum != null">complete_watch_num = #{completeWatchNum},</if>
+            <if test="onlineRate != null">online_rate = #{onlineRate},</if>
+            <if test="completeWatchRate != null">complete_watch_rate = #{completeWatchRate},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+    </insert>
+
+</mapper>

+ 224 - 3
fs-service/src/main/resources/mapper/his/FsUserMapper.xml

@@ -211,10 +211,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         fs_user.phone,
         fs_user_course_count.id,
         fs_user_course_count.watch_course_count,
+--         fs_course_watch_log.watch_course_count,
         fs_user_course_count.miss_course_count,
         fs_user_course_count.miss_course_status,
         fs_user_course_count.course_ids,
         fs_user_course_count.part_course_count,
+--         fs_course_watch_log.part_course_count,
         fs_user_course_count.last_watch_date,
         fs_user_course_count.STATUS AS courseCountStatus,
         fs_user_course_count.stop_watch_days,
@@ -434,6 +436,80 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <select id="selectFsUserVOList" resultType="com.fs.his.vo.FsUserVO">
+        SELECT
+        b.total_amount,b.last_buy_time,p.pay_money as number,p.payment_id,p.pay_time,
+        u.*,
+        fs_course_watch_log.watch_course_count, fs_course_watch_log.part_course_count, company_user.nick_name AS companyUserNickName, fs_course_watch_log.last_watch_date
+        ,company.company_name
+        FROM
+        fs_user u
+        LEFT JOIN (
+        SELECT max( payment_id ) AS payment_id, max( pay_time ) AS last_buy_time, SUM( pay_money ) AS total_amount,
+        user_id
+        FROM
+        fs_store_payment
+        WHERE
+        STATUS = 1
+        AND user_id IS NOT NULL
+        GROUP BY
+        user_id
+        ) b ON u.user_id = b.user_id
+        LEFT JOIN fs_store_payment p ON u.user_id = p.user_id
+        AND b.last_buy_time = p.pay_time
+        AND b.payment_id = p.payment_id
+        LEFT JOIN (
+        SELECT
+        fs_course_watch_log.user_id,
+        Max( fs_course_watch_log.last_heartbeat_time ) AS last_watch_date,
+        count( DISTINCT fs_course_watch_log.video_id ) watch_course_count,
+        count( DISTINCT fs_course_watch_log.period_id ) part_course_count
+        FROM
+        fs_course_watch_log
+        GROUP BY
+        fs_course_watch_log.user_id
+        ) fs_course_watch_log ON fs_course_watch_log.user_id = u.user_id
+        LEFT JOIN company_user ON company_user.user_id = u.company_user_id
+        LEFT JOIN company on company.company_id = company_user.company_id
+        <where>
+            1 = 1
+            <if test = "maps.userId != null">
+                AND u.user_id LIKE CONCAT("%",#{maps.userId},"%")
+            </if >
+            <if test = "maps.nickname != null and  maps.nickname !='' " >
+                AND u.nickname LIKE CONCAT("%",#{maps.nickname},"%")
+            </if >
+            <if test = "maps.phone != null   and  maps.phone !='' " >
+                AND u.phone LIKE CONCAT("%",#{maps.phone},"%")
+            </if >
+            <if test = "maps.startCreateTime != null and maps.endCreateTime != null" >
+                AND (DATE_FORMAT( u.create_time, "%Y-%m-%d" ) &gt;= DATE_FORMAT(#{maps.startCreateTime}, "%Y-%m-%d")
+                and DATE_FORMAT( u.create_time, "%Y-%m-%d" ) &lt;= DATE_FORMAT(#{maps.endCreateTime}, "%Y-%m-%d")
+                )
+            </if >
+            <if test = "maps.registerCode != null  and  maps.registerCode !=''  " >
+                AND u.register_code = #{maps.registerCode}
+            </if >
+            <if test = "maps.status != null" >
+                AND u.STATUS = #{maps.status}
+            </if >
+            <if test = "maps.companyUserNickName != null and maps.companyUserNickName != '' " >
+                AND company_user.nick_name like CONCAT ("%",#{maps.companyUserNickName},"%")
+            </if >
+            <if test = "maps.companyName != null and maps.companyName != '' " >
+                AND company.company_name like CONCAT ("%",#{maps.companyName},"%")
+            </if >
+            <if test = "maps.level != null  and  maps.level !=''" >
+                AND u.LEVEL = #{maps.level}
+            </if >
+            <if test = "maps.isPromoter != null  and  maps.isPromoter !=''" >
+                AND u.is_promoter = #{maps.isPromoter}
+            </if >
+        </where>
+        ORDER BY
+        user_id DESC
+    </select>
+
     <insert id="insertFsUser" parameterType="FsUser" useGeneratedKeys="true" keyProperty="userId">
         insert into fs_user
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -591,6 +667,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </foreach>
     </update>
 
+    <update id="batchUpdateCompanyUserRelation">
+        update fs_user_company_user
+        set company_id = #{companyId},
+        company_user_id = #{companyUserId}
+        where is_repeat_fans = 0 and
+        <foreach collection="userIds" open="(" close=")" separator="or" item="userId" index="index">
+            user_id = #{userId}
+        </foreach>
+    </update>
+
     <select id="selectUserListByMap" resultType="com.fs.his.vo.OptionsVO">
         select
         u.user_id dictValue,
@@ -626,13 +712,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="getUserNumber" resultType="com.fs.store.vo.h5.UserListCountVO">
         SELECT
-        fs_user.`status` as status,
+        fs_user_company_user.`status` as status,
         count( DISTINCT fs_user.user_id ) AS num
         FROM
         fs_user
         LEFT JOIN fs_user_company_user ON fs_user_company_user.user_id = fs_user.user_id
         LEFT JOIN company_user ON company_user.user_id = fs_user_company_user.company_user_id
-        WHERE fs_user.is_del = 0
+        WHERE fs_user.is_del = 0 and fs_user_company_user.is_repeat_fans is not null
         <if test="userId != null and userId != 0 ">
             and (fs_user_company_user.company_user_id = #{userId} OR company_user.parent_id = #{userId} )
         </if>
@@ -640,7 +726,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and fs_user_company_user.company_id = #{companyId}
         </if>
         GROUP BY
-            fs_user.`status`
+        fs_user_company_user.`status`,fs_user_company_user.project_id
     </select>
 
     <select id="getRepeatUserNumber" resultType="int">
@@ -1618,4 +1704,139 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{userId}
         </foreach>
     </select>
+
+    <select id="selectFsUserTotal" resultType="FsUserWatchStatistics">
+        SELECT
+            count( fs_user.user_id ) as userNum,
+            count( DISTINCT CASE WHEN to_days( fs_user.create_time ) = to_days( now()) THEN fs_user.user_id END ) as newUserNum,
+            fs_user.company_id
+        FROM
+            fs_user
+                LEFT JOIN company_user ON company_user.user_id = fs_user.company_user_id
+        WHERE
+            fs_user.is_del = 0
+          AND fs_user.`status` = 1
+          AND company_user.user_id is not null
+        GROUP BY
+            fs_user.company_id
+    </select>
+
+
+    <select id="selectWatchLogCount" resultType="FsUserWatchCourseStatistics">
+        SELECT
+            count( DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END ) AS watchNum,
+
+            count( DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) AS completeWatchNum,
+
+            ifnull(
+                    ROUND(
+                            (
+                                COUNT( DISTINCT CASE WHEN fwl.log_type = 2 THEN fwl.user_id END ) / count( DISTINCT CASE WHEN fwl.log_type != 3 THEN fwl.user_id END )) * 100,
+                            2
+                    ),
+                    0
+            ) AS completeWatchRate,
+            fwl.period_id, fwl.video_id, fwl.company_user_id, fwl.company_id
+        FROM
+            fs_course_watch_log fwl
+
+        WHERE
+            fwl.send_type = 1
+        GROUP BY
+            fwl.period_id, fwl.video_id, fwl.company_user_id
+    </select>
+
+    <select id="selectRedPacketLogCount" resultType="FsUserWatchCourseStatistics">
+        SELECT
+            count( flog.log_id ) AS redPacketNum,
+            ifnull ( sum( flog.amount ), 0 ) AS redPacketAmount,
+            flog.period_id,
+            flog.video_id,
+            flog.company_user_id,
+            flog.company_id
+        FROM
+            fs_course_red_packet_log flog
+                LEFT JOIN fs_user ON fs_user.user_id = flog.user_id
+                LEFT JOIN company_user ON company_user.user_id = fs_user.company_user_id
+        GROUP BY
+            flog.period_id,
+            flog.video_id,
+            flog.company_user_id
+    </select>
+
+    <select id="selectAnswerLogCount" resultType="FsUserWatchCourseStatistics">
+        SELECT
+            count( DISTINCT fs_user.user_id ) AS answerNum,
+            COUNT( DISTINCT CASE WHEN fs_course_answer_logs.is_right = 1 THEN fs_user.user_id END ) AS answerRightNum,
+            ifnull(
+                    ROUND(
+                            (
+                                COUNT( DISTINCT CASE WHEN fs_course_answer_logs.is_right = 1 THEN fs_user.user_id END ) / count( DISTINCT fs_user.user_id )) * 100,
+                            2
+                    ),
+                    0
+            ) AS answerRightRate,
+            fs_course_answer_logs.period_id,
+            fs_course_answer_logs.video_id,
+            fs_course_answer_logs.company_user_id,
+            fs_course_answer_logs.company_id
+        FROM
+            fs_course_answer_logs
+                LEFT JOIN fs_user ON fs_user.user_id = fs_course_answer_logs.user_id
+                LEFT JOIN company_user ON company_user.user_id = fs_user.company_user_id
+        GROUP BY
+            fs_course_answer_logs.period_id,
+            fs_course_answer_logs.video_id,
+            fs_course_answer_logs.company_user_id
+    </select>
+
+    <select id="selectFsUserDetail" resultType="FsUserWatchCourseStatistics">
+        SELECT
+            count( fs_user.user_id ) as userNum,
+            count( DISTINCT CASE WHEN to_days( fs_user.create_time ) = to_days( now()) THEN fs_user.user_id END ) as newUserNum,
+            date(fs_user.create_time) as userCreateDate
+                ,company.company_id,
+            company.company_name,
+            company_user.user_id AS companyUserId,
+            company_user.nick_name AS companyUserName
+
+        FROM
+            fs_user
+            LEFT JOIN company_user ON company_user.user_id = fs_user.company_user_id
+            LEFT JOIN company ON company.company_id = fs_user.company_id
+        WHERE
+            fs_user.is_del = 0
+          AND fs_user.`status` = 1
+          AND company_user.user_id is not null
+        GROUP BY
+            fs_user.company_user_id, date(fs_user.create_time)
+    </select>
+
+
+    <select id="selectFsUserListByJointUserNameKey" parameterType="FsUser" resultMap="FsUserResult">
+        <include refid="selectFsUserVo"/>
+        <where>
+            <if test="userName != null  and userName != ''">
+                AND (
+                nickname like concat('%', #{userName}, '%')
+                or  user_id LIKE concat('%',#{userName},'%')
+                )
+            </if>
+        </where>
+        order by user_id desc
+    </select>
+    <select id="selectFsUserCount" resultType="java.lang.Long">
+        select count(1) from fs_user
+        where 1=1
+        <if test = "type != null and  type ==1">
+            and DATE_FORMAT(create_time, '%Y-%m-%d')  = DATE_FORMAT(NOW(), '%Y-%m-%d')
+        </if>
+        <if test = "companyId != null">
+            and company_id=#{companyId}
+        </if>
+        <if test="companyUserId != null">
+            and company_user_id = #{companyUserId}
+        </if>
+    </select>
+
 </mapper>

+ 202 - 0
fs-service/src/main/resources/mapper/his/FsUserOnlineStateMapper.xml

@@ -0,0 +1,202 @@
+<?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.his.mapper.FsUserOnlineStateMapper">
+
+    <resultMap type="FsUserOnlineState" id="FsUserOnlineStateResult">
+        <result property="id"    column="id"    />
+        <result property="userId"    column="user_id"    />
+        <result property="nickname"    column="nickname"    />
+        <result property="avatar"    column="avatar"    />
+        <result property="phone"    column="phone"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="maOpenId"    column="ma_open_id"    />
+        <result property="mpOpenId"    column="mp_open_id"    />
+        <result property="unionId"    column="union_id"    />
+        <result property="status"    column="status"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyUserId"    column="company_user_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="companyUserName"    column="company_user_name"    />
+        <result property="onlineStatus"    column="online_status"    />
+        <result property="onlineTime"    column="online_time"    />
+        <result property="watchCourseCount"    column="watch_course_count"    />
+        <result property="partCourseCount"    column="part_course_count"    />
+        <result property="lastWatchDate"    column="last_watch_date"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectFsUserOnlineStateVo">
+        select id, user_id, nickname, avatar, phone, create_time, ma_open_id, mp_open_id, union_id, status, company_id, company_user_id, company_name, company_user_name, online_status, online_time, watch_course_count, part_course_count, last_watch_date, update_time from fs_user_online_state
+    </sql>
+
+    <select id="selectFsUserOnlineStateList" parameterType="FsUserOnlineState" resultMap="FsUserOnlineStateResult">
+        <include refid="selectFsUserOnlineStateVo"/>
+        <where>
+            <if test="nickname != null  and nickname != ''"> and nickname like concat('%', #{nickname}, '%')</if>
+            <if test="avatar != null  and avatar != ''"> and avatar = #{avatar}</if>
+            <if test="phone != null  and phone != ''"> and phone like concat ('%', #{phone}, '%')</if>
+            <if test="maOpenId != null  and maOpenId != ''"> and ma_open_id = #{maOpenId}</if>
+            <if test="mpOpenId != null  and mpOpenId != ''"> and mp_open_id = #{mpOpenId}</if>
+            <if test="unionId != null  and unionId != ''"> and union_id = #{unionId}</if>
+            <if test="status != null "> and status = #{status}</if>
+            <if test="companyId != null "> and company_id = #{companyId}</if>
+            <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
+            <if test="companyName != null and companyName !='' "> and company_name like concat('%', #{companyName}, '%')</if>
+            <if test="companyUserName != null and companyUserName !='' "> and company_user_name like concat('%', #{companyUserName}, '%')</if>
+            <if test="onlineStatus != null "> and online_status = #{onlineStatus}</if>
+            <if test="onlineTime != null "> and online_time = #{onlineTime}</if>
+            <if test="watchCourseCount != null "> and watch_course_count = #{watchCourseCount}</if>
+            <if test="partCourseCount != null "> and part_course_count = #{partCourseCount}</if>
+            <if test="lastWatchDate != null "> and last_watch_date = #{lastWatchDate}</if>
+            <if test="updateTime != null "> and update_time = #{updateTime}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserOnlineStateById" parameterType="Long" resultMap="FsUserOnlineStateResult">
+        <include refid="selectFsUserOnlineStateVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertFsUserOnlineState" parameterType="FsUserOnlineState" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_online_state
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="userId != null">user_id,</if>
+            <if test="nickname != null">nickname,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="phone != null">phone,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="maOpenId != null">ma_open_id,</if>
+            <if test="mpOpenId != null">mp_open_id,</if>
+            <if test="unionId != null">union_id,</if>
+            <if test="status != null">status,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="companyUserName != null">company_user_name,</if>
+            <if test="onlineStatus != null">online_status,</if>
+            <if test="onlineTime != null">online_time,</if>
+            <if test="watchCourseCount != null">watch_course_count,</if>
+            <if test="partCourseCount != null">part_course_count,</if>
+            <if test="lastWatchDate != null">last_watch_date,</if>
+            <if test="updateTime != null ">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="userId != null">#{userId},</if>
+            <if test="nickname != null">#{nickname},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="phone != null">#{phone},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="maOpenId != null">#{maOpenId},</if>
+            <if test="mpOpenId != null">#{mpOpenId},</if>
+            <if test="unionId != null">#{unionId},</if>
+            <if test="status != null">#{status},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="companyUserName != null">#{companyUserName},</if>
+            <if test="onlineStatus != null">#{onlineStatus},</if>
+            <if test="onlineTime != null">#{onlineTime},</if>
+            <if test="watchCourseCount != null">#{watchCourseCount},</if>
+            <if test="partCourseCount != null">#{partCourseCount},</if>
+            <if test="lastWatchDate != null">#{lastWatchDate},</if>
+            <if test="updateTime != null ">#{updateTime},</if>
+         </trim>
+        on duplicate key update
+        <trim suffixOverrides=",">
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyName != null">company_Name = #{companyName},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="updateTime != null ">update_time = #{updateTime}</if>
+        </trim>
+    </insert>
+
+    <update id="updateFsUserOnlineState" parameterType="FsUserOnlineState">
+        update fs_user_online_state
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="nickname != null">nickname = #{nickname},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="phone != null">phone = #{phone},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="maOpenId != null">ma_open_id = #{maOpenId},</if>
+            <if test="mpOpenId != null">mp_open_id = #{mpOpenId},</if>
+            <if test="unionId != null">union_id = #{unionId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="companyName != null">company_Name = #{companyName},</if>
+            <if test="companyUserName != null">company_user_name = #{companyUserName},</if>
+            <if test="onlineStatus != null">online_status = #{onlineStatus},</if>
+            <if test="onlineTime != null">online_time = #{onlineTime},</if>
+            <if test="watchCourseCount != null">watch_course_count = #{watchCourseCount},</if>
+            <if test="partCourseCount != null">part_course_count = #{partCourseCount},</if>
+            <if test="lastWatchDate != null">last_watch_date = #{lastWatchDate},</if>
+            <if test="updateTime != null ">update_time = #{updateTime}</if>
+        </trim>
+        where user_id = #{userId}
+    </update>
+
+    <delete id="deleteFsUserOnlineStateById" parameterType="Long">
+        delete from fs_user_online_state where user_id = #{userId}
+    </delete>
+
+    <delete id="deleteFsUserOnlineStateByIds" parameterType="String">
+        delete from fs_user_online_state where user_id in
+        <foreach item="userId" collection="array" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </delete>
+
+    <select id="selectUserNotOnline" resultType="FsUserOnlineState">
+        SELECT
+            a.*,
+            company_user.nick_name as companyUserName,
+            company.company_name
+        FROM
+            (
+                SELECT
+                    user_id,
+                    nickname,
+                    avatar,
+                    phone,
+                    create_time,
+                    ma_open_id,
+                    mp_open_id,
+                    union_id,
+                    STATUS,
+                    company_id,
+                    company_user_id,
+                    2 AS onlineStatus,
+                    0 AS watchCourseCount,
+                    0 AS partCourseCount,
+                    NOW() AS updateTime
+                FROM
+                    fs_user
+                WHERE
+                    is_del = 0
+                  AND user_id NOT IN ( SELECT DISTINCT user_id FROM fs_course_watch_log WHERE send_type = 1 )
+            ) a
+                LEFT JOIN company_user ON company_user.user_id = a.company_user_id
+                LEFT JOIN company ON company.company_id = a.company_id
+    </select>
+
+    <select id="selectExistWatchLogUser" resultType="FsUser">
+        SELECT
+            count( 1 ),
+            fs_course_watch_log.user_id,
+            create_time
+        FROM
+            fs_course_watch_log
+        WHERE
+            send_type = 1
+        GROUP BY
+            fs_course_watch_log.user_id
+        HAVING
+            count( 1 ) = 1
+           AND create_time >= DATE_SUB( NOW(), INTERVAL 15 MINUTE )
+    </select>
+
+</mapper>

+ 28 - 1
fs-service/src/main/resources/mapper/store/FsUserCourseCountMapper.xml

@@ -54,8 +54,16 @@
         <include refid="selectFsUserCourseCountVo"/>
         where id = #{id}
     </select>
+
     <select id="findByUserId" resultType="com.fs.store.domain.FsUserCourseCount">
-        select * from fs_user_course_count where user_id = ${userId} limit 1
+        select
+            fs_user_course_count.user_id,
+            ifnull ( sum( fs_user_course_count.watch_course_count ), 0 ) AS watch_course_count,
+            ifnull ( sum( fs_user_course_count.miss_course_count ), 0 ) AS miss_course_count,
+            ifnull ( sum( fs_user_course_count.part_course_count ), 0 ) AS part_course_count,
+            Max( fs_user_course_count.last_watch_date ) AS last_watch_date,
+            Max( fs_user_course_count.complete_watch_date) AS complete_watch_date
+        from fs_user_course_count where user_id = ${userId}
     </select>
 
     <insert id="insertFsUserCourseCount" parameterType="FsUserCourseCount">
@@ -248,5 +256,24 @@
         </trim>
     </insert>
 
+    <select id="selectUserLastCount" resultType="com.fs.store.vo.FsUserLastCount">
+        SELECT
+        fs_user_course_count.user_id,
+        fs_user_course_count.`status`,
+        fs_user_course_count.last_watch_date
+        FROM
+        fs_user_course_count
+        INNER JOIN ( SELECT MAX( id ) AS id FROM fs_user_course_count
+        where fs_user_course_count.user_id in
+        <foreach item="userId" collection="userIds" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+        GROUP BY user_id ) t2 ON fs_user_course_count.id = t2.id
+        where fs_user_course_count.user_id in
+        <foreach item="userId" collection="userIds" open="(" separator="," close=")">
+            #{userId}
+        </foreach>
+    </select>
+
 
 </mapper>

+ 4 - 0
fs-user-app/src/main/java/com/fs/app/controller/UserController.java

@@ -1,6 +1,7 @@
 package com.fs.app.controller;
 
 
+import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.FsDoctorRegisterParam;
 import com.fs.app.param.FsUserEditParam;
@@ -151,6 +152,9 @@ public class UserController extends  AppBaseController {
         user.setUserId(Long.parseLong(getUserId()));
         user.setAvatar(param.getAvatar());
         user.setNickName(param.getNickname());
+        if (ObjectUtils.isNotEmpty(param.getIsWeixinAuth())){
+            user.setIsWeixinAuth(param.getIsWeixinAuth());
+        }
         if(userService.updateFsUser(user)>0){
             return R.ok("修改成功");
         }

+ 1 - 0
fs-user-app/src/main/java/com/fs/app/param/FsUserEditParam.java

@@ -21,4 +21,5 @@ public class FsUserEditParam implements Serializable {
     private String avatar;
 
     private Long userId;
+    private Integer isWeixinAuth;
 }