feat: 08 创作者与内容审核落地(入驻审核 / 内容提交审核 / 定时发布 / 分集定价)
为什么: 1) 内容是创作者供的,必须有一条可管控的供给链路:创作者入驻要审、内容提交要审、 审核通过才能真正上架 —— 否则平台无法对内容和主体负责。 2) 审核不能绕过状态机。内容提交审核通过后,是顺着短剧状态机 SUBMIT → APPROVE → PUBLISH 推上去,而不是直接写 status 字段, 这样审核记录与操作日志都不会缺环。 3) 定时发布(publish_at 到点自动上线)同样走状态机,并且做成幂等任务: 重复执行不会把已上线的剧再推一次。 4) 定价落在 is_free / unlock_coin 上,与权益表解耦:改价立即影响播放与下载判定, 但已购用户的权益不受影响,不会因为改价把用户买过的东西收回去。 5) 补齐 PM 草稿未做的部分:5 个权限点 + 3 条后台菜单(含前端组件路径)+ 超管授权(幂等), 以及后台定价页需要的价格读取接口 GET /admin/prices。
This commit is contained in:
1 parent
7e730fbbca
commit
1fb8427f28
25 files changed
+935
-3
No files matched your search
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 定时发布:把已到 publish_at 的待发布短剧推上线。 */
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DramaPublishJob {
|
||||
|
||||
private final DramaService dramaService;
|
||||
|
||||
@Scheduled(cron = "${fruitshorts.content.publish-cron:0 * * * * *}")
|
||||
public void run() {
|
||||
int published = dramaService.publishDue();
|
||||
if (published > 0) {
|
||||
log.info("定时发布:本轮上线 {} 部短剧", published);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
@@ -143,6 +143,24 @@ public class DramaService {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时发布:把已到 publish_at 的待发布短剧推上线。
|
||||
* 状态机不允许把 approved 直接写库,所以逐条走 PUBLISH 动作,顺带留下审核记录。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int publishDue() {
|
||||
List<Drama> due = dramaMapper.selectList(Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getStatus, ContentStatus.APPROVED.getCode())
|
||||
.isNotNull(Drama::getPublishAt)
|
||||
.le(Drama::getPublishAt, LocalDateTime.now()));
|
||||
int published = 0;
|
||||
for (Drama drama : due) {
|
||||
changeStatus(drama.getId(), "PUBLISH", "定时发布到点", 0L, "system");
|
||||
published++;
|
||||
}
|
||||
return published;
|
||||
}
|
||||
|
||||
public List<DramaReviewRecord> listReviewRecords(Long dramaId) {
|
||||
return dramaReviewRecordMapper.selectList(Wrappers.<DramaReviewRecord>lambdaQuery()
|
||||
.eq(DramaReviewRecord::getBizType, "DRAMA")
|
||||
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
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.vo.PriceRespVO;
|
||||
import com.fruitshorts.content.vo.PriceSaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 分集价格与免费集配置。
|
||||
*
|
||||
* <p>价格直接落在 {@code drama_episode.is_free / unlock_coin}(未单独指定时回落到 {@code drama.unlock_coin}),
|
||||
* 播放与下载的付费判定读的就是这两个字段,因此改价**立即生效**;已购用户的权益在 {@code entitlement},
|
||||
* 与价格解耦,改价不会回收或重算已购权益。</p>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PriceService {
|
||||
|
||||
private final DramaService dramaService;
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
|
||||
/** 读取当前价格配置:后台定价页进入时先看现状再改。 */
|
||||
public PriceRespVO detail(Long dramaId) {
|
||||
Drama drama = dramaService.getExisting(dramaId);
|
||||
List<DramaEpisode> episodes = dramaEpisodeMapper.selectList(Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getDramaId, drama.getId())
|
||||
.orderByAsc(DramaEpisode::getEpNo));
|
||||
PriceRespVO vo = new PriceRespVO();
|
||||
vo.setDramaId(drama.getId());
|
||||
vo.setFreeEpisodes(drama.getFreeEpisodes());
|
||||
vo.setUnlockCoin(drama.getUnlockCoin());
|
||||
vo.setEpisodeCount(episodes.size());
|
||||
vo.setChangedCount(0);
|
||||
vo.setEpisodes(episodes.stream().map(episode -> {
|
||||
PriceRespVO.DramaEpisodePriceVO item = new PriceRespVO.DramaEpisodePriceVO();
|
||||
item.setEpisodeId(episode.getId());
|
||||
item.setNumber(episode.getEpNo());
|
||||
item.setTitle(episode.getTitle());
|
||||
item.setFree(episode.getIsFree() != null && episode.getIsFree() == 1);
|
||||
item.setUnlockCoin(episode.getUnlockCoin());
|
||||
return item;
|
||||
}).toList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PriceRespVO save(PriceSaveReqVO req) {
|
||||
Drama drama = dramaService.getExisting(req.getDramaId());
|
||||
int freeEpisodes = req.getFreeEpisodes() != null
|
||||
? Math.max(0, req.getFreeEpisodes())
|
||||
: (drama.getFreeEpisodes() == null ? 0 : drama.getFreeEpisodes());
|
||||
int unlockCoin = req.getUnlockCoin() != null
|
||||
? Math.max(0, req.getUnlockCoin())
|
||||
: (drama.getUnlockCoin() == null ? 0 : drama.getUnlockCoin());
|
||||
|
||||
drama.setFreeEpisodes(freeEpisodes);
|
||||
drama.setUnlockCoin(unlockCoin);
|
||||
dramaMapper.updateById(drama);
|
||||
|
||||
List<DramaEpisode> episodes = dramaEpisodeMapper.selectList(Wrappers.<DramaEpisode>lambdaQuery()
|
||||
.eq(DramaEpisode::getDramaId, drama.getId())
|
||||
.orderByAsc(DramaEpisode::getEpNo));
|
||||
|
||||
Map<Long, PriceSaveReqVO.EpisodePrice> 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());
|
||||
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 (!Objects.equals(episode.getIsFree(), isFree) || !Objects.equals(episode.getUnlockCoin(), coin)) {
|
||||
episode.setIsFree(isFree);
|
||||
episode.setUnlockCoin(coin);
|
||||
dramaEpisodeMapper.updateById(episode);
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
|
||||
PriceRespVO vo = new PriceRespVO();
|
||||
vo.setDramaId(drama.getId());
|
||||
vo.setFreeEpisodes(freeEpisodes);
|
||||
vo.setUnlockCoin(unlockCoin);
|
||||
vo.setEpisodeCount(episodes.size());
|
||||
vo.setChangedCount(changed);
|
||||
vo.setEpisodes(episodes.stream().map(episode -> {
|
||||
PriceRespVO.DramaEpisodePriceVO item = new PriceRespVO.DramaEpisodePriceVO();
|
||||
item.setEpisodeId(episode.getId());
|
||||
item.setNumber(episode.getEpNo());
|
||||
item.setTitle(episode.getTitle());
|
||||
item.setFree(episode.getIsFree() != null && episode.getIsFree() == 1);
|
||||
item.setUnlockCoin(episode.getUnlockCoin());
|
||||
return item;
|
||||
}).toList());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 价格配置结果:便于运营确认改动范围。 */
|
||||
@Data
|
||||
public class PriceRespVO {
|
||||
|
||||
private Long dramaId;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
/** 该剧剧集总数 */
|
||||
private Integer episodeCount;
|
||||
/** 本次实际发生变化的剧集数 */
|
||||
private Integer changedCount;
|
||||
/** 剧集明细:后台定价页要按集展示当前价格与免费状态 */
|
||||
private List<DramaEpisodePriceVO> episodes;
|
||||
|
||||
@Data
|
||||
public static class DramaEpisodePriceVO {
|
||||
private Long episodeId;
|
||||
private Integer number;
|
||||
private String title;
|
||||
private Boolean free;
|
||||
private Integer unlockCoin;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.fruitshorts.content.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 分集价格与免费集配置。 */
|
||||
@Data
|
||||
public class PriceSaveReqVO {
|
||||
|
||||
private Long dramaId;
|
||||
/** 整剧免费集数:前 N 集免费,其余按 unlockCoin 收费 */
|
||||
private Integer freeEpisodes;
|
||||
/** 整剧默认解锁金币(逐集未单独指定时生效) */
|
||||
private Integer unlockCoin;
|
||||
/** 逐集覆盖,可空;列表里的集优先于整剧默认值 */
|
||||
private List<EpisodePrice> episodes;
|
||||
|
||||
@Data
|
||||
public static class EpisodePrice {
|
||||
private Long episodeId;
|
||||
/** 1 免费 / 0 收费 */
|
||||
private Integer isFree;
|
||||
private Integer unlockCoin;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user