feat: 获取Redis中活跃用户

This commit is contained in:
bunny 2025-05-04 19:32:31 +08:00
parent 015112c996
commit 3dd368140d
12 changed files with 2109 additions and 1340 deletions

View File

@ -261,7 +261,7 @@ docker compose up -d
- [x] 系统监控后端返回403停止请求
- [x] 优化用户配置权限逻辑,配置后热更新逻辑等
- [x] 完善后端注释有需要添加ReadMe文档
- [ ] Redis中获取活跃用户
- [x] 获取Redis中活跃用户
## 前后端接口规范

View File

@ -1,11 +1,11 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.Result;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
@ -56,7 +56,7 @@ public class UserController {
public Result<String> updateUserByAdmin(@Valid AdminUserUpdateDto dto,
@RequestPart(value = "avatar", required = false) MultipartFile avatar) {
if (avatar != null) dto.setAvatar(avatar);
userService.updateUserByAdmin(dto);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
}
@ -89,17 +89,16 @@ public class UserController {
return Result.success();
}
@Operation(summary = "更新本地用户信息", description = "更新本地用户信息需要更新Redis中的内容")
@PutMapping("private/update/userinfo")
public Result<String> updateAdminUserByLocalUser(@Valid @RequestBody AdminUserUpdateByLocalUserDto dto) {
userService.updateAdminUserByLocalUser(dto);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
@Operation(summary = "已登录用户", description = "查询缓存中已登录用户", tags = "user::query")
@GetMapping("getCacheUserPage/{page}/{limit}")
public Result<PageResult<LoginVo>> getCacheUserPage(
@Parameter(name = "page", description = "当前页", required = true)
@PathVariable("page") Integer page,
@Parameter(name = "limit", description = "每页记录数", required = true)
@PathVariable("limit") Integer limit) {
Page<AdminUser> pageParams = new Page<>(page, limit);
PageResult<LoginVo> pageResult = userService.getCacheUserPage(pageParams);
return Result.success(pageResult);
}
@Operation(summary = "更新本地用户密码", description = "更新本地用户密码")
@PutMapping("private/update/password")
public Result<String> updateUserPasswordByLocalUser(String password) {
userService.updateUserPasswordByLocalUser(password);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
}
}

View File

@ -4,6 +4,7 @@ import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.Result;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
@ -60,4 +61,18 @@ public class UserLoginController {
userLoginService.logout();
return Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
}
@Operation(summary = "更新本地用户信息", description = "更新本地用户信息需要更新Redis中的内容")
@PutMapping("private/update/userinfo")
public Result<String> updateAdminUserByLocalUser(@Valid @RequestBody AdminUserUpdateByLocalUserDto dto) {
userLoginService.updateAdminUserByLocalUser(dto);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
}
@Operation(summary = "更新本地用户密码", description = "更新本地用户密码")
@PutMapping("private/update/password")
public Result<String> updateUserPasswordByLocalUser(String password) {
userLoginService.updateUserPasswordByLocalUser(password);
return Result.success(ResultCodeEnum.UPDATE_SUCCESS);
}
}

View File

@ -0,0 +1,51 @@
package cn.bunny.services.redis;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class RedisService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 分页扫描Redis中匹配特定前缀的key
*
* @param pageNum 当前页码从1开始
* @param pageSize 每页大小
* @return 当前页的key列表
*/
@NotNull
public List<String> scannerRedisKeyByPage(long pageNum, long pageSize) {
String prefix = RedisUserConstant.getAdminLoginInfoPrefix("*");
List<String> keys = new ArrayList<>();
ScanOptions scanOptions = ScanOptions.scanOptions()
.match(prefix) // 匹配前缀
.count(1000) // 每次扫描的 key 数量优化性能
.build();
try (Cursor<String> cursor = redisTemplate.scan(scanOptions)) {
int skip = Math.toIntExact((pageNum - 1) * pageSize);
int count = 0;
while (cursor.hasNext()) {
String key = cursor.next();
if (count >= skip && keys.size() < pageSize) {
keys.add(key);
}
count++;
}
}
return keys;
}
}

View File

