feat: 07 创作者中心批 1(账号绑定 + 我的作品 / 审核进度 / 通知)
为什么:创作者要在 Web 端(做进后台管理)看到自己的作品与审核进度,但 creator 模块
此前只有「后台审核视角」的服务,创作者本人没有任何入口;更要紧的是,全项目没有任何
地方把「后台账号」和「创作者主体」关联起来 —— 缺这层映射,横向越权就只能靠每个接口
自觉加 where 过滤,迟早会漏。所以批 1 先把归属判定的地基打好,再给工作台接口。
- creator 增加 admin_user_id(幂等 ALTER + 部分唯一索引,0 = 未绑定,多条 0 不冲突)
- 新增角色 creator 与权限点 creator-center:view(创作者角色 + 超管各授权一条),菜单等页面做好再挂
- 新增 CreatorCenterService:requireCurrentCreator() 是归属判定的唯一入口,requireMyDrama() 供后续批次复用
- 新增 /admin/creator-center 四个接口(profile / dramas/page / dramas/{id} / notices),
一律不接收 creatorId,查询强制 where creator_id = 当前创作者,访问他人作品统一 403
- 通知不新建表:由入驻审核结果 + 内容提交审核结果派生(ops_notice 无产出方,等有公告功能再接)
- 顺手接通 drama.trailer_url:DDL 与实体早就有,但保存 VO / 响应 VO 都没带,预告片存不进也读不出
- ContentSubmissionService 补 typeText + TYPE_* 常量(通知文案与批 2 的提交都要用)
This commit is contained in:
1 parent
8b261b71de
commit
ea875c3e2b
14 files changed
+454
No files matched your search
+2
@@ -20,6 +20,8 @@ public class Creator {
|
||||
private Long id;
|
||||
/** 关联 C 端账号 app_user.id */
|
||||
private Long userId;
|
||||
/** 绑定的后台账号 sys_admin_user.id(创作者中心的登录身份),0 表示未绑定 */
|
||||
private Long adminUserId;
|
||||
private String code;
|
||||
private String name;
|
||||
/** INDIVIDUAL / COMPANY */
|
||||
|
||||
+16
@@ -33,6 +33,11 @@ public class ContentSubmissionService {
|
||||
public static final String STATUS_REJECTED = "REJECTED";
|
||||
public static final String STATUS_WITHDRAWN = "WITHDRAWN";
|
||||
|
||||
public static final String TYPE_CREATE = "CREATE";
|
||||
public static final String TYPE_UPDATE = "UPDATE";
|
||||
public static final String TYPE_PUBLISH = "PUBLISH";
|
||||
public static final String TYPE_OFFLINE = "OFFLINE";
|
||||
|
||||
private static final Set<String> REVIEWABLE = Set.of(STATUS_SUBMITTED, STATUS_REVIEWING);
|
||||
|
||||
private final ContentSubmissionMapper submissionMapper;
|
||||
@@ -141,4 +146,15 @@ public class ContentSubmissionService {
|
||||
default -> code;
|
||||
};
|
||||
}
|
||||
|
||||
/** 提交类型文案:创作者看的通知里要写清楚是"新建"还是"申请上线" */
|
||||
public static String typeText(String code) {
|
||||
return switch (code == null ? "" : code) {
|
||||
case TYPE_CREATE -> "新建";
|
||||
case TYPE_UPDATE -> "编辑";
|
||||
case TYPE_PUBLISH -> "申请上线";
|
||||
case TYPE_OFFLINE -> "申请下线";
|
||||
default -> code == null ? "" : code;
|
||||
};
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
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.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.enums.ContentStatus;
|
||||
import com.fruitshorts.creator.dal.entity.ContentSubmission;
|
||||
import com.fruitshorts.creator.dal.entity.Creator;
|
||||
import com.fruitshorts.creator.dal.mapper.ContentSubmissionMapper;
|
||||
import com.fruitshorts.creator.dal.mapper.CreatorMapper;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterDramaQueryReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterDramaVO;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterNoticeVO;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterProfileVO;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 创作者中心(Web 端,做在后台管理里):创作者本人视角的只读查询与归属校验。
|
||||
*
|
||||
* **归属判定的唯一入口是 {@link #requireCurrentCreator()}**:
|
||||
* 创作者身份从登录态(sys_admin_user.id)解析,creator-center 的所有接口都**不收 creatorId 参数**,
|
||||
* 查询一律带 `where creator_id = 当前创作者`。所以横向越权不是靠"记得加过滤",
|
||||
* 而是参数层面就没有入口;真被别人猜到 id 时,{@link #requireMyDrama} 会直接 403。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CreatorCenterService {
|
||||
|
||||
private static final int NOTICE_LIMIT = 50;
|
||||
|
||||
private final CreatorMapper creatorMapper;
|
||||
private final DramaMapper dramaMapper;
|
||||
private final ContentSubmissionMapper submissionMapper;
|
||||
|
||||
/** 当前登录人绑定的创作者主体;没绑定就是 403,不返回"你是管理员"之类的细节。 */
|
||||
public Creator requireCurrentCreator() {
|
||||
Long adminUserId = SecurityUtils.getUserId();
|
||||
if (adminUserId == null) {
|
||||
throw new ServiceException(ErrorCode.UNAUTHORIZED);
|
||||
}
|
||||
Creator creator = creatorMapper.selectOne(Wrappers.<Creator>lambdaQuery()
|
||||
.eq(Creator::getAdminUserId, adminUserId)
|
||||
.last("limit 1"));
|
||||
if (creator == null) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN, "当前账号未绑定创作者主体,无法进入创作者中心");
|
||||
}
|
||||
return creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 作品归属校验:不是自己的作品,不区分"不存在"与"别人的",统一 403。
|
||||
* 后续各批(编辑、上传挂载、收费配置)都复用这个方法,避免各写一份过滤条件。
|
||||
*/
|
||||
public Drama requireMyDrama(Long dramaId) {
|
||||
Creator creator = requireCurrentCreator();
|
||||
Drama drama = dramaId == null ? null : dramaMapper.selectById(dramaId);
|
||||
if (drama == null || !Objects.equals(drama.getCreatorId(), creator.getId())) {
|
||||
throw new ServiceException(ErrorCode.FORBIDDEN, "无权访问该作品");
|
||||
}
|
||||
return drama;
|
||||
}
|
||||
|
||||
public CreatorCenterProfileVO profile() {
|
||||
Creator creator = requireCurrentCreator();
|
||||
CreatorCenterProfileVO vo = new CreatorCenterProfileVO();
|
||||
vo.setId(creator.getId());
|
||||
vo.setCode(creator.getCode());
|
||||
vo.setName(creator.getName());
|
||||
vo.setType(creator.getType());
|
||||
vo.setStatus(creator.getStatus());
|
||||
vo.setStatusText(CreatorService.statusText(creator.getStatus()));
|
||||
vo.setAuditNote(creator.getAuditNote());
|
||||
vo.setAuditAt(creator.getAuditAt());
|
||||
vo.setContactName(creator.getContactName());
|
||||
vo.setContactEmail(creator.getContactEmail());
|
||||
vo.setCountryCode(creator.getCountryCode());
|
||||
vo.setDramaTotal(dramaMapper.selectCount(Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getCreatorId, creator.getId())));
|
||||
return vo;
|
||||
}
|
||||
|
||||
public PageResult<CreatorCenterDramaVO> pageMyDramas(CreatorCenterDramaQueryReqVO req) {
|
||||
Creator creator = requireCurrentCreator();
|
||||
Page<Drama> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getCreatorId, creator.getId())
|
||||
.like(StringUtils.hasText(req.getKeyword()), Drama::getTitle, req.getKeyword())
|
||||
.eq(StringUtils.hasText(req.getStatus()), Drama::getStatus, req.getStatus())
|
||||
.orderByDesc(Drama::getUpdatedAt)
|
||||
.orderByDesc(Drama::getId));
|
||||
return new PageResult<>(result.getTotal(), toDramaVOList(creator.getId(), result.getRecords()));
|
||||
}
|
||||
|
||||
public CreatorCenterDramaVO myDramaDetail(Long dramaId) {
|
||||
Creator creator = requireCurrentCreator();
|
||||
Drama drama = requireMyDrama(dramaId);
|
||||
return toDramaVOList(creator.getId(), List.of(drama)).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的通知 = 入驻审核结果 + 我提交内容的审核结果(都从已有表派生,不新建通知表)。
|
||||
* 系统公告(ops_notice)当前没有产出方,等有公告功能再接,避免出现"永远空列表"的入口。
|
||||
*/
|
||||
public List<CreatorCenterNoticeVO> notices() {
|
||||
Creator creator = requireCurrentCreator();
|
||||
List<CreatorCenterNoticeVO> list = new ArrayList<>();
|
||||
|
||||
if (StringUtils.hasText(creator.getStatus()) && !CreatorService.STATUS_PENDING.equals(creator.getStatus())
|
||||
&& creator.getAuditAt() != null) {
|
||||
CreatorCenterNoticeVO notice = new CreatorCenterNoticeVO();
|
||||
notice.setId(creator.getId());
|
||||
notice.setBizType("CREATOR");
|
||||
notice.setTitle("入驻审核结果:" + CreatorService.statusText(creator.getStatus()));
|
||||
notice.setContent(StringUtils.hasText(creator.getAuditNote()) ? creator.getAuditNote() : "—");
|
||||
notice.setResult(creator.getStatus());
|
||||
notice.setTime(creator.getAuditAt());
|
||||
list.add(notice);
|
||||
}
|
||||
|
||||
List<ContentSubmission> reviewed = submissionMapper.selectList(Wrappers.<ContentSubmission>lambdaQuery()
|
||||
.eq(ContentSubmission::getCreatorId, creator.getId())
|
||||
.in(ContentSubmission::getStatus,
|
||||
ContentSubmissionService.STATUS_APPROVED, ContentSubmissionService.STATUS_REJECTED)
|
||||
.orderByDesc(ContentSubmission::getReviewAt)
|
||||
.last("limit " + NOTICE_LIMIT));
|
||||
for (ContentSubmission submission : reviewed) {
|
||||
CreatorCenterNoticeVO notice = new CreatorCenterNoticeVO();
|
||||
notice.setId(submission.getId());
|
||||
notice.setBizType("SUBMISSION");
|
||||
notice.setTitle("作品 ID " + submission.getDramaId() + " 的"
|
||||
+ ContentSubmissionService.typeText(submission.getType()) + "审核结果:"
|
||||
+ ContentSubmissionService.statusText(submission.getStatus()));
|
||||
notice.setContent(StringUtils.hasText(submission.getReviewNote())
|
||||
? submission.getReviewNote() : "—");
|
||||
notice.setResult(submission.getStatus());
|
||||
notice.setTime(submission.getReviewAt());
|
||||
list.add(notice);
|
||||
}
|
||||
|
||||
list.sort(Comparator.comparing(CreatorCenterNoticeVO::getTime,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 作品 + 最近一次提交:列表页要显示"审核状态与进度",两个表拼一次拿全,避免前端 N+1。 */
|
||||
private List<CreatorCenterDramaVO> toDramaVOList(Long creatorId, List<Drama> dramas) {
|
||||
if (dramas.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> dramaIds = dramas.stream().map(Drama::getId).toList();
|
||||
List<ContentSubmission> submissions = submissionMapper.selectList(Wrappers.<ContentSubmission>lambdaQuery()
|
||||
.eq(ContentSubmission::getCreatorId, creatorId)
|
||||
.in(ContentSubmission::getDramaId, dramaIds)
|
||||
.orderByDesc(ContentSubmission::getId));
|
||||
Map<Long, ContentSubmission> latest = new HashMap<>();
|
||||
for (ContentSubmission submission : submissions) {
|
||||
latest.putIfAbsent(submission.getDramaId(), submission);
|
||||
}
|
||||
|
||||
return dramas.stream().map(drama -> {
|
||||
CreatorCenterDramaVO vo = new CreatorCenterDramaVO();
|
||||
vo.setId(drama.getId());
|
||||
vo.setCode(drama.getCode());
|
||||
vo.setTitle(drama.getTitle());
|
||||
vo.setCover(drama.getCoverUrl());
|
||||
vo.setStatus(drama.getStatus());
|
||||
vo.setStatusText(ContentStatus.textOf(drama.getStatus()));
|
||||
vo.setEpisodes(drama.getTotalEpisodes());
|
||||
vo.setViews(drama.getViewCount());
|
||||
vo.setUpdatedAt(drama.getUpdatedAt());
|
||||
|
||||
ContentSubmission submission = latest.get(drama.getId());
|
||||
if (submission != null) {
|
||||
vo.setSubmissionId(submission.getId());
|
||||
vo.setSubmissionType(submission.getType());
|
||||
vo.setSubmissionStatus(submission.getStatus());
|
||||
vo.setSubmissionStatusText(ContentSubmissionService.statusText(submission.getStatus()));
|
||||
vo.setReviewNote(submission.getReviewNote());
|
||||
vo.setRejectCode(submission.getRejectCode());
|
||||
vo.setSubmitAt(submission.getSubmitAt());
|
||||
vo.setReviewAt(submission.getReviewAt());
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import com.fruitshorts.common.pojo.PageParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 创作者中心 · 我的作品检索条件。
|
||||
*
|
||||
* 注意:这里**没有 creatorId** —— 归属一律从登录态解析,前端传不进来,也就没有横向越权的入口。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class CreatorCenterDramaQueryReqVO extends PageParam {
|
||||
|
||||
private String keyword;
|
||||
private String status;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 创作者中心 · 我的作品(作品 + 最近一次提交的审核进度,一个接口给全,避免前端拼两个请求)。
|
||||
*/
|
||||
@Data
|
||||
public class CreatorCenterDramaVO {
|
||||
|
||||
private Long id;
|
||||
private String code;
|
||||
private String title;
|
||||
private String cover;
|
||||
private String status;
|
||||
private String statusText;
|
||||
private Integer episodes;
|
||||
private Long views;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/** 最近一次内容提交(没有提交过则为空) */
|
||||
private Long submissionId;
|
||||
private String submissionType;
|
||||
private String submissionStatus;
|
||||
private String submissionStatusText;
|
||||
private String reviewNote;
|
||||
private String rejectCode;
|
||||
private LocalDateTime submitAt;
|
||||
private LocalDateTime reviewAt;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 创作者中心 · 通知。
|
||||
*
|
||||
* 第一阶段的通知来源是「审核结果」:入驻审核(CREATOR)与内容提交审核(SUBMISSION),
|
||||
* 都从已有表派生,不新建通知表;系统公告(ops_notice)目前没有产出方,等有公告功能再接。
|
||||
*/
|
||||
@Data
|
||||
public class CreatorCenterNoticeVO {
|
||||
|
||||
/** 来源单据 id */
|
||||
private Long id;
|
||||
/** CREATOR(入驻审核)/ SUBMISSION(内容提交审核) */
|
||||
private String bizType;
|
||||
private String title;
|
||||
private String content;
|
||||
/** 通过 / 驳回 等结果文案 */
|
||||
private String result;
|
||||
private LocalDateTime time;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 创作者中心 · 当前登录创作者的主体信息(只返回自己的,不含他人数据)。 */
|
||||
@Data
|
||||
public class CreatorCenterProfileVO {
|
||||
|
||||
private Long id;
|
||||
private String code;
|
||||
private String name;
|
||||
/** INDIVIDUAL / COMPANY */
|
||||
private String type;
|
||||
/** pending / approved / rejected / frozen */
|
||||
private String status;
|
||||
private String statusText;
|
||||
/** 入驻审核意见(驳回时是原因) */
|
||||
private String auditNote;
|
||||
private LocalDateTime auditAt;
|
||||
private String contactName;
|
||||
private String contactEmail;
|
||||
private String countryCode;
|
||||
/** 我的作品数(只统计自己的) */
|
||||
private Long dramaTotal;
|
||||
}
|
||||
Reference in new issue
Block a user