feat: 优化登录功能;

删除不用的函数和冗余代码,将用户业务功能放在service下;Minio更名为MinioService将业务类和服务类拆分
This commit is contained in:
bunny 2025-05-01 11:57:31 +08:00
parent a1e85e65f9
commit 0570ddd249
38 changed files with 948 additions and 693 deletions

View File

@ -3,7 +3,7 @@ package cn.bunny.services.aop;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
import cn.bunny.services.domain.common.enums.JobEnums;
import cn.bunny.services.domain.system.log.entity.ScheduleExecuteLog;
import cn.bunny.services.domain.common.model.dto.quartz.ScheduleExecuteLogJson;
import cn.bunny.services.domain.common.quartz.ScheduleExecuteLogJson;
import cn.bunny.services.mapper.log.ScheduleExecuteLogMapper;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.Resource;

View File

@ -1,16 +1,15 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.context.BaseContext;
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.*;
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;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.service.system.UserService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -33,49 +31,6 @@ public class UserController {
@Resource
private UserService userService;
// -----------------------------------------
// 用户登录和退出
// -----------------------------------------
@Operation(summary = "用户登录", description = "前端用户登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
LoginVo loginVo = userService.login(loginDto);
return Result.success(loginVo);
}
@Operation(summary = "登录发送邮件验证码", description = "登录发送邮件验证码")
@PostMapping("public/sendLoginEmail")
public Result<String> sendLoginEmail(String email) {
if (!StringUtils.hasText(email)) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
userService.sendLoginEmail(email);
return Result.success(ResultCodeEnum.EMAIL_CODE_SEND_SUCCESS);
}
@Operation(summary = "刷新token", description = "刷新用户token")
@PostMapping("private/refreshToken")
public Result<RefreshTokenVo> refreshToken(@Valid @RequestBody RefreshTokenDto dto) {
RefreshTokenVo vo = userService.refreshToken(dto);
return Result.success(vo);
}
@Operation(summary = "获取本地登录用户信息", description = "获取用户信息从Redis中获取")
@GetMapping("private/userinfo")
public Result<LoginVo> userinfo() {
LoginVo vo = BaseContext.getLoginVo();
return Result.success(vo);
}
@Operation(summary = "退出登录", description = "退出登录")
@PostMapping("private/logout")
public Result<String> logout() {
userService.logout();
return Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
}
// -----------------------------------------
// 管理用户CURD
// -----------------------------------------
@Operation(summary = "分页查询", description = "分页查询用户信息", tags = "user::query")
@GetMapping("{page}/{limit}")
public Result<PageResult<AdminUserVo>> getUserPageByAdmin(
@ -89,21 +44,19 @@ public class UserController {
return Result.success(pageResult);
}
@Operation(summary = "添加", description = "添加用户信息", tags = "user::add")
@Operation(summary = "添加用户", description = "添加用户信息", tags = "user::add")
@PostMapping()
public Result<Object> addUserByAdmin(@Valid @RequestBody AdminUserAddDto dto) {
userService.addUserByAdmin(dto);
return Result.success(ResultCodeEnum.ADD_SUCCESS);
}
@Operation(summary = "更新", description = "更新用户信息需要更新Redis中的内容", tags = "user::update")
@Operation(summary = "更新用户", description = "更新用户信息需要更新Redis中的内容", tags = "user::update")
@PutMapping()
public Result<String> updateUserByAdmin(
@Valid AdminUserUpdateDto dto,
@RequestPart(value = "avatar", required = false) MultipartFile avatar) {
if (avatar != null) {
dto.setAvatar(avatar);
}
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);
}
@ -129,16 +82,13 @@ public class UserController {
return Result.success(voList);
}
@Operation(summary = "强制退出", description = "强制退出", tags = "user::update")
@Operation(summary = "强制退出用户", description = "强制退出", tags = "user::update")
@PutMapping("forcedOffline")
public Result<String> forcedOfflineByAdmin(@RequestBody Long id) {
userService.forcedOfflineByAdmin(id);
return Result.success();
}
// -----------------------------------------
// 普通用户
// -----------------------------------------
@Operation(summary = "更新本地用户信息", description = "更新本地用户信息需要更新Redis中的内容")
@PutMapping("private/update/userinfo")
public Result<String> updateAdminUserByLocalUser(@Valid @RequestBody AdminUserUpdateByLocalUserDto dto) {

View File

@ -0,0 +1,63 @@
package cn.bunny.services.controller.system;
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.LoginDto;
import cn.bunny.services.domain.system.system.dto.user.RefreshTokenDto;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.service.system.UserLoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@Tag(name = "用户登录", description = "用户登录相关接口")
@RestController
@RequestMapping("/api/user")
public class UserLoginController {
@Resource
private UserLoginService userLoginService;
@Operation(summary = "用户登录", description = "前端用户登录")
@PostMapping("login")
public Result<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
LoginVo loginVo = userLoginService.login(loginDto);
return Result.success(loginVo);
}
@Operation(summary = "登录发送邮件验证码", description = "登录发送邮件验证码")
@PostMapping("public/sendLoginEmail")
public Result<String> sendLoginEmail(String email) {
if (!StringUtils.hasText(email)) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
userLoginService.sendLoginEmail(email);
return Result.success(ResultCodeEnum.EMAIL_CODE_SEND_SUCCESS);
}
@Operation(summary = "刷新token", description = "刷新用户token")
@PostMapping("private/refreshToken")
public Result<RefreshTokenVo> refreshToken(@Valid @RequestBody RefreshTokenDto dto) {
RefreshTokenVo vo = userLoginService.refreshToken(dto);
return Result.success(vo);
}
@Operation(summary = "获取本地登录用户信息", description = "获取用户信息从Redis中获取")
@GetMapping("private/userinfo")
public Result<LoginVo> userinfo() {
LoginVo vo = BaseContext.getLoginVo();
return Result.success(vo);
}
@Operation(summary = "退出登录", description = "退出登录")
@PostMapping("private/logout")
public Result<String> logout() {
userLoginService.logout();
return Result.success(ResultCodeEnum.LOGOUT_SUCCESS);
}
}

View File

@ -0,0 +1,10 @@
public abstract class AbstractPermissionCheckHandler {
private AbstractPermissionCheckHandler abstractPermissionCheckHandler;
public AbstractPermissionCheckHandler(AbstractPermissionCheckHandler abstractPermissionCheckHandler) {
this.abstractPermissionCheckHandler = abstractPermissionCheckHandler;
}
abstract protected void checkPermission(String requestUrl);
}

View File

@ -8,20 +8,20 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@TestConfiguration
public class WebConfig {
@Value("${server.port}")
private String port;
@Autowired
private TokenUtilsTest tokenUtils;
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
String token = tokenUtils.getToken();
return builder.rootUri("http://localhost:" + port)
.defaultHeader("token", token)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
// @TestConfiguration
// public class WebConfig {
// @Value("${server.port}")
// private String port;
//
// @Autowired
// private TokenUtilsTest tokenUtils;
//
// @Bean
// public RestTemplate restTemplate(RestTemplateBuilder builder) {
// String token = tokenUtils.getToken();
// return builder.rootUri("http://localhost:" + port)
// .defaultHeader("token", token)
// .defaultHeader("Content-Type", "application/json")
// .build();
// }
// }

View File

@ -0,0 +1,58 @@
package cn.bunny.services.controller.system;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserControllerTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
void test() {
// Set<String> keys = redisTemplate.keys("admin::login_info::*");
// for (String key : keys) {
// System.out.println(key);
// }
Map<String, Object> adminLoginInfoWithScan = getAdminLoginInfoWithScan();
JSONObject adminLoginInfo = new JSONObject(adminLoginInfoWithScan);
System.out.println(adminLoginInfo);
}
public Map<String, Object> getAdminLoginInfoWithScan() {
String pattern = "admin::login_info::*";
Map<String, Object> result = new HashMap<>();
// 使用scan命令迭代查找
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(100).build();
Cursor<String> cursor = redisTemplate.scan(options);
while (cursor.hasNext()) {
String key = cursor.next();
Object value = redisTemplate.opsForValue().get(key);
result.put(key, value);
}
try {
cursor.close();
} catch (Exception e) {
// 处理异常
}
return result;
}
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.utils;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.utils.system.UserUtil;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -11,7 +11,7 @@ import org.springframework.stereotype.Component;
@Component
public class TokenUtilsTest {
@Autowired
private UserUtil userUtil;
private UserLoginHelper userUtil;
@Autowired
private UserMapper userMapper;

View File

@ -0,0 +1,33 @@
package system;
import cn.bunny.services.controller.system.UserController;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.service.system.impl.UserServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.web.WebAppConfiguration;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = UserServiceImpl.class)
class UserServiceTest {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Test
void test() {
String prefix = RedisUserConstant.getAdminUserEmailCodePrefix("");
Set<String> keys = redisTemplate.keys(prefix);
for (String key : keys) {
System.out.println(key);
}
}
}

View File

@ -6,4 +6,7 @@ import lombok.Data;
public class UserConstant {
public static final String USER_AVATAR = "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132";
public static final String PERSON_DESCRIPTION = "这个人很懒没有介绍...";
public static final String LOGIN = "login";
public static final String LOGOUT = "logout";
public static final String FORCE_LOGOUT = "force_logout";
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.model.dto.ip;
package cn.bunny.services.domain.common.ip;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.model.dto.quartz;
package cn.bunny.services.domain.common.quartz;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.domain.common.model.dto.security;
package cn.bunny.services.domain.common.security;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import io.swagger.v3.oas.annotations.media.Schema;

View File

@ -1,4 +1,4 @@
package cn.bunny.services.config.mail;
package cn.bunny.services.mail;
import cn.bunny.services.domain.common.model.dto.email.EmailSend;
import cn.bunny.services.domain.common.model.dto.email.EmailSendInit;

View File

@ -0,0 +1,111 @@
package cn.bunny.services.minio;
import cn.bunny.services.domain.common.constant.UserConstant;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@Component
public class MinioHelper {
@Resource
private MinioProperties properties;
/**
* 格式化用户头像URL
*
* <p>实现头像URL的统一存储和访问格式转换</p>
*
* <ol>
* <li><b>存储处理</b>将带HTTP前缀的头像URL转换为数据库存储格式
* <ul>
* <li>匹配正则表达式^https?://.*?/(.*)</li>
* <li>提取路径部分matcher.group(1)</li>
* <li>转换为存储格式"/" + 提取的路径</li>
* </ul>
* </li>
* <li><b>访问处理</b>返回带HTTP前缀的完整访问URL</li>
* </ol>
*
* <p>典型用例</p>
* <pre>
* 输入"http|s://example.com/images/avatar.jpg"
* 存储"images/avatar.jpg"
* 访问"http|s://xxx/images/avatar.jpg"
* </pre>
*
* @param avatar 头像URL可能包含HTTP前缀或数据库存储格式
* @return 格式化后的头像URL确保包含HTTP前缀
* @throws PatternSyntaxException 当正则表达式匹配失败时抛出
* @throws IllegalArgumentException 当头像参数为空时抛出
*/
public String formatUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (!matcher.matches()) return avatar;
// 匹配后返回内容
return "/" + matcher.group(1);
}
/**
* 检查并格式化用户头像URL
*
* <p>处理逻辑</p>
* <ol>
* <li>当头像为空或与默认头像相同时返回系统默认头像</li>
* <li>尝试移除头像URL中的HTTP协议和域名部分如果存在</li>
* <li>最终返回MinIO存储中的完整对象访问路径</li>
* </ol>
*
* @param avatar 用户头像URL可能为以下格式
* - /null使用默认头像
* - 完整HTTP URL"http|s://example.com/images/1.jpg"
* - MinIO对象路径"images/1.jpg"
* @return 格式化后的头像URL保证是可访问的完整路径
* - 默认头像当输入无效时
* - MinIO完整访问路径处理成功时
* @see UserConstant#USER_AVATAR 默认头像常量
* @see #getObjectNameFullPath MinIO路径处理方法
*/
public String getUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (matcher.matches()) return avatar;
// 匹配后返回内容
return getObjectNameFullPath(avatar);
}
/**
* 获取Minio全路径名Object带有桶名称
*
* @param objectName 对象名称
* @return 全路径
*/
public String getObjectNameFullPath(String objectName) {
String url = properties.getEndpointUrl();
return url + objectName;
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.config.minio;
package cn.bunny.services.minio;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;

View File

@ -1,14 +1,15 @@
package cn.bunny.services.config.minio;
package cn.bunny.services.minio;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.constant.MinioConstant;
import cn.bunny.services.domain.common.model.dto.file.MinioFilePath;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.exception.AuthCustomerException;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@ -23,17 +24,14 @@ import java.util.UUID;
* Minio操作工具类 简化操作步骤
* 自定义封装
*/
@Component
@Slf4j
public class MinioUtil {
private final MinioProperties properties;
@Service
public class MinioService {
@Resource
private MinioProperties properties;
private final MinioClient minioClient;
public MinioUtil(MinioProperties properties, MinioClient minioClient) {
this.properties = properties;
this.minioClient = minioClient;
}
@Resource
private MinioClient minioClient;
/**
* 获取Minio文件路径
@ -112,18 +110,6 @@ public class MinioUtil {
throw new AuthCustomerException(ResultCodeEnum.GET_BUCKET_EXCEPTION);
}
/**
* 获取Minio全路径名Object带有桶名称
*
* @param objectName 对象名称
* @return 全路径
*/
public String getObjectNameFullPath(String objectName) {
String url = properties.getEndpointUrl();
return url + objectName;
}
/**
* 上传文件
*
@ -211,4 +197,5 @@ public class MinioUtil {
throw new AuthCustomerException("创建失败");
}
}
}

View File

@ -1,6 +1,6 @@
package cn.bunny.services.utils;
import cn.bunny.services.domain.common.model.dto.ip.IpEntity;
import cn.bunny.services.domain.common.ip.IpEntity;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

View File

@ -1,7 +1,7 @@
package cn.bunny.services.security.service;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.dto.security.TokenInfo;
import cn.bunny.services.domain.common.security.TokenInfo;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.security.exception.CustomAuthenticationException;

View File

@ -1,7 +1,7 @@
package cn.bunny.services.security.service;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.model.dto.security.TokenInfo;
import cn.bunny.services.domain.common.security.TokenInfo;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.security.exception.CustomAuthenticationException;

View File

@ -1,5 +1,8 @@
package cn.bunny.services.service.message.impl;
import cn.bunny.services.context.BaseContext;
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.message.dto.MessageReceivedDto;
import cn.bunny.services.domain.system.message.dto.MessageReceivedUpdateDto;
import cn.bunny.services.domain.system.message.dto.MessageUserDto;
@ -7,13 +10,10 @@ import cn.bunny.services.domain.system.message.entity.Message;
import cn.bunny.services.domain.system.message.entity.MessageReceived;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithMessageVo;
import cn.bunny.services.domain.system.message.vo.MessageUserVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.message.MessageReceivedMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.message.MessageReceivedService;
import cn.bunny.services.utils.system.UserUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -39,7 +39,7 @@ import java.util.List;
public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMapper, MessageReceived> implements MessageReceivedService {
@Resource
private UserUtil userUtil;
private MinioHelper minioHelper;
/**
* 管理员管理用户消息接收分页查询
@ -58,7 +58,7 @@ public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMappe
// 设置封面返回内容
String cover = vo.getCover();
cover = userUtil.checkGetUserAvatar(cover);
cover = minioHelper.getUserAvatar(cover);
vo.setCover(cover);
return vo;
}).toList();
@ -113,7 +113,7 @@ public class MessageReceivedServiceImpl extends ServiceImpl<MessageReceivedMappe
// 设置封面返回内容
String cover = vo.getCover();
cover = userUtil.checkGetUserAvatar(cover);
cover = minioHelper.getUserAvatar(cover);
vo.setCover(cover);
return vo;
}).toList();

View File

@ -1,6 +1,9 @@
package cn.bunny.services.service.message.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.entity.BaseEntity;
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.message.dto.MessageAddDto;
import cn.bunny.services.domain.system.message.dto.MessageDto;
import cn.bunny.services.domain.system.message.dto.MessageUpdateDto;
@ -10,16 +13,13 @@ import cn.bunny.services.domain.system.message.vo.MessageDetailVo;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithMessageVo;
import cn.bunny.services.domain.system.message.vo.MessageReceivedWithUserVo;
import cn.bunny.services.domain.system.message.vo.MessageVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.message.MessageMapper;
import cn.bunny.services.mapper.message.MessageReceivedMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.service.message.MessageReceivedService;
import cn.bunny.services.service.message.MessageService;
import cn.bunny.services.utils.system.UserUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -47,17 +47,14 @@ import java.util.stream.Collectors;
@Transactional
public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> implements MessageService {
@Resource
private UserUtil userUtil;
@Resource
private MessageReceivedMapper messageReceivedMapper;
@Resource
private UserMapper userMapper;
@Resource
private MessageReceivedService messageReceivedService;
@Resource
private MinioHelper minioHelper;
/**
* 分页查询发送消息
@ -152,7 +149,7 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
// 设置封面返回内容
String cover = dto.getCover();
dto.setCover(userUtil.checkGetUserAvatar(cover));
dto.setCover(minioHelper.getUserAvatar(cover));
// 先保存消息数据之后拿到保存消息的id
Message message = new Message();
@ -198,7 +195,7 @@ public class MessageServiceImpl extends ServiceImpl<MessageMapper, Message> impl
// 设置封面返回内容
String cover = dto.getCover();
dto.setCover(userUtil.checkGetUserAvatar(cover));
dto.setCover(minioHelper.getUserAvatar(cover));
// 更新内容
Message message = new Message();

View File

@ -0,0 +1,42 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.common.model.vo.LoginVo;
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 org.jetbrains.annotations.NotNull;
public interface UserLoginService extends IService<AdminUser> {
/**
* 前台用户登录接口
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
LoginVo login(LoginDto loginDto);
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
void sendLoginEmail(@NotNull String email);
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
/**
* * 退出登录
*/
void logout();
}

View File

@ -1,16 +1,16 @@
package cn.bunny.services.service.system;
import cn.bunny.services.domain.system.system.dto.user.*;
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;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.PageResult;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.validation.Valid;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@ -24,14 +24,6 @@ import java.util.List;
*/
public interface UserService extends IService<AdminUser> {
/**
* 前台用户登录接口
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
LoginVo login(LoginDto loginDto);
/**
* * 获取用户信息列表
*
@ -60,27 +52,6 @@ public interface UserService extends IService<AdminUser> {
*/
void deleteUserByAdmin(List<Long> ids);
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
void sendLoginEmail(@NotNull String email);
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto);
/**
* * 退出登录
*/
void logout();
/**
* * 获取用户信息
*

View File

@ -0,0 +1,189 @@
package cn.bunny.services.service.system.helper;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
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.system.log.entity.UserLoginLog;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Permission;
import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import cn.bunny.services.utils.system.RoleUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@Transactional
public class UserLoginHelper {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private UserMapper userMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private PermissionMapper permissionMapper;
@Resource
private MinioHelper minioHelper;
/**
* 构建用户登录返回对象(LoginVo)
*
* <p>主要处理流程</p>
* <ol>
* <li><b>参数校验</b>检查用户对象是否为空</li>
* <li><b>权限处理</b>
* <ul>
* <li>查询用户角色和权限数据</li>
* <li>非管理员用户从数据库加载权限信息</li>
* <li>管理员用户通过RoleUtil.checkAdmin()自动设置管理员权限</li>
* <li>可选对角色和权限列表去重</li>
* </ul>
* </li>
* <li><b>信息装配</b>
* <ul>
* <li>记录用户IP等访问信息</li>
* <li>使用BeanUtils.copyProperties()复制用户基础属性</li>
* <li>设置记住我功能及token过期时间</li>
* </ul>
* </li>
* <li><b>缓存处理</b>将完整用户信息存入Redis</li>
* </ol>
*
* <p>注意事项</p>
* <ul>
* <li>属性复制操作放在流程最后确保所有字段正确同步</li>
* <li>IP信息需要同时更新到用户实体和返回对象</li>
* </ul>
*
* @param user 用户实体对象不可为空
* @param readMeDay 记住我时长单位
* @return 完整的登录响应对象
* @throws IllegalArgumentException 当用户对象为空时抛出
*/
public LoginVo buildLoginUserVo(AdminUser user, long readMeDay) {
String username = user.getUsername();
Long userId = user.getId();
// 用户角色
List<String> roles = new ArrayList<>(roleMapper.selectListByUserId(userId).stream().map(Role::getRoleCode).toList());
// 判断是否是 admin 如果是admin 赋予所有权限
List<String> permissions = new ArrayList<>();
boolean isAdmin = RoleUtil.checkAdmin(roles, permissions, user);
if (!isAdmin) {
permissions = permissionMapper.selectListByUserId(userId).stream()
.map(Permission::getPowerCode)
.toList();
}
// 为这两个去重
permissions = permissions.stream().distinct().toList();
roles = roles.stream().distinct().toList();
// 获取IP地址并更新用户登录信息
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 设置用户IP地址并更新用户信息
user.setIpAddress(ipAddr);
user.setIpRegion(ipRegion);
userMapper.updateById(user);
LoginVo loginVo = new LoginVo();
BeanUtils.copyProperties(user, loginVo);
loginVo.setPersonDescription(user.getSummary());
loginVo.setRoles(roles);
loginVo.setPermissions(permissions);
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
loginVo.setToken(token);
loginVo.setRefreshToken(token);
loginVo.setReadMeDay(readMeDay);
// 计算过期时间并格式化返回
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime plusDay = localDateTime.plusDays(readMeDay);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH);
String expires = plusDay.format(dateTimeFormatter);
loginVo.setExpires(expires);
// 设置用户头像
String userAvatar = minioHelper.getUserAvatar(user.getAvatar());
loginVo.setAvatar(userAvatar);
// 将用户登录保存在用户登录日志表中
setUserLoginLog(user, token, UserConstant.LOGIN);
// 将信息保存在Redis中一定要确保用户名是唯一的
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(username);
redisTemplate.opsForValue().set(loginInfoPrefix, loginVo, readMeDay, TimeUnit.DAYS);
return loginVo;
}
/**
* 设置用户登录日志内容
* <p>
* 该方法用于将管理员用户信息复制到用户登录日志对象中同时处理特殊字段映射关系
* <p>
* 实现说明
* 1. 使用BeanUtils.copyProperties()复制属性时会自动将AdminUser.id复制到UserLoginLog.id
* 2. 由于UserLoginLog实际需要的是userId字段而非id字段需要特殊处理
* - 先进行属性复制
* - 然后将UserLoginLog.userId设置为AdminUser.id
* - 最后将UserLoginLog.id显式设为null避免自动生成的id被覆盖
*
* @param user 管理员用户实体对象包含用户基本信息
* @param token 本次登录/退出的认证令牌
* @param type 操作类型LOGIN-登录/LOGOUT-退出
*/
public void setUserLoginLog(AdminUser user, String token, String type) {
UserLoginLog userLoginLog = new UserLoginLog();
BeanUtils.copyProperties(user, userLoginLog);
userLoginLog.setUserId(user.getId());
userLoginLog.setId(null);
userLoginLog.setToken(token);
userLoginLog.setType(type);
// 当前请求request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
// 获取User-Agent
String userAgent = request.getHeader("User-Agent");
userLoginLog.setUserAgent(userAgent);
// 获取X-Requested-With
String xRequestedWith = request.getHeader("X-Requested-With");
userLoginLog.setXRequestedWith(xRequestedWith);
}
userLoginLogMapper.insert(userLoginLog);
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
@ -31,4 +31,16 @@ public class DefaultLoginStrategy implements LoginStrategy {
queryWrapper.eq(AdminUser::getUsername, username);
return userMapper.selectOne(queryWrapper);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser 用户
*/
@Override
public void authenticateAfter(LoginDto loginDto, AdminUser adminUser) {
}
}

View File

@ -1,9 +1,9 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.mapper.system.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.data.redis.core.RedisTemplate;
@ -53,4 +53,18 @@ public class EmailLoginStrategy implements LoginStrategy {
queryWrapper.eq(AdminUser::getEmail, username);
return userMapper.selectOne(queryWrapper);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser 用户
*/
@Override
public void authenticateAfter(LoginDto loginDto, AdminUser adminUser) {
// 将Redis中验证码删除
String emailCodePrefix = RedisUserConstant.getAdminUserEmailCodePrefix(loginDto.getUsername());
redisTemplate.delete(emailCodePrefix);
}
}

View File

@ -1,9 +1,8 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Map;
@ -13,11 +12,9 @@ import java.util.Map;
public class LoginContext {
private final Map<String, LoginStrategy> strategies;
private final PasswordEncoder passwordEncoder;
public LoginContext(Map<String, LoginStrategy> strategies, PasswordEncoder passwordEncoder) {
public LoginContext(Map<String, LoginStrategy> strategies) {
this.strategies = strategies;
this.passwordEncoder = passwordEncoder;
}
/**
@ -37,4 +34,16 @@ public class LoginContext {
return strategy.authenticate(loginDto);
}
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
*/
public void loginAfter(LoginDto loginDto, AdminUser adminUser) {
String type = loginDto.getType();
LoginStrategy strategy = strategies.get(type);
strategy.authenticateAfter(loginDto, adminUser);
}
}

View File

@ -1,4 +1,4 @@
package cn.bunny.services.utils.login;
package cn.bunny.services.service.system.helper.login;
import cn.bunny.services.domain.system.system.dto.user.LoginDto;
@ -16,4 +16,12 @@ public interface LoginStrategy {
* @return 鉴定身份验证
*/
AdminUser authenticate(LoginDto loginDto);
/**
* 登录完成后的内容
*
* @param loginDto 登录参数
* @param adminUser
*/
void authenticateAfter(LoginDto loginDto, AdminUser adminUser);
}

View File

@ -1,11 +1,9 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.config.minio.MinioProperties;
import cn.bunny.services.config.minio.MinioUtil;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.model.dto.file.MinioFilePath;
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.common.model.dto.file.MinioFilePath;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
import cn.bunny.services.domain.system.files.dto.FilesAddDto;
import cn.bunny.services.domain.system.files.dto.FilesDto;
@ -15,6 +13,9 @@ import cn.bunny.services.domain.system.files.vo.FileInfoVo;
import cn.bunny.services.domain.system.files.vo.FilesVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.FilesMapper;
import cn.bunny.services.minio.MinioHelper;
import cn.bunny.services.minio.MinioProperties;
import cn.bunny.services.minio.MinioService;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.utils.FileUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -59,11 +60,15 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
private MinioProperties properties;
@Resource
private MinioUtil minioUtil;
private MinioService minioService;
@Resource
private MinioHelper minioHelper;
@Resource
private FilesMapper filesMapper;
/**
* * 系统文件表 服务实现类
*
@ -92,7 +97,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
public void addFiles(FilesAddDto dto) {
List<Files> list = dto.getFiles().stream().map(file -> {
try {
MinioFilePath minioFilePath = minioUtil.uploadObjectReturnFilePath(file, dto.getFilepath());
MinioFilePath minioFilePath = minioService.uploadObjectReturnFilePath(file, dto.getFilepath());
Files files = new Files();
files.setFileType(file.getContentType());
@ -124,7 +129,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
if (file != null) {
// 文件路径
String filePath = files.getFilepath().replace("/" + properties.getBucketName() + "/", "");
minioUtil.updateFile(properties.getBucketName(), filePath, file);
minioService.updateFile(properties.getBucketName(), filePath, file);
// 设置文件信息
files.setFileSize(file.getSize());
@ -160,7 +165,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
String filename = file.getOriginalFilename();
// 上传文件
MinioFilePath minioFIlePath = minioUtil.uploadObjectReturnFilePath(file, type);
MinioFilePath minioFIlePath = minioService.uploadObjectReturnFilePath(file, type);
String bucketNameFilepath = minioFIlePath.getBucketNameFilepath();
// 盘读研数据是否过大
@ -183,7 +188,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
.fileSize(fileSize)
.fileType(contentType)
.filename(filename)
.url(minioUtil.getObjectNameFullPath(bucketNameFilepath))
.url(minioHelper.getObjectNameFullPath(bucketNameFilepath))
.build();
}
@ -205,7 +210,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
}).toList();
// 删除目标文件
minioUtil.removeObjects(list);
minioService.removeObjects(list);
// 删除数据库内容
removeByIds(ids);
@ -229,7 +234,7 @@ public class FilesServiceImpl extends ServiceImpl<FilesMapper, Files> implements
String filepath = files.getFilepath();
int end = filepath.indexOf("/", 1);
filepath = filepath.substring(end + 1);
byte[] bytes = minioUtil.getBucketObjectByte(filepath);
byte[] bytes = minioService.getBucketObjectByte(filepath);
// 设置响应头
HttpHeaders headers = new HttpHeaders();

View File

@ -94,7 +94,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @return 所有角色列表
*/
@Override
@Cacheable(cacheNames = "role", key = "'allRole'", cacheManager = "cacheManagerWithMouth")
@Cacheable(cacheNames = "role", key = "'roleList'", cacheManager = "cacheManagerWithMouth")
public List<RoleVo> roleList() {
return list().stream().map(role -> {
RoleVo roleVo = new RoleVo();
@ -155,7 +155,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param file Excel文件
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void updateRoleByFile(MultipartFile file) {
if (file == null) {
throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);
@ -176,7 +176,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param dto 角色添加
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void addRole(@Valid RoleAddDto dto) {
Role role = new Role();
BeanUtils.copyProperties(dto, role);
@ -189,7 +189,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param dto 角色更新
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void updateRole(@Valid RoleUpdateDto dto) {
// 查询更新的角色是否存在
List<Role> roleList = list(Wrappers.<Role>lambdaQuery().eq(Role::getId, dto.getId()));
@ -213,7 +213,7 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
* @param ids 删除id列表
*/
@Override
@CacheEvict(cacheNames = "role", key = "'allRole'", beforeInvocation = true)
@CacheEvict(cacheNames = "role", key = "'roleList'", beforeInvocation = true)
public void deleteRole(List<Long> ids) {
// 判断数据请求是否为空
if (ids.isEmpty()) throw new AuthCustomerException(ResultCodeEnum.REQUEST_IS_EMPTY);

View File

@ -0,0 +1,184 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.UserConstant;
import cn.bunny.services.domain.common.enums.EmailTemplateEnums;
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.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 cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.system.UserLoginService;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import cn.bunny.services.service.system.helper.login.DefaultLoginStrategy;
import cn.bunny.services.service.system.helper.login.EmailLoginStrategy;
import cn.bunny.services.service.system.helper.login.LoginContext;
import cn.bunny.services.service.system.helper.login.LoginStrategy;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import cn.bunny.services.utils.email.ConcreteSenderEmailTemplate;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
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 org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
@Service
public class UserLoginServiceImpl extends ServiceImpl<UserMapper, AdminUser> implements UserLoginService {
@Resource
private UserLoginHelper userloginHelper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserMapper userMapper;
@Resource
private EmailTemplateMapper emailTemplateMapper;
@Resource
private ConcreteSenderEmailTemplate concreteSenderEmailTemplate;
/**
* 前台用户登录接口
* 这里不用判断用户是否为空因为在登录时已经校验过了
* <p>
* 抛出异常使用自带的 UsernameNotFoundException 或者自己封装<br/>
* 但是这两个效果传入参数都是一样的所以全部使用 UsernameNotFoundException
* </p>
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
@Override
public LoginVo login(LoginDto loginDto) {
// 初始化所有策略可扩展
HashMap<String, LoginStrategy> loginStrategyHashMap = new HashMap<>();
// 默认的登录方式
loginStrategyHashMap.put(LoginEnums.default_STRATEGY.getValue(), new DefaultLoginStrategy(userMapper));
// 注册邮箱
loginStrategyHashMap.put(LoginEnums.EMAIL_STRATEGY.getValue(), new EmailLoginStrategy(redisTemplate, userMapper));
// 使用登录上下文调用登录策略
LoginContext loginContext = new LoginContext(loginStrategyHashMap);
AdminUser user = loginContext.executeStrategy(loginDto);
// 验证登录逻辑
if (user == null) throw new UsernameNotFoundException(ResultCodeEnum.USER_IS_EMPTY.getMessage());
// 数据库密码
String dbPassword = user.getPassword();
String password = loginDto.getPassword();
if (!passwordEncoder.matches(password, dbPassword)) {
throw new UsernameNotFoundException(ResultCodeEnum.LOGIN_ERROR.getMessage());
}
// 判断用户是否禁用
if (user.getStatus()) {
throw new UsernameNotFoundException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED.getMessage());
}
// 登录结束后的操作
loginContext.loginAfter(loginDto, user);
user.setUpdateUser(user.getId());
user.setCreateUser(user.getId());
return userloginHelper.buildLoginUserVo(user, loginDto.getReadMeDay());
}
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
@Override
public void sendLoginEmail(@NotNull String email) {
// 查询验证码邮件模板
LambdaQueryWrapper<EmailTemplate> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(EmailTemplate::getIsDefault, true);
lambdaQueryWrapper.eq(EmailTemplate::getType, EmailTemplateEnums.VERIFICATION_CODE.getType());
EmailTemplate emailTemplate = emailTemplateMapper.selectOne(lambdaQueryWrapper);
// 生成验证码
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);
String emailCode = captcha.getCode();
// 需要替换模板内容
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("#title#", "BunnyAdmin");
hashMap.put("#verifyCode#", emailCode);
hashMap.put("#expires#", 15);
hashMap.put("#sendEmailUser#", emailTemplate.getEmailUser());
hashMap.put("#companyName#", "BunnyAdmin");
// 发送邮件
concreteSenderEmailTemplate.sendEmailTemplate(email, emailTemplate, hashMap);
// 在Redis中存储验证码
redisTemplate.opsForValue().set(RedisUserConstant.getAdminUserEmailCodePrefix(email), emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
}
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
@Override
public RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto) {
Long userId = JwtTokenUtil.getUserId(dto.getRefreshToken());
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 用户存在且没有禁用
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
if (adminUser.getStatus()) throw new AuthCustomerException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED);
LoginVo buildUserVo = userloginHelper.buildLoginUserVo(adminUser, dto.getReadMeDay());
RefreshTokenVo refreshTokenVo = new RefreshTokenVo();
BeanUtils.copyProperties(buildUserVo, refreshTokenVo);
return refreshTokenVo;
}
/**
* 退出登录
*/
@Override
public void logout() {
// 获取上下文对象中的用户ID和用户token
LoginVo loginVo = BaseContext.getLoginVo();
String token = loginVo.getToken();
Long userId = BaseContext.getUserId();
// 获取IP地址
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 查询用户信息
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
adminUser.setIpAddress(ipAddr);
adminUser.setIpRegion(ipRegion);
userloginHelper.setUserLoginLog(adminUser, token, UserConstant.LOGOUT);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
}

View File

@ -1,17 +1,17 @@
package cn.bunny.services.service.system.impl;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.domain.common.constant.RedisUserConstant;
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.system.dto.user.AssignRolesToUsersDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.UserRole;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.context.BaseContext;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.mapper.system.UserRoleMapper;
import cn.bunny.services.service.system.UserRoleService;
import cn.bunny.services.utils.system.UserUtil;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
@ -35,14 +35,11 @@ import java.util.concurrent.TimeUnit;
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> implements UserRoleService {
@Resource
private UserRoleMapper userRoleMapper;
private UserLoginHelper userloginHelper;
@Resource
private UserUtil userUtil;
private UserRoleMapper userRoleMapper;
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@ -94,7 +91,7 @@ public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> i
// 重新设置Redis中的用户存储信息vo对象
String username = adminUser.getUsername();
loginVo = userUtil.buildLoginUserVo(adminUser, readMeDay);
loginVo = userloginHelper.buildLoginUserVo(adminUser, readMeDay);
redisTemplate.opsForValue().set(RedisUserConstant.getAdminLoginInfoPrefix(username), loginVo, readMeDay, TimeUnit.DAYS);
}
}

View File

@ -1,40 +1,34 @@
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.enums.EmailTemplateEnums;
import cn.bunny.services.domain.common.enums.LoginEnums;
import cn.bunny.services.domain.common.model.vo.LoginVo;
import cn.bunny.services.domain.common.constant.UserConstant;
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.email.entity.EmailTemplate;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
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.*;
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;
import cn.bunny.services.domain.system.system.entity.UserDept;
import cn.bunny.services.domain.system.system.views.ViewUserDept;
import cn.bunny.services.domain.system.system.vo.user.AdminUserVo;
import cn.bunny.services.domain.system.system.vo.user.RefreshTokenVo;
import cn.bunny.services.domain.system.system.vo.user.UserVo;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.configuration.EmailTemplateMapper;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.RoleMapper;
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.service.system.FilesService;
import cn.bunny.services.service.system.UserService;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import cn.bunny.services.utils.email.ConcreteSenderEmailTemplate;
import cn.bunny.services.utils.login.DefaultLoginStrategy;
import cn.bunny.services.utils.login.EmailLoginStrategy;
import cn.bunny.services.utils.login.LoginContext;
import cn.bunny.services.utils.login.LoginStrategy;
import cn.bunny.services.utils.system.UserUtil;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -42,18 +36,15 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@ -67,150 +58,23 @@ import java.util.concurrent.TimeUnit;
public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implements UserService {
@Resource
private UserUtil userUtil;
private UserLoginHelper userloginHelper;
@Resource
private ConcreteSenderEmailTemplate concreteSenderEmailTemplate;
private PasswordEncoder passwordEncoder;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private FilesService filesService;
@Resource
private UserDeptMapper userDeptMapper;
@Resource
private UserRoleMapper userRoleMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private EmailTemplateMapper emailTemplateMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private UserMapper userMapper;
@Resource
private PasswordEncoder passwordEncoder;
/**
* 前台用户登录接口
* 这里不用判断用户是否为空因为在登录时已经校验过了
* <p>
* 抛出异常使用自带的 UsernameNotFoundException 或者自己封装<br/>
* 但是这两个效果传入参数都是一样的所以全部使用 UsernameNotFoundException
* </p>
*
* @param loginDto 登录参数
* @return 登录后结果返回
*/
@Override
public LoginVo login(LoginDto loginDto) {
Long readMeDay = loginDto.getReadMeDay();
// 初始化所有策略可扩展
HashMap<String, LoginStrategy> loginStrategyHashMap = new HashMap<>();
// 默认的登录方式
loginStrategyHashMap.put(LoginEnums.default_STRATEGY.getValue(), new DefaultLoginStrategy(userMapper));
// 注册邮箱
loginStrategyHashMap.put(LoginEnums.EMAIL_STRATEGY.getValue(), new EmailLoginStrategy(redisTemplate, userMapper));
// 使用登录上下文调用登录策略
LoginContext loginContext = new LoginContext(loginStrategyHashMap, passwordEncoder);
AdminUser user = loginContext.executeStrategy(loginDto);
// 验证登录逻辑
if (user == null) throw new UsernameNotFoundException(ResultCodeEnum.USER_IS_EMPTY.getMessage());
// 数据库密码
String dbPassword = user.getPassword();
// 用户登录密码
String password = loginDto.getPassword();
if (!passwordEncoder.matches(password, dbPassword)) {
throw new UsernameNotFoundException(ResultCodeEnum.LOGIN_ERROR.getMessage());
}
// 判断用户是否禁用
if (user.getStatus()) {
throw new UsernameNotFoundException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED.getMessage());
}
return userUtil.buildLoginUserVo(user, readMeDay);
}
/**
* 登录发送邮件验证码
*
* @param email 邮箱
*/
@Override
public void sendLoginEmail(@NotNull String email) {
// 查询验证码邮件模板
LambdaQueryWrapper<EmailTemplate> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(EmailTemplate::getIsDefault, true);
lambdaQueryWrapper.eq(EmailTemplate::getType, EmailTemplateEnums.VERIFICATION_CODE.getType());
EmailTemplate emailTemplate = emailTemplateMapper.selectOne(lambdaQueryWrapper);
// 生成验证码
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(150, 48, 4, 2);
String emailCode = captcha.getCode();
// 需要替换模板内容
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("#title#", "BunnyAdmin");
hashMap.put("#verifyCode#", emailCode);
hashMap.put("#expires#", 15);
hashMap.put("#sendEmailUser#", emailTemplate.getEmailUser());
hashMap.put("#companyName#", "BunnyAdmin");
// 发送邮件
concreteSenderEmailTemplate.sendEmailTemplate(email, emailTemplate, hashMap);
// 在Redis中存储验证码
redisTemplate.opsForValue().set(RedisUserConstant.getAdminUserEmailCodePrefix(email), emailCode, RedisUserConstant.REDIS_EXPIRATION_TIME, TimeUnit.MINUTES);
}
/**
* 刷新用户token
*
* @param dto 请求token
* @return 刷新token返回内容
*/
@NotNull
@Override
public RefreshTokenVo refreshToken(@NotNull RefreshTokenDto dto) {
Long userId = JwtTokenUtil.getUserId(dto.getRefreshToken());
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
// 用户存在且没有禁用
if (adminUser == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
if (adminUser.getStatus()) throw new AuthCustomerException(ResultCodeEnum.FAIL_NO_ACCESS_DENIED_USER_LOCKED);
LoginVo buildUserVo = userUtil.buildLoginUserVo(adminUser, dto.getReadMeDay());
RefreshTokenVo refreshTokenVo = new RefreshTokenVo();
BeanUtils.copyProperties(buildUserVo, refreshTokenVo);
return refreshTokenVo;
}
/**
* 退出登录
*/
@Override
public void logout() {
// 获取上下文对象中的用户ID和用户token
LoginVo loginVo = BaseContext.getLoginVo();
String token = loginVo.getToken();
Long userId = BaseContext.getUserId();
// 获取IP地址
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 查询用户信息
AdminUser adminUser = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
UserLoginLog userLoginLog = userUtil.setUserLoginLog(adminUser, token, ipAddr, ipRegion, "logout");
userLoginLogMapper.insert(userLoginLog);
// 删除Redis中用户信息
redisTemplate.delete(RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername()));
}
private MinioHelper minioHelper;
/**
* * 获取用户信息
@ -226,18 +90,19 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 用户是否存在
if (user == null) throw new AuthCustomerException(ResultCodeEnum.DATA_NOT_EXIST);
// 用户头像
String avatar = user.getAvatar();
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user, userVo);
userVo.setAvatar(userUtil.checkGetUserAvatar(avatar));
String userAvatar = minioHelper.getUserAvatar(avatar);
userVo.setAvatar(userAvatar);
return userVo;
}
/**
* * 强制退出
* 强制退出
*
* @param id 用户id
*/
@ -251,11 +116,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 将用户登录保存在用户登录日志表中
UserLoginLog userLoginLog = new UserLoginLog();
BeanUtils.copyProperties(adminUser, userLoginLog);
userLoginLog.setUserId(adminUser.getId());
userLoginLog.setIpAddress(adminUser.getIpAddress());
userLoginLog.setIpRegion(adminUser.getIpRegion());
userLoginLog.setToken(null);
userLoginLog.setType("forcedOffline");
userLoginLog.setType(UserConstant.FORCE_LOGOUT);
userLoginLogMapper.insert(userLoginLog);
// 删除Redis中用户信息
@ -304,8 +167,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
AdminUser user = getOne(Wrappers.<AdminUser>lambdaQuery().eq(AdminUser::getId, userId));
if (user == null) throw new AuthCustomerException(ResultCodeEnum.USER_IS_EMPTY);
// 检查用户头像
dto.setAvatar(userUtil.checkPostUserAvatar(dto.getAvatar()));
// 检查用户头像因为更新用户信息会带着用户之前的信息如果没有更新头像前端显示的http:xxx
String userAvatar = minioHelper.formatUserAvatar(dto.getAvatar());
dto.setAvatar(userAvatar);
// 更新用户
AdminUser adminUser = new AdminUser();
@ -315,7 +179,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 重新生成用户信息到Redis中
BeanUtils.copyProperties(dto, user);
userUtil.buildUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME);
}
/**
@ -363,7 +227,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
List<AdminUserVo> voList = page.getRecords().stream()
.map(adminUser -> {
// 如果存在用户头像则设置用户头像
String avatar = userUtil.checkGetUserAvatar(adminUser.getAvatar());
String avatar = minioHelper.getUserAvatar(adminUser.getAvatar());
AdminUserVo adminUserVo = new AdminUserVo();
BeanUtils.copyProperties(adminUser, adminUserVo);
@ -442,15 +306,18 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
}
// 更新头像
userUtil.uploadAvatarByAdmin(dto, adminUser);
uploadAvatarByAdmin(dto, adminUser);
// 构建用户返回信息同步到redis
userUtil.buildUserVo(adminUser, RedisUserConstant.REDIS_EXPIRATION_TIME);
userloginHelper.buildLoginUserVo(adminUser, RedisUserConstant.REDIS_EXPIRATION_TIME);
// 更新密码放在最后如果更新密码就将密码删除
userUtil.updateUserPasswordByAdmin(adminUser);
updateUserPasswordByAdmin(adminUser);
updateById(adminUser);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
/**
@ -471,11 +338,56 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, AdminUser> implemen
// 逻辑删除
removeByIds(ids);
// 删除用 也要删除对应的 角色和部门但是如果做的时物理删除就不需要因为数据库中设置了外键检查如果删除用户相关表也会删除
// 删除部门相关
userDeptMapper.deleteBatchIdsByUserIds(ids);
// 删除用户角色相关
userRoleMapper.deleteBatchIdsByUserIds(ids);
}
/**
* * 管理员修改管理员用户密码
*
* @param adminUser 管理员用户修改密码
*/
private void updateUserPasswordByAdmin(AdminUser adminUser) {
Long userId = adminUser.getId();
String password = adminUser.getPassword();
// 密码更新是否存在
if (!StringUtils.hasText(password)) return;
// 对密码加密
String encode = passwordEncoder.encode(password);
// 判断新密码是否与旧密码相同
if (adminUser.getPassword().equals(encode)) {
throw new AuthCustomerException(ResultCodeEnum.UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD);
}
// 更新用户密码
adminUser.setPassword(encode);
adminUser.setId(userId);
}
/**
* 管理员上传用户头像
*
* @param dto 管理员用户修改头像
*/
private void uploadAvatarByAdmin(AdminUserUpdateDto dto, AdminUser adminUser) {
MultipartFile avatar = dto.getAvatar();
Long userId = dto.getId();
// 上传头像是否存在
if (avatar == null) return;
// 上传头像
FileUploadDto uploadDto = FileUploadDto.builder().file(avatar).type(MinioConstant.avatar).build();
FileInfoVo fileInfoVo = filesService.upload(uploadDto);
// 更新用户
adminUser.setId(userId);
adminUser.setAvatar(fileInfoVo.getFilepath());
}
}

View File

@ -1,12 +1,12 @@
package cn.bunny.services.utils.email;
import cn.bunny.services.config.mail.MailSenderConfiguration;
import cn.bunny.services.domain.common.model.vo.result.ResultCodeEnum;
import cn.bunny.services.domain.common.model.dto.email.EmailSend;
import cn.bunny.services.domain.common.model.dto.email.EmailSendInit;
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.email.entity.EmailUsers;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mail.MailSenderConfiguration;
import cn.bunny.services.mapper.configuration.EmailUsersMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.mail.MessagingException;

View File

@ -5,6 +5,7 @@ import cn.bunny.services.domain.common.constant.RedisUserConstant;
import cn.bunny.services.domain.common.constant.SecurityConfigConstant;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.system.helper.UserLoginHelper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
@ -22,15 +23,30 @@ public class RoleUtil {
private UserMapper userMapper;
@Resource
private UserUtil userUtil;
private UserLoginHelper userloginHelper;
/**
* 判断是否是管理员
* 判断用户是否具有管理员权限
* <p>
* 管理员判定规则
* 1. 系统默认管理员用户ID为1的预设管理员账号用户名为Administrator
* 2. 角色授权管理员用户角色列表包含"admin"角色的账号
* <p>
* 权限控制说明
* - 管理员权限用于前端按钮级权限控制支持以下通配符格式
* - "*::*::*"全部模块的全部操作权限
* - "*::*" 指定模块的全部操作权限
* - "*" 基础通配权限
* - 若无细粒度按钮控制需求可不设置具体权限
* 详细查看
* <a href="https://pure-admin.cn/pages/RBAC/#%E7%AC%AC%E4%BA%8C%E7%A7%8D%E6%A8%A1%E5%BC%8F-%E7%99%BB%E5%BD%95%E6%8E%A5%E5%8F%A3%E8%BF%94%E5%9B%9E%E6%8C%89%E9%92%AE%E7%BA%A7%E5%88%AB%E6%9D%83%E9%99%90">
* 查看前端权限设置
* </a>
*
* @param roleList 角色代码列表
* @param permissions 权限列表
* @param adminUser 用户信息
* @return 是否是管理员
* @param roleList 用户角色编码列表可能包含"admin"角色
* @param permissions 用户权限列表用于前端按钮控制
* @param adminUser 用户实体对象需包含userId字段
* @return true-是管理员false-管理员
*/
public static boolean checkAdmin(List<String> roleList, List<String> permissions, AdminUser adminUser) {
// 判断是否是超级管理员
@ -87,6 +103,6 @@ public class RoleUtil {
Object object = redisTemplate.opsForValue().get(adminLoginInfoPrefix);
return object != null;
})
.forEach(user -> userUtil.buildUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME));
.forEach(user -> userloginHelper.buildLoginUserVo(user, RedisUserConstant.REDIS_EXPIRATION_TIME));
}
}

View File

@ -1,316 +0,0 @@
package cn.bunny.services.utils.system;
import cn.bunny.services.config.minio.MinioUtil;
import cn.bunny.services.domain.common.constant.LocalDateTimeConstant;
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.ResultCodeEnum;
import cn.bunny.services.domain.system.files.dto.FileUploadDto;
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.AdminUserUpdateDto;
import cn.bunny.services.domain.system.system.entity.AdminUser;
import cn.bunny.services.domain.system.system.entity.Permission;
import cn.bunny.services.domain.system.system.entity.Role;
import cn.bunny.services.exception.AuthCustomerException;
import cn.bunny.services.mapper.log.UserLoginLogMapper;
import cn.bunny.services.mapper.system.PermissionMapper;
import cn.bunny.services.mapper.system.RoleMapper;
import cn.bunny.services.mapper.system.UserMapper;
import cn.bunny.services.service.system.FilesService;
import cn.bunny.services.utils.IpUtil;
import cn.bunny.services.utils.JwtTokenUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class UserUtil {
@Resource
private MinioUtil minioUtil;
@Resource
private PermissionMapper permissionMapper;
@Resource
private RoleMapper roleMapper;
@Resource
private UserMapper userMapper;
@Resource
private UserLoginLogMapper userLoginLogMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private FilesService filesService;
/**
* 构建登录用户返回对象
*
* @param user 用户
* @param readMeDay 保存登录信息时间
* @return 登录信息
*/
@Transactional(rollbackFor = AuthCustomerException.class)
public LoginVo buildLoginUserVo(AdminUser user, long readMeDay) {
Long userId = user.getId();
String email = user.getEmail();
String username = user.getUsername();
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
// 获取IP地址并更新用户登录信息
String ipAddr = IpUtil.getCurrentUserIpAddress().getIpAddr();
String ipRegion = IpUtil.getCurrentUserIpAddress().getIpRegion();
// 设置用户IP地址并更新用户信息
AdminUser updateUser = new AdminUser();
updateUser.setId(userId);
updateUser.setIpAddress(ipAddr);
updateUser.setIpRegion(ipRegion);
userMapper.updateById(updateUser);
// 将用户登录保存在用户登录日志表中
userLoginLogMapper.insert(setUserLoginLog(user, token, ipAddr, ipRegion, "login"));
// 设置用户返回信息
LoginVo loginVo = setLoginVo(user, token, readMeDay, ipAddr, ipRegion);
// 将Redis中验证码删除
redisTemplate.delete(RedisUserConstant.getAdminUserEmailCodePrefix(email));
// 将信息保存在Redis中一定要确保用户名是唯一的
redisTemplate.opsForValue().set(RedisUserConstant.getAdminLoginInfoPrefix(username), loginVo, readMeDay, TimeUnit.DAYS);
return loginVo;
}
/**
* * 构建用户返回对象LoginVo
*
* @param user 用户对象
* @param readMeDay 记住我时间
*/
public void buildUserVo(AdminUser user, long readMeDay) {
Long userId = user.getId();
String username = user.getUsername();
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(username);
// 使用用户名创建token
String token = JwtTokenUtil.createToken(userId, username, (int) readMeDay);
// 设置用户返回信息
LoginVo loginVo = setLoginVo(user, token, readMeDay, user.getIpAddress(), user.getIpRegion());
// 将信息保存在Redis中一定要确保用户名是唯一的
redisTemplate.opsForValue().set(loginInfoPrefix, loginVo, readMeDay, TimeUnit.DAYS);
}
/**
* 设置更新用户设置内容
*
* @param user AdminUser
* @param token token
* @param readMeDay 记住我天数
* @param ipAddr IP地址
* @param ipRegion IP属地
* @return LoginVo
*/
public LoginVo setLoginVo(AdminUser user, String token, long readMeDay, String ipAddr, String ipRegion) {
Long userId = user.getId();
// 判断用户是否有头像如果没有头像设置默认头像并且用户头像不能和默认头像相同
String avatar = checkGetUserAvatar(user.getAvatar());
// 查找用户橘色
List<String> roles = new ArrayList<>(roleMapper.selectListByUserId(userId).stream().map(Role::getRoleCode).toList());
List<String> permissions = new ArrayList<>();
// 判断是否是 admin 如果是admin 赋予所有权限
boolean isAdmin = RoleUtil.checkAdmin(roles, permissions, user);
if (!isAdmin) {
permissions = permissionMapper.selectListByUserId(userId).stream().map(Permission::getPowerCode).toList();
}
// 计算过期时间并格式化返回
LocalDateTime localDateTime = LocalDateTime.now();
LocalDateTime plusDay = localDateTime.plusDays(readMeDay);
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(LocalDateTimeConstant.YYYY_MM_DD_HH_MM_SS_SLASH);
String expires = plusDay.format(dateTimeFormatter);
// 构建返回对象设置用户需要内容
LoginVo loginVo = new LoginVo();
BeanUtils.copyProperties(user, loginVo);
loginVo.setNickname(user.getNickname());
loginVo.setAvatar(avatar);
loginVo.setToken(token);
loginVo.setRefreshToken(token);
loginVo.setIpAddress(ipAddr);
loginVo.setIpRegion(ipRegion);
loginVo.setRoles(roles);
loginVo.setPermissions(permissions);
loginVo.setUpdateUser(userId);
loginVo.setPersonDescription(user.getSummary());
loginVo.setExpires(expires);
loginVo.setReadMeDay(readMeDay);
return loginVo;
}
/**
* 检查用户头像是否合规
*
* @param avatar 头像字符串
* @return 整理好的头像内容
*/
public String checkPostUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (!matcher.matches()) return avatar;
// 匹配后返回内容
return "/" + matcher.group(1);
}
/**
* 检查用户头像是否合规
*
* @param avatar 头像字符串
* @return 整理好的头像内容
*/
public String checkGetUserAvatar(String avatar) {
// 如果用户没有头像或者用户头像和默认头像相同返回默认头像
String userAvatar = UserConstant.USER_AVATAR;
if (!StringUtils.hasText(avatar) || avatar.equals(userAvatar)) return userAvatar;
// 替换前端发送的host前缀将其删除只保留路径名称
String regex = "^https?://.*?/(.*)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(avatar);
// 如果没有匹配
if (matcher.matches()) return avatar;
// 匹配后返回内容
return minioUtil.getObjectNameFullPath(avatar);
}
/**
* 设置用户登录日志内容
*
* @param user AdminUser
* @param token token
* @param ipAddr IP地址
* @param ipRegion IP属地
* @param type 类型登录/退出
* @return UserLoginLog
*/
public UserLoginLog setUserLoginLog(AdminUser user, String token, String ipAddr, String ipRegion, String type) {
Long userId = user.getId();
UserLoginLog userLoginLog = new UserLoginLog();
userLoginLog.setUsername(user.getUsername());
userLoginLog.setUserId(userId);
userLoginLog.setIpAddress(ipAddr);
userLoginLog.setIpRegion(ipRegion);
userLoginLog.setToken(token);
userLoginLog.setType(type);
userLoginLog.setCreateUser(userId);
userLoginLog.setUpdateUser(userId);
userLoginLog.setCreateTime(LocalDateTime.now());
userLoginLog.setUpdateTime(LocalDateTime.now());
// 当前请求request
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return userLoginLog;
HttpServletRequest request = requestAttributes.getRequest();
// 获取User-Agent
String userAgent = request.getHeader("User-Agent");
userLoginLog.setUserAgent(userAgent);
// 获取X-Requested-With
String xRequestedWith = request.getHeader("X-Requested-With");
userLoginLog.setXRequestedWith(xRequestedWith);
return userLoginLog;
}
/**
* * 管理员修改管理员用户密码
*
* @param adminUser 管理员用户修改密码
*/
public void updateUserPasswordByAdmin(AdminUser adminUser) {
Long userId = adminUser.getId();
String password = adminUser.getPassword();
// 密码更新是否存在
if (!StringUtils.hasText(password)) return;
// 对密码加密
String encode = passwordEncoder.encode(password);
// 判断新密码是否与旧密码相同
if (adminUser.getPassword().equals(encode))
throw new AuthCustomerException(ResultCodeEnum.UPDATE_NEW_PASSWORD_SAME_AS_OLD_PASSWORD);
// 更新用户密码
adminUser.setPassword(encode);
adminUser.setId(userId);
// 删除Redis中用户信息
String loginInfoPrefix = RedisUserConstant.getAdminLoginInfoPrefix(adminUser.getUsername());
redisTemplate.delete(loginInfoPrefix);
}
/**
* 管理员上传用户头像
*
* @param dto 管理员用户修改头像
*/
public void uploadAvatarByAdmin(AdminUserUpdateDto dto, AdminUser adminUser) {
MultipartFile avatar = dto.getAvatar();
Long userId = dto.getId();
// 上传头像是否存在
if (avatar == null) return;
// 上传头像
FileUploadDto uploadDto = FileUploadDto.builder().file(avatar).type(MinioConstant.avatar).build();
FileInfoVo fileInfoVo = filesService.upload(uploadDto);
// 更新用户
adminUser.setId(userId);
adminUser.setAvatar(fileInfoVo.getFilepath());
}
}