2 Komitmen 4679685f85 ... d51128850e

Pembuat SHA1 Pesan Tanggal
  yys d51128850e 1、调整答题参数 4 hari lalu
  yys 543deba2a8 1、调整完课答题领优惠卷需要留存 4 hari lalu

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

@@ -104,25 +104,8 @@ public class LiveCompletionPointsTask {
                 return;
             }
 
-            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) -> {
-                LiveCompletionCouponNotifyResult notifyResult =
-                        completionCouponService.prepareCompletionCouponNotify(liveId, userId, duration);
-                if (notifyResult == null || !notifyResult.isShouldNotify()) {
-                    return;
-                }
-                String bizName = resolveCompletionCouponBizName(notifyResult);
-                Long bizId = notifyResult.getCoupon() != null ? notifyResult.getCoupon().getCouponId() : null;
-                LiveConsoleOpLog opLog = liveConsoleOpLogService.saveLog(
-                        liveId,
-                        LiveConsoleOpLog.OP_COMPLETION_COUPON,
-                        LiveConsoleOpLog.HANDLE_AUTO,
-                        bizId,
-                        bizName
-                );
-                if (pushCompletionCouponQuestion(liveId, userId, notifyResult, opLog)) {
-                    completionCouponService.markCompletionCouponNotified(liveId, userId);
-                }
-            });
+            processCompletionByWatchDuration(activeLives, (liveId, userId, duration) ->
+                    dispatchCompletionCouponNotify(liveId, userId, duration, false));
 
         } catch (Exception e) {
             log.error("检查完课优惠券定时任务执行失败", e);
@@ -139,21 +122,50 @@ public class LiveCompletionPointsTask {
             if (notifyResult == null || !notifyResult.isShouldNotify()) {
                 return R.ok("当前无需推送弹窗").put("data", notifyResult);
             }
-            boolean pushed = pushCompletionCouponQuestion(liveId, userId, notifyResult);
-            if (pushed) {
-                completionCouponService.markCompletionCouponNotified(liveId, userId);
-            }
+            LiveConsoleOpLog opLog = saveAndPushCompletionCouponNotify(liveId, userId, notifyResult);
+            boolean pushed = opLog != null;
             return pushed
-                    ? R.ok("今日问题弹窗 WebSocket 推送成功").put("data", notifyResult).put("pushed", true)
-                    : R.error("用户未在线,WebSocket 推送失败").put("data", notifyResult).put("pushed", false);
+                    ? R.ok("今日问题弹窗 WebSocket 推送成功")
+                        .put("data", notifyResult)
+                        .put("pushed", true)
+                        .put("opLogId", opLog.getId())
+                    : 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) {
-        return pushCompletionCouponQuestion(liveId, userId, notifyResult, null);
+    /**
+     * 完课优惠券弹窗:与定时任务一致,先写留存再推送 WebSocket
+     */
+    private LiveConsoleOpLog dispatchCompletionCouponNotify(Long liveId, Long userId, Long watchDuration, boolean forcePush) {
+        LiveCompletionCouponNotifyResult notifyResult =
+                completionCouponService.prepareCompletionCouponNotify(liveId, userId, watchDuration, forcePush);
+        if (notifyResult == null || !notifyResult.isShouldNotify()) {
+            return null;
+        }
+        return saveAndPushCompletionCouponNotify(liveId, userId, notifyResult);
+    }
+
+    private LiveConsoleOpLog saveAndPushCompletionCouponNotify(Long liveId, Long userId,
+                                                                 LiveCompletionCouponNotifyResult notifyResult) {
+        String bizName = resolveCompletionCouponBizName(notifyResult);
+        Long bizId = notifyResult.getCoupon() != null ? notifyResult.getCoupon().getCouponId() : null;
+        LiveConsoleOpLog opLog = liveConsoleOpLogService.saveLog(
+                liveId,
+                LiveConsoleOpLog.OP_COMPLETION_COUPON,
+                LiveConsoleOpLog.HANDLE_AUTO,
+                bizId,
+                bizName
+        );
+        if (pushCompletionCouponQuestion(liveId, userId, notifyResult, opLog)) {
+            completionCouponService.markCompletionCouponNotified(liveId, userId);
+            return opLog;
+        }
+        return null;
     }
 
     private boolean pushCompletionCouponQuestion(Long liveId, Long userId, LiveCompletionCouponNotifyResult notifyResult,

+ 23 - 4
fs-live-app/src/main/java/com/fs/live/task/Task.java

@@ -542,8 +542,11 @@ public class Task {
                             openRewardLive.getLiveId(),
                             resolveWatchRewardPointsBizName(config, userIds.size())
                     );
-                    userIds.forEach(userId -> webSocketServer.sendIntegralMessage(
-                            openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog));
+                    userIds.forEach(userId -> {
+                        liveConsoleOpLogService.bindOpLogUser(watchPointsOpLog.getId(), openRewardLive.getLiveId(), userId);
+                        webSocketServer.sendIntegralMessage(
+                                openRewardLive.getLiveId(), userId, config.getScoreAmount(), watchPointsOpLog);
+                    });
                     rewardLiveCount++;
                     rewardedUserCount += userIds.size();
                     log.info("{} autoUpdateWatchReward 积分发放完成: liveId={}, 用户数={}",
@@ -718,8 +721,24 @@ public class Task {
                 targetCouponId = Long.parseLong(actionCouponIdStr);
             }
 
-            bindCouponToUsers(live, Collections.singletonList(userId), targetCouponId, true);
-            return R.ok("观看奖励优惠券发放完成").put("couponId", targetCouponId);
+            List<LiveConsoleOpLogUser> couponRelations = bindCouponToUsers(live, Collections.singletonList(userId), targetCouponId, false);
+            if (couponRelations.isEmpty()) {
+                return R.error("优惠券发放失败,请检查优惠券配置及库存");
+            }
+            LiveCoupon watchRewardCoupon = liveCouponService.selectLiveCouponById(targetCouponId);
+            LiveConsoleOpLog watchCouponOpLog = liveConsoleOpLogService.saveLog(
+                    liveId,
+                    LiveConsoleOpLog.OP_WATCH_REWARD_COUPON,
+                    LiveConsoleOpLog.HANDLE_AUTO,
+                    targetCouponId,
+                    resolveWatchRewardCouponBizName(watchRewardCoupon, targetCouponId, couponRelations.size())
+            );
+            liveConsoleOpLogService.bindOpLogUsers(watchCouponOpLog.getId(), liveId, couponRelations);
+            couponRelations.forEach(relation -> sendCouponRewardMessage(
+                    liveId, relation.getUserId(), watchRewardCoupon, watchCouponOpLog));
+            return R.ok("观看奖励优惠券发放完成")
+                    .put("couponId", targetCouponId)
+                    .put("opLogId", watchCouponOpLog.getId());
         } catch (Exception e) {
             log.error("手动触发观看奖励优惠券失败, liveId={}, userId={}", liveId, userId, e);
             return R.error("触发失败:" + e.getMessage());

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

@@ -1138,6 +1138,7 @@ public class WebSocketServer {
             return false;
         }
         try {
+            embedOpLogIdInMessageData(sendMsgVo);
             sendMessage(session, JSONObject.toJSONString(R.ok().put("data", sendMsgVo)));
             return true;
         } catch (Exception e) {

+ 11 - 2
fs-service/src/main/java/com/fs/live/param/LiveCompletionCouponAnswerItem.java

@@ -13,8 +13,17 @@ public class LiveCompletionCouponAnswerItem {
 
     /**
      * 用户答案
-     * 单选:如 A
-     * 多选:如 A,B 或 JSON 数组字符串 ["A","B"]
+     * 单选:选项文案,如「一斤半」
+     * 多选:JSON 数组字符串,如 ["A","B"]
      */
     private String answer;
+
+    /** 选项下标(App 扁平提交时使用) */
+    private Integer answerIndex;
+
+    /** 选项文案(App 扁平提交时使用,优先于 answer) */
+    private String answerName;
+
+    /** 是否为用户选中项:1 是 0 否(App 扁平提交时使用) */
+    private Integer isAnswer;
 }

+ 77 - 1
fs-service/src/main/java/com/fs/live/service/impl/LiveCompletionCouponServiceImpl.java

@@ -212,7 +212,8 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
             throw new BaseException("未配置今日问题");
         }
 
-        boolean allCorrect = evaluateAnswers(param.getAnswers(), configuredQuestionIds);
+        List<LiveCompletionCouponAnswerItem> normalizedAnswers = normalizeUserAnswers(param.getAnswers());
+        boolean allCorrect = evaluateAnswers(normalizedAnswers, configuredQuestionIds);
         saveAnswerRecordToday(liveId, userId, allCorrect);
 
         LiveCompletionCouponAnswerResult result = new LiveCompletionCouponAnswerResult();
@@ -255,6 +256,81 @@ public class LiveCompletionCouponServiceImpl implements ILiveCompletionCouponSer
         return couponUser;
     }
 
+    /**
+     * 将 App 扁平选项格式归一为每题一条作答记录。
+     * <p>App 可能按「每个选项一条」提交(含 answerIndex / answerName / isAnswer),
+     * 而题库答案按选项文案存储,需合并后再校验。</p>
+     */
+    private List<LiveCompletionCouponAnswerItem> normalizeUserAnswers(List<LiveCompletionCouponAnswerItem> rawAnswers) {
+        if (rawAnswers == null || rawAnswers.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        boolean flatOptionFormat = rawAnswers.stream().anyMatch(a -> a.getIsAnswer() != null)
+                || rawAnswers.stream()
+                .filter(a -> a.getQuestionId() != null)
+                .collect(Collectors.groupingBy(LiveCompletionCouponAnswerItem::getQuestionId))
+                .values().stream().anyMatch(list -> list.size() > 1);
+        if (!flatOptionFormat) {
+            return rawAnswers;
+        }
+
+        Map<Long, List<LiveCompletionCouponAnswerItem>> grouped = rawAnswers.stream()
+                .filter(a -> a.getQuestionId() != null)
+                .collect(Collectors.groupingBy(LiveCompletionCouponAnswerItem::getQuestionId));
+
+        List<Long> questionIds = new ArrayList<>(grouped.keySet());
+        Map<Long, LiveQuestionBank> questionMap = liveQuestionBankMapper.selectLiveQuestionBankByIds(questionIds)
+                .stream()
+                .collect(Collectors.toMap(LiveQuestionBank::getId, q -> q, (a, b) -> a));
+
+        List<LiveCompletionCouponAnswerItem> normalized = new ArrayList<>();
+        for (Map.Entry<Long, List<LiveCompletionCouponAnswerItem>> entry : grouped.entrySet()) {
+            Long questionId = entry.getKey();
+            List<LiveCompletionCouponAnswerItem> options = entry.getValue();
+
+            List<LiveCompletionCouponAnswerItem> selected = options.stream()
+                    .filter(a -> a.getIsAnswer() != null && a.getIsAnswer() == 1)
+                    .collect(Collectors.toList());
+
+            LiveCompletionCouponAnswerItem item = new LiveCompletionCouponAnswerItem();
+            item.setQuestionId(questionId);
+
+            if (selected.isEmpty()) {
+                if (options.size() == 1) {
+                    item.setAnswer(resolveAnswerText(options.get(0)));
+                    normalized.add(item);
+                }
+                continue;
+            }
+
+            LiveQuestionBank question = questionMap.get(questionId);
+            boolean multiChoice = question != null && question.getType() != null && question.getType() == 2L;
+
+            if (multiChoice) {
+                List<String> names = selected.stream()
+                        .map(this::resolveAnswerText)
+                        .filter(StringUtils::isNotEmpty)
+                        .collect(Collectors.toList());
+                item.setAnswer(JSON.toJSONString(names));
+            } else {
+                item.setAnswer(resolveAnswerText(selected.get(0)));
+            }
+            normalized.add(item);
+        }
+        return normalized;
+    }
+
+    private String resolveAnswerText(LiveCompletionCouponAnswerItem item) {
+        if (item == null) {
+            return null;
+        }
+        if (StringUtils.isNotEmpty(item.getAnswerName())) {
+            return item.getAnswerName().trim();
+        }
+        return StringUtils.isEmpty(item.getAnswer()) ? null : item.getAnswer().trim();
+    }
+
     /**
      * 校验是否答完全部题目,并返回是否全部答对(答错不阻断记录)
      */

+ 10 - 0
fs-user-app/src/main/java/com/fs/app/exception/FSExceptionHandler.java

@@ -7,6 +7,7 @@ import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.R;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.exception.base.BaseException;
 import com.fs.common.utils.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -82,6 +83,15 @@ public class FSExceptionHandler {
 	public R handleException(CustomException e){
 		return R.error(e.getMessage());
 	}
+
+	/**
+	 * 业务校验异常(如完课优惠券答题/领取校验)
+	 */
+	@ExceptionHandler(BaseException.class)
+	public R handleBaseException(BaseException e) {
+		return R.error(e.getMessage());
+	}
+
 	@ExceptionHandler(Exception.class)
 	public R handleException(Exception e){
 		logger.error(e.getMessage(), e);