فهرست منبع

公域课数据统计 营期收藏 点播接口

yuhongqi 4 هفته پیش
والد
کامیت
819719cfcb
48فایلهای تغییر یافته به همراه1362 افزوده شده و 94 حذف شده
  1. 13 2
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java
  2. 23 0
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCommentController.java
  3. 20 6
      fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java
  4. 0 6
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  5. 15 0
      fs-admin/src/main/java/com/fs/course/controller/PublicCourseWatchStatisticsController.java
  6. 10 1
      fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java
  7. 4 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  8. 22 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductPackageScrmController.java
  9. 58 0
      fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java
  10. 34 4
      fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java
  11. 2 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java
  12. 23 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreProductPackageScrmController.java
  13. 43 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseMarketingEvent.java
  14. 49 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseShareLog.java
  15. 63 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseMarketingEventMapper.java
  16. 64 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseShareLogMapper.java
  17. 2 0
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchCommentMapper.java
  18. 10 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseMapper.java
  19. 6 0
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseStudyMapper.java
  20. 5 0
      fs-service/src/main/java/com/fs/course/mapper/PublicCourseWatchStatisticsMapper.java
  21. 5 1
      fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java
  22. 2 0
      fs-service/src/main/java/com/fs/course/param/FsUserCourseListUParam.java
  23. 61 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseMarketingEventService.java
  24. 61 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseShareLogService.java
  25. 1 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseWatchCommentService.java
  26. 3 0
      fs-service/src/main/java/com/fs/course/service/IPublicCourseWatchStatisticsService.java
  27. 96 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseMarketingEventServiceImpl.java
  28. 12 23
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  29. 95 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseShareLogServiceImpl.java
  30. 6 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchCommentServiceImpl.java
  31. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseCategoryServiceImpl.java
  32. 55 3
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java
  33. 17 0
      fs-service/src/main/java/com/fs/course/service/impl/PublicCourseWatchStatisticsServiceImpl.java
  34. 40 0
      fs-service/src/main/java/com/fs/course/vo/CatalogUserStudyVO.java
  35. 3 0
      fs-service/src/main/java/com/fs/course/vo/FsUserCoursePublicAppVO.java
  36. 6 6
      fs-service/src/main/java/com/fs/his/utils/RedisCacheUtil.java
  37. 4 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreAfterSalesScrmServiceImpl.java
  38. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java
  39. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java
  40. 79 0
      fs-service/src/main/resources/mapper/course/FsCourseMarketingEventMapper.xml
  41. 79 0
      fs-service/src/main/resources/mapper/course/FsCourseShareLogMapper.xml
  42. 53 0
      fs-service/src/main/resources/mapper/course/FsCourseWatchCommentMapper.xml
  43. 19 3
      fs-service/src/main/resources/mapper/course/FsUserCourseCategoryMapper.xml
  44. 29 8
      fs-service/src/main/resources/mapper/course/FsUserCourseMapper.xml
  45. 4 0
      fs-service/src/main/resources/mapper/course/FsUserCourseVideoMapper.xml
  46. 54 30
      fs-service/src/main/resources/mapper/course/PublicCourseWatchStatisticsMapper.xml
  47. 85 0
      fs-user-app/src/main/java/com/fs/app/controller/course/PublicCourseWatchStatisticsController.java
  48. 20 0
      fs-user-app/src/main/java/com/fs/app/controller/store/CourseCommentScrmController.java

+ 13 - 2
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCategoryController.java

@@ -90,6 +90,17 @@ public class FsUserCourseCategoryController extends BaseController
         return AjaxResult.success(list);
     }
 
+    /**
+     * 查询课堂分类列表
+     */
+    @PreAuthorize("@ss.hasPermi('course:userCourseCategory:list')")
+    @GetMapping("/publicList")
+    public AjaxResult publicList(FsUserCourseCategory fsUserCourseCategory)
+    {
+        List<FsUserCourseCategory> list = fsUserCourseCategoryService.selectFsUserCourseCategoryList(fsUserCourseCategory);
+        return AjaxResult.success(list);
+    }
+
     /**
      * 导出课堂分类列表
      */
@@ -178,10 +189,10 @@ public class FsUserCourseCategoryController extends BaseController
                     continue;
                 }
                 if (fsUserCourseMapper.countCourseByCategoryId(cateId) > 0) {
-                    return AjaxResult.error("该分类下有关联课程,不能删除");
+                    return AjaxResult.error("该分类下有关联课程或者素材,不能删除");
                 }
                 if (fsVideoResourceMapper.countVideoResourceByCourseCategoryId(cateId) > 0) {
-                    return AjaxResult.error("该分类下有关联课程,不能删除");
+                    return AjaxResult.error("该分类下有关联课程或者素材,不能删除");
                 }
             }
         }

+ 23 - 0
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseCommentController.java

@@ -3,8 +3,10 @@ package com.fs.course.controller;
 import java.util.List;
 
 import com.fs.course.param.FsUserCourseCommentParam;
+import com.fs.course.param.FsCourseWatchCommentPageParam;
 import com.fs.course.vo.FsUserCourseCommentListVO;
 import com.fs.course.vo.FsUserCourseCommentVO;
+import com.fs.course.vo.FsCourseWatchCommentListVO;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -21,6 +23,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.enums.BusinessType;
 import com.fs.course.domain.FsUserCourseComment;
 import com.fs.course.service.IFsUserCourseCommentService;
+import com.fs.course.service.IFsCourseWatchCommentService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
 
@@ -37,6 +40,9 @@ public class FsUserCourseCommentController extends BaseController
     @Autowired
     private IFsUserCourseCommentService fsUserCourseCommentService;
 
+    @Autowired
+    private IFsCourseWatchCommentService fsCourseWatchCommentService;
+
     /**
      * 查询课堂评论列表
      */
@@ -49,6 +55,23 @@ public class FsUserCourseCommentController extends BaseController
         return getDataTable(list);
     }
 
+    /**
+     * 查询公域视频下的评论列表(给公域课页面复用)。
+     * <p>
+     * 这里复用的是 fs_course_watch_comment(看课评论)表的查询逻辑;
+     * 由 fs-service 的 Mapper 在 cateType=1(公域看课评论)时完成公域视频范围过滤。
+     * </p>
+     */
+    @PreAuthorize("@ss.hasPermi('course:courseWatchComment:list')")
+    @GetMapping("/publicCourseVideoWatchComment/list")
+    public TableDataInfo publicCourseVideoWatchCommentList(FsCourseWatchCommentPageParam fsCourseWatchCommentPageParam)
+    {
+        startPage();
+        // 前端传 cateType=1、isAll=true 时,Mapper 会自动限定“公域课程 -> 对应视频 -> 评论”范围
+        List<FsCourseWatchCommentListVO> list = fsCourseWatchCommentService.selectFsCourseWatchCommentPublicList(fsCourseWatchCommentPageParam);
+        return getDataTable(list);
+    }
+
     /**
      * 导出课堂评论列表
      */

+ 20 - 6
fs-admin/src/main/java/com/fs/course/controller/FsUserCourseController.java

@@ -1,7 +1,9 @@
 package com.fs.course.controller;
 
+import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONObject;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.core.controller.BaseController;
@@ -11,6 +13,7 @@ import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.cache.PublicCourseAppCacheNames;
 import com.fs.course.config.CourseConfig;
@@ -22,14 +25,20 @@ 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.hisStore.dto.StoreOrderProductDTO;
+import com.fs.hisStore.vo.FsStoreOrderItemExportZMVO;
 import com.fs.qw.param.FsUserCourseRedPageParam;
 import com.fs.sop.service.IQwSopTempService;
 import com.fs.system.service.ISysConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
+import java.math.BigDecimal;
 import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 课程Controller
@@ -76,6 +85,9 @@ public class FsUserCourseController extends BaseController {
         return getDataTable(list);
     }
 
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
     /**
      * 查询公域课程列表
      */
@@ -83,12 +95,14 @@ public class FsUserCourseController extends BaseController {
     @GetMapping("/publicList")
     public TableDataInfo publicList(FsUserCourse fsUserCourse) {
         startPage();
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        Long userId = loginUser.getUser().getUserId();
-        String json = configService.selectConfigByKey("course.config");
-        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
-            fsUserCourse.setUserId(userId);
+        if(!"北京卓美".equals(signProjectName)){
+            LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+            Long userId = loginUser.getUser().getUserId();
+            String json = configService.selectConfigByKey("course.config");
+            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+            if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
+                fsUserCourse.setUserId(userId);
+            }
         }
         List<FsUserCourseListPVO> list = fsUserCourseService.selectFsUserCourseListPVO(fsUserCourse);
         return getDataTable(list);

+ 0 - 6
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -114,12 +114,6 @@ public class FsVideoResourceController extends BaseController {
             videoType = 1;
         }
         params.put("videoType", videoType);
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        String json = configService.selectConfigByKey("course.config");
-        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
-        if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
-            params.put("userId", loginUser.getUser().getUserId());
-        }
         PageHelper.startPage(pageNum, pageSize);
         List<FsVideoResourceVO> list = fsVideoResourceService.selectPublicVideoResourceListByMap(params);
         return getDataTable(list);

+ 15 - 0
fs-admin/src/main/java/com/fs/course/controller/PublicCourseWatchStatisticsController.java

@@ -8,12 +8,14 @@ import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.param.PublicCourseWatchStatQueryParam;
 import com.fs.course.service.IPublicCourseWatchStatisticsService;
+import com.fs.course.vo.CatalogUserStudyVO;
 import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
 import com.fs.course.vo.PublicCourseWatchStatCourseVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.List;
@@ -61,4 +63,17 @@ public class PublicCourseWatchStatisticsController extends BaseController {
         ExcelUtil<PublicCourseWatchStatCatalogVO> util = new ExcelUtil<>(PublicCourseWatchStatCatalogVO.class);
         return util.exportExcel(list, "公域看课统计-目录数据");
     }
