|
|
@@ -22,10 +22,12 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
import java.time.ZoneId;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
import java.util.Date;
|
|
|
import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
|
|
|
/**
|
|
|
* 直播完课积分记录Service业务层处理
|
|
|
@@ -66,17 +68,17 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
|
|
|
// 2. 从数据库获取完课积分配置
|
|
|
CompletionPointsConfig config = getCompletionPointsConfig(live);
|
|
|
-
|
|
|
+
|
|
|
// 检查是否开启完课积分功能
|
|
|
if (!config.isEnabled()) {
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 检查配置完整性
|
|
|
Integer completionRate = config.getCompletionRate();
|
|
|
int[] pointsConfig = config.getPointsConfig();
|
|
|
-
|
|
|
+
|
|
|
if (completionRate == null || pointsConfig == null || pointsConfig.length == 0) {
|
|
|
|
|
|
return null;
|
|
|
@@ -104,7 +106,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
BigDecimal watchRate = BigDecimal.valueOf(actualWatchDuration)
|
|
|
.multiply(BigDecimal.valueOf(100))
|
|
|
.divide(BigDecimal.valueOf(videoDuration), 2, RoundingMode.HALF_UP);
|
|
|
-
|
|
|
+
|
|
|
// 限制完课比例最大值为100.00%(防止数据库字段溢出)
|
|
|
if (watchRate.compareTo(BigDecimal.valueOf(100)) > 0) {
|
|
|
watchRate = BigDecimal.valueOf(100);
|
|
|
@@ -116,51 +118,40 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- // 7. 本场直播(按 startTime)是否已创建过完课记录;结束后重开可再领
|
|
|
- LiveCompletionPointsRecord sessionRecord = recordMapper.selectLatestByUserAndLiveId(liveId, userId);
|
|
|
- if (sessionRecord != null && live.getStartTime() != null) {
|
|
|
- Date recordTime = sessionRecord.getCreateTime() != null
|
|
|
- ? sessionRecord.getCreateTime()
|
|
|
- : sessionRecord.getCurrentCompletionDate();
|
|
|
- if (recordTime != null) {
|
|
|
- LocalDateTime recordLdt = recordTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
|
|
- if (!recordLdt.isBefore(live.getStartTime())) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
- } else if (sessionRecord != null && live.getStartTime() == null) {
|
|
|
- LocalDate today = LocalDate.now();
|
|
|
- Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
- LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
|
|
|
- if (todayRecord != null) {
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
+
|
|
|
+ // 7. 按唯一键 uk_live_user_date(live+user+当天)先查主库当天记录,避免从库延迟/并发重复插入
|
|
|
+ LiveCompletionPointsRecord todayRecord = recordMapper.selectByUserAndDate(liveId, userId, currentDate);
|
|
|
+ if (todayRecord != null) {
|
|
|
+ if (isRecordInCurrentSession(todayRecord, live.getStartTime())) {
|
|
|
+ // 本场已创建过,不再插入
|
|
|
return null;
|
|
|
}
|
|
|
+ // 同日上一场残留:复用更新,避开唯一键冲突,重开后可再领
|
|
|
+ LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
|
|
|
+ int continuousDays = calcContinuousDays(latestRecord, today);
|
|
|
+ int points = calculatePoints(continuousDays, pointsConfig);
|
|
|
+
|
|
|
+ todayRecord.setWatchDuration(actualWatchDuration);
|
|
|
+ todayRecord.setVideoDuration(videoDuration);
|
|
|
+ todayRecord.setCompletionRate(watchRate);
|
|
|
+ todayRecord.setContinuousDays(continuousDays);
|
|
|
+ todayRecord.setPointsAwarded(points);
|
|
|
+ if (latestRecord != null && !Objects.equals(latestRecord.getId(), todayRecord.getId())) {
|
|
|
+ todayRecord.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
|
|
|
+ }
|
|
|
+ recordMapper.reuseRecordForNewSession(todayRecord);
|
|
|
+ todayRecord.setReceiveStatus(0);
|
|
|
+ todayRecord.setReceiveTime(null);
|
|
|
+ todayRecord.setCreateTime(new Date());
|
|
|
+ log.info("同日重开复用完课记录, liveId={}, userId={}, recordId={}", liveId, userId, todayRecord.getId());
|
|
|
+ return todayRecord;
|
|
|
}
|
|
|
|
|
|
- LocalDate today = LocalDate.now();
|
|
|
- Date currentDate = Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
|
|
-
|
|
|
// 8. 查询最近一次完课记录(不限直播间),计算连续天数
|
|
|
LiveCompletionPointsRecord latestRecord = recordMapper.selectLatestByUser(userId);
|
|
|
- int continuousDays = 1;
|
|
|
-
|
|
|
- if (latestRecord != null) {
|
|
|
- LocalDate lastDate = latestRecord.getCurrentCompletionDate()
|
|
|
- .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
|
|
-
|
|
|
- long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
|
|
|
-
|
|
|
- if (daysBetween == 0) {
|
|
|
- continuousDays = latestRecord.getContinuousDays();
|
|
|
-
|
|
|
- } else if (daysBetween == 1) {
|
|
|
- // 昨天完课了,连续天数+1
|
|
|
- continuousDays = latestRecord.getContinuousDays() + 1;
|
|
|
- } else {
|
|
|
- // 中断了,重新开始
|
|
|
- continuousDays = 1;
|
|
|
- }
|
|
|
- }
|
|
|
+ int continuousDays = calcContinuousDays(latestRecord, today);
|
|
|
|
|
|
// 9. 计算积分
|
|
|
int points = calculatePoints(continuousDays, pointsConfig);
|
|
|
@@ -181,16 +172,58 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
record.setLastCompletionDate(latestRecord.getCurrentCompletionDate());
|
|
|
}
|
|
|
|
|
|
- recordMapper.insertRecord(record);
|
|
|
+ try {
|
|
|
+ recordMapper.insertRecord(record);
|
|
|
+ } catch (org.springframework.dao.DuplicateKeyException e) {
|
|
|
+ // 实时推送与定时任务并发时可能撞唯一键,视为已存在
|
|
|
+ log.info("完课记录已存在(并发), liveId={}, userId={}", liveId, userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
|
|
|
return record;
|
|
|
|
|
|
+ } catch (org.springframework.dao.DuplicateKeyException e) {
|
|
|
+ log.info("完课记录已存在(并发), liveId={}, userId={}", liveId, userId);
|
|
|
+ return null;
|
|
|
} catch (Exception e) {
|
|
|
log.error("检查并创建完课记录失败, liveId={}, userId={}", liveId, userId, e);
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /** 记录是否属于当前开播场次(create_time >= startTime) */
|
|
|
+ private boolean isRecordInCurrentSession(LiveCompletionPointsRecord record, LocalDateTime startTime) {
|
|
|
+ if (record == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (startTime == null) {
|
|
|
+ // 无开播时间时,当天有记录即视为已处理
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ Date recordTime = record.getCreateTime() != null ? record.getCreateTime() : record.getCurrentCompletionDate();
|
|
|
+ if (recordTime == null) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ LocalDateTime recordLdt = recordTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
|
|
+ return !recordLdt.isBefore(startTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ private int calcContinuousDays(LiveCompletionPointsRecord latestRecord, LocalDate today) {
|
|
|
+ int continuousDays = 1;
|
|
|
+ if (latestRecord == null || latestRecord.getCurrentCompletionDate() == null) {
|
|
|
+ return continuousDays;
|
|
|
+ }
|
|
|
+ LocalDate lastDate = latestRecord.getCurrentCompletionDate()
|
|
|
+ .toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
|
|
+ long daysBetween = ChronoUnit.DAYS.between(lastDate, today);
|
|
|
+ if (daysBetween == 0) {
|
|
|
+ continuousDays = latestRecord.getContinuousDays() != null ? latestRecord.getContinuousDays() : 1;
|
|
|
+ } else if (daysBetween == 1) {
|
|
|
+ continuousDays = (latestRecord.getContinuousDays() != null ? latestRecord.getContinuousDays() : 0) + 1;
|
|
|
+ }
|
|
|
+ return continuousDays;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 用户领取完课积分
|
|
|
*/
|
|
|
@@ -214,10 +247,12 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
}
|
|
|
|
|
|
// 4. 发放积分并写入 fs_user_integral_logs(logType=27 直播完课积分)
|
|
|
+ // businessId 带上 createTime,同日重开复用同一 recordId 时可再领
|
|
|
FsUserAddIntegralTemplateParam integralParam = new FsUserAddIntegralTemplateParam();
|
|
|
integralParam.setUserId(userId);
|
|
|
integralParam.setLogType(FsUserIntegralLogTypeEnum.TYPE_27);
|
|
|
- integralParam.setBusinessId("live_completion_" + recordId);
|
|
|
+ long sessionTs = record.getCreateTime() != null ? record.getCreateTime().getTime() : System.currentTimeMillis();
|
|
|
+ integralParam.setBusinessId("live_completion_" + recordId + "_" + sessionTs);
|
|
|
integralParam.setPoints(Long.valueOf(record.getPointsAwarded()));
|
|
|
R integralResult = fsUserIntegralLogsService.addIntegralTemplate(integralParam);
|
|
|
if (integralResult == null || !"200".equals(String.valueOf(integralResult.get("code")))) {
|
|
|
@@ -389,12 +424,12 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
config.setEnabled(false);
|
|
|
config.setCompletionRate(null);
|
|
|
config.setPointsConfig(null);
|
|
|
-
|
|
|
+
|
|
|
String configJson = live.getConfigJson();
|
|
|
if (configJson == null || configJson.isEmpty()) {
|
|
|
return config;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
JSONObject jsonConfig = JSON.parseObject(configJson);
|
|
|
|
|
|
@@ -416,10 +451,10 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
} catch (Exception e) {
|
|
|
log.warn("解析完课积分配置失败, liveId={}, 配置未生效", live.getLiveId(), e);
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
return config;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 计算积分
|
|
|
* 根据连续天数和积分配置计算应得积分
|
|
|
@@ -437,7 +472,7 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
}
|
|
|
return pointsConfig[continuousDays - 1];
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
/**
|
|
|
* 完课积分配置内部类
|
|
|
*/
|
|
|
@@ -445,27 +480,27 @@ public class LiveCompletionPointsRecordServiceImpl implements ILiveCompletionPoi
|
|
|
private boolean enabled;
|
|
|
private Integer completionRate;
|
|
|
private int[] pointsConfig;
|
|
|
-
|
|
|
+
|
|
|
public boolean isEnabled() {
|
|
|
return enabled;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setEnabled(boolean enabled) {
|
|
|
this.enabled = enabled;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public Integer getCompletionRate() {
|
|
|
return completionRate;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setCompletionRate(Integer completionRate) {
|
|
|
this.completionRate = completionRate;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public int[] getPointsConfig() {
|
|
|
return pointsConfig;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
public void setPointsConfig(int[] pointsConfig) {
|
|
|
this.pointsConfig = pointsConfig;
|
|
|
}
|