Kaynağa Gözat

Merge remote-tracking branch 'origin/master'

xgb 1 hafta önce
ebeveyn
işleme
f1bd8e8c35
66 değiştirilmiş dosya ile 2171 ekleme ve 235 silme
  1. 4 1
      fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.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. 42 33
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  7. 17 4
      fs-admin/src/main/java/com/fs/his/task/Task.java
  8. 6 1
      fs-admin/src/main/resources/logback.xml
  9. 3 1
      fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  10. 8 8
      fs-ipad-task/src/main/java/com/fs/app/service/IpadSendServer.java
  11. 10 10
      fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java
  12. 6 2
      fs-qw-task/src/main/java/com/fs/app/controller/CommonController.java
  13. 1 1
      fs-qw-task/src/main/java/com/fs/app/task/qwTask.java
  14. 2 1
      fs-qw-task/src/main/java/com/fs/app/taskService/SopLogsTaskService.java
  15. 29 10
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  16. 6 4
      fs-service/src/main/java/com/fs/company/mapper/CompanyRechargeMapper.java
  17. 1 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  18. 83 0
      fs-service/src/main/java/com/fs/course/domain/FsUserCourseCompanyStatistics.java
  19. 21 5
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  20. 69 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseCompanyStatisticsMapper.java
  21. 1 1
      fs-service/src/main/java/com/fs/course/mapper/FsUserCoursePeriodDaysMapper.java
  22. 10 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  23. 69 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseCompanyStatisticsService.java
  24. 8 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  25. 237 0
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCompanyStatisticsServiceImpl.java
  26. 108 42
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  27. 21 3
      fs-service/src/main/java/com/fs/course/vo/FsCourseWatchLogStatisticsListVO.java
  28. 5 0
      fs-service/src/main/java/com/fs/his/domain/FsUser.java
  29. 58 0
      fs-service/src/main/java/com/fs/his/dto/FsUserDTO.java
  30. 6 0
      fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderMapper.java
  31. 16 1
      fs-service/src/main/java/com/fs/his/mapper/FsStorePaymentMapper.java
  32. 26 26
      fs-service/src/main/java/com/fs/his/mapper/FsUserMapper.java
  33. 16 5
      fs-service/src/main/java/com/fs/his/service/impl/FsUserServiceImpl.java
  34. 7 5
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java
  35. 6 4
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java
  36. 1 1
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  37. 1 1
      fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java
  38. 15 0
      fs-service/src/main/java/com/fs/qw/param/FsUserCourseRedPageParam.java
  39. 11 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  40. 6 3
      fs-service/src/main/java/com/fs/sop/mapper/SopUserLogsMapper.java
  41. 2 2
      fs-service/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java
  42. 5 5
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  43. 1 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsServiceImpl.java
  44. 104 0
      fs-service/src/main/resources/application-config-dev-czt.yml
  45. 2 1
      fs-service/src/main/resources/application-config-druid-hdt.yml
  46. 94 0
      fs-service/src/main/resources/application-config-druid-heyantang.yml
  47. 1 1
      fs-service/src/main/resources/application-config-druid-jnsyj.yml
  48. 1 1
      fs-service/src/main/resources/application-config-zkzh.yml
  49. 157 0
      fs-service/src/main/resources/application-dev-czt.yml
  50. 3 3
      fs-service/src/main/resources/application-dev.yml
  51. 159 0
      fs-service/src/main/resources/application-druid-heyantang.yml
  52. 1 1
      fs-service/src/main/resources/application-druid-jnmy-test.yml
  53. 63 0
      fs-service/src/main/resources/db/20251028-会员每日看课统计.sql
  54. 248 0
      fs-service/src/main/resources/mapper/course/FsUserCourseCompanyStatisticsMapper.xml
  55. 25 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  56. 1 1
      fs-service/src/main/resources/mapper/his/FsPackageMapper.xml
  57. 4 1
      fs-service/src/main/resources/mapper/his/FsUserMapper.xml
  58. 9 4
      fs-service/src/main/resources/mapper/hisStore/FsStoreCouponScrmMapper.xml
  59. 4 0
      fs-service/src/main/resources/mapper/sop/SopUserLogsMapper.xml
  60. 14 0
      fs-user-app/src/main/java/com/fs/app/controller/CourseController.java
  61. 13 5
      fs-user-app/src/main/java/com/fs/app/controller/WxH5MpController.java
  62. 3 0
      fs-user-app/src/main/java/com/fs/app/controller/WxUserController.java
  63. 1 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java
  64. 5 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwController.java
  65. 3 0
      fs-user-app/src/main/java/com/fs/app/controller/course/CourseQwLoginController.java
  66. 87 34
      fs-user-app/src/main/java/com/fs/framework/aspectj/UserOperationLogAspect.java

+ 4 - 1
fs-admin/src/main/java/com/fs/course/controller/FsCoursePlaySourceConfigController.java

@@ -48,11 +48,14 @@ public class FsCoursePlaySourceConfigController extends BaseController {
                               @RequestParam(required = false) String appid,
                               @RequestParam(required = false) Integer isMall,
                               @RequestParam(required = false, defaultValue = "1") Integer pageNum,
-                              @RequestParam(required = false, defaultValue = "10") Integer pageSize) {
+                              @RequestParam(required = false, defaultValue = "10") Integer pageSize,
+                              @RequestParam(required = false) Long companyId
+    ) {
         Map<String, Object> params = new HashMap<>();
         params.put("name", name);
         params.put("appid", appid);
         params.put("isMall", isMall);
+        params.put("companyId", companyId);
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         String json = configService.selectConfigByKey("course.config");
         CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);

+ 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

@@ -23,6 +23,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;
@@ -176,6 +177,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);
+
+    }
+
+}

+ 42 - 33
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -18,6 +18,7 @@ import com.fs.course.param.FsCourseLinkCreateParam;
 import com.fs.course.service.IFsUserCompanyUserService;
 import com.fs.course.service.IFsUserCourseService;
 import com.fs.his.domain.FsUserAddress;
+import com.fs.his.dto.FsUserDTO;
 import com.fs.his.enums.FsUserIntegralLogTypeEnum;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
