test(content): 覆盖短剧免费集数同步分集价格

This commit is contained in:
Zhou Weidong committed 2026-10-21 22:34:21 +08:00
1 parent 979e6ebea3
commit d7f226659c
3 files changed
+224

No files matched your search

@@ -0,0 +1,30 @@
# 2026-10-21 | 短剧保存免费集数同步分集
## 目标
后台短剧编辑改 `freeEpisodes` / `unlockCoin` 后,同步 `drama_episode.is_free` / `unlock_coin`,与定价页 `PriceService.save()` 规则一致。
## 方案
1. 抽出 `EpisodePriceSync.syncByDramaRule(dramaId, freeEpisodes, unlockCoin)`,供 `DramaService.update` 与 `PriceService.save` 共用。
2. `DramaService.update` 在 `updateById` 后,仅当免费集数或解锁金币相对更新前有变化时调用同步。
3. TDD:先写 `DramaServiceTest` 复现失败,再改实现转绿。
## 范围
- 仅 `fruitshorts-module-content`
- 不改 Controller / VO / 契约 / 前端
## 门禁
```bash
mvn -pl fruitshorts-module-content test -Dtest=DramaServiceTest
mvn -pl fruitshorts-module-content test
```
## 分支 / 提交
- `feature/drama-free-episodes-sync` ← `develop`
- `test(content): 覆盖短剧免费集数同步分集价格`
- `fix(content): 短剧保存免费集数时同步分集免费状态`
- `merge --no-ff` 回 develop 并 push
+5
View File
@@ -25,5 +25,10 @@
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-system</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,189 @@
// Author: fruit.com.cn
package com.fruitshorts.content.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.content.dal.mapper.DramaReviewRecordMapper;
import com.fruitshorts.content.vo.DramaSaveReqVO;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
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;
/**
* 短剧保存时免费集数 / 解锁金币须同步分集价格。
*/
class DramaServiceTest {
private static final long DRAMA_ID = 1001L;
private static final int UNLOCK_COIN = 60;
private DramaMapper dramaMapper;
private DramaEpisodeMapper dramaEpisodeMapper;
private DramaService dramaService;
private List<DramaEpisode> episodes;
@BeforeAll
static void initTableInfoCache() {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(assistant, Drama.class);
TableInfoHelper.initTableInfo(assistant, DramaEpisode.class);
}
@BeforeEach
void setUp() {
dramaMapper = mock(DramaMapper.class);
dramaEpisodeMapper = mock(DramaEpisodeMapper.class);
DramaReviewRecordMapper reviewRecordMapper = mock(DramaReviewRecordMapper.class);
DramaCategoryService categoryService = mock(DramaCategoryService.class);
dramaService = new DramaService(
dramaMapper, dramaEpisodeMapper, reviewRecordMapper, categoryService);
episodes = new ArrayList<>();
given(dramaEpisodeMapper.selectList(any())).willAnswer(invocation -> List.copyOf(episodes));
given(dramaEpisodeMapper.updateById(any(DramaEpisode.class))).willAnswer(invocation -> {
DramaEpisode updated = invocation.getArgument(0);
for (int i = 0; i < episodes.size(); i++) {
if (episodes.get(i).getId().equals(updated.getId())) {
episodes.set(i, updated);
break;
}
}
return 1;
});
}
@Test
void should_mark_episodes_4_and_5_free_when_free_episodes_increase_from_3_to_5() {
givenExistingDrama(3, UNLOCK_COIN);
seedEpisodesAlignedToFree(3, 6);
dramaService.update(DRAMA_ID, saveReq(5, UNLOCK_COIN));
Map<Integer, DramaEpisode> byEpNo = episodeMap();
assertThat(byEpNo.get(4).getIsFree()).isEqualTo(1);
assertThat(byEpNo.get(4).getUnlockCoin()).isZero();
assertThat(byEpNo.get(5).getIsFree()).isEqualTo(1);
assertThat(byEpNo.get(5).getUnlockCoin()).isZero();
assertThat(byEpNo.get(6).getIsFree()).isZero();
assertThat(byEpNo.get(6).getUnlockCoin()).isEqualTo(UNLOCK_COIN);
verify(dramaEpisodeMapper, times(2)).updateById(any(DramaEpisode.class));
}
@Test
void should_mark_episodes_3_to_5_paid_when_free_episodes_decrease_from_5_to_2() {
givenExistingDrama(5, UNLOCK_COIN);
seedEpisodesAlignedToFree(5, 5);
dramaService.update(DRAMA_ID, saveReq(2, UNLOCK_COIN));
Map<Integer, DramaEpisode> byEpNo = episodeMap();
assertThat(byEpNo.get(3).getIsFree()).isZero();
assertThat(byEpNo.get(3).getUnlockCoin()).isEqualTo(UNLOCK_COIN);
assertThat(byEpNo.get(4).getIsFree()).isZero();
assertThat(byEpNo.get(4).getUnlockCoin()).isEqualTo(UNLOCK_COIN);
assertThat(byEpNo.get(5).getIsFree()).isZero();
assertThat(byEpNo.get(5).getUnlockCoin()).isEqualTo(UNLOCK_COIN);
verify(dramaEpisodeMapper, times(3)).updateById(any(DramaEpisode.class));
}
@Test
void should_skip_episode_updates_when_free_episodes_unchanged() {
givenExistingDrama(3, UNLOCK_COIN);
seedEpisodesAlignedToFree(3, 4);
AtomicInteger episodeUpdates = new AtomicInteger();
given(dramaEpisodeMapper.updateById(any(DramaEpisode.class))).willAnswer(invocation -> {
episodeUpdates.incrementAndGet();
return 1;
});
dramaService.update(DRAMA_ID, saveReq(3, UNLOCK_COIN));
assertThat(episodeUpdates.get()).isZero();
verify(dramaEpisodeMapper, never()).selectList(any());
verify(dramaEpisodeMapper, never()).updateById(any(DramaEpisode.class));
}
@Test
void should_not_fail_when_drama_has_no_episodes() {
givenExistingDrama(3, UNLOCK_COIN);
episodes.clear();
assertThatCode(() -> dramaService.update(DRAMA_ID, saveReq(5, UNLOCK_COIN)))
.doesNotThrowAnyException();
verify(dramaEpisodeMapper, never()).updateById(any(DramaEpisode.class));
}
private void givenExistingDrama(int freeEpisodes, int unlockCoin) {
Drama drama = new Drama();
drama.setId(DRAMA_ID);
drama.setTitle("测试短剧");
drama.setFreeEpisodes(freeEpisodes);
drama.setUnlockCoin(unlockCoin);
drama.setSubtitle("");
drama.setGenre("");
drama.setSynopsis("");
drama.setCoverUrl("");
drama.setTrailerUrl("");
drama.setLanguage("id-ID");
drama.setIsHot(0);
drama.setSort(0);
given(dramaMapper.selectById(DRAMA_ID)).willReturn(drama);
given(dramaMapper.updateById(any(Drama.class))).willReturn(1);
}
private void seedEpisodesAlignedToFree(int freeEpisodes, int total) {
episodes.clear();
for (int epNo = 1; epNo <= total; epNo++) {
DramaEpisode episode = new DramaEpisode();
episode.setId((long) epNo);
episode.setDramaId(DRAMA_ID);
episode.setEpNo(epNo);
episode.setTitle("第" + epNo + "集");
boolean free = epNo <= freeEpisodes;
episode.setIsFree(free ? 1 : 0);
episode.setUnlockCoin(free ? 0 : UNLOCK_COIN);
episodes.add(episode);
}
}
private DramaSaveReqVO saveReq(int freeEpisodes, int unlockCoin) {
DramaSaveReqVO req = new DramaSaveReqVO();
req.setTitle("测试短剧");
req.setFreeEpisodes(freeEpisodes);
req.setUnlockCoin(unlockCoin);
return req;
}
private Map<Integer, DramaEpisode> episodeMap() {
ArgumentCaptor<DramaEpisode> captor = ArgumentCaptor.forClass(DramaEpisode.class);
verify(dramaEpisodeMapper, org.mockito.Mockito.atLeastOnce()).updateById(captor.capture());
Map<Integer, DramaEpisode> updated = captor.getAllValues().stream()
.collect(Collectors.toMap(DramaEpisode::getEpNo, Function.identity(), (a, b) -> b));
for (DramaEpisode episode : episodes) {
updated.putIfAbsent(episode.getEpNo(), episode);
}
return updated;
}
}