Kaynağa Gözat

1、新增直播答题模块,修改完课优惠卷答题页面

yys 1 hafta önce
ebeveyn
işleme
c473a5fb13
32 değiştirilmiş dosya ile 1172 ekleme ve 91 silme
  1. 75 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionBankController.java
  2. 57 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java
  3. 27 3
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  4. 16 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  5. 26 16
      fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java
  6. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserTalentServiceImpl.java
  7. 4 0
      fs-service/src/main/java/com/fs/course/vo/FsUserVideoListUVO.java
  8. 45 0
      fs-service/src/main/java/com/fs/live/domain/LiveQuestionBank.java
  9. 23 0
      fs-service/src/main/java/com/fs/live/domain/LiveQuestionLive.java
  10. 3 2
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  11. 26 0
      fs-service/src/main/java/com/fs/live/mapper/LiveQuestionBankMapper.java
  12. 24 0
      fs-service/src/main/java/com/fs/live/mapper/LiveQuestionLiveMapper.java
  13. 17 0
      fs-service/src/main/java/com/fs/live/param/LiveCompletionCouponAnswerParam.java
  14. 24 6
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java
  15. 23 0
      fs-service/src/main/java/com/fs/live/service/ILiveQuestionBankService.java
  16. 19 0
      fs-service/src/main/java/com/fs/live/service/ILiveQuestionLiveService.java
  17. 282 50
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java
  18. 10 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java
  19. 53 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionBankServiceImpl.java
  20. 67 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionLiveServiceImpl.java
  21. 11 2
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  22. 16 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponNotifyResult.java
  23. 27 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponStatusVO.java
  24. 24 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionQuestionVO.java
  25. 5 2
      fs-service/src/main/java/com/fs/live/vo/LiveQuestionLiveVO.java
  26. 3 0
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  27. 43 0
      fs-service/src/main/resources/db/20250610-直播课题.sql
  28. 13 7
      fs-service/src/main/resources/mapper/course/FsUserTalentFollowMapper.xml
  29. 100 0
      fs-service/src/main/resources/mapper/live/LiveQuestionBankMapper.xml
  30. 57 0
      fs-service/src/main/resources/mapper/live/LiveQuestionLiveMapper.xml
  31. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/VideoController.java
  32. 50 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionCouponController.java

+ 75 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveQuestionBankController.java

@@ -0,0 +1,75 @@
+package com.fs.live.controller;

+

+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.model.LoginUser;

+import com.fs.common.core.page.TableDataInfo;

+import com.fs.common.enums.BusinessType;

+import com.fs.common.utils.ServletUtils;

+import com.fs.framework.web.service.TokenService;

+import com.fs.live.domain.LiveQuestionBank;

+import com.fs.live.service.ILiveQuestionBankService;

+import org.springframework.beans.factory.annotation.Autowired;

+import org.springframework.security.access.prepost.PreAuthorize;

+import org.springframework.web.bind.annotation.*;

+

+import java.util.List;

+

+/**

+ * 直播课题Controller

+ */

+@RestController

+@RequestMapping("/live/liveQuestionBank")

+public class LiveQuestionBankController extends BaseController {

+

+    @Autowired

+    private ILiveQuestionBankService liveQuestionBankService;

+

+    @Autowired

+    private TokenService tokenService;

+

+    @PreAuthorize("@ss.hasPermi('live:liveQuestionBank:list')")

+    @GetMapping("/list")

+    public TableDataInfo list(LiveQuestionBank query) {

+        startPage();

+        List<LiveQuestionBank> list = liveQuestionBankService.selectLiveQuestionBankList(query);

+        return getDataTable(list);

+    }

+

+    @PreAuthorize("@ss.hasPermi('live:liveQuestionBank:query')")

+    @GetMapping("/{id}")

+    public AjaxResult getInfo(@PathVariable Long id) {

+        return AjaxResult.success(liveQuestionBankService.selectLiveQuestionBankById(id));

+    }

+

+    @PreAuthorize("@ss.hasPermi('live:liveQuestionBank:add')")

+    @Log(title = "直播课题", businessType = BusinessType.INSERT)

+    @PostMapping

+    public AjaxResult add(@RequestBody LiveQuestionBank entity) {

+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());

+        entity.setCreateBy(loginUser.getUser().getNickName());

+        return toAjax(liveQuestionBankService.insertLiveQuestionBank(entity));

+    }

+

+    @PreAuthorize("@ss.hasPermi('live:liveQuestionBank:edit')")

+    @Log(title = "直播课题", businessType = BusinessType.UPDATE)

+    @PutMapping

+    public AjaxResult edit(@RequestBody LiveQuestionBank entity) {

+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());

+        entity.setUpdateBy(loginUser.getUser().getNickName());

+        return toAjax(liveQuestionBankService.updateLiveQuestionBank(entity));

+    }

+

+    @PreAuthorize("@ss.hasPermi('live:liveQuestionBank:remove')")

+    @Log(title = "直播课题", businessType = BusinessType.DELETE)

+    @DeleteMapping("/{ids}")

+    public AjaxResult remove(@PathVariable Long[] ids) {

+        return toAjax(liveQuestionBankService.deleteLiveQuestionBankByIds(ids));

+    }

+

+    @GetMapping("/getByIds")

+    public AjaxResult getByIds(@RequestParam List<Long> ids) {

+        return AjaxResult.success(liveQuestionBankService.selectLiveQuestionBankByIds(ids));

+    }

+}


+ 57 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java

@@ -0,0 +1,57 @@
+package com.fs.live.controller;

+

+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.model.LoginUser;

+import com.fs.common.core.page.TableDataInfo;

+import com.fs.common.enums.BusinessType;

+import com.fs.common.utils.ServletUtils;

+import com.fs.framework.web.service.TokenService;

+import com.fs.live.service.ILiveQuestionLiveService;

+import com.fs.live.vo.LiveQuestionLiveVO;

+import org.springframework.beans.factory.annotation.Autowired;

+import org.springframework.web.bind.annotation.*;

+

+import java.util.List;

+

+/**

+ * 直播间关联课题Controller

+ */

+@RestController

+@RequestMapping("/live/liveQuestionLive")

+public class LiveQuestionLiveController extends BaseController {

+

+    @Autowired

+    private ILiveQuestionLiveService liveQuestionLiveService;

+

+    @Autowired

+    private TokenService tokenService;

+

+    @GetMapping("/list")

+    public TableDataInfo list(@RequestParam Long liveId) {

+        startPage();

+        List<LiveQuestionLiveVO> list = liveQuestionLiveService.selectLiveQuestionLiveList(liveId);

+        return getDataTable(list);

+    }

+

+    @GetMapping("/optionList")

+    public TableDataInfo optionList(@RequestParam Long liveId, @RequestParam(required = false) String title) {

+        startPage();

+        List<LiveQuestionLiveVO> list = liveQuestionLiveService.selectOptionList(liveId, title);

+        return getDataTable(list);

+    }

+

+    @Log(title = "直播间关联课题", businessType = BusinessType.INSERT)

+    @PostMapping

+    public AjaxResult add(@RequestParam Long liveId, @RequestParam String questionIds) {

+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());

+        return toAjax(liveQuestionLiveService.addLiveQuestions(liveId, questionIds, loginUser.getUser().getNickName()));

+    }

