From ea875c3e2b3c86cc76e40d7b8d108783b8e9baab Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Thu, 15 Oct 2026 13:45:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=2007=20=E5=88=9B=E4=BD=9C=E8=80=85?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E6=89=B9=201=EF=BC=88=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=20+=20=E6=88=91=E7=9A=84=E4=BD=9C=E5=93=81?= =?UTF-8?q?=20/=20=E5=AE=A1=E6=A0=B8=E8=BF=9B=E5=BA=A6=20/=20=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为什么:创作者要在 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 的提交都要用) --- docs/api-phase1.md | 21 ++ .../controller/CreatorCenterController.java | 60 ++++++ .../content/service/DramaService.java | 2 + .../fruitshorts/content/vo/DramaRespVO.java | 2 + .../content/vo/DramaSaveReqVO.java | 2 + .../creator/dal/entity/Creator.java | 2 + .../service/ContentSubmissionService.java | 16 ++ .../creator/service/CreatorCenterService.java | 202 ++++++++++++++++++ .../vo/CreatorCenterDramaQueryReqVO.java | 18 ++ .../creator/vo/CreatorCenterDramaVO.java | 32 +++ .../creator/vo/CreatorCenterNoticeVO.java | 25 +++ .../creator/vo/CreatorCenterProfileVO.java | 27 +++ sql/data-phase1.sql | 35 +++ sql/schema-phase1.sql | 10 + 14 files changed, 454 insertions(+) create mode 100644 fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/CreatorCenterController.java create mode 100644 fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorCenterService.java create mode 100644 fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaQueryReqVO.java create mode 100644 fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaVO.java create mode 100644 fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterNoticeVO.java create mode 100644 fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterProfileVO.java diff --git a/docs/api-phase1.md b/docs/api-phase1.md index c88a84f..6e258dd 100644 --- a/docs/api-phase1.md +++ b/docs/api-phase1.md @@ -354,6 +354,27 @@ POST /app/auth/register 菜单、权限点与超管授权由 `sql/data-phase1.sql` 幂等落库(内容治理 / 订单与权益 / 数据看板挂菜单 id 20–25,创作者两条为 17–19)。 页面只读态与可操作态按权限点区分:没有 `*:update` / `*:audit` 的角色进入页面时按钮隐藏或显示「只读」。 +### 2.10 创作者中心(批 1 已实现:工作台) + +创作者 Web 端做在后台管理里(`ReelShortWeb`),身份复用 `sys_admin_user`: +`creator.admin_user_id` 绑定后台账号(部分唯一索引,0 = 未绑定),角色 `creator` 只拿 `creator-center:*` 权限点。 + +| 方法 | 路径 | 权限点 | 说明 | +| --- | --- | --- | --- | +| GET | `/admin/creator-center/profile` | `creator-center:view` | 当前创作者主体与入驻状态 | +| GET | `/admin/creator-center/dramas/page` | `creator-center:view` | 我的作品(含最近一次提交的审核进度:类型 / 状态 / 驳回原因码 / 时间) | +| GET | `/admin/creator-center/dramas/{id}` | `creator-center:view` | 我的作品详情 | +| GET | `/admin/creator-center/notices` | `creator-center:view` | 我的通知:入驻审核结果 + 我提交内容的审核结果 | + +**契约红线**:以上接口一律**不接收 `creatorId`**,归属由登录态(`sys_admin_user.id` → `creator.admin_user_id`)解析, +查询强制带 `where creator_id = 当前创作者`;访问他人作品统一返回 `{"code":403,"message":"无权访问该作品"}`, +未绑定创作者主体返回 `{"code":403,"message":"当前账号未绑定创作者主体,无法进入创作者中心"}`。 + +通知不新建表:第一阶段由 `creator`(入驻审核)与 `content_submission`(内容提交审核)派生; +系统公告 `ops_notice` 目前没有产出方,等有公告功能再接。 + +**进度**:批 2(内容编辑与分集、提交审核)、批 3(分片上传、分集收费)、批 4(创作者数据看板)、批 5(前端页面)未开始。 + ## 3. 契约与实现状态对照 | 模块 | 表结构 | 接口 | 代码 | diff --git a/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/CreatorCenterController.java b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/CreatorCenterController.java new file mode 100644 index 0000000..70839cf --- /dev/null +++ b/fruitshorts-admin-api/src/main/java/com/fruitshorts/admin/controller/CreatorCenterController.java @@ -0,0 +1,60 @@ +package com.fruitshorts.admin.controller; + +import com.fruitshorts.common.pojo.PageResult; +import com.fruitshorts.common.pojo.Result; +import com.fruitshorts.creator.service.CreatorCenterService; +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 lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 创作者中心(Web 端,挂在后台管理里):创作者看自己的作品、审核进度与通知。 + * + * **契约红线**:本控制器所有接口都**不接收 creatorId**, + * 归属由 {@link CreatorCenterService#requireCurrentCreator()} 从登录态解析, + * 查询强制带 `where creator_id = 当前创作者`;访问他人作品统一 403。 + */ +@RestController +@RequestMapping("/admin/creator-center") +@RequiredArgsConstructor +public class CreatorCenterController { + + private final CreatorCenterService creatorCenterService; + + /** 当前创作者的主体信息与入驻状态。 */ + @GetMapping("/profile") + @PreAuthorize("@ss.hasPermission('creator-center:view')") + public Result profile() { + return Result.ok(creatorCenterService.profile()); + } + + /** 我的作品列表(含最近一次提交的审核进度)。 */ + @GetMapping("/dramas/page") + @PreAuthorize("@ss.hasPermission('creator-center:view')") + public Result> pageMyDramas(CreatorCenterDramaQueryReqVO req) { + return Result.ok(creatorCenterService.pageMyDramas(req)); + } + + /** 我的作品详情:id 是别人的作品时返回 403,不区分"不存在"。 */ + @GetMapping("/dramas/{id}") + @PreAuthorize("@ss.hasPermission('creator-center:view')") + public Result myDrama(@PathVariable Long id) { + return Result.ok(creatorCenterService.myDramaDetail(id)); + } + + /** 我的通知:入驻审核结果 + 我提交内容的审核结果。 */ + @GetMapping("/notices") + @PreAuthorize("@ss.hasPermission('creator-center:view')") + public Result> notices() { + return Result.ok(creatorCenterService.notices()); + } +} diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/DramaService.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/DramaService.java index 5ec6f3b..cb75f15 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/DramaService.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/DramaService.java @@ -263,6 +263,7 @@ public class DramaService { drama.setGenre(req.getGenre() == null ? "" : req.getGenre()); drama.setSynopsis(req.getDescription() == null ? "" : req.getDescription()); drama.setCoverUrl(req.getCover() == null ? "" : req.getCover()); + drama.setTrailerUrl(req.getTrailerUrl() == null ? "" : req.getTrailerUrl()); drama.setHorizontalCoverUrl(drama.getHorizontalCoverUrl() == null ? "" : drama.getHorizontalCoverUrl()); drama.setLanguage(req.getLanguage() == null ? "zh-CN" : req.getLanguage()); drama.setFreeEpisodes(req.getFreeEpisodes() == null ? 3 : req.getFreeEpisodes()); @@ -293,6 +294,7 @@ public class DramaService { vo.setGenre(drama.getGenre()); vo.setDescription(drama.getSynopsis()); vo.setCover(drama.getCoverUrl()); + vo.setTrailerUrl(drama.getTrailerUrl()); vo.setLanguage(drama.getLanguage()); vo.setEpisodes(drama.getTotalEpisodes()); vo.setFreeEpisodes(drama.getFreeEpisodes()); diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaRespVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaRespVO.java index c797dae..2056b48 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaRespVO.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaRespVO.java @@ -20,6 +20,8 @@ public class DramaRespVO { private String genre; private String description; private String cover; + /** 整剧预告片地址 */ + private String trailerUrl; private String language; private Integer episodes; private Integer freeEpisodes; diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaSaveReqVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaSaveReqVO.java index e09f3e4..9a57156 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaSaveReqVO.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/DramaSaveReqVO.java @@ -14,6 +14,8 @@ public class DramaSaveReqVO { private String genre; private String description; private String cover; + /** 整剧预告片地址(复用已有的 drama.trailer_url 列) */ + private String trailerUrl; private String language; private Integer freeEpisodes; private Integer unlockCoin; 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 index 9f35951..3283e84 100644 --- 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 @@ -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 */ 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 index bfd5a95..aa9080b 100644 --- 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 @@ -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 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; + }; + } } diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorCenterService.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorCenterService.java new file mode 100644 index 0000000..25ef472 --- /dev/null +++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/service/CreatorCenterService.java @@ -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.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.lambdaQuery() + .eq(Drama::getCreatorId, creator.getId()))); + return vo; + } + + public PageResult pageMyDramas(CreatorCenterDramaQueryReqVO req) { + Creator creator = requireCurrentCreator(); + Page page = new Page<>(req.getSafePageIndex(), req.getSafePageSize()); + IPage result = dramaMapper.selectPage(page, Wrappers.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 notices() { + Creator creator = requireCurrentCreator(); + List 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 reviewed = submissionMapper.selectList(Wrappers.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 toDramaVOList(Long creatorId, List dramas) { + if (dramas.isEmpty()) { + return List.of(); + } + List dramaIds = dramas.stream().map(Drama::getId).toList(); + List submissions = submissionMapper.selectList(Wrappers.lambdaQuery() + .eq(ContentSubmission::getCreatorId, creatorId) + .in(ContentSubmission::getDramaId, dramaIds) + .orderByDesc(ContentSubmission::getId)); + Map 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(); + } + +} diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaQueryReqVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaQueryReqVO.java new file mode 100644 index 0000000..eecaf55 --- /dev/null +++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaQueryReqVO.java @@ -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; +} diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaVO.java new file mode 100644 index 0000000..f2bf54f --- /dev/null +++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterDramaVO.java @@ -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; +} diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterNoticeVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterNoticeVO.java new file mode 100644 index 0000000..2f1f083 --- /dev/null +++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterNoticeVO.java @@ -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; +} diff --git a/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterProfileVO.java b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterProfileVO.java new file mode 100644 index 0000000..31c3877 --- /dev/null +++ b/fruitshorts-module-creator/src/main/java/com/fruitshorts/creator/vo/CreatorCenterProfileVO.java @@ -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; +} diff --git a/sql/data-phase1.sql b/sql/data-phase1.sql index d1260a5..630196f 100644 --- a/sql/data-phase1.sql +++ b/sql/data-phase1.sql @@ -163,3 +163,38 @@ CROSS JOIN sys_menu m WHERE r.code = 'super_admin' AND m.id IN (20, 21, 22, 23, 24, 25) ON CONFLICT DO NOTHING; + +-- ---------- 07 创作者中心:角色、权限点、授权 ---------- +-- 创作者第一阶段直接登后台(复用 sys_admin_user + sys_role + sys_menu), +-- 这里只落"身份与权限";菜单等页面做出来再挂 —— 菜单先入库会让前端动态路由指向不存在的组件。 +INSERT INTO sys_role_category (id, name, description, sort) VALUES + (4, '创作者', '外部内容创作者,只能管理自己的作品与数据', 4) +ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, sort = EXCLUDED.sort; + +INSERT INTO sys_role (id, name, code, category_id, level, description, status, is_builtin) VALUES + (5, '创作者', 'creator', 4, 5, '只能查看与编辑自己的作品与数据', 1, 0) +ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, code = EXCLUDED.code, + description = EXCLUDED.description, category_id = EXCLUDED.category_id, level = EXCLUDED.level; + +-- 权限点按批补:批 1 只有「进创作者中心」这一个,后面各批用到什么再幂等追加 +INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES + ('creator-center:view', '访问创作者中心', '创作者中心', 'BUTTON', 150) +ON CONFLICT (code) DO NOTHING; + +-- 创作者角色只拿创作者中心这几个点,不给任何后台运营权限 +INSERT INTO sys_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM sys_role r +CROSS JOIN sys_permission p +WHERE r.code = 'creator' + AND p.code = 'creator-center:view' +ON CONFLICT DO NOTHING; + +-- 超管也留一份,方便排查创作者视角的问题(超管没有绑定创作者主体,进 creator-center 仍会被挡) +INSERT INTO sys_role_permission (role_id, permission_id) +SELECT r.id, p.id +FROM sys_role r +CROSS JOIN sys_permission p +WHERE r.code = 'super_admin' + AND p.code = 'creator-center:view' +ON CONFLICT DO NOTHING; diff --git a/sql/schema-phase1.sql b/sql/schema-phase1.sql index 4a6206e..aca3725 100644 --- a/sql/schema-phase1.sql +++ b/sql/schema-phase1.sql @@ -903,3 +903,13 @@ COMMENT ON TABLE download_task IS '离线下载任务与断点续传进度(分 DROP TRIGGER IF EXISTS trg_download_task_updated_at ON download_task; CREATE TRIGGER trg_download_task_updated_at BEFORE UPDATE ON download_task FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ============================================================ +-- 九、07 创作者中心(Web 端):后台账号 ↔ 创作者主体 +-- ============================================================ +-- 第一阶段创作者直接登后台,身份复用 sys_admin_user / sys_role / sys_menu, +-- 所以 creator 需要一列账号映射;用「部分唯一索引」: +-- admin_user_id = 0 表示尚未绑定账号,多条 0 不冲突;绑定之后一账号只能对应一个创作者主体。 +ALTER TABLE creator ADD COLUMN IF NOT EXISTS admin_user_id BIGINT NOT NULL DEFAULT 0; +CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_admin_user ON creator (admin_user_id) WHERE admin_user_id > 0; +COMMENT ON COLUMN creator.admin_user_id IS '绑定的后台账号 sys_admin_user.id,0 表示未绑定(创作者中心据此判定归属)';