+
+    /**
+     * 查询目录下用户学习记录
+     */
+    @PreAuthorize("@ss.hasPermi('course:publicCourseWatchStat:list')")
+    @GetMapping("/catalog/userStudy")
+    public TableDataInfo catalogUserStudy(@RequestParam Long videoId,
+                                          @RequestParam(required = false) String nickName,
+                                          @RequestParam(required = false) String phone) {
+        startPage();
+        List<CatalogUserStudyVO> list = publicCourseWatchStatisticsService.listCatalogUserStudy(videoId, nickName, phone);
+        return getDataTable(list);
+    }
 }

+ 10 - 1
fs-admin/src/main/java/com/fs/his/controller/FsIntegralOrderController.java

@@ -16,6 +16,7 @@ import com.fs.common.utils.CloudHostUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.utils.uuid.IdUtils;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.his.domain.*;
 import com.fs.his.dto.ExpressInfoDTO;
 import com.fs.his.enums.ShipperCodeEnum;
@@ -143,6 +144,10 @@ public class FsIntegralOrderController extends BaseController
         return sysRole;
     }
 
+    @Autowired
+    // 260506 积分订单没有接入erp 暂时这么处理 卓美 需要对订单进行发货处理
+    private CloudHostProper cloudHostProper;
+
     /**
      * 导出积分商品订单列表 卓美个性化导出
      */
@@ -167,7 +172,11 @@ public class FsIntegralOrderController extends BaseController
         for (FsIntegralOrderListVO vo : fsIntegralOrderListVOS) {
             if (!(sysRole.getIsCheckPhone()==1)){
                 // 加密手机号
-                vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+
+                // 260506 积分订单没有接入erp 暂时这么处理 卓美 需要对订单进行发货处理
+                if (!"北京卓美".equals(cloudHostProper.getCompanyName())) {
+                    vo.setUserPhone(decryptAutoPhoneMk(vo.getUserPhone()));
+                }
             }
             if (StringUtils.isNotEmpty(vo.getItemJson())) {
                 try {

+ 4 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -138,6 +138,8 @@ public class FsStoreAfterSalesScrmController extends BaseController
                                 zmvo.setCompanyUserNickName(vo.getCompanyUserNickName());
                                 zmvo.setRefundTime(vo.getCreateTime());
                                 zmvo.setRefundMoney(vo.getRefundAmount());
+                                zmvo.setRefundType(vo.getRefundAmount() != null && vo.getPayPrice() != null
+                                        ? (vo.getRefundAmount().compareTo(vo.getPayPrice()) < 0 ? "部分退款" : "全额退款") : "");
                                 zmvo.setBankTransactionId(vo.getBankTransactionId());
                                 zmvo.setReasons(vo.getReasons());
                                 zmvo.setExplains(vo.getExplains());
@@ -159,6 +161,8 @@ public class FsStoreAfterSalesScrmController extends BaseController
         }
         for (FsStoreAfterSalesVO vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setRefundType(vo.getRefundAmount() != null && vo.getPayPrice() != null
+                    ? (vo.getRefundAmount().compareTo(vo.getPayPrice()) < 0 ? "部分退款" : "全额退款") : "");
         }
 
         ExcelUtil<FsStoreAfterSalesVO> util = new ExcelUtil<FsStoreAfterSalesVO>(FsStoreAfterSalesVO.class);

+ 22 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductPackageScrmController.java

@@ -6,8 +6,10 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.model.LoginUser;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.domain.FsStoreProductAttrValueScrm;
@@ -184,6 +186,26 @@ public class FsStoreProductPackageScrmController extends BaseController
         return toAjax(fsStoreProductPackageService.deleteFsStoreProductPackageByIds(packageIds));
     }
 
+    /**
+     * 一键复制商品组合套餐
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:add')")
+    @Log(title = "商品组合套餐", businessType = BusinessType.INSERT)
+    @PostMapping("/copy/{packageId}")
+    public AjaxResult copyPackage(@PathVariable("packageId") Long packageId)
+    {
+        FsStoreProductPackageScrm source = fsStoreProductPackageService.selectFsStoreProductPackageById(packageId);
+        if (source == null) {
+            return AjaxResult.error("未找到对应的套餐信息");
+        }
+        // 清空主键,让数据库自增生成新ID
+        source.setPackageId(null);
+        // 标题加"-副本"后缀
+        source.setTitle(source.getTitle() + "-副本");
+        // 设置当前登录用户的公司和部门
+        return toAjax(fsStoreProductPackageService.insertFsStoreProductPackage(source));
+    }
+
 
     @GetMapping("/listBySearch")
     public R listBySearCh(FsStoreProductPackageScrm productPackage)

+ 58 - 0
fs-admin/src/main/java/com/fs/user/controller/FsUserIntegralController.java

@@ -1,7 +1,9 @@
 package com.fs.user.controller;
 
+import com.fs.common.annotation.Log;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.StringUtils;
 import com.fs.his.domain.FsUser;
 import com.fs.his.domain.FsUserIntegralLogs;
@@ -14,6 +16,7 @@ import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
@@ -139,6 +142,7 @@ public class FsUserIntegralController {
      */
     @ApiOperation("添加积分")
     @PostMapping("/add")
+    @PreAuthorize("@ss.hasPermi('user:integral:add')")
     public R addIntegral(@RequestBody AddIntegralParam param) {
         Long userId = param.getUserId();
         Integer logType = param.getLogType();
@@ -183,4 +187,58 @@ public class FsUserIntegralController {
 
         return R.ok("添加积分成功");
     }
+
+    /**
+     * 校验用户是否存在
+     */
+    @ApiOperation("校验用户是否存在")
+    @GetMapping("/checkUser/{userId}")
+    public R checkUser(@PathVariable Long userId) {
+        FsUser user = userService.selectFsUserByUserId(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+        return R.ok().put("nickName", user.getNickName()).put("phone", user.getPhone()).put("integral", user.getIntegral());
+    }
+
+    /**
+     * 扣除积分
+     */
+    @ApiOperation("扣除积分")
+    @Log(title = "扣除积分", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('user:integral:deduct')")
+    @PostMapping("/deduct")
+    public R deductIntegral(@RequestBody AddIntegralParam param) {
+        Long userId = param.getUserId();
+        Integer logType = param.getLogType();
+        Long integral = param.getIntegral();
+        if (integral == null || integral <= 0) {
+            return R.error("扣除积分必须大于0");
+        }
+        // 查询用户信息
+        FsUser user = userService.selectFsUserByUserId(userId);
+        if (user == null) {
+            return R.error("用户不存在");
+        }
+        // 计算新的积分余额
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        if (currentIntegral < integral) {
+            return R.error("用户积分不足,当前积分:" + currentIntegral);
+        }
+        Long newIntegral = currentIntegral - integral;
+        // 更新用户积分(传入负数表示扣除)
+        userService.increaseIntegral(Collections.singletonList(user.getUserId()), -integral);
+        // 创建积分记录
+        Date now = new Date();
+        FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
+        integralLogs.setUserId(userId);
+        integralLogs.setLogType(logType);
+        integralLogs.setIntegral(-integral);
+        integralLogs.setBalance(newIntegral);
+        integralLogs.setCreateTime(now);
+        integralLogs.setUpdateTime(now);
+        integralLogsService.insertFsUserIntegralLogs(integralLogs);
+
+        return R.ok("扣除积分成功");
+    }
 }

+ 34 - 4
fs-common/src/main/java/com/fs/common/core/redis/RedisCache.java

@@ -3,10 +3,7 @@ package com.fs.common.core.redis;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.BoundSetOperations;
-import org.springframework.data.redis.core.HashOperations;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.data.redis.core.*;
 import org.springframework.stereotype.Component;
 
 /**
@@ -228,6 +225,39 @@ public class RedisCache
         return redisTemplate.keys(pattern);
     }
 
+    /**
+     * 使用SCAN命令遍历匹配的key,避免阻塞主线程
+     *
+     * @param pattern 匹配模式(如 "cacheName::*")
+     * @return 匹配的key集合
+     */
+    public Set<String> scan(final String pattern)
+    {
+        return (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
+            Set<String> keys = new HashSet<>();
+            Cursor<byte[]> cursor = connection.keyCommands().scan(
+                    ScanOptions.scanOptions()
+                            .match(pattern)
+                            .count(1000)
+                            .build()
+            );
+
+            try {
+                while (cursor.hasNext()) {
+                    keys.add(new String(cursor.next()));
+                }
+            } finally {
+                try {
+                    cursor.close();
+                } catch (Exception e) {
+                    // 忽略关闭异常
+                }
+            }
+
+            return keys;
+        });
+    }
+
 
     public Boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
         return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);

+ 2 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreAfterSalesScrmController.java

@@ -110,6 +110,8 @@ public class FsStoreAfterSalesScrmController extends BaseController
         List<FsStoreAfterSalesVO> list = fsStoreAfterSalesService.selectFsStoreAfterSalesListVO(fsStoreAfterSales);
         for (FsStoreAfterSalesVO vo : list){
             vo.setUserPhone(ParseUtils.parsePhone(vo.getUserPhone()));
+            vo.setRefundType(vo.getRefundAmount() != null && vo.getPayPrice() != null
+                    ? (vo.getRefundAmount().compareTo(vo.getPayPrice()) < 0 ? "部分退款" : "全额退款") : "");
         }
         ExcelUtil<FsStoreAfterSalesVO> util = new ExcelUtil<FsStoreAfterSalesVO>(FsStoreAfterSalesVO.class);
         return util.exportExcel(list, "storeAfterSales");

+ 23 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreProductPackageScrmController.java

@@ -156,6 +156,29 @@ public class FsStoreProductPackageScrmController extends BaseController
         return toAjax(fsStoreProductPackageService.updateFsStoreProductPackage(fsStoreProductPackage));
     }
 