@@ -121,6 +122,47 @@ public class FsUserController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 导出用户列表
+     */
+    @PreAuthorize("@ss.hasPermi('his:user:export')")
+    @Log(title = "用户", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(FsUserParam fsUser)
+    {
+        logger.info("导出用户列表:"+ SecurityUtils.getUserId());
+        if (fsUserService.isEntityNull(fsUser)){
+            return AjaxResult.error("请筛选数据导出");
+        }
+        Long count = fsUserService.selectFsUserExportListVOCount(fsUser);
+        if (count>10000){
+            return AjaxResult.error("导出数据不可超过1w条");
+        }
+        List<FsUserVO> list = fsUserService.selectFsUserListVO(fsUser);
+        SysRole sysRole = isCheckPermission();
+        List<FsUserDTO> listDTO = Lists.newArrayList();
+        for (FsUserVO fsUserVO : list) {
+            if(fsUserVO.getPhone() != null&&fsUserVO.getPhone()!=""){
+                if (!(sysRole.getIsCheckPhone()==1)){
+                    if (fsUserVO.getPhone().length()>11){
+                        fsUserVO.setPhone(decryptPhoneMk(fsUserVO.getPhone()));
+                    }else {
+                        fsUserVO.setPhone(fsUserVO.getPhone().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
+                    }
+                } else {
+                    if (fsUserVO.getPhone().length()>11) {
+                        fsUserVO.setPhone(decryptPhone(fsUserVO.getPhone()));
+                    }
+                }
+            }
+            FsUserDTO fsUserDTO = new FsUserDTO();
+            BeanUtils.copyProperties(fsUserVO,fsUserDTO);
+            listDTO.add(fsUserDTO);
+        }
+        ExcelUtil<FsUserDTO> util = new ExcelUtil<FsUserDTO>(FsUserDTO.class);
+        return util.exportExcel(listDTO, "用户数据");
+    }
+
     @Autowired
     private ISysRoleService sysRoleService;
     private SysRole isCheckPermission() {
@@ -196,37 +238,6 @@ public class FsUserController extends BaseController
         return util.exportExcel(list, "项目会员数据");
     }
 
-    /**
-     * 导出用户列表
-     */
-    @PreAuthorize("@ss.hasPermi('his:user:export')")
-    @Log(title = "用户", businessType = BusinessType.EXPORT)
-    @GetMapping("/export")
-    public AjaxResult export(FsUserParam fsUser)
-    {
-        logger.info("导出用户列表:"+ SecurityUtils.getUserId());
-        if (fsUserService.isEntityNull(fsUser)){
-            return AjaxResult.error("请筛选数据导出");
-        }
-        Long count = fsUserService.selectFsUserExportListVOCount(fsUser);
-        if (count>10000){
-            return AjaxResult.error("导出数据不可超过1w条");
-        }
-        List<FsUserExportListVO> list = fsUserService.selectFsUserExportListVO(fsUser);
-        SysRole sysRole = isCheckPermission();
-        for (FsUserExportListVO vo : list) {
-            if (vo.getMobile()!=null && !(sysRole.getIsCheckPhone()==1)){
-                vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
-            } else {
-                if (vo.getMobile().length()>11){
-                    vo.setMobile(decryptPhone(vo.getMobile()));
-                }
-            }
-        }
-        ExcelUtil<FsUserExportListVO> util = new ExcelUtil<FsUserExportListVO>(FsUserExportListVO.class);
-        return util.exportExcel(list, "用户数据");
-    }
-
     /**
      * 获取用户详细信息
      */
@@ -238,8 +249,6 @@ public class FsUserController extends BaseController
         return AjaxResult.success(fsUser);
     }
 
-
-
     @GetMapping(value = "/getUserAddr/{userId}")
     public AjaxResult getUserAddr(@PathVariable("userId") Long userId)
     {

+ 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;
@@ -72,6 +72,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;
@@ -192,6 +193,8 @@ public class Task {
 
     @Autowired
     private QwRestrictionPushRecordMapper qwRestrictionPushRecordMapper;
+    @Autowired
+    private FsUserOperationLogMapper fsUserOperationLogMapper;
 
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
 
@@ -1518,5 +1521,15 @@ public class Task {
         }
     }
 
+    //定时删除行为轨迹记录 (数据量太大 默认保留一天的)
+    @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);
+    }
+
 
 }

+ 6 - 1
fs-admin/src/main/resources/logback.xml

@@ -76,7 +76,12 @@
 	<!-- 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>
 

+ 3 - 1
fs-company-app/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -225,8 +225,10 @@ public class FsUserCourseVideoController extends AppBaseController {
     public ResponseResult<PageInfo<FsUserCourseVideoPageListVO>> todayCourseList(@RequestParam(defaultValue = "1") Integer pageNum,
                                                                                  @RequestParam(defaultValue = "10") Integer pageSize, String keyword) {
         Long companyId = getCompanyId();
+        log.info("销售小程序-今日课程,获取公司id:{}", companyId);
         if (Objects.isNull(companyId)) {
-            ResponseResult.fail(400, "未获取到公司ID,请重新登录后再试");
+            log.error("未获取到公司id,进入提示逻辑。。。。。。。。");
+            return ResponseResult.fail(400, "未获取到公司ID,请重新登录后再试");
         }
 
         Map<String, Object> params = new HashMap<>();

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

@@ -297,20 +297,20 @@ 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.debug("企微查询:{}", 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.debug("ID:{}-看课记录参数:videoID:{}, qwUserID:{}, extID:{}", qwSopLogs.getId(), setting.getVideoId().longValue(), qwUser.getId(), qwExternalContactId);
+                log.debug("ID:{}-看课记录参数:videoID:{}, qwUserID:{}, extID:{}", qwSopLogs.getId(), setting.getVideoId().longValue(), qwUser.getId(), qwSopLogs.getExternalId());
                 log.debug("ID:{}-看课记录:{}", qwSopLogs.getId(), watchLog);
                 String logId = qwSopLogs.getId();
                 if (watchLog != null) {

+ 10 - 10
fs-ipad-task/src/main/java/com/fs/app/task/SendMsg.java

@@ -264,16 +264,16 @@ public class SendMsg {
                 }
             }
             // 推送 APP
-//            if (!setting.getSetting().isEmpty()) {
-//                new Thread(() -> {
-//                    try {
-//                        List<QwSopTempSetting.Content.Setting> settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "9".equals(e.getContentType())).collect(Collectors.toList());
-//                        asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(settings, qwSopLogs.getCorpId(), user.getCompanyUserId(), qwSopLogs.getFsUserId());
-//                    } catch (Exception e) {
-//                        log.error("推送APP失败", e);
-//                    }
-//                }).start();
-//            }
+            if (!setting.getSetting().isEmpty()) {
+                new Thread(() -> {
+                    try {
+                        List<QwSopTempSetting.Content.Setting> settings = JSON.parseArray(JSON.toJSONString(setting.getSetting()), QwSopTempSetting.Content.Setting.class).stream().filter(e -> "9".equals(e.getContentType())).collect(Collectors.toList());
+                        asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(settings, qwSopLogs.getCorpId(), user.getCompanyUserId(), qwSopLogs.getFsUserId());
+                    } catch (Exception e) {
+                        log.error("推送APP失败", e);
+                    }
+                }).start();
+            }
             qwSopLogs.setSend(true);
             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
             QwSopLogs updateQwSop = new QwSopLogs();

+ 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);
 

+ 6 - 4
fs-service/src/main/java/com/fs/company/mapper/CompanyRechargeMapper.java

@@ -84,11 +84,13 @@ public interface CompanyRechargeMapper
             "<if test = 'maps.payType != null  '> " +
             "and r.pay_type = #{maps.payType}" +
             "</if>" +
-            "<if test = 'maps.beginTime != null and maps.beginTime != \"\" '> " +
-            "and date_format(r.pay_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
+            "<if test= 'maps.params != null and maps.params !=\"\"'>"+
+            "<if test = 'maps.params.beginTime != null and maps.params.beginTime != \"\" '> " +
+            "and date_format(r.pay_time,'%y%m%d') &gt;= date_format(#{maps.params.beginTime},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.endTime != null and maps.endTime != \"\" '> " +
-            "and date_format(r.pay_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
+            "<if test = 'maps.params.endTime != null and maps.params.endTime != \"\" '> " +
+            "and date_format(r.pay_time,'%y%m%d') &lt;= date_format(#{maps.params.endTime},'%y%m%d') " +
+            "</if>"+
             "</if>" +
             "order by r.recharge_id desc " +
             "</script>"})

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

