feat(admin): 新增邀请码生成及注册校验功能

- 在管理员页面新增邀请码生成面板,支持限制使用次数和有效期
- 新增后端接口支持创建邀请码,邀请码存储在Redis并设置过期时间
- 用户注册接口新增邀请码参数,校验邀请码有效性和剩余使用次数
- 注册时成功使用邀请码后,Redis中对应邀请码的使用次数减1
- 登录接口及相关服务层逻辑新增邀请码字段支持
- 后端权限配置增加/admin路径的root角色校验
- 优化角色权限同步时Redis存储格式为列表类型
- 调整SaToken相关接口实现以支持角色ID转换逻辑
This commit is contained in:
lbw
2025-12-29 15:44:05 +08:00
parent bddf6c0936
commit 5858bf2ecc
14 changed files with 163 additions and 24 deletions

View File

@@ -24,24 +24,12 @@ public class SaTokenConfigure implements WebMvcConfigurer {
.back();
SaRouter.match("/**")
.notMatch("/class/list")
.notMatch("/exam/words/get")
.notMatch("/exam/words/detail")
.notMatch("/exam/words/student/history")
.notMatch("grade/list")
.notMatch("/student/list")
.notMatch("/student/detail")
.notMatch("/studentLessonPlans/list")
.notMatch("/studentLessonPlans/history")
.notMatch("/student/analyze")
.notMatch("/student/mastery/detail")
.notMatch("/unit/list")
.notMatch("/vocabulary/list")
.notMatch("/vocabulary/student/detail")
.notMatch("/plan/download")
.notMatch("/login/**")
.check(r -> StpUtil.checkLogin());
SaRouter.match("/admin/**")
.check(r -> StpUtil.checkRole("root"));
}))
.addPathPatterns("/**")
.excludePathPatterns("/error");

View File

@@ -28,7 +28,11 @@ public class StpInterfaceImpl implements StpInterface {
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return userToRole((Long) loginId) ;
long l = 0L;
if (loginId instanceof String loginIdStr) {
l = Long.parseLong(loginIdStr);
}
return userToRole(l);
}
private List<String> userToRole(Long userId) {

View File

@@ -4,7 +4,13 @@ public class UserRedisConstants {
public static final String USER_LOGIN_CODE = "user:login:code:";
public static final String USER_INVITATION_CODE = "user:invitation:code:";
public static String buildUserLoginCode(String phone) {
return USER_LOGIN_CODE + phone;
}
public static String buildUserInvitationCode(String code) {
return USER_INVITATION_CODE + code;
}
}

View File

@@ -1,7 +1,10 @@
package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.constant.UserRedisConstants;
import com.yinlihupo.enlish.service.domain.dataobject.RoleDO;
import com.yinlihupo.enlish.service.domain.dataobject.UserDO;
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeReqVO;
import com.yinlihupo.enlish.service.model.vo.admin.CreateInvitationCodeRspVO;
import com.yinlihupo.enlish.service.model.vo.user.CreateUserReqVO;
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRepVO;
import com.yinlihupo.enlish.service.model.vo.user.FindUserListRspVO;
@@ -11,6 +14,7 @@ import com.yinlihupo.framework.biz.operationlog.aspect.ApiOperationLog;
import com.yinlihupo.framework.common.response.PageResponse;
import com.yinlihupo.framework.common.response.Response;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -19,6 +23,8 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@RequestMapping("/admin/")
@RestController
@@ -30,6 +36,8 @@ public class AdminController {
private PasswordEncoder passwordEncoder;
@Resource
private RoleService roleService;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@PostMapping("user/list")
@ApiOperationLog(description = "查询用户列表")
@@ -66,4 +74,18 @@ public class AdminController {
return Response.success();
}
@PostMapping("user/create/invitation/code")
@ApiOperationLog(description = "创建邀请码")
public Response<CreateInvitationCodeRspVO> createInvitationCode(@RequestBody CreateInvitationCodeReqVO createInvitationCodeReqVO) {
String code = String.valueOf(new Random().nextInt(1000000));
Integer limit = createInvitationCodeReqVO.getLimit();
Integer expire = createInvitationCodeReqVO.getExpire();
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(code), limit);
redisTemplate.expire(UserRedisConstants.buildUserInvitationCode(code), expire, TimeUnit.DAYS);
return Response.success(CreateInvitationCodeRspVO.builder().invitationCode(code).build());
}
}

