ソースを参照

Merge branch 'master' into bjcz_his_scrm

# Conflicts:
#	fs-admin/src/main/java/com/fs/his/task/Task.java
#	fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
#	fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
#	fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
吴树波 3 週間 前
コミット
32d346225a
83 ファイル変更2180 行追加186 行削除
  1. 19 3
      fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java
  2. 172 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCompanyStatisticsController.java
  3. 17 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  4. 19 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java
  5. 17 0
      fs-admin/src/main/java/com/fs/course/task/CourseStatisticsTask.java
  6. 31 0
      fs-admin/src/main/java/com/fs/course/task/WatchCourseStatistics.java
  7. 17 4
      fs-admin/src/main/java/com/fs/his/task/Task.java
  8. 27 27
      fs-admin/src/main/java/com/fs/task/SgTestController.java
  9. 7 2
      fs-admin/src/main/resources/logback.xml
  10. 2 0
      fs-common/src/main/java/com/fs/common/constant/FsConstants.java
  11. 59 0
      fs-common/src/main/java/com/fs/common/utils/DateUtils.java
  12. 26 0
      fs-common/src/main/java/com/fs/common/utils/model/DateTimeEntity.java
  13. 26 2
      fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java
  14. 13 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseFinishTempController.java
  15. 20 2
      fs-company/src/main/java/com/fs/company/controller/course/qw/FsQwCourseWatchLogController.java
  16. 15 14
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java
  17. 3 1
      fs-company/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java
  18. 7 7
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  19. 6 2
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  20. 1 1
      fs-qw-task/src/main/java/com/fs/app/task/qwTask.java
  21. 2 1
      fs-qw-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java
  22. 29 10
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  23. 3 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java
  24. 9 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseFinishTemp.java
  25. 83 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseCompanyStatistics.java
  26. 12 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseFinishTempMapper.java
  27. 23 6
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  28. 69 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCompanyStatisticsMapper.java
  29. 10 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  30. 2 1
      fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java
  31. 7 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseFinishTempService.java
  32. 1 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchLogService.java
  33. 69 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseCompanyStatisticsService.java
  34. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  35. 9 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java
  36. 2 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  37. 237 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCompanyStatisticsServiceImpl.java
  38. 39 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  39. 21 3
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogStatisticsListVO.java
  40. 7 0
      fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java
  41. 3 1
      fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java
  42. 1 1
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  43. 7 2
      fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatUserMapper.java
  44. 42 2
      fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java
  45. 15 0
      fs-service/src/main/java/com/fs/qw/param/FsUserCourseRedPageParam.java
  46. 3 0
      fs-service/src/main/java/com/fs/qw/service/IQwWatchLogService.java
  47. 25 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java
  48. 8 0
      fs-service/src/main/java/com/fs/qw/vo/QwUserVoiceLogVo.java
  49. 21 1
      fs-service/src/main/java/com/fs/qw/vo/QwWatchLogStatisticsListVO.java
  50. 14 0
      fs-service/src/main/java/com/fs/sop/domain/QwSopTemp.java
  51. 1 1
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java
  52. 1 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java
  53. 1 0
      fs-service/src/main/java/com/fs/statis/dto/AnalysisPreviewQueryDTO.java
  54. 29 0
      fs-service/src/main/java/com/fs/statis/dto/WatchCourseStatisticsResultDTO.java
  55. 7 0
      fs-service/src/main/java/com/fs/statis/service/IStatisticsService.java
  56. 57 0
      fs-service/src/main/java/com/fs/statis/service/impl/StatisticsServiceImpl.java
  57. 22 22
      fs-service/src/main/java/com/fs/tulin/service/impl/TulinInfoSyncLogServiceImpl.java
  58. 104 0
      fs-service/src/main/resources/application-config-dev-czt.yml
  59. 7 1
      fs-service/src/main/resources/application-config-druid-czt.yml
  60. 2 1
      fs-service/src/main/resources/application-config-druid-hdt.yml
  61. 1 1
      fs-service/src/main/resources/application-config-druid-jnsyj.yml
  62. 1 1
      fs-service/src/main/resources/application-config-zkzh.yml
  63. 157 0
      fs-service/src/main/resources/application-dev-czt.yml
  64. 3 3
      fs-service/src/main/resources/application-druid-jnmy-test.yml
  65. 2 2
      fs-service/src/main/resources/application-druid-jnmy.yml
  66. 63 0
      fs-service/src/main/resources/db/20251028-会员每日看课统计.sql
  67. 21 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  68. 248 0
      fs-service/src/main/resources/mapper/course/FsUserCourseCompanyStatisticsMapper.xml
  69. 3 3
      fs-service/src/main/resources/mapper/course/FsUserCoursePeriodMapper.xml
  70. 25 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  71. 1 0
      fs-service/src/main/resources/mapper/his/FsExportTaskMapper.xml
  72. 1 1
      fs-service/src/main/resources/mapper/his/FsPackageMapper.xml
  73. 3 0
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  74. 9 4
      fs-service/src/main/resources/mapper/hisStore/FsStoreCouponScrmMapper.xml
  75. 10 10
      fs-service/src/main/resources/mapper/hisStore/SysOperLogScrmMapper.xml
  76. 2 2
      fs-service/src/main/resources/mapper/qw/QwFriendWelcomeMapper.xml
  77. 1 0
      fs-service/src/main/resources/mapper/qw/QwUserMapper.xml
  78. 4 0
      fs-service/src/main/resources/mapper/sop/SopUserLogsMapper.xml
  79. 13 5
      fs-user-app/src/main/java/com/fs/app/controller/WxH5MpController.java
  80. 3 0
      fs-user-app/src/main/java/com/fs/app/controller/WxUserController.java
  81. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java
  82. 3 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwLoginController.java
  83. 87 34
      fs-user-app/src/main/java/com/fs/framework/aspectj/UserOperationLogAspect.java

+ 19 - 3
fs-admin/src/main/java/com/fs/api/controller/IndexStatisticsController.java

@@ -7,19 +7,17 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.company.constant.CompanyTrafficConstants;
 import com.fs.company.domain.Company;
 import com.fs.company.service.ICompanyService;
-import com.fs.company.service.ICompanyTrafficRecordService;
-import com.fs.company.service.impl.CompanyTrafficRecordServiceImpl;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.hisStore.config.MedicalMallConfig;
 import com.fs.statis.StatisticsRedisConstant;
 import com.fs.statis.dto.*;
 import com.fs.statis.param.StatisticsDeptCompanyParam;
+import com.fs.statis.service.IStatisticsService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysDeptService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
@@ -55,6 +53,9 @@ public class IndexStatisticsController {
 
     @Autowired
     private MedicalMallConfig medicalMallConfig;
+
+    @Autowired
+    private IStatisticsService statisticsService;
     /**
      * 分析概览
      */
@@ -857,4 +858,19 @@ public class IndexStatisticsController {
             }
         }
     }
+
+    /**
+     * @Description: 看课统计按公司
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 16:29
+     */
+    @PostMapping("/getWatchCourseStatisticsData")
+    public R getWatchCourseStatisticsData(@RequestBody AnalysisPreviewQueryDTO param){
+        // 从缓存获取看客统计数据
+        List<WatchCourseStatisticsResultDTO> data=statisticsService.getWatchCourseStatisticsData( param);
+
+        return R.ok().put("data", data);
+    }
 }

+ 172 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCompanyStatisticsController.java

@@ -0,0 +1,172 @@
+package com.fs.course.controller;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+import com.fs.common.exception.ServiceException;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.enums.BusinessType;
+import com.fs.course.domain.FsUserCourseCompanyStatistics;
+import com.fs.course.service.IFsUserCourseCompanyStatisticsService;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.common.core.page.TableDataInfo;
+
+/**
+ * 会员每日看课统计Controller
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+@RestController
+@RequestMapping("/course/statistics")
+public class FsUserCourseCompanyStatisticsController extends BaseController
+{
+    @Autowired
+    private IFsUserCourseCompanyStatisticsService fsUserCourseCompanyStatisticsService;
+
+    /**
+     * 查询会员每日看课统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        if (fsUserCourseCompanyStatistics.getBeginTime() == null || fsUserCourseCompanyStatistics.getEndTime() == null) {
+            throw new ServiceException("请选择开始时间和结束时间!");
+        }
+
+        startPage();
+        List<FsUserCourseCompanyStatistics> list =
+                fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsTotal(fsUserCourseCompanyStatistics);
+        Optional.ofNullable(list).orElse(Collections.emptyList())
+                .forEach(item -> {
+                    // 完播率
+                    Long watchCount = item.getWatchCount() != null ? item.getWatchCount() : 0L;
+                    Long completeWatchCount = item.getCompleteWatchCount() != null ? item.getCompleteWatchCount() : 0L;
+                    if (watchCount > 0) {
+                        BigDecimal rate = BigDecimal.valueOf(completeWatchCount)
+                                .multiply(BigDecimal.valueOf(100))
+                                .divide(BigDecimal.valueOf(watchCount), 2, RoundingMode.HALF_UP);
+                        item.setCompleteRate(rate.longValue());
+                    } else {
+                        item.setCompleteRate(0L);
+                    }
+
+                    // 正确率
+                    Long answerCount = item.getAnswerCount() != null ? item.getAnswerCount() : 0L;
+                    Long correctCount = item.getCorrectCount() != null ? item.getCorrectCount() : 0L;
+                    if (answerCount > 0) {
+                        BigDecimal rate = BigDecimal.valueOf(correctCount)
+                                .multiply(BigDecimal.valueOf(100))
+                                .divide(BigDecimal.valueOf(answerCount), 2, RoundingMode.HALF_UP);
+                        item.setCorrectRate(rate.longValue());
+                    } else {
+                        item.setCorrectRate(0L);
+                    }
+                });
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出会员每日看课统计列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:export')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        List<FsUserCourseCompanyStatistics> list =
+                fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsTotal(fsUserCourseCompanyStatistics);
+
+        Optional.ofNullable(list).orElse(Collections.emptyList())
+                .forEach(item -> {
+                    // 计算完播率 (完播次数 / 观看次数 * 100)
+                    item.setCompleteRate(
+                            Optional.ofNullable(item.getWatchCount())
+                                    .filter(watchCount -> watchCount > 0)
+                                    .map(watchCount -> BigDecimal.valueOf(
+                                                    Optional.ofNullable(item.getCompleteWatchCount()).orElse(0L))
+                                            .multiply(BigDecimal.valueOf(100))
+                                            .divide(BigDecimal.valueOf(watchCount), 2, RoundingMode.HALF_UP)
+                                            .longValue()
+                                    )
+                                    .orElse(0L)
+                    );
+
+                    // 计算正确率 (正确人次 / 答题人次 * 100)
+                    item.setCorrectRate(
+                            Optional.ofNullable(item.getAnswerCount())
+                                    .filter(answerCount -> answerCount > 0)
+                                    .map(answerCount -> BigDecimal.valueOf(
+                                                    Optional.ofNullable(item.getCorrectCount()).orElse(0L))
+                                            .multiply(BigDecimal.valueOf(100))
+                                            .divide(BigDecimal.valueOf(answerCount), 2, RoundingMode.HALF_UP)
+                                            .longValue()
+                                    )
+                                    .orElse(0L)
+                    );
+                });
+
+        ExcelUtil<FsUserCourseCompanyStatistics> util = new ExcelUtil<FsUserCourseCompanyStatistics>(FsUserCourseCompanyStatistics.class);
+        return util.exportExcel(list, "会员每日看课统计数据");
+    }
+
+    /**
+     * 获取会员每日看课统计详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:query')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        return AjaxResult.success(fsUserCourseCompanyStatisticsService.selectFsUserCourseCompanyStatisticsById(id));
+    }
+
+    /**
+     * 新增会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:add')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.insertFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics));
+    }
+
+    /**
+     * 修改会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:edit')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.updateFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics));
+    }
+
+    /**
+     * 删除会员每日看课统计
+     */
+    @PreAuthorize("@ss.hasPermi('course:statistics:remove')")
+    @Log(title = "会员每日看课统计", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids)
+    {
+        return toAjax(fsUserCourseCompanyStatisticsService.deleteFsUserCourseCompanyStatisticsByIds(ids));
+    }
+}

+ 17 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -8,10 +8,12 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.utils.ServletUtils;
 import com.fs.course.config.CourseConfig;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseListPVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.system.service.ISysConfigService;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -45,6 +47,9 @@ public class FsUserCourseController extends BaseController
     @Autowired
     private IFsUserCourseService fsUserCourseService;
 
+    @Autowired
+    private IFsUserCourseVideoService courseVideoService;
+
     @Autowired
     private RedisCacheUtil redisCacheUtil;
 
@@ -207,6 +212,18 @@ public class FsUserCourseController extends BaseController
         return toAjax(1);
     }
 
+    /**
+     * 统一修改课程红包
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourse:editRedPage')")
+    @Log(title = "修改课程红包", businessType = BusinessType.UPDATE)
+    @PostMapping("/editRedPage")
+    public AjaxResult editRedPage(@RequestBody FsUserCourseRedPageParam redPageParam)
+    {
+        courseVideoService.updateFsUserCourseRedPage(redPageParam);
+        return toAjax(1);
+    }
+
     /**
      * 修改课程
      */

+ 19 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseVideoController.java

@@ -28,6 +28,7 @@ import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.vo.FsUserCourseVideoChooseVO;
 import com.fs.framework.web.service.TokenService;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.vo.SortDayVo;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
@@ -232,6 +233,24 @@ public class FsUserCourseVideoController extends BaseController
         return getDataTable(list);
     }
 
