3 Komitmen 506f1af989 ... c9d690a09c

Pembuat SHA1 Pesan Tanggal
  yys c9d690a09c 1、直播定时处理下 1 Minggu lalu
  yys be854c0344 1、定时完课发送优惠卷 1 Minggu lalu
  yys c473a5fb13 1、新增直播答题模块,修改完课优惠卷答题页面 1 Minggu lalu
68 mengubah file dengan 1743 tambahan dan 287 penghapusan
  1. 6 3
      fs-admin/src/main/java/com/fs/his/controller/FsInquiryOrderController.java
  2. 5 0
      fs-admin/src/main/java/com/fs/his/controller/FsStoreOrderController.java
  3. 3 0
      fs-admin/src/main/java/com/fs/his/controller/FsUserController.java
  4. 3 2
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  5. 1 1
      fs-admin/src/main/java/com/fs/live/controller/LiveHealthOrderController.java
  6. 75 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionBankController.java
  7. 57 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java
  8. 7 6
      fs-admin/src/main/java/com/fs/live/controller/LiveWatchConfigController.java
  9. 30 0
      fs-common/src/main/java/com/fs/common/utils/OwnershipAssert.java
  10. 5 1
      fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java
  11. 1 18
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  12. 11 0
      fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java
  13. 2 1
      fs-doctor-app/src/main/java/com/fs/app/config/WebMvcConfig.java
  14. 31 3
      fs-doctor-app/src/main/java/com/fs/app/controller/FsUserInformationCollectionController.java
  15. 36 0
      fs-doctor-app/src/main/java/com/fs/app/controller/InquiryOrderController.java
  16. 0 22
      fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java
  17. 49 0
      fs-live-app/src/main/java/com/fs/live/controller/LiveRewardTaskController.java
  18. 23 0
      fs-live-app/src/main/java/com/fs/live/param/LiveCompletionCouponTriggerParam.java
  19. 23 0
      fs-live-app/src/main/java/com/fs/live/param/LiveWatchRewardCouponTriggerParam.java
  20. 60 5
      fs-live-app/src/main/java/com/fs/live/task/LiveCompletionPointsTask.java
  21. 46 14
      fs-live-app/src/main/java/com/fs/live/task/Task.java
  22. 24 58
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  23. 1 1
      fs-live-app/src/main/resources/application.yml
  24. 31 16
      fs-service/src/main/java/com/fs/course/mapper/FsUserVideoMapper.java
  25. 1 1
      fs-service/src/main/java/com/fs/course/param/FsUserVideoCommentDelUParam.java
  26. 1 1
      fs-service/src/main/java/com/fs/course/service/impl/FsUserTalentServiceImpl.java
  27. 14 5
      fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoCommentServiceImpl.java
  28. 4 0
      fs-service/src/main/java/com/fs/course/vo/FsUserVideoListUVO.java
  29. 45 0
      fs-service/src/main/java/com/fs/live/domain/LiveQuestionBank.java
  30. 23 0
      fs-service/src/main/java/com/fs/live/domain/LiveQuestionLive.java
  31. 7 1
      fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java
  32. 0 1
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  33. 26 0
      fs-service/src/main/java/com/fs/live/mapper/LiveQuestionBankMapper.java
  34. 24 0
      fs-service/src/main/java/com/fs/live/mapper/LiveQuestionLiveMapper.java
  35. 17 0
      fs-service/src/main/java/com/fs/live/param/LiveCompletionCouponAnswerParam.java
  36. 47 5
      fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java
  37. 2 0
      fs-service/src/main/java/com/fs/live/service/ILiveCouponIssueService.java
  38. 23 0
      fs-service/src/main/java/com/fs/live/service/ILiveQuestionBankService.java
  39. 19 0
      fs-service/src/main/java/com/fs/live/service/ILiveQuestionLiveService.java
  40. 4 4
      fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java
  41. 347 51
      fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java
  42. 5 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCouponIssueServiceImpl.java
  43. 53 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionBankServiceImpl.java
  44. 67 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveQuestionLiveServiceImpl.java
  45. 8 1
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  46. 21 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponConfigVO.java
  47. 22 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponInfoVO.java
  48. 24 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponNotifyResult.java
  49. 27 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponStatusVO.java
  50. 24 0
      fs-service/src/main/java/com/fs/live/vo/LiveCompletionQuestionVO.java
  51. 5 2
      fs-service/src/main/java/com/fs/live/vo/LiveQuestionLiveVO.java
  52. 3 0
      fs-service/src/main/java/com/fs/live/vo/LiveVo.java
  53. 4 15
      fs-service/src/main/java/com/fs/utils/SensitiveDataUtils.java
  54. 43 0
      fs-service/src/main/resources/db/20250610-直播课题.sql
  55. 13 7
      fs-service/src/main/resources/mapper/course/FsUserTalentFollowMapper.xml
  56. 13 0
      fs-service/src/main/resources/mapper/course/FsUserVideoMapper.xml
  57. 14 14
      fs-service/src/main/resources/mapper/live/LiveCouponUserMapper.xml
  58. 100 0
      fs-service/src/main/resources/mapper/live/LiveQuestionBankMapper.xml
  59. 57 0
      fs-service/src/main/resources/mapper/live/LiveQuestionLiveMapper.xml
  60. 2 1
      fs-user-app/src/main/java/com/fs/app/config/WebMvcConfig.java
  61. 6 12
      fs-user-app/src/main/java/com/fs/app/controller/InquiryOrderController.java
  62. 0 6
      fs-user-app/src/main/java/com/fs/app/controller/PackageOrderController.java
  63. 35 7
      fs-user-app/src/main/java/com/fs/app/controller/StoreOrderController.java
  64. 0 1
      fs-user-app/src/main/java/com/fs/app/controller/UserVipController.java
  65. 1 1
      fs-user-app/src/main/java/com/fs/app/controller/VideoController.java
  66. 50 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveCompletionCouponController.java
  67. 6 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java
  68. 6 0
      fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

+ 6 - 3
fs-admin/src/main/java/com/fs/his/controller/FsInquiryOrderController.java

@@ -54,6 +54,7 @@ public class FsInquiryOrderController extends BaseController
     /**
      * 查询问诊订单列表
      */
+    @PreAuthorize("@ss.hasPermi('his:inquiryOrder:list')")
     @GetMapping("/list")
    public TableDataInfo list(FsInquiryOrderParam fsInquiryOrder)
     {
@@ -78,9 +79,6 @@ public class FsInquiryOrderController extends BaseController
         if(!StringUtils.isEmpty(fsInquiryOrder.getPayTimeRange())){
             fsInquiryOrder.setPayTimeList(fsInquiryOrder.getPayTimeRange().split("--"));
         }
-        if (getUserId().equals(54L)||getUserId().equals(211L)){
-            fsInquiryOrder.setCompanyId(188L);
-        }
         List<FsInquiryOrderListVO> list = fsInquiryOrderService.selectFsInquiryOrderVOList(fsInquiryOrder);
         return getDataTable(list);
     }
