浏览代码

直播领积分功能

xw 3 天之前
父节点
当前提交
b49ff33682

+ 108 - 0
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -0,0 +1,108 @@
+package com.fs.live.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.websocket.bean.SendMsgVo;
+import com.fs.live.websocket.service.WebSocketServer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 直播完课积分定时任务
+ */
+@Slf4j
+@Component
+public class LiveCompletionPointsTask {
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private WebSocketServer webSocketServer;
+
+    /**
+     * 定时检查观看时长并创建完课记录
+     * 每分钟执行一次
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void checkCompletionStatus() {
+        try {
+            // 1. 获取所有观看时长的Redis key
+            Collection<String> keys = redisCache.keys("live:watch:duration:*");
+            
+            if (keys == null || keys.isEmpty()) {
+                return;
+            }
+
+            // 2. 遍历处理每个用户的观看时长
+            for (String key : keys) {
+                try {
+                    String[] parts = key.split(":");
+                    if (parts.length != 5) {
+                        continue;
+                    }
+
+                    Long liveId = Long.parseLong(parts[3]);
+                    Long userId = Long.parseLong(parts[4]);
+
+                    // 3. 获取观看时长(秒)
+                    Object durationObj = redisCache.getCacheObject(key);
+                    if (durationObj == null) {
+                        continue;
+                    }
+
+                    Long watchDuration = Long.parseLong(durationObj.toString());
+
+                    // 4. 检查并创建完课记录
+                    completionPointsRecordService.checkAndCreateCompletionRecord(liveId, userId, watchDuration);
+
+                    // 5. 检查是否有新的完课记录待领取,推送弹窗消息
+                    sendCompletionNotification(liveId, userId);
+
+                } catch (Exception e) {
+                    log.error("处理观看时长失败, key={}", key, e);
+                }
+            }
+
+        } catch (Exception e) {
+            log.error("检查完课状态定时任务执行失败", e);
+        }
+    }
+
+    /**
+     * 发送完课通知(通过WebSocket推送弹窗)
+     */
+    private void sendCompletionNotification(Long liveId, Long userId) {
+        try {
+            // 查询未领取的完课记录
+            List<LiveCompletionPointsRecord> unreceivedRecords = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+            
+            if (unreceivedRecords != null && !unreceivedRecords.isEmpty()) {
+                // 构造弹窗消息
+                SendMsgVo sendMsgVo = new SendMsgVo();
+                sendMsgVo.setLiveId(liveId);
+                sendMsgVo.setUserId(userId);
+                sendMsgVo.setCmd("completionPoints");
+                sendMsgVo.setMsg("完成任务!");
+                sendMsgVo.setData(JSONObject.toJSONString(unreceivedRecords.get(0)));
+
+                // 通过WebSocket发送给特定用户(调用已有的发送方法)
+                webSocketServer.sendCompletionPointsMessage(liveId, userId, sendMsgVo);
+                
+                log.info("发送完课积分弹窗通知成功, liveId={}, userId={}", liveId, userId);
+            }
+        } catch (Exception e) {
+            log.error("发送完课通知失败, liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+}

+ 29 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -315,6 +315,23 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
+                    
+                    // 心跳时同步更新观看时长
+                    long watchUserId = (long) userProperties.get("userId");
+                    String durationKey = "live:watch:duration:" + liveId + ":" + watchUserId;
+                    
+                    if (msg.getData() != null && !msg.getData().isEmpty()) {
+                        try {
+                            Long currentDuration = Long.parseLong(msg.getData());
+                            Object existingDuration = redisCache.getCacheObject(durationKey);
+                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
+                                redisCache.setCacheObject(durationKey, currentDuration.toString(), 2, TimeUnit.HOURS);
+                            }
+                        } catch (Exception e) {
+                            log.error("心跳更新观看时长失败, liveId={}, userId={}", liveId, watchUserId, e);
+                        }
+                    }
+                    
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -610,6 +627,18 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 发送完课积分弹窗通知给特定用户
+     */
+    public void sendCompletionPointsMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        session.getAsyncRemote().sendText(JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+    }
+
     private void sendBlockMessage(Long liveId, Long userId) {
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);

+ 3 - 2
fs-quartz/src/main/java/com/fs/quartz/config/ScheduleConfig.java

@@ -50,8 +50,9 @@ public class ScheduleConfig
         // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
         factory.setOverwriteExistingJobs(true);
         // 设置自动启动,默认为true
-        factory.setAutoStartup(true);
-//        factory.setAutoStartup(false);
+//        todo xw
+//        factory.setAutoStartup(true);
+        factory.setAutoStartup(false);
 
         return factory;
     }

+ 58 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCompletionPointsRecord.java

@@ -0,0 +1,58 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 直播完课积分领取记录对象 live_completion_points_record
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveCompletionPointsRecord extends BaseEntity {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 直播ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 观看时长(秒) */
+    private Long watchDuration;
+
+    /** 视频总时长(秒) */
+    private Long videoDuration;
+
+    /** 完课比例(%) */
+    private BigDecimal completionRate;
+
+    /** 连续完课天数 */
+    private Integer continuousDays;
+
+    /** 获得积分 */
+    private Integer pointsAwarded;
+
+    /** 上次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date lastCompletionDate;
+
+    /** 本次完课日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date currentCompletionDate;
+
+    /** 领取状态 0-未领取 1-已领取 */
+    private Integer receiveStatus;
+
+    /** 领取时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date receiveTime;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/live/domain/LiveWatchConfig.java

@@ -27,7 +27,7 @@ public class LiveWatchConfig extends BaseEntity{
     private Boolean enabled;
 
     /** 参与条件 1达到指定观看时长 */
-    @Excel(name = "参与条件 1达到指定观看时长")
+    @Excel(name = "参与条件 1达到指定观看时长 2观看比例达到指定积分")
     private Long participateCondition;
 
     /** 观看时长 */

+ 53 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCompletionPointsRecordMapper.java

@@ -0,0 +1,53 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Mapper接口
+ */
+public interface LiveCompletionPointsRecordMapper {
+
+    /**
+     * 插入完课积分记录
+     */
+    int insertRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 更新完课积分记录
+     */
+    int updateRecord(LiveCompletionPointsRecord record);
+
+    /**
+     * 查询用户某天的完课记录
+     */
+    LiveCompletionPointsRecord selectByUserAndDate(@Param("liveId") Long liveId, 
+                                                     @Param("userId") Long userId, 
+                                                     @Param("currentDate") Date currentDate);
+
+    /**
+     * 查询用户最近一次完课记录
+     */
+    LiveCompletionPointsRecord selectLatestByUser(@Param("liveId") Long liveId, 
+                                                   @Param("userId") Long userId);
+
+    /**
+     * 查询用户未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> selectUnreceivedByUser(@Param("liveId") Long liveId, 
+                                                             @Param("userId") Long userId);
+
+    /**
+     * 查询用户的完课积分领取记录列表
+     */
+    List<LiveCompletionPointsRecord> selectRecordsByUser(@Param("liveId") Long liveId, 
+                                                          @Param("userId") Long userId);
+
+    /**
+     * 根据ID查询
+     */
+    LiveCompletionPointsRecord selectById(@Param("id") Long id);
+}

+ 43 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCompletionPointsRecordService.java

@@ -0,0 +1,43 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.LiveCompletionPointsRecord;
+
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service接口
+ */
+public interface ILiveCompletionPointsRecordService {
+
+    /**
+     * 检查并创建完课记录(定时任务调用)
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @param watchDuration 观看时长(秒)
+     */
+    void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 用户领取完课积分
+     * @param recordId 完课记录ID
+     * @param userId 用户ID
+     * @return 领取结果
+     */
+    LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId);
+
+    /**
+     * 获取用户完课状态
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 未领取的完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId);
+
+    /**
+     * 查询用户积分领取记录
+     * @param liveId 直播ID
+     * @param userId 用户ID
+     * @return 完课记录列表
+     */
+    List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId);
+}

+ 328 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -0,0 +1,328 @@
+package com.fs.live.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.exception.base.BaseException;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserIntegralLogs;
+import com.fs.his.mapper.FsUserIntegralLogsMapper;
+import com.fs.his.mapper.FsUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.mapper.LiveCompletionPointsRecordMapper;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import com.fs.live.service.ILiveService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 直播完课积分记录Service业务层处理
+ */
+@Slf4j
+@Service
+public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPointsRecordService {
+
+    @Autowired
+    private LiveCompletionPointsRecordMapper recordMapper;
+
+    @Autowired
+    private ILiveService liveService;
+
+    @Autowired
+    private FsUserMapper fsUserMapper;
+
+    @Autowired
+    private FsUserIntegralLogsMapper fsUserIntegralLogsMapper;
+
+
+    /**
+     * 检查并创建完课记录(由定时任务调用)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void checkAndCreateCompletionRecord(Long liveId, Long userId, Long watchDuration) {
+        try {
+            // 1. 获取直播信息和配置
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                log.warn("直播间不存在, liveId={}", liveId);
+                return;
+            }
+
+            // 2. 从数据库获取完课积分配置
+            CompletionPointsConfig config = getCompletionPointsConfig(live);
+            
+            // 检查是否开启完课积分功能
+            if (!config.isEnabled()) {
+                log.debug("直播间未开启完课积分功能, liveId={}", liveId);
+                return;
+            }
+            
+            // 检查配置完整性
+            Integer completionRate = config.getCompletionRate();
+            int[] pointsConfig = config.getPointsConfig();
+            
+            if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
+                log.warn("完课积分配置不完整, liveId={}, completionRate={}, pointsConfig={}", 
+                        liveId, completionRate, pointsConfig);
+                return;
+            }
+
+            // 3. 获取视频总时长(秒)
+            Long videoDuration = live.getDuration();
+            if (videoDuration == null || videoDuration <= 0) {
+                log.warn("直播间视频时长无效, liveId={}, duration={}", liveId, videoDuration);
+                return;
+            }
+
+            // 4. 计算完课比例
+            BigDecimal watchRate = BigDecimal.valueOf(watchDuration)
+                    .multiply(BigDecimal.valueOf(100))
+                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+
+            // 5. 判断是否达到完课标准
+            if (watchRate.compareTo(BigDecimal.valueOf(completionRate)) < 0) {
+                log.debug("观看时长未达到完课标准, liveId={}, userId={}, watchRate={}%, required={}%",
+                        liveId, userId, watchRate, completionRate);
+                return;
+            }
+
+            // 6. 检查今天是否已有完课记录
+            LocalDate today = LocalDate.now();
+            Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
+            if (todayRecord != null) {
+                log.debug("今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                return;
+            }
+
+            // 7. 查询最近一次完课记录,计算连续天数
+            LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(liveId, userId);
+            int continuousDays = 1;
+
+            if (latestRecord != null) {
+                LocalDate lastDate = latestRecord.getCurrentCompletionDate()
+                        .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+                long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
+
+                if (daysBetween == 1) {
+                    // 昨天完课了,连续天数+1
+                    continuousDays = latestRecord.getContinuousDays() + 1;
+                } else if (daysBetween > 1) {
+                    // 中断了,重新开始
+                    continuousDays = 1;
+                } else {
+                    // daysBetween == 0 说明今天已经有记录了(理论上不会进入这里,因为前面已经检查过)
+                    log.warn("异常情况: 今天已有完课记录, liveId={}, userId={}", liveId, userId);
+                    return;
+                }
+            }
+
+            // 8. 计算积分
+            int points = calculatePoints(continuousDays, pointsConfig);
+
+            // 9. 创建完课记录
+            LiveCompletionPointsRecord record = new LiveCompletionPointsRecord();
+            record.setLiveId(liveId);
+            record.setUserId(userId);
+            record.setWatchDuration(watchDuration);
+            record.setVideoDuration(videoDuration);
+            record.setCompletionRate(watchRate);
+            record.setContinuousDays(continuousDays);
+            record.setPointsAwarded(points);
+            record.setCurrentCompletionDate(currentDate);
+            record.setReceiveStatus(0); // 未领取
+
+            if (latestRecord != null) {
+                record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
+            }
+
+            recordMapper.insertRecord(record);
+
+            log.info("创建完课记录成功, liveId={}, userId={}, continuousDays={}, points={}",
+                    liveId, userId, continuousDays, points);
+
+        } catch (Exception e) {
+            log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
+            throw e;
+        }
+    }
+
+    /**
+     * 用户领取完课积分
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LiveCompletionPointsRecord receiveCompletionPoints(Long recordId, Long userId) {
+        // 1. 查询完课记录
+        LiveCompletionPointsRecord record = recordMapper.selectById(recordId);
+        if (record == null) {
+            throw new BaseException("完课记录不存在");
+        }
+
+        // 2. 校验用户
+        if (!record.getUserId().equals(userId)) {
+            throw new BaseException("无权领取该完课积分");
+        }
+
+        // 3. 校验领取状态
+        if (record.getReceiveStatus() == 1) {
+            throw new BaseException("该完课积分已领取");
+        }
+
+        // 4. 更新用户积分
+        FsUser user = fsUserMapper.selectFsUserByUserId(userId);
+        if (user == null) {
+            throw new BaseException("用户不存在");
+        }
+
+        Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
+        Long newIntegral = currentIntegral + record.getPointsAwarded();
+
+        FsUser updateUser = new FsUser();
+        updateUser.setUserId(userId);
+        updateUser.setIntegral(newIntegral);
+        fsUserMapper.updateFsUser(updateUser);
+
+        // 5. 记录积分变动日志
+        FsUserIntegralLogs integralLog = new FsUserIntegralLogs();
+        integralLog.setUserId(userId);
+        integralLog.setIntegral(Long.valueOf(record.getPointsAwarded()));
+        integralLog.setBalance(newIntegral);
+        integralLog.setLogType(5); // 5-直播完课积分
+        integralLog.setBusinessId("live_completion_" + recordId); // 业务ID:直播完课记录ID
+        integralLog.setBusinessType(5); // 5-直播完课
+        integralLog.setStatus(1);
+        integralLog.setCreateTime(new Date());
+        fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLog);
+
+        // 6. 更新完课记录状态
+        LiveCompletionPointsRecord updateRecord = new LiveCompletionPointsRecord();
+        updateRecord.setId(recordId);
+        updateRecord.setReceiveStatus(1);
+        updateRecord.setReceiveTime(new Date());
+        recordMapper.updateRecord(updateRecord);
+
+        // 7. 返回更新后的记录
+        record.setReceiveStatus(1);
+        record.setReceiveTime(new Date());
+
+        log.info("用户领取完课积分成功, userId={}, recordId={}, points={}", userId, recordId, record.getPointsAwarded());
+
+        return record;
+    }
+
+    /**
+     * 获取用户未领取的完课记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserUnreceivedRecords(Long liveId, Long userId) {
+        return recordMapper.selectUnreceivedByUser(liveId, userId);
+    }
+
+    /**
+     * 查询用户积分领取记录
+     */
+    @Override
+    public List<LiveCompletionPointsRecord> getUserRecords(Long liveId, Long userId) {
+        return recordMapper.selectRecordsByUser(liveId, userId);
+    }
+
+    /**
+     * 从直播配置中获取完课积分配置
+     */
+    private CompletionPointsConfig getCompletionPointsConfig(Live live) {
+        CompletionPointsConfig config = new CompletionPointsConfig();
+        config.setEnabled(false);
+        config.setCompletionRate(null);
+        config.setPointsConfig(null);
+        
+        String configJson = live.getConfigJson();
+        if (configJson == null || configJson.isEmpty()) {
+            return config;
+        }
+        
+        try {
+            JSONObject jsonConfig = JSON.parseObject(configJson);
+
+            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+
+            Integer rate = jsonConfig.getInteger("completionRate");
+            if (rate != null && rate > 0 && rate <= 100) {
+                config.setCompletionRate(rate);
+            }
+
+            List<Integer> pointsList = jsonConfig.getObject("pointsConfig", List.class);
+            if (pointsList != null && !pointsList.isEmpty()) {
+                config.setPointsConfig(pointsList.stream().mapToInt(Integer::intValue).toArray());
+            }
+        } catch (Exception e) {
+            log.warn("解析完课积分配置失败, liveId={}, 配置未生效", live.getLiveId(), e);
+        }
+        
+        return config;
+    }
+    
+    /**
+     * 计算积分
+     * 根据连续天数和积分配置计算应得积分
+     * @param continuousDays 连续完课天数
+     * @param pointsConfig 积分配置数组
+     * @return 应得积分
+     */
+    private int calculatePoints(int continuousDays, int[] pointsConfig) {
+        if (continuousDays <= 0) {
+            return pointsConfig[0];
+        }
+        if (continuousDays > pointsConfig.length) {
+            // 超过配置天数,使用最后一天的积分
+            return pointsConfig[pointsConfig.length - 1];
+        }
+        return pointsConfig[continuousDays - 1];
+    }
+    
+    /**
+     * 完课积分配置内部类
+     */
+    private static class CompletionPointsConfig {
+        private boolean enabled;
+        private Integer completionRate;
+        private int[] pointsConfig;
+        
+        public boolean isEnabled() {
+            return enabled;
+        }
+        
+        public void setEnabled(boolean enabled) {
+            this.enabled = enabled;
+        }
+        
+        public Integer getCompletionRate() {
+            return completionRate;
+        }
+        
+        public void setCompletionRate(Integer completionRate) {
+            this.completionRate = completionRate;
+        }
+        
+        public int[] getPointsConfig() {
+            return pointsConfig;
+        }
+        
+        public void setPointsConfig(int[] pointsConfig) {
+            this.pointsConfig = pointsConfig;
+        }
+    }
+}

+ 116 - 0
fs-service/src/main/resources/mapper/live/LiveCompletionPointsRecordMapper.xml

@@ -0,0 +1,116 @@
+<?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.live.mapper.LiveCompletionPointsRecordMapper">
+
+    <resultMap type="com.fs.live.domain.LiveCompletionPointsRecord" id="LiveCompletionPointsRecordResult">
+        <id     property="id"                      column="id"    />
+        <result property="liveId"                  column="live_id"    />
+        <result property="userId"                  column="user_id"    />
+        <result property="watchDuration"           column="watch_duration"    />
+        <result property="videoDuration"           column="video_duration"    />
+        <result property="completionRate"          column="completion_rate"    />
+        <result property="continuousDays"          column="continuous_days"    />
+        <result property="pointsAwarded"           column="points_awarded"    />
+        <result property="lastCompletionDate"      column="last_completion_date"    />
+        <result property="currentCompletionDate"   column="current_completion_date"    />
+        <result property="receiveStatus"           column="receive_status"    />
+        <result property="receiveTime"             column="receive_time"    />
+        <result property="createTime"              column="create_time"    />
+        <result property="updateTime"              column="update_time"    />
+    </resultMap>
+
+    <!-- 插入完课积分记录 -->
+    <insert id="insertRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord" useGeneratedKeys="true" keyProperty="id">
+        INSERT INTO live_completion_points_record (
+            live_id,
+            user_id,
+            watch_duration,
+            video_duration,
+            completion_rate,
+            continuous_days,
+            points_awarded,
+            last_completion_date,
+            current_completion_date,
+            receive_status,
+            receive_time,
+            create_time,
+            update_time
+        ) VALUES (
+            #{liveId},
+            #{userId},
+            #{watchDuration},
+            #{videoDuration},
+            #{completionRate},
+            #{continuousDays},
+            #{pointsAwarded},
+            #{lastCompletionDate},
+            #{currentCompletionDate},
+            #{receiveStatus},
+            #{receiveTime},
+            NOW(),
+            NOW()
+        )
+    </insert>
+
+    <!-- 更新完课积分记录 -->
+    <update id="updateRecord" parameterType="com.fs.live.domain.LiveCompletionPointsRecord">
+        UPDATE live_completion_points_record
+        <set>
+            <if test="watchDuration != null">watch_duration = #{watchDuration},</if>
+            <if test="videoDuration != null">video_duration = #{videoDuration},</if>
+            <if test="completionRate != null">completion_rate = #{completionRate},</if>
+            <if test="continuousDays != null">continuous_days = #{continuousDays},</if>
+            <if test="pointsAwarded != null">points_awarded = #{pointsAwarded},</if>
+            <if test="lastCompletionDate != null">last_completion_date = #{lastCompletionDate},</if>
+            <if test="currentCompletionDate != null">current_completion_date = #{currentCompletionDate},</if>
+            <if test="receiveStatus != null">receive_status = #{receiveStatus},</if>
+            <if test="receiveTime != null">receive_time = #{receiveTime},</if>
+            update_time = NOW()
+        </set>
+        WHERE id = #{id}
+    </update>
+
+    <!-- 查询用户某天的完课记录 -->
+    <select id="selectByUserAndDate" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND current_completion_date = #{currentDate}
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户最近一次完课记录 -->
+    <select id="selectLatestByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+        LIMIT 1
+    </select>
+
+    <!-- 查询用户未领取的完课记录列表 -->
+    <select id="selectUnreceivedByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+          AND receive_status = 0
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 查询用户的完课积分领取记录列表 -->
+    <select id="selectRecordsByUser" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE live_id = #{liveId}
+          AND user_id = #{userId}
+        ORDER BY current_completion_date DESC
+    </select>
+
+    <!-- 根据ID查询 -->
+    <select id="selectById" resultMap="LiveCompletionPointsRecordResult">
+        SELECT * FROM live_completion_points_record
+        WHERE id = #{id}
+    </select>
+
+</mapper>

+ 92 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionPointsController.java

@@ -0,0 +1,92 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.live.domain.LiveCompletionPointsRecord;
+import com.fs.live.service.ILiveCompletionPointsRecordService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直播完课积分Controller
+ */
+@RestController
+@RequestMapping("/app/live/completion")
+public class LiveCompletionPointsController extends AppBaseController {
+
+    @Autowired
+    private ILiveCompletionPointsRecordService completionPointsRecordService;
+
+    @Autowired
+    private IFsUserService fsUserService;
+
+    /**
+     * 领取完课积分
+     */
+    @PostMapping("/receive")
+    public R receive(@RequestParam Long recordId) {
+        Long userId = Long.parseLong(getUserId());
+        LiveCompletionPointsRecord record = completionPointsRecordService.receiveCompletionPoints(recordId, userId);
+        return R.ok().put("data", record);
+    }
+
+    /**
+     * 获取用户未领取的积分列表
+     */
+    @GetMapping("/unreceived")
+    public R getUnreceived(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserUnreceivedRecords(liveId, userId);
+        return R.ok().put("data", records);
+    }
+
+    /**
+     * 查询用户积分领取记录
+     */
+    @GetMapping("/records")
+    public R getRecords(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
+        return R.ok().put("data", records);
+    }
+
+    /**
+     * 查询用户积分余额和看直播信息统计
+     */
+    @GetMapping("/info")
+    public R getInfo(@RequestParam Long liveId) {
+        Long userId = Long.parseLong(getUserId());
+        
+        // 1. 获取用户积分余额
+        FsUser user = fsUserService.selectFsUserByUserId(userId);
+        Long integral = user != null && user.getIntegral() != null ? user.getIntegral() : 0L;
+        
+        // 2. 获取完课记录列表(包含已领取和未领取)
+        List<LiveCompletionPointsRecord> records = completionPointsRecordService.getUserRecords(liveId, userId);
+        
+        // 3. 统计信息
+        long totalPoints = records.stream()
+                .filter(r -> r.getReceiveStatus() == 1)
+                .mapToLong(LiveCompletionPointsRecord::getPointsAwarded)
+                .sum();
+        
+        long unreceivedCount = records.stream()
+                .filter(r -> r.getReceiveStatus() == 0)
+                .count();
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("integral", integral);  // 当前积分余额
+        result.put("totalPoints", totalPoints);  // 本直播间累计获得积分
+        result.put("totalDays", records.size());  // 累计看直播天数
+        result.put("unreceivedCount", unreceivedCount);  // 未领取记录数
+        result.put("records", records);  // 完课记录列表
+        
+        return R.ok().put("data", result);
+    }
+}