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

+22
View File
@@ -479,6 +479,28 @@ POST /app/auth/register
**本阶段进度**:07 全部 5 批完成,第一阶段 05 / 06 / 07 / 08 四块闭环。
**超管代看(只读)**
后台账号如果没绑定创作者主体,又点进了创作者中心,接口不再直接 403 白屏,而是三级解析:
| 情况 | 结果 |
| --- | --- |
| 账号绑定了 `creator.admin_user_id` | 用自己的身份(**创作者角色永远走这条**,代看逻辑不影响他) |
| 未绑定 + 有 `creator-center:impersonate` + 请求带 `X-Creator-Id` | 只读代看该创作者 |
| 其余 | `200004`(未绑定)/ `200005`(有代看权限但没选目标)—— 前端据此渲染引导页 |
**红线**:
- 目标创作者走**独立请求头 `X-Creator-Id`**,不做查询参数 —— creator-center 接口依旧不接收 `creatorId`;
- 这个头**只有**持有 `creator-center:impersonate` 的账号才生效,创作者自己带上没有任何作用(实测:带头上限仍只看到自己的数据,仍能正常编辑自己的作品);
- **代看只读**:作品编辑、分集增删改与排序、上传会话(`/admin/media/upload/*` 的 init/chunk/complete)与产物挂载、
提交 / 撤回、收费配置**全部拒绝**,错误码 `200006`;
- 代看进入写 `audit_log`(`biz_type=CREATOR`、`action=IMPERSONATE_VIEW`、操作人、IP、目标创作者),
同一操作人对同一目标 10 分钟内去重,避免翻页把审计表刷爆。
| 方法 | 路径 | 权限点 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/creator-center/impersonation/targets` | `creator-center:impersonate` | 代看目标下拉的数据源(id / 名称 / 状态) |
## 3. 契约与实现状态对照
| 模块 | 表结构 | 接口 | 代码 |
@@ -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()));
}
@@ -27,7 +27,15 @@ public enum ErrorCode {
DATA_EXISTS(200001, "数据已存在"),
DATA_NOT_FOUND(200002, "数据不存在"),
OPERATION_NOT_ALLOWED(200003, "当前状态不允许该操作");
OPERATION_NOT_ALLOWED(200003, "当前状态不允许该操作"),
/**
* 创作者中心的身份类错误:单独给码,前端才能区分「未绑定」(渲染引导页)
* 与「没选代看目标」「代看只读」三种情况,而不是一律弹通用报错。
*/
CREATOR_NOT_BOUND(200004, "当前账号未绑定创作者主体"),
CREATOR_TARGET_REQUIRED(200005, "请先选择要代看的创作者"),
CREATOR_IMPERSONATE_READONLY(200006, "代看模式下只读,不能修改创作者数据");
private final int code;
private final String message;
@@ -0,0 +1,72 @@
package com.fruitshorts.framework.security;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 超管「代看创作者」的识别与只读闸。
*
* 约定(红线):
* 1. 目标创作者走**独立请求头** `X-Creator-Id`,不做查询参数 —— 创作中心的接口永远不接收 creatorId 参数;
* 2. 这个头**只有**持有 `creator-center:impersonate` 的账号才生效。创作者自己带上它没有任何作用,
* 也不影响"只能看自己的"这条规则;
* 3. 代看是**只读**:写操作(作品编辑、分集增删改、上传会话与产物挂载、提交/撤回、收费配置)一律拒绝。
*/
public final class ImpersonationGuard {
public static final String CREATOR_HEADER = "X-Creator-Id";
public static final String PERMISSION = "creator-center:impersonate";
private ImpersonationGuard() {
}
/** 请求头里的目标创作者 id;没带或格式不对都返回 null。 */
public static Long requestedCreatorId() {
HttpServletRequest request = currentRequest();
if (request == null) {
return null;
}
String raw = request.getHeader(CREATOR_HEADER);
if (!StringUtils.hasText(raw)) {
return null;
}
try {
long value = Long.parseLong(raw.trim());
return value > 0 ? value : null;
} catch (NumberFormatException ignored) {
return null;
}
}
/** 当前这次请求是否属于"代看":带了目标 id + 调用方确实有代看权限。 */
public static boolean isImpersonating() {
LoginUser loginUser = SecurityUtils.getLoginUserOrNull();
return loginUser != null
&& loginUser.hasPermission(PERMISSION)
&& requestedCreatorId() != null;
}
/** 代看状态下拒绝写操作;不是代看则什么也不做。 */
public static void rejectWrite() {
if (isImpersonating()) {
throw new ServiceException(ErrorCode.CREATOR_IMPERSONATE_READONLY,
"代看模式下只读:不能修改创作者数据(当前代看创作者 #" + requestedCreatorId() + ")");
}
}
public static String clientIp() {
HttpServletRequest request = currentRequest();
return request == null ? "" : com.fruitshorts.framework.web.WebUtils.getClientIp(request);
}
private static HttpServletRequest currentRequest() {
if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attributes) {
return attributes.getRequest();
}
return null;
}
}
+5
View File
@@ -32,5 +32,10 @@
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-media</artifactId>
</dependency>
<!-- 代看创作者要写 audit_log(审计表在系统域) -->
<dependency>
<groupId>com.fruitshorts</groupId>
<artifactId>fruitshorts-module-system</artifactId>
</dependency>
</dependencies>
</project>
@@ -17,7 +17,11 @@ 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.framework.security.ImpersonationGuard;
import com.fruitshorts.framework.security.LoginUser;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.system.service.AuditLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -30,12 +34,16 @@ import java.util.Map;
import java.util.Objects;
/**
* 创作者中心(Web 端,做在后台管理里):创作者本人视角的只读查询与归属校验。
* 创作者中心(Web 端,做在后台管理里):创作者本人视角的查询与归属校验。
*
* **归属判定的唯一入口是 {@link #requireCurrentCreator()}**:
* 创作者身份从登录态(sys_admin_user.id)解析,creator-center 的所有接口都**不收 creatorId 参数**,
* 查询一律带 `where creator_id = 当前创作者`。所以横向越权不是靠"记得加过滤",
* 而是参数层面就没有入口;真被别人猜到 id 时,{@link #requireMyDrama} 会直接 403。
* **归属判定的唯一入口是 {@link #currentContext()},三级解析**:
* ① 账号绑定了 creator → 用自己(**创作者角色永远走这条**);
* ② 未绑定 + 有 `creator-center:impersonate` + 请求带 `X-Creator-Id` → **只读代看**;
* ③ 其余 → 带可识别错误码的 403({@link ErrorCode#CREATOR_NOT_BOUND} / {@link ErrorCode#CREATOR_TARGET_REQUIRED}),
* 前端据此渲染引导页而不是白屏。
*
* creator-center 的所有接口都**不收 creatorId 参数**,查询一律带 `where creator_id = 当前创作者`;
* 真被别人猜到 id 时,{@link #requireMyDrama} 会直接 403。代看态写操作由 {@link #requireWritable()} 拦。
*/
@Service
@RequiredArgsConstructor
@@ -46,20 +54,65 @@ public class CreatorCenterService {
private final CreatorMapper creatorMapper;
private final DramaMapper dramaMapper;
private final ContentSubmissionMapper submissionMapper;
private final AuditLogService auditLogService;
/** 当前登录人绑定的创作者主体;没绑定就是 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)
/**
* 当前请求对应的创作者上下文:自己的 or 只读代看的。
* 三级解析见类注释;代看成功时写一条审计(10 分钟内同人同目标去重)。
*/
public CreatorContext currentContext() {
LoginUser loginUser = SecurityUtils.getLoginUser();
// ① 绑定优先:创作者角色永远走这条,代看逻辑不会影响他自己
Creator own = creatorMapper.selectOne(Wrappers.<Creator>lambdaQuery()
.eq(Creator::getAdminUserId, loginUser.userId())
.last("limit 1"));
if (creator == null) {
throw new ServiceException(ErrorCode.FORBIDDEN, "当前账号未绑定创作者主体,无法进入创作者中心");
if (own != null) {
return new CreatorContext(own, false);
}
return creator;
// ② 未绑定:只有带代看权限的账号才能指定目标创作者
if (!loginUser.hasPermission(ImpersonationGuard.PERMISSION)) {
throw new ServiceException(ErrorCode.CREATOR_NOT_BOUND,
"当前账号未绑定创作者主体,无法进入创作者中心");
}
Long targetId = ImpersonationGuard.requestedCreatorId();
if (targetId == null) {
throw new ServiceException(ErrorCode.CREATOR_TARGET_REQUIRED,
"当前账号未绑定创作者主体,请选择要代看的创作者");
}
Creator target = creatorMapper.selectById(targetId);
if (target == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "要代看的创作者不存在");
}
auditLogService.recordImpersonationView(loginUser.userId(), loginUser.username(),
target.getId(), target.getCode(), ImpersonationGuard.clientIp());
return new CreatorContext(target, true);
}
/** 当前登录人(或代看目标)对应的创作者主体。 */
public Creator requireCurrentCreator() {
return currentContext().creator();
}
/** 代看态拒绝写操作;创作者本人不受影响。 */
public void requireWritable() {
ImpersonationGuard.rejectWrite();
}
/** 代看目标下拉的数据源:只有带代看权限的账号能调(接口层再加 @PreAuthorize)。 */
public List<CreatorCenterTargetVO> impersonationTargets() {
return creatorMapper.selectList(Wrappers.<Creator>lambdaQuery()
.orderByAsc(Creator::getCode))
.stream().map(creator -> {
CreatorCenterTargetVO vo = new CreatorCenterTargetVO();
vo.setId(creator.getId());
vo.setCode(creator.getCode());
vo.setName(creator.getName());
vo.setStatus(creator.getStatus());
vo.setStatusText(CreatorService.statusText(creator.getStatus()));
return vo;
}).toList();
}
/**
@@ -75,6 +128,16 @@ public class CreatorCenterService {
return drama;
}
/**
* 写路径专用:先确认作品属于"当前生效的创作者",再挡住代看态。
* 写接口一律用这个方法(而不是 requireMyDrama + 手写 requireWritable),漏一处就是越权。
*/
public Drama requireWritableDrama(Long dramaId) {
Drama drama = requireMyDrama(dramaId);
requireWritable();
return drama;
}
public CreatorCenterProfileVO profile() {
Creator creator = requireCurrentCreator();
CreatorCenterProfileVO vo = new CreatorCenterProfileVO();
@@ -52,6 +52,7 @@ public class CreatorContentService {
@Transactional(rollbackFor = Exception.class)
public Long createDrama(CreatorDramaEditReqVO req) {
Creator creator = centerService.requireCurrentCreator();
centerService.requireWritable();
requireApprovedCreator(creator);
return dramaService.createForCreator(toSaveReq(req), creator.getId(), creator.getAdminUserId());
}
@@ -59,7 +60,7 @@ public class CreatorContentService {
/** 编辑作品信息。定价、热门、排序不在本接口的可改范围,原样回填,避免被默认值冲掉。 */
@Transactional(rollbackFor = Exception.class)
public void updateDrama(Long dramaId, CreatorDramaEditReqVO req) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
// 没传的字段沿用现值:底层 apply() 对 null 会写空串 / 默认值,直接透传会把"没改的字段"抹掉
DramaSaveReqVO vo = toSaveReq(req, drama);
@@ -78,7 +79,7 @@ public class CreatorContentService {
/** 新增分集:集号不传就接在当前最大集号后面;新分集默认免费,收费由批 3 的配置页决定。 */
@Transactional(rollbackFor = Exception.class)
public Long addEpisode(Long dramaId, CreatorEpisodeReqVO req) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
Integer epNo = req.getEpNo();
@@ -105,7 +106,7 @@ public class CreatorContentService {
@Transactional(rollbackFor = Exception.class)
public void updateEpisode(Long dramaId, Long episodeId, CreatorEpisodeReqVO req) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
DramaEpisode episode = requireMyEpisode(dramaId, episodeId);
@@ -126,7 +127,7 @@ public class CreatorContentService {
@Transactional(rollbackFor = Exception.class)
public void deleteEpisode(Long dramaId, Long episodeId) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
requireMyEpisode(dramaId, episodeId);
episodeService.delete(episodeId);
@@ -135,7 +136,7 @@ public class CreatorContentService {
/** 批量排序:先确认列表里每一集都属于这部剧,避免借排序接口动别人的分集。 */
@Transactional(rollbackFor = Exception.class)
public void sortEpisodes(Long dramaId, List<Long> ids) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
List<DramaEpisode> episodes = episodeMapper.selectBatchIds(ids);
boolean allMine = episodes.size() == ids.size()
@@ -148,12 +149,14 @@ public class CreatorContentService {
public ContentSubmission submit(Long dramaId, String type) {
Creator creator = centerService.requireCurrentCreator();
centerService.requireWritable();
requireApprovedCreator(creator);
return submissionService.submit(creator, dramaId, type);
}
public ContentSubmission withdraw(Long submissionId) {
Creator creator = centerService.requireCurrentCreator();
centerService.requireWritable();
return submissionService.withdraw(creator, submissionId);
}
@@ -172,7 +175,7 @@ public class CreatorContentService {
*/
@Transactional(rollbackFor = Exception.class)
public void attachAsset(Long dramaId, Long episodeId, Long assetId) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
DramaEpisode episode = requireMyEpisode(dramaId, episodeId);
@@ -217,7 +220,7 @@ public class CreatorContentService {
*/
@Transactional(rollbackFor = Exception.class)
public PriceRespVO savePrice(Long dramaId, PriceSaveReqVO req) {
Drama drama = centerService.requireMyDrama(dramaId);
Drama drama = centerService.requireWritableDrama(dramaId);
requireEditable(drama);
req.setDramaId(drama.getId());
return priceService.save(req);
@@ -0,0 +1,12 @@
package com.fruitshorts.creator.service;
import com.fruitshorts.creator.dal.entity.Creator;
/**
* 一次创作者中心请求的身份上下文。
*
* @param creator 生效的创作者主体(自己的,或代看目标)
* @param impersonated 是否处于超管代看状态(只读)
*/
public record CreatorContext(Creator creator, boolean impersonated) {
}
@@ -0,0 +1,14 @@
package com.fruitshorts.creator.vo;
import lombok.Data;
/** 代看目标下拉项:超管选择要看哪个创作者时用(只给 id / 名称 / 状态,不给联系方式)。 */
@Data
public class CreatorCenterTargetVO {
private Long id;
private String code;
private String name;
private String status;
private String statusText;
}
@@ -0,0 +1,33 @@
package com.fruitshorts.system.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 业务对象变更审计(支付、权益、定价、代看等敏感操作)。
*
* 与 sys_operation_log(接口级操作日志)区分:这里是"谁对哪个业务对象做了什么",
* 表上没有 deleted 列,审计记录只增不改。
*/
@Data
@TableName("audit_log")
public class AuditLog {
@TableId(type = IdType.AUTO)
private Long id;
/** ORDER / ENTITLEMENT / USER / CREATOR / PRICE */
private String bizType;
private String bizId;
private String action;
private String beforeValue;
private String afterValue;
private Long operatorId;
private String operatorName;
private String ip;
private String remark;
private LocalDateTime createdAt;
}
@@ -0,0 +1,9 @@
package com.fruitshorts.system.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.system.dal.entity.AuditLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AuditLogMapper extends BaseMapper<AuditLog> {
}
@@ -0,0 +1,54 @@
package com.fruitshorts.system.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fruitshorts.system.dal.entity.AuditLog;
import com.fruitshorts.system.dal.mapper.AuditLogMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/** 审计写入。目前只有"代看创作者"在用,后续支付/定价等敏感操作也走这里。 */
@Service
@RequiredArgsConstructor
public class AuditLogService {
public static final String BIZ_CREATOR = "CREATOR";
public static final String ACTION_IMPERSONATE_VIEW = "IMPERSONATE_VIEW";
/** 同一操作人看同一个创作者,10 分钟内只记一条,避免下拉/翻页把审计表刷爆 */
private static final int DEDUPE_MINUTES = 10;
private final AuditLogMapper auditLogMapper;
/**
* 记录"代看进入"。
*
* 前端切换代看目标不是一次独立请求(只是给后续请求加了个头),所以这里做去重:
* 同一操作人对同一创作者,10 分钟内的连续访问合并成一条 —— 既留下"谁什么时候看了谁",
* 又不会因为列表翻页刷出几百条。
*/
public void recordImpersonationView(Long operatorId, String operatorName, Long creatorId,
String creatorCode, String ip) {
LocalDateTime since = LocalDateTime.now().minusMinutes(DEDUPE_MINUTES);
Long recent = auditLogMapper.selectCount(Wrappers.<AuditLog>lambdaQuery()
.eq(AuditLog::getBizType, BIZ_CREATOR)
.eq(AuditLog::getBizId, String.valueOf(creatorId))
.eq(AuditLog::getAction, ACTION_IMPERSONATE_VIEW)
.eq(AuditLog::getOperatorId, operatorId == null ? 0L : operatorId)
.ge(AuditLog::getCreatedAt, since));
if (recent != null && recent > 0) {
return;
}
AuditLog log = new AuditLog();
log.setBizType(BIZ_CREATOR);
log.setBizId(String.valueOf(creatorId));
log.setAction(ACTION_IMPERSONATE_VIEW);
log.setOperatorId(operatorId == null ? 0L : operatorId);
log.setOperatorName(operatorName == null ? "" : operatorName);
log.setIp(ip == null ? "" : ip);
log.setRemark("代看创作者 " + (creatorCode == null ? "" : creatorCode) + "(只读)");
auditLogMapper.insert(log);
}
}
+16 -1
View File
@@ -185,6 +185,19 @@ INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
('creator-center:stat:view', '创作者中心-查看看板', '创作者中心', 'BUTTON', 154)
ON CONFLICT (code) DO NOTHING;
-- 代看创作者(只读):第一阶段只给超管,content_operator 不给
INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
('creator-center:impersonate', '代看创作者(只读)', '创作者中心', 'BUTTON', 155)
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 = 'super_admin'
AND p.code = 'creator-center:impersonate'
ON CONFLICT DO NOTHING;
-- 创作者角色只拿创作者中心这几个点,不给任何后台运营权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT r.id, p.id
@@ -220,7 +233,9 @@ ON CONFLICT DO NOTHING;
-- 组件路径对应 src/views/creator-center/ 下的四个页面。
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
permission_code, type, always_show, affix, sort, visible) VALUES
(26, 0, '创作者中心', '/creator-center', 'CreatorCenter', '#', '/creator-center/workspace', 'ep:film', 'creator-center:view', 'CATALOG', 1, 0, 3, 1),
-- route_name 必须全局唯一:17 号菜单(创作者管理)已经叫 CreatorCenter,
-- 同名会让 Vue Router 后者覆盖前者、整个 /creator-center 子树注册不上(超管点进去就 404)
(26, 0, '创作者中心', '/creator-center', 'CreatorWorkspace', '#', '/creator-center/workspace', 'ep:film', 'creator-center:view', 'CATALOG', 1, 0, 3, 1),
(27, 26, '我的作品', 'workspace', 'creator-workspace', 'views/creator-center/WorkspaceView', '', '', 'creator-center:view', 'MENU', 0, 0, 1, 1),
(28, 26, '作品编辑', 'dramas/:id', 'creator-drama-edit', 'views/creator-center/DramaEditView', '', '', 'creator-center:drama:edit', 'MENU', 0, 0, 2, 0),
(29, 26, '收费配置', 'prices', 'creator-prices', 'views/creator-center/PriceConfigView', '', '', 'creator-center:price:update', 'MENU', 0, 0, 3, 1),