+    /**
+     * 一键复制商品组合套餐
+     */
+    @PreAuthorize("@ss.hasPermi('store:storeProductPackage:add')")
+    @Log(title = "商品组合套餐", businessType = BusinessType.INSERT)
+    @PostMapping("/copy/{packageId}")
+    public AjaxResult copyPackage(@PathVariable("packageId") Long packageId)
+    {
+        FsStoreProductPackageScrm source = fsStoreProductPackageService.selectFsStoreProductPackageById(packageId);
+        if (source == null) {
+            return AjaxResult.error("未找到对应的套餐信息");
+        }
+        // 清空主键,让数据库自增生成新ID
+        source.setPackageId(null);
+        // 标题加"-副本"后缀
+        source.setTitle(source.getTitle() + "-副本");
+        // 设置当前登录用户的公司和部门
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        source.setCompanyId(loginUser.getCompany().getCompanyId());
+        source.setDeptId(loginUser.getUser().getDeptId());
+        return toAjax(fsStoreProductPackageService.insertFsStoreProductPackage(source));
+    }
+    
     /**
      * 删除商品组合套餐
      */

+ 43 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseMarketingEvent.java

@@ -0,0 +1,43 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 课程曝光/点击埋点对象 fs_course_marketing_event
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Data
+public class FsCourseMarketingEvent extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 课程id */
+    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 小节id,课程级事件可为空 */
+    @Excel(name = "小节id")
+    private Long videoId;
+
+    /** 0-曝光 1-点击 */
+    @Excel(name = "事件类型", readConverterExp = "0=曝光,1=点击")
+    private Integer eventType;
+
+    /** 0-首页推荐 1-课程中心 */
+    @Excel(name = "曝光位置", readConverterExp = "0=首页推荐,1=课程中心")
+    private Integer exposurePosition;
+}

+ 49 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseShareLog.java

@@ -0,0 +1,49 @@
+package com.fs.course.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 课程分享记录对象 fs_course_share_log
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Data
+public class FsCourseShareLog implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 用户id */
+    @Excel(name = "用户id")
+    private Long userId;
+
+    /** 课程id */
+    @Excel(name = "课程id")
+    private Long courseId;
+
+    /** 小节id */
+    @Excel(name = "小节id")
+    private Long videoId;
+
+    /** 分享类型 */
+    @Excel(name = "分享类型")
+    private Integer shareType;
+
+    /** 曝光位置 0-首页推荐 1-课程中心 */
+    @Excel(name = "曝光位置", readConverterExp = "0=首页推荐,1=课程中心")
+    private Integer exposurePosition;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 63 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseMarketingEventMapper.java