+    @GetMapping("/getVideoListByCourseIdAll")
+    public TableDataInfo getVideoListByCourseIdAll(Long courseId)
+    {
+
+        FsUserCourseVideo courseVideo=new FsUserCourseVideo();
+        courseVideo.setCourseId(courseId);
+        courseVideo.setIsDel(0);
+        List<FsUserCourseVideo> list = fsUserCourseVideoService.selectFsUserCourseVideoList(courseVideo);
+        return getDataTable(list);
+    }
+
+    @PostMapping("/sortCourseVideo")
+    public AjaxResult sortCourseVideo(@RequestBody List<FsUserCourseVideo> list){
+        fsUserCourseVideoService.sortCourseVideo(list);
+        return toAjax(1);
+    }
+
+
     @GetMapping("/getSort/{courseId}")
     public R remove(@PathVariable("courseId") Long courseId)
     {

+ 17 - 0
fs-admin/src/main/java/com/fs/course/task/CourseStatisticsTask.java

@@ -0,0 +1,17 @@
+package com.fs.course.task;
+
+import com.fs.course.service.IFsUserCourseCompanyStatisticsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component("courseStatisticsTask")
+public class CourseStatisticsTask {
+    @Autowired
+    private IFsUserCourseCompanyStatisticsService fsUserCourseCompanyStatisticsService;
+
+    public void saveCourseStatisticsTask(Integer status,Integer day) {
+        fsUserCourseCompanyStatisticsService.courseDailyStatisticsTask(status,day);
+
+    }
+
+}

+ 31 - 0
fs-admin/src/main/java/com/fs/course/task/WatchCourseStatistics.java

@@ -0,0 +1,31 @@
+package com.fs.course.task;
+
+import com.fs.statis.service.IStatisticsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * @description: 看客相关统计定时任务
+ * @author: Xgb
+ * @createDate: 2025/10/27
+ * @version: 1.0
+ */
+@Component("watchCourseStatistics")
+public class WatchCourseStatistics {
+
+    @Autowired
+    private IStatisticsService statisticsService;
+
+    /**
+     * @Description: 统计统计按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;各公司的观看人数和完播人数,
+     * 各公司的观看人数和完播人数, 存到redis中,定时任务每15分钟执行一次
+     *
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 11:47
+     */
+    public void watchCourseStatisticsGroupByCompany() {
+        statisticsService.watchCourseStatisticsGroupByCompany();
+    }
+}

+ 17 - 4
fs-admin/src/main/java/com/fs/his/task/Task.java

@@ -3,6 +3,9 @@ package com.fs.his.task;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.service.impl.SmsServiceImpl;
 import com.fs.common.utils.DateUtils;
@@ -40,10 +43,7 @@ import com.fs.fastgptApi.vo.AudioVO;
 import com.fs.gtPush.mapper.PushLogMapper;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.config.StoreConfig;
-import com.fs.his.domain.FsInquiryOrder;
-import com.fs.his.domain.FsStoreAfterSales;
-import com.fs.his.domain.FsStoreOrder;
-import com.fs.his.domain.FsUser;
+import com.fs.his.domain.*;
 import com.fs.his.dto.FsInquiryOrderPatientDTO;
 import com.fs.his.enums.FsStoreOrderLogEnum;
 import com.fs.his.enums.FsStoreOrderStatusEnum;
@@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDate;
@@ -197,6 +198,8 @@ public class Task {
 
     @Autowired
     private QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper;
+    @Autowired
+    private FsUserOperationLogMapper fsUserOperationLogMapper;
 
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
 
@@ -1526,5 +1529,15 @@ public class Task {
         fsStoreOrderScrmService.refreshOrderSettlementStatus();
     }
 
+    //定时删除行为轨迹记录 (数据量太大 默认保留一天的)
+    @Scheduled(cron = "0 0 1 * * ?")
+    //@Scheduled(cron = "0 * * * * ?") //测试每分钟执行一次
+    public void deleteUserOperationLog(){
+        LambdaQueryWrapper<FsUserOperationLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.lt(FsUserOperationLog::getCreateTime, DateUtils.addDays(new Date(), -1));
+        int deleteCount  = fsUserOperationLogMapper.delete(wrapper);
+        log.info("定时删除行为轨迹记录 {} 条", deleteCount);
+    }
+
 
 }

+ 27 - 27
fs-admin/src/main/java/com/fs/task/SgTestController.java

@@ -1,27 +1,27 @@
-//package com.fs.task;
-//
-//import org.springframework.web.bind.annotation.RequestMapping;
-//import org.springframework.web.bind.annotation.RestController;
-//
-//import javax.annotation.Resource;
-//
-///**
-// * @description:
-// * @author: Guos
-// * @time: 2025/10/23 下午2:18
-// */
-//@RestController
-//@RequestMapping("/sg/test")
-//public class SgTestController {
-//
-//    @Resource
-//    private SyncTuLinStudentInfoTask syncTuLinStudentInfoTask;
-//
-//
-//    @RequestMapping("/execute")
-//    public void execute(){
-//        syncTuLinStudentInfoTask.execute();
-//    }
-//
-//
-//}
+package com.fs.task;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/23 下午2:18
+ */
+@RestController
+@RequestMapping("/sg/test")
+public class SgTestController {
+
+    @Resource
+    private SyncTuLinStudentInfoTask syncTuLinStudentInfoTask;
+
+
+    @RequestMapping("/execute")
+    public void execute(){
+        syncTuLinStudentInfoTask.execute();
+    }
+
+
+}

+ 7 - 2
fs-admin/src/main/resources/logback.xml

@@ -72,11 +72,16 @@
     </appender>
 
 	<!-- 系统模块日志级别控制  -->
-	<logger name="com.fs" level="info" />
+	<logger name="com.fs" level="debug" />
 	<!-- Spring日志级别控制  -->
 	<logger name="org.springframework" level="warn" />
 
-	<root level="info">
+    <!-- log4j2.xml -->
+    <Logger name="com.fs.his.mapper" level="debug"/>
+    <Logger name="org.apache.ibatis" level="debug"/>
+
+
+    <root level="info">
 		<appender-ref ref="console" />
 	</root>
 

+ 2 - 0
fs-common/src/main/java/com/fs/common/constant/FsConstants.java

@@ -16,4 +16,6 @@ public interface FsConstants {
     String COMPANY_MONEY_KEY = "company:money:";
     // 公司余额redis 锁
     String COMPANY_MONEY_LOCK = "company_money_lock:";
+    // 看客统计  按公司分组 按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;
+    String WATCH_COURSE_STATISTICS_GROUP_COMPANY = "watch_course_statistics:group_company:";
 }

+ 59 - 0
fs-common/src/main/java/com/fs/common/utils/DateUtils.java

@@ -6,8 +6,12 @@ import java.text.SimpleDateFormat;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalAdjusters;
 import java.util.Calendar;
 import java.util.Date;
+import java.util.Map;
+
+import com.fs.common.utils.model.DateTimeEntity;
 import org.apache.commons.lang3.time.DateFormatUtils;
 
 /**
@@ -296,4 +300,59 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
         return calendar.getTimeInMillis();
     }
 
+
+    /**
+     * @Description: 根据类型返回开始时间和结束时间 最大时间是今天23:59:59 如本月 是1号00:00:00到今天的23:59:59
+     * @Param:  type: 0-今天 1-昨天 2-本周 3-本月 4-上月
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 14:09
+     */
+    public static DateTimeEntity getBetweenTime(int type){
+        // 根据type计算出时间范围
+        String startDate = "";
+        String endDate = "";
+
+        LocalDateTime now = LocalDateTime.now();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalTime startOfDayTime = LocalTime.MIN;
+        LocalTime endOfDayTime = LocalTime.of(23, 59, 59);
+        if(0 == type){
+            LocalDateTime startOfDay = now.with(startOfDayTime);
+            LocalDateTime endOfDay = now.with(endOfDayTime);
+            startDate = startOfDay.format(formatter);
+            endDate = endOfDay.format(formatter);
+        } else if(1 == type){
+            LocalDateTime yesterday = now.minusDays(1);
+            LocalDateTime startOfYesterday = yesterday.with(startOfDayTime);
+            LocalDateTime endOfYesterday = yesterday.with(endOfDayTime);
+            startDate = startOfYesterday.format(formatter);
+            endDate = endOfYesterday.format(formatter);
+        } else if(2 == type) {
+            LocalDateTime startOfWeek = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
+            LocalDateTime startOfStartOfWeek = startOfWeek.with(startOfDayTime);
+            LocalDateTime endOfToday = now.with(endOfDayTime);
+            startDate = startOfStartOfWeek.format(formatter);
+            endDate = endOfToday.format(formatter);
+        } else if(3 == type) {
+            LocalDateTime startOfMonth = now.withDayOfMonth(1);
+            LocalDateTime startOfStartOfMonth = startOfMonth.with(startOfDayTime);
+            LocalDateTime endOfToday = now.with(endOfDayTime);
+            startDate = startOfStartOfMonth.format(formatter);
+            endDate = endOfToday.format(formatter);
+        } else if(4 == type) {
+            LocalDateTime firstDayOfPreviousMonth = now.minusMonths(1).withDayOfMonth(1);
+            LocalDateTime lastDayOfPreviousMonth = now.withDayOfMonth(1).minusDays(1);
+
+            LocalDateTime startOfPrevMonthStart = firstDayOfPreviousMonth.with(startOfDayTime);
+            LocalDateTime endOfPrevMonthEnd = lastDayOfPreviousMonth.with(endOfDayTime);
+
+            startDate = startOfPrevMonthStart.format(formatter);
+            endDate = endOfPrevMonthEnd.format(formatter);
+        }
+
+        return new DateTimeEntity(startDate, endDate);
+    }
+
 }

+ 26 - 0
fs-common/src/main/java/com/fs/common/utils/model/DateTimeEntity.java

@@ -0,0 +1,26 @@
+package com.fs.common.utils.model;
+
+import lombok.Data;
+
+/**
+ * @description: 时间对象
+ * @author: Xgb
+ * @createDate: 2025/10/27
+ * @version: 1.0
+ */
+
+@Data
+public class DateTimeEntity {
+
+    private String startTime;
+
+    private String endTime;
+
+    public DateTimeEntity(String startTime, String endTime) {
+        this.startTime = startTime;
+        this.endTime = endTime;
+    }
+
+    public DateTimeEntity() {
+    }
+}

+ 26 - 2
fs-company/src/main/java/com/fs/company/controller/company/IndexStatisticsController.java

@@ -7,8 +7,7 @@ import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
 import com.fs.statis.StatisticsRedisConstant;
 import com.fs.statis.dto.*;
-import com.fs.system.domain.SysConfig;
-import com.fs.system.service.ISysConfigService;
+import com.fs.statis.service.IStatisticsService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -28,6 +27,10 @@ public class IndexStatisticsController {
 
     @Autowired
     private TokenService tokenService;
+
+    @Autowired
+    private IStatisticsService statisticsService;
+
     /**
      * 分析概览
      */
@@ -239,4 +242,25 @@ public class IndexStatisticsController {
         R result = redisCache.getCacheObject(String.format("%s:%d",StatisticsRedisConstant.THIS_MONTH_RECV_COUNT,companyId));
         return result;
     }
+
+    /**
+     * @Description: 看课统计按公司
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 16:29
+     */
+    @PostMapping("/getWatchCourseStatisticsData")
+    public R getWatchCourseStatisticsData(@RequestBody AnalysisPreviewQueryDTO param){
+        // 获取公司ID
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long companyId = loginUser.getCompany().getCompanyId();
+        if(param.getCompanyId()==null){
+            param.setCompanyId(companyId);
+        }
+        // 从缓存获取看客统计数据
+        List<WatchCourseStatisticsResultDTO> data=statisticsService.getWatchCourseStatisticsData(param);
+
+        return R.ok().put("data", data);
+    }
 }

+ 13 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseFinishTempController.java

@@ -1,10 +1,12 @@
 package com.fs.company.controller.course;
 
+import com.alibaba.fastjson.JSON;
 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.DateUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.domain.FsCourseFinishTemp;
 import com.fs.course.service.IFsCourseFinishTempService;
@@ -104,4 +106,15 @@ public class FsCourseFinishTempController extends BaseController
     {
         return toAjax(fsCourseFinishTempService.deleteFsCourseFinishTempByIds(ids));
     }
+
+
+    @Log(title = "完课模板", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateStatusBatch")
+    public AjaxResult updateStatusBatch(@RequestBody FsCourseFinishTemp fsCourseFinishTemp)
+    {
+        fsCourseFinishTemp.setUpdateTime(DateUtils.getNowDate());
+
+        return toAjax(fsCourseFinishTempService.updateFsCourseFinishTempBatch(fsCourseFinishTemp));
+    }
+
 }

+ 20 - 2
fs-company/src/main/java/com/fs/company/controller/course/qw/FsQwCourseWatchLogController.java

@@ -19,6 +19,7 @@ import com.fs.framework.service.TokenService;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
+import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 import com.fs.sop.mapper.SopUserLogsMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -81,6 +82,21 @@ public class FsQwCourseWatchLogController extends BaseController
         List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
         return getDataTable(list);
     }
+    @PreAuthorize("@ss.hasPermi('course:courseWatchLog:statisticsList')")
+    @GetMapping("/statisticsExport")
+    public AjaxResult statisticsExport(FsCourseWatchLogStatisticsListParam param)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId( loginUser.getCompany().getCompanyId());
+        if (param.getSTime()==null||param.getETime()==null){
+            return AjaxResult.error("请选择时间");
+        }
+        param.setSendType(2);
+        List<FsCourseWatchLogStatisticsListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogStatisticsListVO(param);
+        ExcelUtil<FsCourseWatchLogStatisticsListVO> util = new ExcelUtil<FsCourseWatchLogStatisticsListVO>(FsCourseWatchLogStatisticsListVO.class);
+        return util.exportExcel(list, "企微看课统计");
+
+    }
 
     @GetMapping("/qwWatchLogStatisticsList")
     public TableDataInfo qwWatchLogStatisticsList(QwWatchLogStatisticsListParam param)
@@ -196,8 +212,10 @@ public class FsQwCourseWatchLogController extends BaseController
     {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId( loginUser.getCompany().getCompanyId());
-        List<FsCourseWatchLogListVO> list = fsCourseWatchLogService.selectFsCourseWatchLogListVO(param);
-        ExcelUtil<FsCourseWatchLogListVO> util = new ExcelUtil<FsCourseWatchLogListVO>(FsCourseWatchLogListVO.class);
+
+        List<QwWatchLogStatisticsListVO> list = qwWatchLogService.selectQwWatchLogStatisticsListVOExport(param);
+
+        ExcelUtil<QwWatchLogStatisticsListVO> util = new ExcelUtil<QwWatchLogStatisticsListVO>(QwWatchLogStatisticsListVO.class);
         return util.exportExcel(list, "短链课程看课记录数据");
     }
 

+ 15 - 14
fs-company/src/main/java/com/fs/company/controller/qw/QwSopTempController.java

@@ -63,6 +63,21 @@ public class QwSopTempController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 导出sop模板列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sopTemp:export') or @ss.hasPermi('qw:sopTemp:myExport') or @ss.hasPermi('qw:sopTemp:deptExport')")
+    @Log(title = "sop模板", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(QwSopTemp qwSopTemp)
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        qwSopTemp.setCompanyId(loginUser.getCompany().getCompanyId());
+        List<QwSopTemp> list = qwSopTempService.selectQwSopTempListNew(qwSopTemp);
+        ExcelUtil<QwSopTemp> util = new ExcelUtil<QwSopTemp>(QwSopTemp.class);
+        return util.exportExcel(list, "sop模板数据");
+    }
+
     /**
      * 查询我创建的sop模板列表
      */
@@ -111,20 +126,6 @@ public class QwSopTempController extends BaseController
         return getDataTable(list);
     }
 
