diff --git a/README.md b/README.md index e9f677b..e7c324f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # helio-boot ## 项目介绍 -`helio-boot`基于 Spring Boot 2.7.x,是一款预置SaaS、RBAC能力的单体项目脚手架,适合初学者学习 JavaWeb 开发的良好实践 +基于 Spring Boot 2.7.x,是一款预置SaaS、RBAC能力的单体项目脚手架,适合初学者学习 JavaWeb 开发的良好实践 +单Maven模块,定位为快速开发脚手架 +多模块版请移步至[helio-boot-modular](https://github.com/uncarbon97/helio-boot-modular)项目 -JDK compatibility: 1.8 - 17 +JDK compatibility: 1.8 - 21 【[前端演示站](https://helio-demo.uncarbon.cc/)】 【[官方文档](https://helio.uncarbon.cc/)】 @@ -16,24 +18,13 @@ JDK compatibility: 1.8 - 17 基础支撑构件 [helio-starters](https://github.com/uncarbon97/helio-starters) 已推送至Maven中央仓库,加载时会自动拉取 -## 配套后台管理前端模板 && 代码生成器 +## 配套后台管理前端模板 & 代码生成器 | 项目名 | 简介 | Gitee | GitHub | |----------------------|-----------------------------------------------------------------------------|------------------------------------------------------------|--------------------------------------------------------------| | helio-generator | 可一键生成单体or微服务版的前、后端代码,减少无谓的重复劳动 | [Gitee](https://gitee.com/uncarbon97/helio-generator) | [GitHub](https://github.com/uncarbon97/helio-generator) | | helio-admin-vue-vben | 基于[Vue vben admin](https://github.com/anncwb/vue-vben-admin) 改造适配的前端框架,开箱即用 | [Gitee](https://gitee.com/uncarbon97/helio-admin-vue-vben) | [GitHub](https://github.com/uncarbon97/helio-admin-vue-vben) | -## 代码分支 -| 分支名 | 简介 | -|------------|-------------------------------------------------------------------| -| single/1.x | 单Maven模块,定位为快速开发脚手架,适合初学者学习 JavaWeb 开发的良好实践 | -| multi/1.x | 按职责拆分为多Maven模块,依然是大单体,但命名及用途对标`helio-cloud`;适合多人协作、但不打算使用微服务架构的团队 | - -## 演示项目 -| 项目名 | 后端 | 前端 | 需导入数据库文件 | -|----------------|-----------------------------------------------------------------------|---------------------------------------------------------------------------------|------------------------------------------------------------------------------| -| library 图书管理系统 | [Gitee](https://gitee.com/uncarbon97/helio-boot/tree/demo%2Flibrary/) | [Gitee](https://gitee.com/uncarbon97/helio-admin-vue-vben/tree/demo%2Flibrary/) | attachments/db/MySQL/helio_boot.sql
attachments/db/demo/library_MySQL.sql | - -## 目录结构 +## 源码包结构 ``` ├─attachments 附件 │ └─db 数据库变更脚本 diff --git a/attachments/db/MySQL/upgrade/1.10.0_to_1.11.0.sql b/attachments/db/MySQL/upgrade/1.10.0_to_1.11.0.sql new file mode 100644 index 0000000..d970c56 --- /dev/null +++ b/attachments/db/MySQL/upgrade/1.10.0_to_1.11.0.sql @@ -0,0 +1,12 @@ +-- v1.11.0 - 「后台角色」中,`title`字段更名为「角色名」,`value`字段更名为「角色编码」 +ALTER TABLE sys_role + MODIFY COLUMN title varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名', + MODIFY COLUMN `value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色编码'; + +-- v1.11.0 - 「数据字典」中,`value`字段更名为「数据值」 +ALTER TABLE sys_data_dict + MODIFY COLUMN `value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '数据值'; + +-- v1.11.0 - 扩充「后台用户」数据表,「昵称」字段的长度上限为255 +ALTER TABLE sys_user + MODIFY COLUMN `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '昵称'; diff --git a/attachments/db/PostgreSQL/upgrade/1.10.0_to_1.11.0.sql b/attachments/db/PostgreSQL/upgrade/1.10.0_to_1.11.0.sql new file mode 100644 index 0000000..72a3f51 --- /dev/null +++ b/attachments/db/PostgreSQL/upgrade/1.10.0_to_1.11.0.sql @@ -0,0 +1,10 @@ +-- v1.11.0 - 「后台角色」中,`title`字段更名为「角色名」,`value`字段更名为「角色编码」 +COMMENT ON COLUMN "sys_role"."title" IS '角色名'; +COMMENT ON COLUMN "sys_role"."value" IS '角色编码'; + +-- v1.11.0 - 「数据字典」中,`value`字段更名为「数据值」 +COMMENT ON COLUMN "sys_data_dict"."value" IS '数据值'; + +-- v1.11.0 - 扩充「后台用户」数据表,「昵称」字段的长度上限为255 +ALTER TABLE "sys_user" + ALTER COLUMN "nickname" TYPE varchar(255); diff --git a/checkstyle-v1.xml b/checkstyle-v1.xml new file mode 100644 index 0000000..a605810 --- /dev/null +++ b/checkstyle-v1.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 3313574..a3e0d02 100644 --- a/pom.xml +++ b/pom.xml @@ -8,17 +8,18 @@ cc.uncarbon.framework helio-starters - 1.10.0 + 1.11.0 helio-boot + Helio 单模块版单体脚手架 - 1.10.0 + 1.11.0 - 2.0.0 + 2.1.0 8.4.3 diff --git a/src/main/java/cc/uncarbon/aspect/SysLogAspect.java b/src/main/java/cc/uncarbon/aspect/SysLogAspect.java index 0f3040a..a53c44f 100644 --- a/src/main/java/cc/uncarbon/aspect/SysLogAspect.java +++ b/src/main/java/cc/uncarbon/aspect/SysLogAspect.java @@ -97,13 +97,13 @@ public void returning(JoinPoint joinPoint, SysLog annotation, Object ret) { HttpServletRequest request = SpringMVCUtil.getRequest(); AspectContext aspectContext = new AspectContext(request); - if (!annotation.syncSave()) { + if (annotation.syncSave()) { + // 同步保存 + saveSysLog(joinPoint, annotation, aspectContext, null, ret); + } else { // 异步保存 saveSysLogAsync(joinPoint, annotation, aspectContext, null, ret); - return; } - - saveSysLog(joinPoint, annotation, aspectContext, null, ret); } /** @@ -119,13 +119,13 @@ public void throwing(JoinPoint joinPoint, SysLog annotation, Throwable e) { HttpServletRequest request = SpringMVCUtil.getRequest(); AspectContext aspectContext = new AspectContext(request); - if (!annotation.syncSave()) { + if (annotation.syncSave()) { + // 同步保存 + saveSysLog(joinPoint, annotation, aspectContext, e, null); + } else { // 异步保存 saveSysLogAsync(joinPoint, annotation, aspectContext, e, null); - return; } - - saveSysLog(joinPoint, annotation, aspectContext, e, null); } /** @@ -136,7 +136,8 @@ public void throwing(JoinPoint joinPoint, SysLog annotation, Throwable e) { * @param e 异常实例,可以为null * @param ret 返回值,可以为null */ - private void saveSysLog(final JoinPoint joinPoint, SysLog annotation, final AspectContext aspectContext, final Throwable e, Object ret) { + private void saveSysLog(final JoinPoint joinPoint, SysLog annotation, final AspectContext aspectContext, + final Throwable e, Object ret) { try { // 指定本线程用户态 UserContextHolder.setUserContext(aspectContext.getUserContext()); @@ -149,42 +150,8 @@ private void saveSysLog(final JoinPoint joinPoint, SysLog annotation, final Aspe extensionInstance = ReflectUtil.newInstance(extensionClazz); } - AdminInsertSysLogDTO dto = new AdminInsertSysLogDTO() - // 记录操作人 - .setUserId(UserContextHolder.getUserId()) - .setUsername(UserContextHolder.getUserName()) - // 记录请求方法 - .setMethod(CharSequenceUtil.builder( - joinPoint.getTarget().getClass().getName(), - "#", - joinPoint.getSignature().getName() - ).toString()) - // 记录操作内容 - .setOperation(annotation.value()) - .setIp(aspectContext.getClientIP()) - // 默认置为成功 - .setStatus(SysLogStatusEnum.SUCCESS) - .setUserAgent(aspectContext.getUserAgent()); - - // 记录请求参数 - Map afterMasked = new LinkedHashMap<>(32, 1); - String params = Arrays.stream(joinPoint.getArgs()).map( - item -> { - if (ClassUtil.isBasicType(item.getClass())) { - // 基元类型 OR 其包装类型,且拿不到参数名,保存在DB时保持原样 - return StrUtil.toStringOrNull(item); - } - - // 先去除敏感字段后再入库 - afterMasked.clear(); - BeanUtil.copyProperties(item, afterMasked, MASKING_COPY_OPTIONS); - return JSONUtil.toJsonStr(afterMasked, TO_JSON_STR_JSON_CONFIG); - } - ).collect(Collectors.joining(StrPool.LF)); - if (CharSequenceUtil.length(params) > MAX_STRING_SAVE_LENGTH) { - params = CharSequenceUtil.subPre(params, MAX_STRING_SAVE_LENGTH); - } - dto.setParams(params); + AdminInsertSysLogDTO dto = buildInsertDTO(joinPoint, annotation, aspectContext); + setParamInDTO(joinPoint, dto); if (e != null) { // 异常不为空,置状态为失败 @@ -221,16 +188,7 @@ private void saveSysLog(final JoinPoint joinPoint, SysLog annotation, final Aspe */ /** - * 异步保存系统日志 - */ - private void saveSysLogAsync(final JoinPoint joinPoint, SysLog annotation, final AspectContext aspectContext, final Throwable e, Object ret) { - taskExecutor.submit( - () -> this.saveSysLog(joinPoint, annotation, aspectContext, e, ret) - ); - } - - /** - * 本切面上下文 + * 本切面上下文,有点像对单次请求做个快照,用来缓解:servlet容器线程复用机制导致HttpServletRequest对象实例丢失的问题 * 内部使用 */ @Getter @@ -254,7 +212,7 @@ private static final class AspectContext { /** * 从 HTTP Request 新建 */ - public AspectContext(HttpServletRequest request) { + AspectContext(HttpServletRequest request) { this.userContext = UserContextHolder.getUserContext(); /* 记录IP地址 @@ -271,4 +229,60 @@ public AspectContext(HttpServletRequest request) { } } + /** + * 异步保存系统日志 + */ + private void saveSysLogAsync(final JoinPoint joinPoint, SysLog annotation, final AspectContext aspectContext, + final Throwable e, Object ret) { + taskExecutor.submit( + () -> this.saveSysLog(joinPoint, annotation, aspectContext, e, ret) + ); + } + + /** + * 构造新增DTO + */ + private static AdminInsertSysLogDTO buildInsertDTO(JoinPoint joinPoint, SysLog annotation, AspectContext aspectContext) { + return new AdminInsertSysLogDTO() + // 记录操作人 + .setUserId(UserContextHolder.getUserId()) + .setUsername(UserContextHolder.getUserName()) + // 记录请求方法 + .setMethod(CharSequenceUtil.builder( + joinPoint.getTarget().getClass().getName(), + "#", + joinPoint.getSignature().getName() + ).toString()) + // 记录操作内容 + .setOperation(annotation.value()) + .setIp(aspectContext.getClientIP()) + // 默认置为成功 + .setStatus(SysLogStatusEnum.SUCCESS) + .setUserAgent(aspectContext.getUserAgent()); + } + + /** + * 记录请求参数,设置dto的param字段 + */ + private static void setParamInDTO(JoinPoint joinPoint, AdminInsertSysLogDTO dto) { + Map afterMasked = new LinkedHashMap<>(32, 1); + String params = Arrays.stream(joinPoint.getArgs()).map( + item -> { + if (ClassUtil.isBasicType(item.getClass())) { + // 基元类型 OR 其包装类型,且拿不到参数名,保存在DB时保持原样 + return StrUtil.toStringOrNull(item); + } + + // 先去除敏感字段后再入库 + afterMasked.clear(); + BeanUtil.copyProperties(item, afterMasked, MASKING_COPY_OPTIONS); + return JSONUtil.toJsonStr(afterMasked, TO_JSON_STR_JSON_CONFIG); + } + ).collect(Collectors.joining(StrPool.LF)); + if (CharSequenceUtil.length(params) > MAX_STRING_SAVE_LENGTH) { + params = CharSequenceUtil.subPre(params, MAX_STRING_SAVE_LENGTH); + } + dto.setParams(params); + } + } diff --git a/src/main/java/cc/uncarbon/config/AsyncConfiguration.java b/src/main/java/cc/uncarbon/config/AsyncConfiguration.java index b8bdc0a..3dae183 100644 --- a/src/main/java/cc/uncarbon/config/AsyncConfiguration.java +++ b/src/main/java/cc/uncarbon/config/AsyncConfiguration.java @@ -36,14 +36,16 @@ public class AsyncConfiguration implements AsyncConfigurer { public ThreadPoolTaskExecutor taskExecutor() { final String threadNamePrefix = "taskExecutor-"; - log.debug("[异步任务线程池] 创建默认线程池【taskExecutor】,该线程池参数可通过 spring.task.execution 调节 >> " + - "corePoolSize核心线程池大小={}, maxPoolSize最大线程数={}, queueCapacity队列容量={}" + - ", rejectedExecutionHandler拒绝策略={}", - taskExecutionProperties.getPool().getCoreSize(), - taskExecutionProperties.getPool().getMaxSize(), - taskExecutionProperties.getPool().getQueueCapacity(), - "CallerRunsPolicy" - ); + if (log.isDebugEnabled()) { + log.debug("[异步任务线程池] 创建默认线程池【taskExecutor】,该线程池参数可通过 spring.task.execution 调节 >> " + + "corePoolSize核心线程池大小={}, maxPoolSize最大线程数={}, queueCapacity队列容量={}" + + ", rejectedExecutionHandler拒绝策略={}", + taskExecutionProperties.getPool().getCoreSize(), + taskExecutionProperties.getPool().getMaxSize(), + taskExecutionProperties.getPool().getQueueCapacity(), + "CallerRunsPolicy" + ); + } ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 核心线程池大小,默认 8 diff --git a/src/main/java/cc/uncarbon/helper/CaptchaHelper.java b/src/main/java/cc/uncarbon/helper/CaptchaHelper.java index 08c6411..ffd046a 100644 --- a/src/main/java/cc/uncarbon/helper/CaptchaHelper.java +++ b/src/main/java/cc/uncarbon/helper/CaptchaHelper.java @@ -1,12 +1,19 @@ package cc.uncarbon.helper; +import cc.uncarbon.framework.core.exception.BusinessException; +import cc.uncarbon.module.adminapi.enums.AdminApiErrorEnum; +import cc.uncarbon.module.adminapi.model.interior.AdminCaptchaContainer; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.ShearCaptcha; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.UUID; import cn.hutool.core.text.CharSequenceUtil; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; /** @@ -22,43 +29,66 @@ public class CaptchaHelper { private static final String CACHE_KEY_CAPTCHA_ANSWER = "Authorization:captcha:uuid_%s"; + /** + * 验证码答案长度 + */ + private static final int CAPTCHA_ANSWER_LENGTH = 4; /** - * 生成一个验证码图片对象 - * - * @param uuid UUID - * @return ShearCaptcha + * 生成一个验证码 */ - public ShearCaptcha generate(String uuid) { + public AdminCaptchaContainer generate() { + // redis预占位;随机10个UUID,应该有个能成的吧…… + UUID uuid = UUID.randomUUID(); + String captchaCacheKey = null; + Boolean stubFlag = Boolean.FALSE; + for (int count = 0; count < 10; count++) { + captchaCacheKey = String.format(CACHE_KEY_CAPTCHA_ANSWER, uuid.toString(true)); + stubFlag = stringRedisTemplate.opsForValue().setIfAbsent(captchaCacheKey, CharSequenceUtil.EMPTY); + if (Boolean.TRUE.equals(stubFlag)) { + break; + } + uuid = UUID.randomUUID(); + } + if (!Boolean.TRUE.equals(stubFlag)) { + throw new BusinessException(AdminApiErrorEnum.CAPTCHA_GENERATE_FAILED); + } + // 定义图形验证码的长、宽、验证码字符数、干扰线宽度 - ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(196, 50, 4, 4); + ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(196, 50, CAPTCHA_ANSWER_LENGTH, 4); - // 将验证码答案保存至 redis , 有效期5分钟 - stringRedisTemplate.opsForValue().set(String.format(CACHE_KEY_CAPTCHA_ANSWER, uuid), captcha.getCode(), 300, TimeUnit.SECONDS); + // 将验证码答案保存至 redis, 有效期5分钟 + stringRedisTemplate.opsForValue().set(captchaCacheKey, captcha.getCode(), 300, TimeUnit.SECONDS); + LocalDateTime expiredAt = LocalDateTimeUtil.offset(LocalDateTimeUtil.now(), 300, ChronoUnit.SECONDS); - return captcha; + return new AdminCaptchaContainer(captcha, uuid.toString(true), expiredAt); } /** - * 校验验证码是否输入正确 + * 核验验证码是否输入正确 * - * @param uuid UUID - * @param captchaAnswer 验证码答案 - * @param removeWhenEquals 匹配时自动移除缓存键 + * @param uuid 验证码唯一标识(UUID) + * @param captchaAnswer 验证码答案 * @return 是否正确 */ - public boolean validate(String uuid, String captchaAnswer, boolean removeWhenEquals) { + public boolean validate(String uuid, String captchaAnswer) { if (CharSequenceUtil.hasBlank(uuid, captchaAnswer)) { return false; } String cacheKey = String.format(CACHE_KEY_CAPTCHA_ANSWER, uuid); - String answerInRedis = stringRedisTemplate.opsForValue().get(cacheKey); - boolean equals = CharSequenceUtil.equalsIgnoreCase(answerInRedis, captchaAnswer); - if (equals && removeWhenEquals) { + boolean equals; + try { + if (CharSequenceUtil.length(captchaAnswer) != CAPTCHA_ANSWER_LENGTH) { + // 长度不同 + return false; + } + + String answerInRedis = stringRedisTemplate.opsForValue().get(cacheKey); + equals = CharSequenceUtil.equalsIgnoreCase(answerInRedis, captchaAnswer); + } finally { stringRedisTemplate.delete(cacheKey); } - return equals; } } diff --git a/src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java b/src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java index 15347f3..66badde 100644 --- a/src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java +++ b/src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java @@ -35,8 +35,9 @@ public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServl // SA-Token 会自动从请求头中解析 token,所以这里可以直接拿到对应 session,从而取出业务字段 if (AdminStpUtil.isLogin()) { setContextsFromSaSession(AdminStpUtil.getSession(), request); - - log.debug("[SA-Token][Admin] 从请求头解析出用户上下文 >> {}", UserContextHolder.getUserContext()); + if (log.isDebugEnabled()) { + log.debug("[SA-Token][Admin] 从请求头解析出用户上下文 >> {}", UserContextHolder.getUserContext()); + } } else { UserContextHolder.clear(); TenantContextHolder.clear(); diff --git a/src/main/java/cc/uncarbon/interceptor/DefaultSaTokenParseInterceptor.java b/src/main/java/cc/uncarbon/interceptor/DefaultSaTokenParseInterceptor.java index e3a4ac7..a521fed 100644 --- a/src/main/java/cc/uncarbon/interceptor/DefaultSaTokenParseInterceptor.java +++ b/src/main/java/cc/uncarbon/interceptor/DefaultSaTokenParseInterceptor.java @@ -32,7 +32,9 @@ public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServl // SA-Token 会自动从请求头中解析 token,所以这里可以直接拿到对应 session,从而取出业务字段 if (StpUtil.isLogin()) { setContextsFromSaSession(StpUtil.getSession(), request); - log.debug("[SA-Token] 从请求头解析出用户上下文 >> {}", UserContextHolder.getUserContext()); + if (log.isDebugEnabled()) { + log.debug("[SA-Token] 从请求头解析出用户上下文 >> {}", UserContextHolder.getUserContext()); + } } else { UserContextHolder.clear(); TenantContextHolder.clear(); diff --git a/src/main/java/cc/uncarbon/module/adminapi/constant/AdminApiConstant.java b/src/main/java/cc/uncarbon/module/adminapi/constant/AdminApiConstant.java index 3b233bb..980204a 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/constant/AdminApiConstant.java +++ b/src/main/java/cc/uncarbon/module/adminapi/constant/AdminApiConstant.java @@ -4,7 +4,7 @@ /** * 后台管理接口常量 */ -public class AdminApiConstant { +public final class AdminApiConstant { private AdminApiConstant() { } diff --git a/src/main/java/cc/uncarbon/module/adminapi/enums/AdminApiErrorEnum.java b/src/main/java/cc/uncarbon/module/adminapi/enums/AdminApiErrorEnum.java new file mode 100644 index 0000000..6624820 --- /dev/null +++ b/src/main/java/cc/uncarbon/module/adminapi/enums/AdminApiErrorEnum.java @@ -0,0 +1,23 @@ +package cc.uncarbon.module.adminapi.enums; + +import cc.uncarbon.framework.core.enums.HelioBaseEnum; +import lombok.AllArgsConstructor; +import lombok.Getter; + + +/** + * admin-api模块错误枚举类 + */ +@AllArgsConstructor +@Getter +public enum AdminApiErrorEnum implements HelioBaseEnum { + + CAPTCHA_GENERATE_FAILED(500, "验证码生成失败,请稍后再试"), + + CAPTCHA_VALIDATE_FAILED(400, "验证码不正确,请重新输入"), + + ; + private final Integer value; + private final String label; + +} diff --git a/src/main/java/cc/uncarbon/module/adminapi/event/KickOutSysUsersEvent.java b/src/main/java/cc/uncarbon/module/adminapi/event/KickOutSysUsersEvent.java new file mode 100644 index 0000000..f256442 --- /dev/null +++ b/src/main/java/cc/uncarbon/module/adminapi/event/KickOutSysUsersEvent.java @@ -0,0 +1,32 @@ +package cc.uncarbon.module.adminapi.event; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEvent; + +import java.util.Collection; + +/** + * 强制登出后台用户事件 + */ +@Getter +public final class KickOutSysUsersEvent extends ApplicationEvent { + + private final transient EventData data; + + public KickOutSysUsersEvent(EventData data) { + super(data); + this.data = data; + } + + @Getter + @RequiredArgsConstructor + public static final class EventData { + + /** + * 需要被强制登出的后台用户IDs + */ + private final Collection sysUserIds; + + } +} diff --git a/src/main/java/cc/uncarbon/module/adminapi/listener/AdminApiEventListener.java b/src/main/java/cc/uncarbon/module/adminapi/listener/AdminApiEventListener.java new file mode 100644 index 0000000..83c9412 --- /dev/null +++ b/src/main/java/cc/uncarbon/module/adminapi/listener/AdminApiEventListener.java @@ -0,0 +1,31 @@ +package cc.uncarbon.module.adminapi.listener; + +import cc.uncarbon.module.adminapi.event.KickOutSysUsersEvent; +import cc.uncarbon.module.adminapi.util.AdminStpUtil; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * admin-api模块事件监听器 + */ +@Component +@RequiredArgsConstructor +public class AdminApiEventListener { + + private final ThreadPoolTaskExecutor taskExecutor; + + + @EventListener(value = KickOutSysUsersEvent.class) + public void handleKickOutSysUsersEvent(KickOutSysUsersEvent event) { + Collection sysUserIds = event.getData().getSysUserIds(); + if (CollUtil.isNotEmpty(sysUserIds)) { + // 异步强制登出;同一时间大量登出,会操作大量Redis键,可能存在缓存雪崩的风险 + taskExecutor.submit(() -> sysUserIds.forEach(AdminStpUtil::kickout)); + } + } +} diff --git a/src/main/java/cc/uncarbon/module/adminapi/model/interior/AdminCaptchaContainer.java b/src/main/java/cc/uncarbon/module/adminapi/model/interior/AdminCaptchaContainer.java new file mode 100644 index 0000000..335923c --- /dev/null +++ b/src/main/java/cc/uncarbon/module/adminapi/model/interior/AdminCaptchaContainer.java @@ -0,0 +1,31 @@ +package cc.uncarbon.module.adminapi.model.interior; + +import cn.hutool.captcha.AbstractCaptcha; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 后台管理-验证码容器 + */ +@RequiredArgsConstructor +@Getter +public class AdminCaptchaContainer { + + /** + * 验证码图片对象 + */ + private final AbstractCaptcha image; + + /** + * 验证码唯一标识(UUID) + */ + private final String uuid; + + /** + * 验证码失效时刻 + */ + private final LocalDateTime expiredAt; + +} diff --git a/src/main/java/cc/uncarbon/module/adminapi/model/response/AdminCaptchaVO.java b/src/main/java/cc/uncarbon/module/adminapi/model/response/AdminCaptchaVO.java new file mode 100644 index 0000000..322757d --- /dev/null +++ b/src/main/java/cc/uncarbon/module/adminapi/model/response/AdminCaptchaVO.java @@ -0,0 +1,30 @@ +package cc.uncarbon.module.adminapi.model.response; + +import cc.uncarbon.module.adminapi.model.interior.AdminCaptchaContainer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; + +import java.time.LocalDateTime; + +/** + * 后台管理-验证码 VO + */ +@Getter +public class AdminCaptchaVO { + + @ApiModelProperty(value = "验证码图片Base64") + private final String captchaImage; + + @ApiModelProperty(value = "验证码唯一标识") + private final String captchaId; + + @ApiModelProperty(value = "验证码失效时刻") + private final LocalDateTime expiredAt; + + + public AdminCaptchaVO(AdminCaptchaContainer source) { + this.captchaImage = source.getImage().getImageBase64Data(); + this.captchaId = source.getUuid(); + this.expiredAt = source.getExpiredAt(); + } +} diff --git a/src/main/java/cc/uncarbon/module/adminapi/model/response/SelectOptionItemVO.java b/src/main/java/cc/uncarbon/module/adminapi/model/response/SelectOptionItemVO.java index be40ad5..9169cdd 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/model/response/SelectOptionItemVO.java +++ b/src/main/java/cc/uncarbon/module/adminapi/model/response/SelectOptionItemVO.java @@ -62,15 +62,13 @@ public SelectOptionItemVO(HelioBaseEnum helioBaseEnum) { /* ---------------------------------------------------------------- - 其他业务字段都写在这里 + 自定义业务字段都写在这里 都要标记释义、用处、新增时版本号 免得每个人各取一个名,不统一 ---------------------------------------------------------------- */ - @ApiModelProperty(value = "(演示字段;可删除)文本字段1") - @Setter - private String demoString1; + /* ---------------------------------------------------------------- diff --git a/src/main/java/cc/uncarbon/module/adminapi/util/AdminStpUtil.java b/src/main/java/cc/uncarbon/module/adminapi/util/AdminStpUtil.java index 201b19f..9c95418 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/util/AdminStpUtil.java +++ b/src/main/java/cc/uncarbon/module/adminapi/util/AdminStpUtil.java @@ -14,13 +14,14 @@ /** * 后台管理专用 StpUtil * 文档见 SA-Token -> 多账户认证:https://sa-token.cc/doc.html#/up/many-account - * @see cn.dev33.satoken.stp.StpUtil * * @author Uncarbon + * @see cn.dev33.satoken.stp.StpUtil */ -public class AdminStpUtil { +public final class AdminStpUtil { - private AdminStpUtil() {} + private AdminStpUtil() { + } /** * 多账号体系下的类型标识 @@ -37,7 +38,7 @@ private AdminStpUtil() {} * * @return / */ - public static String getLoginType(){ + public static String getLoginType() { return stpLogic.getLoginType(); } @@ -88,17 +89,17 @@ public static String getTokenName() { * * @param tokenValue token 值 */ - public static void setTokenValue(String tokenValue){ + public static void setTokenValue(String tokenValue) { stpLogic.setTokenValue(tokenValue); } /** * 在当前会话写入指定 token 值 * - * @param tokenValue token 值 + * @param tokenValue token 值 * @param cookieTimeout Cookie存活时间(秒) */ - public static void setTokenValue(String tokenValue, int cookieTimeout){ + public static void setTokenValue(String tokenValue, int cookieTimeout) { stpLogic.setTokenValue(tokenValue, cookieTimeout); } @@ -108,7 +109,7 @@ public static void setTokenValue(String tokenValue, int cookieTimeout){ * @param tokenValue token 值 * @param loginModel 登录参数 */ - public static void setTokenValue(String tokenValue, SaLoginModel loginModel){ + public static void setTokenValue(String tokenValue, SaLoginModel loginModel) { stpLogic.setTokenValue(tokenValue, loginModel); } @@ -126,7 +127,7 @@ public static String getTokenValue() { * * @return / */ - public static String getTokenValueNotCut(){ + public static String getTokenValueNotCut() { return stpLogic.getTokenValueNotCut(); } @@ -156,7 +157,7 @@ public static void login(Object id) { /** * 会话登录,并指定登录设备类型 * - * @param id 账号id,建议的类型:(long | int | String) + * @param id 账号id,建议的类型:(long | int | String) * @param device 设备类型 */ public static void login(Object id, String device) { @@ -166,7 +167,7 @@ public static void login(Object id, String device) { /** * 会话登录,并指定是否 [记住我] * - * @param id 账号id,建议的类型:(long | int | String) + * @param id 账号id,建议的类型:(long | int | String) * @param isLastingCookie 是否为持久Cookie,值为 true 时记住我,值为 false 时关闭浏览器需要重新登录 */ public static void login(Object id, boolean isLastingCookie) { @@ -186,7 +187,7 @@ public static void login(Object id, long timeout) { /** * 会话登录,并指定所有登录参数 Model * - * @param id 账号id,建议的类型:(long | int | String) + * @param id 账号id,建议的类型:(long | int | String) * @param loginModel 此次登录的参数Model */ public static void login(Object id, SaLoginModel loginModel) { @@ -206,7 +207,7 @@ public static String createLoginSession(Object id) { /** * 创建指定账号 id 的登录会话数据 * - * @param id 账号id,建议的类型:(long | int | String) + * @param id 账号id,建议的类型:(long | int | String) * @param loginModel 此次登录的参数Model * @return 返回会话令牌 */ @@ -236,7 +237,7 @@ public static void logout(Object loginId) { * 会话注销,根据账号id 和 设备类型 * * @param loginId 账号id - * @param device 设备类型 (填 null 代表注销该账号的所有设备类型) + * @param device 设备类型 (填 null 代表注销该账号的所有设备类型) */ public static void logout(Object loginId, String device) { stpLogic.logout(loginId, device); @@ -266,7 +267,7 @@ public static void kickout(Object loginId) { *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-5

