feat: 创作者中心支持超管只读代看(未绑定不再白屏)

为什么:/creator-center 菜单同时给了超管和创作者,但接口只认"绑定了 creator.admin_user_id 的账号",
而 creator 表为空 —— 超管点进去每个接口都 403,页面全白。这是设计缺口:需要一条"超管代看"的合法路径,
且必须既能让运营排查问题,又不能变成越权入口。

- requireCurrentCreator 改三级解析:① 绑定优先(创作者角色永远走这条)② 未绑定 + creator-center:impersonate
  + 请求头 X-Creator-Id → 只读代看 ③ 其余给可识别错误码(200004 未绑定 / 200005 未选目标)
- 目标走独立请求头而不是查询参数:creator-center 接口继续"不接收 creatorId";
  且只有具备 impersonate 权限的账号这个头才生效 —— 创作者自己带也不影响"只看自己的"
- 只读硬边界:代看态下作品编辑、分集增删改/排序、上传会话与产物挂载、提交/撤回、收费配置一律 200006
  (写路径统一走 requireWritableDrama/requireWritable;上传会话在 MediaController 里用 ImpersonationGuard 拦)
- 代看进入写 audit_log(AuditLog 实体 + AuditLogService,同人同目标 10 分钟去重),表本来就有、之前无代码引用
- 新增 GET /admin/creator-center/impersonation/targets(仅 impersonate)供顶部下拉取数
- 权限点 creator-center:impersonate 只授予 super_admin(content_operator 第一阶段不给)
- 修掉菜单 26 的路由名冲突:它和 17 号菜单(创作者管理)都叫 CreatorCenter,Vue Router 同名路由互相覆盖,
  导致超管整个 /creator-center 子树注册不上(点进去 404)。改名 CreatorWorkspace,并核对全表无重名

实测:创作者带 X-Creator-Id 仍只看自己的、仍能编辑自己的作品;代看下 9 个写操作全 403;
audit_log 有且仅有一条代看记录(12 次代看请求去重成 1 条)。
This commit is contained in:
Vicky committed 2026-10-15 15:25:22 +08:00
1 parent a635de7111
commit efeb9fcfaf
14 files changed
+351 -25

No files matched your search

@@ -7,6 +7,7 @@ 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.creator.vo.CreatorCenterTargetVO;
import com.fruitshorts.admin.service.CreatorDashboardService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -67,4 +68,14 @@ public class CreatorCenterController {
public Result<Map<String, Object>> dashboard() {
return Result.ok(creatorDashboardService.overview());
}
/**
* 代看目标下拉的数据源:只有带 `creator-center:impersonate` 的账号(第一阶段 = 超管)能调。
* 前端拿它渲染顶部的「当前创作者」下拉;没权限的账号不显示下拉,也不会请求这个接口。
*/
@GetMapping("/impersonation/targets")
@PreAuthorize("@ss.hasPermission('creator-center:impersonate')")
public Result<List<CreatorCenterTargetVO>> impersonationTargets() {
return Result.ok(creatorCenterService.impersonationTargets());
}
}
@@ -2,6 +2,7 @@ package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.security.ImpersonationGuard;
import com.fruitshorts.media.service.MediaService;
import com.fruitshorts.media.vo.MediaAssetRespVO;
import com.fruitshorts.media.vo.UploadCompleteReqVO;
@@ -33,6 +34,8 @@ public class MediaController {
@PostMapping("/upload/init")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<UploadInitRespVO> init(@Valid @RequestBody UploadInitReqVO req) {
// 代看是只读:上传会话属于写操作,代看态直接拒(不带 X-Creator-Id 的普通后台上传不受影响)
ImpersonationGuard.rejectWrite();
return Result.ok(mediaService.init(req, SecurityUtils.getUserId()));
}
@@ -41,6 +44,7 @@ public class MediaController {
public Result<Integer> uploadChunk(@RequestParam String uploadId,
@RequestParam int index,
@RequestParam("file") MultipartFile file) {
ImpersonationGuard.rejectWrite();
mediaService.saveChunk(uploadId, index, file, SecurityUtils.getUserId());
return Result.ok(index);
}
@@ -48,6 +52,7 @@ public class MediaController {
@PostMapping("/upload/complete")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) {
ImpersonationGuard.rejectWrite();
return Result.ok(mediaService.complete(req, SecurityUtils.getUserId()));
}