@@ -16,6 +16,7 @@ public class CourseConfig implements Serializable {
     private Integer maxBufferLength;//最大缓冲时长
     private Integer videoIntegral;//每十分钟获取多少积分
     private Integer answerIntegral;//答题获得积分
+    private Integer appAnswerIntegral; //app答题积分
     private Integer defaultLine;//默认看课线路
     private String realLinkDomainName;//真链域名
     private String authDomainName;//网页授权域名

+ 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;
+
+
+}

+ 21 - 5
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -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,19 @@ 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" +
+            ",(SELECT SUM(amount) FROM fs_course_red_packet_log \n" +
+            "     WHERE user_id = o.user_id AND video_id = o.video_id) as 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 +290,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 +463,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(

+ 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
+    );
+}

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

@@ -120,6 +120,6 @@ public interface FsUserCoursePeriodDaysMapper extends BaseMapper<FsUserCoursePer
 
     Long selectFsUserCoursePeriodDaysCount(FsUserCoursePeriodDays fsUserCoursePeriodDays);
 
-    @Select("SELECT distinct period_id from fs_user_course_period_days  where start_date_time >=#{periodSTime} and end_date_time <=#{periodETime} ")
+    @Select("SELECT distinct period_id from fs_user_course_period_days  where day_date >=#{periodSTime} and day_date <=#{periodETime} ")
     List<Long> selectFsUserCoursePeriodDaysByTime(@Param("periodSTime") String periodSTime,@Param("periodETime") String periodETime);
 }

+ 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);
+
     /**
      * 删除课堂视频
      *
@@ -245,4 +253,6 @@ public interface FsUserCourseVideoMapper
      * 根据视频id集合查询列表
      */
     List<FsUserCourseVideoAppletVO> getFsUserCourseVideoAppletVOListByIds(@Param("videoIds") List<Long> videoIds);
+
+    FsUserCourseVO selectFsUserCourseVideoVoByVideoIdAndCourdeId(@Param("videoId") Long videoId,@Param("courseId") Long courseId);
 }

+ 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);
+}

+ 8 - 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);
+
     /**
      * 批量删除课堂视频
      *
@@ -202,4 +208,6 @@ public interface IFsUserCourseVideoService
      * 查询选择使用的视频列表
      */
     List<FsUserCourseVideoChooseVO> getChooseCourseVideoListByMap(Map<String, Object> params);
+
+    R sendAppReward(FsCourseSendRewardUParam 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);
+    }
+
+
+
+}

+ 108 - 42
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;
@@ -46,6 +48,7 @@ import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.WxSendRedPacketParam;
 import com.fs.his.service.IFsStorePaymentService;
+import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserService;
 import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.ConfigUtil;
@@ -55,12 +58,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;
@@ -248,6 +254,9 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
     @Autowired
     private BalanceRollbackErrorMapper balanceRollbackErrorMapper;
 
+    @Autowired
+    private IFsUserIntegralLogsService iFsUserIntegralLogsService;
+
 
 
 
