lambdaQuery()
.eq(DramaReviewRecord::getBizType, "DRAMA")
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
new file mode 100644
index 0000000..ec33d1f
--- /dev/null
+++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/PriceService.java
@@ -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;
+
+/**
+ * 分集价格与免费集配置。
+ *
+ * 价格直接落在 {@code drama_episode.is_free / unlock_coin}(未单独指定时回落到 {@code drama.unlock_coin}),
+ * 播放与下载的付费判定读的就是这两个字段,因此改价**立即生效**;已购用户的权益在 {@code entitlement},
+ * 与价格解耦,改价不会回收或重算已购权益。
+ */
+@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 episodes = dramaEpisodeMapper.selectList(Wrappers.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 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());
+ 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;
+ }
+}
diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceRespVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceRespVO.java
new file mode 100644
index 0000000..758d2e5
--- /dev/null
+++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceRespVO.java
@@ -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 episodes;
+
+ @Data
+ public static class DramaEpisodePriceVO {
+ private Long episodeId;
+ private Integer number;
+ private String title;
+ private Boolean free;
+ private Integer unlockCoin;
+ }
+}
diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceSaveReqVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceSaveReqVO.java
new file mode 100644
index 0000000..6e3fda9
--- /dev/null
+++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/PriceSaveReqVO.java
@@ -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 episodes;
+
+ @Data
+ public static class EpisodePrice {
+ private Long episodeId;
+ /** 1 免费 / 0 收费 */
+ private Integer isFree;
+ private Integer unlockCoin;
+ }
+}
diff --git a/fruitshorts-module-creator/pom.xml b/fruitshorts-module-creator/pom.xml
new file mode 100644
index 0000000..482e19f
--- /dev/null
+++ b/fruitshorts-module-creator/pom.xml
@@ -0,0 +1,31 @@
+
+
+ 4.0.0
+
+
+ com.fruitshorts
+ fruitshorts
+ 1.0.0-SNAPSHOT
+
+
+ fruitshorts-module-creator
+ fruitshorts-module-creator
+ 创作者域:入驻资质审核、内容提交审核流转
+
+
+
+ com.fruitshorts
+ fruitshorts-framework
+
+
+ com.fruitshorts
+ fruitshorts-module-content
+
+
+ com.fruitshorts
+ fruitshorts-module-user
+
+
+
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/ContentSubmission.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/ContentSubmission.java
new file mode 100644
index 0000000..1b20a57
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/ContentSubmission.java
@@ -0,0 +1,39 @@
+package com.fruitshorts.creator.dal.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/** 创作者内容提交与审核流转,对应 content_submission 表。 */
+@Data
+@TableName("content_submission")
+public class ContentSubmission {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long creatorId;
+ private Long dramaId;
+ /** CREATE / UPDATE / PUBLISH / OFFLINE */
+ private String type;
+ /** DRAFT / SUBMITTED / REVIEWING / APPROVED / REJECTED / WITHDRAWN */
+ private String status;
+ private Integer version;
+ private LocalDateTime submitAt;
+ private Long reviewerId;
+ private LocalDateTime reviewAt;
+ private String reviewNote;
+ private String rejectCode;
+ private LocalDateTime createdAt;
+
+ @TableField(updateStrategy = FieldStrategy.NEVER)
+ private LocalDateTime updatedAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/Creator.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/Creator.java
new file mode 100644
index 0000000..9f35951
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/Creator.java
@@ -0,0 +1,52 @@
+package com.fruitshorts.creator.dal.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/** 创作者主体与资质,对应 creator 表。 */
+@Data
+@TableName("creator")
+public class Creator {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ /** 关联 C 端账号 app_user.id */
+ private Long userId;
+ private String code;
+ private String name;
+ /** INDIVIDUAL / COMPANY */
+ private String type;
+ private String contactName;
+ private String contactMobile;
+ private String contactEmail;
+ private String countryCode;
+ /** pending / approved / rejected / frozen */
+ private String status;
+ private String auditNote;
+ private Long auditBy;
+ private LocalDateTime auditAt;
+ private String settlementCurrency;
+ /** 预留:加密存储(第二阶段结算) */
+ private String bankAccount;
+ /** 预留:税号 */
+ private String taxNo;
+ /** 预留:KYC 状态 */
+ private String kycStatus;
+ /** 预留:分成比例(第二阶段) */
+ private BigDecimal shareRate;
+ private LocalDateTime createdAt;
+
+ @TableField(updateStrategy = FieldStrategy.NEVER)
+ private LocalDateTime updatedAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/CreatorDocument.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/CreatorDocument.java
new file mode 100644
index 0000000..7c0a601
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/entity/CreatorDocument.java
@@ -0,0 +1,36 @@
+package com.fruitshorts.creator.dal.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldStrategy;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/** 创作者资质文件,对应 creator_document 表。 */
+@Data
+@TableName("creator_document")
+public class CreatorDocument {
+
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private Long creatorId;
+ /** ID_CARD / BUSINESS_LICENSE / CONTRACT */
+ private String type;
+ private String fileUrl;
+ /** pending / approved / rejected */
+ private String status;
+ private String auditNote;
+ /** 证件有效期 */
+ private LocalDateTime expireAt;
+ private LocalDateTime createdAt;
+
+ @TableField(updateStrategy = FieldStrategy.NEVER)
+ private LocalDateTime updatedAt;
+
+ @TableLogic
+ private Integer deleted;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/ContentSubmissionMapper.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/ContentSubmissionMapper.java
new file mode 100644
index 0000000..b3305b5
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/ContentSubmissionMapper.java
@@ -0,0 +1,7 @@
+package com.fruitshorts.creator.dal.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fruitshorts.creator.dal.entity.ContentSubmission;
+
+public interface ContentSubmissionMapper extends BaseMapper {
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorDocumentMapper.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorDocumentMapper.java
new file mode 100644
index 0000000..f6b3cbe
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorDocumentMapper.java
@@ -0,0 +1,7 @@
+package com.fruitshorts.creator.dal.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fruitshorts.creator.dal.entity.CreatorDocument;
+
+public interface CreatorDocumentMapper extends BaseMapper {
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorMapper.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorMapper.java
new file mode 100644
index 0000000..7783a95
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/dal/mapper/CreatorMapper.java
@@ -0,0 +1,7 @@
+package com.fruitshorts.creator.dal.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fruitshorts.creator.dal.entity.Creator;
+
+public interface CreatorMapper extends BaseMapper {
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/ContentSubmissionService.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/ContentSubmissionService.java
new file mode 100644
index 0000000..bfd5a95
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/ContentSubmissionService.java
@@ -0,0 +1,144 @@
+package com.fruitshorts.creator.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fruitshorts.common.exception.ErrorCode;
+import com.fruitshorts.common.exception.ServiceException;
+import com.fruitshorts.common.pojo.PageResult;
+import com.fruitshorts.content.dal.entity.Drama;
+import com.fruitshorts.content.enums.ContentStatus;
+import com.fruitshorts.content.service.DramaService;
+import com.fruitshorts.creator.dal.entity.ContentSubmission;
+import com.fruitshorts.creator.dal.mapper.ContentSubmissionMapper;
+import com.fruitshorts.creator.vo.SubmissionAuditReqVO;
+import com.fruitshorts.creator.vo.SubmissionQueryReqVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+/** 创作者内容提交:后台检索、审核,通过后按 type 落到短剧状态机。 */
+@Service
+@RequiredArgsConstructor
+public class ContentSubmissionService {
+
+ public static final String STATUS_DRAFT = "DRAFT";
+ public static final String STATUS_SUBMITTED = "SUBMITTED";
+ public static final String STATUS_REVIEWING = "REVIEWING";
+ public static final String STATUS_APPROVED = "APPROVED";
+ public static final String STATUS_REJECTED = "REJECTED";
+ public static final String STATUS_WITHDRAWN = "WITHDRAWN";
+
+ private static final Set REVIEWABLE = Set.of(STATUS_SUBMITTED, STATUS_REVIEWING);
+
+ private final ContentSubmissionMapper submissionMapper;
+ private final DramaService dramaService;
+
+ public PageResult page(SubmissionQueryReqVO req) {
+ Page page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
+ IPage result = submissionMapper.selectPage(page,
+ Wrappers.lambdaQuery()
+ .eq(req.getCreatorId() != null, ContentSubmission::getCreatorId, req.getCreatorId())
+ .eq(req.getDramaId() != null, ContentSubmission::getDramaId, req.getDramaId())
+ .eq(StringUtils.hasText(req.getStatus()), ContentSubmission::getStatus, req.getStatus())
+ .eq(StringUtils.hasText(req.getType()), ContentSubmission::getType, req.getType())
+ .orderByDesc(ContentSubmission::getId));
+ return new PageResult<>(result.getTotal(), result.getRecords());
+ }
+
+ public ContentSubmission getExisting(Long id) {
+ ContentSubmission submission = id == null ? null : submissionMapper.selectById(id);
+ if (submission == null) {
+ throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "内容提交记录不存在");
+ }
+ return submission;
+ }
+
+ /** 审核:通过 / 驳回 / 撤回。 */
+ @Transactional(rollbackFor = Exception.class)
+ public ContentSubmission audit(Long id, SubmissionAuditReqVO req, Long operatorId, String operatorName) {
+ ContentSubmission submission = getExisting(id);
+ String current = submission.getStatus();
+ String action = req.getAction() == null ? "" : req.getAction();
+
+ switch (action) {
+ case "APPROVE" -> {
+ requireState(current, "通过", REVIEWABLE);
+ submission.setStatus(STATUS_APPROVED);
+ applyToDrama(submission, operatorId, operatorName);
+ }
+ case "REJECT" -> {
+ requireState(current, "驳回", REVIEWABLE);
+ if (!StringUtils.hasText(req.getReason())) {
+ throw new ServiceException(ErrorCode.BAD_REQUEST, "驳回必须填写原因");
+ }
+ submission.setStatus(STATUS_REJECTED);
+ }
+ case "WITHDRAW" -> {
+ requireState(current, "撤回", Set.of(STATUS_DRAFT, STATUS_SUBMITTED));
+ submission.setStatus(STATUS_WITHDRAWN);
+ }
+ default -> throw new ServiceException(ErrorCode.BAD_REQUEST, "未知的审核动作: " + action);
+ }
+
+ submission.setReviewerId(operatorId == null ? 0L : operatorId);
+ submission.setReviewAt(LocalDateTime.now());
+ submission.setReviewNote(req.getReason() == null ? "" : req.getReason());
+ submission.setRejectCode(req.getRejectCode() == null ? "" : req.getRejectCode());
+ submissionMapper.updateById(submission);
+ return submission;
+ }
+
+ /**
+ * 审核通过后落到短剧状态机:PUBLISH 上线、OFFLINE 下线,CREATE / UPDATE 只留档不改状态。
+ * 上线时若短剧还停在草稿,按 SUBMIT → APPROVE → PUBLISH 顺推,保证状态机不被绕过。
+ */
+ private void applyToDrama(ContentSubmission submission, Long operatorId, String operatorName) {
+ String type = submission.getType() == null ? "" : submission.getType();
+ Long dramaId = submission.getDramaId();
+ String reason = "创作者内容审核通过";
+ if ("PUBLISH".equals(type)) {
+ Drama drama = dramaService.getExisting(dramaId);
+ String status = drama.getStatus();
+ if (ContentStatus.PUBLISHED.getCode().equals(status)) {
+ return;
+ }
+ if (ContentStatus.DRAFT.getCode().equals(status)) {
+ dramaService.changeStatus(dramaId, "SUBMIT", reason, operatorId, operatorName);
+ }
+ Drama afterSubmit = dramaService.getExisting(dramaId);
+ if (ContentStatus.REVIEWING.getCode().equals(afterSubmit.getStatus())) {
+ dramaService.changeStatus(dramaId, "APPROVE", reason, operatorId, operatorName);
+ }
+ dramaService.changeStatus(dramaId, "PUBLISH", reason, operatorId, operatorName);
+ } else if ("OFFLINE".equals(type)) {
+ Drama drama = dramaService.getExisting(dramaId);
+ if (ContentStatus.PUBLISHED.getCode().equals(drama.getStatus())) {
+ dramaService.changeStatus(dramaId, "OFFLINE", reason, operatorId, operatorName);
+ }
+ }
+ }
+
+ private void requireState(String current, String action, Set allowed) {
+ if (!allowed.contains(current)) {
+ throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
+ "当前状态(" + statusText(current) + ")不允许执行「" + action + "」");
+ }
+ }
+
+ public static String statusText(String code) {
+ return switch (code == null ? "" : code) {
+ case STATUS_DRAFT -> "草稿";
+ case STATUS_SUBMITTED -> "已提交";
+ case STATUS_REVIEWING -> "审核中";
+ case STATUS_APPROVED -> "已通过";
+ case STATUS_REJECTED -> "已驳回";
+ case STATUS_WITHDRAWN -> "已撤回";
+ default -> code;
+ };
+ }
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorService.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorService.java
new file mode 100644
index 0000000..dd6fd19
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorService.java
@@ -0,0 +1,126 @@
+package com.fruitshorts.creator.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fruitshorts.common.exception.ErrorCode;
+import com.fruitshorts.common.exception.ServiceException;
+import com.fruitshorts.common.pojo.PageResult;
+import com.fruitshorts.creator.dal.entity.Creator;
+import com.fruitshorts.creator.dal.entity.CreatorDocument;
+import com.fruitshorts.creator.dal.mapper.CreatorDocumentMapper;
+import com.fruitshorts.creator.dal.mapper.CreatorMapper;
+import com.fruitshorts.creator.vo.CreatorAuditReqVO;
+import com.fruitshorts.creator.vo.CreatorQueryReqVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+
+/** 创作者主体与资质:后台检索、详情、入驻审核。 */
+@Service
+@RequiredArgsConstructor
+public class CreatorService {
+
+ public static final String STATUS_PENDING = "pending";
+ public static final String STATUS_APPROVED = "approved";
+ public static final String STATUS_REJECTED = "rejected";
+ public static final String STATUS_FROZEN = "frozen";
+
+ private final CreatorMapper creatorMapper;
+ private final CreatorDocumentMapper creatorDocumentMapper;
+
+ public PageResult page(CreatorQueryReqVO req) {
+ Page page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
+ IPage result = creatorMapper.selectPage(page, Wrappers.lambdaQuery()
+ .and(StringUtils.hasText(req.getKeyword()), w -> w
+ .like(Creator::getName, req.getKeyword())
+ .or().like(Creator::getCode, req.getKeyword())
+ .or().like(Creator::getContactName, req.getKeyword())
+ .or().like(Creator::getContactMobile, req.getKeyword()))
+ .eq(StringUtils.hasText(req.getStatus()), Creator::getStatus, req.getStatus())
+ .eq(StringUtils.hasText(req.getType()), Creator::getType, req.getType())
+ .eq(StringUtils.hasText(req.getCountryCode()), Creator::getCountryCode, req.getCountryCode())
+ .orderByDesc(Creator::getId));
+ return new PageResult<>(result.getTotal(), result.getRecords());
+ }
+
+ public Creator getExisting(Long id) {
+ Creator creator = id == null ? null : creatorMapper.selectById(id);
+ if (creator == null) {
+ throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "创作者不存在");
+ }
+ return creator;
+ }
+
+ /** 资质文件列表:审核时要能看到证件原件。 */
+ public List documents(Long creatorId) {
+ getExisting(creatorId);
+ return creatorDocumentMapper.selectList(Wrappers.lambdaQuery()
+ .eq(CreatorDocument::getCreatorId, creatorId)
+ .orderByAsc(CreatorDocument::getType)
+ .orderByDesc(CreatorDocument::getId));
+ }
+
+ /** 入驻审核:通过 / 驳回 / 冻结。 */
+ @Transactional(rollbackFor = Exception.class)
+ public Creator audit(Long id, CreatorAuditReqVO req, Long operatorId) {
+ Creator creator = getExisting(id);
+ String current = creator.getStatus();
+ String action = req.getAction() == null ? "" : req.getAction();
+ String target = switch (action) {
+ case "APPROVE" -> {
+ requireState(current, "通过", Set.of(STATUS_PENDING, STATUS_REJECTED, STATUS_FROZEN));
+ yield STATUS_APPROVED;
+ }
+ case "REJECT" -> {
+ requireState(current, "驳回", Set.of(STATUS_PENDING));
+ if (!StringUtils.hasText(req.getReason())) {
+ throw new ServiceException(ErrorCode.BAD_REQUEST, "驳回必须填写原因");
+ }
+ yield STATUS_REJECTED;
+ }
+ case "FREEZE" -> {
+ requireState(current, "冻结", Set.of(STATUS_APPROVED));
+ yield STATUS_FROZEN;
+ }
+ default -> throw new ServiceException(ErrorCode.BAD_REQUEST, "未知的审核动作: " + action);
+ };
+
+ creator.setStatus(target);
+ creator.setAuditNote(buildNote(req));
+ creator.setAuditBy(operatorId == null ? 0L : operatorId);
+ creator.setAuditAt(LocalDateTime.now());
+ creatorMapper.updateById(creator);
+ return creator;
+ }
+
+ /** 驳回原因码没有独立列,与说明合并存进 audit_note,保留可追溯性。 */
+ private String buildNote(CreatorAuditReqVO req) {
+ String reason = req.getReason() == null ? "" : req.getReason();
+ return StringUtils.hasText(req.getRejectCode())
+ ? "[" + req.getRejectCode() + "] " + reason
+ : reason;
+ }
+
+ private void requireState(String current, String action, Set allowed) {
+ if (!allowed.contains(current)) {
+ throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
+ "当前状态(" + statusText(current) + ")不允许执行「" + action + "」");
+ }
+ }
+
+ public static String statusText(String code) {
+ return switch (code == null ? "" : code) {
+ case STATUS_PENDING -> "待审核";
+ case STATUS_APPROVED -> "已通过";
+ case STATUS_REJECTED -> "已驳回";
+ case STATUS_FROZEN -> "已冻结";
+ default -> code;
+ };
+ }
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorAuditReqVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorAuditReqVO.java
new file mode 100644
index 0000000..e7083da
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorAuditReqVO.java
@@ -0,0 +1,15 @@
+package com.fruitshorts.creator.vo;
+
+import lombok.Data;
+
+/** 创作者入驻审核请求。 */
+@Data
+public class CreatorAuditReqVO {
+
+ /** APPROVE / REJECT / FREEZE */
+ private String action;
+ /** 驳回原因码,REJECT 时必填 */
+ private String rejectCode;
+ /** 审核说明 / 驳回原因 */
+ private String reason;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorQueryReqVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorQueryReqVO.java
new file mode 100644
index 0000000..c74ddc1
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorQueryReqVO.java
@@ -0,0 +1,19 @@
+package com.fruitshorts.creator.vo;
+
+import com.fruitshorts.common.pojo.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/** 后台创作者检索条件。 */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CreatorQueryReqVO extends PageParam {
+
+ /** 名称 / 编号 / 联系人 / 联系方式 关键字 */
+ private String keyword;
+ /** pending / approved / rejected / frozen */
+ private String status;
+ /** INDIVIDUAL / COMPANY */
+ private String type;
+ private String countryCode;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionAuditReqVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionAuditReqVO.java
new file mode 100644
index 0000000..548f8fd
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionAuditReqVO.java
@@ -0,0 +1,15 @@
+package com.fruitshorts.creator.vo;
+
+import lombok.Data;
+
+/** 内容提交审核请求。 */
+@Data
+public class SubmissionAuditReqVO {
+
+ /** APPROVE / REJECT / WITHDRAW */
+ private String action;
+ /** 驳回原因码,REJECT 时必填 */
+ private String rejectCode;
+ /** 审核说明 / 驳回原因 */
+ private String reason;
+}
diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionQueryReqVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionQueryReqVO.java
new file mode 100644
index 0000000..4041964
--- /dev/null
+++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/SubmissionQueryReqVO.java
@@ -0,0 +1,18 @@
+package com.fruitshorts.creator.vo;
+
+import com.fruitshorts.common.pojo.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/** 后台内容提交审核检索条件。 */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SubmissionQueryReqVO extends PageParam {
+
+ private Long creatorId;
+ private Long dramaId;
+ /** DRAFT / SUBMITTED / REVIEWING / APPROVED / REJECTED / WITHDRAWN */
+ private String status;
+ /** CREATE / UPDATE / PUBLISH / OFFLINE */
+ private String type;
+}
diff --git a/pom.xml b/pom.xml
index 1c92ae2..2cac673 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,6 +28,7 @@
fruitshorts-module-playback
fruitshorts-module-interaction
fruitshorts-module-commerce
+ fruitshorts-module-creator
fruitshorts-admin-api
fruitshorts-app-api
@@ -90,6 +91,11 @@
fruitshorts-module-commerce
${project.version}