@ -135,4 +135,14 @@ public class IpUtil {
}
return ipAddress;
}
/**
* 替换IP地址格式127.**.**.1 192.**.**.100
*
* @param ipAddress IP地址
* @return 新的IP地址格式
*/
public static String replaceIp(String ipAddress) {
return ipAddress.replaceAll("(\\d{1,3}\\.)(\\d{1,3}\\.)(\\d{1,3}\\.)(\\d{1,3})", "$1**.**.$4");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -178,7 +178,7 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
}
// 设置响应头
HttpHeaders headers = FileUtil.buildHttpHeadersByBinary("i18n-configuration.zip");
HttpHeaders headers = FileUtil.buildHttpHeadersByBinary("i18n-configuration-" + type + ".zip");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return new ResponseEntity<>(byteArrayInputStream.readAllBytes(), headers, HttpStatus.OK);
@ -212,9 +212,9 @@ public class I18nServiceImpl extends ServiceImpl<I18nMapper, I18n> implements I1
// 内容存在删除这个数据库中所有关于这个key的多语言
List<I18n> i18nList = baseMapper.selectList(Wrappers.<I18n>lambdaQuery().eq(I18n::getTypeName, type));
List<Long> ids = i18nList.stream().map(BaseEntity::getId).toList();
if (!ids.isEmpty()) {
removeByIds(ids);
}
// 删除这个类型下所有的多语言
if (!ids.isEmpty()) removeByIds(ids);
// 存入内容
if (fileType.equals(FileType.JSON)) {

View File

@ -1,13 +1,14 @@
package cn.bunny.services.service.log.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.system.log.dto.UserLoginLogDto;
import cn.bunny.services.domain.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.log.vo.UserLoginLogLocalVo;
import cn.bunny.services.domain.system.log.vo.UserLoginLogVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.service.log.UserLoginLogService;
import cn.bunny.services.utils.IpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@ -44,6 +45,10 @@ public class UserLoginLogServiceImpl extends ServiceImpl<UserLoginLogMapper, Use
.map(userLoginLog -> {
UserLoginLogVo userLoginLogVo = new UserLoginLogVo();
BeanUtils.copyProperties(userLoginLog, userLoginLogVo);
// 隐藏部分IP地址
String ipAddress = userLoginLogVo.getIpAddress();
userLoginLogVo.setIpAddress(IpUtil.replaceIp(ipAddress));
return userLoginLogVo;
}).toList();

View File

@ -1,11 +1,13 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import org.jetbrains.annotations.NotNull;
public interface UserLoginService extends IService<AdminUser> {
@ -35,8 +37,22 @@ public interface UserLoginService extends IService<AdminUser> {
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
/**
* * 退出登录
* 退出登录
*/
void logout();
/**
* 更新本地用户信息
*
* @param dto 用户信息
*/
void updateAdminUserByLocalUser(AdminUserUpdateByLocalUserDto dto);
/**
* 更新本地用户密码
*
* @param password 更新本地用户密码
*/
void updateUserPasswordByLocalUser(@Valid String password);
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
@ -76,16 +76,10 @@ public interface UserService extends IService<AdminUser> {
List<UserVo> getUserListByKeyword(String keyword);
/**
* * 更新本地用户信息
* 查询缓存中已登录用户
*
* @param dto 用户信息
* @param pageParams 分页查询
* @return 分页查询结果
*/
void updateAdminUserByLocalUser(AdminUserUpdateByLocalUserDto dto);
/**
* * 更新本地用户密码
*
* @param password 更新本地用户密码
*/
void updateUserPasswordByLocalUser(@Valid String password);
PageResult<LoginVo> getCacheUserPage(Page<AdminUser> pageParams);
}

View File

@ -8,6 +8,7 @@ import cn.bunny.services.domain.common.enums.LoginEnums;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.email.entity.EmailTemplate;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
@ -15,6 +16,7 @@ import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.configuration.helper.email.ConcreteSenderEmailTemplate;
import cn.bunny.services.service.system.UserLoginService;
import cn.bunny.services.service.system.helper.UserLoginHelper;
@ -30,6 +32,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
@ -54,6 +57,8 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> imp
private EmailTemplateMapper emailTemplateMapper;
@Resource
private ConcreteSenderEmailTemplate concreteSenderEmailTemplate;
@Resource
private MinioHelper minioHelper;
/**
* 前台用户登录接口
@ -198,4 +203,64 @@ public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> imp
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
/**
* * 更新本地用户信息
*
* @param dto 用户信息
*/
@Override
public void updateAdminUserByLocalUser(AdminUserUpdateByLocalUserDto dto) {
Long userId = BaseContext.getUserId();
// 判断是否存在这个用户
AdminUser user = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
if (user == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
// 检查用户头像因为更新用户信息会带着用户之前的信息如果没有更新头像前端显示的http:xxx
String userAvatar = minioHelper.formatUserAvatar(dto.getAvatar());
dto.setAvatar(userAvatar);
// 更新用户
AdminUser adminUser = new AdminUser();
adminUser.setId(userId);
BeanUtils.copyProperties(dto, adminUser);
updateById(adminUser);
// 重新生成用户信息到Redis中
BeanUtils.copyProperties(dto, user);
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
}
/**
* * 更新本地用户密码
*
* @param password 更新本地用户密码
*/
@Override
public void updateUserPasswordByLocalUser(@Valid String password) {
// 根据当前用户查询用户信息
Long userId = BaseContext.getUserId();
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 判断用户是否存在
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
// 数据库中的密码
String dbPassword = adminUser.getPassword();
password = passwordEncoder.encode(password);
// 判断数据库中密码是否和更新用户密码相同
if (dbPassword.equals(password)) throw new AuthCustomerException(ResultCodeEnum.NEW_PASSWORD_SAME_OLD_PASSWORD);
// 更新用户密码
adminUser = new AdminUser();
adminUser.setId(userId);
adminUser.setPassword(password);
updateById(adminUser);
// 删除Redis中登录用户信息
redisTemplate.delete(RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername()));
}
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
@ -11,7 +11,6 @@ import cn.bunny.services.domain.system.files.vo.FileInfoVo;
import cn.bunny.services.domain.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.system.dto.user.AdminUserAddDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateByLocalUserDto;
import cn.bunny.services.domain.system.system.dto.user.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Role;
@ -26,9 +25,12 @@ import cn.bunny.services.mapper.system.UserDeptMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.redis.RedisService;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.service.system.UserService;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import cn.bunny.services.utils.IpUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -37,6 +39,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@ -75,6 +78,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
private RoleMapper roleMapper;
@Resource
private MinioHelper minioHelper;
@Autowired
private RedisService redisService;
/**
* * 获取用户信息
@ -175,63 +180,36 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
}
/**
* * 更新本地用户信息
* 查询缓存中已登录用户
*
* @param dto 用户信息
* @param pageParams 分页查询
* @return 分页查询结果
*/
@Override
public void updateAdminUserByLocalUser(AdminUserUpdateByLocalUserDto dto) {
Long userId = BaseContext.getUserId();
public PageResult<LoginVo> getCacheUserPage(Page<AdminUser> pageParams) {
long pageNum = pageParams.getCurrent();
long pageSize = pageParams.getSize();
List<String> keys = redisService.scannerRedisKeyByPage(pageNum, pageSize);
// 判断是否存在这个用户
AdminUser user = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
if (user == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
List<LoginVo> list = keys.stream().map(key -> {
Object object = redisTemplate.opsForValue().get(key);
LoginVo loginVo = JSON.parseObject(JSON.toJSONString(object), LoginVo.class);
// 检查用户头像因为更新用户信息会带着用户之前的信息如果没有更新头像前端显示的http:xxx
String userAvatar = minioHelper.formatUserAvatar(dto.getAvatar());
dto.setAvatar(userAvatar);
// 隐藏IP地址
String ip = IpUtil.replaceIp(loginVo.getIpAddress());
loginVo.setIpAddress(ip);
// 更新用户
AdminUser adminUser = new AdminUser();
adminUser.setId(userId);
BeanUtils.copyProperties(dto, adminUser);
updateById(adminUser);
loginVo.setPassword("********");
// 重新生成用户信息到Redis中
BeanUtils.copyProperties(dto, user);
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
return loginVo;
}).toList();
return PageResult.<LoginVo>builder()
.pageNo(pageNum).pageSize(pageSize)
.list(list).total((long) keys.size())
.build();
}
/**
* * 更新本地用户密码
*
* @param password 更新本地用户密码
*/
@Override
public void updateUserPasswordByLocalUser(@Valid String password) {
// 根据当前用户查询用户信息
Long userId = BaseContext.getUserId();
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 判断用户是否存在
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
// 数据库中的密码
String dbPassword = adminUser.getPassword();
password = passwordEncoder.encode(password);
// 判断数据库中密码是否和更新用户密码相同
if (dbPassword.equals(password)) throw new AuthCustomerException(ResultCodeEnum.NEW_PASSWORD_SAME_OLD_PASSWORD);
// 更新用户密码
adminUser = new AdminUser();
adminUser.setId(userId);
adminUser.setPassword(password);
updateById(adminUser);
// 删除Redis中登录用户信息
redisTemplate.delete(RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername()));
}
/**
* * 用户信息 服务实现类