@@ -309,6 +318,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);
+    }
+
     /**
      * 批量删除课堂视频
      *
@@ -1223,7 +1257,7 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
 
         try {
             // 尝试获取锁,等待时间5秒,锁过期时间30秒
-            boolean isLocked = lock.tryLock(5, 60, TimeUnit.SECONDS);
+            boolean isLocked = lock.tryLock(5, 300, TimeUnit.SECONDS);
             if (!isLocked) {
                 logger.warn("获取锁失败,用户ID:{},视频ID:{}", param.getUserId(), param.getVideoId());
                 return R.error("操作频繁,请稍后再试!");
@@ -1348,45 +1382,19 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         // 准备发送红包参数
         WxSendRedPacketParam packetParam = new WxSendRedPacketParam();
 
+        //判断是否走服务号openId发红包
         if (user.getMpOpenId()!=null&&!isNewWxMerchant){
             packetParam.setOpenId(user.getMpOpenId());
         }else {
-            //修复数据
+            //查询是否绑定小程序
             FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),user.getUserId(),1);
             if (fsUserWx ==null){
-                if (user.getCourseMaOpenId()==null){
-                    logger.error(" 【转账openId参数错误】:{}", user.getUserId());
-                    return R.error("openId参数错误,请清理缓存后重新授权!");
-                }
-                packetParam.setOpenId(user.getCourseMaOpenId());
-                try {
-                    handleFsUserWx(user,param.getAppId());
-                }catch (Exception e){
-                    logger.error(" 【更新或插入用户与小程序的绑定关系失败】:{}", user.getUserId(),e);
-                }
-
+                return R.error("openId参数错误,请清理缓存重新授权");
             }else {
                 packetParam.setOpenId(fsUserWx.getOpenId());
             }
         }
 
-//        packetParam.setOpenId(user.getMpOpenId());
-//        // 来源是小程序切换openId
-//        if (param.getSource() == 2) {
-//            //处理多小程序问题
-//            FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),user.getUserId(),1);
-//            if (fsUserWx ==null){
-//                try {
-//                    handleFsUserWx(user,param.getAppId());
-//                }catch (Exception e){
-//                    logger.error("【更新或插入用户与小程序的绑定关系失败】:{}", user.getUserId());
-//                }
-//            }else {
-//                packetParam.setOpenId(fsUserWx.getOpenId());
-//            }
-//            //查出公司绑定openid并赋值
-//        }
-
         //判断服务号配置是否存在
         if (StringUtils.isNotEmpty(config.getMpAppId())){
             packetParam.setMpAppId(config.getMpAppId());
@@ -1534,20 +1542,10 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
             if (user.getMpOpenId()!=null&&!isNewWxMerchant){
                 packetParam.setOpenId(user.getMpOpenId());
             }else {
-                //修复数据
+                //查询是否绑定小程序
                 FsUserWx fsUserWx = fsUserWxService.selectByAppIdAndUserId(param.getAppId(),user.getUserId(),1);
                 if (fsUserWx ==null){
-                    if (user.getCourseMaOpenId()==null){
-                        logger.error(" 【转账openId参数错误】:{}", user.getUserId());
-                        return R.error("openId参数错误,请清理缓存后重新授权!");
-                    }
-                    packetParam.setOpenId(user.getCourseMaOpenId());
-                    try {
-                        handleFsUserWx(user,param.getAppId());
-                    }catch (Exception e){
-                        logger.error(" 【更新或插入用户与小程序的绑定关系失败】:{}", user.getUserId(),e);
-                    }
-
+                    return R.error("openId参数错误,请清理缓存重新授权");
                 }else {
                     packetParam.setOpenId(fsUserWx.getOpenId());
                 }
@@ -3214,5 +3212,73 @@ public class FsUserCourseVideoServiceImpl implements IFsUserCourseVideoService
         return fsUserCourseVideoMapper.getChooseCourseVideoListByMap(params);
     }
 
+    @Override
+    public R sendAppReward(FsCourseSendRewardUParam param) {
+        // 获取用户信息
+        FsUser user = fsUserMapper.selectFsUserByUserId(param.getUserId());
+        if (user==null){
+            return R.error("会员被停用,无权限,请联系客服!");
+        }
+        FsCourseWatchLog log = courseWatchLogMapper.getWatchCourseVideo(param.getUserId(), param.getVideoId(), param.getQwUserId(), param.getQwExternalId());
+        if (log == null) {
+            return R.error("无记录");
+        }
+        if (log.getRewardType() != null) {
+            return R.error("奖励已发放");
+        }
+
+        // 获取配置信息
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+
+        // 更新用户积分
+        FsUser userMap=new FsUser();
+        userMap.setUserId(user.getUserId());
+        Integer appAnswerIntegral = config.getAppAnswerIntegral();
+        if (appAnswerIntegral == null ){
+            appAnswerIntegral = config.getAnswerIntegral();
+        }
+        userMap.setIntegral(user.getIntegral()+(appAnswerIntegral==null?0:appAnswerIntegral));
+        fsUserMapper.updateFsUser(userMap);
+        CompletableFuture.runAsync(() -> {
+            FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+            integralLogs.setIntegral(config.getAppAnswerIntegral().longValue());
+            integralLogs.setUserId(user.getUserId());
+            integralLogs.setBalance(userMap.getIntegral());
+            integralLogs.setLogType(17);
+            integralLogs.setBusinessId(StringUtils.isNotEmpty(log.getLogId().toString()) ? log.getLogId().toString() : null);
+            integralLogs.setCreateTime(new Date());
+//            integralLogs.setNickName(user.getNickName());
+//            integralLogs.setPhone(user.getPhone());
+            //integralLogs.setId(integralLogsService.getFsUserIntegralLogsInsertId());
+//        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
+//            iFsUserIntegralLogsService.insertFsUserIntegralLogsMySql(integralLogs);
+            iFsUserIntegralLogsService.insertFsUserIntegralLogs(integralLogs);
+            //asyncAddIntegralLogs.saveLogAsync(integralLogs);
+
+            // 更新观看记录的奖励类型
+            log.setRewardType(2);
+            courseWatchLogMapper.updateFsCourseWatchLog(log);
+
+
+            //转换红包
+            FsCourseRedPacketLog redPacketLog = new FsCourseRedPacketLog();
+            redPacketLog.setCourseId(param.getCourseId());
+            redPacketLog.setOutBatchNo(integralLogs.getId().toString());
+            redPacketLog.setCompanyId(param.getCompanyId());
+            redPacketLog.setUserId(param.getUserId());
+            redPacketLog.setVideoId(param.getVideoId());
+            redPacketLog.setStatus(1);
+            redPacketLog.setQwUserId(param.getQwUserId() != null ? param.getQwUserId() : null);
+            redPacketLog.setCompanyUserId(param.getCompanyUserId());
+            redPacketLog.setCreateTime(new Date());
+            redPacketLog.setAmount(BigDecimal.valueOf(config.getAppAnswerIntegral()).divide(BigDecimal.valueOf(1000)));
+            redPacketLog.setRemark("点播答题领取积分转");
+            redPacketLog.setWatchLogId(log.getLogId() != null ? log.getLogId() : null);
+            redPacketLogMapper.insertFsCourseRedPacketLog(redPacketLog);
+        });
+        return R.ok("奖励发放成功");
+    }
+
 }
 

+ 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;
 }

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

@@ -122,6 +122,11 @@ public class FsUser extends BaseEntity
     private Date  vipStartDate;
     private Date  vipEndDate;
     private Integer vipLevel;
+
+    /**
+     * 会员等级
+     */
+    private Integer level;
     private Integer vipStatus;
 
     private Integer sex;

+ 58 - 0
fs-service/src/main/java/com/fs/his/dto/FsUserDTO.java

@@ -0,0 +1,58 @@
+package com.fs.his.dto;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @description:
+ * @author: Guos
+ * @time: 2025/10/29 上午9:40
+ */
+@Data
+public class FsUserDTO {
+
+    @Excel(name = "用户ID")
+    private Long userId;
+
+    @Excel(name = "用户昵称")
+    private String nickName;
+
+    @Excel(name = "手机号码")
+    private String phone;
+
+    @Excel(name = "用户积分")
+    private Long integral;
+
+    @Excel(name = "用户状态:1为正常,0为禁止")
+    private Integer status;
+
+    @Excel(name = "用户备注")
+    private String remark;
+
+    @Excel(name = "上级昵称")
+    private String tuiName;
+
+    @Excel(name = "app来源")
+    private String source;
+
+    @Excel(name = "登陆设备")
+    private String loginDevice;
+
+    @Excel(name = "上级手机号码")
+    private String tuiPhone;
+
+    @Excel(name = "下级人数")
+    private Integer tuiUserCount;
+
+    @Excel(name = "最后一次登录ip")
+    private String lastIp;
+
+    @Excel(name = "余额")
+    private BigDecimal balance;
+
+    @Excel(name = "会员注册时间", dateFormat = "yyyy-MM-dd HH:mm:ss" , sort = 6)
+    private Date createTime;
+}

+ 6 - 0
fs-service/src/main/java/com/fs/his/mapper/FsStoreOrderMapper.java

@@ -219,6 +219,12 @@ public interface FsStoreOrderMapper
             "            <if test=\"maps.packageSecondName != null and maps.packageSecondName != '' \"> and so.package_second_name like concat('%', #{maps.packageSecondName}, '%')</if>"+
             "            <if test=\"maps.storeId != null \"> and so.store_id = #{maps.storeId}</if>\n" +
             "            <if test=\"maps.orderCode != null  and maps.orderCode != ''\"> and so.order_code = #{maps.orderCode}</if>\n" +