+

+    @Log(title = "直播间关联课题", businessType = BusinessType.DELETE)

+    @DeleteMapping("/{liveId}")

+    public AjaxResult remove(@PathVariable Long liveId, @RequestParam Long[] ids) {

+        return toAjax(liveQuestionLiveService.deleteLiveQuestionLiveByIds(liveId, ids));

+    }

+}


+ 27 - 3
fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java

@@ -1,10 +1,15 @@
 package com.fs.live.task;
 
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.live.domain.Live;
 import com.fs.live.service.ILiveCompletionCouponService;
 import com.fs.live.service.ILiveCompletionPointsRecordService;
 import com.fs.live.service.ILiveService;
+import com.fs.live.vo.LiveCompletionCouponNotifyResult;
+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;
@@ -32,6 +37,9 @@ public class LiveCompletionPointsTask {
     @Autowired
     private ILiveService liveService;
 
+    @Autowired
+    private WebSocketServer webSocketServer;
+
     /**
      * 定时检查观看时长并创建完课积分记录(兜底机制)
      * 每分钟执行一次
@@ -55,7 +63,7 @@ public class LiveCompletionPointsTask {
     }
 
     /**
-     * 定时检查观看时长并发放完课优惠券(兜底机制)
+     * 定时检查观看时长并推送完课优惠券今日问题弹窗(兜底机制)
      * 每分钟执行一次
      */
     @Scheduled(cron = "0 */1 * * * ?")
@@ -68,14 +76,30 @@ public class LiveCompletionPointsTask {
                 return;
             }
 
-            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) ->
-                    completionCouponService.checkAndIssueCompletionCoupon(liveId, userId, duration));
+            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) -> {
+                LiveCompletionCouponNotifyResult notifyResult =
+                        completionCouponService.checkAndNotifyCompletionCoupon(liveId, userId, duration);
+                if (notifyResult != null && notifyResult.isShouldNotify()) {
+                    pushCompletionCouponQuestion(liveId, userId, notifyResult);
+                }
+            });
 
         } catch (Exception e) {
             log.error("检查完课优惠券定时任务执行失败", e);
         }
     }
 
