Parcourir la source

直播代码优化

yuhongqi il y a 1 mois
Parent
commit
0f13a13d3d

+ 13 - 13
fs-live-socket/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -750,32 +750,32 @@ public class WebSocketServer {
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
         List<Session> adminRoom = getAdminRoom(liveId);
 
-        // 使用Set去重,避免同一Session被多次处理
-        Set<Session> uniqueSessions = new HashSet<>();
+
         
         // 收集普通用户房间的Session(去重)
         room.forEach((k, v) -> {
             if (v != null && v.isOpen()) {
-                uniqueSessions.add(v);
+                try {
+                    sendMessage(v, message);
+                } catch (Exception e) {
+                    // 单个Session发送失败不影响其他Session
+                    log.error(e.getMessage());
+                }
             }
         });
         
         // 收集admin房间的Session(去重)
         adminRoom.forEach(v -> {
             if (v != null && v.isOpen()) {
-                uniqueSessions.add(v);
+                try {
+                    sendMessage(v, message);
+                } catch (Exception e) {
+                    // 单个Session发送失败不影响其他Session
+                    log.error(e.getMessage());
+                }
             }
         });
 
-        // 并行发送给所有唯一Session
-        uniqueSessions.parallelStream().forEach(session -> {
-            try {
-                sendWithRetry(session, message, 1);
-            } catch (Exception e) {
-                // 单个Session发送失败不影响其他Session
-                log.error(e.getMessage());
-            }
-        });
     }
 
     /**

+ 88 - 1
fs-service-system/src/main/java/com/fs/live/service/impl/LiveGoodsServiceImpl.java

@@ -4,6 +4,8 @@ package com.fs.live.service.impl;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysUser;
 import com.fs.common.core.redis.service.StockDeductService;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.constant.RedisConstant;
 import com.fs.common.utils.DateUtils;
 import com.fs.company.domain.CompanyUser;
 import com.fs.live.domain.LiveGoods;
@@ -19,6 +21,7 @@ import com.fs.live.domain.LiveAutoTask;
 import com.fs.live.service.ILiveAutoTaskService;
 import cn.hutool.json.JSONUtil;
 import cn.hutool.json.JSONObject;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -32,6 +35,7 @@ import java.util.stream.Collectors;
  * @author fs
  * @date 2025-07-08
  */