+            "            <if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">\n" +
+            "                and so.order_code in\n" +
+            "                <foreach collection=\"maps.orderCodes\" item=\"orderCode\" open=\"(\" close=\")\" separator=\",\">\n" +
+            "                    #{orderCode}\n" +
+            "                </foreach>\n" +
+            "            </if>" +
             "            <if test=\"maps.prescribeCode != null  and maps.prescribeCode != ''\"> and p.prescribe_code = #{maps.prescribeCode}</if>\n" +
             "            <if test=\"maps.userName != null  and maps.userName != ''\"> and so.user_name like concat('%', #{maps.userName}, '%')</if>\n" +
             "            <if test=\"maps.userPhone != null  and maps.userPhone != ''\"> and so.user_phone = #{maps.userPhone}</if>\n" +

+ 16 - 1
fs-service/src/main/java/com/fs/his/mapper/FsStorePaymentMapper.java

@@ -181,7 +181,22 @@ public interface FsStorePaymentMapper
     @Select("select * from fs_store_payment where business_type=#{type} and  business_id=#{businessId} and (status=1 or starus=-1)")
     List<FsStorePayment> selectFsStorePaymentByPayOrRefund(int i, Long orderId);
     @Select({"<script> " +
-            " SELECT CONCAT('package-', sp.pay_code) AS pay_code,sp.*,u.nick_name,u.phone,s.store_name,c.company_name,cu.nick_name as companyUserName,fso.delivery_name,fso.package_name,fso.package_second_name ,csc.name miniProgramName " +
+            " SELECT " +
+            " CASE " +
+            "   WHEN sp.status = -1 THEN CONCAT('refund-', sp.pay_code) " +
+            "   ELSE " +
+            "     CASE " +
+            "       WHEN sp.business_type = 1 THEN CONCAT('inquiry-', sp.pay_code) " +
+            "       WHEN sp.business_type = 2 THEN CONCAT('store-', sp.pay_code) " +
+            "       WHEN sp.business_type = 3 THEN CONCAT('package-', sp.pay_code) " +
+            "       WHEN sp.business_type = 4 THEN CONCAT('course-', sp.pay_code) " +
+            "       WHEN sp.business_type = 5 THEN CONCAT('appvip-', sp.pay_code) " +
+            "       WHEN sp.business_type = 6 THEN CONCAT('integral-', sp.pay_code) " +
+            "       WHEN sp.business_type = 7 THEN CONCAT('payment-', sp.pay_code) " +
+            "       ELSE sp.pay_code " +
+            "     END " +
+            " END AS pay_code, " +
+            "sp.*,u.nick_name,u.phone,s.store_name,c.company_name,cu.nick_name as companyUserName,fso.delivery_name,fso.package_name,fso.package_second_name ,csc.name miniProgramName " +
             " FROM fs_store_payment sp " +
             " LEFT JOIN  fs_user u ON u.user_id=sp.user_id " +
             " LEFT JOIN fs_store s ON s.store_id=sp.store_id " +

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

@@ -105,6 +105,32 @@ public interface FsUserMapper
     })
     List<FsUserVO> selectFsUserListVO(FsUserParam fsUser);
 
+    @Select({"<script> " +
+            "SELECT  \n" +
+            "    fp.create_time ,\n" +
+            "    cu.nick_name ,\n" +
+            "    fu.user_id ,\n" +
+            "    fp.patient_name ,\n" +
+            "    fp.mobile ,\n" +
+            "    fu.is_buy \n" +
+            "FROM fs_user fu\n" +
+            "LEFT JOIN company_user_user cuu ON fu.user_id = cuu.user_id\n" +
+            "LEFT JOIN company_user cu ON cuu.company_user_id = cu.user_id\n" +
+            "LEFT JOIN fs_patient fp ON fu.user_id = fp.user_id\n" +
+            "WHERE fp.create_time IS NOT NULL and fu.is_del = 0 \n" +
+            "  <if test=\"companyUserId != null \"> and cuu.company_user_id=#{companyUserId} </if>\n" +
+            "  <if test=\"companyId != null \"> and cuu.company_id=#{companyId} </if>\n" +
+            "  <if test=\"nickName != null  and nickName != ''\"> and fu.nick_name like concat( #{nickName}, '%')</if>\n" +
+            "   <if test=\"phone != null  and phone != ''\"> and fu.phone like concat( #{phone}, '%')</if>\n" +
+            "   <if test=\"status != null \"> and fu.status = #{status}</if>\n" +
+            "            <if test=\"isBuy != null \"> and fu.is_buy = #{isBuy}</if>\n" +
+            "            <if test=\"userId != null \"> and fu.user_id = #{userId}</if>\n" +
+            "            <if test=\"sTime != null \">  and DATE(fu.create_time) &gt;= DATE(#{sTime})</if>\n" +
+            "            <if test=\"eTime != null \">  and DATE(fu.create_time) &lt;= DATE(#{eTime})</if>\n" +
+            "ORDER BY fu.user_id DESC "+
+            "</script>"})
+    List<FsUserExportListVO> selectFsUserExportListVO(FsUserParam fsUser);
+
     @Update("update fs_user set is_del=1 where user_id=#{userId}")
     int updateFsUserByUserId(Long userId);
 
@@ -144,32 +170,6 @@ public interface FsUserMapper
             "</script>"})
     List<FsUserVO> selectFsUserListVOByComponentsUser(FsUserParam fsUser);
 
-    @Select({"<script> " +
-            "SELECT  \n" +
-            "    fp.create_time ,\n" +
-            "    cu.nick_name ,\n" +
-            "    fu.user_id ,\n" +
-            "    fp.patient_name ,\n" +
-            "    fp.mobile ,\n" +
-            "    fu.is_buy \n" +
-            "FROM fs_user fu\n" +
-            "LEFT JOIN company_user_user cuu ON fu.user_id = cuu.user_id\n" +
-            "LEFT JOIN company_user cu ON cuu.company_user_id = cu.user_id\n" +
-            "LEFT JOIN fs_patient fp ON fu.user_id = fp.user_id\n" +
-            "WHERE fp.create_time IS NOT NULL and fu.is_del = 0 \n" +
-            "  <if test=\"companyUserId != null \"> and cuu.company_user_id=#{companyUserId} </if>\n" +
-            "  <if test=\"companyId != null \"> and cuu.company_id=#{companyId} </if>\n" +
-            "  <if test=\"nickName != null  and nickName != ''\"> and fu.nick_name like concat( #{nickName}, '%')</if>\n" +
-            "   <if test=\"phone != null  and phone != ''\"> and fu.phone like concat( #{phone}, '%')</if>\n" +
-            "   <if test=\"status != null \"> and fu.status = #{status}</if>\n" +
-            "            <if test=\"isBuy != null \"> and fu.is_buy = #{isBuy}</if>\n" +
-            "            <if test=\"userId != null \"> and fu.user_id = #{userId}</if>\n" +
-            "            <if test=\"sTime != null \">  and DATE(fu.create_time) &gt;= DATE(#{sTime})</if>\n" +
-            "            <if test=\"eTime != null \">  and DATE(fu.create_time) &lt;= DATE(#{eTime})</if>\n" +
-            "ORDER BY fu.user_id DESC "+
-            "</script>"})
-    List<FsUserExportListVO> selectFsUserExportListVO(FsUserParam fsUser);
-
     @Select("SELECT user_id\n" +
             "FROM fs_user_integral_logs\n" +
             "WHERE business_type = 2\n" +

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

@@ -271,11 +271,26 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.updateFsUserByUserId(userId);
     }
 