+    private void pushCompletionCouponQuestion(Long liveId, Long userId, LiveCompletionCouponNotifyResult notifyResult) {
+        SendMsgVo sendMsgVo = new SendMsgVo();
+        sendMsgVo.setLiveId(liveId);
+        sendMsgVo.setUserId(userId);
+        sendMsgVo.setCmd("completionCouponQuestion");
+        sendMsgVo.setMsg("今日问题");
+        sendMsgVo.setData(JSONObject.toJSONString(notifyResult.getQuestions()));
+        webSocketServer.sendCompletionCouponQuestionMessage(liveId, userId, sendMsgVo);
+        log.info("[完课优惠券] 推送今日问题弹窗, liveId={}, userId={}", liveId, userId);
+    }
+
     private void processCompletionByWatchDuration(List<Live> activeLives, CompletionHandler handler) {
         for (Live live : activeLives) {
             try {

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

@@ -997,6 +997,22 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 发送完课优惠券今日问题弹窗通知给特定用户
+     */
+    public void sendCompletionCouponQuestionMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+        } catch (Exception e) {
+            log.error("发送完课优惠券今日问题消息失败: liveId={}, userId={}", liveId, userId, e);
+        }
+    }
+
     private void sendBlockMessage(Long liveId, Long userId) {
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);

+ 26 - 16
fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java

@@ -132,9 +132,13 @@ public interface FsUserVideoMapper
     int minusFavorite(Long videoId);
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num from fs_user_video v " +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+            "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num " +
+            "<if test = 'maps.userId != null'> " +
+            ",CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{maps.userId}) THEN '1' ELSE '0' END as isFollow " +
+            "</if> " +
+            "from fs_user_video v " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
             "where v.is_del = 0 and v.status = 1  " +
@@ -150,9 +154,11 @@ public interface FsUserVideoMapper
     void updateCommentCount(@Param("videoId") Long videoId, @Param("commentCount") Integer commentCount);
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares from fs_user_video_favorite f " +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares," +
+            "CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{userId}) THEN '1' ELSE '0' END as isFollow " +
+            "from fs_user_video_favorite f " +
             "left join fs_user_video v on v.video_id = f.video_id " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
@@ -163,9 +169,11 @@ public interface FsUserVideoMapper
 
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares from fs_user_video_like l " +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares," +
+            "CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{userId}) THEN '1' ELSE '0' END as isFollow " +
+            "from fs_user_video_like l " +
             "left join fs_user_video v on v.video_id = l.video_id " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
@@ -176,9 +184,11 @@ public interface FsUserVideoMapper
 
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
-            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares from fs_user_video_comment c " +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
+            "v.create_time,v.views as playNumber,v.product_id,v.product_json,p.img_url,p.package_name,v.shares," +
+            "CASE WHEN EXISTS (SELECT 1 FROM fs_user_talent_follow tf WHERE tf.talent_id = v.talent_id AND tf.user_id = #{userId}) THEN '1' ELSE '0' END as isFollow " +
+            "from fs_user_video_comment c " +
             "left join fs_user_video v on v.video_id = c.video_id " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
@@ -205,8 +215,8 @@ public interface FsUserVideoMapper
     int updateViews(Long videoId);
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
             "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares from fs_user_video v " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +
@@ -252,8 +262,8 @@ public interface FsUserVideoMapper
     int countFavoriteVideos(@Param("userId") Long userId);
 
     @Select({"<script> " +
-            "select v.video_id as id,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
-            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num," +
+            "select v.video_id as id,v.talent_id as talentId,t.user_id as userId,v.title,v.description as msg,t.nick_name as username,t.avatar as headImg, " +
+            "v.thumbnail as cover,v.url as src,v.likes as likeNum,v.comments as smsNum,v.favorite_num as favoriteNum," +
             "v.create_time,v.views as playNumber,v.product_id,p.img_url,p.package_name,v.upload_type,v.shares,v.add_num,v.is_audit,v.fail_reason,v.status from fs_user_video v " +
             "left join fs_user_talent t on t.talent_id = v.talent_id " +
             " left join fs_package p on p.package_id = v.product_id " +

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

@@ -160,7 +160,7 @@ public class FsUserTalentServiceImpl implements IFsUserTalentService
         fsUserTalent.setIsAudit(1l);
         fsUserTalent.setAuditTime(new Date());
         fsUserTalent.setStatus(1l);
-        fsUserTalent.setIsDel(1l);
+//        fsUserTalent.setIsDel(1l);
         fsUserTalentMapper.insertFsUserTalent(fsUserTalent);
         return 1;
     }

+ 4 - 0
fs-service/src/main/java/com/fs/course/vo/FsUserVideoListUVO.java

@@ -40,6 +40,10 @@ public class FsUserVideoListUVO {
     private String packageName;
     private String productJson;
     private Integer uploadType;
+
+    /**
+     * 分享数量
+     */
     private Long shares;
     private Integer isAudit;
     private Integer status;

+ 45 - 0
fs-service/src/main/java/com/fs/live/domain/LiveQuestionBank.java

@@ -0,0 +1,45 @@
+package com.fs.live.domain;

+

+import com.fs.common.annotation.Excel;

+import com.fs.common.core.domain.BaseEntity;

+import lombok.Data;

+import lombok.EqualsAndHashCode;

+

+/**

+ * 直播课题对象 live_question_bank

+ */

+@Data

+@EqualsAndHashCode(callSuper = true)

+public class LiveQuestionBank extends BaseEntity {

+

+    private static final long serialVersionUID = 1L;

+

+    /** 主键ID */

+    private Long id;

+

+    /** 题干 */

+    @Excel(name = "题干")

+    private String title;

+

+    /** 排序 */

+    @Excel(name = "排序")

+    private Long sort;

+

+    /** 题型 1单选 2多选 */

+    @Excel(name = "题型", readConverterExp = "1=单选,2=多选")

+    private Long type;

+

+    /** 状态 0停用 1启用 */

+    @Excel(name = "状态", readConverterExp = "0=停用,1=启用")

+    private Long status;

+

+    /** 选项JSON */

+    private String question;

+

+    /** 答案 */

+    @Excel(name = "答案")

+    private String answer;

+

+    /** 排除已关联该直播的课题(查询用,非表字段) */

+    private Long excludeLiveId;

+}


+ 23 - 0
fs-service/src/main/java/com/fs/live/domain/LiveQuestionLive.java

@@ -0,0 +1,23 @@
+package com.fs.live.domain;

+

+import com.fs.common.core.domain.BaseEntity;

+import lombok.Data;

+import lombok.EqualsAndHashCode;

+

+/**

+ * 直播间关联课题 live_question_live

+ */

+@Data

+@EqualsAndHashCode(callSuper = true)

+public class LiveQuestionLive extends BaseEntity {

+

+    private static final long serialVersionUID = 1L;

+

+    private Long id;

+

+    private Long liveId;

+

+    private Long questionId;

+

+    private Integer sort;

+}


+ 3 - 2
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -127,7 +127,9 @@ public interface LiveMapper
      */
     @Select("select * from live where status != 3 and live_type in (2,3) and is_audit = 1 " +
             "and config_json is not null " +
-            "and JSON_EXTRACT(config_json, '$.enabled') = true")
+            "and JSON_EXTRACT(config_json, '$.enabled') = true " +
+            "and (JSON_EXTRACT(config_json, '$.participateCondition') = 2 " +
+            "or JSON_EXTRACT(config_json, '$.participateCondition') is null)")
     List<Live> selectLiveListWithCompletionPointsEnabled();
 
     /**
@@ -138,7 +140,6 @@ public interface LiveMapper
             "and config_json is not null " +
             "and JSON_EXTRACT(config_json, '$.enabled') = true " +
             "and JSON_EXTRACT(config_json, '$.participateCondition') = 3 " +
-            "and JSON_EXTRACT(config_json, '$.action') = 3 " +
             "and JSON_EXTRACT(config_json, '$.finishCouponId') is not null " +
             "and JSON_EXTRACT(config_json, '$.finishCouponId') != ''")
     List<Live> selectLiveListWithCompletionCouponEnabled();

+ 26 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveQuestionBankMapper.java

@@ -0,0 +1,26 @@
+package com.fs.live.mapper;

+

+import com.fs.live.domain.LiveQuestionBank;

+import org.apache.ibatis.annotations.Param;

+

+import java.util.List;

+

+/**

+ * 直播课题Mapper

+ */

+public interface LiveQuestionBankMapper {

+

+    LiveQuestionBank selectLiveQuestionBankById(Long id);

+

+    List<LiveQuestionBank> selectLiveQuestionBankList(LiveQuestionBank query);

+

+    List<LiveQuestionBank> selectLiveQuestionBankByIds(@Param("list") List<Long> ids);

+

+    int insertLiveQuestionBank(LiveQuestionBank entity);

+

+    int updateLiveQuestionBank(LiveQuestionBank entity);

+

+    int deleteLiveQuestionBankById(Long id);

+

+    int deleteLiveQuestionBankByIds(Long[] ids);

+}


+ 24 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveQuestionLiveMapper.java

@@ -0,0 +1,24 @@
+package com.fs.live.mapper;

+

+import com.fs.live.domain.LiveQuestionBank;

+import com.fs.live.domain.LiveQuestionLive;

+import com.fs.live.vo.LiveQuestionLiveVO;

+import org.apache.ibatis.annotations.Param;

+

+import java.util.List;

+

+/**

+ * 直播间关联课题Mapper

+ */

+public interface LiveQuestionLiveMapper {

+

+    List<LiveQuestionLiveVO> selectLiveQuestionLiveList(@Param("liveId") Long liveId);

+

+    List<LiveQuestionLiveVO> selectOptionList(LiveQuestionBank query);

+

+    int insertLiveQuestionLive(LiveQuestionLive entity);

+

+    int deleteLiveQuestionLiveByIds(@Param("liveId") Long liveId, @Param("ids") Long[] ids);

+

+    int countByLiveIdAndQuestionId(@Param("liveId") Long liveId, @Param("questionId") Long questionId);

+}


+ 17 - 0
fs-service/src/main/java/com/fs/live/param/LiveCompletionCouponAnswerParam.java

@@ -0,0 +1,17 @@
+package com.fs.live.param;

+

+import com.fs.live.domain.LiveQuestionBank;

+import lombok.Data;

+

+import java.util.List;

+

+/**

+ * 直播完课优惠券答题参数

+ */

+@Data

+public class LiveCompletionCouponAnswerParam {

+

+    private Long liveId;

+

+    private List<LiveQuestionBank> questions;

+}


+ 24 - 6
fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java

@@ -1,16 +1,34 @@
 package com.fs.live.service;
 
+import com.fs.live.param.LiveCompletionCouponAnswerParam;
+import com.fs.live.vo.LiveCompletionCouponNotifyResult;
+import com.fs.live.vo.LiveCompletionCouponStatusVO;
+import com.fs.live.vo.LiveCompletionQuestionVO;
+
+import java.util.List;
+
 /**
  * 直播完课优惠券Service接口
  */
 public interface ILiveCompletionCouponService {
 
     /**
-     * 检查完课状态并发放优惠券(定时任务调用)
-     *
-     * @param liveId        直播ID
-     * @param userId        用户ID
-     * @param watchDuration 观看时长(秒)
+     * 检查完课状态,达到条件时标记可弹窗(不直接发券)
+     */
+    LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 查询完课优惠券状态及今日问题
+     */
+    LiveCompletionCouponStatusVO getCompletionCouponStatus(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 获取今日问题列表(不含答案)
+     */
+    List<LiveCompletionQuestionVO> getCompletionQuestions(Long liveId);
+
+    /**
+     * 提交答题,全部答对后发放优惠券
      */
-    void checkAndIssueCompletionCoupon(Long liveId, Long userId, Long watchDuration);
+    void submitAnswerAndIssueCoupon(LiveCompletionCouponAnswerParam param, Long userId);
 }

+ 23 - 0
fs-service/src/main/java/com/fs/live/service/ILiveQuestionBankService.java

@@ -0,0 +1,23 @@
+package com.fs.live.service;

+

+import com.fs.live.domain.LiveQuestionBank;

+

+import java.util.List;

+

+/**

+ * 直播课题Service

+ */

+public interface ILiveQuestionBankService {

+

+    LiveQuestionBank selectLiveQuestionBankById(Long id);

+

+    List<LiveQuestionBank> selectLiveQuestionBankList(LiveQuestionBank query);

+

+    List<LiveQuestionBank> selectLiveQuestionBankByIds(List<Long> ids);

+

+    int insertLiveQuestionBank(LiveQuestionBank entity);

+

+    int updateLiveQuestionBank(LiveQuestionBank entity);

+

+    int deleteLiveQuestionBankByIds(Long[] ids);

+}


+ 19 - 0
fs-service/src/main/java/com/fs/live/service/ILiveQuestionLiveService.java

@@ -0,0 +1,19 @@
+package com.fs.live.service;

+

+import com.fs.live.vo.LiveQuestionLiveVO;

+

+import java.util.List;

+

+/**

+ * 直播间关联课题Service

+ */

+public interface ILiveQuestionLiveService {

+

+    List<LiveQuestionLiveVO> selectLiveQuestionLiveList(Long liveId);

+

+    List<LiveQuestionLiveVO> selectOptionList(Long liveId, String title);

+

+    int addLiveQuestions(Long liveId, String questionIds, String createBy);

+

+    int deleteLiveQuestionLiveByIds(Long liveId, Long[] ids);

+}


+ 282 - 50
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -2,10 +2,18 @@ package com.fs.live.service.impl;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.StringUtils;
 import com.fs.live.domain.*;
+import com.fs.live.mapper.LiveQuestionBankMapper;
+import com.fs.live.param.LiveCompletionCouponAnswerParam;
 import com.fs.live.service.*;
+import com.fs.live.vo.LiveCompletionCouponNotifyResult;
+import com.fs.live.vo.LiveCompletionCouponStatusVO;
+import com.fs.live.vo.LiveCompletionQuestionVO;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -14,9 +22,10 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
 import java.time.ZoneId;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * 直播完课优惠券Service业务层处理
@@ -28,6 +37,8 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     /** 获取方式:4-完课奖励 */
     private static final String COMPLETION_COUPON_TYPE = "4";
 
+    private static final String NOTIFY_REDIS_KEY_PREFIX = "live:completion:coupon:notify:";
+
     @Autowired
     private ILiveService liveService;
 
@@ -46,66 +57,214 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
     @Autowired
     private ILiveRewardRecordService liveRewardRecordService;
 
+    @Autowired
+    private LiveQuestionBankMapper liveQuestionBankMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void checkAndIssueCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+    public LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+        LiveCompletionCouponNotifyResult result = new LiveCompletionCouponNotifyResult();
+        result.setShouldNotify(false);
+
         try {
-            Live live = liveService.selectLiveByLiveId(liveId);
-            if (live == null) {
-                return;
+            CompletionCouponConfig config = resolveConfig(liveId);
+            if (!config.isEnabled()) {
+                return result;
             }
 
-            CompletionCouponConfig config = getCompletionCouponConfig(live);
-            if (!config.isEnabled() || config.getCouponId() == null || config.getCompletionRate() == null) {
-                return;
+            if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
+                return result;
             }
 
-            Long actualWatchDuration = watchDuration;
-            if (actualWatchDuration == null) {
-                actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
-            }
-            if (actualWatchDuration == null || actualWatchDuration <= 0) {
-                return;
+            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+                return result;
             }
 
-            Long videoDuration = live.getDuration();
-            if (videoDuration == null || videoDuration <= 0) {
-                return;
+            List<LiveCompletionQuestionVO> questions = loadQuestions(config.getFinishQuestionIds());
+            if (questions.isEmpty()) {
+                log.warn("完课优惠券已开启但未配置今日问题, liveId={}", liveId);
+                return result;
             }
 
-            BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
-                    .multiply(BigDecimal.valueOf(100))
-                    .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
-            if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
-                watchRate = BigDecimal.valueOf(100);
+            if (hasNotifiedToday(liveId, userId)) {
+                return result;
             }
 
-            if (watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) < 0) {
-                return;
-            }
+            markNotifiedToday(liveId, userId);
+            result.setShouldNotify(true);
+            result.setQuestions(questions);
+        } catch (Exception e) {
+            log.error("检查完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
+        }
+        return result;
+    }
 
-            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
-                return;
+    @Override
+    public LiveCompletionCouponStatusVO getCompletionCouponStatus(Long liveId, Long userId, Long watchDuration) {
+        LiveCompletionCouponStatusVO status = new LiveCompletionCouponStatusVO();
+        status.setEnabled(false);
+        status.setEligible(false);
+        status.setReceivedToday(false);
+        status.setHasQuestions(false);
+        status.setQuestions(Collections.emptyList());
+
+        CompletionCouponConfig config = resolveConfig(liveId);
+        if (!config.isEnabled()) {
+            return status;
+        }
+
+        status.setEnabled(true);
+        List<LiveCompletionQuestionVO> questions = loadQuestions(config.getFinishQuestionIds());
+        status.setHasQuestions(!questions.isEmpty());
+        status.setQuestions(questions);
+        status.setEligible(isWatchRateEligible(liveId, userId, watchDuration, config));
+        status.setReceivedToday(hasIssuedToday(liveId, userId, config.getCouponId()));
+        return status;
+    }
+
+    @Override
+    public List<LiveCompletionQuestionVO> getCompletionQuestions(Long liveId) {
+        CompletionCouponConfig config = resolveConfig(liveId);
+        if (!config.isEnabled()) {
+            return Collections.emptyList();
+        }
+        return loadQuestions(config.getFinishQuestionIds());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void submitAnswerAndIssueCoupon(LiveCompletionCouponAnswerParam param, Long userId) {
+        if (param == null || param.getLiveId() == null) {
+            throw new BaseException("参数错误");
+        }
+        if (param.getQuestions() == null || param.getQuestions().isEmpty()) {
+            throw new BaseException("请完成今日问题");
+        }
+
+        Long liveId = param.getLiveId();
+        CompletionCouponConfig config = resolveConfig(liveId);
+        if (!config.isEnabled() || config.getCouponId() == null) {
+            throw new BaseException("完课优惠券未开启");
+        }
+
+        if (!isWatchRateEligible(liveId, userId, null, config)) {
+            throw new BaseException("未达到完课要求");
+        }
+
+        if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+            throw new BaseException("今日福利券已领取");
+        }
+
+        List<Long> configuredQuestionIds = parseQuestionIds(config.getFinishQuestionIds());
+        if (configuredQuestionIds.isEmpty()) {
+            throw new BaseException("未配置今日问题");
+        }
+
+        validateAnswers(param.getQuestions(), configuredQuestionIds);
+
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            throw new BaseException("直播不存在");
+        }
+        issueCoupon(live, userId, config.getCouponId());
+    }
+
+    private void validateAnswers(List<LiveQuestionBank> userAnswers, List<Long> configuredQuestionIds) {
+        Set<Long> submittedIds = userAnswers.stream()
+                .map(LiveQuestionBank::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (submittedIds.size() != configuredQuestionIds.size()
+                || !submittedIds.containsAll(configuredQuestionIds)) {
+            throw new BaseException("请完成全部今日问题");
+        }
+
+        Map<Long, LiveQuestionBank> correctAnswersMap = liveQuestionBankMapper
+                .selectLiveQuestionBankByIds(new ArrayList<>(submittedIds))
+                .stream()
+                .collect(Collectors.toMap(LiveQuestionBank::getId, q -> q));
+
+        for (LiveQuestionBank userAnswer : userAnswers) {
+            LiveQuestionBank correctAnswer = correctAnswersMap.get(userAnswer.getId());
+            if (correctAnswer == null || correctAnswer.getStatus() == null || correctAnswer.getStatus() == 0) {
+                throw new BaseException("题目不存在或已停用");
+            }
+            if (!isAnswerCorrect(userAnswer, correctAnswer)) {
+                throw new BaseException("回答错误,请重新作答");
             }
+        }
+    }
 
-            issueCoupon(live, userId, config.getCouponId());
-        } catch (Exception e) {
-            log.error("检查并发放完课优惠券失败, liveId={}, userId={}", liveId, userId, e);
-            throw e;
+    private boolean isAnswerCorrect(LiveQuestionBank userAnswer, LiveQuestionBank correctAnswer) {
+        if (correctAnswer.getType() == null || correctAnswer.getType() == 1L) {
+            return Objects.equals(userAnswer.getAnswer(), correctAnswer.getAnswer());
+        }
+        String[] userAnswers = parseAnswerArray(userAnswer.getAnswer());
+        String[] correctAnswers = parseAnswerArray(correctAnswer.getAnswer());
+        Arrays.sort(userAnswers);
+        Arrays.sort(correctAnswers);
+        return Arrays.equals(userAnswers, correctAnswers);
+    }
+
+    private String[] parseAnswerArray(String answer) {
+        if (StringUtils.isEmpty(answer)) {
+            return new String[0];
         }
+        String trimmed = answer.trim();
+        if (trimmed.startsWith("[")) {
+            List<String> list = JSON.parseArray(trimmed, String.class);
+            return list == null ? new String[0] : list.toArray(new String[0]);
+        }
+        return trimmed.split(",");
+    }
+
+    private boolean isWatchRateEligible(Long liveId, Long userId, Long watchDuration, CompletionCouponConfig config) {
+        Long actualWatchDuration = watchDuration;
+        if (actualWatchDuration == null) {
+            actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
+        }
+        if (actualWatchDuration == null || actualWatchDuration <= 0) {
+            return false;
+        }
+
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            return false;
+        }
+
+        Long videoDuration = live.getDuration();
+        if (videoDuration == null || videoDuration <= 0) {
+            return false;
+        }
+
+        BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
+        if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
+            watchRate = BigDecimal.valueOf(100);
+        }
+        return watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) >= 0;
+    }
+
+    private CompletionCouponConfig resolveConfig(Long liveId) {
+        Live live = liveService.selectLiveByLiveId(liveId);
+        if (live == null) {
+            return disabledConfig();
+        }
+        return getCompletionCouponConfig(live);
     }
 
     private void issueCoupon(Live live, Long userId, Long couponId) {
         LiveCoupon coupon = liveCouponService.selectLiveCouponById(couponId);
         if (coupon == null) {
-            log.warn("完课优惠券不存在, couponId={}", couponId);
-            return;
+            throw new BaseException("优惠券不存在");
         }
 
         LiveCouponIssue couponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(couponId);
         if (couponIssue == null || couponIssue.getStatus() == null || couponIssue.getStatus() != 1) {
-            log.warn("完课优惠券领取配置不可用, couponId={}", couponId);
-            return;
+            throw new BaseException("优惠券领取配置不可用");
         }
 
         Date now = new Date();
@@ -162,9 +321,54 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                         && item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(today));
     }
 
