dramaTypes = DramaMultiTagSupport.normalizeDramaTypes(req.getDramaTypes());
@@ -109,6 +111,8 @@ public class DramaService {
@Transactional(rollbackFor = Exception.class)
public void update(Long id, DramaSaveReqVO req) {
Drama drama = getExisting(id);
+ Integer oldFreeEpisodes = drama.getFreeEpisodes();
+ Integer oldUnlockCoin = drama.getUnlockCoin();
apply(drama, req, false);
if (req.getFavoriteCount() != null) {
drama.setFavoriteCount(nonNegative(req.getFavoriteCount(), "收藏数"));
@@ -120,6 +124,18 @@ public class DramaService {
drama.setViewCount(nonNegative(req.getHeat(), "热度"));
}
dramaMapper.updateById(drama);
+ syncEpisodePricesIfNeeded(drama, oldFreeEpisodes, oldUnlockCoin);
+ }
+
+ /** 免费集数或解锁金币变化时,按定价规则回写分集。 */
+ private void syncEpisodePricesIfNeeded(Drama drama, Integer oldFreeEpisodes, Integer oldUnlockCoin) {
+ if (Objects.equals(oldFreeEpisodes, drama.getFreeEpisodes())
+ && Objects.equals(oldUnlockCoin, drama.getUnlockCoin())) {
+ return;
+ }
+ int freeEpisodes = drama.getFreeEpisodes() == null ? 0 : Math.max(0, drama.getFreeEpisodes());
+ int unlockCoin = drama.getUnlockCoin() == null ? 0 : Math.max(0, drama.getUnlockCoin());
+ episodePriceSync.syncByDramaRule(drama.getId(), freeEpisodes, unlockCoin);
}
@Transactional(rollbackFor = Exception.class)
diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/EpisodePriceSync.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/EpisodePriceSync.java
new file mode 100644
index 0000000..9e2bd1e
--- /dev/null
+++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/EpisodePriceSync.java
@@ -0,0 +1,62 @@
+// Author: fruit.com.cn
+
+package com.fruitshorts.content.service;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fruitshorts.content.dal.entity.DramaEpisode;
+import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 按短剧免费集数规则同步分集 {@code is_free} / {@code unlock_coin}。
+ *
+ * 定价页与短剧编辑共用同一套规则,避免两处口径漂移。
+ */
+@Component
+@RequiredArgsConstructor
+public class EpisodePriceSync {
+
+ private final DramaEpisodeMapper dramaEpisodeMapper;
+
+ /**
+ * 全量同步某剧全部分集价格。
+ *
+ * 规则:{@code epNo > 0 && epNo <= freeEpisodes} 为免费且 coin=0,
+ * 否则收费且 coin={@code unlockCoin}。
+ *
+ * @return 实际写库的分集数
+ */
+ public int syncByDramaRule(Long dramaId, int freeEpisodes, int unlockCoin) {
+ List episodes = dramaEpisodeMapper.selectList(Wrappers.lambdaQuery()
+ .eq(DramaEpisode::getDramaId, dramaId)
+ .orderByAsc(DramaEpisode::getEpNo));
+ int changed = 0;
+ for (DramaEpisode episode : episodes) {
+ int epNo = episode.getEpNo() == null ? 0 : episode.getEpNo();
+ int isFree = resolveIsFree(epNo, freeEpisodes);
+ int coin = resolveUnlockCoin(isFree, unlockCoin);
+ if (!Objects.equals(episode.getIsFree(), isFree)
+ || !Objects.equals(episode.getUnlockCoin(), coin)) {
+ episode.setIsFree(isFree);
+ episode.setUnlockCoin(coin);
+ dramaEpisodeMapper.updateById(episode);
+ changed++;
+ }
+ }
+ return changed;
+ }
+
+ /** 集号落在免费区间内则为免费。 */
+ public static int resolveIsFree(int epNo, int freeEpisodes) {
+ return epNo > 0 && epNo <= freeEpisodes ? 1 : 0;
+ }
+
+ /** 免费集 coin 置 0,收费集用短剧解锁金币。 */
+ public static int resolveUnlockCoin(int isFree, int unlockCoin) {
+ return isFree == 1 ? 0 : unlockCoin;
+ }
+}
diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/PriceService.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/PriceService.java
index 3bb5c49..af65333 100644
--- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/PriceService.java
+++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/PriceService.java
@@ -32,6 +32,7 @@ public class PriceService {
private final DramaService dramaService;
private final DramaMapper dramaMapper;
private final DramaEpisodeMapper dramaEpisodeMapper;
+ private final EpisodePriceSync episodePriceSync;
/** 读取当前价格配置:后台定价页进入时先看现状再改。 */
public PriceRespVO detail(Long dramaId) {
@@ -72,55 +73,13 @@ public class PriceService {
drama.setUnlockCoin(unlockCoin);
dramaMapper.updateById(drama);
+ int changed = episodePriceSync.syncByDramaRule(drama.getId(), freeEpisodes, unlockCoin);
+ changed += applyEpisodeOverrides(drama.getId(), unlockCoin, req);
+
List episodes = dramaEpisodeMapper.selectList(Wrappers.lambdaQuery()
.eq(DramaEpisode::getDramaId, drama.getId())
.orderByAsc(DramaEpisode::getEpNo));
- Map overrides = new LinkedHashMap<>();
- if (req.getEpisodes() != null) {
- for (PriceSaveReqVO.EpisodePrice item : req.getEpisodes()) {
- if (item != null && item.getEpisodeId() != null) {
- overrides.put(item.getEpisodeId(), item);
- }
- }
- }
-
- int changed = 0;
- for (DramaEpisode episode : episodes) {
- int epNo = episode.getEpNo() == null ? 0 : episode.getEpNo();
- int isFree = epNo > 0 && epNo <= freeEpisodes ? 1 : 0;
- int coin = isFree == 1 ? 0 : unlockCoin;
-
- PriceSaveReqVO.EpisodePrice override = overrides.get(episode.getId());
- Integer previewSeconds = null;
- if (override != null) {
- if (override.getIsFree() != null) {
- isFree = override.getIsFree() == 1 ? 1 : 0;
- coin = isFree == 1 ? 0 : unlockCoin;
- }
- if (isFree == 0 && override.getUnlockCoin() != null) {
- coin = Math.max(0, override.getUnlockCoin());
- }
- if (override.getPreviewSeconds() != null) {
- previewSeconds = Math.max(0, override.getPreviewSeconds());
- }
- }
-
- boolean priceChanged = !Objects.equals(episode.getIsFree(), isFree)
- || !Objects.equals(episode.getUnlockCoin(), coin);
- boolean previewChanged = previewSeconds != null
- && !Objects.equals(episode.getPreviewSeconds(), previewSeconds);
- if (priceChanged || previewChanged) {
- episode.setIsFree(isFree);
- episode.setUnlockCoin(coin);
- if (previewSeconds != null) {
- episode.setPreviewSeconds(previewSeconds);
- }
- dramaEpisodeMapper.updateById(episode);
- changed++;
- }
- }
-
PriceRespVO vo = new PriceRespVO();
vo.setDramaId(drama.getId());
vo.setFreeEpisodes(freeEpisodes);
@@ -139,4 +98,56 @@ public class PriceService {
}).toList());
return vo;
}
+
+ /** 在整剧规则同步后再落单集覆盖(含试看秒数)。 */
+ private int applyEpisodeOverrides(Long dramaId, int unlockCoin, PriceSaveReqVO req) {
+ Map overrides = new LinkedHashMap<>();
+ if (req.getEpisodes() != null) {
+ for (PriceSaveReqVO.EpisodePrice item : req.getEpisodes()) {
+ if (item != null && item.getEpisodeId() != null) {
+ overrides.put(item.getEpisodeId(), item);
+ }
+ }
+ }
+ if (overrides.isEmpty()) {
+ return 0;
+ }
+ List episodes = dramaEpisodeMapper.selectList(Wrappers.lambdaQuery()
+ .eq(DramaEpisode::getDramaId, dramaId)
+ .orderByAsc(DramaEpisode::getEpNo));
+ int changed = 0;
+ for (DramaEpisode episode : episodes) {
+ PriceSaveReqVO.EpisodePrice override = overrides.get(episode.getId());
+ if (override == null) {
+ continue;
+ }
+ int isFree = episode.getIsFree() == null ? 0 : episode.getIsFree();
+ int coin = episode.getUnlockCoin() == null ? 0 : episode.getUnlockCoin();
+ Integer previewSeconds = null;
+ if (override.getIsFree() != null) {
+ isFree = override.getIsFree() == 1 ? 1 : 0;
+ coin = EpisodePriceSync.resolveUnlockCoin(isFree, unlockCoin);
+ }
+ if (isFree == 0 && override.getUnlockCoin() != null) {
+ coin = Math.max(0, override.getUnlockCoin());
+ }
+ if (override.getPreviewSeconds() != null) {
+ previewSeconds = Math.max(0, override.getPreviewSeconds());
+ }
+ boolean priceChanged = !Objects.equals(episode.getIsFree(), isFree)
+ || !Objects.equals(episode.getUnlockCoin(), coin);
+ boolean previewChanged = previewSeconds != null
+ && !Objects.equals(episode.getPreviewSeconds(), previewSeconds);
+ if (priceChanged || previewChanged) {
+ episode.setIsFree(isFree);
+ episode.setUnlockCoin(coin);
+ if (previewSeconds != null) {
+ episode.setPreviewSeconds(previewSeconds);
+ }
+ dramaEpisodeMapper.updateById(episode);
+ changed++;
+ }
+ }
+ return changed;
+ }
}
diff --git a/fruitshorts-module-content/src/test/java/com/fruitshorts/content/service/DramaServiceTest.java b/fruitshorts-module-content/src/test/java/com/fruitshorts/content/service/DramaServiceTest.java
index 22e6fac..4b4baca 100644
--- a/fruitshorts-module-content/src/test/java/com/fruitshorts/content/service/DramaServiceTest.java
+++ b/fruitshorts-module-content/src/test/java/com/fruitshorts/content/service/DramaServiceTest.java
@@ -58,8 +58,9 @@ class DramaServiceTest {
dramaEpisodeMapper = mock(DramaEpisodeMapper.class);
DramaReviewRecordMapper reviewRecordMapper = mock(DramaReviewRecordMapper.class);
DramaCategoryService categoryService = mock(DramaCategoryService.class);
+ EpisodePriceSync episodePriceSync = new EpisodePriceSync(dramaEpisodeMapper);
dramaService = new DramaService(
- dramaMapper, dramaEpisodeMapper, reviewRecordMapper, categoryService);
+ dramaMapper, dramaEpisodeMapper, reviewRecordMapper, categoryService, episodePriceSync);
episodes = new ArrayList<>();
given(dramaEpisodeMapper.selectList(any())).willAnswer(invocation -> List.copyOf(episodes));
given(dramaEpisodeMapper.updateById(any(DramaEpisode.class))).willAnswer(invocation -> {