+    /**
+     * 列表查询
+     * @param fsUser
+     * @return
+     */
     @Override
     public List<FsUserVO> selectFsUserListVO(FsUserParam fsUser) {
         return fsUserMapper.selectFsUserListVO(fsUser);
     }
 
+    /**
+     * 导出用户列表
+     * @param fsUser
+     * @return
+     */
+    @Override
+    public List<FsUserExportListVO> selectFsUserExportListVO(FsUserParam fsUser) {
+        return fsUserMapper.selectFsUserExportListVO(fsUser);
+    }
+
     @Override
     public FsUser selectFsUserByOpenId(String openId) {
         return fsUserMapper.selectFsUserByOpenId(openId);
@@ -313,10 +328,6 @@ public class FsUserServiceImpl implements IFsUserService {
         return fsUserMapper.selectFsUserListVOByComponentsUser(fsUser);
     }
 
-    @Override
-    public List<FsUserExportListVO> selectFsUserExportListVO(FsUserParam fsUser) {
-        return fsUserMapper.selectFsUserExportListVO(fsUser);
-    }
     @Autowired
     private FsUserIntegralLogsMapper integralLogsMapper;
 
@@ -1050,7 +1061,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 - 5
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreAfterSalesScrmMapper.java

@@ -119,15 +119,17 @@ public interface FsStoreAfterSalesScrmMapper
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
             "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
             "</if>" +
-            "<if test = 'maps.beginTime != null and maps.beginTime != \"\"   '> " +
-            " AND date_format(s.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
+            "<if test = 'maps.params != null and maps.params != \"\"   '> " +
+            "<if test = 'maps.params.beginTime != null and maps.params.beginTime != \"\"   '> " +
+            " AND date_format(s.create_time,'%y%m%d') &gt;= date_format(#{maps.params.beginTime},'%y%m%d') " +
+            "</if>" +
+            "<if test = 'maps.params.endTime != null and maps.params.endTime != \"\"   '> " +
+            " AND date_format(s.create_time,'%y%m%d') &lt;= date_format(#{maps.params.endTime},'%y%m%d') " +
+            "</if>" +
             "</if>" +
             "<if test = 'maps.consigneePhone != null and  maps.consigneePhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.consigneePhone},'%') " +
             "</if>" +
-            "<if test = 'maps.endTime != null and maps.endTime != \"\"   '> " +
-            " AND date_format(s.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
-            "</if>" +
             "<if test = 'maps.deptId != null    '> " +
             "  AND (o.dept_id = #{maps.deptId} OR o.dept_id IN ( SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors) )) " +
             "</if>" +

+ 6 - 4
fs-service/src/main/java/com/fs/hisStore/mapper/FsStorePaymentScrmMapper.java

@@ -103,11 +103,13 @@ public interface FsStorePaymentScrmMapper
 //            "<if test = 'maps.createTime != null    '> " +
 //            "and DATE_FORMAT(p.create_time,'%Y-%m-%d') = DATE_FORMAT(#{maps.createTime},'%Y-%m-%d')  " +
 //            "</if>" +
-            "<if test = 'maps.beginTime != null and maps.beginTime != \"\"   '> " +
-            " AND date_format(p.pay_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d') " +
+            "<if test = 'maps.params != null and maps.params != \"\"   '> " +
+            "<if test = 'maps.params.beginTime != null and maps.params.beginTime != \"\"   '> " +
+            " AND date_format(p.pay_time,'%y%m%d') &gt;= date_format(#{maps.params.beginTime},'%y%m%d') " +
+            "</if>" +
+            "<if test = 'maps.params.endTime != null and maps.params.endTime != \"\"   '> " +
+            " AND date_format(p.pay_time,'%y%m%d') &lt;= date_format(#{maps.params.endTime},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.endTime != null and maps.endTime != \"\"   '> " +
-            " AND date_format(p.pay_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
             "</if>" +
 
             "<if test = 'maps.refundBeginTime != null  and maps.refundBeginTime != \"\"    '> " +

+ 1 - 1
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -743,7 +743,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             //绑定销售
             FsUserScrm fsuser = userService.selectFsUserById(userId);
             if (param.getCompanyUserId() != null) {
-                if (ObjectUtil.isNotEmpty(fsuser.getCompanyUserId()) && !Objects.equals(fsuser.getCompanyUserId(), param.getCompanyUserId())) {
+                if (!CloudHostUtils.hasCloudHostName("鸿森堂") && ObjectUtil.isNotEmpty(fsuser.getCompanyUserId()) && !Objects.equals(fsuser.getCompanyUserId(), param.getCompanyUserId())) {
                     CompanyUser companyUser = companyUserService.selectCompanyUserById(fsuser.getCompanyUserId());
                     return R.error(String.format("请联系%s销售进行购买商品!", companyUser.getNickName()));
                 } else {

+ 1 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwTagMapper.java

@@ -86,7 +86,7 @@ public interface QwTagMapper
     QwTagVO selectQwTagByName(@Param("trimTag") String trimTag, @Param("corpId")String corpId );
 
     @Select("<script>" +
-            "SELECT name FROM qw_tag WHERE tag_id IN " +
+            "SELECT distinct name FROM qw_tag WHERE tag_id IN " +
             "<foreach collection='date.tagIds' item='tagId' open='(' separator=',' close=')'>" +
             "#{tagId}" +
             "</foreach>" +

+ 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;
+}
+

+ 11 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -3929,9 +3929,10 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
                         qw.setDescription(followUser.getDescription()); // 设置描述信息
                         List<Tag> tags = followUser.getTags();
 
+                        List<String> tagArr = new ArrayList<>();
+
                         if (tags != null && tags.size() > 0) {
 
-                            List<String> tagArr = new ArrayList<>();
                             for (Tag tag : tags) {
                                 if (tag != null && tag.getTag_id() != null) {
                                     tagArr.add(tag.getTag_id());
@@ -3944,10 +3945,18 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
 
                             //新客对话
 //                            processTagsAllByAiChat(qwExternalContact1,corpId,tagArr);
+                        }else {
 
-                            qw.setTagIds(JSON.toJSONString(tagArr)); // 设置标签ID
+                            //客户自带标签空干净了
+                            logger.info("客户自带标签空干净了:"+qwExternalContact1.getName()+"|公司"+qwExternalContact1.getCorpId()+"|员工"+qwExternalContact1.getUserId()+"|总标签"+tagArr+"|原标签"+qwExternalContact1.getTagIds());
+
+                            //销售删除客户-找这个销售的sop任务中所有的营期里有没有这个客户,删了
+                            sopUserLogsInfoMapper.deleteByQwUserIdAndCorpIdToContactId(userID,corpId, externalUserID);
+                            sopUserLogsInfoMapper.deleteByQwUserIdAndCorpIdToContactIdByChat(userID,corpId, externalUserID);
 
                         }
+
+                        qw.setTagIds(JSON.toJSONString(tagArr)); // 设置标签ID
                         if (followUser.getRemark_mobiles() != null && followUser.getRemark_mobiles().size() > 0) {
                             List<String> remarkMobiles = followUser.getRemark_mobiles();
                             qw.setRemarkMobiles(JSON.toJSONString(remarkMobiles));

+ 6 - 3
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();
@@ -389,6 +389,9 @@ List<SopUserLogsVO> selectSopUserLogsGroupListByParam(@Param("maps") SopUserLogs
     void replaceUser(@Param("vo") ReplaceUserDto vo);
 
     @DataSource(DataSourceType.SOP)
-    @Select("SELECT * FROM sop_user_logs WHERE sop_id = #{sopId} AND qw_user_id = #{qwUserId}")
-    SopUserLogs queryUserBySopId(@Param("qwUserId")String qwUserId,@Param("sopId") String sopId);
+    @Select("<script>" +"SELECT * FROM sop_user_logs WHERE sop_id = #{sopId} AND qw_user_id = #{qwUserId} " +
+            "<if test=\"chatId != null and chatId != ''\"> and chat_id = #{chatId} </if>" +
+            " limit 1 " +
+            "</script>")
+    SopUserLogs queryUserBySopId(@Param("qwUserId")String qwUserId,@Param("sopId") String sopId,@Param("chatId") String chatId);
 }

+ 2 - 2
fs-service/src/main/java/com/fs/sop/service/impl/QwSopLogsServiceImpl.java

@@ -1099,8 +1099,8 @@ public class QwSopLogsServiceImpl extends ServiceImpl<QwSopLogsMapper, QwSopLogs
 
                                 //有app的异步推送消息
                                 if (!setting.isEmpty()) {
-                                    asyncSopTestService.asyncSendMsgBySopAppLinkNormal(setting, param.getExternalId());
-                                    //asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(setting, param.getCorpId(),qwUser.getCompanyUserId(),log.getFsUserId());
+//                                    asyncSopTestService.asyncSendMsgBySopAppLinkNormal(setting, param.getExternalId());
+                                    asyncSopTestService.asyncSendMsgBySopAppLinkNormalIM(setting, param.getCorpId(),qwUser.getCompanyUserId(),log.getFsUserId());
                                 }
 
                                 if (log.getExpiryTime() == null) {

+ 5 - 5
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -558,7 +558,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     sopLogs.setQwUserKey(qwUser.getId());
 
                     // 设置实际发送人
-                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId());
+                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),groupUser.getChatId());
                     //域名
                     String companyUserId = qwUser.getCompanyUserId().toString();
                     String domainName = companyUserMapper.selectDomainByUserId(Long.parseLong(companyUserId));
@@ -696,7 +696,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                     sopLogs.setExternalUserName(groupChat.getName());
                     sopLogs.setQwUserKey(qwUser.getId());
                     // 设置实际发送人
-                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId());
+                    updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),groupChat.getChatId());
 
                     QwSopCourseFinishTempSetting setting = new QwSopCourseFinishTempSetting();
 
@@ -1009,7 +1009,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                 }
                 sopLogs.setContentJson(JSON.toJSONString(setting));
                 // 设置实际发送人
-                updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId());
+                updateQwUserKey(sopLogs,qwUser.getQwUserId(),param.getSopId(),null);
 
                 sopLogsList.add(sopLogs);
             });