-
-    /**
-     * 导出sop模板列表
-     */
-    @PreAuthorize("@ss.hasPermi('qw:sopTemp:export') or @ss.hasPermi('qw:sopTemp:myExport') or @ss.hasPermi('qw:sopTemp:deptExport')")
-    @Log(title = "sop模板", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(QwSopTemp qwSopTemp)
-    {
-        List<QwSopTemp> list = qwSopTempService.selectQwSopTempList(qwSopTemp);
-        ExcelUtil<QwSopTemp> util = new ExcelUtil<QwSopTemp>(QwSopTemp.class);
-        return util.exportExcel(list, "sop模板数据");
-    }
-
     /**
      * 获取sop模板详细信息
      */

+ 3 - 1
fs-company/src/main/java/com/fs/framework/service/UserDetailsServiceImpl.java

@@ -9,6 +9,7 @@ import com.fs.company.domain.CompanyUser;
 import com.fs.company.service.ICompanyService;
 import com.fs.company.service.ICompanyUserService;
 import com.fs.framework.security.LoginUser;
+import com.fs.framework.security.SecurityUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -20,7 +21,7 @@ import org.springframework.stereotype.Service;
 /**
  * 用户验证处理
  *
- 
+
  */
 @Service
 public class UserDetailsServiceImpl implements UserDetailsService
@@ -45,6 +46,7 @@ public class UserDetailsServiceImpl implements UserDetailsService
 
 
         CompanyUser user = userService.selectUserByUserName(username);
+
         if (StringUtils.isNull(user))
         {
             log.info("登录用户:{} 不存在.", username);

+ 7 - 7
fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java

@@ -297,18 +297,18 @@ public class IpadSendServer {
 
         if (qwSopLogs.getSendType() != 12 && noSop) {
             // 客户的信息
-            QwExternalContactHParam contactHParam = new QwExternalContactHParam();
-            contactHParam.setUserId(qwUser.getQwUserId().trim());
-            contactHParam.setExternalUserId(qwSopLogs.getExternalUserId().trim());
-            contactHParam.setCorpId(qwUser.getCorpId().trim());
+//            QwExternalContactHParam contactHParam = new QwExternalContactHParam();
+//            contactHParam.setUserId(qwUser.getQwUserId().trim());
+//            contactHParam.setExternalUserId(qwSopLogs.getExternalUserId().trim());
+//            contactHParam.setCorpId(qwUser.getCorpId().trim());
             Integer courseType = setting.getCourseType();
             if (setting.getType() == 2 && courseType != 0) {// 课程消息,进行复杂的条件判断
-                log.info("企微查询:{}", contactHParam);
-                Long qwExternalContactId = qwExternalContactMapper.getQwExternalContactId(contactHParam);
+//                log.debug("企微查询:{}", contactHParam);
+//                Long qwExternalContactId = qwExternalContactMapper.getQwExternalContactId(contactHParam);
                 FsCourseWatchLog watchLog = watchLogService.getWatchCourseLogVideoBySop(
                         setting.getVideoId().longValue(),
                         String.valueOf(qwUser.getId()),
-                        qwExternalContactId
+                        qwSopLogs.getExternalId()
                 );
                 log.info("ID:{}-看课记录参数:videoID:{}, qwUserID:{}, extID:{}", qwSopLogs.getId(), setting.getVideoId().longValue(), qwUser.getId(), qwExternalContactId);
                 log.info("ID:{}-看课记录:{}", qwSopLogs.getId(), watchLog);

+ 6 - 2
fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java

@@ -259,7 +259,7 @@ public class CommonController {
     }
 
     @GetMapping("/test")
-    public R test(String time) throws Exception {
+    public R test(String time, String sopId) throws Exception {
         log.info("进入sop任务");
 //        LocalDateTime currentTime = DateUtil.parseLocalDateTime(time);
 //        // 计算下一个整点时间
@@ -268,7 +268,11 @@ public class CommonController {
 //        // 打印日志,确认时间
 //        log.info("任务实际执行时间: {}", currentTime);
 //        log.info("传递给任务的时间参数: {}", nextHourTime);
-        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time));
+        List<String> sopidList = new ArrayList<>();
+        if(StringUtils.isNotEmpty(sopId)){
+            sopidList = Arrays.asList(sopId.split(","));
+        }
+        sopLogsTaskService.selectSopUserLogsListByTime(DateUtil.parseLocalDateTime(time), sopidList);
         return R.ok();
     }
     @GetMapping("/testWx")

+ 1 - 1
fs-qw-task/src/main/java/com/fs/app/task/qwTask.java

@@ -117,7 +117,7 @@ public class qwTask {
         log.info("任务实际执行时间: {}", currentTime);
 
         // 调用服务方法处理SOP用户日志
-        sopLogsTaskService.selectSopUserLogsListByTime(currentTime);
+        sopLogsTaskService.selectSopUserLogsListByTime(currentTime, null);
     }
 
     /**

+ 2 - 1
fs-qw-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java

@@ -1,10 +1,11 @@
 package com.fs.app.taskService;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 public interface SopLogsTaskService {
 
-    public void selectSopUserLogsListByTime(LocalDateTime currentTime) throws Exception;
+    public void selectSopUserLogsListByTime(LocalDateTime currentTime, List<String> sopidList) throws Exception;
 
 
     /**

+ 29 - 10
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -5,8 +5,6 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.fs.app.taskService.SopLogsTaskService;
-import com.fs.common.core.domain.R;
-import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.domain.Company;
@@ -19,7 +17,6 @@ import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.*;
 import com.fs.course.mapper.*;
-import com.fs.course.param.FsCourseLinkCreateParam;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
 import com.fs.qw.domain.*;
@@ -286,7 +283,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
     }
 
     @Override
-    public void selectSopUserLogsListByTime(LocalDateTime currentTime) throws Exception {
+    public void selectSopUserLogsListByTime(LocalDateTime currentTime, List<String> sopidList) throws Exception {
         long startTimeMillis = System.currentTimeMillis();
         log.info("====== 开始选择和处理 SOP 用户日志 ======");
 
@@ -296,7 +293,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             config = cachedCourseConfig;
         }
 
-        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTime();
+        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTime(sopidList);
         if (sopUserLogsVos.isEmpty()) {
             log.info("没有需要处理的 SOP 用户日志。");
             return;
@@ -1002,8 +999,22 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                     break;
                 //小程序单独
                 case "4":
-
-                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    if (isGroupChat) {
+                        try {
+                            groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
+                                Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
+                                GroupUserExternalVo vo = userMap.get(groupChat.getOwner());
+                                if (vo != null && vo.getId() != null) {
+                                    sopLogs.setFsUserId(vo.getFsUserId());
+                                    addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, vo.getId().toString(), logVo);
+                                }
+                            });
+                        } catch (Exception e) {
+                            log.error("群聊创建看课记录失败!", e);
+                        }
+                    } else {
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId,logVo);
+                    }
 
                     String sortLink = createLinkByMiniApp(setting, logVo, sendTime, courseId, videoId,
                             qwUserId, companyUserId, companyId, externalId,isOfficial,sopLogs.getFsUserId(), isGroupChat ? groupChat.getChatId() : null);
@@ -1075,6 +1086,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
             }
 
         }
+        clonedContent.getSetting().stream().filter(e -> "1".equals(e.getIsBindUrl())).forEach(e -> {
+            e.setIsBindUrl("0");
+            e.setLinkDescribe(null);
+            e.setLinkUrl(null);
+            e.setLinkImageUrl(null);
+        });
         sopLogs.setContentJson(JSON.toJSONString(clonedContent));
         enqueueQwSopLogs(sopLogs);
     }
@@ -1364,10 +1381,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         link.setCompanyId(Long.parseLong(companyId));
         link.setQwUserId(Long.parseLong(qwUserId));
         link.setCompanyUserId(Long.parseLong(companyUserId));
-        link.setVideoId(videoId.longValue());
+        link.setVideoId(videoId);
         link.setCorpId(logVo.getCorpId());
-        link.setCourseId(courseId.longValue());
-        link.setQwExternalId(Long.parseLong(externalId));
+        link.setCourseId(courseId);
+        if(StringUtils.isEmpty(chatId)){
+            link.setQwExternalId(Long.parseLong(externalId));
+        }
         link.setProjectCode(cloudHostProper.getProjectCode());
         link.setChatId(chatId);
 

+ 3 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyServiceImpl.java

@@ -38,6 +38,7 @@ import com.fs.store.config.CompanyMenuConfig;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
+import com.github.pagehelper.PageHelper;
 import com.google.gson.Gson;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ObjectUtils;
@@ -840,7 +841,8 @@ public class CompanyServiceImpl implements ICompanyService
     @Override
     public List<DeptDataVO> getDeptData(Long companyId, Long currentCompanyUserId, Long currentDeptId) {
         List<DeptDataVO> result = new ArrayList<>();
-
+        // 线程中可能会残留的分页信息,这里清除,解决报错
+        PageHelper.clearPage();
         Long isAdmin = companyUserRoleMapper.companyUserIsAdmin(currentCompanyUserId);
         logger.info("当前用户 {} 是公司admin 返回公司所有部门树",currentDeptId);
         if(isAdmin!=null){

+ 9 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseFinishTemp.java

@@ -1,9 +1,12 @@
 package com.fs.course.domain;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * 完课模板对象 fs_course_finish_temp
  *
@@ -57,4 +60,10 @@ public class FsCourseFinishTemp extends BaseEntity
 
     @Excel(name = "全选销售标志")
     private Integer isAllCompanyUser;
+
+    /**
+     * 用于批量更新状态
+     */
+    @TableField(exist = false)
+    private List<Long> ids;
 }

+ 83 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseCompanyStatistics.java

@@ -0,0 +1,83 @@
+package com.fs.course.domain;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 会员每日看课统计对象 fs_user_course_company_statistics
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class FsUserCourseCompanyStatistics extends BaseEntity{
+
+    /** 主键ID */
+    private Long id;
+
+    /** 项目ID */
+//    @Excel(name = "项目ID")
+    private Long projectId;
+
+    /** 完播次数(人次) */
+    @Excel(name = "完播次数", readConverterExp = "人=次")
+    private Long completeWatchCount;
+
+    /** 观看次数(人次) */
+    @Excel(name = "观看次数", readConverterExp = "人=次")
+    private Long watchCount;
+
+    /** 完播率(完播次数/观看次数) */
+    @Excel(name = "完播率", readConverterExp = "完=播次数/观看次数")
+    private Long completeRate;
+
+    /** 答题人次 */
+    @Excel(name = "答题人次")
+    private Long answerCount;
+
+    /** 正确人次 */
+    @Excel(name = "正确人次")
+    private Long correctCount;
+
+    /** 正确率(正确人次/答题人次) */
+    @Excel(name = "正确率", readConverterExp = "正=确人次/答题人次")
+    private Long correctRate;
+
+    /** 领取次数 */
+    @Excel(name = "领取次数")
+    private Long receiveCount;
+
+    /** 领取金额(元) */
+    @Excel(name = "领取金额", readConverterExp = "元=")
+    private BigDecimal receiveAmount;
+
+    /** 会员数量 */
+    @Excel(name = "会员数量")
+    private Long userCount;
+
+    /** 会员黑名单数量 */
+    @Excel(name = "会员黑名单数量")
+    private Long userBlacklistCount;
+
+    /** 公司ID */
+//    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 公司名称 */
+    @Excel(name = "公司名称")
+    private String companyName;
+
+    /** 统计日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "统计日期", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date createDate;
+
+
+}

+ 12 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseFinishTempMapper.java

@@ -6,6 +6,7 @@ import com.fs.course.vo.FsCourseFinishTempListVO;
 import com.fs.course.vo.FsCourseFinishTempVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 
 /**
  * 完课模板Mapper接口
@@ -111,4 +112,15 @@ public interface FsCourseFinishTempMapper
     public FsCourseFinishTempVO selectFsCourseFinishTempByIdVO(Long id);
 
     void deleteByParentIds(@Param("ids") Long[] ids);
+
+    @Update({"<script> " +
+            " update fs_course_finish_temp " +
+            " set status = #{data.status} ," +
+            " update_time = #{data.updateTime} " +
+            " where id in " +
+            " <foreach collection='data.ids' item='id' open='(' separator=',' close=')'>" +
+              " #{id} " +
+            " </foreach> " +
+            "</script>"})
+    int updateFsCourseFinishTempBatch(@Param("data") FsCourseFinishTemp fsCourseFinishTemp);
 }

+ 23 - 6
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -5,10 +5,10 @@ import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.dto.WatchLogDTO;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
-import com.fs.im.dto.OpenImBatchResponseDataDTO;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.sop.vo.QwRatingVO;
+import com.fs.statis.dto.WatchCourseStatisticsResultDTO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -218,6 +218,12 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     List<FsCourseWatchLog> selectFsCourseWatchLogFinish();
 
     @Select({"<script> " +
+            "select t.* " +
+            "<if test= 'sendType != 1 '> " +
+            " ,concat(round(if(t.send_number=0,0,(t.on_line_num/t.send_number)*100),2),'%') on_line_rate" +
+            " ,concat(round(if(t.send_number=0,0,(t.type2/t.send_number)*100),2),'%') finished_rate" +
+            "</if> " +
+            "from (" +
             "SELECT \n" +
             "o.video_id,o.company_id,o.qw_user_id,DATE(o.create_time) create_time," +
             "<if test= 'sendType != 1 '> " +
@@ -235,10 +241,17 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             "  SUM(CASE WHEN o.log_type = '1' THEN 1 ELSE 0 END) +\n" +
             "  SUM(CASE WHEN o.log_type = '2' THEN 1 ELSE 0 END) +\n" +
             "  SUM(CASE WHEN o.log_type = '4' THEN 1 ELSE 0 END)\n" +
-            ") AS on_line_num\n" +
-            "FROM fs_course_watch_log o\n" +
+            ") AS on_line_num " +
+            "<if test= 'sendType != 1 '> " +
+            " ,count(o.log_id) send_number" +
+            " ,sum(if((o.user_id is not null or o.user_id>0) and o.log_type=3,1,0)) is_user_wait_number" +
+            " ,sum(if((o.user_id is null or o.user_id=0) and o.log_type=3,1,0)) no_user_wait_number" +
+            " ,sum(ifnull(fcr.amount,0)) red_amount" +
+            "</if> " +
+            "FROM fs_course_watch_log o " +
             "<if test= 'sendType != 1 '> " +
-            " LEFT JOIN qw_user qu on qu.id=o.qw_user_id\n" +
+            " LEFT JOIN qw_user qu on qu.id=o.qw_user_id " +
+            " LEFT JOIN fs_course_red_packet_log fcr on o.user_id = fcr.user_id and fcr.video_id = o.video_id" +
             "</if>\n" +
             "LEFT JOIN fs_user_course_video v on v.video_id=o.video_id \n" +
             "LEFT JOIN fs_user_course uc on uc.course_id=v.course_id\n" +
@@ -275,7 +288,8 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
             " o.company_user_id," +
             "</if>\n" +
             "DATE(o.create_time)\n" +
-            "ORDER BY o.video_id ,DATE(o.create_time) \n"+
+            "ORDER BY o.video_id ,DATE(o.create_time) " +
+            ") t \n"+
             "</script>"})
     List<FsCourseWatchLogStatisticsListVO> selectFsCourseWatchLogStatisticsListVO(FsCourseWatchLogStatisticsListParam param);
 
@@ -447,7 +461,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     List<WatchLogDTO> selectFsCourseWatchLog30DayByExtId(@Param("extId") Long extId);
 
     @Select("SELECT * FROM fs_course_watch_log " +
-            "WHERE log_type = 2 AND send_finish_msg = 0 " +
+            "WHERE log_type = 2 AND send_finish_msg = 0 and send_type = 2 " +
             "AND send_type = 2 and finish_time >= #{startDate} AND finish_time < #{endDate} and log_id > #{maxId} order by log_id asc  " +
             "LIMIT #{limit}")
     List<FsCourseWatchLog> selectFsCourseWatchLogFinishBatchByDate(
@@ -539,4 +553,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
      * 看课统计
      * */
     List<FsCourseWatchLogStatisticsListVO> selectQwFsCourseWatchLogStatisticsListVO(QwSidebarStatsParam param);
+
+    // 统计当天各公司的观看人数和完播人数, 存到redis中,定时任务每 ? 分钟执行一次
+    List<WatchCourseStatisticsResultDTO> watchCourseStatisticsGroupByCompany(@Param("params") Map<String, Object> params);
 }

+ 69 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCompanyStatisticsMapper.java

@@ -0,0 +1,69 @@
+package com.fs.course.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.domain.FsUserCourseCompanyStatistics;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 会员每日看课统计Mapper接口
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+public interface FsUserCourseCompanyStatisticsMapper extends BaseMapper<FsUserCourseCompanyStatistics>{
+    /**
+     * 查询会员每日看课统计
+     *
+     * @param id 会员每日看课统计主键
+     * @return 会员每日看课统计
+     */
+    FsUserCourseCompanyStatistics selectFsUserCourseCompanyStatisticsById(Long id);
+
+    /**
+     * 查询会员每日看课统计列表
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 会员每日看课统计集合
+     */
+    List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsList(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+    List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsTotal(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 新增会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    int insertFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 修改会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    int updateFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 删除会员每日看课统计
+     *
+     * @param id 会员每日看课统计主键
+     * @return 结果
+     */
+    int deleteFsUserCourseCompanyStatisticsById(Long id);
+
+    /**
+     * 批量删除会员每日看课统计
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteFsUserCourseCompanyStatisticsByIds(Long[] ids);
+
+    List<FsUserCourseCompanyStatistics> selectStatisticsByDate(
+            @Param("companyId") Long companyId,
+                @Param("startTime") String startTime,
+            @Param("endTime") String endTime
+    );
+}

+ 10 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -9,6 +9,7 @@ import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.vo.*;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.param.FsUserCourseRedPageParam;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -59,6 +60,13 @@ public interface FsUserCourseVideoMapper
      */
     public int updateFsUserCourseVideo(FsUserCourseVideo fsUserCourseVideo);
 
+    @Update("<script> " +
+            "update fs_user_course_video set red_packet_money=#{data.redPacketMoney} where course_id=#{data.courseId} " +
+            "</script>")
+    public int updateFsUserCourseRedPage(@Param("data") FsUserCourseRedPageParam courseRedPageParam);
+
+    int batchUpdateByVideoId(@Param("list") List<Map<String, Object>> list);
+
     /**
      * 删除课堂视频
      *
@@ -255,4 +263,6 @@ public interface FsUserCourseVideoMapper
      * 根据视频id集合查询列表
      */
     List<FsUserCourseVideoAppletVO> getFsUserCourseVideoAppletVOListByIds(@Param("videoIds") List<Long> videoIds);
+
+    FsUserCourseVO selectFsUserCourseVideoVoByVideoIdAndCourdeId(@Param("videoId") Long videoId,@Param("courseId") Long courseId);
 }

+ 2 - 1
fs-service/src/main/java/com/fs/course/param/FsCourseWatchLogListParam.java

@@ -97,5 +97,6 @@ public class FsCourseWatchLogListParam implements Serializable {
      * 企微名称
      */
     private String qwUserName;
-
+    private Long deptId;
+    private String ids;
 }

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

@@ -77,4 +77,11 @@ public interface IFsCourseFinishTempService
      * 完课用户打备注
      */
     public void finishCourseExtContactIdByRemark(FsCourseWatchLog watchLog);
+
+    /**
+     * 批量更新完课模板状态
+     * @param fsCourseFinishTemp
+     * @return
+     */
+    int updateFsCourseFinishTempBatch(FsCourseFinishTemp fsCourseFinishTemp);
 }

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

@@ -5,6 +5,7 @@ import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.param.*;
 import com.fs.course.vo.*;
 import com.fs.qw.param.QwSidebarStatsParam;
+import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 
 import java.time.LocalDateTime;
 import java.util.List;

+ 69 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseCompanyStatisticsService.java

@@ -0,0 +1,69 @@
+package com.fs.course.service;
+
+import java.util.List;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.course.domain.FsUserCourseCompanyStatistics;
+
+/**
+ * 会员每日看课统计Service接口
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+public interface IFsUserCourseCompanyStatisticsService extends IService<FsUserCourseCompanyStatistics>{
+    /**
+     * 查询会员每日看课统计
+     *
+     * @param id 会员每日看课统计主键
+     * @return 会员每日看课统计
+     */
+    FsUserCourseCompanyStatistics selectFsUserCourseCompanyStatisticsById(Long id);
+
+    /**
+     * 查询会员每日看课统计列表
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 会员每日看课统计集合
+     */
+    List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsList(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 新增会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    int insertFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 修改会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    int updateFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+
+    /**
+     * 批量删除会员每日看课统计
+     *
+     * @param ids 需要删除的会员每日看课统计主键集合
+     * @return 结果
+     */
+    int deleteFsUserCourseCompanyStatisticsByIds(Long[] ids);
+
+    /**
+     * 删除会员每日看课统计信息
+     *
+     * @param id 会员每日看课统计主键
+     * @return 结果
+     */
+    int deleteFsUserCourseCompanyStatisticsById(Long id);
+
+    /**
+     * 会员每日统计定时任务
+     * @param status
+     */
+    void courseDailyStatisticsTask(Integer status,Integer day);
+
+    List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsTotal(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics);
+}

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

@@ -1,5 +1,6 @@
 package com.fs.course.service;
 
+import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.ResponseResult;
 import com.fs.course.domain.FsUserCourseVideo;
@@ -15,6 +16,8 @@ import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.course.vo.newfs.FsUserVideoListVO;
 import com.fs.his.domain.FsUser;
 import com.fs.his.vo.OptionsVO;
+import com.fs.qw.param.FsUserCourseRedPageParam;
+import com.fs.sop.domain.QwSopTempDay;
 
 import java.util.List;
 import java.util.Map;
@@ -59,6 +62,9 @@ public interface IFsUserCourseVideoService
      */
     public int updateFsUserCourseVideo(FsUserCourseVideo fsUserCourseVideo);
 
+    public int updateFsUserCourseRedPage(FsUserCourseRedPageParam userCourseRedPageParam);
+    public void sortCourseVideo(List<FsUserCourseVideo> list);
+
     /**
      * 批量删除课堂视频
      *

+ 9 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseFinishTempServiceImpl.java

@@ -497,4 +497,13 @@ public class FsCourseFinishTempServiceImpl implements IFsCourseFinishTempService
             log.error("保存重试记录失败", e);
         }
     }
+
+    /**
+     * 批量更新完课模板状态
+     * @param fsCourseFinishTemp
+     * @return
+     */
+    public int updateFsCourseFinishTempBatch(FsCourseFinishTemp fsCourseFinishTemp){
+        return fsCourseFinishTempMapper.updateFsCourseFinishTempBatch(fsCourseFinishTemp);
+    }
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -42,6 +42,7 @@ import com.fs.qw.param.QwSidebarStatsParam;
 import com.fs.qw.param.SendSopParamDetails;
 import com.fs.qw.vo.QwSopCourseFinishTempSetting;
 import com.fs.qw.vo.QwSopTempSetting;
+import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 import com.fs.sop.domain.QwSopLogs;
 import com.fs.sop.mapper.SopUserLogsMapper;
 import com.fs.store.service.cache.IFsUserCacheService;
@@ -1255,4 +1256,5 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         return fsCourseWatchLogMapper.selectQwFsCourseWatchLogStatisticsListVO(param);
     }
 
+
 }

+ 237 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCompanyStatisticsServiceImpl.java

@@ -0,0 +1,237 @@
+package com.fs.course.service.impl;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.DateUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.company.domain.Company;
+import com.fs.company.mapper.CompanyMapper;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.course.mapper.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import com.fs.course.domain.FsUserCourseCompanyStatistics;
+import com.fs.course.service.IFsUserCourseCompanyStatisticsService;
+
+/**
+ * 会员每日看课统计Service业务层处理
+ *
+ * @author fs
+ * @date 2025-10-27
+ */
+@Service
+@Slf4j
+public class FsUserCourseCompanyStatisticsServiceImpl extends ServiceImpl<FsUserCourseCompanyStatisticsMapper, FsUserCourseCompanyStatistics> implements IFsUserCourseCompanyStatisticsService {
+    @Autowired
+    private CompanyMapper companyMapper;
+    /**
+     * 查询会员每日看课统计
+     *
+     * @param id 会员每日看课统计主键
+     * @return 会员每日看课统计
+     */
+    @Override
+    public FsUserCourseCompanyStatistics selectFsUserCourseCompanyStatisticsById(Long id)
+    {
+        return baseMapper.selectFsUserCourseCompanyStatisticsById(id);
+    }
+
+    /**
+     * 查询会员每日看课统计列表
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 会员每日看课统计
+     */
+    @Override
+    public List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsList(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        return baseMapper.selectFsUserCourseCompanyStatisticsList(fsUserCourseCompanyStatistics);
+    }
+
+    /**
+     * 新增会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    @Override
+    public int insertFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        fsUserCourseCompanyStatistics.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics);
+    }
+
+    /**
+     * 修改会员每日看课统计
+     *
+     * @param fsUserCourseCompanyStatistics 会员每日看课统计
+     * @return 结果
+     */
+    @Override
+    public int updateFsUserCourseCompanyStatistics(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics)
+    {
+        fsUserCourseCompanyStatistics.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateFsUserCourseCompanyStatistics(fsUserCourseCompanyStatistics);
+    }
+
+    /**
+     * 批量删除会员每日看课统计
+     *
+     * @param ids 需要删除的会员每日看课统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserCourseCompanyStatisticsByIds(Long[] ids)
+    {
+        return baseMapper.deleteFsUserCourseCompanyStatisticsByIds(ids);
+    }
+
+    /**
+     * 删除会员每日看课统计信息
+     *
+     * @param id 会员每日看课统计主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsUserCourseCompanyStatisticsById(Long id)
+    {
+        return baseMapper.deleteFsUserCourseCompanyStatisticsById(id);
+    }
+    /**
+     * 会员每日统计定时任务
+     * @param status
+     */
+    @Override
+    public void courseDailyStatisticsTask(Integer status, Integer day) {
+        /**
+         * 课程数据统计任务
+         *
+         * 统计内容:
+         * 1. 看课次数、完播次数、完播率(fs_course_watch_log)
+         * 2. 答题数量、正确数量、正确率(fs_course_answer_logs)
+         * 3. 红包领取次数、金额(fs_course_red_packet_log)
+         * 4. 会员数量、黑名单数量(fs_user_company_user)
+         *
+         * 参数说明:
+         * status=1 → 查询前一天的数据(00:00:00 ~ 23:59:59)
+         * status=2 → 查询前一个整点小时的数据(例如 17:15 → 16:00:00~16:59:59)
+         * status=3 → 查询最近 day 天到昨天23:59:59的数据
+         */
+        log.info("【课程统计任务开始】status={}, day={}", status, day);
+
+        // 参数校验
+        if (status == null || (!status.equals(1) && !status.equals(2) && !status.equals(3))) {
+            log.warn("课程统计任务状态参数错误:{}", status);
+            return;
+        }
+        if (status.equals(3) && (day == null || day <= 0)) {
+            log.warn("课程统计任务参数错误:status=3 时 day 不能为空且 > 0");
+            return;
+        }
+
+        try {
+            // 计算时间范围
+            LocalDateTime now = LocalDateTime.now();
+            LocalDateTime startTime;
+            LocalDateTime endTime;
+
+            //统计时间(创建时间)
+            Date date = new Date();
+            switch (status) {
+                case 1:
+                    // 前一天 00:00:00 ~ 23:59:59
+                    LocalDate yesterday = LocalDate.now().minusDays(1);
+                    startTime = yesterday.atStartOfDay();
+                    endTime = yesterday.atTime(23, 59, 59);
+                    date=DateUtils.addDays(new Date(),-1);
+                    break;
+
+                case 2:
+                    // 前一个整点小时:例如现在17:15 → 16:00:00 - 16:59:59
+                    LocalDateTime lastHour = now.truncatedTo(ChronoUnit.HOURS).minusHours(1);
+                    startTime = lastHour;
+                    endTime = lastHour.withMinute(59).withSecond(59);
+                    break;
+
+                case 3:
+                    // 最近 day 天到昨天晚上23:59:59
+                    // 结束时间:昨天23:59:59
+                    LocalDate yesterdayEnd = LocalDate.now().minusDays(1);
+                    endTime = yesterdayEnd.atTime(23, 59, 59);
+                    date=DateUtils.addDays(new Date(),-1);
+                    // 开始时间:day天前的00:00:00
+                    LocalDate startDate = yesterdayEnd.minusDays(day - 1);
+                    startTime = startDate.atStartOfDay();
+                    break;
+
+                default:
+                    log.warn("未知状态值:{}", status);
+                    return;
+            }
+
+            String start = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            String end = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            log.info("【课程统计时间范围】{} - {}", start, end);
+
+            // 查询公司列表
+            List<Company> companyList = companyMapper.selectCompanyAllList();
+            if (companyList == null || companyList.isEmpty()) {
+                log.warn("未查询到任何公司信息,任务结束");
+                return;
+            }
+
+            int total = 0;
+            for (Company company : companyList) {
+                try {
+                    List<FsUserCourseCompanyStatistics> statisticsList =
+                            baseMapper.selectStatisticsByDate(company.getCompanyId(), start, end);
+
+                    if (statisticsList == null || statisticsList.isEmpty()) {
+                        log.info("公司[{}]({}) 在时间段 {} - {} 无统计数据", company.getCompanyName(), company.getCompanyId(), start, end);
+                        continue;
+                    }
+
+                    for (FsUserCourseCompanyStatistics stat : statisticsList) {
+                        stat.setCompanyId(company.getCompanyId());
+                        stat.setCompanyName(company.getCompanyName());
+                        stat.setCreateDate(date);
+                        baseMapper.insertFsUserCourseCompanyStatistics(stat);
+                        total++;
+                    }
+
+                    log.info("公司[{}]({}) 数据统计完成,共 {} 条", company.getCompanyName(), company.getCompanyId(), statisticsList.size());
+
+                } catch (Exception ex) {
+                    log.error("公司[{}]({}) 统计异常:{}", company.getCompanyName(), company.getCompanyId(), ex.getMessage(), ex);
+                }
+            }
+
+            log.info("【课程统计任务完成】共处理公司数={},插入统计数据={} 条", companyList.size(), total);
+
+        } catch (Exception e) {
+            log.error("课程统计任务执行异常:{}", e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public List<FsUserCourseCompanyStatistics> selectFsUserCourseCompanyStatisticsTotal(FsUserCourseCompanyStatistics fsUserCourseCompanyStatistics) {
+        // 判断对象是否为空,或 companyId 是否为空
+        Long companyId = Optional.ofNullable(fsUserCourseCompanyStatistics)
+                .map(FsUserCourseCompanyStatistics::getCompanyId)
+                .orElseThrow(() -> new ServiceException("请选择公司后再进行统计查询!"));
+
+        // companyId 不为空,再执行查询
+        return baseMapper.selectFsUserCourseCompanyStatisticsTotal(fsUserCourseCompanyStatistics);
+    }
+
+
+
+}

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

@@ -15,8 +15,10 @@ import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BizResponseEnum;
 import com.fs.common.exception.CustomException;
+import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.PubFun;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.date.DateUtil;
 import com.fs.company.constant.CompanyTrafficConstants;
@@ -52,12 +54,15 @@ import com.fs.qw.mapper.QwExternalContactMapper;
 import com.fs.qw.mapper.QwGroupChatMapper;
 import com.fs.qw.mapper.QwGroupChatUserMapper;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.qw.service.IQwCompanyService;
 import com.fs.qw.service.IQwExternalContactService;
+import com.fs.qw.vo.SortDayVo;
 import com.fs.qwApi.Result.QwAddContactWayResult;
 import com.fs.qwApi.Result.QwGroupChatDetailsResult;
 import com.fs.qwApi.param.QwAddContactWayParam;
 import com.fs.qwApi.service.QwApiService;
+import com.fs.sop.domain.QwSopTempDay;
 import com.fs.sop.domain.SopUserLogsInfo;
 import com.fs.sop.mapper.QwSopLogsMapper;
 import com.fs.sop.mapper.SopUserLogsInfoMapper;
@@ -312,6 +317,31 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         return fsUserCourseVideoMapper.updateFsUserCourseVideo(fsUserCourseVideo);
     }
 
+    @Override
+    public int updateFsUserCourseRedPage(FsUserCourseRedPageParam userCourseRedPageParam) {
+
+        return fsUserCourseVideoMapper.updateFsUserCourseRedPage(userCourseRedPageParam);
+    }
+
+    @Override
+    public void sortCourseVideo(List<FsUserCourseVideo> list) {
+        if (list.isEmpty()){
+            return;
+        }
+        // 直接构建更新参数
+        List<Map<String, Object>> updateParams = list.stream()
+                .map(item -> {
+                    Map<String, Object> param = new HashMap<>();
+                    param.put("videoId", item.getVideoId());
+                    param.put("courseSort", item.getCourseSort());
+                    return param;
+                })
+                .collect(Collectors.toList());
+
+        // 批量更新
+        fsUserCourseVideoMapper.batchUpdateByVideoId(updateParams);
+    }
+
     /**
      * 批量删除课堂视频
      *
@@ -465,6 +495,10 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             return R.error(504,"未授权");
         }
 
+        if (StringUtil.strIsNullOrEmpty(fsUser.getMpOpenId())){
+            return R.error(401,"授权后可继续!");
+        }
+
         if (fsUser.getStatus()==0){
             return R.error("会员被停用,无权限,请联系客服!");
         }
@@ -1113,6 +1147,11 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             if (user.getStatus()==0){
                 return R.error("会员被停用,无权限,请联系客服!");
             }
+
+            if (StringUtil.strIsNullOrEmpty(user.getMpOpenId())){
+                return R.error(401,"授权后可继续!");
+            }
+
             if (!isNewWxMerchant && StringUtils.isEmpty(user.getUnionId())){
                 return R.error("会员信息有误,无法领取红包,请联系客服!");
             }

+ 21 - 3
fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogStatisticsListVO.java

@@ -22,11 +22,11 @@ public class FsCourseWatchLogStatisticsListVO {
     @Excel(name = "小节名称")
     private String videoName;
 
-    @Excel(name = "待看课")
-    private String type1;
     @Excel(name = "看课中")
-    private String type2;
+    private String type1;
     @Excel(name = "已完课")
+    private String type2;
+    @Excel(name = "待看课")
     private String type3;
     @Excel(name = "看课中断")
     private String type4;
@@ -46,4 +46,22 @@ public class FsCourseWatchLogStatisticsListVO {
     private Long companyUserId;
     @Excel(name = "销售名称")
     private String companyUserName;
+
+    /** 发课数 */
+    private String  sendNumber;
+
+    /** 已注册用户待看课数 */
+    private String  isUserWaitNumber;
+
+    /** 未注册用户待看课数 */
+    private String  noUserWaitNumber;
+
+    /** 上线率 */
+    private String  onLineRate;
+
+    /** 完课率 */
+    private String  finishedRate;
+
+    /** 消耗红包金额 */
+    private String  redAmount;
 }

+ 7 - 0
fs-service/src/main/java/com/fs/crm/param/CrmCustomerListQueryParam.java

@@ -1,6 +1,7 @@
 package com.fs.crm.param;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fs.common.annotation.Excel;
 import lombok.Data;
 
@@ -96,5 +97,11 @@ public class CrmCustomerListQueryParam extends BaseQueryParam
 
     private String[] receiveTimeList;
 
+    /** 开始时间 */
+    private String beginTime;
+
+    /** 结束时间 */
+    private String endTime;
+
 
 }

+ 3 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsStoreOrderServiceImpl.java

@@ -1658,7 +1658,9 @@ public class FsStoreOrderServiceImpl implements IFsStoreOrderService {
                 if (order.getTotalPrice().compareTo(minThreshold) >= 0) {
                     //根据用户id获取fs_user表对应的下单次数并更新
                     FsUser user = fsUserMapper.selectFsUserById(order.getUserId());
-                    user.setOrderCount(user.getOrderCount() + 1);
+                    //处理 orderCount 为 null 的情况:null 视为 0
+                    long currentCount = user.getOrderCount() != null ? user.getOrderCount() : 0;
+                    user.setOrderCount(currentCount + 1);
                     fsUserMapper.updateFsUser(user);
                 }
             } catch (Exception ex) {

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

@@ -1049,7 +1049,7 @@ public class FsUserServiceImpl implements IFsUserService {
         //查询用户
         FsUser fsUser = fsUserMapper.selectFsUserById(param.getUserId());
         if (Objects.isNull(fsUser)) {
-            return ResponseResult.fail(404, "当前用户信息不存在");
+            return ResponseResult.fail(401, "当前用户信息不存在");
         }
 
         //判断该销售是否存在

+ 7 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwGroupChatUserMapper.java

@@ -97,15 +97,20 @@ public interface QwGroupChatUserMapper
             "</script> ")
     public List<HashMap<String,String>> getDataWeekMonthCount(@Param("map") QwGroupChatUserDataType qwGroupChatUserDataType);
 
-    @Select("select " +
+    @Select(" <script> " +
+            " select " +
             "DATE_FORMAT(join_time,'%Y-%m') as toDay, " +
             "COUNT(join_time) AS dailyJoinCount , " +
             "COUNT(out_time) AS dailyOutCount " +
             "from qw_group_chat_user " +
             "where DATE_FORMAT(join_time,'%Y-%m')> DATE_FORMAT(date_sub(curdate(), interval 12 month),'%Y-%m') " +
+            " <if test='map.chatId != null'>" +
+            " and chat_id = #{map.chatId}" +
+            " </if>" +
             "group  by  DATE_FORMAT(join_time,'%Y-%m') " +
             "ORDER BY  " +
-            "  DATE_FORMAT(join_time,'%Y-%m') asc ")
+            "  DATE_FORMAT(join_time,'%Y-%m') asc " +
+            " </script> ")
     public List<HashMap<String,String>> getDataWeekMonthCountMonth(@Param("map") QwGroupChatUserDataType qwGroupChatUserDataType);
 
 

+ 42 - 2
fs-service/src/main/java/com/fs/qw/mapper/QwWatchLogMapper.java

@@ -1,6 +1,7 @@
 package com.fs.qw.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwWatchLog;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
@@ -80,7 +81,8 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "    COUNT(CASE WHEN qec.`level` = 5 THEN 1 END) AS E,\n" +
             "    COUNT(CASE WHEN qec.fs_user_id IS NOT NULL THEN 1 END) AS sign,\n" +
             "    COUNT(CASE WHEN qec.`status` =3 THEN 1 END) AS los,\n" +
-            "    COUNT(CASE WHEN qec.`status` IN (4, 5,6) THEN 1 END) AS del\n" +
+            "    COUNT(CASE WHEN qec.`status` IN (4, 5,6) THEN 1 END) AS del,\n" +
+            "    COUNT(CASE WHEN qec.fs_user_id IS NOT NULL and qec.fs_user_id != 0 THEN 1 END) AS reg_num\n"+
             "FROM\n" +
             "    qw_external_contact qec\n" +
             "JOIN\n" +
@@ -103,6 +105,44 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "    DATE(qec.create_time) "+
             "</script>"})
     List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAnd(QwWatchLogStatisticsListParam param);
+
+    @Select({"<script> " +
+            "SELECT\n" +
+            "    qec.qw_user_id id,\n" +
+            "    qu.qw_user_name AS qw_user_name, \n" +
+            "    DATE(qec.create_time) AS create_time, \n" +
+            "    COUNT(1) AS line,\n" +
+            "    COUNT(CASE WHEN qec.is_interact = 1 THEN 1 END) AS interact,\n" +
+            "    COUNT(CASE WHEN qec.`level` = 1 THEN 1 END) AS A,\n" +
+            "    COUNT(CASE WHEN qec.`level` = 2 THEN 1 END) AS B,\n" +
+            "    COUNT(CASE WHEN qec.`level` = 3 THEN 1 END) AS C,\n" +
+            "    COUNT(CASE WHEN qec.`level` = 4 THEN 1 END) AS D,\n" +
+            "    COUNT(CASE WHEN qec.`level` = 5 THEN 1 END) AS E,\n" +
+            "    COUNT(CASE WHEN qec.fs_user_id IS NOT NULL THEN 1 END) AS sign,\n" +
+            "    COUNT(CASE WHEN qec.`status` =3 THEN 1 END) AS los,\n" +
+            "    COUNT(CASE WHEN qec.`status` IN (4, 5,6) THEN 1 END) AS del\n" +
+            "FROM\n" +
+            "    qw_external_contact qec\n" +
+            "JOIN\n" +
+            "    qw_user qu ON qec.qw_user_id = qu.id \n" +
+            "left join company_user cu on qec.company_user_id = cu.user_id "+
+            "WHERE\n" +
+            "    DATE(qec.create_time) &gt;= DATE(#{sTime}) and  DATE(qec.create_time) &lt;= DATE(#{eTime}) and qec.company_id =#{companyId} " +
+            "<if test ='nickName !=null and nickName!=\"\"'>\n" +
+            "   and qu.qw_user_name like concat( #{nickName}, '%')\n" +
+            "</if>" +
+            "<if test ='deptId !=null and deptId!=\"\"'>\n" +
+            "   and cu.dept_id = #{deptId}\n" +
+            "</if>" +
+            "<if test ='ids !=null and ids!=\"\"'>\n" +
+            "   and qec.qw_user_id in (${ids})\n" +
+            "</if>" +
+            "GROUP BY\n" +
+            "    qec.qw_user_id, DATE(qec.create_time) \n" +
+            "ORDER BY\n" +
+            "    DATE(qec.create_time) "+
+            "</script>"})
+    List<QwWatchLogStatisticsListVO> selectQwExtCountByDayAnd(FsCourseWatchLogListParam param);
     @Select("select \n" +
             "COUNT(CASE WHEN day = 0 and status in (1,2) THEN 1 END) AS firstOnline,\n" +
             "COUNT(CASE WHEN day = 0 and status=2 THEN 1 END) AS firstOver,\n" +
@@ -183,7 +223,7 @@ public interface QwWatchLogMapper extends BaseMapper<QwWatchLog>{
             "    qu.qw_user_name AS qw_user_name, \n" +
             "    DATE(qec.create_time) AS create_time, \n" +
             "    COUNT(1) AS line," +
-            "    COUNT(CASE WHEN fs_user_id IS NOT NULL THEN 1 ELSE NULL END) AS reg_num\n" +
+            "    COUNT(CASE WHEN qec.fs_user_id IS NOT NULL and qec.fs_user_id != 0 THEN 1 END) AS reg_num\n" +
             "FROM\n" +
             "    qw_external_contact qec\n" +
             "JOIN\n" +

+ 15 - 0
fs-service/src/main/java/com/fs/qw/param/FsUserCourseRedPageParam.java

@@ -0,0 +1,15 @@
+package com.fs.qw.param;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class FsUserCourseRedPageParam {
+
+    //课程
+    private Long courseId;
+    //红包
+    private BigDecimal redPacketMoney;
+}
+

+ 3 - 0
fs-service/src/main/java/com/fs/qw/service/IQwWatchLogService.java

@@ -2,6 +2,7 @@ package com.fs.qw.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.qw.domain.QwWatchLog;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
@@ -70,4 +71,6 @@ public interface IQwWatchLogService extends IService<QwWatchLog>{
     TableDataInfo selectQwWatchLogAllStatisticsListVONew(QwWatchLogStatisticsListParam param);
 
     TableDataInfo selectQwWatchLogStatisticsListVONew(QwWatchLogStatisticsListParam param);
+
+    List<QwWatchLogStatisticsListVO> selectQwWatchLogStatisticsListVOExport(FsCourseWatchLogListParam param);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwWatchLogServiceImpl.java

@@ -16,6 +16,7 @@ import com.fs.company.mapper.CompanyDeptMapper;
 import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.course.domain.FsUserCourse;
 import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
 import com.fs.his.domain.FsUser;
 import com.fs.qw.domain.QwWatchLog;
@@ -144,6 +145,29 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
         return baseMapper.deleteQwWatchLogById(id);
     }
 
+
+    @Override
+    public List<QwWatchLogStatisticsListVO> selectQwWatchLogStatisticsListVOExport(FsCourseWatchLogListParam param) {
+        CompanyDept companyDept = companyDeptMapper.selectCompanyDeptById(param.getDeptId());
+        if (ObjectUtils.isNotEmpty(companyDept)&&companyDept.getParentId()==0L){
+            param.setDeptId(null);
+        }
+        if (param.getCompanyUserId()!=null){
+            param.setIds(companyUserMapper.selectQwUserIdsByCompany(param.getCompanyUserId()));
+        }
+        List<QwWatchLogStatisticsListVO> vos = qwWatchLogMapper.selectQwExtCountByDayAnd(param);
+        for (QwWatchLogStatisticsListVO vo : vos) {
+            Long id = vo.getId();
+            Date createTime = vo.getCreateTime();
+            QwWatchLogStatisticsListVO stat = qwWatchLogMapper.selectQwWatchLogByQwUserId(id, createTime);
+            vo.setD1Online(stat.getD1Online());
+            vo.setD1Over(stat.getD1Over());
+            vo.setFirstOnline(stat.getFirstOnline());
+            vo.setFirstOver(stat.getFirstOver());
+        }
+        return vos;
+    }
+
     @Override
     public TableDataInfo selectQwWatchLogStatisticsListVO(QwWatchLogStatisticsListParam param) {
         CompanyDept companyDept = companyDeptMapper.selectCompanyDeptById(param.getDeptId());
@@ -205,6 +229,7 @@ public class QwWatchLogServiceImpl extends ServiceImpl<QwWatchLogMapper, QwWatch
             stat.setCreateTime(vo.getCreateTime());
             stat.setQwUserName(vo.getQwUserName());
             stat.setLine(vo.getLine());
+            stat.setRegNum(vo.getRegNum());
             list.add(stat);
         }
 

+ 8 - 0
fs-service/src/main/java/com/fs/qw/vo/QwUserVoiceLogVo.java

@@ -1,5 +1,7 @@
 package com.fs.qw.vo;
 
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntity;
 import com.fs.company.domain.Company;
@@ -83,4 +85,10 @@ public class QwUserVoiceLogVo extends BaseEntity {
 
     private Integer pageSize;
 
+    /** 开始时间 */
+    private String beginTime;
+
+    /** 结束时间 */
+    private String endTime;
+
 }

+ 21 - 1
fs-service/src/main/java/com/fs/qw/vo/QwWatchLogStatisticsListVO.java

@@ -1,6 +1,7 @@
 package com.fs.qw.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
 import lombok.Data;
 
 import java.util.Date;
@@ -9,10 +10,12 @@ import java.util.Date;
 public class QwWatchLogStatisticsListVO {
     private Long id;
     private String qwUserName;
+    @Excel(name = "企微员工名称")
     private String companyUserName;
     private Long companyUserId;
     private Long companyId;
     @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "进线时间",dateFormat = "yyyy-MM-dd")
     private Date createTime;
     /**
      * 项目
@@ -25,26 +28,43 @@ public class QwWatchLogStatisticsListVO {
      */
     private Long courseId;
     private String courseName;
+    /**
+     * 注册数
+     */
+    private Long regNum;
 
     /**
      * 小节
      */
     private Long videoId;
     private String videoName;
-
+    @Excel(name = "进线数")
     private Long line;//进线数
+    @Excel(name = "先导课上线")
     private Long firstOnline;//先导课上线
+    @Excel(name = "先导课完课")
     private Long firstOver;//先导课完课
+    @Excel(name = "首日上线")
     private Long d1Online;//首日上线
+    @Excel(name = "首日完课")
     private Long d1Over;//首日完课
+    @Excel(name = "综合报名数")
     private Long sign;//综合报名数
+    @Excel(name = "互动数")
     private Long interact;//互动数
+    @Excel(name = "A级用户")
     private Long a;
+    @Excel(name = "B级用户")
     private Long b;
+    @Excel(name = "C级用户")
     private Long c;
+    @Excel(name = "D级用户")
     private Long d;
+    @Excel(name = "E级用户")
     private Long e;
+    @Excel(name = "拉黑数")
     private Long black;
     private Long los;//流失数
+    @Excel(name = "删除数")
     private Long del;//删除数
 }

+ 14 - 0
fs-service/src/main/java/com/fs/sop/domain/QwSopTemp.java

@@ -24,10 +24,12 @@ public class QwSopTemp implements Serializable
 {
 
     /** id */
+    @Excel(name = "模板编号")
     @TableId(type = IdType.INPUT)
     private String id;
 
     /** 模板标题 */
+
     @Excel(name = "模板标题")
     private String name;
 
@@ -48,6 +50,7 @@ public class QwSopTemp implements Serializable
     @Excel(name = "公司id")
     private Long companyId;
 
+    @Excel(name = "间隔天数")
     private Integer gap;
 
     /**
@@ -61,27 +64,38 @@ public class QwSopTemp implements Serializable
     /**
     * 模板类型
     */
+    @Excel(name = "模板类型")
     private Integer sendType;
 
+    @Excel(name = "创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private String createTime;
 
+    @Excel(name = "修改时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private String updateTime;
 
     private String createBy;
+
     private Long project;
+
     private Long courseId;
+
     @TableField(exist = false)
     private String modeContent;
+
     @TableField(exist = false)
     private List<Map<String, Object>> rules;
+
     @TableField(exist = false)
     private List<QwSopTempDay> list = new ArrayList<>();
+
     @TableField(exist = false)
     private LocalTime time;
+
     @TableField(exist = false)
     private boolean cuoser;
+
     @TableField(exist = false)
     private List<String> timeList;
 

+ 1 - 1
fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java

@@ -54,7 +54,7 @@ public interface SopUserLogsMapper {
 //    int updateSopUserLogsDistinctByList(@Param("data") SopUserLogsArray userLogsArray);
 
     @DataSource(DataSourceType.SOP)
-    public List<SopUserLogsVo> selectSopUserLogsListByTime();
+    public List<SopUserLogsVo> selectSopUserLogsListByTime(@Param("sopIds") List<String> sopidList);
 
     @DataSource(DataSourceType.SOP)
     public List<SopUserLogs> meetsTheRatingByUserInfo();

+ 1 - 1
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java

@@ -165,7 +165,7 @@ public class SopUserLogsServiceImpl  implements ISopUserLogsService {
         // 当前时间
         LocalDateTime currentDateTime = LocalDateTime.now();
 
-        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTime();
+        List<SopUserLogsVo> sopUserLogsVos = sopUserLogsMapper.selectSopUserLogsListByTime(null);
         // 创建一个 Map,用于分组:key 是sop_id,value 是对应的 QwSopLogs 列表
         Map<String, List<SopUserLogsVo>> stringListMap = sopUserLogsVos.stream()
                 .collect(Collectors.groupingBy(SopUserLogsVo::getSopId));

+ 1 - 0
fs-service/src/main/java/com/fs/statis/dto/AnalysisPreviewQueryDTO.java

@@ -39,6 +39,7 @@ public class AnalysisPreviewQueryDTO implements Serializable {
 
     /**
      * 0 用户 1 企微
+     * 1 会员 2 企微
      */
     private Integer userType;
 

+ 29 - 0
fs-service/src/main/java/com/fs/statis/dto/WatchCourseStatisticsResultDTO.java

@@ -0,0 +1,29 @@
+package com.fs.statis.dto;
+
+import lombok.Data;
+
+/**
+ * @description: 看客统计实体类 没有统计观看完成人数
+ * @author: Xgb
+ * @createDate: 2025/10/27
+ * @version: 1.0
+ */
+@Data
+public class WatchCourseStatisticsResultDTO {
+
+    // 公司id
+    private String companyId;
+    // 公司名称
+    private String companyName;
+    // 1.个微 手动 2.企微 自动
+    private String sendType;
+    // 观看用户数
+    private Integer watchUserCount;
+    // 观看次数
+    private Integer watchCount;
+    // 观看完成数
+    private Integer finishCount;
+    // 观看完成用户数
+    private Integer finishUserCount;
+
+}

+ 7 - 0
fs-service/src/main/java/com/fs/statis/service/IStatisticsService.java

@@ -115,4 +115,11 @@ public interface IStatisticsService {
      * 查询看课统计
      */
     WatchCourseStatisticsDTO queryWatchCourse(WatchCourseStatisticsParam param);
+
+    /**
+     * 统计今日看课
+     */
+    void watchCourseStatisticsGroupByCompany();
+
+    List<WatchCourseStatisticsResultDTO> getWatchCourseStatisticsData(AnalysisPreviewQueryDTO param);
 }

+ 57 - 0
fs-service/src/main/java/com/fs/statis/service/impl/StatisticsServiceImpl.java

@@ -1,10 +1,14 @@
 package com.fs.statis.service.impl;
 
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.constant.FsConstants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.TimeUtils;
+import com.fs.common.utils.model.DateTimeEntity;
 import com.fs.company.cache.ICompanyCacheService;
+import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.dto.WatchLogDTO;
 import com.fs.course.mapper.FsCourseTrafficLogMapper;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
@@ -24,6 +28,7 @@ import com.fs.statis.service.utils.TrendDataFiller;
 import com.fs.store.service.cache.IFsUserCourseCacheService;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.service.ISysConfigService;
+import com.hc.openapi.tool.fastjson.JSON;
 import com.hc.openapi.tool.util.ObjectUtils;
 import com.hc.openapi.tool.util.StringUtils;
 import org.apache.http.util.Asserts;
@@ -38,7 +43,9 @@ import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 import com.fs.statistics.dto.WatchCourseStatisticsDTO;
 
@@ -80,6 +87,8 @@ public class StatisticsServiceImpl implements IStatisticsService {
     @Autowired
     private IQwIpadServerService qwIpadServerService;
 
+
+
     @Override
     public void dataOverviewTask() {
         DealerAggregatedDTO dealerAggregatedDTO = this.dealerAggregated();
@@ -998,4 +1007,52 @@ public class StatisticsServiceImpl implements IStatisticsService {
 
         return watchCourseStatisticsDTO;
     }
+
+    /**
+     * @Description: 统计按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;各公司的观看人数和完播人数, 存到redis中,定时任务每 ? 分钟执行一次
+     * @Param:
+     * @Return:
+     * @Author xgb
+     * @Date 2025/10/27 13:09
+     */
+    @Override
+    public void watchCourseStatisticsGroupByCompany() {
+        for(int i = 0; i < 5; i++){
+            Map<String,Object> params=new HashMap<>();
+            DateTimeEntity timeEntity =DateUtils.getBetweenTime(i);
+            params.put("startTime",timeEntity.getStartTime());
+            params.put("endTime",timeEntity.getEndTime());
+
+            List<WatchCourseStatisticsResultDTO> watchCourseStatisticsDTOS=fsCourseWatchLogMapper.watchCourseStatisticsGroupByCompany(params);
+            // 存到redis中 按TimeType 0-今天,1-昨天,2-本周,3-本月,4-上月;
+            redisCache.setCacheObject(FsConstants.WATCH_COURSE_STATISTICS_GROUP_COMPANY+i, JSON.toJSONString(watchCourseStatisticsDTOS));
+        }
+    }
+
+    @Override
+    public List<WatchCourseStatisticsResultDTO> getWatchCourseStatisticsData(AnalysisPreviewQueryDTO param) {
+
+        String redisData = redisCache.getCacheObject(FsConstants.WATCH_COURSE_STATISTICS_GROUP_COMPANY + param.getType());
+        List<WatchCourseStatisticsResultDTO> watchCourseStatisticsDTOS = new ArrayList<>();
+        if (StringUtils.isNotBlank(redisData)) {
+            watchCourseStatisticsDTOS = JSONObject.parseArray(redisData, WatchCourseStatisticsResultDTO.class);
+        }
+        String sendType;
+        if (param.getUserType() == 1) {
+            sendType = "1";
+        } else if (param.getUserType() == 2) {
+            sendType = "2";
+        } else {
+            return new ArrayList<>();
+        }
+
+        watchCourseStatisticsDTOS = watchCourseStatisticsDTOS.stream()
+                .filter(dto -> StringUtils.equals(sendType, dto.getSendType()))
+                .filter(dto -> param.getCompanyId() == null ||
+                        StringUtils.equals(dto.getCompanyId(), String.valueOf(param.getCompanyId())))
+                .collect(Collectors.toList());
+
+
+        return watchCourseStatisticsDTOS;
+    }
 }

+ 22 - 22
fs-service/src/main/java/com/fs/tulin/service/impl/TulinInfoSyncLogServiceImpl.java

@@ -74,28 +74,28 @@ public class TulinInfoSyncLogServiceImpl implements ITulinInfoSyncLogService {
                 }
             }
         }
-//        if(activeProfile.equals("dev")) {
-//            log.info("线下开始执行数据同步");
-//            for (QwExUserInfo qwExUserInfo : qwExUserInfos) {
-//                StudentInfo studentInfo = new StudentInfo();
-//                BeanUtils.copyProperties(qwExUserInfo, studentInfo);
-//                studentInfo.setAppid("wx6688e6b9b6fb8700");
-//                studentInfo.setColumn_name(ColumnNameEnum.GYBNX.getName());
-//                studentInfo.setPharmacy_id(PharmacyEnum.YJKYSY.getPharmacyId());
-//                studentInfo.setService_phone("15141712344");
-//                List<PeriodInfo> periodInfos = tulinInfoSyncLogMapper.selectPeriodInfo(qwExUserInfo.getFs_user_id());
-//                if(CollectionUtils.isNotEmpty(periodInfos)){
-//                    studentInfo.setPeriod_name(periodInfos.get(0).getCourseName());
-//                }
-//                try {
-//                    TulinInfoSyncLog tulinInfoSyncLog = SyncStudentInfoService.send(PROD_URL,PROD_CORPID, studentInfo);
-//                    tulinInfoSyncLog.setFsUserId(qwExUserInfo.getFs_user_id());
-//                    insert(tulinInfoSyncLog);
-//                } catch (JsonProcessingException e) {
-//                    throw new RuntimeException(e);
-//                }
-//            }
-//        }
+        if(activeProfile.equals("dev")) {
+            log.info("线下开始执行数据同步");
+            for (QwExUserInfo qwExUserInfo : qwExUserInfos) {
+                StudentInfo studentInfo = new StudentInfo();
+                BeanUtils.copyProperties(qwExUserInfo, studentInfo);
+                studentInfo.setAppid("wx6688e6b9b6fb8700");
+                studentInfo.setColumn_name(ColumnNameEnum.GYBNX.getName());
+                studentInfo.setPharmacy_id(PharmacyEnum.YJKYSY.getPharmacyId());
+                studentInfo.setService_phone("15141712344");
+                List<PeriodInfo> periodInfos = tulinInfoSyncLogMapper.selectPeriodInfo(qwExUserInfo.getFs_user_id());
+                if(CollectionUtils.isNotEmpty(periodInfos)){
+                    studentInfo.setPeriod_name(periodInfos.get(0).getCourseName());
+                }
+                try {
+                    TulinInfoSyncLog tulinInfoSyncLog = SyncStudentInfoService.send(PROD_URL,PROD_CORPID, studentInfo);
+                    tulinInfoSyncLog.setFsUserId(qwExUserInfo.getFs_user_id());
+                    insert(tulinInfoSyncLog);
+                } catch (JsonProcessingException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
     }
 
 

+ 104 - 0
fs-service/src/main/resources/application-config-dev-czt.yml

@@ -0,0 +1,104 @@
+baidu:
+  token: 12313231232
+  back-domain: https://www.xxxx.com
+#配置
+logging:
+  level:
+    org.springframework.web: INFO
+    com.github.binarywang.demo.wx.cp: DEBUG
+    me.chanjar.weixin: DEBUG
+wx:
+  miniapp:
+    configs:
+      - appid: wxa6a215ad7353fd8a   #纯正堂药速通
+        secret: 4aa21869d9ed5bfc9477afb231a30f05 #纯正堂药速通
+        token: Ncbnd7lJvkripxxna6NAWCxCrvC
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+  cp:
+    corpId: wwa46ffb9ff6ac35b8 #企业ID北京存在文化
+    appConfigs:
+      - agentId: 1000070       #北京存在文化
+        secret: pu2EFz6gY2Fo2K-aRUxLPaAkKIaMJJRp8ES9JdpHkp4 #北京存在文化
+        token: PPKOdAlCoMO
+        aesKey: PKvaxtpSv8NGpfTDm7VUHIK8Wok2ESyYX24qpXJAdMP
+  pay:
+    appId:  #微信公众号或者小程序等的appid
+    mchId:  #微信支付商户号
+    mchKey:  #微信支付商户密钥
+    subAppId:  #服务商模式下的子商户公众账号ID
+    subMchId:  #服务商模式下的子商户号
+    keyPath: c:\\cert\\apiclient_cert.p12 # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    notifyUrl: https://userapp.his.runtzh.com/app/wxpay/wxPayNotify
+  mp:
+    useRedis: false
+    redisConfig:
+      host: 127.0.0.1
+      port: 6379
+      timeout: 2000
+    configs:
+      - appId: wx6d3706feab2b9926 # 第一个公众号的appid  //公众号名称:纯正堂大药房
+        secret: eedddc683062b258625f036a71d7cbc0 # 公众号的appsecret--纯正堂大药房
+        token: PPKOdAlCoMO # 接口配置里的Token值
+        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+aifabu:  #爱链接
+  appKey: 7b471be905ab17e00f3b858c6710dd117601d008
+watch:
+  watchUrl: watch.ylrzcloud.com/prod-api
+  #  account: tcloud
+  #  password: mdf-m2h_6yw2$hq
+  account1: ccif #866655060138751
+  password1: cp-t5or_6xw7$mt
+  account2: tcloud #rt500台
+  password2: mdf-m2h_6yw2$hq
+  account3: whr
+  password3: v9xsKuqn_$d2y
+
+fs :
+  commonApi: http://127.0.0.1:7771
+  h5CommonApi: http://127.0.0.1:7771
+  jwt:
+    # 加密秘钥
+    secret: e10adc3949ba59abbe56e057f20f883e
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
+nuonuo:
+  key: 10924508
+  secret: A2EB20764D304D16
+
+# 存储捅配置
+tencent_cloud_config:
+  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+  bucket: czt-1323137866
+  app_id: 1323137866
+  region: ap-chongqing
+  proxy: czt
+tmp_secret_config:
+  secret_id: AKIDCj7NSNAovtqeJpBau8GZ4CGB71thXIxX
+  secret_key: lTB5zwqqz7CNhzDOWivFWedgfTBgxgBT
+  bucket: fs-1319721001
+  app_id: 1319721001
+  region: ap-chongqing
+  proxy: fs
+cloud_host:
+  company_name: 纯正堂
+  projectCode: CZT
+headerImg:
+  imgUrl:
+
+ipad:
+  ipadUrl: http://ipad.cykbja.cn
+  aiApi: 1212121212
+  commonApi:
+  voiceApi:
+wx_miniapp_temp:
+  pay_order_temp_id:
+  inquiry_temp_id:
+
+# 0 代表关闭 1代表开启(润天老商户号的扣款限制)
+enableRedPackAccount: 0
+video:
+  videoUploadDir:
+  frameOutputDir:

+ 7 - 1
fs-service/src/main/resources/application-config-druid-czt.yml

@@ -57,6 +57,12 @@ watch:
 fs :
   commonApi: http://127.0.0.1:7771
   h5CommonApi: http://127.0.0.1:7771
+  jwt:
+    # 加密秘钥
+    secret: e10adc3949ba59abbe56e057f20f883e
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
 nuonuo:
   key: 10924508
   secret: A2EB20764D304D16
@@ -95,4 +101,4 @@ wx_miniapp_temp:
 enableRedPackAccount: 0
 video:
   videoUploadDir:
-  frameOutputDir:
+  frameOutputDir:

+ 2 - 1
fs-service/src/main/resources/application-config-druid-hdt.yml

@@ -67,7 +67,8 @@ fs :
   h5CommonApi: http://119.29.195.254:8010
   jwt:
     # 加密秘钥
-    secret: f4e2e52034348f86b67cde581c0f9eb5
+#    secret: f4e2e52034348f86b67cde581c0f9eb5
+    secret: hdt-zxqzbl
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken

+ 1 - 1
fs-service/src/main/resources/application-config-druid-jnsyj.yml

@@ -67,7 +67,7 @@ fs :
   h5CommonApi: http://119.29.195.254:8010
   jwt:
     # 加密秘钥
-    secret: f4e2e52034348f86b67cde581c0f9eb5
+    secret: jnsyj-zxqzbl
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken

+ 1 - 1
fs-service/src/main/resources/application-config-zkzh.yml

@@ -136,7 +136,7 @@ fs:
   commonApi: http://172.21.76.167:8010
   jwt:
     # 加密秘钥
-    secret: f4e2e52034348f86b67cde581c0f9eb5
+    secret: zkzh-zxqzbl
     # token有效时长,7天,单位秒
     expire: 31536000
     header: AppToken

+ 157 - 0
fs-service/src/main/resources/application-dev-czt.yml

@@ -0,0 +1,157 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-dev-czt,common
+    # redis 配置
+    redis:
+        host: localhost
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password:
+        # 连接超时时间
+        timeout: 10s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 100
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+#        clickhouse:
+#            type: com.alibaba.druid.pool.DruidDataSource
+#            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
+#            url: jdbc:clickhouse://1.14.104.71:8123/sop_test?compress=0&use_server_time_zone=true&use_client_time_zone=false&timezone=Asia/Shanghai
+#            username: rt_2024
+#            password: Yzx_19860213
+#            initialSize: 10
+#            maxActive: 100
+#            minIdle: 10
+#            maxWait: 6000
+        mysql:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                  url: jdbc:mysql://nj-cdb-5rexc1if.sql.tencentcdb.com:28670/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                  username: root
+                  password: Ylrz_1q2w3e4r5t6y
+                # 从库数据源
+                slave:
+                    # 从数据源开关/默认关闭
+                    enabled: false
+                    url:
+                    username:
+                    password:
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+        sop:
+            type: com.alibaba.druid.pool.DruidDataSource
+            driverClassName: com.mysql.cj.jdbc.Driver
+            druid:
+                # 主库数据源
+                master:
+                    url: jdbc:mysql://nj-cdb-5rexc1if.sql.tencentcdb.com:28670/fs_his_sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_1q2w3e4r5t6y
+                # 初始连接数
+                initialSize: 5
+                # 最小连接池数量
+                minIdle: 10
+                # 最大连接池数量
+                maxActive: 20
+                # 配置获取连接等待超时的时间
+                maxWait: 60000
+                # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
+                timeBetweenEvictionRunsMillis: 60000
+                # 配置一个连接在池中最小生存的时间,单位是毫秒
+                minEvictableIdleTimeMillis: 300000
+                # 配置一个连接在池中最大生存的时间,单位是毫秒
+                maxEvictableIdleTimeMillis: 900000
+                # 配置检测连接是否有效
+                validationQuery: SELECT 1 FROM DUAL
+                testWhileIdle: true
+                testOnBorrow: false
+                testOnReturn: false
+                webStatFilter:
+                    enabled: true
+                statViewServlet:
+                    enabled: true
+                    # 设置白名单,不填则允许所有访问
+                    allow:
+                    url-pattern: /druid/*
+                    # 控制台管理用户名和密码
+                    login-username: fs
+                    login-password: 123456
+                filter:
+                    stat:
+                        enabled: true
+                        # 慢SQL记录
+                        log-slow-sql: true
+                        slow-sql-millis: 1000
+                        merge-sql: true
+                    wall:
+                        config:
+                            multi-statement-allow: true
+rocketmq:
+    name-server: rmq-16xj8o92zp.rocketmq.cd.qcloud.tencenttdmq.com:8080
+    producer:
+        group: my-producer-group
+        access-key: ak16xj8o92zp984557f83ba2 # 替换为实际的 accessKey
+        secret-key: sk2ff1c6b15b74b888 # 替换为实际的 secretKey
+    consumer:
+        group: common-group
+        access-key: ak16xj8o92zp984557f83ba2 # 替换为实际的 accessKey
+        secret-key: sk2ff1c6b15b74b888 # 替换为实际的 secretKey
+openIM:
+    secret: openIM123
+    userID: imAdmin
+    url: https://web.im.fbylive.com/api
+#是否使用新im
+im:
+    type: NONE
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: true

+ 3 - 3
fs-service/src/main/resources/application-druid-jnmy-test.yml

@@ -41,7 +41,7 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://120.46.174.121:2345/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    url: jdbc:mysql://120.46.174.121:2345/fs_his_test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
                     password: Ylrztek250218!3@.
                 # 从库数据源
@@ -151,10 +151,10 @@ rocketmq:
 openIM:
     secret: openIM123
     userID: imAdmin
-    url: https://web.im.fbylive.com/api
+    url: https://web.jnmyim.ylrzfs.com/api
 #是否使用新im
 im:
-    type: NONE
+    type: OPENIM
 isNewWxMerchant: true
 
 

+ 2 - 2
fs-service/src/main/resources/application-druid-jnmy.yml

@@ -154,10 +154,10 @@ rocketmq:
 openIM:
     secret: openIM123
     userID: imAdmin
-    url: https://web.im.fbylive.com/api
+    url: https://web.jnmyim.ylrzfs.com/api
 #是否使用新im
 im:
-    type: NONE
+    type: OPENIM
 #是否为新商户,新商户不走mpOpenId
 isNewWxMerchant: true
 

+ 63 - 0
fs-service/src/main/resources/db/20251028-会员每日看课统计.sql

@@ -0,0 +1,63 @@
+
+
+
+CREATE TABLE `fs_user_course_company_statistics` (
+ `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+ `project_id` BIGINT DEFAULT NULL COMMENT '项目ID',
+
+ `complete_watch_count` INT DEFAULT NULL COMMENT '完播次数(人次)',
+ `watch_count` INT DEFAULT NULL COMMENT '观看次数(人次)',
+
+ `answer_count` INT DEFAULT NULL COMMENT '答题人次',
+ `correct_count` INT DEFAULT NULL COMMENT '正确人次',
+
+ `receive_count` INT DEFAULT NULL COMMENT '领取次数',
+ `receive_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '领取金额(元)',
+
+ `user_count` INT DEFAULT NULL COMMENT '会员数量',
+ `user_blacklist_count` INT DEFAULT NULL COMMENT '会员黑名单数量',
+ `company_id` BIGINT DEFAULT NULL COMMENT '公司ID',
+ `company_name` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '公司名称',
+
+ `create_date` DATE DEFAULT NULL COMMENT '统计日期',
+ `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ `create_by` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '创建人',
+ `update_by` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '更新人',
+
+ PRIMARY KEY (`id`) USING BTREE,
+ KEY `idx_project_id` (`project_id`),
+ KEY `idx_create_date` (`create_date`)
+) ENGINE=InnoDB
+  DEFAULT CHARSET=utf8mb4
+  COLLATE=utf8mb4_0900_ai_ci
+  ROW_FORMAT=DYNAMIC
+    COMMENT='用户看课统计表(含完播率、答题统计、领取信息等)';
+
+
+
+-- 菜单 SQL
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计', '1680', '1', 'statistics', 'course/statistics/index', 1, 0, 'C', '0', '0', 'course:statistics:list', '#', 'admin', sysdate(), '', null, '会员每日看课统计菜单');
+
+-- 按钮父菜单ID
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计查询', @parentId, '1',  '#', '', 1, 0, 'F', '0', '0', 'course:statistics:query',        '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计新增', @parentId, '2',  '#', '', 1, 0, 'F', '0', '0', 'course:statistics:add',          '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计修改', @parentId, '3',  '#', '', 1, 0, 'F', '0', '0', 'course:statistics:edit',         '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计删除', @parentId, '4',  '#', '', 1, 0, 'F', '0', '0', 'course:statistics:remove',       '#', 'admin', sysdate(), '', null, '');
+
+insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
+values('会员每日看课统计导出', @parentId, '5',  '#', '', 1, 0, 'F', '0', '0', 'course:statistics:export',       '#', 'admin', sysdate(), '', null, '');
+
+
+

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

@@ -976,6 +976,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         o.create_time DESC
     </select>
 
+    <!-- 统计当天各公司的观看人数和完播人数, 存到redis中,定时任务每 ? 分钟执行一次    -->
+    <select id="watchCourseStatisticsGroupByCompany" resultType="com.fs.statis.dto.WatchCourseStatisticsResultDTO">
+        SELECT
+            o.company_id AS companyId,
+            c.company_name AS companyName,
+            o.send_type,
+            COUNT(DISTINCT o.user_id) AS watchUserCount,
+            COUNT(o.log_id) AS watchCount,
+            sum(case when o.log_type = 2 then 1 else 0 end)  AS finishCount,
+            COUNT(DISTINCT CASE WHEN o.log_type = 2 THEN o.user_id END) AS finishUserCount
+        FROM
+            fs_course_watch_log o
+            LEFT JOIN company c ON c.company_id = o.company_id
+        WHERE
+            o.create_time &gt;= #{params.startTime}
+            AND o.create_time &lt;= #{params.endTime}
+        GROUP BY
+            o.company_id,
+            o.send_type
+    </select>
+
     <select id="getWatchCourseByVideoId" resultType="com.fs.course.domain.FsCourseWatchLog">
         SELECT
             *

+ 248 - 0
fs-service/src/main/resources/mapper/course/FsUserCourseCompanyStatisticsMapper.xml

@@ -0,0 +1,248 @@
+<?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.FsUserCourseCompanyStatisticsMapper">
+
+    <resultMap type="FsUserCourseCompanyStatistics" id="FsUserCourseCompanyStatisticsResult">
+        <result property="id"    column="id"    />
+        <result property="projectId"    column="project_id"    />
+        <result property="watchCount"    column="watch_count"    />
+        <result property="answerCount"    column="answer_count"    />
+        <result property="correctCount"    column="correct_count"    />
+        <result property="receiveCount"    column="receive_count"    />
+        <result property="receiveAmount"    column="receive_amount"    />
+        <result property="userCount"    column="user_count"    />
+        <result property="userBlacklistCount"    column="user_blacklist_count"    />
+        <result property="companyId"    column="company_id"    />
+        <result property="companyName"    column="company_name"    />
+        <result property="createDate"    column="create_date"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+        <result property="createBy"    column="create_by"    />
+        <result property="updateBy"    column="update_by"    />
+    </resultMap>
+
+    <sql id="selectFsUserCourseCompanyStatisticsVo">
+        select id, project_id,  complete_watch_count, watch_count, answer_count, correct_count,receive_count, receive_amount, user_count, user_blacklist_count, company_id, company_name, create_date, create_time, update_time, create_by, update_by from fs_user_course_company_statistics
+    </sql>
+
+    <select id="selectFsUserCourseCompanyStatisticsList" parameterType="FsUserCourseCompanyStatistics" resultMap="FsUserCourseCompanyStatisticsResult">
+        <include refid="selectFsUserCourseCompanyStatisticsVo"/>
+        <where>
+            <if test="projectId != null "> and project_id = #{projectId}</if>
+            <if test="watchCount != null "> and watch_count = #{watchCount}</if>
+            <if test="answerCount != null "> and answer_count = #{answerCount}</if>
+            <if test="correctCount != null "> and correct_count = #{correctCount}</if>
+            <if test="receiveCount != null "> and receive_count = #{receiveCount}</if>
+            <if test="receiveAmount != null "> and receive_amount = #{receiveAmount}</if>
+            <if test="userCount != null "> and user_count = #{userCount}</if>
+            <if test="userBlacklistCount != null "> and user_blacklist_count = #{userBlacklistCount}</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="createDate != null "> and create_date = #{createDate}</if>
+        </where>
+    </select>
+
+    <select id="selectFsUserCourseCompanyStatisticsById" parameterType="Long" resultMap="FsUserCourseCompanyStatisticsResult">
+        <include refid="selectFsUserCourseCompanyStatisticsVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertFsUserCourseCompanyStatistics" parameterType="FsUserCourseCompanyStatistics" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_user_course_company_statistics
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="projectId != null">project_id,</if>
+            <if test="watchCount != null">watch_count,</if>
+            <if test="completeWatchCount != null">complete_watch_count,</if>
+            <if test="answerCount != null">answer_count,</if>
+            <if test="correctCount != null">correct_count,</if>
+            <if test="receiveCount != null">receive_count,</if>
+            <if test="receiveAmount != null">receive_amount,</if>
+            <if test="userCount != null">user_count,</if>
+            <if test="userBlacklistCount != null">user_blacklist_count,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="companyName != null">company_name,</if>
+            <if test="createDate != null">create_date,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateBy != null">update_by,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="projectId != null">#{projectId},</if>
+            <if test="watchCount != null">#{watchCount},</if>
+            <if test="completeWatchCount != null">#{completeWatchCount},</if>
+            <if test="answerCount != null">#{answerCount},</if>
+            <if test="correctCount != null">#{correctCount},</if>
+            <if test="receiveCount != null">#{receiveCount},</if>
+            <if test="receiveAmount != null">#{receiveAmount},</if>
+            <if test="userCount != null">#{userCount},</if>
+            <if test="userBlacklistCount != null">#{userBlacklistCount},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="companyName != null">#{companyName},</if>
+            <if test="createDate != null">#{createDate},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+         </trim>
+    </insert>
+
+    <update id="updateFsUserCourseCompanyStatistics" parameterType="FsUserCourseCompanyStatistics">
+        update fs_user_course_company_statistics
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="projectId != null">project_id = #{projectId},</if>
+            <if test="watchCount != null">watch_count = #{watchCount},</if>
+            <if test="answerCount != null">answer_count = #{answerCount},</if>
+            <if test="correctCount != null">correct_count = #{correctCount},</if>
+            <if test="receiveCount != null">receive_count = #{receiveCount},</if>
+            <if test="receiveAmount != null">receive_amount = #{receiveAmount},</if>
+            <if test="userCount != null">user_count = #{userCount},</if>
+            <if test="userBlacklistCount != null">user_blacklist_count = #{userBlacklistCount},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="companyName != null">company_name = #{companyName},</if>
+            <if test="createDate != null">create_date = #{createDate},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsUserCourseCompanyStatisticsById" parameterType="Long">
+        delete from fs_user_course_company_statistics where id = #{id}
+    </delete>
+
+    <delete id="deleteFsUserCourseCompanyStatisticsByIds" parameterType="String">
+        delete from fs_user_course_company_statistics where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+
+    <select id="selectStatisticsByDate" resultType="FsUserCourseCompanyStatistics">
+        WITH watch_stats AS (
+<!--            看课统计-->
+        SELECT
+        project,
+        COUNT(DISTINCT CASE WHEN log_type = 2 THEN user_id END) AS courseCompleteNum,
+        COUNT(DISTINCT CASE WHEN log_type != 3 THEN user_id END) AS courseWatchNum
+        FROM fs_course_watch_log
+        WHERE create_time &gt; #{startTime}
+        AND create_time &lt; #{endTime}
+        AND company_id = #{companyId}
+        GROUP BY project
+        ),
+        answer_stats AS (
+
+<!--        答题统计-->
+        SELECT
+        l.project,
+        COUNT(DISTINCT a.user_id) as answerNum,
+        COUNT(DISTINCT CASE WHEN a.is_right = 1 THEN a.user_id END) as answerRightNum
+        FROM fs_course_answer_logs a
+        INNER JOIN fs_course_watch_log l ON a.watch_log_id = l.log_id
+        WHERE a.create_time &gt;= #{startTime}
+        AND a.create_time &lt; #{endTime}
+        AND a.company_id = #{companyId}
+        GROUP BY l.project
+        ),
+        redpacket_stats AS (
+
+<!--        红包统计-->
+        SELECT
+        l.project,
+        COUNT(r.log_id) as redPacketNum,
+        IFNULL(SUM(r.amount), 0) as redPacketAmount
+        FROM fs_course_red_packet_log r
+        INNER JOIN fs_course_watch_log l ON r.watch_log_id = l.log_id
+        WHERE r.create_time &gt;= #{startTime}
+        AND r.create_time &lt; #{endTime}
+        AND r.company_id = #{companyId}
+        GROUP BY l.project
+        ),
+        user_stats AS (
+
+<!--        用户统计-->
+        SELECT
+        project_id as project,
+        COUNT(DISTINCT user_id) as userCount,
+        COUNT(DISTINCT CASE WHEN status = 2 THEN user_id END) as blacklist
+        FROM fs_user_company_user
+        WHERE create_time &gt;= #{startTime}
+        AND create_time &lt; #{endTime}
+        AND company_id = #{companyId}
+        GROUP BY project_id
+        )
+
+<!--        合并数据-->
+        SELECT
+        w.project AS projectId,
+        w.courseCompleteNum AS completeWatchCount,
+        w.courseWatchNum AS watchCount,
+        COALESCE(a.answerNum, 0) AS answerCount,
+        COALESCE(a.answerRightNum, 0) AS correctCount,
+        COALESCE(r.redPacketNum, 0) AS receiveCount,
+        COALESCE(r.redPacketAmount, 0) AS receiveAmount,
+        COALESCE(u.userCount, 0) AS userCount,
+        COALESCE(u.blacklist, 0) AS userBlacklistCount
+        FROM watch_stats w
+        LEFT JOIN answer_stats a ON w.project = a.project
+        LEFT JOIN redpacket_stats r ON w.project = r.project
+        LEFT JOIN user_stats u ON w.project = u.project
+    </select>
+
+
+    <select id="selectFsUserCourseCompanyStatisticsTotal" parameterType="FsUserCourseCompanyStatistics" resultType="FsUserCourseCompanyStatistics">
+        SELECT
+        id,
+        project_id,
+        COALESCE(SUM(complete_watch_count), 0) AS complete_watch_count,
+        COALESCE(SUM(watch_count), 0) AS watch_count,
+        COALESCE(SUM(answer_count), 0) AS answer_count,
+        COALESCE(SUM(correct_count), 0) AS correct_count,
+        COALESCE(SUM(receive_count), 0) AS receive_count,
+        COALESCE(SUM(receive_amount), 0) AS receive_amount,
+        COALESCE(SUM(user_count), 0) AS user_count,
+        COALESCE(SUM(user_blacklist_count), 0) AS user_blacklist_count,
+        company_id,
+        company_name,
+       create_date,
+       create_time,
+       update_time,
+       create_by,
+       update_by
+        FROM fs_user_course_company_statistics
+        <where>
+            <if test="projectId != null">AND project_id = #{projectId}</if>
+            <if test="watchCount != null">AND watch_count = #{watchCount}</if>
+            <if test="answerCount != null">AND answer_count = #{answerCount}</if>
+            <if test="correctCount != null">AND correct_count = #{correctCount}</if>
+            <if test="receiveCount != null">AND receive_count = #{receiveCount}</if>
+            <if test="receiveAmount != null">AND receive_amount = #{receiveAmount}</if>
+            <if test="userCount != null">AND user_count = #{userCount}</if>
+            <if test="userBlacklistCount != null">AND user_blacklist_count = #{userBlacklistCount}</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="createDate != null">AND create_date = #{createDate}</if>
+
+            <!-- ✅ 新增时间筛选 -->
+            <if test="beginTime != null and beginTime != ''">
+                AND DATE(create_date) <![CDATA[ >= ]]> #{beginTime}
+            </if>
+            <if test="endTime != null and endTime != ''">
+                AND DATE(create_date) <![CDATA[ <= ]]> #{endTime}
+            </if>
+        </where>
+
+        <!-- 排序:合计行放最后 -->
+        ORDER BY create_time DESC
+    </select>
+
+
+</mapper>

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

@@ -76,10 +76,10 @@
             <if test="trainingCampId != null"> and fs_user_course_period.training_camp_id = #{trainingCampId}</if>
             <if test="periodName != null  and periodName != ''"> and period_name like concat('%', #{periodName}, '%')</if>
             <if test="companyIdList != null and companyIdList.size() > 0 ">
-                and fs_user_course_period.company_id in
+                and
                 <foreach item="companyId" index="index" collection="companyIdList"
-                         open="(" separator="," close=")">
-                    #{companyId}
+                         open="(" separator="or" close=")">
+                    FIND_IN_SET(#{companyId}, fs_user_course_period.company_id)
                 </foreach>
             </if>
             <if test="periodStartingTime != null "> and period_starting_time &gt;= #{periodStartingTime}</if>

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

@@ -60,6 +60,7 @@
             <if test="userId != null "> and user_id = #{userId}</if>
             <if test="projectId != null "> and project_id = #{projectId}</if>
             <if test="userId != null "> and user_id = #{userId}</if>
+            <if test="isDel != null "> and is_del = #{isDel}</if>
         </where>
     </select>
 
@@ -395,6 +396,18 @@
         update fs_user_course_video set red_packet_money = #{redPacketMoney} where video_id = #{videoId}
     </update>
 
+    <update id="batchUpdateByVideoId">
+        UPDATE fs_user_course_video
+        SET course_sort =
+        <foreach collection="list" item="item" index="index" separator=" " open="CASE video_id" close="END">
+            WHEN #{item.videoId} THEN #{item.courseSort}
+        </foreach>
+        WHERE video_id IN
+        <foreach collection="list" item="item" index="index" open="(" close=")" separator=",">
+            #{item.videoId}
+        </foreach>
+    </update>
+
 
     <select id="selectByFileKey"  resultMap="FsUserCourseVideoResult">
         <include refid="selectFsUserCourseVideoVo"/>
@@ -471,4 +484,16 @@
             #{videoId}
         </foreach>
     </select>
+    <select id="selectFsUserCourseVideoVoByVideoIdAndCourdeId" resultType="com.fs.course.vo.FsUserCourseVO">
+        select
+            video.video_id,
+            video.title,
+            course.course_id,
+            course.course_name
+        from `fs_user_course_video` video
+                 left join fs_user_course course ON video.course_id = course.course_id
+        where course.is_del = 0 and video.video_id = #{videoId}
+           and video.is_del = 0  and video.course_id= #{courseId}
+            limit 1
+    </select>
 </mapper>

+ 1 - 0
fs-service/src/main/resources/mapper/his/FsExportTaskMapper.xml

@@ -29,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
             <if test="startTime != null "> and start_time = #{startTime}</if>
             <if test="finishTime != null "> and finish_time = #{finishTime}</if>
+            <if test="status != null "> and status = #{status}</if>
             <if test="fileUrl != null  and fileUrl != ''"> and file_url = #{fileUrl}</if>
         </where>
         order by task_id desc

+ 1 - 1
fs-service/src/main/resources/mapper/his/FsPackageMapper.xml

@@ -74,7 +74,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectIcdNameByPackageId" resultType="java.lang.String">
         SELECT i.icd_name
         FROM fs_package p
-                 LEFT JOIN fs_icd i ON FIND_IN_SET(i.icd_code, p.icd_code) > 0
+                 inner join fs_icd i ON FIND_IN_SET(i.icd_code, p.icd_code) > 0
         WHERE package_id = #{packageId}
     </select>
 

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

@@ -554,6 +554,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isAddQw != null">is_add_qw,</if>
             <if test="courseMaOpenId != null">course_ma_open_id,</if>
             <if test="qwExtId != null">qw_ext_id,</if>
+            <if test="orderCount != null">order_count,</if>
             <if test="companyId != null">company_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
          </trim>
@@ -596,6 +597,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="isAddQw != null">#{isAddQw},</if>
             <if test="courseMaOpenId != null">#{courseMaOpenId},</if>
             <if test="qwExtId != null">#{qwExtId},</if>
+            <if test="orderCount != null">#{orderCount},</if>
             <if test="companyId != null">#{companyId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
          </trim>
@@ -644,6 +646,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="parentId != null">parent_id = #{parentId},</if>
             <if test="qwExtId != null">qw_ext_id = #{qwExtId},</if>
             <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="orderCount != null">order_count = #{orderCount},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
         </trim>
         where user_id = #{userId}

+ 9 - 4
fs-service/src/main/resources/mapper/hisStore/FsStoreCouponScrmMapper.xml

@@ -41,10 +41,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="type != null "> and type = #{type}</if>
             <if test="type == null "> and type != 3</if>
             <if test="isDel != null "> and is_del = #{isDel}</if>
-            <if test="beginTime != null and beginTime != ''">
-            and date_format(create_time,'%y%m%d') &gt;= date_format(#{beginTime},'%y%m%d') </if>
-            <if test="endTime != null and endTime != ''">
-            and date_format(create_time,'%y%m%d') &lt;= date_format(#{endTime},'%y%m%d') </if>
+            <if test="params != null">
+                <if test="params.beginTime != null and params.beginTime != ''">
+                    and date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
+                </if>
+                <if test="params.endTime != null and params.endTime != ''">
+                    and date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
+                </if>
+            </if>
+
         </where>
         order by coupon_price
     </select>

+ 10 - 10
fs-service/src/main/resources/mapper/hisStore/SysOperLogScrmMapper.xml

@@ -56,37 +56,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 			left join fs_store_product_scrm fp on fp.product_id = sol.main_id and sol.main_type = '商品'
 		<where>
 			<if test="mainType != null and mainType != ''">
-				AND main_type = #{mainType}
+				AND sol.main_type = #{mainType}
 			</if>
 			<if test="title != null and title != ''">
-				AND title like concat('%', #{title}, '%')
+				AND sol.title like concat('%', #{title}, '%')
 			</if>
 			<if test="businessType != null and businessType != ''">
-				AND business_type = #{businessType}
+				AND sol.business_type = #{businessType}
 			</if>
 			<if test="businessTypes != null and businessTypes.length > 0">
-			    AND business_type in
+			    AND sol.business_type in
 			    <foreach collection="businessTypes" item="businessType" open="(" separator="," close=")">
 		 			#{businessType}
 		        </foreach>
 			</if>
 			<if test="status != null">
-				AND status = #{status}
+				AND sol.status = #{status}
 			</if>
 			<if test="operName != null and operName != ''">
-				AND oper_name like concat('%', #{operName}, '%')
+				AND sol.oper_name like concat('%', #{operName}, '%')
 			</if>
 			<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
-				and date_format(oper_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
+				and date_format(sol.oper_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
 			</if>
 			<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
-				and date_format(oper_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
+				and date_format(sol.oper_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
 			</if>
 			<if test="mainId != null">
-				and main_id = #{mainId}
+				and sol.main_id = #{mainId}
 			</if>
 		</where>
-		order by oper_id desc
+		order by sol.oper_id desc
 	</select>
 
 

+ 2 - 2
fs-service/src/main/resources/mapper/qw/QwFriendWelcomeMapper.xml

@@ -64,13 +64,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 and company_id = #{companyId}
             </if>
             <if test="createTime != null ">
-                and create_time = #{createTime}
+                and DATE(create_time) = #{createTime}
             </if>
             <if test="corpId != null ">
                 and corp_id = #{corpId}
             </if>
             <if test="updateTime != null ">
-                and update_time = #{updateTime}
+                and DATE(update_time) = #{updateTime}
             </if>
             ${params.dataScope}
         </where>

+ 1 - 0
fs-service/src/main/resources/mapper/qw/QwUserMapper.xml

@@ -43,6 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectQwUserList" parameterType="QwUser" resultMap="QwUserResult">
         <include refid="selectQwUserVo"/>
         <where>
+            1=1 and is_del = 0 and company_user_id is not null
             <if test="qwUserId != null  and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
             <if test="qwUserName != null  and qwUserName != ''"> and qw_user_name like concat('%', #{qwUserName}, '%')</if>
             <if test="department != null  and department != ''"> and department = #{department}</if>

+ 4 - 0
fs-service/src/main/resources/mapper/sop/SopUserLogsMapper.xml

@@ -207,6 +207,10 @@
         where a.start_time &lt;= Now()
           and a.status = 1
           and b.send_type != 4 and b.status in (2,3)
+        <if test="sopIds != null and !sopIds.isEmpty()">
+            and a.sop_id in
+            <foreach collection="sopIds" open="(" close=")" index="index" item="item" separator=",">#{item}</foreach>
+        </if>
     </select>
 
     <select id="meetsTheRatingByUserInfo"   resultMap="SopUserLogsResult">

+ 13 - 5
fs-user-app/src/main/java/com/fs/app/controller/WxH5MpController.java

@@ -3,6 +3,7 @@ package com.fs.app.controller;
 import cn.binarywang.wx.miniapp.api.WxMaService;
 import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.hutool.core.date.DateTime;
+import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.param.FsUserLoginByCourseMpParam;
 import com.fs.app.param.FsUserLoginByMpParam;
 import com.fs.app.utils.JwtUtils;
@@ -18,6 +19,7 @@ import com.fs.course.mapper.FsCourseWatchLogMapper;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
+import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.service.IFsUserWxService;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -77,6 +79,7 @@ public class WxH5MpController {
 
     @ApiOperation("课程分享链接公众号登录")
     @PostMapping("/loginByMp")
+    @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
     public R loginByMp(@Valid @RequestBody FsUserLoginByMpParam param) {
         log.info("=====================进入小程序授权登录, 入参: {}", param);
 
@@ -109,11 +112,11 @@ public class WxH5MpController {
             // 处理用户信息
             FsUser user = processUserInfo(wxMpUser, company,companyUser,param);
 
-            // 检查用户是否已绑定其他销售
-            FsUserCompanyUser userCompanyUser = userCompanyUserService.selectByUserIdAndProjectId(user.getUserId(), param.getProjectId());
-            if (Objects.nonNull(userCompanyUser) && !param.getCompanyUserId().equals(userCompanyUser.getCompanyUserId())){
-                return R.error(500, "该用户("+user.getUserId() + ")已成为其他销售会员");
-            }
+//            // 检查用户是否已绑定其他销售
+//            FsUserCompanyUser userCompanyUser = userCompanyUserService.selectByUserIdAndProjectId(user.getUserId(), param.getProjectId());
+//            if (Objects.nonNull(userCompanyUser) && !param.getCompanyUserId().equals(userCompanyUser.getCompanyUserId())){
+//                return R.error(500, "该用户("+user.getUserId() + ")已成为其他销售会员");
+//            }
 
 //            // 处理用户与公司的关系
 //            processUserCompanyRelationship(user, param, companyUser, company);
@@ -132,6 +135,7 @@ public class WxH5MpController {
 
     @ApiOperation("炮灰小程序-公众号登录转小程序")
     @PostMapping("/courseLoginByMp")
+    @UserOperationLog(operationType = FsUserOperationEnum.H5LOGIN)
     public R courseLoginByMp(@Valid @RequestBody FsUserLoginByCourseMpParam param) {
         log.info("=====================进入炮灰小程序-公众号授权登录, 入参: {}", param);
 
@@ -160,6 +164,10 @@ public class WxH5MpController {
                 return R.error("未绑定开放平台");
             }
 
+            if (StringUtils.isEmpty(wxMpUser.getUnionId())){
+                return R.error("未绑定开放平台");
+            }
+
             // 处理用户信息
             FsUser user = processUserInfoByCourseLoginByMp(wxMpUser);
 

+ 3 - 0
fs-user-app/src/main/java/com/fs/app/controller/WxUserController.java

@@ -7,6 +7,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
 import cn.hutool.core.date.DateTime;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.annotation.Login;
+import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.utils.JwtUtils;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
@@ -21,6 +22,7 @@ import com.fs.core.config.WxMpConfiguration;
 import com.fs.course.config.CourseMaConfig;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.*;
+import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.his.mapper.FsUserLoginLogMapper;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.service.IFsUserWxService;
@@ -97,6 +99,7 @@ public class WxUserController extends AppBaseController{
     @ApiOperation("登录")
     @PostMapping("/login")
     @Transactional
+    @UserOperationLog(operationType = FsUserOperationEnum.MINLOGIN)
     public R login( @RequestBody LoginParam param) {
         if (StringUtils.isBlank(param.getCode())) {
             return R.error("code不存在");

+ 5 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java

@@ -4,6 +4,7 @@ package com.fs.app.controller.course;
 import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.fs.app.annotation.Login;
+import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.controller.AppBaseController;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
@@ -18,6 +19,7 @@ import com.fs.course.param.*;
 import com.fs.course.service.*;
 import com.fs.course.service.impl.TencentCloudCosService;
 import com.fs.course.vo.*;
+import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.his.service.IFsIntegralGoodsService;
 import com.fs.sop.domain.QwSop;
 import com.fs.sop.service.IQwSopService;
@@ -112,6 +114,7 @@ public class CourseQwController extends AppBaseController {
     @Login
     @ApiOperation("h5课程详情加问答")
     @GetMapping("/getH5CourseVideoDetails")
+    @UserOperationLog(operationType = FsUserOperationEnum.STUDY)
     public R getCourseVideoDetails(FsUserCourseVideoFinishUParam param)
     {
         param.setUserId(Long.parseLong(getUserId()));
@@ -210,6 +213,7 @@ public class CourseQwController extends AppBaseController {
     @ApiOperation("答题")
     @PostMapping("/courseAnswer")
     @Login
+    @UserOperationLog(operationType = FsUserOperationEnum.ANSWER)
     public R courseAnswer(@RequestBody FsCourseQuestionAnswerUParam param)
     {
         logger.info("zyp \n【答题】:{}", JSON.toJSONString(param));
@@ -231,6 +235,7 @@ public class CourseQwController extends AppBaseController {
     @ApiOperation("发放奖励")
     @PostMapping("/sendReward")
     @RepeatSubmit
+    @UserOperationLog(operationType = FsUserOperationEnum.SENDREWARD)
     public R sendReward(@RequestBody @Valid FsCourseSendRewardUParam param)
     {
         param.setUserId(Long.parseLong(getUserId()));

+ 3 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwLoginController.java

@@ -5,6 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
 import cn.hutool.core.date.DateTime;
 import com.alibaba.fastjson.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.app.annotation.UserOperationLog;
 import com.fs.app.controller.AppBaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
@@ -16,6 +17,7 @@ import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserWx;
+import com.fs.his.enums.FsUserOperationEnum;
 import com.fs.his.mapper.FsUserLoginLogMapper;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.service.IFsUserWxService;
@@ -69,6 +71,7 @@ public class CourseQwLoginController extends AppBaseController {
     @ApiOperation("小程序看课登录")
     @PostMapping("/courseLogin")
     @Transactional
+    @UserOperationLog(operationType = FsUserOperationEnum.MINLOGIN)
     public R courseLogin(@RequestBody LoginParam param) {
         List<FsCoursePlaySourceConfig> list = fsCoursePlaySourceConfigService.list(new QueryWrapper<FsCoursePlaySourceConfig>().ne("type", 2).eq("is_del", 0));
         if (list.isEmpty()){

+ 87 - 34
fs-user-app/src/main/java/com/fs/framework/aspectj/UserOperationLogAspect.java

@@ -9,12 +9,14 @@ import com.fs.common.core.domain.R;
 import com.fs.common.param.LoginMaWxParam;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.mapper.FsUserCourseVideoMapper;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
 import com.fs.course.param.FsCourseSendRewardUParam;
 import com.fs.course.param.newfs.FsUserCourseVideoLinkParam;
 import com.fs.course.vo.FsUserCourseVO;
+import com.fs.course.vo.FsUserCourseVideoH5DVO;
 import com.fs.course.vo.newfs.FsUserCourseVideoPageListVO;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserOperationLog;
@@ -83,7 +85,14 @@ public class UserOperationLogAspect {
                 }
             }
 
+
             if (operationLog.getUserId() != null) {
+                if ("学习课程".equals(operationLog.getOperationType())){
+                    if (StringUtils.isBlank(operationLog.getDetails())){
+                        operationLog = extractCourseFromResult(result);
+
+                    }
+                }
                 logMapper.insertFsUserOperationLog(operationLog);
             }
         } catch (Exception ex) {
@@ -141,22 +150,26 @@ public class UserOperationLogAspect {
 
     private StringBuilder getDetail(UserOperationLog annotation, FsUserOperationEnum opType, FsUser fsUser,Object[] args) {
         StringBuilder details = new StringBuilder();
+        String nickName = fsUser.getNickName();
+        if (StringUtils.isBlank(nickName)){
+            nickName = fsUser.getNickname();
+        }
         if (annotation.detail() == null || annotation.detail().isEmpty()) {
             switch (opType.getValue()) {
                 case 1: // 小程序登录
-                    details.append(fsUser.getNickName())
+                    details.append(nickName)
                             .append("在")
                             .append(DateUtils.getTime())
                             .append("登录了小程序");
                     break;
                 case 2: // h5登录
-                    details.append(fsUser.getNickName())
+                    details.append(nickName)
                             .append("在")
                             .append(DateUtils.getTime())
                             .append("登录了h5");
                     break;
                 case 3: // 成为会员
-                    details.append(fsUser.getNickName())
+                    details.append(nickName)
                             .append("在")
                             .append(DateUtils.getTime())
                             .append("注册成为会员");
@@ -165,14 +178,16 @@ public class UserOperationLogAspect {
                     break;
                 case 5: // 学习课程
                     String courseInfo = extractCourseInfo(args);
-                    details.append(fsUser.getNickName())
-                            .append("在")
-                            .append(DateUtils.getTime())
-                            .append("观看 ").append(courseInfo);
+                    if (courseInfo != null) {
+                        details.append(nickName)
+                                .append("在")
+                                .append(DateUtils.getTime())
+                                .append("观看 ").append(courseInfo);
+                    }
                     break;
                 case 6: // 答题
                     String answerCourse = answerCourse(args);
-                    details.append(fsUser.getNickName())
+                    details.append(nickName)
                             .append("在 ")
                             .append(DateUtils.getTime())
                             .append("\n")
@@ -181,10 +196,10 @@ public class UserOperationLogAspect {
                 case 7: // 发送奖励
                     String sendReward = sendReward(args);
 
-                    details.append(fsUser.getNickName())
+                    details.append(nickName)
                             .append("在 ")
                             .append(DateUtils.getTime())
-                            .append("领取红包")
+                            .append("领取 红包/积分 奖励")
                             .append("\n")
                             .append(sendReward);
                     break;
@@ -215,6 +230,30 @@ public class UserOperationLogAspect {
         }
         return null;
     }
+    private FsUserOperationLog extractCourseFromResult(Object result) {
+        if (result instanceof R) {
+            R r = (R) result;
+            Object course = r.get("course");
+            if (course instanceof FsUserCourseVideoH5DVO) {
+                FsUserOperationLog operationLog = LOG_HOLDER.get();
+                FsUser fsUser = userMapper.selectFsUserByUserId(operationLog.getUserId());
+                FsUserCourseVideoH5DVO vo = (FsUserCourseVideoH5DVO) course;
+                operationLog.setParam(JSON.toJSONString(vo));
+                StringBuilder details = new StringBuilder();
+                String nickName = fsUser.getNickName();
+                if (StringUtils.isBlank(nickName)){
+                    nickName = fsUser.getNickname();
+                }
+                details.append(nickName)
+                        .append("在")
+                        .append(DateUtils.getTime())
+                        .append("观看 课程:").append(vo.getCourseName()).append(" 的 ").append(vo.getTitle()).append(" 小节");
+                operationLog.setDetails(details.toString());
+                return operationLog;
+            }
+        }
+        return null;
+    }
     private String extractCourseInfo(Object[] args) {
         if (args == null) return "未知课程";
 
@@ -230,6 +269,8 @@ public class UserOperationLogAspect {
                         return "课程:"+ vo.getCourseName() + " 的 " + vo.getTitle() + " 小节";
                     }
                 }
+            } else {
+                return null; //自动发课 接口执行完生成
             }
         }
         return "未知课程";
@@ -240,22 +281,28 @@ public class UserOperationLogAspect {
         for (Object arg : args) {
             if (arg instanceof FsCourseQuestionAnswerUParam) {
                 FsCourseQuestionAnswerUParam param = (FsCourseQuestionAnswerUParam) arg;
-                if (param.getVideoId() != null && param.getPeriodId() != null) {
-                    FsUserCourseVO vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoId(param.getVideoId(), param.getPeriodId());
-                    if (vo != null){
-                        FsUserOperationLog operationLog = LOG_HOLDER.get();
-                        operationLog.setParam(JSON.toJSONString(vo));
-                        LOG_HOLDER.set(operationLog);
-                        StringBuilder details =new StringBuilder("课程:"+ vo.getCourseName() + " 的 " + vo.getTitle() + " 小节 问题为:");
-                        List<FsCourseQuestionBank> questions = param.getQuestions();
-                        if (questions != null && !questions.isEmpty()) {
-                            for (int i = 0; i < questions.size(); i++) {
-                                details.append("\n").append(i+1).append(".").append(questions.get(i).getTitle());
-                                details.append(" 提交答案为:").append(questions.get(i).getAnswer());
-                            }
+                Long videoId = param.getVideoId();
+                Long periodId = param.getPeriodId();
+                Long courseId = param.getCourseId();
+                FsUserCourseVO vo = null;
+                if (param.getQwExternalId() != null){ //自动看课
+                    vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoIdAndCourdeId(videoId,courseId);
+                } else if (videoId != null && param.getPeriodId() != null) {  //手动看课
+                    vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoId(videoId, periodId);
+                }
+                if (vo != null){
+                    FsUserOperationLog operationLog = LOG_HOLDER.get();
+                    operationLog.setParam(JSON.toJSONString(vo));
+                    LOG_HOLDER.set(operationLog);
+                    StringBuilder details =new StringBuilder("课程:"+ vo.getCourseName() + " 的 " + vo.getTitle() + " 小节 问题为:");
+                    List<FsCourseQuestionBank> questions = param.getQuestions();
+                    if (questions != null && !questions.isEmpty()) {
+                        for (int i = 0; i < questions.size(); i++) {
+                            details.append("\n").append(i+1).append(".").append(questions.get(i).getTitle());
+                            details.append(" 提交答案为:").append(questions.get(i).getAnswer());
                         }
-                        return details.toString();
                     }
+                    return details.toString();
                 }
             }
         }
@@ -267,16 +314,22 @@ public class UserOperationLogAspect {
         for (Object arg : args) {
             if (arg instanceof FsCourseSendRewardUParam) {
                 FsCourseSendRewardUParam param = (FsCourseSendRewardUParam) arg;
-                if (param.getVideoId() != null && param.getPeriodId() != null) {
-                    FsUserCourseVO vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoId(param.getVideoId(), param.getPeriodId());
-                    if (vo != null){
-                        FsUserOperationLog operationLog = LOG_HOLDER.get();
-                        operationLog.setParam(JSON.toJSONString(vo));
-                        LOG_HOLDER.set(operationLog);
-                        StringBuilder details =new StringBuilder();
-                        details.append("课程:"+ vo.getCourseName() + " 的 " + vo.getTitle() + " 小节");
-                        return details.toString();
-                    }
+                Long videoId = param.getVideoId();
+                Long periodId = param.getPeriodId();
+                Long courseId = param.getCourseId();
+                FsUserCourseVO vo = null;
+                if (param.getQwExternalId() != null){ //自动看课
+                    vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoIdAndCourdeId(videoId,courseId);
+                } else if (videoId != null && param.getPeriodId() != null) {  //手动看课
+                    vo = userCourseVideoMapper.selectFsUserCourseVideoVoByVideoId(videoId, periodId);
+                }
+                if (vo != null){
+                    FsUserOperationLog operationLog = LOG_HOLDER.get();
+                    operationLog.setParam(JSON.toJSONString(vo));
+                    LOG_HOLDER.set(operationLog);
+                    StringBuilder details =new StringBuilder();
+                    details.append("课程:"+ vo.getCourseName() + " 的 " + vo.getTitle() + " 小节");
+                    return details.toString();
                 }
             }
         }