1
0

3 Revīzijas 2cdacc636c ... ea7c8d9ef9

Autors SHA1 Ziņojums Datums
  xw ea7c8d9ef9 redis 1 nedēļu atpakaļ
  xw cfc880b939 重粉限制 1 nedēļu atpakaļ
  xw 93bdf74fca app商城首页分页无限且重复 1 nedēļu atpakaļ

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

@@ -6,6 +6,7 @@ import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalTime;
+import java.util.Date;
 import java.util.List;
 
 @Data
@@ -102,6 +103,22 @@ public class CourseConfig implements Serializable {
     // 控制休息提示是否打开 默认打开 0-关闭 1-打开
     private Integer isOpenRestReminder;
 
+    /**
+     * 是否开启按项目自动看课重粉限制(关闭则不限制)
+     */
+    private Boolean projectRepeatLimitEnabled;
+
+    /**
+     * 按项目重粉限制生效时间,此时间之后的看课行为才受限
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date projectRepeatLimitEffectiveTime;
+
+    /**
+     * 先导课是否豁免重粉限制,默认 true
+     */
+    private Boolean pilotCourseSkipRepeatLimit;
+
 
     @Data
     public static class DisabledTimeVo{

+ 33 - 0
fs-service/src/main/java/com/fs/course/service/ICourseProjectSalesBindService.java

@@ -0,0 +1,33 @@
+package com.fs.course.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.param.FsUserCourseVideoAddKfUParam;
+
+/**
+ * 按项目看课销售绑定:isAddKf 入口同步校验与首次绑定
+ */
+public interface ICourseProjectSalesBindService {
+
+    /**
+     * 全局开关开启且已到生效时间
+     */
+    boolean isLimitActive(CourseConfig config);
+
+    /**
+     * 加载 course.config
+     */
+    CourseConfig loadCourseConfig();
+
+    /**
+     * 是否先导课(豁免重粉限制)
+     */
+    boolean isPilotCourse(Long videoId, CourseConfig config);
+
+    /**
+     * 同步校验并在首次进入时写入 fs_user_company_qw。
+     *
+     * @return null 表示通过,否则为拦截响应
+     */
+    R checkAndBind(FsUserCourseVideoAddKfUParam param);
+}

+ 249 - 0
fs-service/src/main/java/com/fs/course/service/impl/CourseProjectSalesBindServiceImpl.java

@@ -0,0 +1,249 @@
+package com.fs.course.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.FsUserCompanyQw;
+import com.fs.course.domain.FsUserCourse;
+import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.mapper.FsUserCompanyQwMapper;
+import com.fs.course.mapper.FsUserCourseMapper;
+import com.fs.course.mapper.FsUserCourseVideoMapper;
+import com.fs.course.param.FsUserCourseVideoAddKfUParam;
+import com.fs.course.service.ICourseProjectSalesBindService;
+import com.fs.course.support.CourseProjectEquivalence;
+import com.fs.course.support.CourseProjectSalesBindConstants;
+import com.fs.enums.ExceptionCodeEnum;
+import com.fs.system.service.ISysConfigService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+@Service
+public class CourseProjectSalesBindServiceImpl implements ICourseProjectSalesBindService {
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private RedisCache redisCache;
+    @Autowired
+    private FsUserCompanyQwMapper fsUserCompanyQwMapper;
+    @Autowired
+    private FsUserCourseVideoMapper fsUserCourseVideoMapper;
+    @Autowired
+    private FsUserCourseMapper fsUserCourseMapper;
+
+    @Override
+    public boolean isLimitActive(CourseConfig config) {
+        if (config == null || !Boolean.TRUE.equals(config.getProjectRepeatLimitEnabled())) {
+            return false;
+        }
+        Date effectiveTime = config.getProjectRepeatLimitEffectiveTime();
+        if (effectiveTime == null) {
+            return false;
+        }
+        return !new Date().before(effectiveTime);
+    }
+
+    @Override
+    public CourseConfig loadCourseConfig() {
+        String json = configService.selectConfigByKey("course.config");
+        if (StringUtils.isEmpty(json)) {
+            return new CourseConfig();
+        }
+        return JSONUtil.toBean(json, CourseConfig.class);
+    }
+
+    @Override
+    public boolean isPilotCourse(Long videoId, CourseConfig config) {
+        if (videoId == null) {
+            return false;
+        }
+        if (config != null && Boolean.FALSE.equals(config.getPilotCourseSkipRepeatLimit())) {
+            return false;
+        }
+        FsUserCourseVideo video = getVideoCached(videoId);
+        if (video == null) {
+            return false;
+        }
+        if (video.getIsFirst() != null && video.getIsFirst() == 1) {
+            return true;
+        }
+        return StringUtils.isNotEmpty(video.getTitle()) && video.getTitle().contains("先导课");
+    }
+
+    @Override
+    public R checkAndBind(FsUserCourseVideoAddKfUParam param) {
+        if (param == null || param.getUserId() == null || param.getCompanyUserId() == null) {
+            return null;
+        }
+
+        CourseConfig config = loadCourseConfig();
+        if (!isLimitActive(config)) {
+            return null;
+        }
+
+        if (isPilotCourse(param.getVideoId(), config)) {
+            log.debug("按项目重粉限制跳过先导课 userId={}, videoId={}", param.getUserId(), param.getVideoId());
+            return null;
+        }
+
+        Long projectId = resolveCourseProjectId(param);
+        if (projectId == null || projectId == 0L) {
+            return null;
+        }
+
+        List<Long> equivalentProjectIds = CourseProjectEquivalence.equivalentProjectIds(projectId);
+        if (CollectionUtils.isEmpty(equivalentProjectIds)) {
+            return null;
+        }
+
+        Long currentCompanyUserId = param.getCompanyUserId();
+        FsUserCompanyQw matchedBind = null;
+
+        for (Long equivalentProjectId : equivalentProjectIds) {
+            FsUserCompanyQw existBind = getBindRecord(param.getUserId(), equivalentProjectId);
+            if (existBind == null || existBind.getCompanyUserId() == null) {
+                continue;
+            }
+            if (!existBind.getCompanyUserId().equals(currentCompanyUserId)) {
+                log.info("按项目重粉限制拦截 userId={}, projectId={}, boundSales={}, currentSales={}, firstBindTime={}",
+                        param.getUserId(), equivalentProjectId, existBind.getCompanyUserId(),
+                        currentCompanyUserId, existBind.getFirstBindTime());
+                return R.error(ExceptionCodeEnum.USER_PROJECT_OTHER_SALES_BOUND.getCode(),
+                        ExceptionCodeEnum.USER_PROJECT_OTHER_SALES_BOUND.getDescription());
+            }
+            matchedBind = existBind;
+        }
+
+        if (matchedBind != null) {
+            for (Long equivalentProjectId : equivalentProjectIds) {
+                cacheBind(param.getUserId(), equivalentProjectId, matchedBind.getCompanyUserId());
+            }
+            return null;
+        }
+
+        return tryFirstBind(param, projectId, currentCompanyUserId);
+    }
+
+    private R tryFirstBind(FsUserCourseVideoAddKfUParam param, Long projectId, Long currentCompanyUserId) {
+        Long qwUserId = parseQwUserId(param.getQwUserId());
+        try {
+            fsUserCompanyQwMapper.insertOrUpdate(
+                    param.getUserId(),
+                    projectId,
+                    qwUserId,
+                    currentCompanyUserId,
+                    param.getCompanyId(),
+                    param.getQwExternalId(),
+                    param.getCourseId(),
+                    param.getVideoId()
+            );
+        } catch (Exception e) {
+            log.error("按项目销售绑定写入失败 userId={}, projectId={}", param.getUserId(), projectId, e);
+            return null;
+        }
+
+        FsUserCompanyQw bind = fsUserCompanyQwMapper.selectByUserAndProject(param.getUserId(), projectId);
+        if (bind != null && bind.getCompanyUserId() != null
+                && !bind.getCompanyUserId().equals(currentCompanyUserId)) {
+            log.info("按项目重粉限制并发拦截 userId={}, projectId={}, boundSales={}, currentSales={}, firstBindTime={}",
+                    param.getUserId(), projectId, bind.getCompanyUserId(), currentCompanyUserId, bind.getFirstBindTime());
+            return R.error(ExceptionCodeEnum.USER_PROJECT_OTHER_SALES_BOUND.getCode(),
+                    ExceptionCodeEnum.USER_PROJECT_OTHER_SALES_BOUND.getDescription());
+        }
+
+        cacheBind(param.getUserId(), projectId, currentCompanyUserId);
+        log.info("按项目销售首次绑定 userId={}, projectId={}, companyUserId={}, firstBindTime={}",
+                param.getUserId(), projectId, currentCompanyUserId,
+                bind != null ? bind.getFirstBindTime() : null);
+        return null;
+    }
+
+    private FsUserCompanyQw getBindRecord(Long userId, Long projectId) {
+        Long cachedCompanyUserId = redisCache.getCacheObject(CourseProjectSalesBindConstants.bindCacheKey(userId, projectId));
+        if (cachedCompanyUserId != null) {
+            FsUserCompanyQw cached = new FsUserCompanyQw();
+            cached.setFsUserId(userId);
+            cached.setProjectId(projectId);
+            cached.setCompanyUserId(cachedCompanyUserId);
+            return cached;
+        }
+
+        FsUserCompanyQw existBind = fsUserCompanyQwMapper.selectByUserAndProject(userId, projectId);
+        if (existBind != null && existBind.getCompanyUserId() != null) {
+            cacheBind(userId, projectId, existBind.getCompanyUserId());
+        }
+        return existBind;
+    }
+
+    private void cacheBind(Long userId, Long projectId, Long companyUserId) {
+        if (userId == null || projectId == null || companyUserId == null) {
+            return;
+        }
+        redisCache.setCacheObject(
+                CourseProjectSalesBindConstants.bindCacheKey(userId, projectId),
+                companyUserId,
+                CourseProjectSalesBindConstants.BIND_CACHE_DAYS,
+                TimeUnit.DAYS
+        );
+    }
+
+    private Long resolveCourseProjectId(FsUserCourseVideoAddKfUParam param) {
+        if (param.getCourseId() != null) {
+            FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(param.getCourseId());
+            if (course != null && course.getProject() != null && course.getProject() != 0L) {
+                return course.getProject();
+            }
+        }
+        if (param.getVideoId() != null) {
+            FsUserCourseVideo video = getVideoCached(param.getVideoId());
+            if (video != null && video.getCourseId() != null) {
+                FsUserCourse course = fsUserCourseMapper.selectFsUserCourseByCourseId(video.getCourseId());
+                if (course != null && course.getProject() != null && course.getProject() != 0L) {
+                    return course.getProject();
+                }
+            }
+        }
+        return 0L;
+    }
+
+    private FsUserCourseVideo getVideoCached(Long videoId) {
+        String cacheKey = CourseProjectSalesBindConstants.videoMetaCacheKey(videoId);
+        FsUserCourseVideo cached = redisCache.getCacheObject(cacheKey);
+        if (cached != null) {
+            return cached;
+        }
+        FsUserCourseVideo video = fsUserCourseVideoMapper.selectFsUserCourseVideoByVideoId(videoId);
+        if (video != null) {
+            FsUserCourseVideo meta = new FsUserCourseVideo();
+            meta.setVideoId(video.getVideoId());
+            meta.setCourseId(video.getCourseId());
+            meta.setTitle(video.getTitle());
+            meta.setIsFirst(video.getIsFirst());
+            redisCache.setCacheObject(cacheKey, meta,
+                    CourseProjectSalesBindConstants.VIDEO_META_CACHE_HOURS, TimeUnit.HOURS);
+            return meta;
+        }
+        return null;
+    }
+
+    private Long parseQwUserId(String qwUserId) {
+        if (StringUtils.isEmpty(qwUserId)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(qwUserId);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+}

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

@@ -46,6 +46,7 @@ import com.fs.course.param.newfs.UserCourseVideoPageParam;
 import com.fs.course.service.IFsCourseLinkService;
 import com.fs.course.service.IFsUserCompanyBindService;
 import com.fs.course.service.IFsUserCompanyUserService;
+import com.fs.course.service.ICourseProjectSalesBindService;
 import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.utils.H5WxUserWatchRedisUtil;
 import com.fs.course.param.newfs.*;
@@ -266,6 +267,9 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Autowired
     private CourseRepeatByProjectMqProducer courseRepeatByProjectMqProducer;
 
+    @Autowired
+    private ICourseProjectSalesBindService courseProjectSalesBindService;
+
     @Autowired
     private SysDictDataMapper sysDictDataMapper;
 
@@ -820,6 +824,12 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         String noRegisterMsg = "由于您还未完成注册,请联系伴学助手完成注册即可观看!";
         //非独属链接提示
         String noMemberMsg = "此链接已被绑定,请联系伴学助手领取您的专属链接,专属链接请勿分享哦!";
+
+        R bindCheck = courseProjectSalesBindService.checkAndBind(param);
+        if (bindCheck != null) {
+            return bindCheck;
+        }
+
         try {
             courseRepeatByProjectMqProducer.submitCheck(param);
         } catch (Exception e) {

+ 24 - 11
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoFavoriteServiceImpl.java

@@ -1,11 +1,13 @@
 package com.fs.course.service.impl;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import lombok.Synchronized;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import com.fs.course.mapper.FsUserVideoFavoriteMapper;
 import com.fs.course.domain.FsUserVideoFavorite;
@@ -23,6 +25,8 @@ public class FsUserVideoFavoriteServiceImpl implements IFsUserVideoFavoriteServi
 {
     @Autowired
     private FsUserVideoFavoriteMapper fsUserVideoFavoriteMapper;
+    @Autowired(required = false)
+    private RedisTemplate<String, Boolean> redisTemplate;
 
     /**
      * 查询课堂视频收藏
@@ -97,30 +101,39 @@ public class FsUserVideoFavoriteServiceImpl implements IFsUserVideoFavoriteServi
     {
         return fsUserVideoFavoriteMapper.deleteFsUserVideoFavoriteByFavoriteId(favoriteId);
     }
+    private static final String FAVORITE_KEY_PREFIX = "favorite:video:";
+    private static final String NO_FAVORITE_KEY_PREFIX = "nofavorite:video:";
+
     @Override
     public R checkFavorite(Long videoId, long userId) {
-        return fsUserVideoFavoriteMapper.checkFavorite(videoId, userId) > 0
-                ? R.ok().put("isFavorite", 1)
-                : R.error().put("isFavorite", 0);
+        String key = FAVORITE_KEY_PREFIX + videoId + ":user:" + userId;
+        Boolean hasFavorite = redisTemplate.opsForValue().get(key);
+        if (hasFavorite != null && hasFavorite) {
+            return R.ok().put("isFavorite", 1);
+        } else {
+            return fsUserVideoFavoriteMapper.checkFavorite(videoId, userId) > 0
+                    ? R.ok().put("isFavorite", 1)
+                    : R.error().put("isFavorite", 0);
+        }
     }
 
     @Override
     @Transactional
     @Synchronized
     public void favoriteVideo(Long videoId, long userId) {
-        if (fsUserVideoFavoriteMapper.checkFavorite(videoId, userId) == 0) {
-            FsUserVideoFavorite favorite = new FsUserVideoFavorite();
-            favorite.setVideoId(videoId);
-            favorite.setUserId(userId);
-            favorite.setCreateTime(DateUtils.getNowDate());
-            fsUserVideoFavoriteMapper.insertFsUserVideoFavorite(favorite);
-        }
+        String unlikeKey = NO_FAVORITE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.delete(unlikeKey);
+        String key = FAVORITE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.opsForValue().set(key, true, 1, TimeUnit.DAYS);
     }
 
     @Override
     @Transactional
     @Synchronized
     public void deleteFavorite(Long videoId, long userId) {
-        fsUserVideoFavoriteMapper.deleteFavorite(videoId, userId);
+        String key = FAVORITE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.delete(key);
+        String unlikeKey = NO_FAVORITE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.opsForValue().set(unlikeKey, true, 1, TimeUnit.DAYS);
     }
 }

+ 25 - 11
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoLikeServiceImpl.java

@@ -1,11 +1,13 @@
 package com.fs.course.service.impl;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import lombok.Synchronized;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import com.fs.course.mapper.FsUserVideoLikeMapper;
 import com.fs.course.domain.FsUserVideoLike;
@@ -24,6 +26,9 @@ public class FsUserVideoLikeServiceImpl implements IFsUserVideoLikeService
     @Autowired
     private FsUserVideoLikeMapper fsUserVideoLikeMapper;
 
+    @Autowired
+    private RedisTemplate<String, Boolean> redisTemplate;
+
     /**
      * 查询课堂视频点赞
      *
@@ -98,30 +103,39 @@ public class FsUserVideoLikeServiceImpl implements IFsUserVideoLikeService
         return fsUserVideoLikeMapper.deleteFsUserVideoLikeByLikeId(likeId);
     }
 
+    private static final String LIKE_KEY_PREFIX = "like:video:";
+    private static final String UNLIKE_KEY_PREFIX = "unlike:video:";
+
     @Override
     public R checkLike(Long videoId, long userId) {
-        return fsUserVideoLikeMapper.checkLike(videoId, userId) > 0
-                ? R.ok().put("isLike", 1)
-                : R.error().put("isLike", 0);
+        String key = LIKE_KEY_PREFIX + videoId + ":user:" + userId;
+        Boolean hasLiked = redisTemplate.opsForValue().get(key);
+        if (hasLiked != null && hasLiked) {
+            return R.ok().put("isLike", 1);
+        } else {
+            return fsUserVideoLikeMapper.checkLike(videoId, userId) > 0
+                    ? R.ok().put("isLike", 1)
+                    : R.error().put("isLike", 0);
+        }
     }
 
     @Override
     @Transactional
     @Synchronized
     public void likeVideo(Long videoId, long userId) {
-        if (fsUserVideoLikeMapper.checkLike(videoId, userId) == 0) {
-            FsUserVideoLike like = new FsUserVideoLike();
-            like.setVideoId(videoId);
-            like.setUserId(userId);
-            like.setCreateTime(DateUtils.getNowDate());
-            fsUserVideoLikeMapper.insertFsUserVideoLike(like);
-        }
+        String unlikeKey = UNLIKE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.delete(unlikeKey);
+        String key = LIKE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.opsForValue().set(key, true, 1, TimeUnit.DAYS);
     }
 
     @Override
     @Transactional
     @Synchronized
     public void unlikeVideo(Long videoId, long userId) {
-        fsUserVideoLikeMapper.deleteLike(videoId, userId);
+        String key = LIKE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.delete(key);
+        String unlikeKey = UNLIKE_KEY_PREFIX + videoId + ":user:" + userId;
+        redisTemplate.opsForValue().set(unlikeKey, true, 1, TimeUnit.DAYS);
     }
 }

+ 54 - 7
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoServiceImpl.java

@@ -65,6 +65,9 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
 
     @Autowired
     private IVodService vodService;
+    private static final String LIKE_KEY_PREFIX = "like:video:";
+    private static final String FAVORITE_KEY_PREFIX = "favorite:video:";
+
     /**
      * 查询课堂视频
      *
@@ -202,18 +205,62 @@ public class FsUserVideoServiceImpl implements IFsUserVideoService {
         return list;
     }
 
-    // 查询点赞、收藏、评论数(仅数据库,便于本地联调线上库)
+    //查询点赞信息和收藏信息
     public List<FsUserVideoListUVO> selectLikesAndFavorites(Long userId, List<FsUserVideoListUVO> list) {
         if (list != null && !list.isEmpty()) {
             List<Long> videoIds = list.stream().map(vo -> Long.parseLong(vo.getId())).collect(Collectors.toList());
-            Map<Long, VideoLikeStatusDTO> likeMap = fsUserVideoLikeMapper.checkLikes(videoIds, userId);
-            Map<Long, VideoFavoriteStatusDTO> favoriteMap = fsUserVideoFavoriteMapper.checkFavorites(videoIds, userId);
-            for (FsUserVideoListUVO vo : list) {
+
+            List<String> likeKeys = videoIds.stream()
+                    .map(videoId -> LIKE_KEY_PREFIX + videoId + ":user:" + userId)
+                    .collect(Collectors.toList());
+            List<Boolean> redisLikes = redisTemplate.opsForValue().multiGet(likeKeys);
+
+            List<String> favoriteKeys = videoIds.stream()
+                    .map(videoId -> FAVORITE_KEY_PREFIX + videoId + ":user:" + userId)
+                    .collect(Collectors.toList());
+            List<Boolean> redisFavorites = redisTemplate.opsForValue().multiGet(favoriteKeys);
+
+            List<Long> missingLikeVideoIds = new ArrayList<>();
+            List<Long> missingFavoriteVideoIds = new ArrayList<>();
+
+            for (int i = 0; i < list.size(); i++) {
+                FsUserVideoListUVO vo = list.get(i);
                 Long videoId = Long.parseLong(vo.getId());
-                vo.setSmsNum(fsUserVideoCommentMapper.selectCommentCountByVideos(videoId));
-                vo.setLike(likeMap.getOrDefault(videoId, new VideoLikeStatusDTO()).getLiked() ? 1 : 0);
-                vo.setFavorite(favoriteMap.getOrDefault(videoId, new VideoFavoriteStatusDTO()).getFavorite() ? 1 : 0);
+                Integer commentCount = fsUserVideoCommentMapper.selectCommentCountByVideos(videoId);
+                vo.setSmsNum(commentCount);
+                if (redisLikes != null && redisLikes.get(i) != null) {
+                    vo.setLike(Boolean.TRUE.equals(redisLikes.get(i)) ? 1 : 0);
+                } else {
+                    missingLikeVideoIds.add(videoId);
+                }
+
+                if (redisFavorites != null && redisFavorites.get(i) != null) {
+                    vo.setFavorite(Boolean.TRUE.equals(redisFavorites.get(i)) ? 1 : 0);
+                } else {
+                    missingFavoriteVideoIds.add(videoId);
+                }
             }
+
+            if (!missingLikeVideoIds.isEmpty()) {
+                Map<Long, VideoLikeStatusDTO> likeMap = fsUserVideoLikeMapper.checkLikes(missingLikeVideoIds, userId);
+                for (FsUserVideoListUVO vo : list) {
+                    Long videoId = Long.parseLong(vo.getId());
+                    if (missingLikeVideoIds.contains(videoId)) {
+                        vo.setLike(likeMap.getOrDefault(videoId, new VideoLikeStatusDTO()).getLiked() ? 1 : 0);
+                    }
+                }
+            }
+
+            if (!missingFavoriteVideoIds.isEmpty()) {
+                Map<Long, VideoFavoriteStatusDTO> favoriteMap = fsUserVideoFavoriteMapper.checkFavorites(missingFavoriteVideoIds, userId);
+                for (FsUserVideoListUVO vo : list) {
+                    Long videoId = Long.parseLong(vo.getId());
+                    if (missingFavoriteVideoIds.contains(videoId)) {
+                        vo.setFavorite(favoriteMap.getOrDefault(videoId, new VideoFavoriteStatusDTO()).getFavorite() ? 1 : 0);
+                    }
+                }
+            }
+
             fillTalentFollowInfo(userId, list);
         }
         return list;

+ 30 - 0
fs-service/src/main/java/com/fs/course/support/CourseProjectSalesBindConstants.java

@@ -0,0 +1,30 @@
+package com.fs.course.support;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 按项目看课销售绑定 Redis 缓存常量
+ */
+public final class CourseProjectSalesBindConstants {
+
+    /** 用户-项目绑定销售缓存:course:project:sales:bind:{userId}:{projectId} -> companyUserId */
+    public static final String BIND_CACHE_KEY_PREFIX = "course:project:sales:bind:";
+
+    /** 视频元数据缓存:course:project:sales:video:{videoId} */
+    public static final String VIDEO_META_CACHE_KEY_PREFIX = "course:project:sales:video:";
+
+    public static final int BIND_CACHE_DAYS = 7;
+
+    public static final int VIDEO_META_CACHE_HOURS = 1;
+
+    private CourseProjectSalesBindConstants() {
+    }
+
+    public static String bindCacheKey(Long userId, Long projectId) {
+        return BIND_CACHE_KEY_PREFIX + userId + ":" + projectId;
+    }
+
+    public static String videoMetaCacheKey(Long videoId) {
+        return VIDEO_META_CACHE_KEY_PREFIX + videoId;
+    }
+}

+ 1 - 0
fs-service/src/main/java/com/fs/enums/ExceptionCodeEnum.java

@@ -26,6 +26,7 @@ public enum ExceptionCodeEnum {
     WATCH_LATEST_COURSE(482, "请观看最新的课程项目"),
     EXCEED_COURSE_LIMIT(483, "超过项目看课数量限制"),
     ALREADY_WATCHED_OTHER_SALES_COURSE(484, "已看过其他销售分享的此课程,不能重复观看"),
+    USER_PROJECT_OTHER_SALES_BOUND(485, "您已在其他伴学助手处观看过该项目课程,请联系原助手继续学习"),
 
     // ============ 参数相关错误 (500-519) ============
     PARAM_ERROR(501, "参数错误!"),

+ 4 - 4
fs-user-app/src/main/java/com/fs/app/controller/store/ProductScrmController.java

@@ -113,7 +113,7 @@ public class ProductScrmController extends AppBaseController {
     @ApiOperation("获取商品列表")
     @GetMapping("/getProducts")
     public R getProducts(FsStoreProductQueryParam param, HttpServletRequest request){
-        PageHelper.startPage(param.getPage(), param.getPageSize());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
         param.setIsDisplay(1);
         List<FsStoreProductListQueryVO> productList=productService.selectFsStoreProductListQuery(param);
         PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(productList);
@@ -354,7 +354,7 @@ public class ProductScrmController extends AppBaseController {
     @ApiOperation("获取推荐商品数据")
     @GetMapping("/getTuiProducts")
     public R getTuiProducts(BaseQueryParam param, HttpServletRequest request){
-        PageHelper.startPage(param.getPage(), param.getPageSize());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsStoreProductListQueryVO> list=productService.selectFsStoreProductTuiListQuery(param);
         PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(list);
         return R.ok().put("data",listPageInfo);
@@ -362,7 +362,7 @@ public class ProductScrmController extends AppBaseController {
     @ApiOperation("获取喜欢商品数据")
     @GetMapping("/getGoodsProducts")
     public R getGoodsProducts(BaseQueryParam param, HttpServletRequest request){
-        PageHelper.startPage(param.getPage(), param.getPageSize());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsStoreProductListQueryVO> list=productService.selectFsStoreProductGoodListQuery(param);
         PageInfo<FsStoreProductListQueryVO> listPageInfo=new PageInfo<>(list);
         return R.ok().put("data",listPageInfo);
@@ -372,7 +372,7 @@ public class ProductScrmController extends AppBaseController {
     @ApiOperation("获取推广商品列表")
     @GetMapping("/getStoreProductAttrValueList")
     public R getStoreProductAttrValueList(FsStoreProductAttrValueQueryParam param, HttpServletRequest request){
-        PageHelper.startPage(param.getPage(), param.getPageSize());
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsStoreProductAttrValueQueryVO> productList=attrValueService.selectStoreProductAttrValueListQuery(param);
         PageInfo<FsStoreProductAttrValueQueryVO> listPageInfo=new PageInfo<>(productList);
         return R.ok().put("data",listPageInfo);