View File

@@ -25,7 +25,7 @@ public class LoginController {
@ApiOperationLog(description = "登录")
public Response<String> login(@RequestBody LoginReqVO loginReqVO) {
try {
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode());
loginService.login(loginReqVO.getPhone(), loginReqVO.getName(), loginReqVO.getPassword(), loginReqVO.getCode(), loginReqVO.getInvitationCode());
return Response.success(StpUtil.getTokenInfo().getTokenValue());
} catch (Exception e) {
log.error("注册或登录失败 {}", e.getMessage());

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.model.vo.admin;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class CreateInvitationCodeReqVO {
// 限制人数
private Integer limit;
// 有效期
private Integer expire;
}

View File

@@ -0,0 +1,16 @@
package com.yinlihupo.enlish.service.model.vo.admin;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class CreateInvitationCodeRspVO {
private String invitationCode;
}

View File

@@ -15,4 +15,5 @@ public class LoginReqVO {
private String name;
private String password;
private String code;
private String invitationCode;
}

View File

@@ -2,7 +2,7 @@ package com.yinlihupo.enlish.service.service;
public interface LoginService {
void login(String phone, String name, String reqPassword, String reqCode);
void login(String phone, String name, String reqPassword, String reqCode, String invitationCode);
void sendVerificationCode(String phone);
}

View File

@@ -13,11 +13,14 @@ import com.yinlihupo.enlish.service.service.LoginService;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.util.Objects;
@Service
@Slf4j
@@ -28,20 +31,32 @@ public class LoginServiceImpl implements LoginService {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private StringRedisTemplate stringRedisTemplate;
private RedisTemplate<String, Object> redisTemplate;
@Resource
private Client client;
@Override
public void login(String phone, String name, String reqPassword, String reqCode) {
@Transactional(rollbackFor = Exception.class)
public void login(String phone, String name, String reqPassword, String reqCode, String invitationCode) {
UserDO userDO = userDOMapper.selectByPhone(phone);
log.info("userDO:{}", userDO);
String code = stringRedisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone));
String code = JsonUtils.toJsonString(redisTemplate.opsForValue().get(UserRedisConstants.buildUserLoginCode(phone)));
if (userDO == null) {
if (code == null || !code.equals(reqCode)) {
throw new RuntimeException("验证码错误");
}
Object invitationObj = redisTemplate.opsForValue().get(UserRedisConstants.buildUserInvitationCode(invitationCode));
if (invitationObj == null) {
throw new RuntimeException("邀请码错误");
}
int invitationLimit = Integer.parseInt(JsonUtils.toJsonString(invitationObj));
if (invitationLimit <= 0) {
throw new RuntimeException("邀请码已使用完毕");
}
redisTemplate.opsForValue().set(UserRedisConstants.buildUserInvitationCode(invitationCode), invitationLimit - 1);
userDO = UserDO.builder()
.phone(phone)
.name(name)
@@ -68,7 +83,7 @@ public class LoginServiceImpl implements LoginService {
@Override
public void sendVerificationCode(String phone) {
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
redisTemplate.opsForValue().set(UserRedisConstants.buildUserLoginCode(phone), code, Duration.ofSeconds(60));
String signName = "短信测试";
String templateCode = "SMS_154950909";
String templateParam = String.format("{\"code\":\"%s\"}", code);

View File

@@ -48,7 +48,7 @@ public class RoleServiceImpl implements RoleService {
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), JsonUtils.toJsonString(user2RoleKeys));
redisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), user2RoleKeys);
});
}