fix(playback): 恢复已删除的观看进度

This commit is contained in:
Zhou Weidong committed 2026-10-23 12:34:13 +08:00
1 parent 5a80e6693f
commit 9c25b1b397
4 files changed
+111 -20

No files matched your search

@@ -15,6 +15,7 @@ import com.fruitshorts.playback.dal.entity.WatchProgress;
import com.fruitshorts.playback.dal.mapper.WatchHistoryMapper;
import com.fruitshorts.playback.dal.mapper.WatchProgressMapper;
import com.fruitshorts.playback.vo.ProgressRespVO;
import com.fruitshorts.playback.vo.ProgressSaveReqVO;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -28,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -108,6 +110,30 @@ class WatchProgressServiceTest {
return progress;
}
@Test
void should_revive_deleted_progress_when_same_drama_is_reported_again() {
DramaEpisode episode = episode(88L, 1);
episode.setDramaId(5L);
given(episodeService.getPublished(88L)).willReturn(episode);
given(dramaService.getPublished(5L)).willReturn(drama(5L, "剧名", 10));
WatchProgress hidden = progress(5L, 88L);
hidden.setId(9L);
hidden.setUserId(7L);
hidden.setDeleted(1);
given(progressMapper.selectAny(7L, 5L)).willReturn(hidden);
given(progressMapper.selectById(9L)).willReturn(hidden);
ProgressSaveReqVO req = new ProgressSaveReqVO();
req.setEpisodeId(88L);
req.setPositionMs(1_000L);
req.setDurationMs(2_000L);
progressService.save(7L, req);
verify(progressMapper, never()).insert(any(WatchProgress.class));
verify(progressMapper).updateAny(hidden);
assertThat(hidden.getDeleted()).isZero();
}
private Drama drama(Long id, String title, int totalEpisodes) {
Drama drama = new Drama();
drama.setId(id);
@@ -4,6 +4,32 @@ package com.fruitshorts.playback.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.playback.dal.entity.WatchProgress;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
public interface WatchProgressMapper extends BaseMapper<WatchProgress> {
/** 含逻辑删除行。唯一索引不区分 deleted,再看同一部剧必须能找回旧行。 */
@Select("""
SELECT * FROM watch_progress
WHERE user_id = #{userId} AND drama_id = #{dramaId}
LIMIT 1
""")
WatchProgress selectAny(@Param("userId") Long userId, @Param("dramaId") Long dramaId);
/** 恢复并更新。手写 SQL 不带 deleted=0 条件,逻辑删除行也能写回。 */
@Update("""
UPDATE watch_progress SET
deleted = 0,
episode_id = #{episodeId},
position_ms = #{positionMs},
duration_ms = #{durationMs},
finished = #{finished},
play_count = #{playCount},
device_id = #{deviceId},
source = #{source}
WHERE id = #{id}
""")
int updateAny(WatchProgress progress);
}
@@ -17,6 +17,7 @@ import com.fruitshorts.playback.dal.mapper.WatchProgressMapper;
import com.fruitshorts.playback.vo.ProgressRespVO;
import com.fruitshorts.playback.vo.ProgressSaveReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -58,30 +59,23 @@ public class WatchProgressService {
boolean finished = Boolean.TRUE.equals(req.getFinished())
|| (duration > 0 && position + FINISH_THRESHOLD_MS >= duration);
WatchProgress existing = progressMapper.selectOne(Wrappers.<WatchProgress>lambdaQuery()
.eq(WatchProgress::getUserId, userId)
.eq(WatchProgress::getDramaId, drama.getId())
.last("limit 1"));
if (existing == null) {
WatchProgress progress = new WatchProgress();
progress.setUserId(userId);
progress.setDramaId(drama.getId());
progress.setEpisodeId(episode.getId());
progress.setPositionMs(position);
progress.setDurationMs(duration);
progress.setFinished(finished ? 1 : 0);
progress.setPlayCount(1);
progress.setDeviceId(req.getDeviceId() == null ? "" : req.getDeviceId());
progress.setSource(req.getSource() == null ? "" : req.getSource());
progressMapper.insert(progress);
LoadedProgress loaded = loadForSave(userId, drama.getId(), episode, position, duration,
finished, req);
WatchProgress existing = loaded.progress();
if (loaded.created()) {
appendHistory(userId, drama.getId(), episode.getId(), position, duration,
progress.getSource(), progress.getDeviceId());
// updated_at 由触发器维护,回读一次,保证响应与后续 recent 列表口径一致
return toResp(progressMapper.selectById(progress.getId()));
existing.getSource(), existing.getDeviceId());
return toResp(progressMapper.selectById(existing.getId()));
}
boolean hidden = existing.getDeleted() != null && existing.getDeleted() != 0;
boolean sameEpisode = existing.getEpisodeId() != null
&& existing.getEpisodeId().equals(episode.getId());
if (hidden && sameEpisode) {
existing.setPlayCount((existing.getPlayCount() == null ? 0 : existing.getPlayCount()) + 1);
appendHistory(userId, drama.getId(), episode.getId(), position, duration,
req.getSource(), existing.getDeviceId());
}
if (!sameEpisode) {
// 换集:直接采纳新集与新的播放位置
existing.setEpisodeId(episode.getId());
@@ -103,10 +97,52 @@ public class WatchProgressService {
}
existing.setDeviceId(req.getDeviceId() == null ? "" : req.getDeviceId());
existing.setSource(req.getSource() == null ? "" : req.getSource());
progressMapper.updateById(existing);
if (hidden) {
existing.setDeleted(0);
progressMapper.updateAny(existing);
} else {
progressMapper.updateById(existing);
}
return toResp(progressMapper.selectById(existing.getId()));
}
private LoadedProgress loadForSave(Long userId, Long dramaId, DramaEpisode episode, long position,
long duration, boolean finished, ProgressSaveReqVO req) {
WatchProgress existing = progressMapper.selectAny(userId, dramaId);
if (existing != null) {
return new LoadedProgress(existing, false);
}
WatchProgress created = newProgress(userId, dramaId, episode, position, duration, finished, req);
try {
progressMapper.insert(created);
return new LoadedProgress(created, true);
} catch (DuplicateKeyException ex) {
WatchProgress raced = progressMapper.selectAny(userId, dramaId);
if (raced == null) {
throw ex;
}
return new LoadedProgress(raced, false);
}
}
private WatchProgress newProgress(Long userId, Long dramaId, DramaEpisode episode, long position,
long duration, boolean finished, ProgressSaveReqVO req) {
WatchProgress progress = new WatchProgress();
progress.setUserId(userId);
progress.setDramaId(dramaId);
progress.setEpisodeId(episode.getId());
progress.setPositionMs(position);
progress.setDurationMs(duration);
progress.setFinished(finished ? 1 : 0);
progress.setPlayCount(1);
progress.setDeviceId(req.getDeviceId() == null ? "" : req.getDeviceId());
progress.setSource(req.getSource() == null ? "" : req.getSource());
return progress;
}
private record LoadedProgress(WatchProgress progress, boolean created) {
}
public ProgressRespVO ofDrama(Long userId, Long dramaId) {
WatchProgress progress = progressMapper.selectOne(Wrappers.<WatchProgress>lambdaQuery()
.eq(WatchProgress::getUserId, userId)
@@ -185,6 +221,7 @@ public class WatchProgressService {
vo.setDramaTitle(drama.getTitle());
vo.setDramaCover(drama.getCoverUrl());
vo.setTotalEpisodes(drama.getTotalEpisodes());
vo.setViewCount(drama.getViewCount());
}
DramaEpisode episode = episodeById.get(progress.getEpisodeId());
if (episode != null) {
@@ -20,6 +20,8 @@ public class ProgressRespVO {
private Integer episodeNumber;
/** 短剧总集数;短剧不存在时为空。 */
private Integer totalEpisodes;
/** 短剧热度,取 drama.view_count。不是用户自己的播放次数。 */
private Long viewCount;
private Long positionMs;
private Long durationMs;
private Boolean finished;