+    private List<LiveCompletionQuestionVO> loadQuestions(String finishQuestionIds) {
+        List<Long> questionIds = parseQuestionIds(finishQuestionIds);
+        if (questionIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<LiveQuestionBank> questionBanks = liveQuestionBankMapper.selectLiveQuestionBankByIds(questionIds);
+        if (questionBanks == null || questionBanks.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Map<Long, LiveQuestionBank> questionMap = questionBanks.stream()
+                .filter(q -> q.getStatus() != null && q.getStatus() != 0)
+                .collect(Collectors.toMap(LiveQuestionBank::getId, q -> q, (a, b) -> a));
+
+        List<LiveCompletionQuestionVO> result = new ArrayList<>();
+        for (Long questionId : questionIds) {
+            LiveQuestionBank questionBank = questionMap.get(questionId);
+            if (questionBank == null) {
+                continue;
+            }
+            LiveCompletionQuestionVO vo = new LiveCompletionQuestionVO();
+            BeanUtils.copyProperties(questionBank, vo);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    private List<Long> parseQuestionIds(String finishQuestionIds) {
+        if (StringUtils.isEmpty(finishQuestionIds)) {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(finishQuestionIds.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotEmpty)
+                .map(id -> {
+                    try {
+                        return Long.parseLong(id);
+                    } catch (NumberFormatException e) {
+                        return null;
+                    }
+                })
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+    }
+
     private CompletionCouponConfig getCompletionCouponConfig(Live live) {
-        CompletionCouponConfig config = new CompletionCouponConfig();
-        config.setEnabled(false);
+        CompletionCouponConfig config = disabledConfig();
 
         String configJson = live.getConfigJson();
         if (StringUtils.isEmpty(configJson)) {
@@ -173,38 +377,58 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
 
         try {
             JSONObject jsonConfig = JSON.parseObject(configJson);
-            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+            if (!jsonConfig.getBooleanValue("enabled")) {
+                return config;
+            }
 
             Long participateCondition = jsonConfig.getLong("participateCondition");
-            Long action = jsonConfig.getLong("action");
-            if (participateCondition == null || participateCondition != 3L
-                    || action == null || action != 3L) {
-                config.setEnabled(false);
+            if (participateCondition == null || participateCondition != 3L) {
                 return config;
             }
 
             String finishCouponId = jsonConfig.getString("finishCouponId");
             if (StringUtils.isEmpty(finishCouponId)) {
-                config.setEnabled(false);
                 return config;
             }
+
+            config.setEnabled(true);
             config.setCouponId(Long.parseLong(finishCouponId));
+            config.setFinishQuestionIds(jsonConfig.getString("finishQuestionIds"));
 
             Integer completionRate = jsonConfig.getInteger("completionRate");
-            if (completionRate != null && completionRate > 0 && completionRate <= 100) {
-                config.setCompletionRate(completionRate);
-            }
+            config.setCompletionRate(completionRate != null && completionRate > 0 && completionRate <= 100 ? completionRate : 90);
         } catch (Exception e) {
             log.warn("解析完课优惠券配置失败, liveId={}", live.getLiveId(), e);
-            config.setEnabled(false);
+            return disabledConfig();
         }
         return config;
     }
 
+    private CompletionCouponConfig disabledConfig() {
+        CompletionCouponConfig config = new CompletionCouponConfig();
+        config.setEnabled(false);
+        config.setCompletionRate(90);
+        return config;
+    }
+
+    private boolean hasNotifiedToday(Long liveId, Long userId) {
+        return Boolean.TRUE.equals(redisCache.getCacheObject(buildNotifyRedisKey(liveId, userId)));
+    }
+
+    private void markNotifiedToday(Long liveId, Long userId) {
+        redisCache.setCacheObject(buildNotifyRedisKey(liveId, userId), Boolean.TRUE, 1, TimeUnit.DAYS);
+    }
+
+    private String buildNotifyRedisKey(Long liveId, Long userId) {
+        String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+        return NOTIFY_REDIS_KEY_PREFIX + liveId + ":" + userId + ":" + today;
+    }
+
     private static class CompletionCouponConfig {
         private boolean enabled;
         private Integer completionRate;
         private Long couponId;
+        private String finishQuestionIds;
 
         public boolean isEnabled() {
             return enabled;
@@ -229,5 +453,13 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         public void setCouponId(Long couponId) {
             this.couponId = couponId;
         }
+
+        public String getFinishQuestionIds() {
+            return finishQuestionIds;
+        }
+
+        public void setFinishQuestionIds(String finishQuestionIds) {
+            this.finishQuestionIds = finishQuestionIds;
+        }
     }
 }

+ 10 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionPointsRecordServiceImpl.java

@@ -400,7 +400,16 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
         try {
             JSONObject jsonConfig = JSON.parseObject(configJson);
 
-            config.setEnabled(jsonConfig.getBooleanValue("enabled"));
+            if (!jsonConfig.getBooleanValue("enabled")) {
+                return config;
+            }
+
+            Long participateCondition = jsonConfig.getLong("participateCondition");
+            if (participateCondition != null && participateCondition != 2L) {
+                return config;
+            }
+
+            config.setEnabled(true);
 
             Integer rate = jsonConfig.getInteger("completionRate");
             if (rate != null && rate > 0 && rate <= 100) {

+ 53 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionBankServiceImpl.java

@@ -0,0 +1,53 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.live.domain.LiveQuestionBank;
+import com.fs.live.mapper.LiveQuestionBankMapper;
+import com.fs.live.service.ILiveQuestionBankService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class LiveQuestionBankServiceImpl implements ILiveQuestionBankService {
+
+    @Autowired
+    private LiveQuestionBankMapper liveQuestionBankMapper;
+
+    @Override
+    public LiveQuestionBank selectLiveQuestionBankById(Long id) {
+        return liveQuestionBankMapper.selectLiveQuestionBankById(id);
+    }
+
+    @Override
+    public List<LiveQuestionBank> selectLiveQuestionBankList(LiveQuestionBank query) {
+        return liveQuestionBankMapper.selectLiveQuestionBankList(query);
+    }
+
+    @Override
+    public List<LiveQuestionBank> selectLiveQuestionBankByIds(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return liveQuestionBankMapper.selectLiveQuestionBankByIds(ids);
+    }
+
+    @Override
+    public int insertLiveQuestionBank(LiveQuestionBank entity) {
+        entity.setCreateTime(DateUtils.getNowDate());
+        return liveQuestionBankMapper.insertLiveQuestionBank(entity);
+    }
+
+    @Override
+    public int updateLiveQuestionBank(LiveQuestionBank entity) {
+        entity.setUpdateTime(DateUtils.getNowDate());
+        return liveQuestionBankMapper.updateLiveQuestionBank(entity);
+    }
+
+    @Override
+    public int deleteLiveQuestionBankByIds(Long[] ids) {
+        return liveQuestionBankMapper.deleteLiveQuestionBankByIds(ids);
+    }
+}

+ 67 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionLiveServiceImpl.java

@@ -0,0 +1,67 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.LiveQuestionBank;
+import com.fs.live.domain.LiveQuestionLive;
+import com.fs.live.mapper.LiveQuestionLiveMapper;
+import com.fs.live.service.ILiveQuestionLiveService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class LiveQuestionLiveServiceImpl implements ILiveQuestionLiveService {
+
+    @Autowired
+    private LiveQuestionLiveMapper liveQuestionLiveMapper;
+
+    @Override
+    public List<LiveQuestionLiveVO> selectLiveQuestionLiveList(Long liveId) {
+        return liveQuestionLiveMapper.selectLiveQuestionLiveList(liveId);
+    }
+
+    @Override
+    public List<LiveQuestionLiveVO> selectOptionList(Long liveId, String title) {
+        LiveQuestionBank query = new LiveQuestionBank();
+        query.setTitle(title);
+        query.setExcludeLiveId(liveId);
+        return liveQuestionLiveMapper.selectOptionList(query);
+    }
+
+    @Override
+    public int addLiveQuestions(Long liveId, String questionIds, String createBy) {
+        if (liveId == null || StringUtils.isEmpty(questionIds)) {
+            return 0;
+        }
+        List<Long> ids = Arrays.stream(questionIds.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotEmpty)
+                .map(Long::parseLong)
+                .collect(Collectors.toList());
+
+        int count = 0;
+        for (Long questionId : ids) {
+            if (liveQuestionLiveMapper.countByLiveIdAndQuestionId(liveId, questionId) > 0) {
+                continue;
+            }
+            LiveQuestionLive entity = new LiveQuestionLive();
+            entity.setLiveId(liveId);
+            entity.setQuestionId(questionId);
+            entity.setSort(0);
+            entity.setCreateBy(createBy);
+            entity.setCreateTime(DateUtils.getNowDate());
+            count += liveQuestionLiveMapper.insertLiveQuestionLive(entity);
+        }
+        return count;
+    }
+
+    @Override
+    public int deleteLiveQuestionLiveByIds(Long liveId, Long[] ids) {
+        return liveQuestionLiveMapper.deleteLiveQuestionLiveByIds(liveId, ids);
+    }
+}

+ 11 - 2
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -373,16 +373,25 @@ public class LiveServiceImpl implements ILiveService
 		liveVo.setNowDuration(200L);
 
         Boolean completionPointsEnabled = false;
+        Boolean completionCouponEnabled = false;
         String configJson = live.getConfigJson();
         if (StringUtils.isNotEmpty(configJson)) {
             try {
                 JSONObject jsonConfig = JSON.parseObject(configJson);
-                completionPointsEnabled = jsonConfig.getBooleanValue("enabled");
+                boolean enabled = jsonConfig.getBooleanValue("enabled");
+                Long participateCondition = jsonConfig.getLong("participateCondition");
+                if (enabled && (participateCondition == null || participateCondition == 2L)) {
+                    completionPointsEnabled = true;
+                }
+                if (enabled && participateCondition != null && participateCondition == 3L) {
+                    completionCouponEnabled = true;
+                }
             } catch (Exception e) {
-                log.warn("解析直播完课积分配置失败, liveId={}", id, e);
+                log.warn("解析直播完课奖励配置失败, liveId={}", id, e);
             }
         }
         liveVo.setCompletionPointsEnabled(completionPointsEnabled);
+        liveVo.setCompletionCouponEnabled(completionCouponEnabled);
 
         LiveVideo liveVideo = liveVideoService.selectLiveVideoByLiveIdAndType(id, 3);
         if (liveVideo != null) {

+ 16 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponNotifyResult.java

@@ -0,0 +1,16 @@
+package com.fs.live.vo;

+

+import lombok.Data;

+

+import java.util.List;

+

+/**

+ * 完课优惠券弹窗通知结果

+ */

+@Data

+public class LiveCompletionCouponNotifyResult {

+

+    private boolean shouldNotify;

+

+    private List<LiveCompletionQuestionVO> questions;

+}


+ 27 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponStatusVO.java

@@ -0,0 +1,27 @@
+package com.fs.live.vo;

+

+import lombok.Data;

+

+import java.util.List;

+

+/**

+ * 直播完课优惠券状态

+ */

+@Data

+public class LiveCompletionCouponStatusVO {

+

+    /** 是否已开启完课优惠券 */

+    private boolean enabled;

+

+    /** 是否达到完课条件 */

+    private boolean eligible;

+

+    /** 今天是否已领取优惠券 */

+    private boolean receivedToday;

+

+    /** 是否已配置今日问题 */

+    private boolean hasQuestions;

+

+    /** 今日问题列表(不含答案) */

+    private List<LiveCompletionQuestionVO> questions;

+}


+ 24 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionQuestionVO.java

@@ -0,0 +1,24 @@
+package com.fs.live.vo;

+

+import lombok.Data;

+

+/**

+ * 直播完课优惠券-今日问题(不含答案)

+ */

+@Data

+public class LiveCompletionQuestionVO {

+

+    private Long id;

+

+    /** 题干 */

+    private String title;

+

+    /** 排序 */

+    private Long sort;

+

+    /** 类别 1单选 2多选 */

+    private Long type;

+

+    /** 选项(JSON) */

+    private String question;

+}


+ 5 - 2
fs-service/src/main/java/com/fs/live/vo/LiveQuestionLiveVO.java

@@ -10,10 +10,13 @@ import java.time.LocalDateTime;
 @Data
 public class LiveQuestionLiveVO {
     /**
-     * 主键ID
+     * 课题ID
      */
-    
     private Long id;
+    /**
+     * 关联表ID
+     */
+    private Long relationId;
     /**
      * 标题
      */

+ 3 - 0
fs-service/src/main/java/com/fs/live/vo/LiveVo.java

@@ -65,6 +65,9 @@ public class LiveVo {
     /** 是否开启直播完课积分功能 */
     private Boolean completionPointsEnabled;
 
+    /** 是否开启直播完课优惠券功能 */
+    private Boolean completionCouponEnabled;
+
     /** 今天是否已领取完课奖励 */
     private Boolean todayRewardReceived;
 

+ 43 - 0
fs-service/src/main/resources/db/20250610-直播课题.sql

@@ -0,0 +1,43 @@
+-- 直播课题题库
+DROP TABLE IF EXISTS `live_question_bank`;
+CREATE TABLE `live_question_bank` (
+    `id`           bigint       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `title`        varchar(500) NOT NULL COMMENT '题干',
+    `sort`         int          DEFAULT 0 COMMENT '排序',
+    `type`         tinyint      NOT NULL DEFAULT 1 COMMENT '题型 1单选 2多选',
+    `status`       tinyint      NOT NULL DEFAULT 1 COMMENT '状态 0停用 1启用',
+    `question`     text         COMMENT '选项JSON',
+    `answer`       text         COMMENT '答案',
+    `create_by`    varchar(64)  DEFAULT NULL COMMENT '创建人',
+    `create_time`  datetime     DEFAULT NULL COMMENT '创建时间',
+    `update_by`    varchar(64)  DEFAULT NULL COMMENT '更新人',
+    `update_time`  datetime     DEFAULT NULL COMMENT '更新时间',
+    `remark`       varchar(500) DEFAULT NULL COMMENT '备注',
+    PRIMARY KEY (`id`) USING BTREE,
+    KEY `idx_status` (`status`),
+    KEY `idx_sort` (`sort`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直播课题题库';
+
+-- 直播间关联课题
+DROP TABLE IF EXISTS `live_question_live`;
+CREATE TABLE `live_question_live` (
+    `id`           bigint      NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `live_id`      bigint      NOT NULL COMMENT '直播ID',
+    `question_id`  bigint      NOT NULL COMMENT '课题ID',
+    `sort`         int         DEFAULT 0 COMMENT '排序',
+    `create_by`    varchar(64) DEFAULT NULL COMMENT '创建人',
+    `create_time`  datetime    DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`) USING BTREE,
+    UNIQUE KEY `uk_live_question` (`live_id`, `question_id`),
+    KEY `idx_live_id` (`live_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='直播间关联课题';
+
+-- 菜单(parent_id 替换为「直播管理」菜单ID后执行)
+-- INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
+-- VALUES ('直播课题', 0, 10, 'liveQuestionBank', 'live/liveQuestionBank/index', 1, 0, 'C', '0', '0', 'live:liveQuestionBank:list', 'education', 'admin', NOW());
+-- SET @menuId = LAST_INSERT_ID();
+-- INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, create_by, create_time) VALUES
+-- ('直播课题查询', @menuId, 1, '#', '', 'F', '0', '0', 'live:liveQuestionBank:query', 'admin', NOW()),
+-- ('直播课题新增', @menuId, 2, '#', '', 'F', '0', '0', 'live:liveQuestionBank:add', 'admin', NOW()),
+-- ('直播课题修改', @menuId, 3, '#', '', 'F', '0', '0', 'live:liveQuestionBank:edit', 'admin', NOW()),
+-- ('直播课题删除', @menuId, 4, '#', '', 'F', '0', '0', 'live:liveQuestionBank:remove', 'admin', NOW());

+ 13 - 7
fs-service/src/main/resources/mapper/course/FsUserTalentFollowMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.course.mapper.FsUserTalentFollowMapper">
-    
+
     <resultMap type="FsUserTalentFollow" id="FsUserTalentFollowResult">
         <result property="id"    column="id"    />
         <result property="userId"    column="user_id"    />
@@ -18,21 +18,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectFsUserTalentFollowList" parameterType="FsUserTalentFollow" resultMap="FsUserTalentFollowResult">
         <include refid="selectFsUserTalentFollowVo"/>
-        <where>  
+        <where>
             <if test="userId != null "> and user_id = #{userId}</if>
             <if test="talentId != null "> and talent_id = #{talentId}</if>
         </where>
     </select>
-    
+
     <select id="selectFsUserTalentFollowById" parameterType="Long" resultMap="FsUserTalentFollowResult">
         <include refid="selectFsUserTalentFollowVo"/>
         where id = #{id}
     </select>
     <select id="queryFansCount" resultType="java.lang.Integer">
-        select count(user_id) from fs_user_talent_follow where talent_id = #{talentId}
+        SELECT count(t.talent_id)
+        FROM `fs_user_talent_follow` f
+                 LEFT JOIN fs_user_talent t ON f.talent_id = t.talent_id
+        WHERE f.talent_id = #{talentId} and t.is_del = 0
     </select>
     <select id="queryIdolCount" resultType="java.lang.Integer">
-        select count(talent_id) from fs_user_talent_follow where user_id = #{userId}
+        SELECT count(t.talent_id)
+        FROM `fs_user_talent_follow` f
+                 LEFT JOIN fs_user_talent t ON f.talent_id = t.talent_id
+        WHERE f.user_id = #{userId} and t.is_del = 0
     </select>
     <select id="selectFsUserTalentFansVoList" resultType="com.fs.course.vo.FsUserTalentFansVo">
         SELECT f.user_id,t.talent_id,t.nick_name,t.avatar,COUNT(f2.id) fans,count(v.video_id) video_num,
@@ -95,9 +101,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteFsUserTalentFollowByIds" parameterType="String">
-        delete from fs_user_talent_follow where id in 
+        delete from fs_user_talent_follow where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+</mapper>

+ 100 - 0
fs-service/src/main/resources/mapper/live/LiveQuestionBankMapper.xml

@@ -0,0 +1,100 @@
+<?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.LiveQuestionBankMapper">
+
+    <resultMap type="com.fs.live.domain.LiveQuestionBank" id="LiveQuestionBankResult">
+        <result property="id" column="id"/>
+        <result property="title" column="title"/>
+        <result property="sort" column="sort"/>
+        <result property="type" column="type"/>
+        <result property="status" column="status"/>
+        <result property="question" column="question"/>
+        <result property="answer" column="answer"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="remark" column="remark"/>
+    </resultMap>
+
+    <sql id="selectVo">
+        select id, title, sort, type, status, question, answer, create_by, create_time, update_by, update_time, remark
+        from live_question_bank
+    </sql>
+
+    <select id="selectLiveQuestionBankList" parameterType="com.fs.live.domain.LiveQuestionBank" resultMap="LiveQuestionBankResult">
+        <include refid="selectVo"/>
+        <where>
+            <if test="title != null and title != ''">and title like concat('%', #{title}, '%')</if>
+            <if test="type != null">and type = #{type}</if>
+            <if test="status != null">and status = #{status}</if>
+        </where>
+        order by sort, id
+    </select>
+
+    <select id="selectLiveQuestionBankById" parameterType="Long" resultMap="LiveQuestionBankResult">
+        <include refid="selectVo"/>
+        where id = #{id}
+    </select>
+
+    <select id="selectLiveQuestionBankByIds" resultMap="LiveQuestionBankResult">
+        <include refid="selectVo"/>
+        where id in
+        <foreach collection="list" item="item" open="(" separator="," close=")">
+            #{item}
+        </foreach>
+    </select>
+
+    <insert id="insertLiveQuestionBank" parameterType="com.fs.live.domain.LiveQuestionBank" useGeneratedKeys="true" keyProperty="id">
+        insert into live_question_bank
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="title != null">title,</if>
+            <if test="sort != null">sort,</if>
+            <if test="type != null">type,</if>
+            <if test="status != null">status,</if>
+            <if test="question != null">question,</if>
+            <if test="answer != null">answer,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="remark != null">remark,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="title != null">#{title},</if>
+            <if test="sort != null">#{sort},</if>
+            <if test="type != null">#{type},</if>
+            <if test="status != null">#{status},</if>
+            <if test="question != null">#{question},</if>
+            <if test="answer != null">#{answer},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateLiveQuestionBank" parameterType="com.fs.live.domain.LiveQuestionBank">
+        update live_question_bank
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="title != null">title = #{title},</if>
+            <if test="sort != null">sort = #{sort},</if>
+            <if test="type != null">type = #{type},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="question != null">question = #{question},</if>
+            <if test="answer != null">answer = #{answer},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="remark != null">remark = #{remark},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteLiveQuestionBankById" parameterType="Long">
+        delete from live_question_bank where id = #{id}
+    </delete>
+
+    <delete id="deleteLiveQuestionBankByIds" parameterType="String">
+        delete from live_question_bank where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+</mapper>

+ 57 - 0
fs-service/src/main/resources/mapper/live/LiveQuestionLiveMapper.xml

@@ -0,0 +1,57 @@
+<?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.LiveQuestionLiveMapper">
+
+    <select id="selectLiveQuestionLiveList" resultType="com.fs.live.vo.LiveQuestionLiveVO">
+        select b.id,
+               b.title,
+               b.type,
+               lql.sort,
+               lql.create_time as createTime,
+               lql.create_by as createBy,
+               lql.id as relationId
+        from live_question_live lql
+        inner join live_question_bank b on lql.question_id = b.id
+        where lql.live_id = #{liveId}
+        order by lql.sort, lql.id
+    </select>
+
+    <select id="selectOptionList" parameterType="com.fs.live.domain.LiveQuestionBank" resultType="com.fs.live.vo.LiveQuestionLiveVO">
+        select b.id,
+               b.title,
+               b.type,
+               b.sort,
+               b.create_time as createTime,
+               b.create_by as createBy
+        from live_question_bank b
+        where b.status = 1
+        <if test="title != null and title != ''">
+            and b.title like concat('%', #{title}, '%')
+        </if>
+        <if test="excludeLiveId != null">
+            and b.id not in (
+                select question_id from live_question_live where live_id = #{excludeLiveId}
+            )
+        </if>
+        order by b.sort, b.id
+    </select>
+
+    <insert id="insertLiveQuestionLive" parameterType="com.fs.live.domain.LiveQuestionLive" useGeneratedKeys="true" keyProperty="id">
+        insert into live_question_live (live_id, question_id, sort, create_by, create_time)
+        values (#{liveId}, #{questionId}, #{sort}, #{createBy}, #{createTime})
+    </insert>
+
+    <delete id="deleteLiveQuestionLiveByIds">
+        delete from live_question_live
+        where live_id = #{liveId}
+        and id in
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="countByLiveIdAndQuestionId" resultType="int">
+        select count(1) from live_question_live
+        where live_id = #{liveId} and question_id = #{questionId}
+    </select>
+</mapper>

+ 1 - 1
fs-user-app/src/main/java/com/fs/app/controller/VideoController.java

@@ -55,7 +55,7 @@ public class VideoController extends  AppBaseController{
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsUserVideoListUVO> list= videoService.selectFsUserVideoListUVO(param);
         //添加假数据
-        list = videoService.addNum(list);
+//        list = videoService.addNum(list);
         PageInfo<FsUserVideoListUVO> listPageInfo=new PageInfo<>(list);
         if (param.getUserId() != null) {
             // 对分页后的数据进行推荐排序

+ 50 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionCouponController.java

@@ -0,0 +1,50 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.R;
+import com.fs.live.param.LiveCompletionCouponAnswerParam;
+import com.fs.live.service.ILiveCompletionCouponService;
+import com.fs.live.vo.LiveCompletionCouponStatusVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 直播完课优惠券Controller
+ */
+@RestController
+@RequestMapping("/app/live/completion/coupon")
+public class LiveCompletionCouponController extends AppBaseController {
+
+    @Autowired
+    private ILiveCompletionCouponService completionCouponService;
+
+    /**
+     * 查询完课优惠券状态及今日问题(观看完视频后前端轮询或主动调用)
+     */
+    @GetMapping("/status")
+    public R status(@RequestParam Long liveId, @RequestParam(required = false) Long watchDuration) {
+        Long userId = Long.parseLong(getUserId());
+        LiveCompletionCouponStatusVO status = completionCouponService.getCompletionCouponStatus(liveId, userId, watchDuration);
+        return R.ok().put("data", status);
+    }
+
+    /**
+     * 获取今日问题列表
+     */
+    @GetMapping("/questions")
+    public R questions(@RequestParam Long liveId) {
+        return R.ok().put("data", completionCouponService.getCompletionQuestions(liveId));
+    }
+
+    /**
+     * 提交今日问题,答对后发放福利券
+     */
+    @PostMapping("/answer")
+    @RepeatSubmit
+    public R answer(@RequestBody LiveCompletionCouponAnswerParam param) {
+        Long userId = Long.parseLong(getUserId());
+        completionCouponService.submitAnswerAndIssueCoupon(param, userId);
+        return R.ok("恭喜您,福利券已到账");
+    }
+}