* * @param loginId 账号id - * @param device 设备类型 (填 null 代表踢出该账号的所有设备类型) + * @param device 设备类型 (填 null 代表踢出该账号的所有设备类型) */ public static void kickout(Object loginId, String device) { stpLogic.kickout(loginId, device); @@ -287,7 +288,7 @@ public static void kickoutByTokenValue(String tokenValue) { *

当对方再次访问系统时,会抛出 NotLoginException 异常,场景值=-4

* * @param loginId 账号id - * @param device 设备类型 (填 null 代表顶替该账号的所有设备类型) + * @param device 设备类型 (填 null 代表顶替该账号的所有设备类型) */ public static void replaced(Object loginId, String device) { stpLogic.replaced(loginId, device); @@ -332,7 +333,7 @@ public static Object getLoginId() { /** * 获取当前会话账号id, 如果未登录,则返回默认值 * - * @param 返回类型 + * @param 返回类型 * @param defaultValue 默认值 * @return 登录id */ @@ -400,7 +401,7 @@ public static Object getExtra(String key) { * 获取指定 Token 的扩展信息(此函数只在jwt模式下生效) * * @param tokenValue 指定的 Token 值 - * @param key 键值 + * @param key 键值 * @return 对应的扩展数据 */ public static Object getExtra(String tokenValue, String key) { @@ -413,7 +414,7 @@ public static Object getExtra(String tokenValue, String key) { /** * 获取指定账号 id 的 Account-Session, 如果该 SaSession 尚未创建,isCreate=是否新建并返回 * - * @param loginId 账号id + * @param loginId 账号id * @param isCreate 是否新建 * @return SaSession 对象 */ @@ -497,8 +498,8 @@ public static SaSession getAnonTokenSession() { /** * 续签当前 token:(将 [最后操作时间] 更新为当前时间戳) *

- * 请注意: 即使 token 已被冻结 也可续签成功, - * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 + * 请注意: 即使 token 已被冻结 也可续签成功, + * 如果此场景下需要提示续签失败,可在此之前调用 checkActiveTimeout() 强制检查是否冻结即可 *

*/ public static void updateLastActiveToNow() { @@ -574,7 +575,7 @@ public static void renewTimeout(long timeout) { * 对指定 token 的 timeout 值进行续期 * * @param tokenValue 指定 token - * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) + * @param timeout 要修改成为的有效时间 (单位: 秒,填 -1 代表要续为永久有效) */ public static void renewTimeout(String tokenValue, long timeout) { stpLogic.renewTimeout(tokenValue, timeout); @@ -616,7 +617,7 @@ public static boolean hasRole(String role) { * 判断:指定账号是否含有指定角色标识, 返回 true 或 false * * @param loginId 账号id - * @param role 角色标识 + * @param role 角色标识 * @return 是否含有指定角色标识 */ public static boolean hasRole(Object loginId, String role) { @@ -629,7 +630,7 @@ public static boolean hasRole(Object loginId, String role) { * @param roleArray 角色标识数组 * @return true或false */ - public static boolean hasRoleAnd(String... roleArray){ + public static boolean hasRoleAnd(String... roleArray) { return stpLogic.hasRoleAnd(roleArray); } @@ -639,7 +640,7 @@ public static boolean hasRoleAnd(String... roleArray){ * @param roleArray 角色标识数组 * @return true或false */ - public static boolean hasRoleOr(String... roleArray){ + public static boolean hasRoleOr(String... roleArray) { return stpLogic.hasRoleOr(roleArray); } @@ -657,7 +658,7 @@ public static void checkRole(String role) { * * @param roleArray 角色标识数组 */ - public static void checkRoleAnd(String... roleArray){ + public static void checkRoleAnd(String... roleArray) { stpLogic.checkRoleAnd(roleArray); } @@ -666,7 +667,7 @@ public static void checkRoleAnd(String... roleArray){ * * @param roleArray 角色标识数组 */ - public static void checkRoleOr(String... roleArray){ + public static void checkRoleOr(String... roleArray) { stpLogic.checkRoleOr(roleArray); } @@ -705,7 +706,7 @@ public static boolean hasPermission(String permission) { /** * 判断:指定账号 id 是否含有指定权限, 返回 true 或 false * - * @param loginId 账号 id + * @param loginId 账号 id * @param permission 权限码 * @return 是否含有指定权限 */ @@ -719,7 +720,7 @@ public static boolean hasPermission(Object loginId, String permission) { * @param permissionArray 权限码数组 * @return true 或 false */ - public static boolean hasPermissionAnd(String... permissionArray){ + public static boolean hasPermissionAnd(String... permissionArray) { return stpLogic.hasPermissionAnd(permissionArray); } @@ -729,7 +730,7 @@ public static boolean hasPermissionAnd(String... permissionArray){ * @param permissionArray 权限码数组 * @return true 或 false */ - public static boolean hasPermissionOr(String... permissionArray){ + public static boolean hasPermissionOr(String... permissionArray) { return stpLogic.hasPermissionOr(permissionArray); } @@ -766,8 +767,8 @@ public static void checkPermissionOr(String... permissionArray) { /** * 获取指定账号 id 的 token *

- * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, - * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id @@ -780,12 +781,12 @@ public static String getTokenValueByLoginId(Object loginId) { /** * 获取指定账号 id 指定设备类型端的 token *

- * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, - * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId + * 在配置为允许并发登录时,此方法只会返回队列的最后一个 token, + * 如果你需要返回此账号 id 的所有 token,请调用 getTokenValueListByLoginId *

* * @param loginId 账号id - * @param device 设备类型,填 null 代表不限设备类型 + * @param device 设备类型,填 null 代表不限设备类型 * @return token值 */ public static String getTokenValueByLoginId(Object loginId, String device) { @@ -806,7 +807,7 @@ public static List getTokenValueListByLoginId(Object loginId) { * 获取指定账号 id 指定设备类型端的 token 集合 * * @param loginId 账号id - * @param device 设备类型,填 null 代表不限设备类型 + * @param device 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 token */ public static List getTokenValueListByLoginId(Object loginId, String device) { @@ -817,7 +818,7 @@ public static List getTokenValueListByLoginId(Object loginId, String dev * 获取指定账号 id 指定设备类型端的 tokenSign 集合 * * @param loginId 账号id - * @param device 设备类型,填 null 代表不限设备类型 + * @param device 设备类型,填 null 代表不限设备类型 * @return 此 loginId 的所有登录 tokenSign */ public static List getTokenSignListByLoginId(Object loginId, String device) { @@ -839,11 +840,10 @@ public static String getLoginDevice() { /** * 根据条件查询缓存中所有的 token * - * @param keyword 关键字 - * @param start 开始处索引 - * @param size 获取数量 (-1代表一直获取到末尾) + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) - * * @return token集合 */ public static List searchTokenValue(String keyword, int start, int size, boolean sortType) { @@ -853,11 +853,10 @@ public static List searchTokenValue(String keyword, int start, int size, /** * 根据条件查询缓存中所有的 SessionId * - * @param keyword 关键字 - * @param start 开始处索引 - * @param size 获取数量 (-1代表一直获取到末尾) + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) - * * @return sessionId集合 */ public static List searchSessionId(String keyword, int start, int size, boolean sortType) { @@ -867,11 +866,10 @@ public static List searchSessionId(String keyword, int start, int size, /** * 根据条件查询缓存中所有的 Token-Session-Id * - * @param keyword 关键字 - * @param start 开始处索引 - * @param size 获取数量 (-1代表一直获取到末尾) + * @param keyword 关键字 + * @param start 开始处索引 + * @param size 获取数量 (-1代表一直获取到末尾) * @param sortType 排序类型(true=正序,false=反序) - * * @return sessionId集合 */ public static List searchTokenSessionId(String keyword, int start, int size, boolean sortType) { @@ -886,7 +884,7 @@ public static List searchTokenSessionId(String keyword, int start, int s *

此方法不会直接将此账号id踢下线,如需封禁后立即掉线,请追加调用 StpUtil.logout(id) * * @param loginId 指定账号id - * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, long time) { stpLogic.disable(loginId, time); @@ -939,7 +937,7 @@ public static void untieDisable(Object loginId) { * * @param loginId 指定账号id * @param service 指定服务 - * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disable(Object loginId, String service, long time) { stpLogic.disable(loginId, service, time); @@ -959,7 +957,7 @@ public static boolean isDisable(Object loginId, String service) { /** * 校验:指定账号 指定服务 是否已被封禁,如果被封禁则抛出异常 * - * @param loginId 账号id + * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void checkDisable(Object loginId, String... services) { @@ -980,7 +978,7 @@ public static long getDisableTime(Object loginId, String service) { /** * 解封:指定账号、指定服务 * - * @param loginId 账号id + * @param loginId 账号id * @param services 指定服务,可以指定多个 */ public static void untieDisable(Object loginId, String... services) { @@ -994,8 +992,8 @@ public static void untieDisable(Object loginId, String... services) { * 封禁:指定账号,并指定封禁等级 * * @param loginId 指定账号id - * @param level 指定封禁等级 - * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, int level, long time) { stpLogic.disableLevel(loginId, level, time); @@ -1006,8 +1004,8 @@ public static void disableLevel(Object loginId, int level, long time) { * * @param loginId 指定账号id * @param service 指定封禁服务 - * @param level 指定封禁等级 - * @param time 封禁时间, 单位: 秒 (-1=永久封禁) + * @param level 指定封禁等级 + * @param time 封禁时间, 单位: 秒 (-1=永久封禁) */ public static void disableLevel(Object loginId, String service, int level, long time) { stpLogic.disableLevel(loginId, service, level, time); @@ -1017,7 +1015,7 @@ public static void disableLevel(Object loginId, String service, int level, long * 判断:指定账号是否已被封禁到指定等级 * * @param loginId 指定账号id - * @param level 指定封禁等级 + * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, int level) { @@ -1029,7 +1027,7 @@ public static boolean isDisableLevel(Object loginId, int level) { * * @param loginId 指定账号id * @param service 指定封禁服务 - * @param level 指定封禁等级 + * @param level 指定封禁等级 * @return / */ public static boolean isDisableLevel(Object loginId, String service, int level) { @@ -1040,7 +1038,7 @@ public static boolean isDisableLevel(Object loginId, String service, int level) * 校验:指定账号是否已被封禁到指定等级(如果已经达到,则抛出异常) * * @param loginId 指定账号id - * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, int level) { stpLogic.checkDisableLevel(loginId, level); @@ -1051,7 +1049,7 @@ public static void checkDisableLevel(Object loginId, int level) { * * @param loginId 指定账号id * @param service 指定封禁服务 - * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) + * @param level 封禁等级 (只有 封禁等级 ≥ 此值 才会抛出异常) */ public static void checkDisableLevel(Object loginId, String service, int level) { stpLogic.checkDisableLevel(loginId, service, level); @@ -1109,7 +1107,7 @@ public static boolean isSwitch() { /** * 在一个 lambda 代码段里,临时切换身份为指定账号id,lambda 结束后自动恢复 * - * @param loginId 指定账号id + * @param loginId 指定账号id * @param function 要执行的方法 */ public static void switchTo(Object loginId, SaFunction function) { @@ -1131,7 +1129,7 @@ public static void openSafe(long safeTime) { /** * 在当前会话 开启二级认证 * - * @param service 业务标识 + * @param service 业务标识 * @param safeTime 维持时间 (单位: 秒) */ public static void openSafe(String service, long safeTime) { @@ -1161,7 +1159,7 @@ public static boolean isSafe(String service) { * 判断:指定 token 是否处于二级认证时间内 * * @param tokenValue Token 值 - * @param service 业务标识 + * @param service 业务标识 * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时 */ public static boolean isSafe(String tokenValue, String service) { diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/auth/AdminAuthController.java b/src/main/java/cc/uncarbon/module/adminapi/web/auth/AdminAuthController.java index 92846fe..3882a33 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/auth/AdminAuthController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/auth/AdminAuthController.java @@ -11,26 +11,23 @@ import cc.uncarbon.helper.CaptchaHelper; import cc.uncarbon.helper.RolePermissionCacheHelper; import cc.uncarbon.module.adminapi.constant.AdminApiConstant; +import cc.uncarbon.module.adminapi.model.interior.AdminCaptchaContainer; +import cc.uncarbon.module.adminapi.model.response.AdminCaptchaVO; +import cc.uncarbon.module.adminapi.util.AdminStpUtil; import cc.uncarbon.module.sys.annotation.SysLog; import cc.uncarbon.module.sys.constant.SysConstant; -import cc.uncarbon.module.sys.enums.SysErrorEnum; import cc.uncarbon.module.sys.model.request.SysUserLoginDTO; import cc.uncarbon.module.sys.model.response.SysUserLoginBO; import cc.uncarbon.module.sys.model.response.SysUserLoginVO; import cc.uncarbon.module.sys.service.SysUserService; -import cc.uncarbon.module.adminapi.util.AdminStpUtil; import cn.dev33.satoken.annotation.SaCheckLogin; -import cn.hutool.captcha.AbstractCaptcha; import io.swagger.annotations.Api; -import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; -import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; -import java.io.IOException; @Api(value = "后台管理-鉴权接口", tags = {"后台管理-鉴权接口"}) @@ -55,6 +52,9 @@ public class AdminAuthController { @ApiOperation(value = "登录") @PostMapping(value = "/auth/login") public ApiResult login(@RequestBody @Valid SysUserLoginDTO dto) { + // 登录验证码核验;前端项目搜索关键词「 Helio: 登录验证码」 + // AdminApiErrorEnum.CAPTCHA_VALIDATE_FAILED.assertTrue(captchaHelper.validate(dto.getCaptchaId(), dto.getCaptchaAnswer())) + // RPC调用, 失败抛异常, 成功返回用户信息 SysUserLoginBO userInfo = sysUserService.adminLogin(dto); @@ -99,24 +99,12 @@ public ApiResult logout() { return ApiResult.success(); } - @ApiOperation(value = "验证码图片") - @ApiImplicitParam(name = "uuid", value = "验证码图片UUID", required = true) + @ApiOperation(value = "获取验证码") @GetMapping(value = "/auth/captcha") - public void captcha(HttpServletResponse response, String uuid) throws IOException { - /* - 由前端定义 UUID 其实并不算太好的办法,但是够简单 - 更复杂而安全的做法是:由后端生成一个 UUID,通过响应头返回给前端(这对前端有一定的技能技术要求) - */ - // uuid 为空则抛出异常 - SysErrorEnum.UUID_CANNOT_BE_BLANK.assertNotBlank(uuid); - + public ApiResult captcha() { // 核验方法:captchaHelper.validate - AbstractCaptcha captcha = captchaHelper.generate(uuid); - - // 写入响应流 - response.setHeader("Cache-Control", "no-store, no-cache"); - response.setContentType("image/png"); - captcha.write(response.getOutputStream()); + AdminCaptchaContainer captchaContainer = captchaHelper.generate(); + return ApiResult.data(new AdminCaptchaVO(captchaContainer)); } } diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/common/AdminSelectOptionsController.java b/src/main/java/cc/uncarbon/module/adminapi/web/common/AdminSelectOptionsController.java index 017de60..2de94af 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/common/AdminSelectOptionsController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/common/AdminSelectOptionsController.java @@ -1,16 +1,15 @@ package cc.uncarbon.module.adminapi.web.common; -import cc.uncarbon.framework.core.enums.GenderEnum; -import cc.uncarbon.framework.core.enums.YesOrNoEnum; import cc.uncarbon.framework.web.model.response.ApiResult; import cc.uncarbon.module.adminapi.constant.AdminApiConstant; import cc.uncarbon.module.adminapi.model.response.SelectOptionItemVO; +import cc.uncarbon.module.adminapi.util.AdminStpUtil; import cc.uncarbon.module.sys.model.response.SysDeptBO; +import cc.uncarbon.module.sys.model.response.SysRoleBO; import cc.uncarbon.module.sys.service.SysDeptService; -import cc.uncarbon.module.adminapi.util.AdminStpUtil; +import cc.uncarbon.module.sys.service.SysRoleService; import cn.dev33.satoken.annotation.SaCheckLogin; -import cn.dev33.satoken.annotation.SaIgnore; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -30,47 +29,29 @@ @Slf4j public class AdminSelectOptionsController { + private final SysRoleService sysRoleService; private final SysDeptService sysDeptService; /* 这里统一存放所有用于后台管理的下拉框数据源接口 - 避免多人协作时,不知道原来是否已经有了,或者写在某个边边角角里 - 造成重复开发 - */ - - // 👇该注解标记的接口不需要登录 - @SaIgnore - @ApiOperation(value = "(演示接口;可删除)性别下拉框") - @GetMapping(value = "/select-options/genders") - public ApiResult> genders(YesOrNoEnum demo) { - if (demo == YesOrNoEnum.YES) { - // demo=YES时,不输出「未知」、多输出demoString1 - List ret = SelectOptionItemVO.listOf(GenderEnum.class, item -> item != GenderEnum.UNKNOWN); - for (SelectOptionItemVO item : ret) { - item.setDemoString1("文本字段1-" + item.getValue()); - } - return ApiResult.data(ret); - } - // 默认返回 - return ApiResult.data(SelectOptionItemVO.listOf(GenderEnum.class)); + 避免多人协作时,不知道原来是否已经有了,或者写在某个边边角角里,造成重复开发 + */ + + @ApiOperation(value = "后台角色下拉框") + @GetMapping(value = "/select-options/roles") + public ApiResult> roles() { + return ApiResult.data( + SelectOptionItemVO.listOf(sysRoleService.adminSelectOptions(), SysRoleBO::getId, SysRoleBO::getTitle) + ); } - // 👇该注解标记的接口不需要登录 - @SaIgnore - @ApiOperation(value = "(演示接口;可删除)部门下拉框") + @ApiOperation(value = "部门下拉框(前端负责转为树状数据)") @GetMapping(value = "/select-options/depts") - public ApiResult> depts(YesOrNoEnum demo) { - if (demo == YesOrNoEnum.YES) { - // demo=YES时,多输出上级ID、多输出demoString1 - List ret = SelectOptionItemVO.listOf(sysDeptService.adminList(), SysDeptBO::getId, SysDeptBO::getTitle, SysDeptBO::getParentId); - for (SelectOptionItemVO item : ret) { - item.setDemoString1("文本字段1-" + item.getId()); - } - return ApiResult.data(ret); - } - // 默认返回 - return ApiResult.data(SelectOptionItemVO.listOf(sysDeptService.adminList(), SysDeptBO::getId, SysDeptBO::getTitle)); + public ApiResult> depts() { + return ApiResult.data( + SelectOptionItemVO.listOf(sysDeptService.adminSelectOptions(true), SysDeptBO::getId, SysDeptBO::getTitle, SysDeptBO::getParentId) + ); } } diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/oss/AdminOssUploadDownloadController.java b/src/main/java/cc/uncarbon/module/adminapi/web/oss/AdminOssUploadDownloadController.java index fba8ea3..6703549 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/oss/AdminOssUploadDownloadController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/oss/AdminOssUploadDownloadController.java @@ -68,8 +68,7 @@ public ApiResult upload( attr .setOriginalFilename(file.getOriginalFilename()) .setContentType(file.getContentType()) - .setMd5(md5) - ; + .setMd5(md5); bo = ossUploadDownloadFacade.upload(file.getBytes(), attr); } diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysDataDictController.java b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysDataDictController.java index 9fb641a..150ea9e 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysDataDictController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysDataDictController.java @@ -36,7 +36,7 @@ @Slf4j public class AdminSysDataDictController { - private static final String PERMISSION_PREFIX = "SysDataDict:" ; + private static final String PERMISSION_PREFIX = "SysDataDict:"; private final SysDataDictService sysDataDictService; diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysRoleController.java b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysRoleController.java index 37b3069..c2c2e26 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysRoleController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysRoleController.java @@ -65,6 +65,7 @@ public ApiResult getById(@PathVariable Long id) { @ApiOperation(value = "新增") @PostMapping(value = "/sys/roles") public ApiResult insert(@RequestBody @Valid AdminInsertOrUpdateSysRoleDTO dto) { + dto.setTenantId(null); sysRoleService.adminInsert(dto); return ApiResult.success(); @@ -75,7 +76,9 @@ public ApiResult insert(@RequestBody @Valid AdminInsertOrUpdateSysRoleDTO @ApiOperation(value = "编辑") @PutMapping(value = "/sys/roles/{id}") public ApiResult update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysRoleDTO dto) { - dto.setId(id); + dto + .setTenantId(null) + .setId(id); sysRoleService.adminUpdate(dto); return ApiResult.success(); diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysTenantController.java b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysTenantController.java index 290384b..0a6a3ea 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysTenantController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysTenantController.java @@ -6,6 +6,8 @@ import cc.uncarbon.framework.web.model.request.IdsDTO; import cc.uncarbon.framework.web.model.response.ApiResult; import cc.uncarbon.module.adminapi.constant.AdminApiConstant; +import cc.uncarbon.module.adminapi.event.KickOutSysUsersEvent; +import cc.uncarbon.module.adminapi.util.AdminStpUtil; import cc.uncarbon.module.sys.annotation.SysLog; import cc.uncarbon.module.sys.constant.SysConstant; import cc.uncarbon.module.sys.facade.SysTenantFacade; @@ -13,10 +15,11 @@ import cc.uncarbon.module.sys.model.request.AdminListSysTenantDTO; import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO; import cc.uncarbon.module.sys.model.response.SysTenantBO; +import cc.uncarbon.module.sys.model.response.SysTenantKickOutUsersBO; import cc.uncarbon.module.sys.service.SysTenantService; -import cc.uncarbon.module.adminapi.util.AdminStpUtil; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.extra.spring.SpringUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.RequiredArgsConstructor; @@ -75,7 +78,12 @@ public ApiResult insert(@RequestBody @Valid AdminInsertSysTenantDTO dto) { @PutMapping(value = "/sys/tenants/{id}") public ApiResult update(@PathVariable Long id, @RequestBody @Valid AdminUpdateSysTenantDTO dto) { dto.setId(id); - sysTenantService.adminUpdate(dto); + SysTenantKickOutUsersBO needKickOutUsers = sysTenantFacade.adminUpdate(dto); + + // 强制登出所有租户用户 + SpringUtil.publishEvent(new KickOutSysUsersEvent( + new KickOutSysUsersEvent.EventData(needKickOutUsers.getSysUserIds()) + )); return ApiResult.success(); } @@ -85,7 +93,12 @@ public ApiResult update(@PathVariable Long id, @RequestBody @Valid AdminUp @ApiOperation(value = "删除") @DeleteMapping(value = "/sys/tenants") public ApiResult delete(@RequestBody @Valid IdsDTO dto) { - sysTenantService.adminDelete(dto.getIds()); + SysTenantKickOutUsersBO needKickOutUsers = sysTenantFacade.adminDelete(dto.getIds()); + + // 强制登出所有租户用户 + SpringUtil.publishEvent(new KickOutSysUsersEvent( + new KickOutSysUsersEvent.EventData(needKickOutUsers.getSysUserIds()) + )); return ApiResult.success(); } diff --git a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysUserController.java b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysUserController.java index bf1502f..a2466ab 100644 --- a/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysUserController.java +++ b/src/main/java/cc/uncarbon/module/adminapi/web/sys/AdminSysUserController.java @@ -9,6 +9,7 @@ import cc.uncarbon.module.adminapi.constant.AdminApiConstant; import cc.uncarbon.module.sys.annotation.SysLog; import cc.uncarbon.module.sys.constant.SysConstant; +import cc.uncarbon.module.sys.enums.SysUserStatusEnum; import cc.uncarbon.module.sys.model.request.*; import cc.uncarbon.module.sys.model.response.SysUserBO; import cc.uncarbon.module.sys.model.response.VbenAdminUserInfoVO; @@ -75,6 +76,11 @@ public ApiResult update(@PathVariable Long id, @RequestBody @Valid AdminIn dto.setId(id); sysUserService.adminUpdate(dto); + // 新状态是禁用,连带踢出登录 + if (dto.getStatus() == SysUserStatusEnum.BANNED) { + kickOut(dto.getId()); + } + return ApiResult.success(); } @@ -85,6 +91,9 @@ public ApiResult update(@PathVariable Long id, @RequestBody @Valid AdminIn public ApiResult delete(@RequestBody @Valid IdsDTO dto) { sysUserService.adminDelete(dto.getIds()); + // 连带踢出登录 + dto.getIds().forEach(this::kickOut); + return ApiResult.success(); } diff --git a/src/main/java/cc/uncarbon/module/appapi/constant/AppApiConstant.java b/src/main/java/cc/uncarbon/module/appapi/constant/AppApiConstant.java index 3794040..1288d92 100644 --- a/src/main/java/cc/uncarbon/module/appapi/constant/AppApiConstant.java +++ b/src/main/java/cc/uncarbon/module/appapi/constant/AppApiConstant.java @@ -4,7 +4,7 @@ /** * C端接口常量 */ -public class AppApiConstant { +public final class AppApiConstant { private AppApiConstant() { } diff --git a/src/main/java/cc/uncarbon/module/oss/constant/OssConstant.java b/src/main/java/cc/uncarbon/module/oss/constant/OssConstant.java index 5dd0942..4ae8f0d 100644 --- a/src/main/java/cc/uncarbon/module/oss/constant/OssConstant.java +++ b/src/main/java/cc/uncarbon/module/oss/constant/OssConstant.java @@ -1,6 +1,6 @@ package cc.uncarbon.module.oss.constant; -public class OssConstant { +public final class OssConstant { private OssConstant() { } diff --git a/src/main/java/cc/uncarbon/module/oss/enums/OssErrorEnum.java b/src/main/java/cc/uncarbon/module/oss/enums/OssErrorEnum.java index f2a166c..f6d3685 100644 --- a/src/main/java/cc/uncarbon/module/oss/enums/OssErrorEnum.java +++ b/src/main/java/cc/uncarbon/module/oss/enums/OssErrorEnum.java @@ -7,7 +7,7 @@ /** - * OSS异常枚举类 + * oss模块错误枚举类 */ @AllArgsConstructor @Getter diff --git a/src/main/java/cc/uncarbon/module/oss/mapper/OssFileInfoMapper.java b/src/main/java/cc/uncarbon/module/oss/mapper/OssFileInfoMapper.java index 2ae68fa..aff1171 100644 --- a/src/main/java/cc/uncarbon/module/oss/mapper/OssFileInfoMapper.java +++ b/src/main/java/cc/uncarbon/module/oss/mapper/OssFileInfoMapper.java @@ -1,7 +1,7 @@ package cc.uncarbon.module.oss.mapper; -import cc.uncarbon.framework.crud.mapper.HelioBaseMapper; import cc.uncarbon.module.oss.entity.OssFileInfoEntity; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @@ -9,6 +9,6 @@ * 上传文件信息 */ @Mapper -public interface OssFileInfoMapper extends HelioBaseMapper { +public interface OssFileInfoMapper extends BaseMapper { } diff --git a/src/main/java/cc/uncarbon/module/oss/model/request/AdminInsertOrUpdateOssFileInfoDTO.java b/src/main/java/cc/uncarbon/module/oss/model/request/AdminInsertOrUpdateOssFileInfoDTO.java deleted file mode 100644 index cec39dc..0000000 --- a/src/main/java/cc/uncarbon/module/oss/model/request/AdminInsertOrUpdateOssFileInfoDTO.java +++ /dev/null @@ -1,58 +0,0 @@ -package cc.uncarbon.module.oss.model.request; - -import cc.uncarbon.framework.core.enums.EnabledStatusEnum; -import io.swagger.annotations.ApiModelProperty; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.experimental.Accessors; -import lombok.experimental.SuperBuilder; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import java.io.Serializable; - - -/** - * 后台管理-新增/编辑上传文件信息 DTO - */ -@Accessors(chain = true) -@SuperBuilder -@AllArgsConstructor -@NoArgsConstructor -@Data -public class AdminInsertOrUpdateOssFileInfoDTO implements Serializable { - - private static final long serialVersionUID = 1L; - - @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用") - private Long id; - - @ApiModelProperty(value = "前置路径", required = true) - @NotBlank(message = "前置路径不能为空") - private String path; - - @ApiModelProperty(value = "文件名", required = true) - @NotBlank(message = "文件名不能为空") - private String filename; - - @ApiModelProperty(value = "扩展名", required = true) - @NotBlank(message = "扩展名不能为空") - private String extendName; - - @ApiModelProperty(value = "文件大小", required = true) - @NotNull(message = "文件大小不能为空") - private Long filesize; - - @ApiModelProperty(value = "MD5", required = true) - @NotBlank(message = "MD5不能为空") - private String md5; - - @ApiModelProperty(value = "状态", required = true) - @NotNull(message = "状态不能为空") - private EnabledStatusEnum status; - - @ApiModelProperty(value = "类别编号") - private Integer classified; - -} diff --git a/src/main/java/cc/uncarbon/module/oss/service/OssFileInfoService.java b/src/main/java/cc/uncarbon/module/oss/service/OssFileInfoService.java index 9aaf342..6667e13 100644 --- a/src/main/java/cc/uncarbon/module/oss/service/OssFileInfoService.java +++ b/src/main/java/cc/uncarbon/module/oss/service/OssFileInfoService.java @@ -101,7 +101,7 @@ public void adminDelete(Collection ids) { log.info("[后台管理-删除上传文件信息] >> ids={}", ids); // 1. 删除原始文件 - List entityList = ossFileInfoMapper.selectByIds(ids); + List entityList = ossFileInfoMapper.selectBatchIds(ids); for (OssFileInfoEntity entity : entityList) { fileStorageService.delete(toFileInfo(entity)); } @@ -244,8 +244,7 @@ private PageResult entityPage2BOPage(Page enti .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords())); } } diff --git a/src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java b/src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java index 989ff59..333266d 100644 --- a/src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java +++ b/src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java @@ -4,7 +4,7 @@ /** * 系统管理常量 */ -public class SysConstant { +public final class SysConstant { private SysConstant() { } @@ -30,4 +30,15 @@ private SysConstant() { */ public static final Long SUPER_ADMIN_ROLE_ID = 1L; + /** + * 超级管理员角色值(固定) + */ + public static final String SUPER_ADMIN_ROLE_VALUE = "SuperAdmin"; + + /** + * 租户管理员角色值 + * 为了外显美观没有在前面增加Tenant字样 + */ + public static final String TENANT_ADMIN_ROLE_VALUE = "Admin"; + } diff --git a/src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java b/src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java index fc2529b..ddb9c58 100644 --- a/src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java +++ b/src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java @@ -36,11 +36,11 @@ public class SysDataDictEntity extends HelioBaseEntity { @TableField(value = "pascal_case_key") private String pascalCaseKey; - @ApiModelProperty(value = "键值") + @ApiModelProperty(value = "数据值") @TableField(value = "value") private String value; - @ApiModelProperty(value = "参数描述") + @ApiModelProperty(value = "描述") @TableField(value = "description") private String description; diff --git a/src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java b/src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java index 95cc8d2..414a654 100644 --- a/src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java +++ b/src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java @@ -1,6 +1,7 @@ package cc.uncarbon.module.sys.entity; import cc.uncarbon.framework.crud.entity.HelioBaseEntity; +import cc.uncarbon.module.sys.constant.SysConstant; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModelProperty; @@ -24,12 +25,26 @@ @TableName(value = "sys_role") public class SysRoleEntity extends HelioBaseEntity { - @ApiModelProperty(value = "名称") + @ApiModelProperty(value = "角色名") @TableField(value = "title") private String title; - @ApiModelProperty(value = "值") + @ApiModelProperty(value = "角色编码") @TableField(value = "value") private String value; + /** + * 角色实例可被视为超级管理员 + */ + public boolean isSuperAdmin() { + return SysConstant.SUPER_ADMIN_ROLE_ID.equals(getId()) || SysConstant.SUPER_ADMIN_ROLE_VALUE.equalsIgnoreCase(getValue()); + } + + /** + * 角色实例可被视为租户管理员 + */ + public boolean isTenantAdmin() { + return SysConstant.TENANT_ADMIN_ROLE_VALUE.equalsIgnoreCase(getValue()); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java b/src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java index 24f1e40..62a2f8e 100644 --- a/src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java +++ b/src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java @@ -6,7 +6,7 @@ /** - * SYS异常枚举类 + * sys模块错误枚举类 */ @AllArgsConstructor @Getter @@ -28,7 +28,40 @@ public enum SysErrorEnum implements HelioBaseEnum { NO_MENU_AVAILABLE_FOR_CURRENT_ROLE(400, "当前角色没有可用菜单"), - UUID_CANNOT_BE_BLANK(400, "UUID不能为空"), + /* + 以下8个枚举用于后台角色管理的越权检查 + */ + + ROLE_VALUE_CANNOT_BE(403, "角色值 {} 不能用于新增或编辑,请选用其他值"), + + CANNOT_DELETE_SUPER_ADMIN_ROLE(403, "不能删除超级管理员角色"), + + CANNOT_DELETE_TENANT_ADMIN_ROLE(403, "为减少脏数据,不建议直接删除租户管理员角色,需通过【删除租户】关联删除"), + + CANNOT_DELETE_SELF_ROLE(403, "不能删除自身角色"), + + CANNOT_BIND_MENUS_FOR_SUPER_ADMIN_ROLE(403, "不能为超级管理员角色绑定菜单"), + + CANNOT_BIND_MENUS_FOR_SELF(403, "不能为自身角色绑定菜单"), + + BEYOND_AUTHORITY_BIND_MENUS(401, "不得超越自身菜单权限"), + + CANNOT_BIND_MENUS_FOR_TENANT_ADMIN_ROLE(403, "无权为租户管理员绑定菜单"), + + /* + 以下4个枚举用于后台用户管理的越权检查 + */ + CANNOT_OPERATE_SELF_USER(403, "不能对自身进行此操作"), + + CANNOT_OPERATE_THIS_USER(403, "不能该用户进行此操作"), + + CANNOT_UNBIND_SELF_TENANT_ADMIN_ROLE(403, "自身的管理员角色不能被取消"), + + BEYOND_AUTHORITY_BIND_ROLES(401, "不得超越自身角色权限"), + + CANNOT_DELETE_PRIVILEGED_TENANT(403, "不能删除超级租户"), + + NEED_DELETE_EXISTING_TENANT_ADMIN_ROLE(500, "租户ID {} 对应的租户管理员角色已存在,请使用超级管理员账号删除"), ; private final Integer value; diff --git a/src/main/java/cc/uncarbon/module/sys/extension/impl/DefaultSysLogAspectExtension.java b/src/main/java/cc/uncarbon/module/sys/extension/impl/DefaultSysLogAspectExtension.java index 4986bfa..b357810 100644 --- a/src/main/java/cc/uncarbon/module/sys/extension/impl/DefaultSysLogAspectExtension.java +++ b/src/main/java/cc/uncarbon/module/sys/extension/impl/DefaultSysLogAspectExtension.java @@ -6,6 +6,7 @@ import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.Header; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.json.JSONObject; @@ -31,6 +32,8 @@ public IPLocationBO queryIPLocation(String ip) { .form("ip", ip) .form("json", Boolean.TRUE.toString()) .charset(CharsetUtil.CHARSET_GBK) + // since 1.11.0,加个UA避免被当成恶意请求,造成查IP失败 + .header(Header.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36") .timeout(5000); try (HttpResponse httpResponse = httpRequest.execute()) { String repStr = httpResponse.body(); diff --git a/src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java b/src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java index 3db17e0..8875384 100644 --- a/src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java +++ b/src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java @@ -1,9 +1,13 @@ package cc.uncarbon.module.sys.facade; import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO; +import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO; +import cc.uncarbon.module.sys.model.response.SysTenantKickOutUsersBO; + +import java.util.Collection; /** - * 系统租户防腐层,用于解决循环依赖 + * 系统租户解耦层,用于解决循环依赖 */ public interface SysTenantFacade { @@ -12,4 +16,14 @@ public interface SysTenantFacade { */ Long adminInsert(AdminInsertSysTenantDTO dto); + /** + * 后台管理-编辑 + */ + SysTenantKickOutUsersBO adminUpdate(AdminUpdateSysTenantDTO dto); + + /** + * 后台管理-删除 + */ + SysTenantKickOutUsersBO adminDelete(Collection ids); + } diff --git a/src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java b/src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java index 7959be8..8c83169 100644 --- a/src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java +++ b/src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java @@ -1,21 +1,34 @@ package cc.uncarbon.module.sys.facade.impl; +import cc.uncarbon.framework.core.constant.HelioConstant; +import cc.uncarbon.framework.core.enums.EnabledStatusEnum; +import cc.uncarbon.framework.core.function.StreamFunction; +import cc.uncarbon.module.sys.constant.SysConstant; import cc.uncarbon.module.sys.entity.SysTenantEntity; +import cc.uncarbon.module.sys.enums.SysErrorEnum; +import cc.uncarbon.module.sys.enums.SysUserStatusEnum; import cc.uncarbon.module.sys.facade.SysTenantFacade; import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO; import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysUserDTO; import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO; +import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO; +import cc.uncarbon.module.sys.model.response.SysTenantBO; +import cc.uncarbon.module.sys.model.response.SysTenantKickOutUsersBO; import cc.uncarbon.module.sys.service.SysRoleService; import cc.uncarbon.module.sys.service.SysTenantService; import cc.uncarbon.module.sys.service.SysUserRoleRelationService; import cc.uncarbon.module.sys.service.SysUserService; +import cn.hutool.core.collection.CollUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.*; +import java.util.stream.Collectors; + /** - * 系统租户防腐层,用于解决循环依赖 + * 系统租户Facade接口实现类 */ @RequiredArgsConstructor @Service @@ -50,38 +63,92 @@ public Long adminInsert(AdminInsertSysTenantDTO dto) { Long newRoleId = sysRoleService.adminInsert( AdminInsertOrUpdateSysRoleDTO.builder() .tenantId(newTenantId) - .title(dto.getTenantName() + "管理员") - .value("Admin") + .title(dto.getTenantName() + "主管理员") + .value(SysConstant.TENANT_ADMIN_ROLE_VALUE) .build() ); - /* - 3. 创建一个新用户 - */ + // 3. 创建一个新用户 Long newUserId = sysUserService.adminInsert( AdminInsertOrUpdateSysUserDTO.builder() .tenantId(newTenantId) .username(dto.getTenantAdminUsername()) .passwordOfNewUser(dto.getTenantAdminPassword()) - .nickname(dto.getTenantName() + "管理员") + .nickname(dto.getTenantName() + "主管理员") .email(dto.getTenantAdminEmail()) .phoneNo(dto.getTenantAdminPhoneNo()) + // 默认为正常状态 + .status(SysUserStatusEnum.ENABLED) .build() ); - /* - 4. 将新用户绑定至新角色上 - */ + // 4. 将新用户绑定至新角色上 sysUserRoleRelationService.adminInsert(newTenantId, newUserId, newRoleId); - /* - 5. 把管理员账号更新进库 - */ + // 5. 把管理员账号更新进库 SysTenantEntity update = new SysTenantEntity().setTenantAdminUserId(newUserId); update.setId(newTenantEntityId); sysTenantService.adminUpdate(update); - return newTenantId; } + @Override + @Transactional(rollbackFor = Exception.class) + public SysTenantKickOutUsersBO adminUpdate(AdminUpdateSysTenantDTO dto) { + sysTenantService.adminUpdate(dto); + + if (dto.getStatus() == EnabledStatusEnum.DISABLED) { + // 新状态是禁用,查出需要强制登出的用户 + Long tenantId = CollUtil.getFirst(determineTenantIdsByPrimaryKeys(Collections.singleton(dto.getId())).values()); + if (Objects.nonNull(tenantId)) { + List tenantSysUserIds = sysUserService.listUserIdsByTenantId(tenantId, Collections.singleton(EnabledStatusEnum.ENABLED)); + return new SysTenantKickOutUsersBO(tenantSysUserIds); + } + } + return new SysTenantKickOutUsersBO(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public SysTenantKickOutUsersBO adminDelete(Collection ids) { + Collection tenantIds = determineTenantIdsByPrimaryKeys(ids).values(); + if (CollUtil.isNotEmpty(tenantIds)) { + // 不能删除「超级租户」(租户ID=0) + SysErrorEnum.CANNOT_DELETE_PRIVILEGED_TENANT.assertNotContains(tenantIds, HelioConstant.Tenant.DEFAULT_PRIVILEGED_TENANT_ID); + + // 删除租户管理员角色、租户 + sysRoleService.adminDeleteTenantRoles(tenantIds, Collections.singleton(SysConstant.TENANT_ADMIN_ROLE_VALUE)); + sysTenantService.adminDelete(ids); + + // 查出需要强制登出的用户 + List tenantSysUserIds = tenantIds.stream() + .map(tenantId -> sysUserService.listUserIdsByTenantId(tenantId, Collections.singleton(EnabledStatusEnum.ENABLED))) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return new SysTenantKickOutUsersBO(tenantSysUserIds); + } + return new SysTenantKickOutUsersBO(); + } + + /* + ---------------------------------------------------------------- + 私有方法 private methods + ---------------------------------------------------------------- + */ + + /** + * 根据租户主键ID,确定租户IDs + * @return map[主键ID, 租户ID] + */ + private Map determineTenantIdsByPrimaryKeys(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + List sysTenantInfos = sysTenantService.listByIds(ids, false); + if (CollUtil.isEmpty(sysTenantInfos)) { + return Collections.emptyMap(); + } + return sysTenantInfos.stream().collect(Collectors.toMap(SysTenantBO::getId, SysTenantBO::getTenantId, StreamFunction.ignoredThrowingMerger())); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java b/src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java index fb7e24b..57ad0f1 100644 --- a/src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java +++ b/src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java @@ -1,13 +1,23 @@ package cc.uncarbon.module.sys.mapper; import cc.uncarbon.module.sys.entity.SysDeptEntity; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** * 部门 */ @Mapper public interface SysDeptMapper extends BaseMapper { - + + /** + * 列举已排序好的所有部门列表 + */ + default List sortedList() { + return selectList(new LambdaQueryWrapper().orderByAsc(SysDeptEntity::getSort)); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java b/src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java index a92d001..0993314 100644 --- a/src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java +++ b/src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java @@ -9,5 +9,5 @@ */ @Mapper public interface SysRoleMapper extends BaseMapper { - + } diff --git a/src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java b/src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java index 43bac02..0411f44 100644 --- a/src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java +++ b/src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java @@ -1,12 +1,19 @@ package cc.uncarbon.module.sys.mapper; +import cc.uncarbon.framework.core.enums.EnabledStatusEnum; import cc.uncarbon.module.sys.entity.SysUserEntity; import cc.uncarbon.module.sys.model.response.SysUserBaseInfoBO; +import cn.hutool.core.collection.CollUtil; import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + /** * 后台用户 */ @@ -29,4 +36,18 @@ public interface SysUserMapper extends BaseMapper { @InterceptorIgnore(tenantLine = "true") SysUserBaseInfoBO getBaseInfoByUserId(@Param(value = "userId") Long userId); + /** + * 查询所有用户IDs + * @param statusEnums 仅保留符合指定状态的,可以为null + */ + default List selectIds(Collection statusEnums) { + return selectList( + new LambdaQueryWrapper() + // 只取主键ID + .select(SysUserEntity::getId) + // 状态 + .in(CollUtil.isNotEmpty(statusEnums), SysUserEntity::getStatus, statusEnums) + ).stream().map(SysUserEntity::getId).collect(Collectors.toList()); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/mapper/xml/SysUserMapper.xml b/src/main/java/cc/uncarbon/module/sys/mapper/xml/SysUserMapper.xml index 3bccc5e..55069e4 100644 --- a/src/main/java/cc/uncarbon/module/sys/mapper/xml/SysUserMapper.xml +++ b/src/main/java/cc/uncarbon/module/sys/mapper/xml/SysUserMapper.xml @@ -5,7 +5,8 @@ @@ -17,7 +18,8 @@ , email , phone_no FROM sys_user - WHERE id = #{userId} + WHERE del_flag = 0 + AND id = #{userId} LIMIT 1 diff --git a/src/main/java/cc/uncarbon/module/sys/model/interior/UserDeptContainer.java b/src/main/java/cc/uncarbon/module/sys/model/interior/UserDeptContainer.java new file mode 100644 index 0000000..3e9847e --- /dev/null +++ b/src/main/java/cc/uncarbon/module/sys/model/interior/UserDeptContainer.java @@ -0,0 +1,82 @@ +package cc.uncarbon.module.sys.model.interior; + +import cc.uncarbon.module.sys.entity.SysDeptEntity; +import cn.hutool.core.collection.CollUtil; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户关联部门容器 + */ +@Getter +public class UserDeptContainer { + + /** + * 直接关联的部门IDs + * 一般只有0或1个元素 + */ + private final List relatedDeptIds; + + /** + * 直接关联的部门实例集合 + * 一般只有0或1个元素 + */ + private final List relatedDepts; + + /** + * 可见的部门IDs + */ + private List visibleDeptIds; + + /** + * 可见的部门实例集合 + */ + private List visibleDepts; + + + public UserDeptContainer(List relatedDeptIds, List relatedDepts) { + this.relatedDeptIds = relatedDeptIds; + this.relatedDepts = relatedDepts; + this.visibleDeptIds = relatedDeptIds; + this.visibleDepts = relatedDepts; + } + + /** + * 用户是否有实际关联的部门 + */ + public boolean hasRelatedDepts() { + return CollUtil.isNotEmpty(relatedDeptIds) && CollUtil.isNotEmpty(relatedDepts); + } + + /** + * 用户主要关联的部门,默认取第一个元素 + * @return null or 部门实例 + */ + public SysDeptEntity primaryRelatedDept() { + return CollUtil.getFirst(relatedDepts); + } + + /** + * 用户是否有实际可见的部门 + * 也可视为部门的数据权限范围 + */ + public boolean hasVisibleDepts() { + return CollUtil.isNotEmpty(visibleDeptIds) && CollUtil.isNotEmpty(visibleDepts); + } + + /** + * 更新可见的部门 + */ + public void updateVisibleDepts(List visibleDepts) { + if (CollUtil.isEmpty(visibleDepts)) { + this.visibleDeptIds = Collections.emptyList(); + this.visibleDepts = Collections.emptyList(); + } else { + this.visibleDeptIds = visibleDepts.stream().map(SysDeptEntity::getId).collect(Collectors.toList()); + this.visibleDepts = visibleDepts; + } + } +} diff --git a/src/main/java/cc/uncarbon/module/sys/model/interior/UserRoleContainer.java b/src/main/java/cc/uncarbon/module/sys/model/interior/UserRoleContainer.java new file mode 100644 index 0000000..b02958e --- /dev/null +++ b/src/main/java/cc/uncarbon/module/sys/model/interior/UserRoleContainer.java @@ -0,0 +1,48 @@ +package cc.uncarbon.module.sys.model.interior; + +import cc.uncarbon.module.sys.entity.SysRoleEntity; +import lombok.Getter; + +import java.util.List; +import java.util.Set; + +/** + * 用户关联角色容器 + */ +@Getter +public class UserRoleContainer { + + /** + * 直接关联的角色IDs + */ + private final Set relatedRoleIds; + + /** + * 直接关联的角色实例集合 + */ + private final List relatedRoles; + + /** + * 为超级管理员 + */ + private final boolean superAdmin; + + /** + * 为租户管理员 + */ + private final boolean tenantAdmin; + + /** + * 非级管理员or租户管理员 + */ + private final boolean notAnyAdmin; + + + public UserRoleContainer(Set relatedRoleIds, List relatedRoles) { + this.relatedRoleIds = relatedRoleIds; + this.relatedRoles = relatedRoles; + this.superAdmin = relatedRoles.stream().anyMatch(SysRoleEntity::isSuperAdmin); + this.tenantAdmin = relatedRoles.stream().anyMatch(SysRoleEntity::isTenantAdmin); + this.notAnyAdmin = !this.superAdmin && !this.tenantAdmin; + } +} diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java index 146f484..a77b5f6 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java @@ -8,6 +8,7 @@ import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; import java.io.Serializable; @@ -25,32 +26,40 @@ public class AdminInsertOrUpdateSysDataDictDTO implements Serializable { private Long id; @ApiModelProperty(value = "驼峰式键名", required = true) + @Size(max = 100, message = "【驼峰式键名】最长100位") @NotBlank(message = "驼峰式键名不能为空") private String camelCaseKey; @ApiModelProperty(value = "下划线式键名", required = true) + @Size(max = 100, message = "【下划线式键名】最长100位") @NotBlank(message = "下划线式键名不能为空") private String underCaseKey; @ApiModelProperty(value = "帕斯卡式键名", required = true) + @Size(max = 100, message = "【帕斯卡式键名】最长100位") @NotBlank(message = "帕斯卡式键名不能为空") private String pascalCaseKey; - @ApiModelProperty(value = "键值", required = true) - @NotBlank(message = "键值不能为空") + @ApiModelProperty(value = "数据值", required = true) + @Size(max = 255, message = "【数据值】最长255位") + @NotBlank(message = "数据值不能为空") private String value; - @ApiModelProperty(value = "参数描述", required = true) - @NotBlank(message = "参数描述不能为空") + @ApiModelProperty(value = "描述", required = true) + @Size(max = 255, message = "【描述】最长255位") + @NotBlank(message = "描述不能为空") private String description; @ApiModelProperty(value = "单位") + @Size(max = 30, message = "【单位】最长30位") private String unit; @ApiModelProperty(value = "取值范围") + @Size(max = 255, message = "【取值范围】最长255位") private String valueRange; @ApiModelProperty(value = "别称键名") + @Size(max = 100, message = "【别称键名】最长100位") private String aliasKey; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java index 2190c2f..d1a6fe4 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java @@ -9,6 +9,7 @@ import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; import java.io.Serializable; @@ -25,11 +26,9 @@ public class AdminInsertOrUpdateSysDeptDTO implements Serializable { @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用") private Long id; - @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用") - private Long tenantId; - - @ApiModelProperty(value = "名称", required = true) - @NotBlank(message = "名称不能为空") + @ApiModelProperty(value = "部门名称", required = true) + @Size(max = 50, message = "【部门名称】最长50位") + @NotBlank(message = "部门名称不能为空") private String title; @ApiModelProperty(value = "上级ID(无上级节点设置为0)") diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java index 1e22057..575c735 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java @@ -11,6 +11,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.io.Serializable; @@ -27,11 +28,9 @@ public class AdminInsertOrUpdateSysMenuDTO implements Serializable { @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用") private Long id; - @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用") - private Long tenantId; - - @ApiModelProperty(value = "名称", required = true) - @NotBlank(message = "名称不能为空") + @ApiModelProperty(value = "菜单名称", required = true) + @Size(max = 50, message = "【菜单名称】最长50位") + @NotBlank(message = "菜单名称不能为空") private String title; @ApiModelProperty(value = "上级菜单ID(无上级节点设置为0)") @@ -42,12 +41,15 @@ public class AdminInsertOrUpdateSysMenuDTO implements Serializable { private SysMenuTypeEnum type; @ApiModelProperty(value = "组件") + @Size(max = 50, message = "【组件】最长50位") private String component; @ApiModelProperty(value = "权限标识") + @Size(max = 255, message = "【权限标识】最长255位") private String permission; @ApiModelProperty(value = "图标") + @Size(max = 255, message = "【图标】最长255位") private String icon; @ApiModelProperty(value = "排序") @@ -57,6 +59,7 @@ public class AdminInsertOrUpdateSysMenuDTO implements Serializable { private EnabledStatusEnum status; @ApiModelProperty(value = "外链地址") + @Size(max = 255, message = "【外链地址】最长255位") private String externalLink; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java index d0d476f..60a7613 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java @@ -8,6 +8,7 @@ import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; import java.io.Serializable; @@ -25,14 +26,17 @@ public class AdminInsertOrUpdateSysParamDTO implements Serializable { private Long id; @ApiModelProperty(value = "键名", required = true) + @Size(max = 50, message = "【键名】最长50位") @NotBlank(message = "键名不能为空") private String name; @ApiModelProperty(value = "键值", required = true) + @Size(max = 255, message = "【键值】最长255位") @NotBlank(message = "键值不能为空") private String value; @ApiModelProperty(value = "描述", required = true) + @Size(max = 255, message = "【描述】最长255位") @NotBlank(message = "描述不能为空") private String description; diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java index 8cb009e..92378ac 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java @@ -1,14 +1,18 @@ package cc.uncarbon.module.sys.model.request; +import cc.uncarbon.module.sys.constant.SysConstant; import io.swagger.annotations.ApiModelProperty; -import java.io.Serializable; -import javax.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Objects; + /** * 后台管理-新增/编辑后台角色 @@ -26,12 +30,21 @@ public class AdminInsertOrUpdateSysRoleDTO implements Serializable { @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用") private Long tenantId; - @ApiModelProperty(value = "名称", required = true) - @NotBlank(message = "名称不能为空") + @ApiModelProperty(value = "角色名", required = true) + @Size(max = 50, message = "【角色名】最长50位") + @NotBlank(message = "角色名不能为空") private String title; - @ApiModelProperty(value = "值", required = true) - @NotBlank(message = "值不能为空") + @ApiModelProperty(value = "角色编码", required = true) + @Size(max = 100, message = "【角色编码】最长100位") + @NotBlank(message = "角色编码不能为空") private String value; + /** + * 是否用于创建新租户管理员角色 + */ + public boolean creatingNewTenantAdmin() { + return Objects.nonNull(tenantId) && SysConstant.TENANT_ADMIN_ROLE_VALUE.equalsIgnoreCase(value); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java index 0ae4615..1005094 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java @@ -2,7 +2,9 @@ import cc.uncarbon.framework.core.constant.HelioConstant; import cc.uncarbon.framework.core.enums.GenderEnum; +import cc.uncarbon.framework.core.exception.BusinessException; import cc.uncarbon.module.sys.enums.SysUserStatusEnum; +import cn.hutool.core.text.CharSequenceUtil; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -13,7 +15,9 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; import java.io.Serializable; +import java.util.Objects; /** @@ -33,6 +37,7 @@ public class AdminInsertOrUpdateSysUserDTO implements Serializable { private Long tenantId; @ApiModelProperty(value = "账号", required = true) + @Size(min = 6, max = 16, message = "【账号】最短6位,最长16位") @NotBlank(message = "账号不能为空") private String username; @@ -40,6 +45,7 @@ public class AdminInsertOrUpdateSysUserDTO implements Serializable { private String passwordOfNewUser; @ApiModelProperty(value = "昵称", required = true) + @Size(max = 100, message = "【昵称】最长100位") @NotBlank(message = "昵称不能为空") private String nickname; @@ -53,15 +59,29 @@ public class AdminInsertOrUpdateSysUserDTO implements Serializable { @ApiModelProperty(value = "邮箱", required = true) @Pattern(message = "邮箱格式有误", regexp = HelioConstant.Regex.EMAIL) + @Size(max = 255, message = "【邮箱】最长255位") @NotBlank(message = "邮箱不能为空") private String email; @ApiModelProperty(value = "手机号", required = true) @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO) + @Size(max = 20, message = "【手机号】最长20位") @NotBlank(message = "手机号不能为空") private String phoneNo; @ApiModelProperty(value = "所属部门ID") private Long deptId; + + public void validate() { + boolean isUpdate = Objects.nonNull(id); + if (!isUpdate) { + // 新增 + int passwordOfNewUserLen = CharSequenceUtil.length(passwordOfNewUser); + if (passwordOfNewUserLen < 8 || passwordOfNewUserLen > 20) { + throw new BusinessException("【密码】最短8位,最长20位"); + } + } + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java index 1896564..f1aa07f 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java @@ -8,8 +8,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.Accessors; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Pattern; +import javax.validation.constraints.*; /** @@ -22,21 +21,30 @@ @Data public class AdminInsertSysTenantDTO extends AdminUpdateSysTenantDTO { + @ApiModelProperty(value = "租户ID(纯数字)", required = true) + @Positive(message = "租户ID须为正整数") + @NotNull(message = "租户ID不能为空") + private Long tenantId; + @ApiModelProperty(value = "管理员账号", required = true) + @Size(min = 6, max = 16, message = "【管理员账号】最短6位,最长16位") @NotBlank(message = "管理员账号不能为空") private String tenantAdminUsername; @ApiModelProperty(value = "管理员密码", required = true) + @Size(min = 8, max = 20, message = "【管理员密码】最短8位,最长20位") @NotBlank(message = "管理员密码不能为空") private String tenantAdminPassword; @ApiModelProperty(value = "管理员邮箱", required = true) - @Pattern(message = "邮箱格式不正确", regexp = HelioConstant.Regex.EMAIL) + @Pattern(message = "管理员邮箱格式不正确", regexp = HelioConstant.Regex.EMAIL) + @Size(max = 255, message = "【管理员邮箱】最长255位") @NotBlank(message = "管理员邮箱不能为空") private String tenantAdminEmail; @ApiModelProperty(value = "管理员手机号", required = true) - @Pattern(message = "手机号格式不正确", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO) + @Pattern(message = "管理员手机号格式不正确", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO) + @Size(max = 20, message = "【管理员手机号】最长20位") @NotBlank(message = "管理员手机号不能为空") private String tenantAdminPhoneNo; diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java index cb8a81b..698b266 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java @@ -20,7 +20,7 @@ @Data public class AdminListSysDataDictDTO implements Serializable { - @ApiModelProperty(value = "参数描述(关键词)") + @ApiModelProperty(value = "描述(关键词)") private String description; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java index d7c2219..1f98fc9 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java @@ -20,10 +20,10 @@ @Data public class AdminListSysRoleDTO implements Serializable { - @ApiModelProperty(value = "名称(关键词)") + @ApiModelProperty(value = "角色名(关键词)") private String title; - @ApiModelProperty(value = "值(关键词)") + @ApiModelProperty(value = "角色编码(关键词)") private String value; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java index f761e18..bb18976 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java @@ -1,5 +1,6 @@ package cc.uncarbon.module.sys.model.request; +import cc.uncarbon.module.sys.constant.SysConstant; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; @@ -8,6 +9,7 @@ import lombok.experimental.Accessors; import java.io.Serializable; +import java.util.Objects; /** @@ -23,4 +25,14 @@ public class AdminListSysUserDTO implements Serializable { @ApiModelProperty(value = "手机号(关键词)") private String phoneNo; + @ApiModelProperty(value = "手动选择的部门ID") + private Long selectedDeptId; + + /** + * 是否需要根据【手动选择的部门】筛选用户 + */ + public boolean needFilterBySelectedDeptId() { + return Objects.nonNull(selectedDeptId) && selectedDeptId > SysConstant.ROOT_PARENT_ID; + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java index 27722c7..b313103 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java @@ -23,11 +23,11 @@ public class AdminResetSysUserPasswordDTO implements Serializable { @ApiModelProperty(value = "随机新密码", required = true) - @Size(min = 16, max = 64, message = "随机新密码须为16-64位") + @Size(min = 16, max = 64, message = "【随机新密码】最短16位,最长64位") @NotBlank(message = "随机新密码不能为空") private String randomPassword; - @ApiModelProperty(value = "用户ID", hidden = true) + @ApiModelProperty(value = "用户ID", hidden = true) private Long userId; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java index 74c1000..7a150ac 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java @@ -27,12 +27,12 @@ public class AdminUpdateCurrentSysUserPasswordDTO implements Serializable { private String oldPassword; @ApiModelProperty(value = "新密码", required = true) - @Size(min = 8, max = 20, message = "密码须为8-20位") + @Size(min = 8, max = 20, message = "【密码】长度须在 8 至 20 位之间") @NotBlank(message = "密码不能为空") private String newPassword; @ApiModelProperty(value = "确认新密码", required = true) - @Size(min = 8, max = 20, message = "确认密码须为8-20位") + @Size(min = 8, max = 20, message = "【确认密码】长度须在 8 至 20 位之间") @NotBlank(message = "确认密码不能为空") private String confirmNewPassword; diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java index ebc09d8..28cc7b2 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java @@ -10,6 +10,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.io.Serializable; @@ -27,18 +28,16 @@ public class AdminUpdateSysTenantDTO implements Serializable { private Long id; @ApiModelProperty(value = "租户名", required = true) + @Size(max = 50, message = "【租户名】最长50位") @NotBlank(message = "租户名不能为空") private String tenantName; - @ApiModelProperty(value = "租户ID(纯数字)", required = true) - @NotNull(message = "租户ID不能为空") - private Long tenantId; - @ApiModelProperty(value = "状态", required = true) @NotNull(message = "状态不能为空") private EnabledStatusEnum status; @ApiModelProperty(value = "备注") + @Size(max = 255, message = "【备注】最长255位") private String remark; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java b/src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java index 64a3ce1..79e2467 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java @@ -24,12 +24,12 @@ public class SysUserLoginDTO implements Serializable { @ApiModelProperty(value = "账号", required = true) - @Size(min = 5, max = 16, message = "【账号】长度须在 5 至 16 位之间") + @Size(min = 5, max = 16, message = "【账号】最短5位,最长16位") @NotBlank(message = "账号不能为空") private String username; @ApiModelProperty(value = "密码", required = true) - @Size(min = 5, max = 64, message = "【密码】长度须在 5 至 16 位之间") + @Size(min = 5, max = 20, message = "【密码】最短5位,最长20位") @NotBlank(message = "密码不能为空") private String password; @@ -40,10 +40,10 @@ public class SysUserLoginDTO implements Serializable { @ApiModelProperty(value = "租户ID(可选,启用多租户后有效)") private Long tenantId; - @ApiModelProperty(value = "验证码图片UUID(可选,需自行对接业务逻辑)") - private String captchaUUID; + @ApiModelProperty(value = "验证码唯一标识(可选)") + private String captchaId; - @ApiModelProperty(value = "验证码答案(可选,需自行对接业务逻辑)") + @ApiModelProperty(value = "验证码答案(可选)") private String captchaAnswer; } diff --git a/src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java b/src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java index bc40186..468ddf6 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java @@ -46,10 +46,10 @@ public class SysDataDictBO implements Serializable { @ApiModelProperty(value = "帕斯卡式键名") private String pascalCaseKey; - @ApiModelProperty(value = "键值") + @ApiModelProperty(value = "数据值") private String value; - @ApiModelProperty(value = "参数描述") + @ApiModelProperty(value = "描述") private String description; @ApiModelProperty(value = "单位") diff --git a/src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java b/src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java index bc61f96..7b00073 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java @@ -38,10 +38,10 @@ public class SysRoleBO implements Serializable { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT) private LocalDateTime updatedAt; - @ApiModelProperty(value = "名称") + @ApiModelProperty(value = "角色名") private String title; - @ApiModelProperty(value = "值") + @ApiModelProperty(value = "角色编码") private String value; @ApiModelProperty(value = "可见菜单Ids") diff --git a/src/main/java/cc/uncarbon/module/sys/model/response/SysTenantKickOutUsersBO.java b/src/main/java/cc/uncarbon/module/sys/model/response/SysTenantKickOutUsersBO.java new file mode 100644 index 0000000..4cbeffb --- /dev/null +++ b/src/main/java/cc/uncarbon/module/sys/model/response/SysTenantKickOutUsersBO.java @@ -0,0 +1,26 @@ +package cc.uncarbon.module.sys.model.response; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; + +import java.util.Collections; +import java.util.List; + +/** + * 系统租户-需强制登出用户 BO + * 同一时间大量登出,会操作大量Redis键,可能存在缓存雪崩的风险 + */ +@Getter +public class SysTenantKickOutUsersBO { + + @ApiModelProperty(value = "后台用户IDs") + private final List sysUserIds; + + public SysTenantKickOutUsersBO() { + this.sysUserIds = Collections.emptyList(); + } + + public SysTenantKickOutUsersBO(List sysUserIds) { + this.sysUserIds = sysUserIds; + } +} diff --git a/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java b/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java index 4421513..cf0a166 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java @@ -2,16 +2,17 @@ import cc.uncarbon.framework.core.context.TenantContext; import io.swagger.annotations.ApiModelProperty; -import java.io.Serializable; -import java.util.List; -import java.util.Map; -import java.util.Set; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.Set; + /** * 登录后返回的字段 diff --git a/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java b/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java index e868652..8a0d494 100644 --- a/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java +++ b/src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java @@ -1,14 +1,15 @@ package cc.uncarbon.module.sys.model.response; import io.swagger.annotations.ApiModelProperty; -import java.io.Serializable; -import java.util.Collection; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; +import java.io.Serializable; +import java.util.Collection; + /** * 登录后返回的字段 diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java b/src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java index 0a07ed8..c839797 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java @@ -176,8 +176,7 @@ private PageResult entityPage2BOPage(Page enti .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords())); } /** diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java b/src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java index 7b046d9..83a4ca3 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java @@ -1,11 +1,14 @@ package cc.uncarbon.module.sys.service; import cc.uncarbon.framework.core.constant.HelioConstant; +import cc.uncarbon.framework.core.context.UserContextHolder; import cc.uncarbon.framework.core.exception.BusinessException; import cc.uncarbon.module.sys.constant.SysConstant; import cc.uncarbon.module.sys.entity.SysDeptEntity; import cc.uncarbon.module.sys.enums.SysErrorEnum; import cc.uncarbon.module.sys.mapper.SysDeptMapper; +import cc.uncarbon.module.sys.model.interior.UserDeptContainer; +import cc.uncarbon.module.sys.model.interior.UserRoleContainer; import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysDeptDTO; import cc.uncarbon.module.sys.model.response.SysDeptBO; import cn.hutool.core.bean.BeanUtil; @@ -17,10 +20,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** @@ -33,20 +34,14 @@ public class SysDeptService { private final SysDeptMapper sysDeptMapper; private final SysUserDeptRelationService sysUserDeptRelationService; + private final SysRoleService sysRoleService; /** * 后台管理-列表 */ public List adminList() { - List entityList = sysDeptMapper.selectList( - new QueryWrapper() - .lambda() - // 排序 - .orderByAsc(SysDeptEntity::getSort) - ); - - return this.entityList2BOs(entityList); + return entityList2BOs(sysDeptMapper.sortedList()); } /** @@ -62,7 +57,7 @@ public SysDeptBO getOneById(Long id) { /** * 根据 ID 取详情 * - * @param id 主键ID + * @param id 主键ID * @param throwIfInvalidId 是否在 ID 无效时抛出异常 * @return null or BO */ @@ -124,19 +119,51 @@ public void adminDelete(Collection ids) { } /** - * 取所属部门简易信息 - * - * @param userId 用户ID + * 后台管理-下拉框数据 + * @param inferiorsOnly 只能看到本部门及以下 */ - public SysDeptBO getPlainDeptByUserId(Long userId) { - List deptIds = sysUserDeptRelationService.getUserDeptIds(userId); - if (CollUtil.isEmpty(deptIds)) { - return null; + public List adminSelectOptions(boolean inferiorsOnly) { + if (inferiorsOnly) { + UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); + if (currentUser.isNotAnyAdmin()) { + // 非管理员才会限制,只能看到本部门及以下 + UserDeptContainer deptContainer = getCurrentUserDeptContainer(true); + return entityList2BOs(deptContainer.getVisibleDepts()); + } } - SysDeptEntity entity = sysDeptMapper.selectById(CollUtil.getFirst(deptIds)); - return this.entity2BO(entity); + // 能看所有 + return adminList(); } + /** + * 取当前用户关联部门信息 + * 仅内部使用 + * @param queryVisibleDept 是否要进一步查询可见部门 + */ + protected UserDeptContainer getCurrentUserDeptContainer(boolean queryVisibleDept) { + return getSpecifiedUserDeptContainer(UserContextHolder.getUserId(), queryVisibleDept); + } + + /** + * 取指定用户关联部门信息 + * 仅内部使用 + * @param queryVisibleDept 是否要进一步查询可见部门 + */ + protected UserDeptContainer getSpecifiedUserDeptContainer(Long specifiedUserId, boolean queryVisibleDept) { + List currentUserDeptIds = sysUserDeptRelationService.getUserDeptIds(specifiedUserId); + List currentUserDepts = Collections.emptyList(); + if (CollUtil.isNotEmpty(currentUserDeptIds)) { + currentUserDepts = sysDeptMapper.selectBatchIds(currentUserDeptIds); + } + UserDeptContainer container = new UserDeptContainer(currentUserDeptIds, currentUserDepts); + + if (queryVisibleDept && container.hasRelatedDepts()) { + List allDepts = sysDeptMapper.sortedList(); + List inferiors = determineAllInferiors(allDepts, container.primaryRelatedDept()); + container.updateVisibleDepts(inferiors); + } + return container; + } /* ---------------------------------------------------------------- @@ -207,4 +234,34 @@ private void checkExistence(AdminInsertOrUpdateSysDeptDTO dto) { throw new BusinessException(400, "已存在相同部门,请重新输入"); } } + + /** + * 找出本部门及所有下级部门 + * + * @param start 本部门 + * @return 本部门 + 所有下级部门 + */ + private List determineAllInferiors(List entityList, SysDeptEntity start) { + // 结果集合 + List ret = new ArrayList<>(entityList.size()); + Deque deque = new ArrayDeque<>(); + + // 转map提高效率 + Map> groupByParentId = entityList.stream().collect(Collectors.groupingBy(SysDeptEntity::getParentId)); + + // 起点 + ret.add(start); + deque.add(start); + + // 循环填充下级部门实例 + while (CollUtil.isNotEmpty(deque)) { + SysDeptEntity parent = deque.pop(); + List children = groupByParentId.get(parent.getId()); + if (CollUtil.isNotEmpty(children)) { + ret.addAll(children); + deque.addAll(children); + } + } + return ret; + } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysLogService.java b/src/main/java/cc/uncarbon/module/sys/service/SysLogService.java index cefe760..3be9cbb 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysLogService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysLogService.java @@ -173,8 +173,7 @@ private PageResult entityPage2BOPage(Page entityPage) { .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords())); } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java b/src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java index e5b9bc8..441d3d5 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java @@ -174,8 +174,7 @@ public Map> getRoleIdPermissionMap(Collection roleIds) { permissions = sysMenuMapper.selectList(null).stream() .map(SysMenuEntity::getPermission) .filter(StrUtil::isNotEmpty) - .collect(Collectors.toSet()) - ; + .collect(Collectors.toSet()); } else { // 非超级管理员则通过角色ID,关联查询拥有的菜单,菜单上有权限名 @@ -254,34 +253,28 @@ private SysMenuBO entity2BO(SysMenuEntity entity) { // 这里是兼容 JDK8 的写法,使用较高 JDK 版本可使用语法糖 switch (bo.getType()) { case DIR: - case BUTTON: { + case BUTTON: bo .setComponent(SysConstant.VBEN_ADMIN_BLANK_VIEW) .setExternalLink(null) - .setPath(StrPool.SLASH + snowflakeIdStr) - ; + .setPath(StrPool.SLASH + snowflakeIdStr); break; - } - case MENU: { + case MENU: bo .setExternalLink(null) - .setPath(bo.getComponent()) - ; + .setPath(bo.getComponent()); // 防止用户忘记加了, 主动补充/ if (CharSequenceUtil.isNotBlank(bo.getPath()) && !bo.getPath().startsWith(StrPool.SLASH)) { bo.setPath(StrPool.SLASH + bo.getPath()); } break; - } - case EXTERNAL_LINK: { + case EXTERNAL_LINK: bo .setComponent(bo.getExternalLink()) - .setPath(bo.getExternalLink()) - ; + .setPath(bo.getExternalLink()); break; - } + default: break; } - return bo; } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysParamService.java b/src/main/java/cc/uncarbon/module/sys/service/SysParamService.java index 4dd74c8..2434e33 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysParamService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysParamService.java @@ -216,8 +216,7 @@ private PageResult entityPage2BOPage(Page entityPage .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords())); } /** diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java b/src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java index 7928003..145d039 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java @@ -71,9 +71,7 @@ public void cleanAndBind(Long roleId, Collection menuIds) { return; } - /* - 先删除不再需要的关联关系 - */ + // 先删除不再需要的关联关系 sysRoleMenuRelationMapper.delete( new QueryWrapper() .lambda() @@ -81,31 +79,24 @@ public void cleanAndBind(Long roleId, Collection menuIds) { .notIn(SysRoleMenuRelationEntity::getMenuId, menuIds) ); - /* - 取出需要增量更新的部分 - */ + // 取出需要增量更新的部分 Set existingMenuIds = sysRoleMenuRelationMapper.selectList(menuIdsQuery) .stream().map(SysRoleMenuRelationEntity::getMenuId) .collect(Collectors.toSet()); menuIds.removeAll(existingMenuIds); - if (CollUtil.isEmpty(menuIds)) { - // 没有需要增量更新的部分 - return; - } - - /* - 批量插入需要增量更新的部分 - */ - List entityList = new ArrayList<>(menuIds.size()); - for (Long menuId : menuIds) { - entityList.add( - SysRoleMenuRelationEntity.builder() - .roleId(roleId) - .menuId(menuId) - .build() - ); + if (CollUtil.isNotEmpty(menuIds)) { + // 批量插入需要增量更新的部分 + List entityList = new ArrayList<>(menuIds.size()); + for (Long menuId : menuIds) { + entityList.add( + SysRoleMenuRelationEntity.builder() + .roleId(roleId) + .menuId(menuId) + .build() + ); + } + entityList.forEach(sysRoleMenuRelationMapper::insert); } - entityList.forEach(sysRoleMenuRelationMapper::insert); } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java b/src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java index 1db43fe..cb82d7c 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java @@ -1,13 +1,16 @@ package cc.uncarbon.module.sys.service; import cc.uncarbon.framework.core.constant.HelioConstant; +import cc.uncarbon.framework.core.context.UserContextHolder; import cc.uncarbon.framework.core.exception.BusinessException; import cc.uncarbon.framework.core.function.StreamFunction; import cc.uncarbon.framework.core.page.PageParam; import cc.uncarbon.framework.core.page.PageResult; +import cc.uncarbon.module.sys.constant.SysConstant; import cc.uncarbon.module.sys.entity.SysRoleEntity; import cc.uncarbon.module.sys.enums.SysErrorEnum; import cc.uncarbon.module.sys.mapper.SysRoleMapper; +import cc.uncarbon.module.sys.model.interior.UserRoleContainer; import cc.uncarbon.module.sys.model.request.AdminBindRoleMenuRelationDTO; import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO; import cc.uncarbon.module.sys.model.request.AdminListSysRoleDTO; @@ -44,6 +47,7 @@ public class SysRoleService { * 后台管理-分页列表 */ public PageResult adminList(PageParam pageParam, AdminListSysRoleDTO dto) { + Set invisibleRoleIds = determineInvisibleRoleIds(); Page entityPage = sysRoleMapper.selectPage( new Page<>(pageParam.getPageNum(), pageParam.getPageSize()), new QueryWrapper() @@ -52,11 +56,13 @@ public PageResult adminList(PageParam pageParam, AdminListSysRoleDTO .like(CharSequenceUtil.isNotBlank(dto.getTitle()), SysRoleEntity::getTitle, CharSequenceUtil.cleanBlank(dto.getTitle())) // 值 .like(CharSequenceUtil.isNotBlank(dto.getValue()), SysRoleEntity::getValue, CharSequenceUtil.cleanBlank(dto.getValue())) + // 不显示特定角色 + .notIn(CollUtil.isNotEmpty(invisibleRoleIds), SysRoleEntity::getId, invisibleRoleIds) // 排序 .orderByDesc(SysRoleEntity::getCreatedAt) ); - return this.entityPage2BOPage(entityPage); + return this.entityPage2BOPage(entityPage, true); } /** @@ -82,7 +88,7 @@ public SysRoleBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessEx SysErrorEnum.INVALID_ID.assertNotNull(entity); } - return this.entity2BO(entity); + return this.entity2BO(entity, true); } /** @@ -93,6 +99,7 @@ public SysRoleBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessEx @Transactional(rollbackFor = Exception.class) public Long adminInsert(AdminInsertOrUpdateSysRoleDTO dto) { log.info("[后台管理-新增后台角色] >> 入参={}", dto); + preInsertOrUpdateCheck(dto); this.checkExistence(dto); dto.setId(null); @@ -110,8 +117,11 @@ public Long adminInsert(AdminInsertOrUpdateSysRoleDTO dto) { @Transactional(rollbackFor = Exception.class) public void adminUpdate(AdminInsertOrUpdateSysRoleDTO dto) { log.info("[后台管理-编辑后台角色] >> 入参={}", dto); + preInsertOrUpdateCheck(dto); this.checkExistence(dto); + // 暂不检查该角色是否为当前用户关联的角色 + SysRoleEntity entity = new SysRoleEntity(); BeanUtil.copyProperties(dto, entity); @@ -124,6 +134,7 @@ public void adminUpdate(AdminInsertOrUpdateSysRoleDTO dto) { @Transactional(rollbackFor = Exception.class) public void adminDelete(Collection ids) { log.info("[后台管理-删除后台角色] >> 入参={}", ids); + preDeleteCheck(ids); sysRoleMapper.deleteBatchIds(ids); } @@ -134,12 +145,125 @@ public void adminDelete(Collection ids) { */ @Transactional(rollbackFor = Exception.class) public Set adminBindMenus(AdminBindRoleMenuRelationDTO dto) { + preBindRoleMenuRelationCheck(dto); Set newPermissions = sysMenuService.listPermissionsByMenuIds(dto.getMenuIds()); sysRoleMenuRelationService.cleanAndBind(dto.getRoleId(), dto.getMenuIds()); return newPermissions; } + /** + * 后台管理-下拉框数据 + */ + public List adminSelectOptions() { + Set invisibleRoleIds = determineInvisibleRoleIds(); + List entityList = sysRoleMapper.selectList( + new QueryWrapper() + .lambda() + // 只取特定字段 + .select(SysRoleEntity::getId, SysRoleEntity::getTitle) + // 不显示特定角色 + .notIn(CollUtil.isNotEmpty(invisibleRoleIds), SysRoleEntity::getId, invisibleRoleIds) + // 排序 + .orderByAsc(SysRoleEntity::getId) + ); + // 无需填充菜单IDs + return entityList2BOs(entityList, false); + } + + /** + * 后台管理-删除指定租户的特定角色 + * @param tenantIds 租户IDs,非主键ID,不能为空 + * @param roleValues 角色值集合,可以为空 + */ + @Transactional(rollbackFor = Exception.class) + public void adminDeleteTenantRoles(Collection tenantIds, Collection roleValues) { + if (CollUtil.isEmpty(tenantIds)) { + return; + } + + sysRoleMapper.delete( + new QueryWrapper() + .lambda() + // 租户ID + .in(SysRoleEntity::getTenantId, tenantIds) + // 值相符 + .in(CollUtil.isNotEmpty(roleValues), SysRoleEntity::getValue, roleValues) + ); + } + + /** + * 取用户ID拥有角色对应的 角色ID-角色名 map + * + * @param userId 用户ID + * @return 失败返回空 map + */ + public Map getRoleMapByUserId(Long userId) { + Set roleIds = sysUserRoleRelationService.listRoleIdsByUserId(userId); + + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptyMap(); + } + + // 根据角色Ids取 map + return sysRoleMapper.selectList( + new QueryWrapper() + .lambda() + .select(SysRoleEntity::getId, SysRoleEntity::getValue) + .in(SysRoleEntity::getId, roleIds) + ).stream().collect(Collectors.toMap(SysRoleEntity::getId, SysRoleEntity::getValue, StreamFunction.ignoredThrowingMerger())); + } + + /** + * 取当前用户关联角色信息 + * 仅内部使用 + */ + protected UserRoleContainer getCurrentUserRoleContainer() { + return getSpecifiedUserRoleContainer(UserContextHolder.getUserId()); + } + + /** + * 取指定用户关联角色信息 + * 仅内部使用 + */ + protected UserRoleContainer getSpecifiedUserRoleContainer(Long specifiedUserId) { + Set currentUserRoleIds = sysUserRoleRelationService.listRoleIdsByUserId(specifiedUserId); + List currentUserRoles = Collections.emptyList(); + if (CollUtil.isNotEmpty(currentUserRoleIds)) { + currentUserRoles = sysRoleMapper.selectBatchIds(currentUserRoleIds); + } + return new UserRoleContainer(currentUserRoleIds, currentUserRoles); + } + + /** + * 确定不可见角色IDs + * 仅内部使用 + * 租户管理员:列表中不显示超级管理员角色 + * 普通角色:列表中不显示超级管理员、租户管理员角色 + * @return mutable Set,支持外部改变元素 + */ + protected Set determineInvisibleRoleIds() { + UserRoleContainer currentUser = getCurrentUserRoleContainer(); + // 超级管理员:不限制 + if (currentUser.isSuperAdmin()) { + return new HashSet<>(); + } + // 租户管理员:列表中不显示超级管理员角色 + if (currentUser.isTenantAdmin()) { + return CollUtil.newHashSet(SysConstant.SUPER_ADMIN_ROLE_ID); + } + // 普通角色:列表中不显示超级管理员、租户管理员角色 + Set ret = sysRoleMapper.selectList( + new QueryWrapper() + .lambda() + // 仅取主键ID + .select(SysRoleEntity::getId) + // 值相符 + .eq(SysRoleEntity::getValue, SysConstant.TENANT_ADMIN_ROLE_VALUE) + ).stream().map(SysRoleEntity::getId).collect(Collectors.toSet()); + ret.add(SysConstant.SUPER_ADMIN_ROLE_ID); + return ret; + } /* ---------------------------------------------------------------- @@ -151,9 +275,10 @@ public Set adminBindMenus(AdminBindRoleMenuRelationDTO dto) { * 实体转 BO * * @param entity 实体 + * @param fillMenuIds 是否根据实体ID,查询关联菜单IDs并填充到BO * @return BO */ - private SysRoleBO entity2BO(SysRoleEntity entity) { + private SysRoleBO entity2BO(SysRoleEntity entity, boolean fillMenuIds) { if (entity == null) { return null; } @@ -162,7 +287,9 @@ private SysRoleBO entity2BO(SysRoleEntity entity) { BeanUtil.copyProperties(entity, bo); // 可以在此处为BO填充字段 - bo.setMenuIds(sysRoleMenuRelationService.listMenuIdsByRoleIds(Collections.singleton(bo.getId()))); + if (fillMenuIds) { + bo.setMenuIds(sysRoleMenuRelationService.listMenuIdsByRoleIds(Collections.singleton(bo.getId()))); + } return bo; } @@ -170,13 +297,14 @@ private SysRoleBO entity2BO(SysRoleEntity entity) { * 实体 List 转 BO List * * @param entityList 实体 List + * @param fillMenuIds 是否根据实体ID,查询关联菜单IDs并填充到BO * @return BO List */ - private List entityList2BOs(List entityList) { + private List entityList2BOs(List entityList, boolean fillMenuIds) { // 深拷贝 List ret = new ArrayList<>(entityList.size()); entityList.forEach( - entity -> ret.add(this.entity2BO(entity)) + entity -> ret.add(this.entity2BO(entity, fillMenuIds)) ); return ret; @@ -186,15 +314,16 @@ private List entityList2BOs(List entityList) { * 实体分页转 BO 分页 * * @param entityPage 实体分页 + * @param fillMenuIds 是否根据实体ID,查询关联菜单IDs并填充到BO * @return BO 分页 */ - private PageResult entityPage2BOPage(Page entityPage) { + private PageResult entityPage2BOPage(Page entityPage, boolean fillMenuIds) { return new PageResult() .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + // 需填充菜单IDs + .setRecords(this.entityList2BOs(entityPage.getRecords(), fillMenuIds)); } /** @@ -216,27 +345,98 @@ private void checkExistence(AdminInsertOrUpdateSysRoleDTO dto) { if (existingEntity != null && !existingEntity.getId().equals(dto.getId())) { throw new BusinessException(400, "已存在相同后台角色,请重新输入"); } + + if (dto.creatingNewTenantAdmin()) { + long qty = sysRoleMapper.selectCount( + new QueryWrapper() + .lambda() + // 租户ID相同 + .eq(SysRoleEntity::getTenantId, dto.getTenantId()) + // 角色编码相同 + .eq(SysRoleEntity::getValue, dto.getValue()) + .last(HelioConstant.CRUD.SQL_LIMIT_1) + ); + SysErrorEnum.NEED_DELETE_EXISTING_TENANT_ADMIN_ROLE.assertTrue(qty <= 0L, dto.getTenantId()); + } } /** - * 取用户ID拥有角色对应的 角色ID-角色名 map - * - * @param userId 用户ID - * @return 失败返回空 map + * 新增/编辑后台角色信息前检查 */ - public Map getRoleMapByUserId(Long userId) { - Set roleIds = sysUserRoleRelationService.listRoleIdsByUserId(userId); + private void preInsertOrUpdateCheck(AdminInsertOrUpdateSysRoleDTO dto) { + if (SysConstant.SUPER_ADMIN_ROLE_VALUE.equalsIgnoreCase(dto.getValue())) { + // 角色编码不能为SuperAdmin + throw new BusinessException(SysErrorEnum.ROLE_VALUE_CANNOT_BE, SysConstant.SUPER_ADMIN_ROLE_VALUE); + } + if (SysConstant.TENANT_ADMIN_ROLE_VALUE.equalsIgnoreCase(dto.getValue()) && !dto.creatingNewTenantAdmin()) { + // 除非是新增租户时,同时新增租户管理员角色,否则角色编码不能为Admin + throw new BusinessException(SysErrorEnum.ROLE_VALUE_CANNOT_BE, SysConstant.TENANT_ADMIN_ROLE_VALUE); + } - if (CollUtil.isEmpty(roleIds)) { - return Collections.emptyMap(); + boolean isUpdating = Objects.nonNull(dto.getId()); + if (isUpdating) { + SysRoleEntity existingRole = sysRoleMapper.selectById(dto.getId()); + SysErrorEnum.INVALID_ID.assertNotNull(existingRole); + if (existingRole.isSuperAdmin() || existingRole.isTenantAdmin()) { + // 原来角色编码为SuperAdmin或Admin的,不能被改变 + throw new BusinessException(SysErrorEnum.ROLE_VALUE_CANNOT_BE, existingRole.getValue()); + } } + } - // 根据角色Ids取 map - return sysRoleMapper.selectList( - new QueryWrapper() - .lambda() - .select(SysRoleEntity::getId, SysRoleEntity::getValue) - .in(SysRoleEntity::getId, roleIds) - ).stream().collect(Collectors.toMap(SysRoleEntity::getId, SysRoleEntity::getValue, StreamFunction.ignoredThrowingMerger())); + /** + * 删除后台角色前检查 + */ + private void preDeleteCheck(Collection ids) { + if (CollUtil.contains(ids, SysConstant.SUPER_ADMIN_ROLE_ID)) { + throw new BusinessException(SysErrorEnum.CANNOT_DELETE_SUPER_ADMIN_ROLE); + } + + List existingEntityList = sysRoleMapper.selectBatchIds(ids); + for (SysRoleEntity item : existingEntityList) { + if (item.isSuperAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_DELETE_SUPER_ADMIN_ROLE); + } + + if (item.isTenantAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_DELETE_TENANT_ADMIN_ROLE); + } + } + + UserRoleContainer currentUser = getCurrentUserRoleContainer(); + if (CollUtil.containsAny(currentUser.getRelatedRoleIds(), ids)) { + throw new BusinessException(SysErrorEnum.CANNOT_DELETE_SELF_ROLE); + } + } + + /** + * 绑定后台角色与菜单关联关系前检查 + * 防止越权访问漏洞 + */ + private void preBindRoleMenuRelationCheck(AdminBindRoleMenuRelationDTO dto) { + UserRoleContainer currentUser = getCurrentUserRoleContainer(); + if (SysConstant.SUPER_ADMIN_ROLE_ID.equals(dto.getRoleId())) { + throw new BusinessException(SysErrorEnum.CANNOT_BIND_MENUS_FOR_SUPER_ADMIN_ROLE); + } + + if (CollUtil.contains(currentUser.getRelatedRoleIds(), dto.getRoleId())) { + // 不能动自身角色 + throw new BusinessException(SysErrorEnum.CANNOT_BIND_MENUS_FOR_SELF); + } + + // 有且只有当前用户为超级管理员,才可以为租户管理员绑定菜单 + SysRoleEntity targetRole = sysRoleMapper.selectById(dto.getRoleId()); + if (targetRole.isTenantAdmin() && !currentUser.isSuperAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_BIND_MENUS_FOR_TENANT_ADMIN_ROLE); + } + + if (CollUtil.isNotEmpty(dto.getMenuIds()) && !currentUser.isSuperAdmin()) { + // 超级管理员之外的角色,都需要校验自身菜单范围是否满足输入值 + Set visibleMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(currentUser.getRelatedRoleIds()); + if (!CollUtil.containsAll(visibleMenuIds, dto.getMenuIds())) { + // 可能存在超自身权限赋权 + throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_MENUS); + } + } } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java b/src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java index 933d57d..d466c7d 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java @@ -13,6 +13,7 @@ import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO; import cc.uncarbon.module.sys.model.response.SysTenantBO; import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; @@ -24,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; @@ -58,7 +60,7 @@ public PageResult adminList(PageParam pageParam, AdminListSysTenant .orderByDesc(SysTenantEntity::getCreatedAt) ); - return this.entityPage2BOPage(entityPage); + return this.entityPage2BOPage(entityPage, true); } /** @@ -84,7 +86,7 @@ public SysTenantBO getOneById(Long id, boolean throwIfInvalidId) throws Business SysErrorEnum.INVALID_ID.assertNotNull(entity); } - return this.entity2BO(entity); + return this.entity2BO(entity, true); } /** @@ -108,7 +110,6 @@ public SysTenantEntity adminInsert(AdminInsertSysTenantDTO dto) { @Transactional(rollbackFor = Exception.class) public void adminUpdate(AdminUpdateSysTenantDTO dto) { log.info("[后台管理-编辑系统租户] >> 入参={}", dto); - this.checkExistence(dto); SysTenantEntity entity = new SysTenantEntity(); BeanUtil.copyProperties(dto, entity); @@ -151,7 +152,7 @@ public SysTenantEntity getTenantEntityByTenantId(Long tenantId) { * * @param dto DTO */ - public void checkExistence(AdminUpdateSysTenantDTO dto) { + public void checkExistence(AdminInsertSysTenantDTO dto) { SysTenantEntity existingEntity = sysTenantMapper.selectOne( new QueryWrapper() .lambda() @@ -170,6 +171,18 @@ public void checkExistence(AdminUpdateSysTenantDTO dto) { } } + /** + * 根据主键IDs,取租户BOs + * @param fillTenantAdminUser 是否根据租户管理员用户ID,查询关联用户信息并填充到BO + */ + public List listByIds(Collection ids, boolean fillTenantAdminUser) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List entityList = sysTenantMapper.selectBatchIds(ids); + return entityList2BOs(entityList, fillTenantAdminUser); + } + /* ---------------------------------------------------------------- 私有方法 private methods @@ -180,9 +193,10 @@ public void checkExistence(AdminUpdateSysTenantDTO dto) { * 实体转 BO * * @param entity 实体 + * @param fillTenantAdminUser 是否根据租户管理员用户ID,查询关联用户信息并填充到BO * @return BO */ - private SysTenantBO entity2BO(SysTenantEntity entity) { + private SysTenantBO entity2BO(SysTenantEntity entity, boolean fillTenantAdminUser) { if (entity == null) { return null; } @@ -191,10 +205,8 @@ private SysTenantBO entity2BO(SysTenantEntity entity) { BeanUtil.copyProperties(entity, bo); // 可以在此处为BO填充字段 - if (ObjectUtil.isNotNull(entity.getTenantAdminUserId())) { - bo - .setTenantAdminUser(sysUserMapper.getBaseInfoByUserId(entity.getTenantAdminUserId())) - ; + if (fillTenantAdminUser && ObjectUtil.isNotNull(entity.getTenantAdminUserId())) { + bo.setTenantAdminUser(sysUserMapper.getBaseInfoByUserId(entity.getTenantAdminUserId())); } return bo; @@ -204,13 +216,14 @@ private SysTenantBO entity2BO(SysTenantEntity entity) { * 实体 List 转 BO List * * @param entityList 实体 List + * @param fillTenantAdminUser 是否根据租户管理员用户ID,查询关联用户信息并填充到BO * @return BO List */ - private List entityList2BOs(List entityList) { + private List entityList2BOs(List entityList, boolean fillTenantAdminUser) { // 深拷贝 List ret = new ArrayList<>(entityList.size()); entityList.forEach( - entity -> ret.add(this.entity2BO(entity)) + entity -> ret.add(this.entity2BO(entity, fillTenantAdminUser)) ); return ret; @@ -222,13 +235,12 @@ private List entityList2BOs(List entityList) { * @param entityPage 实体分页 * @return BO 分页 */ - private PageResult entityPage2BOPage(Page entityPage) { + private PageResult entityPage2BOPage(Page entityPage, boolean fillTenantAdminUser) { return new PageResult() .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords(), fillTenantAdminUser)); } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java b/src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java index 0052976..52c9603 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java @@ -3,6 +3,7 @@ import cc.uncarbon.framework.core.constant.HelioConstant; import cc.uncarbon.module.sys.entity.SysUserDeptRelationEntity; import cc.uncarbon.module.sys.mapper.SysUserDeptRelationMapper; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.RequiredArgsConstructor; @@ -10,8 +11,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** @@ -28,7 +32,7 @@ public class SysUserDeptRelationService { /** * 列举用户ID关联的部门IDs * - * @return 联的部门IDs;目前最多只有1个元素 + * @return 关联的部门IDs;目前最多只有1个元素 */ public List getUserDeptIds(Long userId) { SysUserDeptRelationEntity entity = sysUserDeptRelationMapper.selectOne( @@ -67,4 +71,21 @@ public void cleanAndBind(Long userId, Long deptId) { } + /** + * 列举部门IDs关联的用户IDs + */ + public Set listUserIdsByDeptIds(Collection deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptySet(); + } + + return sysUserDeptRelationMapper.selectList( + new QueryWrapper() + .lambda() + // 只要用户ID + .select(SysUserDeptRelationEntity::getUserId) + .in(SysUserDeptRelationEntity::getDeptId, deptIds) + ).stream().map(SysUserDeptRelationEntity::getUserId).collect(Collectors.toSet()); + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java b/src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java index 27a4713..404f563 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java @@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; @@ -83,4 +84,23 @@ public Set listRoleIdsByUserId(Long userId) throws IllegalArgumentExceptio .eq(SysUserRoleRelationEntity::getUserId, userId) ).stream().map(SysUserRoleRelationEntity::getRoleId).collect(Collectors.toSet()); } + + /** + * 取角色IDs关联的用户IDs + * + * @param roleIds 角色IDs + * @return 空集合or用户IDs + */ + public Set listUserIdsByRoleIds(Collection roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptySet(); + } + + return sysUserRoleRelationMapper.selectList( + new QueryWrapper() + .lambda() + .select(SysUserRoleRelationEntity::getUserId) + .in(SysUserRoleRelationEntity::getRoleId, roleIds) + ).stream().map(SysUserRoleRelationEntity::getUserId).collect(Collectors.toSet()); + } } diff --git a/src/main/java/cc/uncarbon/module/sys/service/SysUserService.java b/src/main/java/cc/uncarbon/module/sys/service/SysUserService.java index ea20dbf..c9ea5b8 100644 --- a/src/main/java/cc/uncarbon/module/sys/service/SysUserService.java +++ b/src/main/java/cc/uncarbon/module/sys/service/SysUserService.java @@ -8,13 +8,15 @@ import cc.uncarbon.framework.core.page.PageParam; import cc.uncarbon.framework.core.page.PageResult; import cc.uncarbon.framework.core.props.HelioProperties; +import cc.uncarbon.module.sys.constant.SysConstant; import cc.uncarbon.module.sys.entity.SysTenantEntity; import cc.uncarbon.module.sys.entity.SysUserEntity; import cc.uncarbon.module.sys.enums.SysErrorEnum; import cc.uncarbon.module.sys.enums.SysUserStatusEnum; import cc.uncarbon.module.sys.mapper.SysUserMapper; +import cc.uncarbon.module.sys.model.interior.UserDeptContainer; +import cc.uncarbon.module.sys.model.interior.UserRoleContainer; import cc.uncarbon.module.sys.model.request.*; -import cc.uncarbon.module.sys.model.response.SysDeptBO; import cc.uncarbon.module.sys.model.response.SysUserBO; import cc.uncarbon.module.sys.model.response.SysUserLoginBO; import cc.uncarbon.module.sys.model.response.VbenAdminUserInfoVO; @@ -29,10 +31,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; +import java.math.BigInteger; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -53,6 +57,7 @@ public class SysUserService { private final SysTenantService sysTenantService; private final SysUserDeptRelationService sysUserDeptRelationService; private final SysUserRoleRelationService sysUserRoleRelationService; + private final SysRoleMenuRelationService sysRoleMenuRelationService; private final HelioProperties helioProperties; private boolean isTenantEnabled; @@ -66,17 +71,41 @@ public void postConstruct() { * 后台管理-分页列表 */ public PageResult adminList(PageParam pageParam, AdminListSysUserDTO dto) { + // 预处理:根据【手动选择的部门】筛选用户 + Set deptUserIds = Collections.emptySet(); + if (dto.needFilterBySelectedDeptId()) { + deptUserIds = sysUserDeptRelationService.listUserIdsByDeptIds(Collections.singleton(dto.getSelectedDeptId())); + if (CollUtil.isEmpty(deptUserIds)) { + // 【手动选择的部门】没有任何用户ID,直接返回空列表 + return new PageResult<>(pageParam); + } + } + + // 预处理:根据【只能看到本部门及下级部门原则】筛选用户 + Set visibleUserIds = determineVisibleDeptUserIds(); + if (Objects.equals(CollUtil.getFirst(visibleUserIds), BigInteger.ZERO.longValue())) { + // 其实啥也看不到…… + return new PageResult<>(pageParam); + } + + Set invisibleUserIds = determineInvisibleUserIds(); Page entityPage = sysUserMapper.selectPage( new Page<>(pageParam.getPageNum(), pageParam.getPageSize()), new QueryWrapper() .lambda() // 手机号 .like(CharSequenceUtil.isNotBlank(dto.getPhoneNo()), SysUserEntity::getPhoneNo, CharSequenceUtil.cleanBlank(dto.getPhoneNo())) + // 根据【手动选择的部门ID】筛选用户 + .in(CollUtil.isNotEmpty(deptUserIds), SysUserEntity::getId, deptUserIds) + // 根据【只能看到本部门及下级部门原则】筛选用户 + .in(CollUtil.isNotEmpty(visibleUserIds), SysUserEntity::getId, visibleUserIds) + // 不显示特定用户 + .notIn(CollUtil.isNotEmpty(invisibleUserIds), SysUserEntity::getId, invisibleUserIds) // 排序 .orderByDesc(SysUserEntity::getCreatedAt) ); - return this.entityPage2BOPage(entityPage); + return this.entityPage2BOPage(entityPage, true); } /** @@ -97,12 +126,14 @@ public SysUserBO getOneById(Long id) { * @return null or BO */ public SysUserBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException { + dataScopeCheck(Collections.singleton(id)); + SysUserEntity entity = sysUserMapper.selectById(id); if (throwIfInvalidId) { SysErrorEnum.INVALID_ID.assertNotNull(entity); } - return this.entity2BO(entity); + return this.entity2BO(entity, true); } /** @@ -115,6 +146,14 @@ public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) { log.info("[后台管理-新增后台用户] >> 入参={}", dto); this.checkExistence(dto); + if (Objects.nonNull(dto.getDeptId())) { + // 对传入的部门ID,做数据越权检查 + UserDeptContainer deptContainer = sysDeptService.getCurrentUserDeptContainer(true); + if (deptContainer.hasVisibleDepts() && !CollUtil.contains(deptContainer.getVisibleDeptIds(), dto.getDeptId())) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + } + dto.setId(null); SysUserEntity entity = new SysUserEntity(); BeanUtil.copyProperties(dto, entity); @@ -123,8 +162,7 @@ public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) { entity .setSalt(salt) .setPin(dto.getUsername()) - .setPwd(PwdUtil.encrypt(dto.getPasswordOfNewUser(), salt)) - ; + .setPwd(PwdUtil.encrypt(dto.getPasswordOfNewUser(), salt)); sysUserMapper.insert(entity); @@ -139,16 +177,17 @@ public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) { @Transactional(rollbackFor = Exception.class) public void adminUpdate(AdminInsertOrUpdateSysUserDTO dto) { log.info("[后台管理-编辑后台用户] >> 入参={}", dto); + preUpdateCheck(dto.getId(), dto.getStatus()); this.checkExistence(dto); - SysUserEntity updateEntity = new SysUserEntity(); - BeanUtil.copyProperties(dto, updateEntity); + SysUserEntity entity = new SysUserEntity(); + BeanUtil.copyProperties(dto, entity); // 手动处理异名字段 - updateEntity.setPin(dto.getUsername()); + entity.setPin(dto.getUsername()); sysUserDeptRelationService.cleanAndBind(dto.getId(), dto.getDeptId()); - sysUserMapper.updateById(updateEntity); + sysUserMapper.updateById(entity); } /** @@ -157,6 +196,7 @@ public void adminUpdate(AdminInsertOrUpdateSysUserDTO dto) { @Transactional(rollbackFor = Exception.class) public void adminDelete(Collection ids) { log.info("[后台管理-删除后台用户] >> 入参={}", ids); + preDeleteCheck(ids); sysUserMapper.deleteBatchIds(ids); } @@ -166,7 +206,6 @@ public void adminDelete(Collection ids) { public SysUserLoginBO adminLogin(SysUserLoginDTO dto) { /* 如果启用了多租户功能,并且前端指定了租户ID,则先查库确认租户是否有效 - 注意:数据源级多租户,登录前【必须】主动指定租户ID,如: dto.setTenantId(101L) */ // ConcurrentHashMap 的 value 不能为 null,还是 new 一个吧 @@ -205,7 +244,7 @@ public SysUserLoginBO adminLogin(SysUserLoginDTO dto) { this.updateLastLoginAt(sysUserEntity.getId(), LocalDateTimeUtil.now()); // 取账号完整信息 - SysUserBO sysUserBO = this.entity2BO(sysUserEntity); + SysUserBO sysUserBO = this.entity2BO(sysUserEntity, false); Map roleMap = sysRoleService.getRoleMapByUserId(sysUserBO.getId()); Map> roleIdPermissionMap = sysMenuService.getRoleIdPermissionMap(roleMap.keySet()); @@ -221,8 +260,7 @@ public SysUserLoginBO adminLogin(SysUserLoginDTO dto) { .setRoles(new ArrayList<>(roleMap.values())) .setPermissions(permissions) .setRoleIdPermissionMap(roleIdPermissionMap) - .setTenantContext(tenantContext) - ; + .setTenantContext(tenantContext); return ret; } @@ -243,13 +281,13 @@ public VbenAdminUserInfoVO adminGetCurrentUserInfo() { * 后台管理-重置某用户密码 */ public void adminResetUserPassword(AdminResetSysUserPasswordDTO dto) { + preUpdateCheck(dto.getUserId(), null); SysUserEntity sysUserEntity = sysUserMapper.selectById(dto.getUserId()); SysUserEntity templateEntity = new SysUserEntity(); templateEntity .setPwd(PwdUtil.encrypt(dto.getRandomPassword(), sysUserEntity.getSalt())) - .setId(dto.getUserId()) - ; + .setId(dto.getUserId()); sysUserMapper.updateById(templateEntity); } @@ -265,8 +303,7 @@ public void adminUpdateCurrentUserPassword(AdminUpdateCurrentSysUserPasswordDTO sysUserEntity .setPwd(PwdUtil.encrypt(dto.getConfirmNewPassword(), sysUserEntity.getSalt())) - .setId(UserContextHolder.getUserId()) - ; + .setId(UserContextHolder.getUserId()); sysUserMapper.updateById(sysUserEntity); } @@ -275,6 +312,7 @@ public void adminUpdateCurrentUserPassword(AdminUpdateCurrentSysUserPasswordDTO * 后台管理-绑定用户与角色关联关系 */ public void adminBindRoles(AdminBindUserRoleRelationDTO dto) { + preBindUserRoleRelationCheck(dto); sysUserRoleRelationService.cleanAndBind(dto.getUserId(), dto.getRoleIds()); } @@ -294,10 +332,29 @@ public Set listRelatedRoleIds(Long userId) { if (ObjectUtil.isNull(userId)) { return Collections.emptySet(); } - return sysRoleService.getRoleMapByUserId(userId).keySet(); } + /** + * 后台管理 - 取租户用户IDs + * @param tenantId 租户ID,非主键ID + * @param statusEnums 仅保留符合指定状态的,可以为null + */ + public List listUserIdsByTenantId(Long tenantId, Collection statusEnums) { + if (Objects.isNull(tenantId)) { + return Collections.emptyList(); + } + // 备份原始租户上下文;以下查询方式可同时兼容行级、数据源级多租户 + TenantContext originContext = TenantContextHolder.getTenantContext(); + try { + // 临时切换租户 + TenantContextHolder.setTenantContext(new TenantContext(tenantId, CharSequenceUtil.EMPTY)); + return sysUserMapper.selectIds(statusEnums); + } finally { + TenantContextHolder.setTenantContext(originContext); + } + } + /* ---------------------------------------------------------------- 私有方法 private methods @@ -308,9 +365,10 @@ public Set listRelatedRoleIds(Long userId) { * 实体转 BO * * @param entity 实体 + * @param fillDeptInfo 是否根据实体部门ID,查询关联部门信息并填充到BO * @return BO */ - private SysUserBO entity2BO(SysUserEntity entity) { + private SysUserBO entity2BO(SysUserEntity entity, boolean fillDeptInfo) { if (entity == null) { return null; } @@ -320,14 +378,11 @@ private SysUserBO entity2BO(SysUserEntity entity) { // 可以在此处为BO填充字段 bo.setUsername(entity.getPin()); - SysDeptBO dept = sysDeptService.getPlainDeptByUserId(bo.getId()); - if (dept != null) { - bo - .setDeptId(dept.getId()) - .setDeptTitle(dept.getTitle()) - ; + if (fillDeptInfo) { + Optional.ofNullable(sysDeptService.getSpecifiedUserDeptContainer(bo.getId(), false)) + .map(UserDeptContainer::primaryRelatedDept) + .ifPresent(deptInfo -> bo.setDeptId(deptInfo.getId()).setDeptTitle(deptInfo.getTitle())); } - return bo; } @@ -335,9 +390,10 @@ private SysUserBO entity2BO(SysUserEntity entity) { * 实体 List 转 BO List * * @param entityList 实体 List + * @param fillDeptInfo 是否根据实体部门ID,查询关联部门信息并填充到BO * @return BO List */ - private List entityList2BOs(List entityList) { + private List entityList2BOs(List entityList, boolean fillDeptInfo) { if (CollUtil.isEmpty(entityList)) { return Collections.emptyList(); } @@ -345,7 +401,7 @@ private List entityList2BOs(List entityList) { // 深拷贝 List ret = new ArrayList<>(entityList.size()); entityList.forEach( - entity -> ret.add(this.entity2BO(entity)) + entity -> ret.add(this.entity2BO(entity, fillDeptInfo)) ); return ret; @@ -355,15 +411,15 @@ private List entityList2BOs(List entityList) { * 实体分页转 BO 分页 * * @param entityPage 实体分页 + * @param fillDeptInfo 是否根据实体部门ID,查询关联部门信息并填充到BO * @return BO 分页 */ - private PageResult entityPage2BOPage(Page entityPage) { + private PageResult entityPage2BOPage(Page entityPage, boolean fillDeptInfo) { return new PageResult() .setCurrent(entityPage.getCurrent()) .setSize(entityPage.getSize()) .setTotal(entityPage.getTotal()) - .setRecords(this.entityList2BOs(entityPage.getRecords())) - ; + .setRecords(this.entityList2BOs(entityPage.getRecords(), fillDeptInfo)); } /** @@ -379,6 +435,46 @@ private void checkExistence(AdminInsertOrUpdateSysUserDTO dto) { } } + /** + * 确定本部门及下级部门用户IDs + * 返回空集合代表不限制 + * 返回[0]或有元素集合,表示有限制 + */ + private Set determineVisibleDeptUserIds() { + Set visibleUserIds = Collections.emptySet(); + UserDeptContainer deptContainer = sysDeptService.getCurrentUserDeptContainer(true); + if (deptContainer.hasVisibleDepts()) { + visibleUserIds = sysUserDeptRelationService.listUserIdsByDeptIds(deptContainer.getVisibleDeptIds()); + if (CollUtil.isEmpty(visibleUserIds)) { + // 【可见部门】没有任何用户ID,直接返回[0] + return Collections.singleton(BigInteger.ZERO.longValue()); + } + } + return visibleUserIds; + } + + /** + * 确定不可见用户IDs + * 租户管理员:列表中不显示超级管理员用户 + * 普通用户:列表中不显示超级管理员、租户管理员用户 + */ + private Set determineInvisibleUserIds() { + Set invisibleRoleIds = sysRoleService.determineInvisibleRoleIds(); + return sysUserRoleRelationService.listUserIdsByRoleIds(invisibleRoleIds); + } + + /** + * 数据越权检查 + */ + private void dataScopeCheck(Collection userIds) { + Set visibleUserIds = determineVisibleDeptUserIds(); + Set invisibleUserIds = determineInvisibleUserIds(); + if (CollUtil.isNotEmpty(visibleUserIds) && !CollUtil.containsAll(visibleUserIds, userIds) + || CollUtil.isNotEmpty(invisibleUserIds) && CollUtil.containsAny(invisibleUserIds, userIds)) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + } + /** * 检查并获取租户上下文 bean,无效或被禁用则直接抛出异常 * @param tenantId 租户ID @@ -398,16 +494,136 @@ private TenantContext checkAndGetTenantContext(Long tenantId) throws BusinessExc return TenantContext.builder() .tenantId(tenantEntity.getTenantId()) .tenantName(tenantEntity.getTenantName()) - .build() - ; + .build(); } private void updateLastLoginAt(Long userId, LocalDateTime lastLoginAt) { SysUserEntity entity = new SysUserEntity(); entity .setLastLoginAt(lastLoginAt) - .setId(userId) - ; + .setId(userId); sysUserMapper.updateById(entity); } + + /** + * 编辑后台用户信息前检查 + * @param specifiedUserId 被操作用户ID + * @param statusEnum 用户状态枚举,可以为null + */ + private void preUpdateCheck(Long specifiedUserId, @Nullable SysUserStatusEnum statusEnum) { + UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); + if (currentUser.isSuperAdmin()) { + // 超级管理员除禁用自己外为所欲为 + if (statusEnum == SysUserStatusEnum.BANNED && Objects.equals(specifiedUserId, UserContextHolder.getUserId())) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); + } + return; + } + + if (Objects.equals(specifiedUserId, UserContextHolder.getUserId())) { + // 不能动自身用户 + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); + } + + // 目标是超级管理员or租户管理员时,均不能编辑 + UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(specifiedUserId); + if (specifiedUser.isSuperAdmin() || specifiedUser.isTenantAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + + dataScopeCheck(Collections.singleton(specifiedUserId)); + // 暂未实现角色层级,一律平级 + } + + /** + * 删除后台用户前检查 + */ + private void preDeleteCheck(Collection ids) { + if (CollUtil.contains(ids, UserContextHolder.getUserId())) { + // 不能动自身用户 + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); + } + + // 目标是超级管理员时,不能删除 + List specifiedUsers = ids.stream().map(sysRoleService::getSpecifiedUserRoleContainer).collect(Collectors.toList()); + if (specifiedUsers.stream().anyMatch(UserRoleContainer::isSuperAdmin)) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + + // 只有超级管理员可以删租户管理员用户 + UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); + if (specifiedUsers.stream().anyMatch(UserRoleContainer::isTenantAdmin) && !currentUser.isSuperAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + + dataScopeCheck(ids); + // 暂未实现角色层级,一律平级 + } + + /** + * 绑定后台用户与角色关联关系前检查 + * 防止越权访问漏洞 + */ + private void preBindUserRoleRelationCheck(AdminBindUserRoleRelationDTO dto) { + UserRoleContainer currentUser = sysRoleService.getCurrentUserRoleContainer(); + // 是否对自己操作 + boolean selfFlag = Objects.equals(dto.getUserId(), UserContextHolder.getUserId()); + if (currentUser.isSuperAdmin()) { + // 超级管理员不能去掉自己的超级管理员角色 + if (selfFlag && !CollUtil.contains(dto.getRoleIds(), SysConstant.SUPER_ADMIN_ROLE_ID)) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); + } + // 也不能赋予其他人超级管理员角色 + if (!selfFlag && CollUtil.contains(dto.getRoleIds(), SysConstant.SUPER_ADMIN_ROLE_ID)) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + return; + } + + if (selfFlag) { + // 不能动自身用户 + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_SELF_USER); + } + + // 目标已经是超级管理员or租户管理员时,均不能绑定 + UserRoleContainer specifiedUser = sysRoleService.getSpecifiedUserRoleContainer(dto.getUserId()); + if (specifiedUser.isSuperAdmin() || specifiedUser.isTenantAdmin()) { + throw new BusinessException(SysErrorEnum.CANNOT_OPERATE_THIS_USER); + } + + // 超级管理员之外的用户,都需要校验自身角色范围是否满足输入值 + currentUserNotSuperAdmin(dto, currentUser); + + dataScopeCheck(Collections.singleton(dto.getUserId())); + } + + /** + * 绑定后台用户与角色关联关系前检查 + * 超级管理员之外的用户,都需要校验自身角色范围是否满足输入值 + * 拆分子方法以降低Cognitive Complexity + */ + private void currentUserNotSuperAdmin(AdminBindUserRoleRelationDTO dto, UserRoleContainer currentUser) { + if (CollUtil.isNotEmpty(dto.getRoleIds()) && !currentUser.isSuperAdmin()) { + boolean overRoles = !CollUtil.containsAll(currentUser.getRelatedRoleIds(), dto.getRoleIds()); + if (overRoles && currentUser.isNotAnyAdmin()) { + // 普通用户超自身角色授予了;如果当前用户拥有新角色的所有菜单,那么也放行 + Set grantedMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(currentUser.getRelatedRoleIds()); + Set needMenuIds = sysRoleMenuRelationService.listMenuIdsByRoleIds(dto.getRoleIds()); + if (!CollUtil.containsAll(grantedMenuIds, needMenuIds)) { + throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES); + } + } + + if (currentUser.isTenantAdmin()) { + // 超自身权限,但作为租户管理员有额外情况 + Set invisibleRoleIds = sysRoleService.determineInvisibleRoleIds(); + // 除非超越了可见角色IDs授予 or 想要授予用户租户管理员角色,否则不管 + invisibleRoleIds.addAll(currentUser.getRelatedRoleIds()); + if (CollUtil.containsAny(invisibleRoleIds, dto.getRoleIds())) { + throw new BusinessException(SysErrorEnum.BEYOND_AUTHORITY_BIND_ROLES); + } + } + } + } + } diff --git a/src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java b/src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java index f238fab..b4b7b0e 100644 --- a/src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java +++ b/src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java @@ -11,7 +11,7 @@ @UtilityClass public class PwdUtil { public static String encrypt(String str, String salt) { - if (CharSequenceUtil.isEmpty(str)){ + if (CharSequenceUtil.isEmpty(str)) { return ""; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eec3277..ea10e6c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -86,7 +86,8 @@ mybatis-plus: logging: level: # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO - cc.uncarbon: DEBUG + cc.uncarbon.framework: DEBUG + cc.uncarbon.module: DEBUG # 详细配置文档:https://sa-token.cc/doc.html#/use/config sa-token: diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index cd4418e..7ef80b4 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -10,4 +10,4 @@ ${AnsiColor.BLUE} ${AnsiColor.GREEN} Spring Boot Version: ${spring-boot.version} -${AnsiColor.BLACK} +${AnsiColor.WHITE} diff --git "a/\345\244\232\346\250\241\345\235\227\347\211\210\350\257\267\345\210\207\346\215\242\350\207\263multi\345\210\206\346\224\257.md" "b/\345\244\232\346\250\241\345\235\227\347\211\210\350\257\267\347\247\273\346\255\245\350\207\263helio-boot-modular\351\241\271\347\233\256.md" similarity index 100% rename from "\345\244\232\346\250\241\345\235\227\347\211\210\350\257\267\345\210\207\346\215\242\350\207\263multi\345\210\206\346\224\257.md" rename to "\345\244\232\346\250\241\345\235\227\347\211\210\350\257\267\347\247\273\346\255\245\350\207\263helio-boot-modular\351\241\271\347\233\256.md"