@@ -1023,9 +1023,9 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return R.ok();
     }
 
-    private void updateQwUserKey(QwSopLogs sopLogs, String qwUserId, String sopId) {
+    private void updateQwUserKey(QwSopLogs sopLogs, String qwUserId, String sopId,String chatId) {
         // 设置实际发送人
-        SopUserLogs qwSopLogs = sopUserLogsMapper.queryUserBySopId(qwUserId,sopId);
+        SopUserLogs qwSopLogs = sopUserLogsMapper.queryUserBySopId(qwUserId,sopId,chatId);
         if (qwSopLogs != null && ObjectUtil.isNotEmpty(qwSopLogs.getActualQwId())){
             // sopLogs.setQwUserid(qwUser.getQwUserId());
             sopLogs.setQwUserKey(qwSopLogs.getActualQwId());

+ 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));

+ 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:

+ 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

+ 94 - 0
fs-service/src/main/resources/application-config-druid-heyantang.yml

@@ -0,0 +1,94 @@
+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:
+        secret:
+        token:
+        aesKey: HlEiBB55eaWUaeBVAQO3cWKWPYv1vOVQSq7nFNICw4E
+        msgDataFormat: JSON
+  cp:
+    corpId:
+    appConfigs:
+      - agentId:
+        secret: ec7okROXJqkNafq66-L6aKNv0asTzQIG0CYrj3vyBbo
+        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: wx9df02b2236d4fbf2 # 第一个公众号的appid
+        secret: 4f685750d40ab334b6f2f54b7d121389 # 公众号的appsecret
+        token: PPKOdAlCoMO # 接口配置里的Token值
+        aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
+aifabu:  #爱链接
+  appKey: 7b471be905ab17e00f3b858c6710dd117601d00
+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://172.27.0.17:8010
+  h5CommonApi: http://172.27.0.11:7771
+  jwt:
+    # 加密秘钥
+    secret: f4e2e52034348f86b67cde581c0f9eb5
+    # token有效时长,7天,单位秒
+    expire: 31536000
+    header: AppToken
+nuonuo:
+  key: 10924509
+  secret: A2EB20764D304D16
+
+# 存储捅配置
+tencent_cloud_config:
+  secret_id: AKIDiMq9lDf2EOM9lIfqqfKo7FNgM5meD0sT
+  secret_key: u5SuS80342xzx8FRBukza9lVNHKNMSaB
+  bucket: cdhyt-1323137866
+#  bucket: cdhyt-1383856587
+  app_id: 1323137866
+  region: ap-chongqing
+#  region: ap-chengdu
+  proxy: cdhyt
+cloud_host:
+  company_name: 鹤颜堂
+  projectCode: HEYANTANG
+headerImg:
+  imgUrl: https
+ipad:
+  ipadUrl: https://qwipad.yytcdtb.cn
+  aiApi: http://127.0.0.1:3000/api
+  voiceApi:
+  commonApi:
+wx_miniapp_temp:
+  pay_order_temp_id: -SjnK9K6cNKASa6AD9Q_c0YT7J1lPTEpPIpqbMJF8F0
+  inquiry_temp_id: hwFXVh0AWqeasBsZpa0-urb3CrPeYEwBiy3P6AMMGFQ
+
+

+ 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-dev.yml

@@ -43,9 +43,9 @@ spring:
             druid:
                 # 主库数据源
                 master:
-                    url: jdbc:mysql://127.0.0.1:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                    username: root
-                    password: 1101165230
+                    url: jdbc:mysql://139.186.77.83:3306/ylrz_his_scrm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: Rtroot
+                    password: Rtroot
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 159 - 0
fs-service/src/main/resources/application-druid-heyantang.yml

@@ -0,0 +1,159 @@
+# 数据源配置
+spring:
+    profiles:
+        include: config-druid-heyantang,common
+    # redis 配置
+    redis:
+        # 地址
+        host: 172.27.0.13
+        # 端口,默认为6379
+        port: 6379
+        # 数据库索引
+        database: 0
+        # 密码
+        password: Ylrz_c123232014^$
+        # 连接超时时间
+        timeout: 20s
+        lettuce:
+            pool:
+                # 连接池中的最小空闲连接
+                min-idle: 0
+                # 连接池中的最大空闲连接
+                max-idle: 8
+                # 连接池的最大数据库连接数
+                max-active: 8
+                # #连接池最大阻塞等待时间(使用负值表示没有限制)
+                max-wait: -1ms
+    datasource:
+        #        clickhouse:
+        #            type: com.alibaba.druid.pool.DruidDataSource
+        #            driverClassName: com.clickhouse.jdbc.ClickHouseDriver
+        #            url: jdbc:clickhouse://cc-2vc8zzo26w0l7m2l6.public.clickhouse.ads.aliyuncs.com/sop?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://172.27.0.7:3306/fs_his?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_c123232014^$
+                # 从库数据源
+                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://172.27.0.7:3306/sop?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                    username: root
+                    password: Ylrz_c123232014^$
+                # 初始连接数
+                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-1243b25nj.rocketmq.gz.public.tencenttdmq.com:8080 # RocketMQ NameServer 地址
+    producer:
+        group: my-producer-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+    consumer:
+        group: test-group
+        access-key: ak1243b25nj17d4b2dc1a03 # 替换为实际的 accessKey
+        secret-key: sk08a7ea1f9f4b0237 # 替换为实际的 secretKey
+openIM:
+    secret: openIM123
+    userID: imAdmin
+    url: https://web.im.fbylive.com/api
+#是否使用新im
+im:
+    type: NONE
+#是否为新商户,新商户不走mpOpenId
+isNewWxMerchant: false

+ 1 - 1
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@.
                 # 从库数据源

+ 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, '');
+
+
+