@@ -0,0 +1,63 @@
+package com.fs.course.mapper;
+
+import java.util.List;
+import com.fs.course.domain.FsCourseMarketingEvent;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 课程曝光/点击埋点Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Repository
+public interface FsCourseMarketingEventMapper
+{
+    /**
+     * 查询课程曝光/点击埋点
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 课程曝光/点击埋点
+     */
+    public FsCourseMarketingEvent selectFsCourseMarketingEventById(Long id);
+
+    /**
+     * 查询课程曝光/点击埋点列表
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 课程曝光/点击埋点集合
+     */
+    public List<FsCourseMarketingEvent> selectFsCourseMarketingEventList(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 新增课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    public int insertFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 修改课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    public int updateFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 删除课程曝光/点击埋点
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 结果
+     */
+    public int deleteFsCourseMarketingEventById(Long id);
+
+    /**
+     * 批量删除课程曝光/点击埋点
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteFsCourseMarketingEventByIds(Long[] ids);
+}

+ 64 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseShareLogMapper.java

@@ -0,0 +1,64 @@
+package com.fs.course.mapper;
+
+import java.util.List;
+import com.fs.course.domain.FsCourseShareLog;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 课程分享记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Repository
+public interface FsCourseShareLogMapper
+{
+    /**
+     * 查询课程分享记录
+     *
+     * @param id 课程分享记录主键
+     * @return 课程分享记录
+     */
+    public FsCourseShareLog selectFsCourseShareLogById(Long id);
+
+    /**
+     * 查询课程分享记录列表
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 课程分享记录集合
+     */
+    public List<FsCourseShareLog> selectFsCourseShareLogList(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 新增课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    public int insertFsCourseShareLog(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 修改课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    public int updateFsCourseShareLog(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 删除课程分享记录
+     *
+     * @param id 课程分享记录主键
+     * @return 结果
+     */
+    public int deleteFsCourseShareLogById(Long id);
+
+    /**
+     * 批量删除课程分享记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteFsCourseShareLogByIds(Long[] ids);
+}

+ 2 - 0
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchCommentMapper.java

@@ -85,4 +85,6 @@ public interface FsCourseWatchCommentMapper extends BaseMapper<FsCourseWatchComm
      * @return list
      */
     List<FsCourseWatchCommentVO> selectH5CourseWatchCommentsRound(FsCourseWatchCommentListParam param);
+
+    List<FsCourseWatchCommentListVO> selectFsCourseWatchCommentPublicList(FsCourseWatchCommentPageParam fsCourseWatchCommentPageParam);
 }

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

@@ -370,4 +370,14 @@ public interface FsUserCourseMapper
     @Select("SELECT COUNT(1) FROM fs_user_course WHERE IFNULL(is_del,0) = 0 AND (cate_id = #{cateId} OR sub_cate_id = #{cateId})")
     int countCourseByCategoryId(@Param("cateId") Long cateId);
 
+    /**
+     * 查询同分类下推荐位排序值重复的课程(首页课程顶部推荐位)
+     */
+    List<FsUserCourse> selectDuplicateRecHomeCourseTopSort(@Param("cateId") Long cateId, @Param("subCateId") Long subCateId, @Param("recHomeCourseTopSort") Integer recHomeCourseTopSort, @Param("excludeCourseId") Long excludeCourseId);
+
+    /**
+     * 查询同分类下推荐位排序值重复的课程(首页长视频瀑布流)
+     */
+    List<FsUserCourse> selectDuplicateRecHomeLongVideoSort(@Param("cateId") Long cateId, @Param("subCateId") Long subCateId, @Param("recHomeLongVideoSort") Integer recHomeLongVideoSort, @Param("excludeCourseId") Long excludeCourseId);
+
 }

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

@@ -71,6 +71,12 @@ public interface FsUserCourseStudyMapper
             "<if test = ' maps.userId  '> " +
             "and s.user_id = #{maps.userId} " +
             "</if>" +
+            "<if test = ' maps.courseType != null and maps.courseType == 1 '> " +
+            "and c.cate_id = (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1) " +
+            "</if>" +
+            "<if test = ' maps.courseType == null  '> " +
+            "and c.cate_id != (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1) " +
+            "</if>" +
             " order by s.study_id  "+
             "</script>"})
     List<FsUserCourseStudyListUVO> selectFsUserCourseStudyListUVO(@Param("maps")FsUserCourseListUParam param);

+ 5 - 0
fs-service/src/main/java/com/fs/course/mapper/PublicCourseWatchStatisticsMapper.java

@@ -1,6 +1,7 @@
 package com.fs.course.mapper;
 
 import com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.vo.CatalogUserStudyVO;
 import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
 import com.fs.course.vo.PublicCourseWatchStatCourseVO;
 import org.apache.ibatis.annotations.Param;
@@ -15,4 +16,8 @@ public interface PublicCourseWatchStatisticsMapper {
     List<PublicCourseWatchStatCourseVO> selectCourseDayStatList(@Param("q") PublicCourseWatchStatQueryParam q);
 
     List<PublicCourseWatchStatCatalogVO> selectCatalogStatList(@Param("q") PublicCourseWatchStatQueryParam q);
+
+    List<CatalogUserStudyVO> selectCatalogUserStudyList(@Param("videoId") Long videoId,
+                                                       @Param("nickName") String nickName,
+                                                       @Param("phone") String phone);
 }

+ 5 - 1
fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java

@@ -39,6 +39,9 @@ public class FsUserCourseCategoryAppQueryParam implements Serializable {
     @ApiModelProperty(value = "原乡行标签:不传或0=只统计/展示下挂课程中无「原乡行」标签的;1=只统计/展示含「原乡行」标签的课程", example = "0")
     private Integer yxxTag;
 
+    @ApiModelProperty(value = "首页排序 1 是首页,后端添加标签排序,0不是首页", example = "0")
+    private Integer homePage;
+
     /**
      * 与 {@link com.fs.course.service.impl.FsUserCourseCategoryServiceImpl#selectFsUserCourseCategoryAppList}
      * 中默认入参(null 补全)一致,供缓存 key 使用。
@@ -48,7 +51,8 @@ public class FsUserCourseCategoryAppQueryParam implements Serializable {
         int ps = pageSize != null ? pageSize : 10;
         int ct = cateType != null ? cateType : 1;
         int yxx = yxxTag != null ? yxxTag : 0;
-        return pn + "|" + ps + "|" + n(cateName) + "|" + n(pid) + "|" + n(isShow) + "|" + ct + "|" + yxx;
+        int home = homePage != null ? homePage : 0;
+        return pn + "|" + ps + "|" + n(cateName) + "|" + n(pid) + "|" + n(isShow) + "|" + ct + "|" + yxx  + "|" + home;
     }
 
     private static String n(Object o) {

+ 2 - 0
fs-service/src/main/java/com/fs/course/param/FsUserCourseListUParam.java

@@ -22,6 +22,8 @@ public class FsUserCourseListUParam  implements Serializable {
 
     Integer isBest;
 
+    Integer courseType;
+
     @ApiModelProperty(value = "页码,默认为1")
     private Integer pageNum =1;
     @ApiModelProperty(value = "页大小,默认为10")

+ 61 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseMarketingEventService.java

@@ -0,0 +1,61 @@
+package com.fs.course.service;
+
+import java.util.List;
+import com.fs.course.domain.FsCourseMarketingEvent;
+
+/**
+ * 课程曝光/点击埋点Service接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+public interface IFsCourseMarketingEventService
+{
+    /**
+     * 查询课程曝光/点击埋点
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 课程曝光/点击埋点
+     */
+    public FsCourseMarketingEvent selectFsCourseMarketingEventById(Long id);
+
+    /**
+     * 查询课程曝光/点击埋点列表
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 课程曝光/点击埋点集合
+     */
+    public List<FsCourseMarketingEvent> selectFsCourseMarketingEventList(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 新增课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    public int insertFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 修改课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    public int updateFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent);
+
+    /**
+     * 批量删除课程曝光/点击埋点
+     *
+     * @param ids 需要删除的课程曝光/点击埋点主键集合
+     * @return 结果
+     */
+    public int deleteFsCourseMarketingEventByIds(Long[] ids);
+
+    /**
+     * 删除课程曝光/点击埋点信息
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 结果
+     */
+    public int deleteFsCourseMarketingEventById(Long id);
+}

+ 61 - 0
fs-service/src/main/java/com/fs/course/service/IFsCourseShareLogService.java

@@ -0,0 +1,61 @@
+package com.fs.course.service;
+
+import java.util.List;
+import com.fs.course.domain.FsCourseShareLog;
+
+/**
+ * 课程分享记录Service接口
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+public interface IFsCourseShareLogService
+{
+    /**
+     * 查询课程分享记录
+     *
+     * @param id 课程分享记录主键
+     * @return 课程分享记录
+     */
+    public FsCourseShareLog selectFsCourseShareLogById(Long id);
+
+    /**
+     * 查询课程分享记录列表
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 课程分享记录集合
+     */
+    public List<FsCourseShareLog> selectFsCourseShareLogList(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 新增课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    public int insertFsCourseShareLog(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 修改课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    public int updateFsCourseShareLog(FsCourseShareLog fsCourseShareLog);
+
+    /**
+     * 批量删除课程分享记录
+     *
+     * @param ids 需要删除的课程分享记录主键集合
+     * @return 结果
+     */
+    public int deleteFsCourseShareLogByIds(Long[] ids);
+
+    /**
+     * 删除课程分享记录信息
+     *
+     * @param id 课程分享记录主键
+     * @return 结果
+     */
+    public int deleteFsCourseShareLogById(Long id);
+}

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

@@ -86,4 +86,5 @@ public interface IFsCourseWatchCommentService extends IService<FsCourseWatchComm
      */
     List<FsCourseWatchCommentVO> selectH5CourseWatchComments(FsCourseWatchCommentListParam param);
 
+    List<FsCourseWatchCommentListVO> selectFsCourseWatchCommentPublicList(FsCourseWatchCommentPageParam fsCourseWatchCommentPageParam);
 }

+ 3 - 0
fs-service/src/main/java/com/fs/course/service/IPublicCourseWatchStatisticsService.java

@@ -1,6 +1,7 @@
 package com.fs.course.service;
 
 import com.fs.course.param.PublicCourseWatchStatQueryParam;
+import com.fs.course.vo.CatalogUserStudyVO;
 import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
 import com.fs.course.vo.PublicCourseWatchStatCourseVO;
 
@@ -14,4 +15,6 @@ public interface IPublicCourseWatchStatisticsService {
     List<PublicCourseWatchStatCourseVO> listCourseDayStat(PublicCourseWatchStatQueryParam param);
 
     List<PublicCourseWatchStatCatalogVO> listCatalogStat(PublicCourseWatchStatQueryParam param);
+
+    List<CatalogUserStudyVO> listCatalogUserStudy(Long videoId, String nickName, String phone);
 }

+ 96 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseMarketingEventServiceImpl.java

@@ -0,0 +1,96 @@
+package com.fs.course.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FsCourseMarketingEvent;
+import com.fs.course.mapper.FsCourseMarketingEventMapper;
+import com.fs.course.service.IFsCourseMarketingEventService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 课程曝光/点击埋点Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Service
+public class FsCourseMarketingEventServiceImpl implements IFsCourseMarketingEventService
+{
+    @Autowired
+    private FsCourseMarketingEventMapper fsCourseMarketingEventMapper;
+
+    /**
+     * 查询课程曝光/点击埋点
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 课程曝光/点击埋点
+     */
+    @Override
+    public FsCourseMarketingEvent selectFsCourseMarketingEventById(Long id)
+    {
+        return fsCourseMarketingEventMapper.selectFsCourseMarketingEventById(id);
+    }
+
+    /**
+     * 查询课程曝光/点击埋点列表
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 课程曝光/点击埋点
+     */
+    @Override
+    public List<FsCourseMarketingEvent> selectFsCourseMarketingEventList(FsCourseMarketingEvent fsCourseMarketingEvent)
+    {
+        return fsCourseMarketingEventMapper.selectFsCourseMarketingEventList(fsCourseMarketingEvent);
+    }
+
+    /**
+     * 新增课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent)
+    {
+        fsCourseMarketingEvent.setCreateTime(DateUtils.getNowDate());
+        return fsCourseMarketingEventMapper.insertFsCourseMarketingEvent(fsCourseMarketingEvent);
+    }
+
+    /**
+     * 修改课程曝光/点击埋点
+     *
+     * @param fsCourseMarketingEvent 课程曝光/点击埋点
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseMarketingEvent(FsCourseMarketingEvent fsCourseMarketingEvent)
+    {
+        fsCourseMarketingEvent.setUpdateTime(DateUtils.getNowDate());
+        return fsCourseMarketingEventMapper.updateFsCourseMarketingEvent(fsCourseMarketingEvent);
+    }
+
+    /**
+     * 批量删除课程曝光/点击埋点
+     *
+     * @param ids 需要删除的课程曝光/点击埋点主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseMarketingEventByIds(Long[] ids)
+    {
+        return fsCourseMarketingEventMapper.deleteFsCourseMarketingEventByIds(ids);
+    }
+
+    /**
+     * 删除课程曝光/点击埋点信息
+     *
+     * @param id 课程曝光/点击埋点主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseMarketingEventById(Long id)
+    {
+        return fsCourseMarketingEventMapper.deleteFsCourseMarketingEventById(id);
+    }
+}

+ 12 - 23
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -645,17 +645,12 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         if (param.getLinkType() != null && param.getLinkType() == 1) {
             errorCount = courseAnswerLogsMapper.selectErrorCountByCourseVideoToday(param.getVideoId(), param.getUserId(), null, null);
         } else {
-            FsCourseWatchLog courseWatchLog = resolveCourseWatchLogForApp(
-                    param.getVideoId(), param.getUserId(), param.getCompanyUserId(), param.getPeriodId());
-            if (courseWatchLog == null) {
+            if (studyRow == null) {
                 return R.error("无看课记录");
             }
-            if (courseWatchLog.getLogType() == null || courseWatchLog.getLogType() != 2) {
-                return R.error("未完课");
-            }
-            logId = courseWatchLog.getLogId();
+            logId = Long.valueOf(studyRow.getLogId());
             errorCount = courseAnswerLogsMapper.selectErrorCountByCourseVideoToday(
-                    param.getVideoId(), param.getUserId(), param.getQwUserId(), courseWatchLog.getProject());
+                    param.getVideoId(), param.getUserId(), param.getQwUserId(), null);
         }
 
         if (errorCount >= APP_USER_ANSWER_MAX_ATTEMPTS) {
@@ -663,8 +658,12 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         }
         int remainCount = APP_USER_ANSWER_MAX_ATTEMPTS - errorCount - 1;
 
+
         List<FsCourseQuestionBank> questions = param.getQuestions();
         if (questions != null && !questions.isEmpty()) {
+//            String json = configService.selectConfigByKey("course.config");
+//            CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+//            boolean skipAnswerValidation = CourseConfigUserAnswerExpose.skipValidateAnswerOnSubmit(json);
 //            if (skipAnswerValidation) {
 //                thisRightCount = questions.size();
 //            } else {
@@ -775,14 +774,10 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
             if (param.getLinkType() != null && param.getLinkType() == 1) {
                 rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideo(param.getVideoId(), param.getUserId(), null);
             } else {
-                FsCourseWatchLog watchLog = resolveCourseWatchLogForApp(
-                        param.getVideoId(), param.getUserId(), param.getCompanyUserId(), param.getPeriodId());
-                if (watchLog == null) {
+                if (studyRow == null) {
                     return R.error("无看课记录");
                 }
-                if (watchLog.getLogType() == null || watchLog.getLogType() != 2) {
-                    return R.error("未完课");
-                }
+
                 if (param.getPeriodId() != null) {
                     rightLog = courseAnswerLogsMapper.selectRightLogByCourseVideoWithPeriodId(
                             param.getVideoId(), param.getUserId(), param.getQwUserId(), param.getPeriodId());
@@ -875,21 +870,15 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
             return vo;
         }
 
-        FsCourseWatchLog watchLog = resolveCourseWatchLogForApp(
-                param.getVideoId(), param.getUserId(), param.getCompanyUserId(), param.getPeriodId());
-        if (watchLog == null) {
+
+        if (studyRow == null) {
             vo.setStatus(-2);
             vo.setStatusText("无看课记录");
             return vo;
         }
-        if (watchLog.getLogType() == null || watchLog.getLogType() != 2) {
-            vo.setStatus(-3);
-            vo.setStatusText("未完课");
-            return vo;
-        }
 
         int errorCount = courseAnswerLogsMapper.selectErrorCountByCourseVideoToday(
-                param.getVideoId(), param.getUserId(), param.getQwUserId(), watchLog.getProject());
+                param.getVideoId(), param.getUserId(), param.getQwUserId(), null);
         vo.setErrorCount(errorCount);
         fillProgressByErrorCount(vo, errorCount);
         return vo;

+ 95 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseShareLogServiceImpl.java

@@ -0,0 +1,95 @@
+package com.fs.course.service.impl;
+
+import java.util.List;
+import com.fs.common.utils.DateUtils;
+import com.fs.course.domain.FsCourseShareLog;
+import com.fs.course.mapper.FsCourseShareLogMapper;
+import com.fs.course.service.IFsCourseShareLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 课程分享记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-29
+ */
+@Service
+public class FsCourseShareLogServiceImpl implements IFsCourseShareLogService
+{
+    @Autowired
+    private FsCourseShareLogMapper fsCourseShareLogMapper;
+
+    /**
+     * 查询课程分享记录
+     *
+     * @param id 课程分享记录主键
+     * @return 课程分享记录
+     */
+    @Override
+    public FsCourseShareLog selectFsCourseShareLogById(Long id)
+    {
+        return fsCourseShareLogMapper.selectFsCourseShareLogById(id);
+    }
+
+    /**
+     * 查询课程分享记录列表
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 课程分享记录
+     */
+    @Override
+    public List<FsCourseShareLog> selectFsCourseShareLogList(FsCourseShareLog fsCourseShareLog)
+    {
+        return fsCourseShareLogMapper.selectFsCourseShareLogList(fsCourseShareLog);
+    }
+
+    /**
+     * 新增课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    @Override
+    public int insertFsCourseShareLog(FsCourseShareLog fsCourseShareLog)
+    {
+        fsCourseShareLog.setCreateTime(DateUtils.getNowDate());
+        return fsCourseShareLogMapper.insertFsCourseShareLog(fsCourseShareLog);
+    }
+
+    /**
+     * 修改课程分享记录
+     *
+     * @param fsCourseShareLog 课程分享记录
+     * @return 结果
+     */
+    @Override
+    public int updateFsCourseShareLog(FsCourseShareLog fsCourseShareLog)
+    {
+        return fsCourseShareLogMapper.updateFsCourseShareLog(fsCourseShareLog);
+    }
+
+    /**
+     * 批量删除课程分享记录
+     *
+     * @param ids 需要删除的课程分享记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseShareLogByIds(Long[] ids)
+    {
+        return fsCourseShareLogMapper.deleteFsCourseShareLogByIds(ids);
+    }
+
+    /**
+     * 删除课程分享记录信息
+     *
+     * @param id 课程分享记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteFsCourseShareLogById(Long id)
+    {
+        return fsCourseShareLogMapper.deleteFsCourseShareLogById(id);
+    }
+}

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

@@ -1,5 +1,6 @@
 package com.fs.course.service.impl;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -172,4 +173,9 @@ public class FsCourseWatchCommentServiceImpl extends ServiceImpl<FsCourseWatchCo
         }
     }
 
+    @Override
+    public List<FsCourseWatchCommentListVO> selectFsCourseWatchCommentPublicList(FsCourseWatchCommentPageParam fsCourseWatchCommentPageParam) {
+        return baseMapper.selectFsCourseWatchCommentPublicList(fsCourseWatchCommentPageParam);
+    }
+
 }

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

@@ -73,7 +73,7 @@ public class FsUserCourseCategoryServiceImpl implements IFsUserCourseCategorySer
 //    @Cacheable(
 //            value = PublicCourseAppCacheNames.CATEGORY_APP_LIST,
 //            key = "#param == null ? 'default' : #param.appListCacheKey()")
-    @Cacheable(value = "publicCourseCategoryApp", key = "#param")
+    @Cacheable(value = "publicCourseCategoryApp", key = "#param.appListCacheKey()")
     public PageInfo<FsUserCourseCategory> selectFsUserCourseCategoryAppPage(FsUserCourseCategoryAppQueryParam param) {
         if (param == null) {
             param = new FsUserCourseCategoryAppQueryParam();

+ 55 - 3
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseServiceImpl.java

@@ -14,6 +14,8 @@ import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.fs.common.utils.RedisUtil;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.CloudHostUtils;
@@ -43,6 +45,7 @@ import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.param.FsUserAddIntegralTemplateParam;
 import com.fs.his.service.IFsUserIntegralLogsService;
 import com.fs.his.service.IFsUserNewTaskService;
+import com.fs.his.utils.RedisCacheUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.service.IQwCompanyService;
@@ -131,6 +134,9 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     @Autowired
     private RedisCache redisCache;
 
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
     @Autowired
     private AsyncUploadQwCourseImageService asyncUploadQwCourseImageService;
     @Autowired
@@ -147,6 +153,49 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     private static final String appRealLink = "/appcourse/pages_course/videovip?course=";
     public static final String appShortLink = "/courseH5/pages_course/videovip?s=";
 
+    @Autowired
+    private RedisCacheUtil redisCacheUtil;
+
+    /**
+     * 校验同分类下推荐位排序值是否重复(仅非北京卓美客户校验)
+     * @param fsUserCourse 课程对象
+     * @param excludeCourseId 排除的课程ID(修改时排除自身,新增时传null)
+     */
+    private void validateDuplicateSort(FsUserCourse fsUserCourse, Long excludeCourseId) {
+        if (!"北京卓美".equals(cloudHostProper.getCompanyName())) {
+            return;
+        }
+        Long cateId = fsUserCourse.getCateId();
+        Long subCateId = fsUserCourse.getSubCateId();
+
+        // 校验首页课程顶部推荐位排序值重复
+        if (fsUserCourse.getRecHomeCourseTopSort() != null) {
+            List<FsUserCourse> duplicateTop = fsUserCourseMapper.selectDuplicateRecHomeCourseTopSort(
+                    cateId, subCateId, fsUserCourse.getRecHomeCourseTopSort(), excludeCourseId);
+            if (!duplicateTop.isEmpty()) {
+                String duplicateNames = duplicateTop.stream()
+                        .map(FsUserCourse::getCourseName)
+                        .collect(java.util.stream.Collectors.joining("、"));
+                throw new com.fs.common.exception.CustomException(
+                        "首页课程顶部推荐位重复:" + duplicateNames);
+            }
+        }
+
+        // 校验首页长视频瀑布流排序值重复
+        if (fsUserCourse.getRecHomeLongVideoSort() != null) {
+            List<FsUserCourse> duplicateLong = fsUserCourseMapper.selectDuplicateRecHomeLongVideoSort(
+                    cateId, subCateId, fsUserCourse.getRecHomeLongVideoSort(), excludeCourseId);
+            if (duplicateLong != null && !duplicateLong.isEmpty()) {
+                String duplicateNames = duplicateLong.stream()
+                        .map(FsUserCourse::getCourseName)
+                        .collect(java.util.stream.Collectors.joining("、"));
+                throw new com.fs.common.exception.CustomException(
+                        "首页长视频瀑布流重复:" + duplicateNames);
+            }
+        }
+        redisCacheUtil.delSpringCacheAllByName(PublicCourseAppCacheNames.CATEGORY_APP_LIST);
+    }
+
     /**
      * 查询课程
      *
@@ -185,6 +234,8 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
             asyncUploadQwCourseImageService.uploadQwCourseImage(fsUserCourse);
         }
 
+        // 校验推荐位排序值重复
+        validateDuplicateSort(fsUserCourse, null);
 
         fsUserCourse.setCreateTime(DateUtils.getNowDate());
         return  fsUserCourseMapper.insertFsUserCourse(fsUserCourse);
@@ -203,6 +254,9 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
             asyncUploadQwCourseImageService.uploadQwCourseImage(fsUserCourse);
         }
 
+        // 校验推荐位排序值重复
+        validateDuplicateSort(fsUserCourse, fsUserCourse.getCourseId());
+
         fsUserCourse.setUpdateTime(DateUtils.getNowDate());
         return fsUserCourseMapper.updateFsUserCourse(fsUserCourse);
     }
@@ -294,9 +348,7 @@ public class FsUserCourseServiceImpl implements IFsUserCourseService
     }
 
     @Override
-    @Cacheable(
-            value = PublicCourseAppCacheNames.COURSE_PUBLIC_APP_LIST,
-            key = "#param == null ? 'default' : #param.appListCacheKey()")
+    @Cacheable(value = "publicCoursePublicApp", key = "#param.appListCacheKey()")
     public PageInfo<FsUserCoursePublicAppVO> selectFsUserCoursePublicAppPage(FsUserCoursePublicAppQueryParam param) {
         if (param == null) {
             param = new FsUserCoursePublicAppQueryParam();

+ 17 - 0
fs-service/src/main/java/com/fs/course/service/impl/PublicCourseWatchStatisticsServiceImpl.java

@@ -4,8 +4,11 @@ import com.fs.common.utils.StringUtils;
 import com.fs.course.mapper.PublicCourseWatchStatisticsMapper;
 import com.fs.course.param.PublicCourseWatchStatQueryParam;
 import com.fs.course.service.IPublicCourseWatchStatisticsService;
+import com.fs.course.vo.CatalogUserStudyVO;
 import com.fs.course.vo.PublicCourseWatchStatCatalogVO;
 import com.fs.course.vo.PublicCourseWatchStatCourseVO;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -22,6 +25,9 @@ public class PublicCourseWatchStatisticsServiceImpl implements IPublicCourseWatc
     @Autowired
     private PublicCourseWatchStatisticsMapper publicCourseWatchStatisticsMapper;
 
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
     @Override
     public List<PublicCourseWatchStatCourseVO> listCourseDayStat(PublicCourseWatchStatQueryParam param) {
         fillDefaultDateRange(param);
@@ -52,6 +58,17 @@ public class PublicCourseWatchStatisticsServiceImpl implements IPublicCourseWatc
         return publicCourseWatchStatisticsMapper.selectCatalogStatList(param);
     }
 
+    @Override
+    public List<CatalogUserStudyVO> listCatalogUserStudy(Long videoId, String nickName, String phone) {
+        List<CatalogUserStudyVO> list = publicCourseWatchStatisticsMapper.selectCatalogUserStudyList(videoId, nickName, phone);
+        for (CatalogUserStudyVO vo : list) {
+            // 查询用户是否领取过积分(log_type=17 答题积分,business_id = videoId)
+            FsUserIntegralLogs integralLog = fsUserIntegralLogsMapper.selectAnswerRewardIntegralLog(vo.getUserId(), String.valueOf(videoId));
+            vo.setIntegralClaimed(integralLog != null ? "是" : "否");
+        }
+        return list;
+    }
+
     private void fillDefaultDateRange(PublicCourseWatchStatQueryParam param) {
         if (param == null) {
             return;

+ 40 - 0
fs-service/src/main/java/com/fs/course/vo/CatalogUserStudyVO.java

@@ -0,0 +1,40 @@
+package com.fs.course.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 目录用户学习记录VO
+ */
+@Data
+@ApiModel(value = "目录用户学习记录")
+public class CatalogUserStudyVO {
+
+    @ApiModelProperty(value = "用户ID")
+    private Long userId;
+
+    @ApiModelProperty(value = "微信昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "手机号")
+    private String phone;
+
+    @ApiModelProperty(value = "累计学习时长(秒)")
+    private Long totalDuration;
+
+    @ApiModelProperty(value = "首次看课时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date firstWatchTime;
+
+    @ApiModelProperty(value = "最近看课时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastWatchTime;
+
+    @ApiModelProperty(value = "是否领取积分")
+    private String integralClaimed;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserCoursePublicAppVO.java

@@ -36,6 +36,9 @@ public class FsUserCoursePublicAppVO implements Serializable {
     @ApiModelProperty("看课人数(看课记录表 send_type=1 下去重 user_id)")
     private Long watchUserCount;
 
+    @ApiModelProperty("看课默认人数")
+    private Long views;
+
     @ApiModelProperty("首页课程顶部推荐位:0否 1是")
     private Integer recHomeCourseTopEnabled;
     @ApiModelProperty("首页顶部推荐方式:1插入 2替换")

+ 6 - 6
fs-service/src/main/java/com/fs/his/utils/RedisCacheUtil.java

@@ -1,14 +1,11 @@
 package com.fs.his.utils;
 
 import com.fs.common.core.redis.RedisCache;
-import org.apache.ibatis.annotations.Select;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
-import java.util.Base64;
 import java.util.Collection;
+import java.util.Set;
 @Service
 public class RedisCacheUtil {
     @Autowired
@@ -32,9 +29,12 @@ public class RedisCacheUtil {
     }
 
     /**
-     * 清除某个 Spring {@link org.springframework.cache.annotation.Cacheable} 名称下的全部 Redis 项(同前缀 "cacheName::")
+     * 使用SCAN扫描并清除某个 Spring {@link org.springframework.cache.annotation.Cacheable} 名称下的全部 Redis 项(同前缀 "cacheName::")
      */
     public void delSpringCacheAllByName(String cacheName) {
-        delRedisKey(cacheName + "::");
+        Set<String> keys = redisCache.scan(cacheName + "::*");
+        if (keys != null && !keys.isEmpty()) {
+            redisCache.deleteObject(keys);
+        }
     }
 }

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

@@ -722,6 +722,10 @@ public class FsStoreAfterSalesScrmServiceImpl implements IFsStoreAfterSalesScrmS
         boolean mapEmpty = orderItemMap.isEmpty();
         if (null != fsStoreAfterSalesVOS && !fsStoreAfterSalesVOS.isEmpty()) {
             for (FsStoreAfterSalesVO item : fsStoreAfterSalesVOS) {
+                // 计算退款类型:退款金额<实付金额为部分退款,等于为全额退款
+                if (item.getRefundAmount() != null && item.getPayPrice() != null) {
+                    item.setRefundType(item.getRefundAmount().compareTo(item.getPayPrice()) < 0 ? "部分退款" : "全额退款");
+                }
                 if(!mapEmpty && orderItemMap.containsKey(item.getOrderId())){
                     List<FsStoreOrderItemVO> orderItems = orderItemMap.get(item.getOrderId());
                     for (FsStoreOrderItemVO orderItem : orderItems) {

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreAfterSalesVO.java

@@ -150,6 +150,9 @@ public class FsStoreAfterSalesVO implements Serializable
     private Long reasonId1;
 
     private Long reasonId2;
+    @Excel(name = "退款类型")
+    private String refundType;
+
     @Excel(name ="售后原因一级")
     @TableField(exist = false)
     private String reasonValue1;

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreOrderItemExportRefundZMVO.java

@@ -142,6 +142,9 @@ public class FsStoreOrderItemExportRefundZMVO implements Serializable  {
     @Excel(name = "说明",sort = 260)
     private String explains;
 
+    @Excel(name ="退款类型",sort = 245)
+    private String refundType;
+
     @Excel(name ="售后原因一级")
     @TableField(exist = false)
     private String reasonValue1;

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

@@ -0,0 +1,79 @@
+<?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.FsCourseMarketingEventMapper">
+
+    <resultMap id="FsCourseMarketingEventResult" type="com.fs.course.domain.FsCourseMarketingEvent">
+        <id property="id" column="id"/>
+        <result property="userId" column="user_id"/>
+        <result property="courseId" column="course_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="eventType" column="event_type"/>
+        <result property="exposurePosition" column="exposure_position"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <sql id="selectFsCourseMarketingEventVo">
+        select id, user_id, course_id, video_id, event_type, exposure_position, create_time
+        from fs_course_marketing_event
+    </sql>
+
+    <select id="selectFsCourseMarketingEventById" parameterType="Long" resultMap="FsCourseMarketingEventResult">
+        <include refid="selectFsCourseMarketingEventVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectFsCourseMarketingEventList" parameterType="com.fs.course.domain.FsCourseMarketingEvent" resultMap="FsCourseMarketingEventResult">
+        <include refid="selectFsCourseMarketingEventVo"/>
+        <where>
+            <if test="userId != null"> and user_id = #{userId}</if>
+            <if test="courseId != null"> and course_id = #{courseId}</if>
+            <if test="videoId != null"> and video_id = #{videoId}</if>
+            <if test="eventType != null"> and event_type = #{eventType}</if>
+            <if test="exposurePosition != null"> and exposure_position = #{exposurePosition}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <insert id="insertFsCourseMarketingEvent" parameterType="com.fs.course.domain.FsCourseMarketingEvent" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_marketing_event (
+            user_id,
+            course_id,
+            video_id,
+            event_type,
+            exposure_position,
+            create_time
+        ) values (
+            #{userId},
+            #{courseId},
+            #{videoId},
+            #{eventType},
+            #{exposurePosition},
+            #{createTime}
+        )
+    </insert>
+
+    <update id="updateFsCourseMarketingEvent" parameterType="com.fs.course.domain.FsCourseMarketingEvent">
+        update fs_course_marketing_event
+        <set>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="eventType != null">event_type = #{eventType},</if>
+            <if test="exposurePosition != null">exposure_position = #{exposurePosition},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseMarketingEventById" parameterType="Long">
+        delete from fs_course_marketing_event where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseMarketingEventByIds" parameterType="Long">
+        delete from fs_course_marketing_event where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>

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

@@ -0,0 +1,79 @@
+<?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.FsCourseShareLogMapper">
+
+    <resultMap id="FsCourseShareLogResult" type="com.fs.course.domain.FsCourseShareLog">
+        <id property="id" column="id"/>
+        <result property="userId" column="user_id"/>
+        <result property="courseId" column="course_id"/>
+        <result property="videoId" column="video_id"/>
+        <result property="shareType" column="share_type"/>
+        <result property="exposurePosition" column="exposure_position"/>
+        <result property="createTime" column="create_time"/>
+    </resultMap>
+
+    <sql id="selectFsCourseShareLogVo">
+        select id, user_id, course_id, video_id, share_type, exposure_position, create_time
+        from fs_course_share_log
+    </sql>
+
+    <select id="selectFsCourseShareLogById" parameterType="Long" resultMap="FsCourseShareLogResult">
+        <include refid="selectFsCourseShareLogVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectFsCourseShareLogList" parameterType="com.fs.course.domain.FsCourseShareLog" resultMap="FsCourseShareLogResult">
+        <include refid="selectFsCourseShareLogVo"/>
+        <where>
+            <if test="userId != null"> and user_id = #{userId}</if>
+            <if test="courseId != null"> and course_id = #{courseId}</if>
+            <if test="videoId != null"> and video_id = #{videoId}</if>
+            <if test="shareType != null"> and share_type = #{shareType}</if>
+            <if test="exposurePosition != null"> and exposure_position = #{exposurePosition}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <insert id="insertFsCourseShareLog" parameterType="com.fs.course.domain.FsCourseShareLog" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_course_share_log (
+            user_id,
+            course_id,
+            video_id,
+            share_type,
+            exposure_position,
+            create_time
+        ) values (
+            #{userId},
+            #{courseId},
+            #{videoId},
+            #{shareType},
+            #{exposurePosition},
+            #{createTime}
+        )
+    </insert>
+
+    <update id="updateFsCourseShareLog" parameterType="com.fs.course.domain.FsCourseShareLog">
+        update fs_course_share_log
+        <set>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="courseId != null">course_id = #{courseId},</if>
+            <if test="videoId != null">video_id = #{videoId},</if>
+            <if test="shareType != null">share_type = #{shareType},</if>
+            <if test="exposurePosition != null">exposure_position = #{exposurePosition},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteFsCourseShareLogById" parameterType="Long">
+        delete from fs_course_share_log where id = #{id}
+    </delete>
+
+    <delete id="deleteFsCourseShareLogByIds" parameterType="Long">
+        delete from fs_course_share_log where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+</mapper>

+ 53 - 0
fs-service/src/main/resources/mapper/course/FsCourseWatchCommentMapper.xml

@@ -80,6 +80,59 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
     </select>
 
+    <!-- 查询公域课程的评论列表(用于公域课评论页) -->
+    <select id="selectFsCourseWatchCommentPublicList" resultType="com.fs.course.vo.FsCourseWatchCommentListVO">
+        SELECT
+            c.comment_id,
+            c.user_id,
+            c.course_id,
+            c.type,
+            c.parent_id,
+            c.content,
+            c.reply_count,
+            c.likes,
+            c.to_user_id,
+            c.create_time,
+            c.update_time,
+            u.nick_name,
+            COALESCE(uc.title, uc.course_name) AS course_name
+        FROM fs_user_course_comment c
+        INNER JOIN fs_user_course uc ON uc.course_id = c.course_id
+        LEFT JOIN fs_user u ON u.user_id = c.user_id
+        <where>
+            c.is_del = 0
+            <if test="courseId != null"> AND c.course_id = #{courseId}</if>
+            <if test="nickName != null and nickName != ''"> AND u.nick_name LIKE CONCAT('%', #{nickName}, '%')</if>
+            <if test="keywords != null and keywords != '' and isAll != null and isAll == true">
+                AND (
+                    u.nick_name LIKE CONCAT('%',#{keywords},'%')
+                    OR uc.course_name LIKE CONCAT('%',#{keywords},'%')
+                    OR uc.title LIKE CONCAT('%',#{keywords},'%')
+                )
+            </if>
+            <!-- 公域课程筛选:非私域 + 未删除 -->
+            AND IFNULL(uc.is_private, 0) = 0
+            AND EXISTS (
+                SELECT 1
+                FROM fs_user_course_category pc
+                WHERE pc.cate_id = uc.cate_id
+                  AND pc.cate_type = 1
+                  AND IFNULL(pc.is_del, 0) = 0
+            )
+            AND (
+                uc.sub_cate_id IS NULL
+                OR EXISTS (
+                    SELECT 1
+                    FROM fs_user_course_category sc
+                    WHERE sc.cate_id = uc.sub_cate_id
+                      AND sc.cate_type = 1
+                      AND IFNULL(sc.is_del, 0) = 0
+                )
+            )
+        </where>
+        ORDER BY c.comment_id DESC
+    </select>
+
     <select id="selectFsCourseWatchCommentByCommentId" parameterType="Long" resultMap="FsCourseWatchCommentResult">
         <include refid="selectFsCourseWatchCommentVo"/>
         where comment_id = #{commentId}

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

@@ -38,13 +38,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
          pid 有值:只查该一级下的二级;pid 无值:仅保留在公域课 fs_user_course 中 sub_cate_id 有上架使用 的二级 -->
     <select id="selectFsUserCourseCategoryAppList" parameterType="com.fs.course.param.FsUserCourseCategoryAppQueryParam" resultType="com.fs.course.domain.FsUserCourseCategory">
         SELECT
-            c.cate_id, c.pid, c.cate_name, c.sort, c.is_show, c.create_time, c.update_time, c.is_del, c.cate_type
+         distinct c.cate_id, c.pid, c.cate_name, c.sort, c.is_show, c.create_time, c.update_time, c.is_del, c.cate_type
         FROM fs_user_course_category c
-        INNER JOIN fs_user_course_category p ON p.cate_id = c.pid
+        LEFT JOIN fs_user_course_category p ON p.cate_id = c.pid
+        <if test="homePage != null and homePage == 1">
+            left JOIN  fs_user_course d on c.cate_id = d.sub_cate_id
+        </if>
         <where>
             c.is_del = 0
             AND p.is_del = 0
             AND p.pid = 0
+                <if test="yxxTag != null and yxxTag == 1 and homePage != null and homePage == 1">
+                    and d.rec_home_course_top_enabled = 1
+                </if>
+                <if test="yxxTag == null and homePage != null and homePage == 1">
+                    and d.rec_home_long_video_enabled = 1
+                </if>
             <choose>
                 <when test="yxxTag != null and yxxTag == 1">
                     AND c.pid = (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1)
@@ -104,7 +113,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 )
             </if>
         </where>
-        ORDER BY c.sort ASC, c.cate_id ASC
+        ORDER BY
+        <if test="yxxTag != null and yxxTag == 1 and homePage != null and homePage == 1">
+            ifnull(d.rec_home_course_top_sort,9999),
+        </if>
+        <if test="yxxTag == null and homePage != null and homePage == 1">
+            ifnull(d.rec_home_long_video_sort,9999),
+        </if>
+        c.sort ASC, c.cate_id ASC
     </select>
 
     <select id="selectFsUserCourseCategoryByCateId" parameterType="Long" resultMap="FsUserCourseCategoryResult">

+ 29 - 8
fs-service/src/main/resources/mapper/course/FsUserCourseMapper.xml

@@ -389,7 +389,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             c.description AS courseDesc,
             c.img_url AS imgUrl,
             c.second_img AS secondImg,
-            IFNULL(wl_stat.watch_uv, 0) AS watchUserCount,
+            IFNULL(c.views, 0) AS watchUserCount,
             IFNULL(c.rec_home_course_top_enabled, 0) AS recHomeCourseTopEnabled,
             c.rec_home_course_top_mode AS recHomeCourseTopMode,
             c.rec_home_course_top_sort AS recHomeCourseTopSort,
@@ -400,12 +400,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             c.rec_home_long_video_mode AS recHomeLongVideoMode,
             c.rec_home_long_video_sort AS recHomeLongVideoSort
         FROM fs_user_course c
-        LEFT JOIN (
-            SELECT course_id, COUNT(DISTINCT user_id) AS watch_uv
-            FROM fs_course_watch_log
-            WHERE send_type = 1
-            GROUP BY course_id
-        ) wl_stat ON wl_stat.course_id = c.course_id
         WHERE c.is_del = 0
         AND c.is_show = 1
         AND c.is_private = 0
@@ -464,6 +458,33 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <if test="q.recommendSlot != null and q.recommendSlot == 3">
             IFNULL(c.rec_home_long_video_sort, 999999) ASC,
         </if>
-        c.sort ASC, c.course_id DESC
+        c.update_time desc,c.sort ASC, c.course_id DESC
+    </select>
+
+    <!-- 查询同分类下首页课程顶部推荐位排序值重复的课程 -->
+    <select id="selectDuplicateRecHomeCourseTopSort" resultType="com.fs.course.domain.FsUserCourse">
+        SELECT course_id, course_name FROM fs_user_course
+        WHERE is_del = 0
+        <if test="cateId != null">
+            AND cate_id = #{cateId}
+        </if>
+        <if test="cateId == null">
+            AND cate_id IS NULL
+        </if>
+        AND rec_home_course_top_sort = #{recHomeCourseTopSort}
+        <if test="excludeCourseId != null">
+            AND course_id != #{excludeCourseId}
+        </if>
+    </select>
+
+    <!-- 查询同分类下首页长视频瀑布流排序值重复的课程 -->
+    <select id="selectDuplicateRecHomeLongVideoSort" resultType="com.fs.course.domain.FsUserCourse">
+        SELECT course_id, course_name FROM fs_user_course
+        WHERE is_del = 0
+        AND cate_id = #{cateId}
+        AND rec_home_long_video_sort = #{recHomeLongVideoSort}
+        <if test="excludeCourseId != null">
+            AND course_id != #{excludeCourseId}
+        </if>
     </select>
 </mapper>

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

@@ -305,6 +305,7 @@
         fcpd.period_id,
         fcp.period_name,
         fcpd.id,
+        if(fav.id is null, 0, 1) as isFavorite,
         if(ccut.start_date_time is null, fcpd.start_date_time, ccut.start_date_time) as startDateTime,
         if(ccut.end_date_time is null, fcpd.end_date_time, ccut.end_date_time) as endDateTime,
         course.project as projectId,
@@ -317,6 +318,9 @@
         AND ccut.course_id = fcpd.course_id
         AND ccut.video_id = fcpd.video_id
         AND ccut.company_user_id = #{companyUserId}
+        left join fs_company_user_course_favorite fav
+            on fav.period_id = fcpd.period_id
+            and fav.company_user_id = #{companyUserId}
         where course.is_del = 0 and fcp.del_flag = 0 and fcpd.del_flag = 0
         AND FIND_IN_SET(#{companyId}, fcp.company_id)
         <if test="periodId != null and periodId !='' ">

+ 54 - 30
fs-service/src/main/resources/mapper/course/PublicCourseWatchStatisticsMapper.xml

@@ -23,8 +23,8 @@
 
     <select id="selectCourseDayStatList" resultType="com.fs.course.vo.PublicCourseWatchStatCourseVO">
         SELECT
-            x.stat_date AS statDate,
-            x.course_id AS courseId,
+            m.stat_date AS statDate,
+            m.course_id AS courseId,
             COALESCE(uc.title, uc.course_name) AS courseName,
             pc.cate_name AS rootCateName,
             sc.cate_name AS subCateName,
@@ -42,20 +42,23 @@
                 WHEN IFNULL(m.exposure_pv, 0) &gt; 0 THEN ROUND(100.0 * IFNULL(m.click_pv, 0) / m.exposure_pv, 2)
                 ELSE 0
             END AS clickRate,
-            x.watch_uv AS watchUv,
-            x.finish_uv AS finishUv
+            IFNULL(w.watch_uv, 0) AS watchUv,
+            IFNULL(w.finish_uv, 0) AS finishUv
         FROM (
             SELECT
-                DATE(wl.create_time) AS stat_date,
-                wl.course_id AS course_id,
-                COUNT(DISTINCT wl.user_id) AS watch_uv,
-                COUNT(DISTINCT CASE WHEN wl.log_type = 2 THEN wl.user_id END) AS finish_uv
-            FROM fs_course_watch_log wl
-            INNER JOIN fs_user_course uc ON uc.course_id = wl.course_id
+                DATE(e.create_time) AS stat_date,
+                e.course_id AS course_id,
+                SUM(CASE WHEN e.event_type = 0 THEN 1 ELSE 0 END) AS exposure_pv,
+                COUNT(DISTINCT CASE WHEN e.event_type = 0 THEN e.user_id END) AS exposure_uv,
+                SUM(CASE WHEN e.event_type = 1 THEN 1 ELSE 0 END) AS click_pv,
+                COUNT(DISTINCT CASE WHEN e.event_type = 1 THEN e.user_id END) AS click_uv,
+                MAX(CASE WHEN e.event_type = 0 AND e.exposure_position = 0 THEN 1 ELSE 0 END) AS has_home,
+                MAX(CASE WHEN e.event_type = 0 AND e.exposure_position = 1 THEN 1 ELSE 0 END) AS has_center
+            FROM fs_course_marketing_event e
+            LEFT JOIN fs_user_course uc ON uc.course_id = e.course_id
             <include refid="publicCourseWhereCourse"/>
-            WHERE wl.send_type = 1
-              AND wl.create_time &gt;= #{q.beginDate}
-              AND wl.create_time &lt; DATE_ADD(#{q.endDate}, INTERVAL 1 DAY)
+            WHERE e.create_time &gt;= #{q.beginDate}
+              AND e.create_time &lt; DATE_ADD(#{q.endDate}, INTERVAL 1 DAY)
             <if test="q.keywords != null and q.keywords != ''">
                 AND (
                     uc.title LIKE CONCAT('%', #{q.keywords}, '%')
@@ -68,27 +71,23 @@
             <if test="q.subCateId != null">
                 AND uc.sub_cate_id = #{q.subCateId}
             </if>
-            GROUP BY DATE(wl.create_time), wl.course_id
-        ) x
-        INNER JOIN fs_user_course uc ON uc.course_id = x.course_id
+            GROUP BY DATE(e.create_time), e.course_id
+        ) m
+        LEFT JOIN fs_user_course uc ON uc.course_id = m.course_id
         LEFT JOIN fs_user_course_category pc ON pc.cate_id = uc.cate_id AND IFNULL(pc.is_del, 0) = 0
         LEFT JOIN fs_user_course_category sc ON sc.cate_id = uc.sub_cate_id AND IFNULL(sc.is_del, 0) = 0
         LEFT JOIN (
             SELECT
-                DATE(e.create_time) AS dt,
-                e.course_id AS course_id,
-                SUM(CASE WHEN e.event_type = 0 THEN 1 ELSE 0 END) AS exposure_pv,
-                COUNT(DISTINCT CASE WHEN e.event_type = 0 THEN e.user_id END) AS exposure_uv,
-                SUM(CASE WHEN e.event_type = 1 THEN 1 ELSE 0 END) AS click_pv,
-                COUNT(DISTINCT CASE WHEN e.event_type = 1 THEN e.user_id END) AS click_uv,
-                MAX(CASE WHEN e.event_type = 0 AND e.exposure_position = 0 THEN 1 ELSE 0 END) AS has_home,
-                MAX(CASE WHEN e.event_type = 0 AND e.exposure_position = 1 THEN 1 ELSE 0 END) AS has_center
-            FROM fs_course_marketing_event e
-            WHERE e.create_time &gt;= #{q.beginDate}
-              AND e.create_time &lt; DATE_ADD(#{q.endDate}, INTERVAL 1 DAY)
-            GROUP BY DATE(e.create_time), e.course_id
-        ) m ON m.dt = x.stat_date AND m.course_id = x.course_id
-        ORDER BY x.stat_date DESC, x.course_id DESC
+                DATE(wl.create_time) AS stat_date,
+                wl.course_id AS course_id,
+                COUNT(DISTINCT wl.user_id) AS watch_uv,
+                COUNT(DISTINCT CASE WHEN wl.log_type = 2 THEN wl.user_id END) AS finish_uv
+            FROM fs_course_watch_log wl
+            WHERE wl.create_time &gt;= #{q.beginDate}
+              AND wl.create_time &lt; DATE_ADD(#{q.endDate}, INTERVAL 1 DAY)
+            GROUP BY DATE(wl.create_time), wl.course_id
+        ) w ON w.stat_date = m.stat_date AND w.course_id = m.course_id
+        ORDER BY m.stat_date DESC, m.course_id DESC
     </select>
 
     <select id="selectCatalogStatList" resultType="com.fs.course.vo.PublicCourseWatchStatCatalogVO">
@@ -200,4 +199,29 @@
             sc.cate_name
         ORDER BY v.video_id DESC
     </select>
+
+    <!-- 查询目录下用户学习记录 -->
+    <select id="selectCatalogUserStudyList" resultType="com.fs.course.vo.CatalogUserStudyVO">
+        SELECT
+            wl.user_id AS userId,
+            u.nick_name AS nickName,
+            u.phone AS phone,
+            COALESCE(SUM(CASE WHEN COALESCE(wl.duration, 0) &gt; 0 THEN wl.duration ELSE 0 END), 0) AS totalDuration,
+            MIN(wl.create_time) AS firstWatchTime,
+            MAX(wl.create_time) AS lastWatchTime
+        FROM fs_course_watch_log wl
+        LEFT JOIN fs_user u ON u.user_id = wl.user_id
+        <where>
+            wl.video_id = #{videoId}
+            AND wl.user_id IS NOT NULL
+            <if test="nickName != null and nickName != ''">
+                AND u.nick_name LIKE CONCAT('%', #{nickName}, '%')
+            </if>
+            <if test="phone != null and phone != ''">
+                AND u.phone LIKE CONCAT('%', #{phone}, '%')
+            </if>
+        </where>
+        GROUP BY wl.user_id, u.nick_name, u.phone
+        ORDER BY wl.user_id DESC
+    </select>
 </mapper>

+ 85 - 0
fs-user-app/src/main/java/com/fs/app/controller/course/PublicCourseWatchStatisticsController.java

@@ -0,0 +1,85 @@
+package com.fs.app.controller.course;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.domain.FsCourseMarketingEvent;
+import com.fs.course.domain.FsCourseShareLog;
+import com.fs.course.service.IFsCourseMarketingEventService;
+import com.fs.course.service.IFsCourseShareLogService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 公域看课数据统计(独立 Controller)
+ */
+@Api("公域看课数据统计")
+@RestController
+@RequestMapping("/course/publicCourseWatchStat")
+public class PublicCourseWatchStatisticsController extends AppBaseController {
+
+    @Autowired
+    private IFsCourseMarketingEventService fsCourseMarketingEventService;
+
+    @Autowired
+    private IFsCourseShareLogService fsCourseShareLogService;
+
+    /**
+     * 课程曝光/点击埋点上报
+     */
+//    @Login
+    @ApiOperation("课程曝光/点击埋点上报")
+    @PostMapping("/marketingEvent")
+    public R marketingEvent(@RequestBody FsCourseMarketingEvent param) {
+        String userId = "112";
+        if (StringUtils.isEmpty(userId)) {
+            return R.error("用户未登录");
+        }
+        if (param.getCourseId() == null) {
+            return R.error("课程id不能为空");
+        }
+        if (param.getVideoId() == null) {
+            return R.error("小节id不能为空");
+        }
+        if (param.getEventType() == null) {
+            return R.error("事件类型不能为空");
+        }
+        if (param.getExposurePosition() == null) {
+            return R.error("曝光位置不能为空");
+        }
+        param.setUserId(Long.parseLong(userId));
+        int result = fsCourseMarketingEventService.insertFsCourseMarketingEvent(param);
+        return result > 0 ? R.ok() : R.error();
+    }
+
+    /**
+     * 课程分享记录上报
+     */
+//    @Login
+    @ApiOperation("课程分享记录上报")
+    @PostMapping("/shareLog")
+    public R shareLog(@RequestBody FsCourseShareLog param) {
+        String userId = "112";
+        if (StringUtils.isEmpty(userId)) {
+            return R.error("用户未登录");
+        }
+        if (param.getCourseId() == null) {
+            return R.error("课程id不能为空");
+        }
+        if (param.getVideoId() == null) {
+            return R.error("小节id不能为空");
+        }
+        if (param.getShareType() == null) {
+            return R.error("分享类型不能为空");
+        }
+        if (param.getExposurePosition() == null) {
+            return R.error("曝光位置不能为空");
+        }
+        param.setUserId(Long.parseLong(userId));
+        int result = fsCourseShareLogService.insertFsCourseShareLog(param);
+        return result > 0 ? R.ok() : R.error();
+    }
+}

+ 20 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/CourseCommentScrmController.java

@@ -5,6 +5,7 @@ import com.fs.app.controller.AppBaseController;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.StringUtils;
+import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.domain.FsUserCourseComment;
 import com.fs.course.domain.FsUserCourseCommentLike;
 import com.fs.course.mapper.FsUserCourseMapper;
@@ -15,6 +16,7 @@ import com.fs.course.service.IFsUserCourseCommentLikeService;
 import com.fs.course.service.IFsUserCourseCommentService;
 import com.fs.course.vo.FsUserCourseCommentListUVO;
 import com.fs.course.vo.FsUserCourseCommentReplyListUVO;
+import com.fs.sensitive.ProductionWordFilter;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -45,6 +47,13 @@ public class CourseCommentScrmController extends AppBaseController
 
     @Autowired
     private FsUserCourseMapper fsUserCourseMapper;
+
+    @Autowired
+    private CloudHostProper cloudHostProper;
+
+    @Autowired
+    private ProductionWordFilter productionWordFilter;
+
     /**
      * 查询课堂评论列表
      */
@@ -87,6 +96,17 @@ public class CourseCommentScrmController extends AppBaseController
     @PostMapping("/addComment")
     public R add(@RequestBody FsUserCourseCommentAddParam param)
     {
+        String content = param.getContent();
+        if ("北京卓美".equals(cloudHostProper.getCompanyName())) {
+            if (content == null) {
+                content = "";
+            }
+            String filtered = productionWordFilter.filter(content).getFilteredText();
+            if (StringUtils.isEmpty(filtered)) {
+                return R.error("评论内容无效,请重新输入");
+            }
+            param.setContent(filtered);
+        }
         FsUserCourseComment fsUserCourseComment = new FsUserCourseComment();
         fsUserCourseComment.setUserId(Long.parseLong(getUserId()));
         fsUserCourseComment.setCourseId(param.getCourseId());