@@ -197,6 +195,9 @@ public class FsInquiryOrderController extends BaseController
     public AjaxResult getInfo(@PathVariable("orderId") Long orderId)
     {
         FsInquiryOrderVO fsInquiryOrderVO = fsInquiryOrderService.selectFsInquiryOrderVOByOrderId(orderId);
+        if (fsInquiryOrderVO == null) {
+            return AjaxResult.error("订单不存在");
+        }
         String patientJson = fsInquiryOrderVO.getPatientJson();
         if (patientJson!=null&&patientJson!=""){
             Map<String,String> parse = (Map<String,String>) JSON.parse(patientJson);
@@ -288,6 +289,7 @@ public class FsInquiryOrderController extends BaseController
 
 
 
+    @PreAuthorize("@ss.hasPermi('his:inquiryOrder:query')")
     @GetMapping("/msgList")
     public TableDataInfo msgList(Long orderId)
     {
@@ -300,6 +302,7 @@ public class FsInquiryOrderController extends BaseController
     /**
      * 查询订单log列表
      */
+    @PreAuthorize("@ss.hasPermi('his:inquiryOrder:query')")
     @GetMapping("/logList/{orderId}")
     public TableDataInfo logList(@PathVariable("orderId") String orderId)
     {

+ 5 - 0
fs-admin/src/main/java/com/fs/his/controller/FsStoreOrderController.java

@@ -136,6 +136,7 @@ public class FsStoreOrderController extends BaseController
     /**
      * 查询订单列表
      */
+    @PreAuthorize("@ss.hasPermi('his:storeOrder:list')")
     @PostMapping("/list")
     public FsStoreOrderListAndStatisticsVo list(@RequestBody FsStoreOrderParam fsStoreOrder)
     {
@@ -396,9 +397,13 @@ public class FsStoreOrderController extends BaseController
     /**
      * 获取订单详细信息
      */
+    @PreAuthorize("@ss.hasPermi('his:storeOrder:query')")
     @GetMapping(value = "/{orderId}")
     public R getInfo(@PathVariable("orderId") Long orderId) throws ParseException {
         FsStoreOrderVO order = fsStoreOrderService.selectFsStoreOrderByOrderIdVO(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
         if (order.getPhone() != null && order.getPhone().length() > 11) {
             order.setPhone(decryptPhoneMk(order.getPhone()));
         } else {

+ 3 - 0
fs-admin/src/main/java/com/fs/his/controller/FsUserController.java

@@ -249,6 +249,7 @@ public class FsUserController extends BaseController
     /**
      * 获取用户详细信息
      */
+    @PreAuthorize("@ss.hasPermi('his:user:query')")
     @GetMapping(value = "/{userId}")
     public AjaxResult getInfo(@PathVariable("userId") Long userId)
     {
@@ -257,6 +258,7 @@ public class FsUserController extends BaseController
         return AjaxResult.success(fsUser);
     }
 
+    @PreAuthorize("@ss.hasPermi('his:user:query')")
     @GetMapping(value = "/getUserAddr/{userId}")
     public AjaxResult getUserAddr(@PathVariable("userId") Long userId)
     {
@@ -334,6 +336,7 @@ public class FsUserController extends BaseController
         return getDataTable(list);
     }
 
+    @PreAuthorize("@ss.hasPermi('his:user:list')")
     @GetMapping("/listBySearch")
     public R listBySearch(FsUser user)
     {

+ 3 - 2
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -970,6 +970,7 @@ public class FsStoreOrderScrmController extends BaseController {
      * @param phoneList
      * @return
      */
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:erpphone')")
     @PostMapping(value = "/saveErpPhone")
     public AjaxResult saveErpPhone(@RequestBody List<String> phoneList)
     {
@@ -983,7 +984,7 @@ public class FsStoreOrderScrmController extends BaseController {
     /**
      * 批量设置erp手机号
      */
-    @PreAuthorize("@ss.hasPermi('his:storeOrder:createErpOrder')")
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:erpphone')")
     @Log(title = "订单", businessType = BusinessType.UPDATE)
     @PostMapping("/editErpPhone")
     public AjaxResult editErpPhone(@RequestBody FsStoreOrderScrmSetErpPhoneParam param)
@@ -1012,7 +1013,7 @@ public class FsStoreOrderScrmController extends BaseController {
 
     @Log(title = "手动推管易", businessType = BusinessType.INSERT)
     @ApiOperation("批量创建ERP订单")
-    @PreAuthorize("@ss.hasPermi('his:storeOrder:createErpOrder')")
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:createErpOrder')")
     @PostMapping(value = "/batchCreateErpOrder")
     public R batchCreateErpOrder(@RequestBody FsStoreOrderScrmSetErpPhoneParam param)
     {

+ 1 - 1
fs-admin/src/main/java/com/fs/live/controller/LiveHealthOrderController.java

@@ -34,7 +34,7 @@ import org.springframework.web.multipart.MultipartFile;
 import java.math.BigDecimal;
 import java.util.*;
 
-/**`
+/**
  * 订单Controller
  *
  * @author fs

+ 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));

+    }

+}


+ 7 - 6
fs-admin/src/main/java/com/fs/live/controller/LiveWatchConfigController.java

@@ -11,6 +11,7 @@ import com.fs.framework.web.service.TokenService;
 import com.fs.live.domain.LiveWatchConfig;
 import com.fs.live.service.ILiveWatchConfigService;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -33,7 +34,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 查询直播观看奖励设置列表
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:list')")
+    @PreAuthorize("@ss.hasPermi('live:config:list')")
     @GetMapping("/list")
     public TableDataInfo list(LiveWatchConfig liveWatchConfig)
     {
@@ -45,7 +46,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 导出直播观看奖励设置列表
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:export')")
+    @PreAuthorize("@ss.hasPermi('live:config:export')")
     @Log(title = "直播观看奖励设置", businessType = BusinessType.EXPORT)
     @GetMapping("/export")
     public AjaxResult export(LiveWatchConfig liveWatchConfig)
@@ -58,7 +59,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 获取直播观看奖励设置详细信息
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:query')")
+    @PreAuthorize("@ss.hasPermi('live:config:query')")
     @GetMapping(value = "/{id}")
     public AjaxResult getInfo(@PathVariable("id") Long id)
     {
@@ -68,7 +69,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 新增直播观看奖励设置
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:add')")
+    @PreAuthorize("@ss.hasPermi('live:config:add')")
     @Log(title = "直播观看奖励设置", businessType = BusinessType.INSERT)
     @PostMapping
     public AjaxResult add(@RequestBody String jsonConfig,@RequestParam(value = "liveId") Long liveId)
@@ -80,7 +81,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 修改直播观看奖励设置
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:edit')")
+    @PreAuthorize("@ss.hasPermi('live:config:edit')")
     @Log(title = "直播观看奖励设置", businessType = BusinessType.UPDATE)
     @PutMapping
     public AjaxResult edit(@RequestBody String jsonConfig,@RequestParam(value = "liveId") Long liveId)
@@ -91,7 +92,7 @@ public class LiveWatchConfigController extends BaseController
     /**
      * 删除直播观看奖励设置
      */
-//    @PreAuthorize("@ss.hasPermi('live:config:remove')")
+    @PreAuthorize("@ss.hasPermi('live:config:remove')")
     @Log(title = "直播观看奖励设置", businessType = BusinessType.DELETE)
 	@DeleteMapping("/{ids}")
     public AjaxResult remove(@PathVariable Long[] ids)

+ 30 - 0
fs-common/src/main/java/com/fs/common/utils/OwnershipAssert.java

@@ -0,0 +1,30 @@
+package com.fs.common.utils;
+
+/**
+ * ×ÊÔ´¹éÊôУÑ鹤¾ß
+ */
+public final class OwnershipAssert {
+
+    private OwnershipAssert() {
+    }
+
+    public static boolean isSameUser(Long resourceUserId, Long currentUserId) {
+        return resourceUserId != null && currentUserId != null && resourceUserId.equals(currentUserId);
+    }
+
+    public static boolean isSameCompany(Long resourceCompanyId, Long currentCompanyId) {
+        if (currentCompanyId == null) {
+            return true;
+        }
+        return resourceCompanyId != null && resourceCompanyId.equals(currentCompanyId);
+    }
+
+    public static boolean isSameDoctor(Long resourceDoctorId, Long currentDoctorId) {
+        return resourceDoctorId != null && currentDoctorId != null && resourceDoctorId.equals(currentDoctorId);
+    }
+
+    public static boolean isSameCompanyUser(Long resourceCompanyUserId, Long currentCompanyUserId) {
+        return resourceCompanyUserId != null && currentCompanyUserId != null
+                && resourceCompanyUserId.equals(currentCompanyUserId);
+    }
+}

+ 5 - 1
fs-company/src/main/java/com/fs/company/controller/store/FsUserController.java

@@ -89,9 +89,12 @@ public class FsUserController extends BaseController
         }
         return getDataTable(list);
     }
+    @PreAuthorize("@ss.hasPermi('his:user:list')")
     @GetMapping("/getUserList")
     public R getUserList( FsUser fsUser)
     {
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsUser.setCompanyId(loginUser.getCompany().getCompanyId());
         fsUser.setIsDel(0);
         List<FsUser> list=fsUserService.selectFsUserList(fsUser);
         if(list.isEmpty()){
@@ -109,7 +112,8 @@ public class FsUserController extends BaseController
     public TableDataInfo userList(FsUserParam fsUser)
     {
         startPage();
-
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        fsUser.setCompanyId(loginUser.getCompany().getCompanyId());
 
         fsUser.setPhone(encryptPhone(fsUser.getPhone()));
 

+ 1 - 18
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -100,7 +100,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 // 过滤请求
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
-                .antMatchers("/chat/upload/**","/login", "/register", "/captchaImage","/checkIsNeedCheck","/getWechatQrCode","/checkWechatScan","/callback").anonymous()
+                .antMatchers("/login", "/register", "/captchaImage","/checkIsNeedCheck","/getWechatQrCode","/checkWechatScan","/callback").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
                         "/",
@@ -110,29 +110,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                         "/**/*.js",
                         "/profile/**"
                 ).permitAll()
-                .antMatchers("/test").anonymous()
                 .antMatchers("**/callerResult").anonymous()
                 .antMatchers("/qw/getJsapiTicket/**").anonymous()
                 .antMatchers("/msg/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()
-                .antMatchers("/msg/**/**").anonymous()
-                .antMatchers("/msg").anonymous()
-                .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/company/user/common/uploadOSS").anonymous()
                 .antMatchers("/pay/wxPay/payNotify**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/test").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
-                .antMatchers("/swagger-ui.html").anonymous()
-                .antMatchers("/swagger-resources/**").anonymous()
-                .antMatchers("/webjars/**").anonymous()
-                .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
                 .antMatchers("/qw/data/**").anonymous()
-                .antMatchers("/qw/user/selectCloudByCompany").anonymous()
-                .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 .antMatchers("/app/common/callbackAfterSendSingleMsgCommand").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()

+ 11 - 0
fs-company/src/main/java/com/fs/hisStore/controller/FsStoreOrderScrmController.java

@@ -11,6 +11,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
 import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.OwnershipAssert;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.StringUtils;
@@ -107,9 +108,12 @@ public class FsStoreOrderScrmController extends BaseController
     }
 
 
+    @PreAuthorize("@ss.hasPermi('store:storeOrder:list')")
     @GetMapping("/allList")
     public TableDataInfo allList(FsStoreOrderParam param)
     {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
         startPage();
         if(!StringUtils.isEmpty(param.getCreateTimeRange())){
             param.setCreateTimeList(param.getCreateTimeRange().split("--"));
@@ -216,6 +220,13 @@ public class FsStoreOrderScrmController extends BaseController
     public R getInfo(@PathVariable("id") Long id)
     {
         FsStoreOrderScrm order=fsStoreOrderService.selectFsStoreOrderById(id);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        if (!OwnershipAssert.isSameCompany(order.getCompanyId(), loginUser.getCompany().getCompanyId())) {
+            return R.error("无权访问");
+        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseAddress(order.getUserAddress()));
         FsUser user=userService.selectFsUserById(order.getUserId());

+ 2 - 1
fs-doctor-app/src/main/java/com/fs/app/config/WebMvcConfig.java

@@ -19,7 +19,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**");
+        registry.addInterceptor(authorizationInterceptor)
+                .addPathPatterns("/app/**", "/user/**");
     }
 //
 //    @Override

+ 31 - 3
fs-doctor-app/src/main/java/com/fs/app/controller/FsUserInformationCollectionController.java

@@ -1,6 +1,8 @@
 package com.fs.app.controller;
 
+import com.fs.app.annotation.Login;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.OwnershipAssert;
 import com.fs.his.domain.FsUserInformationCollection;
 import com.fs.his.param.UserInformationDoctorType2Param;
 import com.fs.his.service.IFsUserInformationCollectionService;
@@ -16,17 +18,36 @@ import java.util.List;
 public class FsUserInformationCollectionController extends  AppBaseController {
     @Autowired
     private IFsUserInformationCollectionService fsUserInformationCollectionService;
+
+    @Login
     @GetMapping("/getUserInformation")
     public R getUserInformation(@RequestParam("id") Long id) {
-
+        FsUserInformationCollection collection = fsUserInformationCollectionService.selectFsUserInformationCollectionById(id);
+        if (collection == null) {
+            return R.error("记录不存在");
+        }
+        Long doctorId = Long.parseLong(getDoctorId());
+        if (!OwnershipAssert.isSameDoctor(collection.getDoctorId(), doctorId)
+                && !OwnershipAssert.isSameDoctor(collection.getDoctorType2Id(), doctorId)) {
+            return R.error("无权查看该记录");
+        }
         return R.ok().put("data", fsUserInformationCollectionService.selectFsUserInformationCollectionVoById(id));
     }
-    //医生确认
+
+    @Login
     @PostMapping("/doctorConfirm")
     public R doctorConfirm(@RequestBody FsUserInformationCollection collection){
+        FsUserInformationCollection existing = fsUserInformationCollectionService.selectFsUserInformationCollectionById(collection.getId());
+        if (existing == null) {
+            return R.error("记录不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(existing.getDoctorId(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权确认该记录");
+        }
         return fsUserInformationCollectionService.doctorConfirm(collection);
     }
 
+    @Login
     @GetMapping("/getCollectionList")
     private R getCollectionList(UserInformationDoctorType2Param userInformationDoctorType2Param) {
 
@@ -47,9 +68,16 @@ public class FsUserInformationCollectionController extends  AppBaseController {
     }
 
 
-    //药师确认
+    @Login
     @PostMapping("/doctorType2Confirm")
     public R doctorType2Confirm(@RequestBody FsUserInformationCollection collection){
+        FsUserInformationCollection existing = fsUserInformationCollectionService.selectFsUserInformationCollectionById(collection.getId());
+        if (existing == null) {
+            return R.error("记录不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(existing.getDoctorType2Id(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权确认该记录");
+        }
         return fsUserInformationCollectionService.doctorType2Confirm(collection);
     }
 }

+ 36 - 0
fs-doctor-app/src/main/java/com/fs/app/controller/InquiryOrderController.java

@@ -11,6 +11,7 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.domain.R;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.enums.ImTypeEnum;
+import com.fs.common.utils.OwnershipAssert;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.company.service.ICompanyService;
@@ -225,9 +226,17 @@ public class InquiryOrderController extends  AppBaseController {
 
 
 
+    @Login
     @PutMapping("/updateRemark")
     public R edit(@RequestBody FsInquiryOrder fsInquiryOrder)
     {
+        FsInquiryOrder order = inquiryOrderService.selectFsInquiryOrderByOrderId(fsInquiryOrder.getOrderId());
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(order.getDoctorId(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权操作该订单");
+        }
         FsInquiryOrder o = new FsInquiryOrder();
         o.setOrderId(fsInquiryOrder.getOrderId());
         o.setDoctorRemark(fsInquiryOrder.getDoctorRemark());
@@ -343,9 +352,21 @@ public class InquiryOrderController extends  AppBaseController {
         return R.ok("操作成功");
     }
 
+    @Login
     @GetMapping("/getInquiryOrderMsgList")
     public R getInquiryOrderMsgList(FsInquiryOrderMsgListDParam param)
     {
+        if (param.getOrderId() == null) {
+            return R.error("订单ID不能为空");
+        }
+        FsInquiryOrder order = inquiryOrderService.selectFsInquiryOrderByOrderId(param.getOrderId());
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(order.getDoctorId(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权查看该订单消息");
+        }
+        param.setDoctorId(Long.parseLong(getDoctorId()));
         PageHelper.startPage(param.getPageNum(), param.getPageSize());
         List<FsInquiryOrderMsgListDVO> list = inquiryOrderMsgService.selectFsInquiryOrderMsgListDVO(param);
         PageInfo<FsInquiryOrderMsgListDVO> listPageInfo=new PageInfo<>(list);
@@ -353,11 +374,18 @@ public class InquiryOrderController extends  AppBaseController {
     }
 
 
+    @Login
     @GetMapping(value = "/queryPhone/{orderId}")
     @Log(title = "查看电话", businessType = BusinessType.GRANT)
     public R getPhone(@PathVariable("orderId") Long orderId)
     {
         FsInquiryOrder fsInquiryOrder = inquiryOrderService.selectFsInquiryOrderByOrderId(orderId);
+        if (fsInquiryOrder == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(fsInquiryOrder.getDoctorId(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权查看该订单电话");
+        }
         String patientJson = fsInquiryOrder.getPatientJson();
         if (patientJson != null&&!"".equals(patientJson)) {
             FsInquiryOrderPatientDTO fsInquiryOrderPatientDTO = JSON.parseObject(patientJson, FsInquiryOrderPatientDTO.class);
@@ -371,8 +399,16 @@ public class InquiryOrderController extends  AppBaseController {
         return R.ok().put("data","");
     }
 
+    @Login
     @PostMapping("/closeOrder")
     public R closeOrder(@RequestBody Long orderId){
+        FsInquiryOrder order = inquiryOrderService.selectFsInquiryOrderByOrderId(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameDoctor(order.getDoctorId(), Long.parseLong(getDoctorId()))) {
+            return R.error("无权关闭该订单");
+        }
         inquiryOrderService.closeOrder(orderId);
         logger.info("closeOrder: {}", orderId);
         return R.ok();

+ 0 - 22
fs-framework/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -98,7 +98,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .authorizeRequests()
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                 .antMatchers("/login", "/register", "/captchaImage","/getWechatQrCode","/checkWechatScan","/callback","/checkIsNeedCheck","/api/open/kntAiExpress").anonymous()
-                .antMatchers("/app/common/test").anonymous()
                 .antMatchers("/ad/adDyApi/authorized").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
@@ -111,15 +110,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 ).permitAll()
                 .antMatchers("/baidu/**").anonymous()
                 .antMatchers("/baiduBack/**").anonymous()
-                .antMatchers("/test/gtp/*").anonymous()
-                .antMatchers("common/getTask/*").anonymous()
-                .antMatchers("//his/data/endFollow/*").anonymous()
-                .antMatchers("//his/data/end/*").anonymous()
-                .antMatchers("//his/data/addCF/*").anonymous()
-                .antMatchers("//his/data/addCom/*").anonymous()
-                .antMatchers("//his/data/testSendSub/*").anonymous()
-                .antMatchers("//his/data/test/*").anonymous()
-                .antMatchers("//his/data/Follow/*").anonymous()
                 .antMatchers("/company/companyVoiceRobotic/callerResult").anonymous()
                 .antMatchers("/qw/data/*").anonymous()
                 .antMatchers("/app/common/expressNotify").anonymous()
@@ -131,18 +121,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/huFu/*").anonymous()
                 .antMatchers("/tzPay/*").anonymous()
                 .antMatchers("//his/pay/*").anonymous()
-                .antMatchers("/common/getId**").anonymous()
-                .antMatchers("/common/uploadOSS**").anonymous()
-                .antMatchers("/chat/upload/uploadFile**").anonymous()
-                .antMatchers("/common/uploadWang**").anonymous()
-                .antMatchers("/common/download**").anonymous()
-                .antMatchers("/common/download/resource**").anonymous()
-                .antMatchers("/common/unbindQwUserByServerIds").anonymous()
-                .antMatchers("/swagger-ui.html").anonymous()
-                .antMatchers("/swagger-resources/**").anonymous()
-                .antMatchers("/webjars/**").anonymous()
-                .antMatchers("/*/api-docs").anonymous()
-                .antMatchers("/druid/**").anonymous()
                 .antMatchers("/course/userVideo/videoTranscode").anonymous()
                 .antMatchers("/erp/call/**").anonymous()
                 // 除上面外的所有请求全部需要鉴权认证

+ 49 - 0
fs-live-app/src/main/java/com/fs/live/controller/LiveRewardTaskController.java

@@ -0,0 +1,49 @@
+package com.fs.live.controller;
+
+import com.fs.common.core.domain.R;
+import com.fs.live.param.LiveCompletionCouponTriggerParam;
+import com.fs.live.param.LiveWatchRewardCouponTriggerParam;
+import com.fs.live.task.LiveCompletionPointsTask;
+import com.fs.live.task.Task;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 直播奖励定时任务手动触发接口(测试/补发)
+ */
+@Api("直播奖励任务手动触发")
+@RestController
+@AllArgsConstructor
+@RequestMapping("/ws/app/live/reward/task")
+public class LiveRewardTaskController {
+
+    private final LiveCompletionPointsTask liveCompletionPointsTask;
+    private final Task liveTask;
+
+    /**
+     * 手动触发完课优惠券「今日问题」弹窗(等同 checkCompletionCouponStatus 单用户逻辑,强制 WebSocket 推送)
+     */
+    @ApiOperation("手动触发完课优惠券今日问题弹窗")
+    @GetMapping("/triggerCompletionCoupon")
+    public R triggerCompletionCoupon(LiveCompletionCouponTriggerParam param) {
+        if (param == null || param.getLiveId() == null || param.getUserId() == null) {
+            return R.error("liveId、userId 不能为空");
+        }
+        return liveCompletionPointsTask.triggerCompletionCouponQuestion(
+                param.getLiveId(), param.getUserId(), param.getWatchDuration());
+    }
+
+    /**
+     * 手动触发观看奖励优惠券发放(等同 autoUpdateWatchReward 优惠券分支单用户逻辑)
+     */
+    @ApiOperation("手动触发观看奖励优惠券发放")
+    @GetMapping("/triggerWatchRewardCoupon")
+    public R triggerWatchRewardCoupon(LiveWatchRewardCouponTriggerParam param) {
+        if (param == null || param.getLiveId() == null || param.getUserId() == null) {
+            return R.error("liveId、userId 不能为空");
+        }
+        return liveTask.triggerWatchRewardCoupon(param.getLiveId(), param.getUserId(), param.getCouponId());
+    }
+}

+ 23 - 0
fs-live-app/src/main/java/com/fs/live/param/LiveCompletionCouponTriggerParam.java

@@ -0,0 +1,23 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 手动触发完课优惠券「今日问题」弹窗参数
+ */
+@Data
+public class LiveCompletionCouponTriggerParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 直播间ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 观看时长(秒),不传则按数据库累计观看时长校验 */
+    private Long watchDuration;
+}

+ 23 - 0
fs-live-app/src/main/java/com/fs/live/param/LiveWatchRewardCouponTriggerParam.java

@@ -0,0 +1,23 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 手动触发观看奖励优惠券发放参数
+ */
+@Data
+public class LiveWatchRewardCouponTriggerParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 直播间ID */
+    private Long liveId;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 优惠券ID,不传则读取直播间观看奖励配置中的 actionCouponId */
+    private Long couponId;
+}

+ 60 - 5
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;
@@ -20,6 +25,8 @@ import java.util.Map;
 @Component
 public class LiveCompletionPointsTask {
 
+    private static final String WATCH_DURATION_HASH_PREFIX = "live:watch:duration:hash:";
+
     @Autowired
     private RedisCache redisCache;
 
@@ -32,6 +39,9 @@ public class LiveCompletionPointsTask {
     @Autowired
     private ILiveService liveService;
 
+    @Autowired
+    private WebSocketServer webSocketServer;
+
     /**
      * 定时检查观看时长并创建完课积分记录(兜底机制)
      * 每分钟执行一次
@@ -55,8 +65,8 @@ public class LiveCompletionPointsTask {
     }
 
     /**
-     * 定时检查观看时长并发放完课优惠券(兜底机制
-     * 每分钟执行一次
+     * 定时检查观看时长并推送完课优惠券「今日问题」弹窗(兜底,仅完课优惠券业务
+     * 每分钟执行一次;未配置课题时静默跳过,不影响其他奖励逻辑
      */
     @Scheduled(cron = "0 */1 * * * ?")
     public void checkCompletionCouponStatus() {
@@ -68,19 +78,64 @@ public class LiveCompletionPointsTask {
                 return;
             }
 
-            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) ->
-                    completionCouponService.checkAndIssueCompletionCoupon(liveId, userId, duration));
+            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) -> {
+                LiveCompletionCouponNotifyResult notifyResult =
+                        completionCouponService.prepareCompletionCouponNotify(liveId, userId, duration);
+                if (notifyResult == null || !notifyResult.isShouldNotify()) {
+                    return;
+                }
+                if (pushCompletionCouponQuestion(liveId, userId, notifyResult)) {
+                    completionCouponService.markCompletionCouponNotified(liveId, userId);
+                }
+            });
 
         } catch (Exception e) {
             log.error("检查完课优惠券定时任务执行失败", e);
         }
     }
 
+    /**
+     * 手动触发完课优惠券「今日问题」弹窗(单用户,供接口调用)
+     */
+    public R triggerCompletionCouponQuestion(Long liveId, Long userId, Long watchDuration) {
+        try {
+            LiveCompletionCouponNotifyResult notifyResult =
+                    completionCouponService.prepareCompletionCouponNotify(liveId, userId, watchDuration, true);
+            if (notifyResult == null || !notifyResult.isShouldNotify()) {
+                return R.ok("当前无需推送弹窗").put("data", notifyResult);
+            }
+            boolean pushed = pushCompletionCouponQuestion(liveId, userId, notifyResult);
+            if (pushed) {
+                completionCouponService.markCompletionCouponNotified(liveId, userId);
+            }
+            return pushed
+                    ? R.ok("今日问题弹窗 WebSocket 推送成功").put("data", notifyResult).put("pushed", true)
+                    : R.error("用户未在线,WebSocket 推送失败").put("data", notifyResult).put("pushed", false);
+        } catch (Exception e) {
+            log.error("手动触发完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
+            return R.error("触发失败:" + e.getMessage());
+        }
+    }
+
+    private boolean 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));
+        boolean pushed = webSocketServer.sendCompletionCouponQuestionMessage(liveId, userId, sendMsgVo);
+        if (pushed) {
+            log.info("[完课优惠券] 推送今日问题弹窗, liveId={}, userId={}", liveId, userId);
+        }
+        return pushed;
+    }
+
     private void processCompletionByWatchDuration(List<Live> activeLives, CompletionHandler handler) {
         for (Live live : activeLives) {
             try {
                 Long liveId = live.getLiveId();
-                String hashKey = "live:watch:duration:hash:" + liveId;
+                String hashKey = WATCH_DURATION_HASH_PREFIX + liveId;
                 Map<Object, Object> userDurations = redisCache.hashEntries(hashKey);
 
                 if (userDurations == null || userDurations.isEmpty()) {

+ 46 - 14
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -504,15 +504,15 @@ public class Task {
                 return;
             }
 
-            // 查询优惠券领取信息
-            LiveCouponIssue couponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(couponId);
+            // 查询当前直播间关联的优惠券领取信息
+            LiveCouponIssue couponIssue = liveCouponIssueService.selectIssueByLiveIdAndCouponId(live.getLiveId(), couponId);
             if (couponIssue == null) {
-                log.error("优惠券领取信息不存在,couponId={}", couponId);
+                log.error("优惠券领取信息不存在或未关联到直播间liveId={}, couponId={}", live.getLiveId(), couponId);
                 return;
             }
 
             // 检查优惠券状态
-            if (couponIssue.getStatus() == null || couponIssue.getStatus() != 1) {
+            if ((couponIssue.getStatus() == null || couponIssue.getStatus() != 1)&&couponIssue.getCouponType()!=3) {
                 log.error("优惠券状态不正常,couponId={}, status={}", couponId, couponIssue.getStatus());
                 return;
             }
@@ -522,16 +522,6 @@ public class Task {
 
             for (Long userId : userIds) {
                 try {
-                    // 检查用户是否已领取过该优惠券
-                    LiveCouponUser query = new LiveCouponUser();
-                    query.setCouponId(couponId);
-                    query.setUserId(userId.intValue());
-                    List<LiveCouponUser> existingList = liveCouponUserService.selectLiveCouponUserList(query);
-                    if (existingList != null && !existingList.isEmpty()) {
-                        log.info("用户已领取过该优惠券,跳过,userId={}, couponId={}", userId, couponId);
-                        continue;
-                    }
-
                     // 创建用户优惠券记录
                     LiveCouponUser couponUser = new LiveCouponUser();
                     couponUser.setCouponId(couponId);
@@ -580,6 +570,48 @@ public class Task {
         }
     }
 
+    /**
+     * 手动触发观看奖励优惠券发放(单用户,供接口调用)
+     *
+     * @param liveId   直播间ID
+     * @param userId   用户ID
+     * @param couponId 优惠券ID,为空时从直播间观看奖励配置读取
+     */
+    public R triggerWatchRewardCoupon(Long liveId, Long userId, Long couponId) {
+        try {
+            Live live = liveService.selectLiveByLiveId(liveId);
+            if (live == null) {
+                return R.error("直播不存在");
+            }
+
+            Long targetCouponId = couponId;
+            if (targetCouponId == null) {
+                String configJson = live.getConfigJson();
+                if (StringUtils.isEmpty(configJson)) {
+                    return R.error("直播间未配置观看奖励");
+                }
+                LiveWatchConfig config = JSON.parseObject(configJson, LiveWatchConfig.class);
+                if (config == null || !Boolean.TRUE.equals(config.getEnabled())) {
+                    return R.error("观看奖励未开启");
+                }
+                if (config.getAction() == null || config.getAction().intValue() != 3) {
+                    return R.error("观看奖励类型不是优惠券,请传入 couponId");
+                }
+                String actionCouponIdStr = config.getActionCouponId();
+                if (StringUtils.isBlank(actionCouponIdStr)) {
+                    return R.error("观看奖励未配置优惠券ID");
+                }
+                targetCouponId = Long.parseLong(actionCouponIdStr);
+            }
+
+            bindCouponToUsers(live, Collections.singletonList(userId), targetCouponId);
+            return R.ok("观看奖励优惠券发放完成").put("couponId", targetCouponId);
+        } catch (Exception e) {
+            log.error("手动触发观看奖励优惠券失败, liveId={}, userId={}", liveId, userId, e);
+            return R.error("触发失败:" + e.getMessage());
+        }
+    }
+
     /**
      * 发送优惠券奖励消息给前端
      */

+ 24 - 58
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -316,7 +316,6 @@ public class WebSocketServer {
             // 推送完课积分倒计时配置信息给前端
 //            sendCompletionPointsConfigToUser(session, liveId, userId, live);
 
-
         } else {
             adminRoom.add(session);
             // 为admin房间创建单线程执行器,保证串行化发送
@@ -455,62 +454,6 @@ public class WebSocketServer {
                 case "heartbeat":
                     // 更新心跳时间
                     heartbeatCache.put(session.getId(), System.currentTimeMillis());
-
-                    // 心跳时同步更新观看时长到Redis Hash
-                    long watchUserId = (long) userProperties.get("userId");
-
-
-
-//                    if (msg.getData() != null && !msg.getData().isEmpty()) {
-//                        try {
-//                            Long currentDuration = Long.parseLong(msg.getData());
-//
-//                            Live currentLive = liveService.selectLiveByLiveId(liveId);
-//                            if (currentLive == null) {
-//                                break;
-//                            }
-//
-//
-//                            // 判断直播是否已开始:status=2(直播中) 或 当前时间 >= 开播时间
-//                            boolean isLiveStarted = false;
-//                            if (currentLive.getStatus() != null && currentLive.getStatus() == 2) {
-//                                // status=2 表示直播中
-//                                isLiveStarted = true;
-//                            } else if (currentLive.getStartTime() != null) {
-//                                // 判断当前时间是否已超过开播时间
-//                                LocalDateTime now = LocalDateTime.now();
-//                                isLiveStarted = now.isAfter(currentLive.getStartTime()) || now.isEqual(currentLive.getStartTime());
-//                            }
-//
-//                            // 使用Hash结构存储:一个直播间一个Hash,包含所有用户的时长
-//                            String hashKey = "live:watch:duration:hash:" + liveId;
-//                            String userIdField = String.valueOf(watchUserId);
-//
-//                            if (!isLiveStarted) {
-//                                redisCache.hashDelete(hashKey, userIdField);
-//                                log.debug("[心跳-观看时长] 直播未开始,清除预播时长, liveId={}, userId={}", liveId, watchUserId);
-//                                break;
-//                            }
-//
-//                            // 直播已开始,记录观看时长
-//                            // 获取现有时长
-//                            Object existingDuration = redisCache.hashGet(hashKey, userIdField);
-//                            // 只有当新的时长更大时才更新
-//                            if (existingDuration == null || currentDuration > Long.parseLong(existingDuration.toString())) {
-//                                // 更新Hash中的用户时长
-//                                redisCache.hashPut(hashKey, userIdField, currentDuration.toString());
-//                                // 设置过期时间(2小时)
-//                                redisCache.expire(hashKey, 2, TimeUnit.HOURS);
-//
-//                                checkAndSendCompletionPointsInRealTime(liveId, watchUserId, currentDuration);
-//
-//                            }
-//                        } catch (Exception e) {
-//                            log.error("[心跳-观看时长] 更新失败, liveId={}, userId={}, data={}",
-//                                    liveId, watchUserId, msg.getData(), e);
-//                        }
-//                    }
-
                     sendMessage(session, JSONObject.toJSONString(R.ok().put("data", msg)));
                     break;
                 case "sendMsg":
@@ -997,6 +940,25 @@ public class WebSocketServer {
         }
     }
 
+    /**
+     * 发送完课优惠券今日问题弹窗通知给特定用户
+     * @return 是否推送成功(用户在线且发送成功)
+     */
+    public boolean sendCompletionCouponQuestionMessage(Long liveId, Long userId, SendMsgVo sendMsgVo) {
+        ConcurrentHashMap<Long, Session> room = getRoom(liveId);
+        Session session = room.get(userId);
+        if (session == null || !session.isOpen()) {
+            return false;
+        }
+        try {
+            sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
+            return true;
+        } catch (Exception e) {
+            log.error("发送完课优惠券今日问题消息失败: liveId={}, userId={}", liveId, userId, e);
+            return false;
+        }
+    }
+
     private void sendBlockMessage(Long liveId, Long userId) {
 
         ConcurrentHashMap<Long, Session> room = getRoom(liveId);
@@ -1347,7 +1309,11 @@ public class WebSocketServer {
                     log.error("优惠券状态已经关闭");
                     return;
                 }
-                LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
+                LiveCouponIssue liveCouponIssue = liveCouponIssueService.selectIssueByLiveIdAndCouponId(task.getLiveId(), liveCoupon.getCouponId());
+                if (liveCouponIssue == null) {
+                    log.error("优惠券未关联到直播间,liveId={}, couponId={}", task.getLiveId(), liveCoupon.getCouponId());
+                    return;
+                }
                 LiveCouponIssueRelation relation = liveCouponMapper.selectCouponRelation(task.getLiveId(), liveCouponIssue.getId());
                 if (liveCoupon != null) {
                     redisCache.setCacheObject(String.format(LiveKeysConstant.LIVE_COUPON_NUM , liveCouponIssue.getId()), liveCouponIssue.getRemainCount().intValue(), 30, TimeUnit.MINUTES);

+ 1 - 1
fs-live-app/src/main/resources/application.yml

@@ -6,4 +6,4 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-bjzm-test
+    active: druid-tyt-test

+ 31 - 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 " +
@@ -267,5 +277,10 @@ public interface FsUserVideoMapper
             "</if>" +
             "</script>"})
     List<FsUserVideoListUVO> selectFsUserVideoListUVOByUser(@Param("talentId") Long talentId, @Param("oneSelf") boolean oneSelf);
+
+    // 评论数加一
+    int addCommentCount(Long videoId);
+
+    int delCommentCount(Long videoId);
 }
 

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

@@ -20,7 +20,7 @@ public class FsUserVideoCommentDelUParam implements Serializable
     private Long commentId;
 
 //    @NotNull(message = "视频ID不能为空")
-//    private Long videoId;
+    private Long videoId;
 
     private String parentId;
 }

+ 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;
     }

+ 14 - 5
fs-service/src/main/java/com/fs/course/service/impl/FsUserVideoCommentServiceImpl.java

@@ -7,6 +7,7 @@ import java.util.stream.Collectors;
 import com.fs.common.core.domain.R;
 import com.fs.common.utils.DateUtils;
 import com.fs.course.mapper.FsUserVideoCommentLikeMapper;
+import com.fs.course.mapper.FsUserVideoMapper;
 import com.fs.course.param.FsUserVideoCommentAddUParam;
 import com.fs.course.param.FsUserVideoCommentDelUParam;
 import com.fs.course.param.FsUserVideoCommentUParam;
@@ -48,6 +49,9 @@ public class FsUserVideoCommentServiceImpl implements IFsUserVideoCommentService
     @Autowired
     private FsUserMapper fsUserMapper;
 
+    @Autowired
+    private FsUserVideoMapper fsUserVideoMapper;
+
     private static final String COMMENT_LIST_KEY_PREFIX = "comment:list:video:";
     private static final String COMMENT_HASH_KEY_PREFIX = "comment:hash:video:";
     private static final String REPLY_LIST_KEY_PREFIX = "reply:list:comment:";
@@ -191,6 +195,8 @@ public class FsUserVideoCommentServiceImpl implements IFsUserVideoCommentService
             comment.setParentId(param.getParentId());
         }
         if (fsUserVideoCommentMapper.insertFsUserVideoComment(comment)>0){
+            // 更新对应的评论数
+            fsUserVideoMapper.addCommentCount(comment.getVideoId());
             return R.ok().put("data",comment);
         };
         return R.error("新增评论失败");
@@ -485,12 +491,15 @@ public class FsUserVideoCommentServiceImpl implements IFsUserVideoCommentService
     @Override
     @Transactional
     public R delComment(FsUserVideoCommentDelUParam param) {
-        // 更新评论数
-        if (param.getParentId() == null) {
-            //除数据库中的回复
-            fsUserVideoCommentMapper.deleteByParentId(param.getCommentId().toString());
-        }
+        // 只删除自己的评论,回复保留
+//        if (param.getParentId() == null) {
+//            //除数据库中的回复
+//            fsUserVideoCommentMapper.deleteByParentId(param.getCommentId().toString());
+//        }
+
         fsUserVideoCommentMapper.deleteByCommentId(param.getCommentId());
+        // 评论数减一
+        fsUserVideoMapper.delCommentCount(param.getVideoId());
         return R.ok();
     }
 

+ 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;

+}


+ 7 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveCouponIssueMapper.java

@@ -76,6 +76,12 @@ public interface LiveCouponIssueMapper
     @Insert("insert into live_coupon_issue_relation(live_id,coupon_issue_id,is_show,goods_id) values(#{liveId},#{couponIssueId},#{isShow},#{goodsId})")
     void insertLiveCouponIssueRelation(LiveCouponIssueRelation liveCouponIssueRelation);
 
-    @Select("select * from live_coupon_issue where coupon_id= #{couponId}")
+    @Select("select * from live_coupon_issue where coupon_id = #{couponId} order by id desc limit 1")
     LiveCouponIssue selectLiveCouponIssueByCouponId(@Param("couponId") Long couponId);
+
+    @Select("select lci.* from live_coupon_issue lci " +
+            "inner join live_coupon_issue_relation lcir on lci.id = lcir.coupon_issue_id " +
+            "where lcir.live_id = #{liveId} and lci.coupon_id = #{couponId} " +
+            "order by lci.id desc limit 1")
+    LiveCouponIssue selectIssueByLiveIdAndCouponId(@Param("liveId") Long liveId, @Param("couponId") Long couponId);
 }

+ 0 - 1
fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java

@@ -138,7 +138,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;

+}


+ 47 - 5
fs-service/src/main/java/com/fs/live/service/ILiveCompletionCouponService.java

@@ -1,16 +1,58 @@
 package com.fs.live.service;
 
+import com.fs.live.domain.Live;
+import com.fs.live.param.LiveCompletionCouponAnswerParam;
+import com.fs.live.vo.LiveCompletionCouponConfigVO;
+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 {
 
     /**
-     * 检查完课状态并发放优惠券(定时任务调用)
+     * 解析完课优惠券配置
+     */
+    LiveCompletionCouponConfigVO parseCompletionCouponConfig(Live live);
+
+    /**
+     * 预检查完课弹窗(不标记已通知,供定时任务/WebSocket在推送成功后标记)
+     */
+    LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration);
+
+    /**
+     * 预检查完课弹窗
      *
-     * @param liveId        直播ID
-     * @param userId        用户ID
-     * @param watchDuration 观看时长(秒)
+     * @param forcePush true 时跳过「今日已推送」校验,供手动触发接口强制 WebSocket 推送
+     */
+    LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration, boolean forcePush);
+
+    /**
+     * 标记今日已推送完课优惠券弹窗
+     */
+    void markCompletionCouponNotified(Long liveId, Long userId);
+
+    /**
+     * @deprecated 请使用 {@link #prepareCompletionCouponNotify} + {@link #markCompletionCouponNotified}
+     */
+    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);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCouponIssueService.java

@@ -65,4 +65,6 @@ public interface ILiveCouponIssueService
     List<LiveCouponIssue> curCoupon(CouponPO coupon);
 
     LiveCouponIssue selectLiveCouponIssueByCouponId(Long id);
+
+    LiveCouponIssue selectIssueByLiveIdAndCouponId(Long liveId, Long couponId);
 }

+ 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);

+}


+ 4 - 4
fs-service/src/main/java/com/fs/live/service/impl/LiveAutoTaskServiceImpl.java

@@ -147,8 +147,8 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
             // 自动优惠券
             LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(Long.valueOf(liveAutoTask.getContent()));
             if(liveCoupon == null) return R.error("优惠券不存在");
-            LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
-            if(liveCouponIssue == null)return R.error("优惠券未发布");
+            LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectIssueByLiveIdAndCouponId(liveAutoTask.getLiveId(), liveCoupon.getCouponId());
+            if(liveCouponIssue == null)return R.error("优惠券未发布或未关联到直播间");
             LiveCouponIssueRelation liveCouponIssueRelation = liveCouponMapper.selectCouponRelation(liveAutoTask.getLiveId(),liveCouponIssue.getId());
             if(liveCouponIssueRelation == null) return R.error("优惠券尚未添加在直播间");
             if(ObjectUtil.isEmpty(liveCouponIssueRelation.getGoodsId())) return R.error("未绑定商品,无法制定自动化任务!");
@@ -321,8 +321,8 @@ public class LiveAutoTaskServiceImpl implements ILiveAutoTaskService {
             // 自动优惠券
             LiveCoupon liveCoupon = liveCouponMapper.selectLiveCouponById(Long.valueOf(liveAutoTask.getContent()));
             if(liveCoupon == null) return R.error("优惠券不存在!");
-            LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectLiveCouponIssueByCouponId(liveCoupon.getCouponId());
-            if(liveCouponIssue == null)return R.error("未发布优惠券!");
+            LiveCouponIssue liveCouponIssue = liveCouponIssueMapper.selectIssueByLiveIdAndCouponId(liveAutoTask.getLiveId(), liveCoupon.getCouponId());
+            if(liveCouponIssue == null)return R.error("未发布优惠券或未关联到直播间!");
             LiveCouponIssueRelation liveCouponIssueRelation = liveCouponMapper.selectCouponRelation(liveAutoTask.getLiveId(),liveCouponIssue.getId());
             if(liveCouponIssueRelation == null) return R.error("未绑定商品,无法发布!");
             if(ObjectUtil.isEmpty(liveCouponIssueRelation.getGoodsId())) return R.error("未绑定商品,无法发布!");

+ 347 - 51
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -2,10 +2,20 @@ 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.LiveCompletionCouponConfigVO;
+import com.fs.live.vo.LiveCompletionCouponInfoVO;
+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 +24,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 +39,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 +59,254 @@ 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 LiveCompletionCouponConfigVO parseCompletionCouponConfig(Live live) {
+        LiveCompletionCouponConfigVO vo = new LiveCompletionCouponConfigVO();
+        CompletionCouponConfig config = live == null ? disabledConfig() : getCompletionCouponConfig(live);
+        vo.setEnabled(config.isEnabled());
+        vo.setCompletionRate(config.getCompletionRate());
+        vo.setCouponId(config.getCouponId());
+        vo.setFinishQuestionIds(config.getFinishQuestionIds());
+        if (config.isEnabled() && live != null && live.getDuration() != null && live.getDuration() > 0) {
+            vo.setRequiredDurationSeconds(calculateRequiredDuration(live.getDuration(), config.getCompletionRate()));
+        } else {
+            vo.setRequiredDurationSeconds(-1L);
+        }
+        return vo;
+    }
+
+    @Override
+    public LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration) {
+        return prepareCompletionCouponNotify(liveId, userId, watchDuration, false);
+    }
+
+    @Override
+    public LiveCompletionCouponNotifyResult prepareCompletionCouponNotify(Long liveId, Long userId, Long watchDuration, boolean forcePush) {
+        LiveCompletionCouponNotifyResult result = new LiveCompletionCouponNotifyResult();
+        result.setShouldNotify(false);
+        result.setEligible(false);
+        result.setQuestions(Collections.emptyList());
+
         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;
-            }
+            result.setCoupon(loadCouponInfo(config.getCouponId()));
 
-            Long actualWatchDuration = watchDuration;
-            if (actualWatchDuration == null) {
-                actualWatchDuration = liveWatchUserService.getTotalWatchDuration(liveId, userId);
-            }
-            if (actualWatchDuration == null || actualWatchDuration <= 0) {
-                return;
+            if (!isWatchRateEligible(liveId, userId, watchDuration, config)) {
+                return result;
             }
+            result.setEligible(true);
 
-            Long videoDuration = live.getDuration();
-            if (videoDuration == null || videoDuration <= 0) {
-                return;
+            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
+                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);
+            List<LiveCompletionQuestionVO> questions = loadQuestions(config.getFinishQuestionIds());
+            if (questions.isEmpty()) {
+                log.debug("完课优惠券未配置直播课题, 跳过弹窗, liveId={}", liveId);
+                return result;
             }
 
-            if (watchRate.compareTo(BigDecimal.valueOf(config.getCompletionRate())) < 0) {
-                return;
+            if (!forcePush && hasNotifiedToday(liveId, userId)) {
+                result.setQuestions(questions);
+                return result;
             }
 
-            if (hasIssuedToday(liveId, userId, config.getCouponId())) {
-                return;
+            result.setShouldNotify(true);
+            result.setQuestions(questions);
+        } catch (Exception e) {
+            log.error("预检查完课优惠券弹窗失败, liveId={}, userId={}", liveId, userId, e);
+        }
+        return result;
+    }
+
+    @Override
+    public void markCompletionCouponNotified(Long liveId, Long userId) {
+        markNotifiedToday(liveId, userId);
+    }
+
+    @Override
+    public LiveCompletionCouponNotifyResult checkAndNotifyCompletionCoupon(Long liveId, Long userId, Long watchDuration) {
+        LiveCompletionCouponNotifyResult result = prepareCompletionCouponNotify(liveId, userId, watchDuration);
+        if (result.isShouldNotify()) {
+            markCompletionCouponNotified(liveId, userId);
+        }
+        return result;
+    }
+
+    @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);
+        LiveCouponIssue couponIssue = liveCouponIssueService.selectIssueByLiveIdAndCouponId(live.getLiveId(), couponId);
         if (couponIssue == null || couponIssue.getStatus() == null || couponIssue.getStatus() != 1) {
-            log.warn("完课优惠券领取配置不可用, couponId={}", couponId);
-            return;
+            throw new BaseException("优惠券领取配置不可用");
         }
 
         Date now = new Date();
@@ -162,9 +363,71 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
                         && item.getCreateTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().equals(today));
     }
 
+    private LiveCompletionCouponInfoVO loadCouponInfo(Long couponId) {
+        if (couponId == null) {
+            return null;
+        }
+        LiveCoupon coupon = liveCouponService.selectLiveCouponById(couponId);
+        if (coupon == null) {
+            return null;
+        }
+        LiveCompletionCouponInfoVO vo = new LiveCompletionCouponInfoVO();
+        vo.setCouponId(coupon.getCouponId());
+        vo.setTitle(coupon.getTitle());
+        vo.setCouponPrice(coupon.getCouponPrice());
+        vo.setUseMinPrice(coupon.getUseMinPrice());
+        vo.setCouponTime(coupon.getCouponTime());
+        return vo;
+    }
+
+    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 +436,63 @@ 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 long calculateRequiredDuration(long videoDurationSeconds, Integer completionRate) {
+        int rate = completionRate == null || completionRate <= 0 ? 90 : completionRate;
+        return (long) Math.ceil(videoDurationSeconds * rate / 100.0);
+    }
+
     private static class CompletionCouponConfig {
         private boolean enabled;
         private Integer completionRate;
         private Long couponId;
+        private String finishQuestionIds;
 
         public boolean isEnabled() {
             return enabled;
@@ -229,5 +517,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;
+        }
     }
 }

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCouponIssueServiceImpl.java

@@ -162,4 +162,9 @@ public class LiveCouponIssueServiceImpl implements ILiveCouponIssueService
     public LiveCouponIssue selectLiveCouponIssueByCouponId(Long couponId) {
         return liveCouponIssueMapper.selectLiveCouponIssueByCouponId(couponId);
     }
+
+    @Override
+    public LiveCouponIssue selectIssueByLiveIdAndCouponId(Long liveId, Long couponId) {
+        return liveCouponIssueMapper.selectIssueByLiveIdAndCouponId(liveId, couponId);
+    }
 }

+ 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);
+    }
+}

+ 8 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -373,16 +373,23 @@ 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");
+                Long participateCondition = jsonConfig.getLong("participateCondition");
+                if (jsonConfig.getBooleanValue("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) {

+ 21 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponConfigVO.java

@@ -0,0 +1,21 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * 完课优惠券配置(定时任务缓存用)
+ */
+@Data
+public class LiveCompletionCouponConfigVO {
+
+    private boolean enabled;
+
+    private Integer completionRate;
+
+    private Long couponId;
+
+    private String finishQuestionIds;
+
+    /** 完课所需观看时长(秒) */
+    private long requiredDurationSeconds;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/live/vo/LiveCompletionCouponInfoVO.java

@@ -0,0 +1,22 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * Íê¿ÎÓÅ»ÝȯÐÅÏ¢
+ */
+@Data
+public class LiveCompletionCouponInfoVO {
+
+    private Long couponId;
+
+    private String title;
+
+    private BigDecimal couponPrice;
+
+    private BigDecimal useMinPrice;
+
+    private Long couponTime;
+}

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

@@ -0,0 +1,24 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 完课优惠券弹窗通知结果
+ */
+@Data
+public class LiveCompletionCouponNotifyResult {
+
+    /** 是否应推送弹窗 */
+    private boolean shouldNotify;
+
+    /** 是否已达到完课条件 */
+    private boolean eligible;
+
+    /** 今日问题(不含答案) */
+    private List<LiveCompletionQuestionVO> questions;
+
+    /** 完课奖励优惠券信息 */
+    private LiveCompletionCouponInfoVO coupon;
+}

+ 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;
 

+ 4 - 15
fs-service/src/main/java/com/fs/utils/SensitiveDataUtils.java

@@ -1,6 +1,5 @@
 package com.fs.utils;
 
-import java.util.Random;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -8,10 +7,8 @@ public class SensitiveDataUtils {
 
     private static final Pattern MOBILE_PATTERN = Pattern.compile("(?<!\\d)1[3-9]\\d{9}(?!\\d)");
 
-
-
     /**
-     * 对文本中的手机号进行脱敏处理,保留前3位,后8位随机替换
+     * 对文本中的手机号进行脱敏处理,保留前3后4
      */
     public static String maskMobileNumbers(String content) {
         try {
@@ -23,7 +20,8 @@ public class SensitiveDataUtils {
             StringBuffer sb = new StringBuffer();
 
             while (matcher.find()) {
-                String maskedMobile = getPhoneNumber();
+                String mobile = matcher.group();
+                String maskedMobile = mobile.substring(0, 3) + "****" + mobile.substring(7);
                 matcher.appendReplacement(sb, maskedMobile);
             }
 
@@ -33,13 +31,4 @@ public class SensitiveDataUtils {
             return content;
         }
     }
-
-    private static String getPhoneNumber() {
-        String phoneNumbers = "16623962137,15523238506,17749925835,15923875456,18305948549,13883332012,15057469844,13618796139,18620430041,18584668114,18580017521,15111845257,15275039316,18201444980,18813118010,17726645677,15223482407,17623042467,15123822149,18623062201,17749925836,18323465069,18580176027,13452385636,13608870842,17685274759,15315437944,15998971322,18623079553,15025306414,18623592546,17264203997,18983650852,19823410818,15528133197,18987692003,15364612795,15364612795,17782358851,13452011251,13527318467,13637935049,15123970077,18166348566,13668096347,18883781302,18780014073,18602325964,15086929910,15320525962,15210865639,13671138824,19936630315,18223553039,18290561680,18883557568,18280114551,15086823485,18280406822,15084440304,18680821042,19115284897,15823539658,13718194200,13521390467,18253105683,15866702785,18697568671,13435694935,17347643607,15002351429,13640582745,18716432052,13167916563,15523238507,14678903116,18324157410,18121887854,19923671431,17388206297,18225226941,15111111111,18324199200,15223337413,13633333333,15523524367,15376779826,18580137367,13983381612,15523004683,18996016507,15683164217,19946754704,13983370856,15870485887,18280237531,15223298047,18580466998,15923377066,17353257273,18067773557,18103269005,17775537029,15730089438,13668480682,13183501165,13101083915,19102330681,15084400940";
-        String[] split = phoneNumbers.split(",");
-        return split[new Random().nextInt(split.length)];
-    }
-
-
-
-}
+}

+ 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>

+ 13 - 0
fs-service/src/main/resources/mapper/course/FsUserVideoMapper.xml

@@ -156,6 +156,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </trim>
     </insert>
 
+    <insert id="addCommentCount">
+        update fs_user_video
+        set comments = comments + 1
+        where video_id = #{videoId}
+    </insert>
+
+    <insert id="delCommentCount">
+        update fs_user_video
+        set comments = comments - 1
+        where video_id = #{videoId}
+    </insert>
+
+
     <update id="updateFsUserVideo" parameterType="FsUserVideo">
         update fs_user_video
         <trim prefix="SET" suffixOverrides=",">

+ 14 - 14
fs-service/src/main/resources/mapper/live/LiveCouponUserMapper.xml

@@ -69,20 +69,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
          left join fs_user u on cou.user_id=u.user_id
 
         <where>
-            <if test="couponId != null "> and coupon_id = #{couponId}</if>
-            <if test="userId != null "> and user_id = #{userId}</if>
-            <if test="couponTitle != null  and couponTitle != ''"> and coupon_title = #{couponTitle}</if>
-            <if test="couponPrice != null "> and coupon_price = #{couponPrice}</if>
-            <if test="useMinPrice != null "> and use_min_price = #{useMinPrice}</if>
-            <if test="limitTime != null "> and limit_time = #{limitTime}</if>
-            <if test="useTime != null "> and use_time = #{useTime}</if>
-            <if test="type != null  and type != ''"> and type = #{type}</if>
-            <if test="status != null "> and status = #{status}</if>
-            <if test="isFail != null "> and is_fail = #{isFail}</if>
-            <if test="isDel != null "> and is_del = #{isDel}</if>
-            <if test="goodsId != null "> and goods_id = #{goodsId}</if>
-            <if test="verifyCode != null and verifyCode != ''"> and verify_code = #{verifyCode}</if>
-            <if test="verifyUserId != null "> and verify_user_id = #{verifyUserId}</if>
+            <if test="couponId != null "> and cou.coupon_id = #{couponId}</if>
+            <if test="userId != null "> and cou.user_id = #{userId}</if>
+            <if test="couponTitle != null  and couponTitle != ''"> and cou.coupon_title = #{couponTitle}</if>
+            <if test="couponPrice != null "> and cou.coupon_price = #{couponPrice}</if>
+            <if test="useMinPrice != null "> and cou.use_min_price = #{useMinPrice}</if>
+            <if test="limitTime != null "> and cou.limit_time = #{limitTime}</if>
+            <if test="useTime != null "> and cou.use_time = #{useTime}</if>
+            <if test="type != null  and type != ''"> and cou.type = #{type}</if>
+            <if test="status != null "> and cou.status = #{status}</if>
+            <if test="isFail != null "> and cou.is_fail = #{isFail}</if>
+            <if test="isDel != null "> and cou.is_del = #{isDel}</if>
+            <if test="goodsId != null "> and cou.goods_id = #{goodsId}</if>
+            <if test="verifyCode != null and verifyCode != ''"> and cou.verify_code = #{verifyCode}</if>
+            <if test="verifyUserId != null "> and cou.verify_user_id = #{verifyUserId}</if>
         </where>
     </select>
 

+ 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>

+ 2 - 1
fs-user-app/src/main/java/com/fs/app/config/WebMvcConfig.java

@@ -19,7 +19,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**").addPathPatterns("/store/app/**");
+        registry.addInterceptor(authorizationInterceptor)
+                .addPathPatterns("/app/**", "/store/app/**", "/user/**", "/live/**");
     }
 //
 //    @Override

+ 6 - 12
fs-user-app/src/main/java/com/fs/app/controller/InquiryOrderController.java

@@ -704,9 +704,6 @@ public class InquiryOrderController extends  AppBaseController {
     public R getInquiryOrderById(@RequestParam("orderId")Long orderId)
     {
         FsInquiryOrder order=inquiryOrderService.selectFsInquiryOrderByOrderId(orderId);
-//        if(!order.getUserId().equals(Long.parseLong(getUserId()))){
-//            return R.error("非法操作");
-//        }
         if(order!=null&&StringUtils.isNotEmpty(order.getPatientJson())){
             FsInquiryOrderPatientDTO dto=JSONUtil.toBean(order.getPatientJson(),FsInquiryOrderPatientDTO.class);
             dto.setMobile(ParseUtils.parsePhone(dto.getMobile()));
@@ -723,9 +720,12 @@ public class InquiryOrderController extends  AppBaseController {
     {
         Map<String,Object> maps=new HashMap<>();
         FsInquiryOrder order=inquiryOrderService.selectFsInquiryOrderByOrderId(orderId);
-//        if(!order.getUserId().equals(Long.parseLong(getUserId()))){
-//            return R.error("非法操作");
-//        }
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!com.fs.common.utils.OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            return R.error("非法操作");
+        }
         if(order!=null&&StringUtils.isNotEmpty(order.getPatientJson())){
             FsInquiryOrderPatientDTO dto=JSONUtil.toBean(order.getPatientJson(),FsInquiryOrderPatientDTO.class);
             dto.setMobile(ParseUtils.parsePhone(dto.getMobile()));
@@ -798,9 +798,6 @@ public class InquiryOrderController extends  AppBaseController {
     public R getInquiryOrderReport(@RequestParam("orderId")Long orderId)
     {
         FsInquiryOrderReportUVO report=orderReportService.selectFsInquiryOrderReportUVOByOrderId(orderId);
-//        if(!report.getUserId().equals(Long.parseLong(getUserId()))){
-//            return R.error("非法操作");
-//        }
         if(report!=null&&StringUtils.isNotEmpty(report.getPatientJson())){
             FsInquiryOrderPatientDTO dto=JSONUtil.toBean(report.getPatientJson(),FsInquiryOrderPatientDTO.class);
             dto.setMobile(ParseUtils.parsePhone(dto.getMobile()));
@@ -840,9 +837,6 @@ public class InquiryOrderController extends  AppBaseController {
     public R getCompanyUserInquiryOrderById(@RequestParam("orderId")Long orderId)
     {
         FsInquiryOrder order=inquiryOrderService.selectFsInquiryOrderByOrderId(orderId);
-//        if(!order.getCompanyUserId().equals(getCompanyUserId())){
-//            return R.error("非法操作");
-//        }
         if(order!=null&&StringUtils.isNotEmpty(order.getPatientJson())){
             FsInquiryOrderPatientDTO dto=JSONUtil.toBean(order.getPatientJson(),FsInquiryOrderPatientDTO.class);
             dto.setMobile(ParseUtils.parsePhone(dto.getMobile()));

+ 0 - 6
fs-user-app/src/main/java/com/fs/app/controller/PackageOrderController.java

@@ -137,9 +137,6 @@ public class PackageOrderController extends  AppBaseController {
     @GetMapping("/getPackageOrderById")
     public R getPackageOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsPackageOrder order=packageOrderService.selectFsPackageOrderByOrderId(orderId);
-//        if(!order.getUserId().equals(Long.parseLong(getUserId()))){
-//            return R.error("非法操作");
-//        }
 
         if(order!=null&& StringUtils.isNotEmpty(order.getPatientJson())){
             FsPatient dto= JSONUtil.toBean(order.getPatientJson(),FsPatient.class);
@@ -171,9 +168,6 @@ public class PackageOrderController extends  AppBaseController {
     @GetMapping("/getCompanyUserPackageOrderById")
     public R getCompanyUserPackageOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsPackageOrder order=packageOrderService.selectFsPackageOrderByOrderId(orderId);
-//        if(!order.getCompanyUserId().equals(getCompanyUserId())){
-//            return R.error("非法操作");
-//        }
         if(order!=null&& StringUtils.isNotEmpty(order.getPatientJson())){
             FsPatient dto= JSONUtil.toBean(order.getPatientJson(),FsPatient.class);
             dto.setMobile(ParseUtils.parsePhone(dto.getMobile()));

+ 35 - 7
fs-user-app/src/main/java/com/fs/app/controller/StoreOrderController.java

@@ -12,6 +12,7 @@ import com.fs.his.vo.FsStoreOrderBillLogVo;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 
+import com.fs.common.utils.OwnershipAssert;
 import com.fs.common.utils.ParseUtils;
 import com.fs.his.domain.*;
 import com.fs.his.param.*;
@@ -76,6 +77,12 @@ public class StoreOrderController extends  AppBaseController {
     @GetMapping("/getMyStoreOrderById")
     public R getMyStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsStoreOrder order=orderService.selectFsStoreOrderByOrderId(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            return R.error("非法操作");
+        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
         List<FsStoreOrderItemListUVO> list=orderItemService.selectFsStoreOrderItemListUVOByOrderId(orderId);
@@ -114,9 +121,6 @@ public class StoreOrderController extends  AppBaseController {
     @GetMapping("/getStoreOrderById")
     public R getStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsStoreOrder order=orderService.selectFsStoreOrderByOrderId(orderId);
-//        if(!order.getUserId().equals(Long.parseLong(getUserId()))){
-//            return R.error("非法操作");
-//        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
         List<FsStoreOrderItemListUVO> list=orderItemService.selectFsStoreOrderItemListUVOByOrderId(orderId);
@@ -177,6 +181,9 @@ public class StoreOrderController extends  AppBaseController {
         if (ObjectUtil.isNull(order)) {
             throw new CustomException("订单不存在");
         }
+        if (!OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            throw new CustomException("非法操作");
+        }
         return orderService.cancelOrder(param.getOrderId());
 
     }
@@ -188,6 +195,9 @@ public class StoreOrderController extends  AppBaseController {
         if (ObjectUtil.isNull(order)) {
             throw new CustomException("订单不存在");
         }
+        if (!OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            throw new CustomException("非法操作");
+        }
         if (order.getStatus() <3) {
             throw new CustomException("未发货订单不能查询");
         }
@@ -209,6 +219,13 @@ public class StoreOrderController extends  AppBaseController {
     @ApiOperation("完成订单")
     @PostMapping("/finishOrder")
     public R finishOrder(@Validated @RequestBody FsStoreOrderFinishParam param, HttpServletRequest request){
+        FsStoreOrder order = orderService.selectFsStoreOrderByOrderId(param.getOrderId());
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            return R.error("非法操作");
+        }
         return orderService.finishOrder(param.getOrderId());
     }
 
@@ -230,9 +247,6 @@ public class StoreOrderController extends  AppBaseController {
     @GetMapping("/getCompanyUserStoreOrderById")
     public R getCompanyUserStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsStoreOrder order=orderService.selectFsStoreOrderByOrderId(orderId);
-//        if(!order.getCompanyUserId().equals(getCompanyUserId())){
-//            return R.error("非法操作");
-//        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
         List<FsStoreOrderItemListUVO> list=orderItemService.selectFsStoreOrderItemListUVOByOrderId(orderId);
@@ -272,13 +286,27 @@ public class StoreOrderController extends  AppBaseController {
     @GetMapping("/billInfo")
     @ApiResponse(code = 200, message = "", response = FsStoreOrderBillLog.class)
     public R billList(Long id){
-        return R.ok().put("data", orderBillLogService.getById(id));
+        FsStoreOrderBillLog billLog = orderBillLogService.getById(id);
+        if (billLog != null) {
+            FsStoreOrder order = orderService.selectFsStoreOrderByOrderId(billLog.getOrderId());
+            if (order != null && !OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+                return R.error("非法操作");
+            }
+        }
+        return R.ok().put("data", billLog);
     }
 
     @Login
     @ApiOperation("开票冲红")
     @GetMapping("/billBack")
     public R billBack(Long id){
+        FsStoreOrderBillLog billLog = orderBillLogService.getById(id);
+        if (billLog != null) {
+            FsStoreOrder order = orderService.selectFsStoreOrderByOrderId(billLog.getOrderId());
+            if (order != null && !OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+                return R.error("非法操作");
+            }
+        }
         return orderBillLogService.billBack(id);
     }
 

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

@@ -120,7 +120,6 @@ public class UserVipController extends  AppBaseController{
                 userMap.setIsVip(0);
                 userMap.setVipStatus(2);
                 fsUserMapper.updateFsUser(userMap);
-                // 添加日志记录
                 logger.info("User ID: " + user.getUserId() + " VIP status updated.");
             }
         }

+ 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("恭喜您,福利券已到账");
+    }
+}

+ 6 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveOrderController.java

@@ -190,6 +190,12 @@ public class LiveOrderController extends AppBaseController
     @GetMapping("/getMyStoreOrderById")
     public R getMyStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         LiveOrder order=orderService.selectLiveOrderByOrderId(String.valueOf(orderId));
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (order.getUserId() == null || !order.getUserId().equals(getUserId())) {
+            return R.error("非法操作");
+        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
         List<LiveOrderItemListUVO> list = liveOrderItemService.selectLiveOrderItemListUVOByOrderId(orderId);

+ 6 - 0
fs-user-app/src/main/java/com/fs/app/controller/store/StoreOrderScrmController.java

@@ -123,6 +123,12 @@ public class StoreOrderScrmController extends AppBaseController {
     @GetMapping("/getMyStoreOrderById")
     public R getMyStoreOrderById(@RequestParam("orderId") Long orderId, HttpServletRequest request){
         FsStoreOrderScrm order=orderService.selectFsStoreOrderById(orderId);
+        if (order == null) {
+            return R.error("订单不存在");
+        }
+        if (!com.fs.common.utils.OwnershipAssert.isSameUser(order.getUserId(), Long.parseLong(getUserId()))) {
+            return R.error("非法操作");
+        }
         order.setUserPhone(ParseUtils.parsePhone(order.getUserPhone()));
         order.setUserAddress(ParseUtils.parseIdCard(order.getUserAddress()));
         List<FsStoreOrderItemVO> list=itemService.selectFsStoreOrderItemListByOrderId(orderId);