+ 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>

+ 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>
 
@@ -387,6 +388,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"/>
@@ -463,4 +476,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 - 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>
 

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

@@ -48,10 +48,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="courseMaOpenId"    column="course_ma_open_id"    />
         <result property="qwExtId"    column="qw_ext_id"    />
         <result property="qwUserId"    column="qw_user_id"    />
+        <result property="level" column="level"/>
     </resultMap>
 
     <sql id="selectFsUserVo">
-        select user_id,qw_ext_id,sex,is_buy,course_ma_open_id,is_push,is_add_qw,source,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id from fs_user
+        select user_id,qw_ext_id,sex,is_buy,`level`,course_ma_open_id,is_push,is_add_qw,source,login_device,is_individuation_push,store_open_id,password,jpush_id, is_vip,vip_start_date,vip_end_date,vip_level,vip_status,nick_name,integral_status, avatar, phone, integral,sign_num, status, tui_user_id, tui_time, tui_user_count, ma_open_id, mp_open_id, union_id, is_del, user_code, remark, create_time, update_time, last_ip, balance,is_weixin_auth,parent_id,qw_user_id,company_id,company_user_id from fs_user
     </sql>
 
     <select id="selectFsUserList" parameterType="FsUser" resultMap="FsUserResult">
@@ -557,6 +558,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="orderCount != null">order_count,</if>
             <if test="companyId != null">company_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
+            <if test="level != null">`level`,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="nickName != null">#{nickName},</if>
@@ -600,6 +602,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="orderCount != null">#{orderCount},</if>
             <if test="companyId != null">#{companyId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="level != null">#{level},</if>
          </trim>
     </insert>
 

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

@@ -39,10 +39,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="packageCateIds != null  and packageCateIds != ''"> and package_cate_ids = #{packageCateIds}</if>
             <if test="type != null "> and type = #{type}</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>

+ 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">

+ 14 - 0
fs-user-app/src/main/java/com/fs/app/controller/CourseController.java

@@ -2,6 +2,7 @@ package com.fs.app.controller;
 
 
 import com.fs.app.annotation.Login;
+import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.ServletUtils;
@@ -17,6 +18,7 @@ import io.jsonwebtoken.Claims;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.annotation.Cacheable;
 import org.springframework.transaction.annotation.Transactional;
@@ -26,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.util.*;
 
 @Api("课堂接口")
+@Slf4j
 @RestController
 @RequestMapping(value="/app/course")
 public class CourseController extends  AppBaseController{
@@ -341,4 +344,15 @@ public class CourseController extends  AppBaseController{
     public void updateUrl(){
         tencentCloudCosService.updateUrl();
     }
+
+    @Login
+    @ApiOperation("发放奖励App")
+    @PostMapping("/sendAppReward")
+    @RepeatSubmit
+    public R sendAppReward(@RequestBody FsCourseSendRewardUParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        log.info("zyp \n【发放APP奖励】:{}",param);
+        return courseVideoService.sendAppReward(param);
+    }
 }

+ 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);
 
@@ -157,6 +161,10 @@ public class WxH5MpController {
             WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
             WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
 
+            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不存在");

+ 1 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/CourseFsUserController.java

@@ -104,6 +104,7 @@ public class CourseFsUserController extends AppBaseController {
 
     @ApiOperation("获取缓冲流量")
     @PostMapping("/getInternetTraffic")
+    @Login
     public R getInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param) {
         param.setUserId(Long.parseLong(getUserId()));
         return courseVideoService.getInternetTraffic(param);

+ 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();
                 }
             }
         }