+@Slf4j
 @Service
 public class LiveGoodsServiceImpl  implements ILiveGoodsService {
 
@@ -47,6 +51,9 @@ public class LiveGoodsServiceImpl  implements ILiveGoodsService {
     @Autowired
     private StockDeductService stockDeductService;
 
+    @Autowired
+    private RedisCache redisCache;
+
 
     /**
      * 查询直播商品
@@ -194,7 +201,87 @@ public class LiveGoodsServiceImpl  implements ILiveGoodsService {
 
     @Override
     public List<LiveGoodsVo> selectProductListByLiveId(LiveGoods liveGoods) {
-        return baseMapper.selectProductListByLiveIdAll(liveGoods);
+        List<LiveGoodsVo> list = baseMapper.selectProductListByLiveIdAll(liveGoods);
+        
+        // 从缓存获取库存,如果缓存中没有则初始化
+        if (list != null && !list.isEmpty() && liveGoods.getLiveId() != null) {
+            Long liveId = liveGoods.getLiveId();
+            for (LiveGoodsVo goodsVo : list) {
+                if (goodsVo.getProductId() != null) {
+                    // 构建 Redis key
+                    String stockKey = RedisConstant.STOCK_KEY_PREFIX + liveId + ":" + goodsVo.getProductId();
+                    
+                    // 从 Redis 获取库存
+                    Object cachedStock = redisCache.getCacheObject(stockKey);
+                    
+                    if (cachedStock != null) {
+                        // 缓存中有库存,使用缓存值
+                        try {
+                            Integer stock = null;
+                            if (cachedStock instanceof Integer) {
+                                stock = (Integer) cachedStock;
+                            } else if (cachedStock instanceof Long) {
+                                stock = ((Long) cachedStock).intValue();
+                            } else if (cachedStock instanceof String) {
+                                stock = Integer.parseInt((String) cachedStock);
+                            } else {
+                                stock = Integer.parseInt(cachedStock.toString());
+                            }
+                            goodsVo.setStock(stock);
+                            log.debug("从缓存获取库存,liveId: {}, productId: {}, stock: {}", liveId, goodsVo.getProductId(), stock);
+                        } catch (Exception e) {
+                            log.warn("解析缓存库存失败,liveId: {}, productId: {}, cachedStock: {}, error: {}", 
+                                    liveId, goodsVo.getProductId(), cachedStock, e.getMessage());
+                            // 解析失败,使用数据库库存并初始化到缓存
+                            initStockFromDatabase(goodsVo, liveId);
+                        }
+                    } else {
+                        // 缓存中没有库存,从数据库获取并初始化到缓存
+                        initStockFromDatabase(goodsVo, liveId);
+                    }
+                }
+            }
+        }
+        
+        return list;
+    }
+
+    /**
+     * 从数据库获取库存并初始化到 Redis 缓存(永久保存)
+     * @param goodsVo 商品VO
+     * @param liveId 直播间ID
+     */
+    private void initStockFromDatabase(LiveGoodsVo goodsVo, Long liveId) {
+        try {
+            // 从数据库获取库存(goodsVo 中的 stock 字段来自数据库查询 selectProductListByLiveIdAll)
+            // SQL 查询中:lg.stock 来自 live_goods 表
+            Integer dbStock = goodsVo.getStock();
+            if (dbStock == null) {
+                // 如果 VO 中没有库存,尝试从数据库查询 live_goods 表
+                LiveGoods liveGoods = baseMapper.selectLiveGoodsByGoodsId(goodsVo.getGoodsId());
+                if (liveGoods != null && liveGoods.getStock() != null) {
+                    dbStock = liveGoods.getStock().intValue();
+                } else {
+                    // 如果还是没有,使用默认值 0
+                    dbStock = 0;
+                    log.warn("未找到商品库存,使用默认值 0,liveId: {}, productId: {}, goodsId: {}", 
+                            liveId, goodsVo.getProductId(), goodsVo.getGoodsId());
+                }
+            }
+            
+            // 初始化库存到 Redis(永久保存,不设置过期时间)
+            // StockDeductService.initStock 方法会永久保存到 Redis
+            stockDeductService.initStock(goodsVo.getProductId(), liveId, dbStock);
+            
+            // 更新 VO 中的库存值(确保返回给前端的是缓存中的值)
+            goodsVo.setStock(dbStock);
+            
+            log.info("初始化库存到缓存,liveId: {}, productId: {}, stock: {}", liveId, goodsVo.getProductId(), dbStock);
+        } catch (Exception e) {
+            log.error("初始化库存到缓存失败,liveId: {}, productId: {}, error: {}", 
+                    liveId, goodsVo.getProductId(), e.getMessage(), e);
+            // 初始化失败,使用数据库库存值(不更新缓存)
+        }
     }
 
     @Override

+ 4 - 0
fs-service-system/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -3167,8 +3167,12 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             FsStoreProduct fsStoreProduct = fsStoreProductService.selectFsStoreProductById(liveOrder.getProductId());
             LiveGoods goods = liveGoodsMapper.selectLiveGoodsByProductId(liveOrder.getLiveId(), liveOrder.getProductId());
             fsStoreProduct.setStock(fsStoreProduct.getStock() + Integer.parseInt(liveOrder.getTotalNum()));
+            long sales = fsStoreProduct.getSales() - Integer.parseInt(liveOrder.getTotalNum());
+            fsStoreProduct.setSales(sales >= 0 ? sales :0);
             fsStoreProductService.updateFsStoreProduct(fsStoreProduct);
             goods.setStock(goods.getStock() + Long.parseLong(liveOrder.getTotalNum()));
+            int sales2 = Math.toIntExact(goods.getSales() - Long.parseLong(liveOrder.getTotalNum()));
+            goods.setSales(Math.max(sales2, 0));
             liveGoodsMapper.updateLiveGoods(goods);
 
 

+ 1 - 1
fs-service-system/src/main/java/com/fs/store/mapper/FsStoreProductAttrValueMapper.java

@@ -83,7 +83,7 @@ public interface FsStoreProductAttrValueMapper
     @Update("update fs_store_product_attr_value set stock=stock-#{num},sales=sales+#{num}" +
             " where id=#{productAttrValueId} and stock >= #{num} ")
     int decProductAttrStock(@Param("productAttrValueId")Long productAttrValueId, @Param("num") Integer cartNum);
-    @Update("update fs_store_product_attr_value set stock=stock+#{num}, sales=sales-#{num}" +
+    @Update("update fs_store_product_attr_value set stock=stock+#{num},  sales = IF(sales > #{num}, sales-#{num}, 0)" +
             " where product_id=#{productId} and id=#{productAttrValueId}")
     int incProductAttrStock(@Param("num")Long num,@Param("productId") Long productId,@Param("productAttrValueId") Long productAttrValueId);
     @Select({"<script> " +

+ 1 - 1
fs-service-system/src/main/java/com/fs/store/mapper/FsStoreProductMapper.java

@@ -163,7 +163,7 @@ public interface FsStoreProductMapper
     @Update("update fs_store_product set stock=stock-#{num}, sales=sales+#{num}" +
             " where product_id=#{productId} and stock >= #{num}")
     int decProductAttrStock(@Param("productId")Long productId, @Param("num")Integer cartNum);
-    @Update("update fs_store_product set stock=stock+#{num}, sales=sales-#{num}" +
+    @Update("update fs_store_product set stock=stock+#{num}, sales = IF(sales > #{num}, sales-#{num}, 0)" +
             " where product_id=#{productId}")
     int incStockDecSales( @Param("num")Long num, @Param("productId")Long productId);
     @Select("select * from fs_store_product where is_del=0 and is_show=1 and  is_new=1 and is_display=1 order by sort desc, product_id DESC limit #{count}")

+ 136 - 111
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -79,6 +79,83 @@ public class LiveCompletionPointsController extends AppBaseController {
         }
     }
 
+    /**
+     * 获取直播间信息并验证(公共方法)
+     * @param liveId 直播间ID
+     * @return Live对象,如果不存在或未开启奖励则返回null
+     */
+    private Live getLiveAndValidate(Long liveId) {
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            return null;
+        }
+        if (!checkWatchRewardEnabled(live)) {
+            return null;
+        }
+        return live;
+    }
+
+    /**
+     * 解析直播间配置并获取看课时长(公共方法)
+     * @param live 直播间信息
+     * @return 看课时长(秒),如果未配置则返回null
+     */
+    private Long parseRequiredWatchDuration(Live live) {
+        if (live == null) {
+            return null;
+        }
+        String configJson = live.getConfigJson();
+        if (configJson == null || configJson.isEmpty()) {
+            return null;
+        }
+        try {
+            LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
+            if (config != null && config.getWatchDuration() != null) {
+                // watchDuration是分钟,转换为秒
+                return config.getWatchDuration() * 60;
+            }
+        } catch (Exception e) {
+            logger.warn("解析直播间配置失败: {}", e.getMessage());
+        }
+        return null;
+    }
+
+    /**
+     * 获取或创建完课记录(公共方法)
+     * @param liveId 直播间ID
+     * @param userId 用户ID
+     * @return 完课记录
+     */
+    private LiveCompletionPointsRecord getOrCreateRecord(Long liveId, Long userId) {
+        LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+        if (record == null) {
+            record = completionPointsRecordService.createCompletionRecord(liveId, userId);
+        }
+        return record;
+    }
+
+    /**
+     * 计算完课比例(公共方法)
+     * @param watchDuration 观看时长(秒)
+     * @param targetDuration 目标时长(秒)
+     * @return 完课比例(0-100)
+     */
+    private BigDecimal calculateCompletionRate(Long watchDuration, Long targetDuration) {
+        if (targetDuration == null || targetDuration <= 0) {
+            return BigDecimal.ZERO;
+        }
+        BigDecimal rate = BigDecimal.valueOf(watchDuration != null ? watchDuration : 0L)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(BigDecimal.valueOf(targetDuration), 2, java.math.RoundingMode.HALF_UP);
+        if (rate.compareTo(BigDecimal.valueOf(100)) > 0) {
+            rate = BigDecimal.valueOf(100);
+        }
+        if (rate.compareTo(BigDecimal.ZERO) < 0) {
+            rate = BigDecimal.ZERO;
+        }
+        return rate;
+    }
+
     /**
      * 领取完课积分
      */
@@ -170,40 +247,19 @@ public class LiveCompletionPointsController extends AppBaseController {
         Long userId = Long.parseLong(getUserId());
 
         try {
-            // 1. 获取直播间信息
-            Live live = liveService.selectLiveByLiveId(liveId);
+            // 1. 获取直播间信息并验证(复用公共方法)
+            Live live = getLiveAndValidate(liveId);
             if (live == null) {
-                return R.error("直播间不存在");
-            }
-            // 2. 检查直播间是否开启观看积分奖励
-            if (!checkWatchRewardEnabled(live)) {
-                return R.error(406, "直播间未开启直播观看奖励");
+                return R.error(406, "直播间不存在或未开启直播观看奖励");
             }
 
-            // 2. 查询当前用户和当前直播间的最近一次完课记录(不限制日期
-            LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
+            // 2. 获取或创建完课记录(复用公共方法)
+            LiveCompletionPointsRecord record = getOrCreateRecord(liveId, userId);
 
-            // 3. 如果没有记录,查询直播间配置并生成记录
-            if (record == null) {
-                record = completionPointsRecordService.createCompletionRecord(liveId, userId);
-            }
+            // 3. 获取看课时长配置(复用公共方法)
+            Long requiredWatchDuration = parseRequiredWatchDuration(live);
 
-            // 4. 获取看课时长配置(从直播间配置1中获取)
-            Long requiredWatchDuration = null;
-            String configJson = live.getConfigJson();
-            if (configJson != null && !configJson.isEmpty()) {
-                try {
-                    LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-                    if (config != null && config.getWatchDuration() != null) {
-                        // watchDuration是分钟,转换为秒
-                        requiredWatchDuration = config.getWatchDuration() * 60;
-                    }
-                } catch (Exception e) {
-                    logger.warn("解析直播间配置失败: {}", e.getMessage());
-                }
-            }
-
-            // 5. 计算剩余时长
+            // 4. 计算剩余时长
             RemainingTimeVO vo = new RemainingTimeVO();
             Long videoDuration = live.getDuration() != null ? live.getDuration() : 0L;
             Long watchDuration = record != null && record.getWatchDuration() != null
@@ -216,19 +272,11 @@ public class LiveCompletionPointsController extends AppBaseController {
             Long targetDuration = requiredWatchDuration != null ? requiredWatchDuration : videoDuration;
             vo.setRemainingTime(Math.max(0, targetDuration - watchDuration));
             
-            // 使用RemainingTime和videoDuration计算完课比例
-            // 先计算基于videoDuration的剩余时长
-            if (videoDuration > 0) {
-                BigDecimal completionRate = BigDecimal.valueOf(targetDuration)
-                        .multiply(BigDecimal.valueOf(100))
-                        .divide(BigDecimal.valueOf(videoDuration), 2, java.math.RoundingMode.HALF_UP);
-                if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
-                    completionRate = BigDecimal.valueOf(100);
-                }
-                if (completionRate.compareTo(BigDecimal.ZERO) < 0) {
-                    completionRate = BigDecimal.ZERO;
-                }
-                vo.setCompletionRate(completionRate);
+            // 计算完课比例(复用公共方法)
+            if (videoDuration > 0 && watchDuration > 0) {
+                vo.setCompletionRate(calculateCompletionRate(watchDuration, targetDuration));
+            } else {
+                vo.setCompletionRate(BigDecimal.ZERO);
             }
             vo.setHasReceived(record != null && record.getReceiveStatus() != null && record.getReceiveStatus() == 1);
 
@@ -249,18 +297,13 @@ public class LiveCompletionPointsController extends AppBaseController {
         Long userId = Long.parseLong(getUserId());
 
         try {
-            // 1. 获取直播间信息
-            Live live = liveService.selectLiveByLiveId(liveId);
+            // 1. 获取直播间信息并验证(复用公共方法)
+            Live live = getLiveAndValidate(liveId);
             if (live == null) {
-                return R.error("直播间不存在");
-            }
-
-            // 2. 检查直播间是否开启观看积分奖励
-            if (!checkWatchRewardEnabled(live)) {
-                return R.error(406, "直播间未开启直播观看奖励");
+                return R.error(406, "直播间不存在或未开启直播观看奖励");
             }
 
-            // 3. 判断当前时间是否在直播期间(状态为2,直播中)
+            // 2. 判断当前时间是否在直播期间(状态为2,直播中)
             boolean isLiveInProgress = false;
             LocalDateTime now = LocalDateTime.now();
 
@@ -277,10 +320,10 @@ public class LiveCompletionPointsController extends AppBaseController {
                 return R.error("当前不在直播期间,无法更新看课时长");
             }
 
-            // 4. 查询当前直播间的完课记录(不限制日期)
+            // 3. 查询当前直播间的完课记录(不限制日期)
             LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
 
-            // 5. 计算看课时长
+            // 4. 计算看课时长
             Date updateTime = null;
             if (record != null && record.getUpdateTime() != null) {
                 updateTime = record.getUpdateTime();
@@ -306,7 +349,7 @@ public class LiveCompletionPointsController extends AppBaseController {
                 timeDiff = (currentTime.getTime() - startTime.getTime()) / 1000; // 转换为秒
             }
 
-            // 6. 如果请求传入的时间大于这个时间差,就使用计算出的看课时长,否则使用请求传入的时长
+            // 5. 如果请求传入的时间大于这个时间差,就使用计算出的看课时长,否则使用请求传入的时长
             Long finalWatchDuration;
             if (watchDuration > timeDiff) {
                 // 请求传入的时间大于时间差,使用计算出的看课时长
@@ -316,55 +359,53 @@ public class LiveCompletionPointsController extends AppBaseController {
                 finalWatchDuration = watchDuration;
             }
 
-            // 7. 更新完课记录中的看课时长
+            // 6. 更新完课记录中的看课时长(只更新变化的字段)
             if (record == null) {
                 // 如果没有记录,先创建记录
                 record = completionPointsRecordService.createCompletionRecord(liveId, userId);
                 if (record == null) {
                     return R.error(406, "直播间未开启直播观看奖励");
                 }
+                // 只更新 watchDuration 字段
+                LiveCompletionPointsRecord updateRecord = new LiveCompletionPointsRecord();
+                updateRecord.setId(record.getId());
+                updateRecord.setWatchDuration(finalWatchDuration);
+                completionPointsRecordMapper.updateRecord(updateRecord);
                 record.setWatchDuration(finalWatchDuration);
-                completionPointsRecordMapper.updateRecord(record);
             } else {
-                // 更新现有记录的看课时长
+                // 更新现有记录的看课时长(只更新变化的字段)
                 Long currentWatchDuration = record.getWatchDuration() != null
                         ? record.getWatchDuration() : 0L;
-                record.setWatchDuration(currentWatchDuration + finalWatchDuration);
+                Long newWatchDuration = currentWatchDuration + finalWatchDuration;
 
-                // 重新计算完课比例(基于看课时长配置)
+                // 获取看课时长配置(复用公共方法)
+                Long requiredWatchDuration = parseRequiredWatchDuration(live);
                 Long videoDuration = live.getDuration();
-                String configJson = live.getConfigJson();
-                Long requiredWatchDuration = null;
-                
-                // 从直播间配置获取看课时长
-                if (configJson != null && !configJson.isEmpty()) {
-                    try {
-                        LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-                        if (config != null && config.getWatchDuration() != null) {
-                            // watchDuration是分钟,转换为秒
-                            requiredWatchDuration = config.getWatchDuration() * 60;
-                        }
-                    } catch (Exception e) {
-                        logger.warn("解析直播间配置失败: {}", e.getMessage());
-                    }
-                }
-                
-                // 如果配置了看课时长,使用看课时长计算比例;否则使用视频总时长
                 Long targetDuration = requiredWatchDuration != null ? requiredWatchDuration : videoDuration;
+
+                // 计算完课比例(复用公共方法)
+                BigDecimal completionRate = null;
                 if (targetDuration != null && targetDuration > 0) {
-                    BigDecimal completionRate = BigDecimal.valueOf(record.getWatchDuration())
-                            .multiply(BigDecimal.valueOf(100))
-                            .divide(BigDecimal.valueOf(targetDuration), 2, java.math.RoundingMode.HALF_UP);
-                    if (completionRate.compareTo(BigDecimal.valueOf(100)) > 0) {
-                        completionRate = BigDecimal.valueOf(100);
-                    }
-                    record.setCompletionRate(completionRate);
+                    completionRate = calculateCompletionRate(newWatchDuration, targetDuration);
+                }
+
+                // 只更新变化的字段:watchDuration 和 completionRate
+                LiveCompletionPointsRecord updateRecord = new LiveCompletionPointsRecord();
+                updateRecord.setId(record.getId());
+                updateRecord.setWatchDuration(newWatchDuration);
+                if (completionRate != null) {
+                    updateRecord.setCompletionRate(completionRate);
                 }
 
-                int updateResult = completionPointsRecordMapper.updateRecord(record);
+                int updateResult = completionPointsRecordMapper.updateRecord(updateRecord);
                 if (updateResult <= 0) {
                     return R.error("更新看课时间失败");
                 }
+                // 更新本地对象用于返回
+                record.setWatchDuration(newWatchDuration);
+                if (completionRate != null) {
+                    record.setCompletionRate(completionRate);
+                }
             }
 
             UpdateWatchDurationVO vo = new UpdateWatchDurationVO();
@@ -402,46 +443,29 @@ public class LiveCompletionPointsController extends AppBaseController {
             }
 
             try {
-                // 1. 获取直播间信息和配置
-                Live live = liveService.selectLiveByLiveId(liveId);
+                // 1. 获取直播间信息并验证(复用公共方法)
+                Live live = getLiveAndValidate(liveId);
                 if (live == null) {
-                    return R.error("直播间不存在");
+                    return R.error(406, "直播间不存在或未开启直播观看奖励");
                 }
 
-                // 2. 检查直播间是否开启观看积分奖励
-                if (!checkWatchRewardEnabled(live)) {
-                    return R.error(406, "直播间未开启直播观看奖励");
-                }
-
-                // 3. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
+                // 2. 查询当前用户和当前直播间的最近一次完课记录(不限制日期)
                 LiveCompletionPointsRecord record = completionPointsRecordMapper.selectLatestByUserAndLiveId(liveId, userId);
 
                 if (record == null) {
                     return R.error("您还没有看课记录,无法领取积分");
                 }
 
-                // 4. 检查看课记录里面的时长是否达到完课标准
+                // 3. 检查看课记录里面的时长是否达到完课标准
                 Long watchDuration = record.getWatchDuration();
                 if (watchDuration == null || watchDuration <= 0) {
                     return R.error("您的看课时长不足,无法领取积分");
                 }
 
-                // 5. 从直播间配置获取看课时长(配置1中的watchDuration)
-                String configJson = live.getConfigJson();
-                Long requiredWatchDuration = null;
-                if (configJson != null && !configJson.isEmpty()) {
-                    try {
-                        LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
-                        if (config != null && config.getWatchDuration() != null) {
-                            // watchDuration是分钟,转换为秒
-                            requiredWatchDuration = config.getWatchDuration() * 60;
-                        }
-                    } catch (Exception e) {
-                        logger.warn("解析直播间配置失败: {}", e.getMessage());
-                    }
-                }
+                // 4. 获取看课时长配置(复用公共方法)
+                Long requiredWatchDuration = parseRequiredWatchDuration(live);
 
-                // 6. 判断是否达到完课标准(使用看课时长而不是完课比例)
+                // 5. 判断是否达到完课标准(使用看课时长而不是完课比例)
                 if (requiredWatchDuration != null && watchDuration < requiredWatchDuration) {
                     // 转换为分钟显示
                     long requiredMinutes = requiredWatchDuration / 60;
@@ -449,12 +473,13 @@ public class LiveCompletionPointsController extends AppBaseController {
                     return R.error("您的看课时长未达到标准(" + requiredMinutes + "分钟),当前看课时长:" + currentMinutes + "分钟");
                 }
 
-                // 7. 检查是否已领取
+                // 6. 检查是否已领取
                 if (record.getReceiveStatus() != null && record.getReceiveStatus() == 1) {
                     return R.error("该完课积分已领取");
                 }
 
-                // 8. 领取积分(更新看课记录的领取状态,给用户加积分)
+                // 7. 领取积分(更新看课记录的领取状态,给用户加积分)
+                // Service层已经实现了只更新 receiveStatus 和 receiveTime 的部分更新
                 LiveCompletionPointsRecord receivedRecord = completionPointsRecordService.receiveCompletionPoints(record.getId(), userId);
 
                 ReceivePointsVO vo = new ReceivePointsVO();