feat: 创作者账号体系(后台开号 → 登录即进创作者中心)+ 创作者中心菜单只给创作者
为什么:创作者此前根本无法登录 —— creator.admin_user_id 只有实体字段、没有任何接口会写它,
C 端申请入驻也没做,等于整条账号链路缺入口。这批先把"后台手工建主体 → 审核 → 开号 → 登录落地"打通;
同时按用户口径收窄菜单:只要不是创作者,就不显示创作者中心。
- 手工建主体:POST /admin/creators(编号可传可自动生成 CR+日期+序号)+ 审核页「新增创作者」
- 开通账号:POST /admin/creators/{id}/account —— 仅 approved 可开(否则 200003)、已开通幂等返回
(created=false、不回明文)、用户名默认 creator.code 小写且冲突报 200001、随机 10 位初始密码一次性返回、
建 sys_admin_user + 绑 creator.admin_user_id + 授 creator 角色
- 重置密码:POST /admin/creators/{id}/account/reset-password(同样一次性明文,旧密码立即失效)
- 首登强制改密:sys_admin_user 加 must_change_password 列(幂等 ALTER;不新建表、不复用 two_factor 是因为
这个标记要能直接驱动登录响应与前端拦截),开号/重置置 1,自助改密后清 0
- 菜单收窄:26-30 从超管收回(sys_role_menu delete);另外发现超管的菜单是"全量兜底"、根本不看角色-菜单表,
光收授权无效,所以加 sys_menu.super_admin_visible,这几条置 0 —— 超管侧边栏不再出现创作者中心
(代看代码保留,只是没有入口了,见回执)
- 权限点 creator:account(只授超管);creator:list 之外的读接口未变
实测:未审核开号 200003 / 重复开号幂等 / 用户名冲突 200001 / 重置后旧密码失效新密码可登录 /
新账号只能看到自己的作品;创作者登录直接落 /creator-center/workspace 并弹出首登改密。
This commit is contained in:
1 parent
efeb9fcfaf
commit
414d9fd482
15 files changed
+377
-8
No files matched your search
+149
@@ -0,0 +1,149 @@
|
||||
package com.fruitshorts.creator.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.creator.dal.entity.Creator;
|
||||
import com.fruitshorts.creator.dal.mapper.CreatorMapper;
|
||||
import com.fruitshorts.creator.vo.CreatorAccountRespVO;
|
||||
import com.fruitshorts.system.dal.entity.AdminUser;
|
||||
import com.fruitshorts.system.dal.entity.Role;
|
||||
import com.fruitshorts.system.dal.mapper.RoleMapper;
|
||||
import com.fruitshorts.system.service.AdminUserService;
|
||||
import com.fruitshorts.system.vo.AdminUserSaveReqVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* 创作者账号:后台给审核通过的创作者开一个**独立后台账号**,他登录后直接落创作者中心。
|
||||
*
|
||||
* 约定:
|
||||
* - 只有 `approved` 的创作者能开通(没审核过就发账号等于绕过入驻);
|
||||
* - **幂等**:已经绑定了 `admin_user_id` 就不重复建号,返回 `created=false` 且不回明文密码,
|
||||
* 运营要重发就用「重置密码」;
|
||||
* - 初始密码是**一次性明文**:只在开通 / 重置的响应里出现一次,库里存 BCrypt 哈希,
|
||||
* 并且置 `must_change_password=1`,使用者首次登录会被要求改密。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CreatorAccountService {
|
||||
|
||||
private static final String ROLE_CODE = "creator";
|
||||
private static final int PASSWORD_LENGTH = 10;
|
||||
/** 去掉了 0/O/1/l/I 这类容易转达错的字符 */
|
||||
private static final String PASSWORD_CHARS =
|
||||
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private final CreatorMapper creatorMapper;
|
||||
private final AdminUserService adminUserService;
|
||||
private final RoleMapper roleMapper;
|
||||
|
||||
/** 开通账号:用户名默认取 creator.code 小写,可用参数覆盖。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CreatorAccountRespVO open(Long creatorId, String usernameOverride) {
|
||||
Creator creator = requireCreator(creatorId);
|
||||
if (!CreatorService.STATUS_APPROVED.equals(creator.getStatus())) {
|
||||
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED,
|
||||
"只有审核通过的创作者才能开通账号,当前状态:"
|
||||
+ CreatorService.statusText(creator.getStatus()));
|
||||
}
|
||||
if (creator.getAdminUserId() != null && creator.getAdminUserId() > 0) {
|
||||
AdminUser existing = adminUserService.getExisting(creator.getAdminUserId());
|
||||
return resp(creator, existing, null, false,
|
||||
"该创作者已经开通账号(" + existing.getUsername() + ");需要新密码请用「重置密码」");
|
||||
}
|
||||
|
||||
String username = StringUtils.hasText(usernameOverride)
|
||||
? usernameOverride.trim()
|
||||
: creator.getCode().toLowerCase();
|
||||
if (adminUserService.getByUsername(username) != null) {
|
||||
throw new ServiceException(ErrorCode.DATA_EXISTS, "登录账号已被占用:" + username);
|
||||
}
|
||||
|
||||
Role role = roleMapper.selectOne(Wrappers.<Role>lambdaQuery()
|
||||
.eq(Role::getCode, ROLE_CODE).last("limit 1"));
|
||||
if (role == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "系统里没有 creator 角色,请先执行 sql/data-phase1.sql");
|
||||
}
|
||||
|
||||
String rawPassword = randomPassword();
|
||||
AdminUserSaveReqVO req = new AdminUserSaveReqVO();
|
||||
req.setUsername(username);
|
||||
req.setPassword(rawPassword);
|
||||
req.setName(creator.getName());
|
||||
req.setRoleId(role.getId());
|
||||
req.setStatus(1);
|
||||
// 初始密码是运营转达的,首次登录必须改
|
||||
Long adminUserId = adminUserService.create(req, true);
|
||||
|
||||
creator.setAdminUserId(adminUserId);
|
||||
creatorMapper.updateById(creator);
|
||||
|
||||
AdminUser created = adminUserService.getExisting(adminUserId);
|
||||
return resp(creator, created, rawPassword, true,
|
||||
"账号已开通;初始密码只显示这一次,请尽快转达并让创作者首登后修改");
|
||||
}
|
||||
|
||||
/** 重置密码:同样一次性明文 + 置首登强制改密。 */
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CreatorAccountRespVO resetPassword(Long creatorId) {
|
||||
Creator creator = requireCreator(creatorId);
|
||||
if (creator.getAdminUserId() == null || creator.getAdminUserId() <= 0) {
|
||||
throw new ServiceException(ErrorCode.OPERATION_NOT_ALLOWED, "该创作者还没有开通账号,请先开通");
|
||||
}
|
||||
String rawPassword = randomPassword();
|
||||
adminUserService.resetPasswordTo(creator.getAdminUserId(), rawPassword, true);
|
||||
AdminUser user = adminUserService.getExisting(creator.getAdminUserId());
|
||||
return resp(creator, user, rawPassword, false,
|
||||
"密码已重置;新密码只显示这一次,旧密码立即失效");
|
||||
}
|
||||
|
||||
private Creator requireCreator(Long creatorId) {
|
||||
Creator creator = creatorId == null ? null : creatorMapper.selectById(creatorId);
|
||||
if (creator == null) {
|
||||
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "创作者不存在");
|
||||
}
|
||||
return creator;
|
||||
}
|
||||
|
||||
/** 10 位随机密码,保证同时含大写、小写与数字 */
|
||||
private String randomPassword() {
|
||||
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
|
||||
sb.append(randomOf("ABCDEFGHJKLMNPQRSTUVWXYZ"));
|
||||
sb.append(randomOf("abcdefghijkmnopqrstuvwxyz"));
|
||||
sb.append(randomOf("23456789"));
|
||||
while (sb.length() < PASSWORD_LENGTH) {
|
||||
sb.append(randomOf(PASSWORD_CHARS));
|
||||
}
|
||||
// 打散固定位置,避免"首字符永远是大写"这种可猜的规律
|
||||
char[] chars = sb.toString().toCharArray();
|
||||
for (int i = chars.length - 1; i > 0; i--) {
|
||||
int j = RANDOM.nextInt(i + 1);
|
||||
char tmp = chars[i];
|
||||
chars[i] = chars[j];
|
||||
chars[j] = tmp;
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
private char randomOf(String chars) {
|
||||
return chars.charAt(RANDOM.nextInt(chars.length()));
|
||||
}
|
||||
|
||||
private CreatorAccountRespVO resp(Creator creator, AdminUser user, String rawPassword,
|
||||
boolean created, String message) {
|
||||
CreatorAccountRespVO vo = new CreatorAccountRespVO();
|
||||
vo.setCreatorId(creator.getId());
|
||||
vo.setAdminUserId(user == null ? creator.getAdminUserId() : user.getId());
|
||||
vo.setUsername(user == null ? "" : user.getUsername());
|
||||
vo.setPassword(rawPassword);
|
||||
vo.setCreated(created);
|
||||
vo.setMessage(message);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+43
@@ -12,12 +12,15 @@ 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 com.fruitshorts.creator.vo.CreatorSaveReqVO;
|
||||
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.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -57,6 +60,46 @@ public class CreatorService {
|
||||
return creator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台手工录入创作者主体(C 端申请入驻还没做,先用它打通链路)。
|
||||
* 新录入的主体是 pending,必须走一次入驻审核通过才能开通账号。
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long create(CreatorSaveReqVO req) {
|
||||
String code = StringUtils.hasText(req.getCode()) ? req.getCode().trim() : generateCode();
|
||||
Long exists = creatorMapper.selectCount(Wrappers.<Creator>lambdaQuery().eq(Creator::getCode, code));
|
||||
if (exists != null && exists > 0) {
|
||||
throw new ServiceException(ErrorCode.DATA_EXISTS, "创作者编号已存在:" + code);
|
||||
}
|
||||
Creator creator = new Creator();
|
||||
creator.setCode(code);
|
||||
creator.setName(req.getName().trim());
|
||||
creator.setType(StringUtils.hasText(req.getType()) ? req.getType() : "INDIVIDUAL");
|
||||
creator.setContactName(req.getContactName() == null ? "" : req.getContactName());
|
||||
creator.setContactMobile(req.getContactMobile() == null ? "" : req.getContactMobile());
|
||||
creator.setContactEmail(req.getContactEmail() == null ? "" : req.getContactEmail());
|
||||
creator.setCountryCode(StringUtils.hasText(req.getCountryCode()) ? req.getCountryCode() : "ID");
|
||||
creator.setUserId(req.getUserId() == null ? 0L : req.getUserId());
|
||||
creator.setAdminUserId(0L);
|
||||
creator.setStatus(STATUS_PENDING);
|
||||
creator.setAuditNote("");
|
||||
creator.setAuditBy(0L);
|
||||
creator.setSettlementCurrency("IDR");
|
||||
creator.setBankAccount("");
|
||||
creator.setTaxNo("");
|
||||
creator.setKycStatus("not_submitted");
|
||||
creator.setShareRate(java.math.BigDecimal.ZERO);
|
||||
creatorMapper.insert(creator);
|
||||
return creator.getId();
|
||||
}
|
||||
|
||||
/** 编号格式沿用短剧那套:CR + 日期 + 3 位序号 */
|
||||
private String generateCode() {
|
||||
Long count = creatorMapper.selectCount(null);
|
||||
return "CR" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
|
||||
+ String.format("%03d", (count == null ? 0 : count + 1));
|
||||
}
|
||||
|
||||
/** 资质文件列表:审核时要能看到证件原件。 */
|
||||
public List<CreatorDocument> documents(Long creatorId) {
|
||||
getExisting(creatorId);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 开通创作者账号:用户名默认取 creator.code 小写,可用这个字段覆盖。 */
|
||||
@Data
|
||||
public class CreatorAccountOpenReqVO {
|
||||
|
||||
/** 覆盖默认登录名;留空则用 creator.code 的小写形式 */
|
||||
private String username;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 开通 / 重置密码的结果。
|
||||
*
|
||||
* `password` 是**一次性明文**:只在这次响应里出现,库里存的是 BCrypt 哈希,之后谁也拿不回来。
|
||||
* 幂等命中(账号已存在)时 `created=false`、`password=null`,提示走"重置密码"。
|
||||
*/
|
||||
@Data
|
||||
public class CreatorAccountRespVO {
|
||||
|
||||
private Long creatorId;
|
||||
private Long adminUserId;
|
||||
private String username;
|
||||
/** 一次性明文初始密码;幂等命中时为 null */
|
||||
private String password;
|
||||
/** true = 这次真的新建了账号 */
|
||||
private Boolean created;
|
||||
/** 给运营看的一句话说明 */
|
||||
private String message;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.fruitshorts.creator.vo;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 后台手工录入创作者主体。
|
||||
*
|
||||
* C 端 `/app/creator/apply` 还没实现,先用它把"建主体 → 审核 → 开账号"这条链路打通。
|
||||
*/
|
||||
@Data
|
||||
public class CreatorSaveReqVO {
|
||||
|
||||
@NotBlank(message = "请填写创作者名称")
|
||||
private String name;
|
||||
|
||||
/** INDIVIDUAL / COMPANY,默认 INDIVIDUAL */
|
||||
private String type;
|
||||
/** 编号:不传则自动生成 CR+日期+序号;也是开账号时默认的登录名(小写) */
|
||||
private String code;
|
||||
private String contactName;
|
||||
private String contactMobile;
|
||||
private String contactEmail;
|
||||
private String countryCode;
|
||||
/** 可选:绑定已有的 C 端账号 app_user.id */
|
||||
private Long userId;
|
||||
}
|
||||
Reference in new issue
Block a user