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