From 92959efdc66d7363b9efb771fb3e290e412929e3 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Mon, 7 Jul 2025 18:14:26 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E9=99=84=E4=BB=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=90=BA=E5=B8=A6=E9=99=84=E4=BB=B6=E5=AE=9E=E4=BD=93=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E7=9A=84=E6=8E=A5=E5=8F=A3=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/file/FileController.java | 11 +++++++ .../infra/service/file/FileService.java | 5 +++ .../infra/service/file/FileServiceImpl.java | 31 +++++++++++++++---- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index 119774b7..b7177fbb 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -26,6 +26,8 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; @@ -48,6 +50,15 @@ public class FileController { uploadReqVO.getDirectory(), file.getContentType())); } + @PostMapping("/upload-with-return") + @Operation(summary = "上传文件携带返回实体", description = "上传文件携带返回实体") + public CommonResult uploadWithReturn(FileUploadReqVO uploadReqVO) throws IOException { + MultipartFile file = uploadReqVO.getFile(); + byte[] content = IoUtil.readBytes(file.getInputStream()); + return success(fileService.createFileWhitReturn(content, file.getOriginalFilename(), + uploadReqVO.getDirectory(), file.getContentType())); + } + @GetMapping("/presigned-url") @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Parameters({ diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index 01c2bba7..a3976629 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -4,8 +4,10 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import jakarta.validation.constraints.NotEmpty; +import lombok.SneakyThrows; /** * 文件 Service 接口 @@ -34,6 +36,9 @@ public interface FileService { String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, String name, String directory, String type); + @SneakyThrows + FileRespVO createFileWhitReturn(byte[] content, String name, String directory, String type); + /** * 生成文件预签名地址信息 * diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 4a0faada..59876b12 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -10,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileRespVO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; @@ -34,13 +35,11 @@ public class FileServiceImpl implements FileService { /** * 上传文件的前缀,是否包含日期(yyyyMMdd) - * * 目的:按照日期,进行分目录 */ static boolean PATH_PREFIX_DATE_ENABLE = true; /** * 上传文件的后缀,是否包含时间戳 - * * 目的:保证文件的唯一性,避免覆盖 * 定制:可按需调整成 UUID、或者其他方式 */ @@ -60,6 +59,25 @@ public class FileServiceImpl implements FileService { @Override @SneakyThrows public String createFile(byte[] content, String name, String directory, String type) { + FileDO entity = uploadFile(content, name, directory, type); + return entity.getUrl(); + } + + @SneakyThrows + @Override + public FileRespVO createFileWhitReturn(byte[] content, String name, String directory, String type) { + FileDO entity = uploadFile(content, name, directory, type); + return new FileRespVO() + .setId(entity.getId()) + .setName(entity.getName()) + .setPath(entity.getPath()) + .setUrl(entity.getUrl()) + .setType(entity.getType()) + .setSize(entity.getSize()) + .setConfigId(entity.getConfigId()); + } + + private FileDO uploadFile(byte[] content, String name, String directory, String type) throws Exception { // 1.1 处理 type 为空的情况 if (StrUtil.isEmpty(type)) { type = FileTypeUtils.getMineType(content, name); @@ -82,12 +100,13 @@ public class FileServiceImpl implements FileService { FileClient client = fileConfigService.getMasterFileClient(); Assert.notNull(client, "客户端(master) 不能为空"); String url = client.upload(content, path, type); + FileDO entity = new FileDO().setConfigId(client.getId()) + .setName(name).setPath(path).setUrl(url) + .setType(type).setSize(content.length); // 3. 保存到数据库 - fileMapper.insert(new FileDO().setConfigId(client.getId()) - .setName(name).setPath(path).setUrl(url) - .setType(type).setSize(content.length)); - return url; + fileMapper.insert(entity); + return entity; } @VisibleForTesting From 7f0957d9c46fd438c9ffa9a9e399a7c7f073ecc3 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Thu, 10 Jul 2025 19:05:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?1.=20=E4=BF=AE=E5=A4=8D=E5=9B=9E=E6=BB=9A?= =?UTF-8?q?=E7=88=B6=E5=AD=90=E8=A7=92=E8=89=B2=E5=8A=9F=E8=83=BD=E6=97=B6?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=9A=84=E4=BB=A3=E7=A0=81=E9=80=BB=E8=BE=91?= =?UTF-8?q?,=E8=A1=A5=E5=85=A8=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=202.=20=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=90=8E=E4=B8=9A=E5=8A=A1=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E9=9C=80=E9=99=90=E5=AE=9A=E5=8F=AA=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E8=AF=A5=E5=85=AC=E5=8F=B8=E4=B8=9A=E5=8A=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enums/GlobalErrorCodeConstants.java | 3 + .../framework/common/pojo/CommonResult.java | 27 ++++ .../common/pojo/CommonResultCodeEnum.java | 32 +++++ .../common/pojo/CompanyDeptInfo.java | 27 ++++ .../pom.xml | 24 ++-- .../business/annotation/BusinessCode.java | 10 ++ .../YudaoBusinessAutoConfiguration.java | 20 +++ .../BusinessDataPermissionConfiguration.java | 19 +++ .../interceptor/BusinessControllerMarker.java | 9 ++ .../BusinessHeaderInterceptor.java | 101 +++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + ...ompanyDataPermissionAutoConfiguration.java | 36 +++++ ...aoDeptDataPermissionAutoConfiguration.java | 3 +- .../company/CompanyDataPermissionRule.java | 79 +++++++++++ .../CompanyDataPermissionRuleCustomizer.java | 21 +++ .../rule/dept/DeptDataPermissionRule.java | 5 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../yudao-spring-boot-starter-mybatis/pom.xml | 4 + .../core/dataobject}/BusinessBaseDO.java | 10 +- .../core/handler/DefaultDBFieldHandler.java | 37 ++++- .../framework/security/core/LoginUser.java | 8 ++ .../core/util/SecurityFrameworkUtils.java | 11 ++ .../web/core/util/WebFrameworkUtils.java | 32 ++++- .../src/main/resources/application-local.yaml | 2 +- .../src/main/resources/application-local.yaml | 2 +- .../src/main/resources/application-local.yaml | 2 +- .../system/enums/ErrorCodeConstants.java | 4 +- .../admin/permission/RoleController.java | 30 +++- .../admin/permission/vo/role/RoleRespVO.java | 10 ++ .../dal/dataobject/permission/RoleDO.java | 18 ++- .../RoleMenuExclusionDO.java | 44 ++++++ .../dal/dataobject/user/AdminUserDO.java | 11 ++ .../dal/mysql/permission/RoleMapper.java | 7 + .../RoleMenuExclusionMapper.java | 39 +++++ .../system/service/dept/DeptService.java | 3 + .../system/service/dept/DeptServiceImpl.java | 40 ++++++ .../oauth2/OAuth2TokenServiceImpl.java | 8 +- .../permission/PermissionServiceImpl.java | 14 +- .../service/permission/RoleService.java | 8 ++ .../service/permission/RoleServiceImpl.java | 65 ++++++++- .../service/user/AdminUserServiceImpl.java | 10 +- .../src/main/resources/application-local.yaml | 2 +- .../oauth2/OAuth2TokenServiceImplTest.java | 2 +- .../permission/PermissionServiceTest.java | 119 ++++++++++++++++ .../permission/RoleServiceImplTest.java | 118 +++++++++++++++ .../user/AdminUserServiceImplTest.java | 134 +++++++++++++++--- .../template/enums/ErrorCodeConstants.java | 2 + .../yudao-module-template-server/pom.xml | 5 + .../template/TemplateServerApplication.java | 4 + .../contract/DemoContractController.java | 106 ++++++++++++++ .../contract/vo/DemoContractPageReqVO.java | 66 +++++++++ .../admin/contract/vo/DemoContractRespVO.java | 80 +++++++++++ .../contract/vo/DemoContractSaveReqVO.java | 58 ++++++++ .../dataobject/contract/DemoContractDO.java | 69 +++++++++ .../mysql/contract/DemoContractMapper.java | 40 ++++++ .../service/contract/DemoContractService.java | 62 ++++++++ .../contract/DemoContractServiceImpl.java | 93 ++++++++++++ .../src/main/resources/application-local.yaml | 2 +- .../mapper/contract/DemoContractMapper.xml | 12 ++ .../src/main/resources/application-local.yaml | 2 +- 60 files changed, 1749 insertions(+), 64 deletions(-) create mode 100644 yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResultCodeEnum.java create mode 100644 yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CompanyDeptInfo.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/annotation/BusinessCode.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/config/YudaoBusinessAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessControllerMarker.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptor.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRule.java create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRuleCustomizer.java rename yudao-framework/{yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/core/db => yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject}/BusinessBaseDO.java (87%) create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/rolemenuexclusion/RoleMenuExclusionDO.java create mode 100644 yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/rolemenuexclusion/RoleMenuExclusionMapper.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/DemoContractController.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractPageReqVO.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractRespVO.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/dataobject/contract/DemoContractDO.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/mysql/contract/DemoContractMapper.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractService.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractServiceImpl.java create mode 100644 yudao-module-template/yudao-module-template-server/src/main/resources/mapper/contract/DemoContractMapper.xml diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java index edf31f24..3eaa9a19 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -38,4 +38,7 @@ public interface GlobalErrorCodeConstants { ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + // ========== 业务错误段 ========== + // 用户未设置公司信息,无法办理当前业务 + ErrorCode USER_NOT_SET_DEPT = new ErrorCode(1000, "用户未设置有效的公司或部门信息,无法办理当前业务"); } diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java index ac741031..128a0bee 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResult.java @@ -49,6 +49,33 @@ public class CommonResult implements Serializable { return error(result.getCode(), result.getMsg()); } + /** + * 自定义的中间返回 + * @param data + * @param code + * @return + * @param + */ + public static CommonResult customize(T data, int code, String msg) { + CommonResult result = new CommonResult<>(); + result.code = code; + result.data = data; + result.msg = msg; + return result; + } + + /** + * 自定义的中间返回 + */ + public static CommonResult customize(T data, CommonResultCodeEnum commonResultCodeEnum) { + CommonResult result = new CommonResult<>(); + result.code = commonResultCodeEnum.getCode(); + result.data = data; + result.msg = commonResultCodeEnum.getMessage(); + return result; + } + + public static CommonResult error(Integer code, String message) { Assert.notEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), code, "code 必须是错误的!"); CommonResult result = new CommonResult<>(); diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResultCodeEnum.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResultCodeEnum.java new file mode 100644 index 00000000..1df3e171 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CommonResultCodeEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.framework.common.pojo; + +/** + * @author chenbowen + */ + +public enum CommonResultCodeEnum { + // 成功 + SUCCESS(0, "成功"), + + // 根据返回重试 + NEED_ADJUST(400, "需要根据返回二次重新请求"), + + // + ERROR(500, "错误"); + + private final int code; + private final String message; + + CommonResultCodeEnum(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CompanyDeptInfo.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CompanyDeptInfo.java new file mode 100644 index 00000000..2967ecb7 --- /dev/null +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/CompanyDeptInfo.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.framework.common.pojo; +import lombok.Data; + +/** + * 登录用户信息 + * + * @author chenbowen + */ +@Data +public class CompanyDeptInfo { + /** + * 公司Id + */ + private Long companyId; + /** + * 公司名称 + */ + private String companyName; + /** + * 部门Id + */ + private Long deptId; + /** + * 部门名称 + */ + private String deptName; +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml index cab0cc53..958502de 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml @@ -11,20 +11,26 @@ yudao-spring-boot-starter-biz-business jar - - cn.iocoder.cloud - yudao-spring-boot-starter-biz-tenant - - - cn.iocoder.cloud - yudao-spring-boot-starter-biz-tenant - cn.iocoder.cloud yudao-module-system-api - 2.6.0-SNAPSHOT + ${revision} compile + + cn.iocoder.cloud + yudao-spring-boot-starter-security + ${revision} + + + cn.iocoder.cloud + yudao-common + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-data-permission + ${revision} + \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/annotation/BusinessCode.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/annotation/BusinessCode.java new file mode 100644 index 00000000..6b21bdfd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/annotation/BusinessCode.java @@ -0,0 +1,10 @@ +package cn.iocoder.yudao.framework.business.annotation; + +/** + * @author chenbowen + * + * 业务代码自动补全的注解,在 DO filed 中与 @TableField(fill = FieldFill.INSERT) 一起标注后自动新增时生成 Code chenbowen + */ +public @interface BusinessCode { + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/config/YudaoBusinessAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/config/YudaoBusinessAutoConfiguration.java new file mode 100644 index 00000000..714ca87b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/config/YudaoBusinessAutoConfiguration.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.framework.business.config; + +import cn.iocoder.yudao.framework.business.interceptor.BusinessHeaderInterceptor; +import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * @author chenbowen + */ +@AutoConfiguration(after = YudaoWebAutoConfiguration.class) +public class YudaoBusinessAutoConfiguration implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + // 只拦截增删改和 set 相关的 url + registry.addInterceptor(new BusinessHeaderInterceptor()) + .addPathPatterns("/**/add**", "/**/create**", "/**/update**", "/**/edit**", "/**/delete**", "/**/remove**", "/**/set**"); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java new file mode 100644 index 00000000..c09920d0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.business.framework; + +import cn.iocoder.yudao.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author chenbowen + */ +@Configuration(proxyBeanMethods = false) +public class BusinessDataPermissionConfiguration { + @Bean + public CompanyDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + return rule -> { + // companyId + rule.addCompanyColumn("demo_contract", "company_id"); + }; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessControllerMarker.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessControllerMarker.java new file mode 100644 index 00000000..dd441385 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessControllerMarker.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.framework.business.interceptor; + + +/** + * @author chenbowen + * 标记是否业务接口,如果是业务接口需要确定唯一的公司和部门信息才能放行 + */ +public interface BusinessControllerMarker { +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptor.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptor.java new file mode 100644 index 00000000..33714ada --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptor.java @@ -0,0 +1,101 @@ +package cn.iocoder.yudao.framework.business.interceptor; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.CommonResultCodeEnum; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.lang.NonNull; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.*; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.singleton; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUser; + +/** + * @author chenbowen + */ +@RequiredArgsConstructor +public class BusinessHeaderInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + if (!(handler instanceof HandlerMethod handlerMethod)) { + return true; + } + Object bean = handlerMethod.getBean(); + if (!(bean instanceof BusinessControllerMarker)) { + return true; + } + + response.setContentType("application/json;charset=UTF-8"); + String companyId = request.getHeader("visit-company-id"); + String deptId = request.getHeader("visit-dept-id"); + LoginUser loginUser = Optional.ofNullable(getLoginUser()).orElse(new LoginUser().setInfo(new HashMap<>())); + ObjectMapper objectMapper = new ObjectMapper(); + + Set companyDeptSet = JSONUtil.parseArray(loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[]")).stream() + .map(obj -> JSONUtil.toBean((JSONObject) obj, CompanyDeptInfo.class)) + .collect(Collectors.toSet()); + + // 1. 有 companyId + if (companyId != null && !companyId.isBlank()) { + Set companyDeptSetByCompanyId = companyDeptSet.stream().filter(companyDeptInfo -> companyDeptInfo.getCompanyId().toString().equals(companyId)).collect(Collectors.toSet()); + List filtered = companyDeptSetByCompanyId.stream() + .filter(info -> String.valueOf(info.getCompanyId()).equals(companyId)).toList(); + if (filtered.isEmpty()) { + // 当前公司下没有部门 + CompanyDeptInfo data = new CompanyDeptInfo(); + data.setCompanyId(Long.valueOf(companyId)); + data.setDeptId(0L); + return writeResponse(response, HttpStatus.OK.value(), CommonResult.customize(singleton(data), CommonResultCodeEnum.NEED_ADJUST), objectMapper); + } + // 如果有 deptId,校验其是否属于该 companyId + if (deptId != null) { + boolean valid = filtered.stream().anyMatch(info -> String.valueOf(info.getDeptId()).equals(deptId)); + if (!valid) { + return writeResponse(response, HttpStatus.BAD_REQUEST.value(), CommonResult.customize(null, CommonResultCodeEnum.ERROR.getCode(),"当前用户匹配部门不属于此公司"), objectMapper); + }else{ + // 部门存在,放行 + return true; + } + } + if (filtered.size() == 1) { + // 唯一部门 + return writeResponse(response, HttpStatus.OK.value(), CommonResult.customize(filtered,CommonResultCodeEnum.NEED_ADJUST), objectMapper); + } else { + // 多个部门 + return writeResponse(response, HttpStatus.OK.value(), CommonResult.customize(filtered,CommonResultCodeEnum.NEED_ADJUST), objectMapper); + } + } + // 2. 没有公司信息,尝试唯一性自动推断 + // 如果当前用户下只有一个公司和部门的对于关系 + if (companyDeptSet.size() == 1) { + CompanyDeptInfo info = new CompanyDeptInfo(); + CompanyDeptInfo companyDeptInfo = companyDeptSet.iterator().next(); + info.setCompanyId(companyDeptInfo.getCompanyId()); + info.setDeptId(companyDeptInfo.getDeptId()); + return writeResponse(response, HttpStatus.OK.value(), CommonResult.success(singleton(info)), objectMapper); + } else { + return writeResponse(response, HttpStatus.OK.value(), CommonResult.customize(companyDeptSet,CommonResultCodeEnum.NEED_ADJUST), objectMapper); + } + } + + + private boolean writeResponse(HttpServletResponse response, int status, CommonResult result, ObjectMapper objectMapper) throws Exception { + response.setStatus(status); + response.getWriter().write(objectMapper.writeValueAsString(result)); + return false; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..e476ed4c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.business.config.YudaoBusinessAutoConfiguration \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java new file mode 100644 index 00000000..bef0e962 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.framework.datapermission.config; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi; +import cn.iocoder.yudao.framework.datapermission.core.rule.company.CompanyDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 基于部门的数据权限 AutoConfiguration + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass(LoginUser.class) +@ConditionalOnBean(value = {CompanyDataPermissionRuleCustomizer.class}) +public class YudaoCompanyDataPermissionAutoConfiguration { + + @Bean + public CompanyDataPermissionRule companyDataPermissionRule(List customizers) { + + // 创建 CompanyDataPermissionRule 对象 + CompanyDataPermissionRule rule = new CompanyDataPermissionRule(); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java index ba268616..aaacb517 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -24,8 +24,7 @@ import java.util.List; public class YudaoDeptDataPermissionAutoConfiguration { @Bean - public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, - List customizers) { + public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, List customizers) { // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用 // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误 try { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRule.java new file mode 100644 index 00000000..ed1f4179 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRule.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.company; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.datapermission.core.rule.DataPermissionRule; +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; + +import java.util.*; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserCompanyId; + +/** + * 基于公司类型部门的 {@link DataPermissionRule} 数据权限规则实现 + * 注意,使用 CompanyDataPermissionRule 时,需要保证表中有 company_id 公司编号的字段,可自定义。 + * + * @author chenbowen + */ +@AllArgsConstructor +@Slf4j +public class CompanyDataPermissionRule implements DataPermissionRule { + static final Expression EXPRESSION_NULL = new NullValue(); + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * key:表名 + * value:字段名 + */ + private final Map companyColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #companyColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 业务拼接 Company 的条件 + if (getLoginUserCompanyId() == null) { + // 如果没有登录用户的公司编号,则不需要拼接条件 + return null; + } + Expression companyExpression = buildCompanyExpression(tableName, tableAlias, Collections.singleton(getLoginUserCompanyId())); + return Objects.requireNonNullElse(companyExpression, EXPRESSION_NULL); + } + + private Expression buildCompanyExpression(String tableName, Alias tableAlias, Set companyIds) { + // 如果不存在配置,则无需作为条件 + String columnName = companyColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(companyIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + // Parenthesis 的目的,是提供 (1,2,3) 的 () 左右括号 + new ParenthesedExpressionList<>(new ExpressionList<>(CollectionUtils.convertList(companyIds, LongValue::new)))); + } + + public void addCompanyColumn(String tableName, String columnName) { + companyColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRuleCustomizer.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRuleCustomizer.java new file mode 100644 index 00000000..ee15eb53 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/company/CompanyDataPermissionRuleCustomizer.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.framework.datapermission.core.rule.company; + +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface CompanyDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link CompanyDataPermissionRule#addCompanyColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(CompanyDataPermissionRule rule); + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java index b35c2eb8..072b8f69 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -121,6 +121,11 @@ public class DeptDataPermissionRule implements DataPermissionRule { // 添加到上下文中,避免重复计算 loginUser.setContext(CONTEXT_KEY, deptDataPermission); } + // 如果不归属任何部门,且无可查询所有部门的权限,提示当前用户未关联任何部门 +// if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) && !deptDataPermission.getAll()) { +// log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 未关联任何部门]", JsonUtils.toJsonString(loginUser), tableName, tableAlias.getName(), JsonUtils.toJsonString(deptDataPermission)); +// throw new NullPointerException("当前登录用户未关联任何部门,请先联系管理员进行部门关联"); +// } // 如果开启了公司上下文,且缓存的公司编号不等于 CompanyContextHolder 的公司编号,则更新缓存 if(!CompanyContextHolder.isIgnore()) { Long companyId = CompanyContextHolder.getCompanyId(); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index cb87159e..81ba9b1a 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration +cn.iocoder.yudao.framework.datapermission.config.YudaoCompanyDataPermissionAutoConfiguration cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionRpcAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml index 550f1485..2ee30399 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -99,6 +99,10 @@ com.fhs-opensource easy-trans-mybatis-plus-extend + + cn.iocoder.cloud + yudao-spring-boot-starter-security + diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/core/db/BusinessBaseDO.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BusinessBaseDO.java similarity index 87% rename from yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/core/db/BusinessBaseDO.java rename to yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BusinessBaseDO.java index 3d618a97..25738488 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/core/db/BusinessBaseDO.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/dataobject/BusinessBaseDO.java @@ -1,6 +1,5 @@ -package cn.iocoder.yudao.framework.business.core.db; +package cn.iocoder.yudao.framework.mybatis.core.dataobject; -import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.TableField; import lombok.Data; @@ -12,7 +11,7 @@ import org.apache.ibatis.type.JdbcType; */ @Data @EqualsAndHashCode(callSuper = true) -public class BusinessBaseDO extends TenantBaseDO { +public class BusinessBaseDO extends BaseDO { /** 公司编号 */ @TableField(fill = FieldFill.INSERT) @@ -29,6 +28,10 @@ public class BusinessBaseDO extends TenantBaseDO { /** 岗位编号 */ @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) private Long postId; + /** + * 多租户编号 + */ + private Long tenantId; /** * 清除 creator、createTime、updateTime、updater 等字段,避免前端直接传递这些字段,导致被更新 @@ -44,3 +47,4 @@ public class BusinessBaseDO extends TenantBaseDO { } } + diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java index 05ab06aa..596c2514 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -1,16 +1,27 @@ package cn.iocoder.yudao.framework.mybatis.core.handler; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BusinessBaseDO; +import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; +import java.lang.reflect.Field; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.USER_NOT_SET_DEPT; +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUser; /** * 通用参数填充实现类 - * * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 * * @author hexiaowu @@ -41,6 +52,30 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { baseDO.setUpdater(userId.toString()); } } + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BusinessBaseDO businessBaseDO) { + // 公司编号、公司名称、部门编号、部门名称、岗位编号等字段,默认不填充 + // 需要在业务层手动设置 + LoginUser loginUser = getLoginUser(); + Long visitCompanyId = loginUser.getVisitCompanyId(); + Long visitDeptId = loginUser.getVisitDeptId(); + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_POST_IDS,"[]"); + // 更加合理的写法 + Set postIds = new HashSet<>(JSONUtil.parseArray( + loginUser.getInfo().getOrDefault(LoginUser.INFO_KEY_POST_IDS, "[]") + ).toList(Long.class)); + // 如果 visitCompanyId 不存在,不能进行业务办理 + if (Objects.isNull(visitCompanyId) || Objects.isNull(visitDeptId)) { + throw exception(USER_NOT_SET_DEPT); + } + businessBaseDO.setCompanyId(visitCompanyId); + businessBaseDO.setCompanyName(loginUser.getVisitCompanyName()); + businessBaseDO.setDeptId(visitDeptId); + businessBaseDO.setDeptName(loginUser.getVisitDeptName()); + // 暂时没有具体业务要求,岗位默认当前用户第一个 todo chenbowen + businessBaseDO.setPostId(postIds.isEmpty() ? 0L : postIds.iterator().next()); + // 多租户编号,默认不填充 + businessBaseDO.setTenantId(loginUser.getTenantId()); + } } @Override diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java index ccb8ed8b..2bca414d 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/LoginUser.java @@ -20,6 +20,14 @@ public class LoginUser { public static final String INFO_KEY_NICKNAME = "nickname"; public static final String INFO_KEY_TENANT_ID = "tenantId"; + // 用户关联的公司 Id + public static final String INFO_KEY_COMPANY_IDS = "companyIds"; + // 用户关联的部门 Id + public static final String INFO_KEY_DEPT_IDS = "deptIds"; + // 用户关联的公司与部门关联关系 + public static final String INFO_KEY_COMPANY_DEPT_SET = "companyDeptSet"; + // 用户关联的岗位信息 + public static final String INFO_KEY_POST_IDS = "postIds"; /** * 用户编号 diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index ad717f73..857223bd 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -82,6 +82,17 @@ public class SecurityFrameworkUtils { return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; } + /** + * 获得当前用户访问的公司 Id,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserCompanyId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getVisitCompanyId() : null; + } + /** * 获得当前用户的编号,从上下文中 * diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java index a5ecab83..61551024 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java @@ -29,6 +29,8 @@ public class WebFrameworkUtils { public static final String HEADER_VISIT_TENANT_ID = "visit-tenant-id"; public static final String HEADER_VISIT_COMPANY_ID = "visit-company-id"; public static final String HEADER_VISIT_COMPANY_NAME = "visit-company-name"; + public static final String HEADER_VISIT_DEPT_ID = "visit-dept-id"; + public static final String HEADER_VISIT_DEPT_NAME = "visit-dept-name"; /** * 终端的 Header @@ -199,13 +201,22 @@ public class WebFrameworkUtils { } /** - * 获得访问的公司名称,从 header 中 + * 获得访问的公司名称,从 header 中,并进行 URL 解码 * @param request 请求 * @return 公司名称,解析失败或无效时返回空字符串 */ public static String getCompanyName(HttpServletRequest request) { String companyName = request.getHeader(HEADER_VISIT_COMPANY_NAME); - return StrUtil.isBlank(companyName) ? StrUtil.EMPTY : companyName; + if (StrUtil.isBlank(companyName)) { + return StrUtil.EMPTY; + } + try { + // URL 解码 + return java.net.URLDecoder.decode(companyName, java.nio.charset.StandardCharsets.UTF_8); + } catch (Exception e) { + // 解码失败,返回原始值 + return companyName; + } } /** @@ -214,7 +225,7 @@ public class WebFrameworkUtils { * @return 部门编号,解析失败或无效时返回 0 */ public static Long getDeptId(HttpServletRequest request) { - String deptIdHeader = request.getHeader(WebFrameworkUtils.HEADER_VISIT_TENANT_ID); + String deptIdHeader = request.getHeader(WebFrameworkUtils.HEADER_VISIT_DEPT_ID); if (StrUtil.isBlank(deptIdHeader)) { return 0L; } @@ -228,12 +239,21 @@ public class WebFrameworkUtils { } /** - * 获得访问的部门名称,从 header 中 + * 获得访问的部门名称,从 header 中,并进行 URL 解码 * @param request 请求 * @return 部门名称,解析失败或无效时返回空字符串 */ public static String getDeptName(HttpServletRequest request) { - String deptName = request.getHeader(WebFrameworkUtils.HEADER_VISIT_COMPANY_NAME); - return StrUtil.isBlank(deptName) ? StrUtil.EMPTY : deptName; + String deptName = request.getHeader(WebFrameworkUtils.HEADER_VISIT_DEPT_NAME); + if (StrUtil.isBlank(deptName)) { + return StrUtil.EMPTY; + } + try { + // URL 解码 + return java.net.URLDecoder.decode(deptName, java.nio.charset.StandardCharsets.UTF_8); + } catch (Exception e) { + // 解码失败,返回原始值 + return deptName; + } } } diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/resources/application-local.yaml b/yudao-module-ai/yudao-module-ai-server/src/main/resources/application-local.yaml index edf68795..98bb2ed4 100644 --- a/yudao-module-ai/yudao-module-ai-server/src/main/resources/application-local.yaml +++ b/yudao-module-ai/yudao-module-ai-server/src/main/resources/application-local.yaml @@ -91,7 +91,7 @@ spring: xxl: job: admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/resources/application-local.yaml b/yudao-module-bpm/yudao-module-bpm-server/src/main/resources/application-local.yaml index 4fd10e3e..06e29834 100644 --- a/yudao-module-bpm/yudao-module-bpm-server/src/main/resources/application-local.yaml +++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/resources/application-local.yaml @@ -81,7 +81,7 @@ xxl: job: enabled: false # 是否开启调度中心,默认为 true 开启 admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml index ffa19f4c..f88f9813 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/application-local.yaml @@ -102,7 +102,7 @@ xxl: job: enabled: false # 是否开启调度中心,默认为 true 开启 admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index 07a86c57..62073153 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode; * System 错误码枚举类 * * system 系统,使用 1-002-000-000 段 + * @author chenbowen */ public interface ErrorCodeConstants { @@ -33,8 +34,9 @@ public interface ErrorCodeConstants { ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色"); ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用"); - ErrorCode ROLE_CAN_NOT_UPDATE_NORMAL_TYPE_ROLE = new ErrorCode(1_002_002_006, "不能操作类型为标准的角色,除非是管理员角色"); + ErrorCode ROLE_CAN_NOT_DELETE_HAS_CHILDREN = new ErrorCode(1_002_002_007, " 角色【{}】存在子角色,不允许删除"); + ErrorCode ROLE_PARENT_IS_CHILD = new ErrorCode(1_002_002_008, "不能设置自己的子角色为父角色"); // ========== 用户模块 1-002-003-000 ========== ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1_002_003_000, "用户账号已经存在"); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java index 95e2fc81..15e138e0 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/RoleController.java @@ -25,11 +25,15 @@ import org.springframework.web.bind.annotation.*; import java.io.IOException; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static java.util.Collections.singleton; +/** + * @author chenbowen + */ @Tag(name = "管理后台 - 角色") @RestController @RequestMapping("/system/role") @@ -43,7 +47,7 @@ public class RoleController { @Operation(summary = "创建角色") @PreAuthorize("@ss.hasPermission('system:role:create')") public CommonResult createRole(@Valid @RequestBody RoleSaveReqVO createReqVO) { - return success(roleService.createRole(createReqVO, null)); + return success(roleService.createRole(createReqVO, createReqVO.getType() == null ? null : Integer.valueOf(createReqVO.getType()))); } @PutMapping("/update") @@ -76,6 +80,20 @@ public class RoleController { @PreAuthorize("@ss.hasPermission('system:role:query')") public CommonResult> getRolePage(RolePageReqVO pageReqVO) { PageResult pageResult = roleService.getRolePage(pageReqVO); + // 获取所有父级角色信息 + List parentIds = pageResult.getList().stream().filter(role -> role.getParentId() != null && role.getParentId() > 0) + .map(RoleDO::getParentId) + .distinct() + .toList(); + List parentRoles = roleService.getRoleList(parentIds); + // 将父级角色信息转换为 id 与 name 的 Map + var parentRoleMap = parentRoles.stream().collect(Collectors.toMap(RoleDO::getId, RoleDO::getName, (v1, v2) -> v1)); + // 补全父级角色名称 + pageResult.getList().forEach(role -> { + if (role.getParentId() != null && role.getParentId() > 0) { + role.setParentName(parentRoleMap.get(role.getParentId())); + } + }); return success(BeanUtils.toBean(pageResult, RoleRespVO.class)); } @@ -87,6 +105,16 @@ public class RoleController { return success(BeanUtils.toBean(list, RoleRespVO.class)); } + @GetMapping({"/list-all-extend-simple", "/simple-extend-list"}) + @Operation(summary = "获取所有可继承角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项") + public CommonResult> getParentSimpleRoleList() { + List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus())); + // 过滤掉系统内置角色(如有需要) + list.removeIf(role -> role.getType() != null && role.getType().equals(1)); + list.sort(Comparator.comparing(RoleDO::getSort)); + return success(BeanUtils.toBean(list, RoleRespVO.class)); + } + @GetMapping("/export-excel") @Operation(summary = "导出角色 Excel") @ApiAccessLog(operateType = EXPORT) diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java index 89f80c67..74c87aa9 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -12,6 +12,9 @@ import lombok.Data; import java.time.LocalDateTime; import java.util.Set; +/** + * @author chenbowen + */ @Schema(description = "管理后台 - 角色信息 Response VO") @Data @ExcelIgnoreUnannotated @@ -56,4 +59,11 @@ public class RoleRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") private LocalDateTime createTime; + @Schema(description = "父级角色名称", example = "1") + @ExcelProperty("父级角色名称") + private String parentName; + + @Schema(description = "父级角色 Id", example = "1") + @ExcelProperty("父级角色 Id") + private Long parentId; } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java index 7cb2824a..2f7696b0 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/permission/RoleDO.java @@ -33,7 +33,6 @@ public class RoleDO extends TenantBaseDO { private String name; /** * 角色标识 - * * 枚举 */ private String code; @@ -43,13 +42,11 @@ public class RoleDO extends TenantBaseDO { private Integer sort; /** * 角色状态 - * * 枚举 {@link CommonStatusEnum} */ private Integer status; /** * 角色类型 - * * 枚举 {@link RoleTypeEnum} */ private Integer type; @@ -60,16 +57,27 @@ public class RoleDO extends TenantBaseDO { /** * 数据范围 - * * 枚举 {@link DataScopeEnum} */ private Integer dataScope; /** * 数据范围(指定部门数组) - * * 适用于 {@link #dataScope} 的值为 {@link DataScopeEnum#DEPT_CUSTOM} 时 */ @TableField(typeHandler = JacksonTypeHandler.class) private Set dataScopeDeptIds; + /** + * 父级标准角色 Id : 继承的标准角色Id,系统角色为 -1、标准角色为 0 + */ + private Long parentId; + + /** + * 父级角色名称 + * 仅用于前端角色界面展示 + */ + @TableField(exist = false) + private String parentName; + + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/rolemenuexclusion/RoleMenuExclusionDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/rolemenuexclusion/RoleMenuExclusionDO.java new file mode 100644 index 00000000..cf0285a6 --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/rolemenuexclusion/RoleMenuExclusionDO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.system.dal.dataobject.rolemenuexclusion; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 角色菜单剔除 DO + * + * @author 管理员 + */ +@TableName("system_role_menu_exclusion") +@KeySequence("system_role_menu_exclusion_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RoleMenuExclusionDO extends BaseDO { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + /** + * 角色ID + */ + private Long roleId; + /** + * 菜单ID + */ + private Long menuId; + /** + * 备注 + */ + private String remark; + + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java index 4573c6fe..ba510511 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/AdminUserDO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.user; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO; import cn.iocoder.yudao.module.system.enums.common.SexEnum; import com.baomidou.mybatisplus.annotation.*; @@ -53,6 +54,16 @@ public class AdminUserDO extends TenantBaseDO { */ @TableField(exist = false, typeHandler = JacksonTypeHandler.class ) private Set deptIds; + /** + * 公司 ID 列表 + */ + @TableField(exist = false, typeHandler = JacksonTypeHandler.class ) + private Set companyIds; + /** + * 公司与部门关系列表 + */ + @TableField(exist = false) + private Set companyDeptInfos; /** * 岗位编号数组 */ diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java index 2e4da2bb..91c0d90b 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/permission/RoleMapper.java @@ -12,6 +12,9 @@ import org.springframework.lang.Nullable; import java.util.Collection; import java.util.List; +/** + * @author chenbowen + */ @Mapper public interface RoleMapper extends BaseMapperX { @@ -36,4 +39,8 @@ public interface RoleMapper extends BaseMapperX { return selectList(RoleDO::getStatus, statuses); } + default long selectCountByParentId(Long parentId) { + return selectCount(RoleDO::getParentId, parentId); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/rolemenuexclusion/RoleMenuExclusionMapper.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/rolemenuexclusion/RoleMenuExclusionMapper.java new file mode 100644 index 00000000..409542cc --- /dev/null +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/rolemenuexclusion/RoleMenuExclusionMapper.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.system.dal.mysql.rolemenuexclusion; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.system.dal.dataobject.rolemenuexclusion.RoleMenuExclusionDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 角色菜单剔除 Mapper + * + * @author 管理员 + */ +@Mapper +public interface RoleMenuExclusionMapper extends BaseMapperX { + + /** + * 根据角色编号,查询角色菜单剔除列表 + * + * @param roleIds 角色编号 + */ + default List selectMenuIdListByRoleId(Collection roleIds) { + return selectList(RoleMenuExclusionDO::getRoleId, roleIds); + } + + /** + * 根据角色编号,菜单编号,删除角色菜单剔除列表 + * + * @param roleId 角色编号 + * @param menuIds 菜单编号 + */ + default void deleteListByRoleIdAndMenuIds(Long roleId, Collection menuIds) { + delete(new LambdaQueryWrapper() + .eq(RoleMenuExclusionDO::getRoleId, roleId) + .in(RoleMenuExclusionDO::getMenuId, menuIds)); + } +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java index d9526682..81a49748 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.system.service.dept; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; @@ -115,4 +116,6 @@ public interface DeptService { void validateDeptList(Collection ids); List getUserCompanyList(); + + Set getCompanyDeptInfoListByUserId(Long userId); } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java index 8bbbec63..b0087060 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.service.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission; import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptListReqVO; @@ -263,4 +264,43 @@ public class DeptServiceImpl implements DeptService { return getDeptList(companyIds); } + /** + * 根据用户ID查询其归属公司及直属部门关系列表(不递归下级公司) + */ + @Override + public Set getCompanyDeptInfoListByUserId(Long userId) { + // 查询用户所属部门 + Set deptIds = userDeptMapper.selectValidListByUserIds(singleton(userId)) + .stream() + .map(UserDeptDO::getDeptId) + .collect(Collectors.toSet()); + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptySet(); + } + // 查询所有部门信息 + Map deptMap = getDeptList(deptIds).stream() + .collect(Collectors.toMap(DeptDO::getId, d -> d)); + Set result = new HashSet<>(); + for (Long deptId : deptIds) { + DeptDO dept = deptMap.get(deptId); + if (dept == null) continue; + // 向上查找公司,如果到达顶层(parentId为PARENT_ID_ROOT)还没找到公司,则用顶层部门作为公司 + DeptDO company = dept; + while (company != null && !Boolean.TRUE.equals(company.getIsCompany())) { + if (company.getParentId() == null || DeptDO.PARENT_ID_ROOT.equals(company.getParentId())) { + break; + } + company = getDept(company.getParentId()); + } + if (company == null) continue; + CompanyDeptInfo info = new CompanyDeptInfo(); + info.setCompanyId(company.getId()); + info.setCompanyName(company.getName()); + info.setDeptId(dept.getId()); + info.setDeptName(dept.getName()); + result.add(info); + } + return result; + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java index 34e1ac2c..f0545c0c 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; @@ -199,7 +200,12 @@ public class OAuth2TokenServiceImpl implements OAuth2TokenService { if (userType.equals(UserTypeEnum.ADMIN.getValue())) { AdminUserDO user = adminUserService.getUser(userId); return MapUtil.builder(LoginUser.INFO_KEY_NICKNAME, user.getNickname()) - .put(LoginUser.INFO_KEY_TENANT_ID, user.getTenantId().toString()).build(); + .put(LoginUser.INFO_KEY_TENANT_ID, user.getTenantId().toString()) + .put(LoginUser.INFO_KEY_COMPANY_IDS, CollUtil.isEmpty(user.getCompanyIds()) ? "[]" : JsonUtils.toJsonString(user.getCompanyIds())) + .put(LoginUser.INFO_KEY_DEPT_IDS, CollUtil.isEmpty(user.getDeptIds()) ? "[]" : JsonUtils.toJsonString(user.getDeptIds())) + .put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, CollUtil.isEmpty(user.getCompanyDeptInfos()) ? "[]" : JsonUtils.toJsonString(user.getCompanyDeptInfos())) + .put(LoginUser.INFO_KEY_POST_IDS, CollUtil.isEmpty(user.getPostIds()) ? "[]" : JsonUtils.toJsonString(user.getPostIds())) + .build(); } else if (userType.equals(UserTypeEnum.MEMBER.getValue())) { // 注意:目前 Member 暂时不读取,可以按需实现 return Collections.emptyMap(); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index ea4095e2..45ff2d28 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -12,9 +12,11 @@ import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.rolemenuexclusion.RoleMenuExclusionDO; import cn.iocoder.yudao.module.system.dal.dataobject.userdept.UserDeptDO; import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; +import cn.iocoder.yudao.module.system.dal.mysql.rolemenuexclusion.RoleMenuExclusionMapper; import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.system.enums.permission.DataScopeEnum; import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; @@ -69,6 +71,8 @@ public class PermissionServiceImpl implements PermissionService { @Resource private AdminUserService userService; @Resource + private RoleMenuExclusionMapper roleMenuExclusionMapper; + @Resource private UserDeptService userDeptService; @Autowired private PermissionService permissionService; @@ -210,8 +214,14 @@ public class PermissionServiceImpl implements PermissionService { if (roleService.hasAnySuperAdmin(roleIds)) { return convertSet(menuService.getMenuList(), MenuDO::getId); } - // 如果是非管理员的情况下,获得拥有的菜单编号 - return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); + // 递归获取所有父角色id + Set allRoleIds = roleService.getAllParentAndSelfRoleIds(roleIds); + // 如果是非管理员的情况下,获得拥有的菜单编号(含父角色,需要剔除当前角色排除的菜单) + Set menuIds = convertSet(roleMenuMapper.selectListByRoleId(allRoleIds), RoleMenuDO::getMenuId); + // 排除当前角色排除的菜单编号 + Set excludeMenuIds = convertSet(roleMenuExclusionMapper.selectMenuIdListByRoleId(allRoleIds), RoleMenuExclusionDO::getMenuId); + menuIds.removeAll(excludeMenuIds); + return menuIds; } @Override diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java index c251a20f..1d84dcc4 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleService.java @@ -123,4 +123,12 @@ public interface RoleService { */ void validateRoleList(Collection ids); + /** + * 获取所有父角色id(递归) + * @param roleIds 当前角色id集合 + * @return 包含自身和所有父级的id集合 + */ + Set getAllParentAndSelfRoleIds(Collection roleIds); + + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java index f5f4bfc2..f3940871 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImpl.java @@ -24,6 +24,7 @@ import com.mzt.logapi.context.LogRecordContext; import com.mzt.logapi.service.impl.DiffParseFunction; import com.mzt.logapi.starter.annotation.LogRecord; import jakarta.annotation.Resource; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; @@ -33,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.*; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; @@ -69,8 +71,11 @@ public class RoleServiceImpl implements RoleService { // 2. 插入到数据库 RoleDO role = BeanUtils.toBean(createReqVO, RoleDO.class) .setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())) + // 如果类型不为公司角色则设置 parentId 为 0 + .setParentId(!ObjectUtil.equal(RoleTypeEnum.CUSTOM.getType(), type) ? 0L : createReqVO.getParentId()) .setStatus(ObjUtil.defaultIfNull(createReqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) - .setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 + // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 + .setDataScope(DataScopeEnum.ALL.getScope()); roleMapper.insert(role); // 3. 记录操作日志上下文 @@ -87,6 +92,11 @@ public class RoleServiceImpl implements RoleService { RoleDO role = validateRoleForUpdate(updateReqVO.getId()); // 1.2 校验角色的唯一字段是否重复 validateRoleDuplicate(updateReqVO.getName(), updateReqVO.getCode(), updateReqVO.getId()); + // 1.3 校验角色当前修改的父角色是否为当前角色的子角色 + if (updateReqVO.getParentId() != null && !updateReqVO.getParentId().equals(0L) && isChildRole(updateReqVO.getId(), updateReqVO.getParentId())) { + throw exception(ROLE_PARENT_IS_CHILD, updateReqVO.getName()); + } + // 2. 更新到数据库 RoleDO updateObj = BeanUtils.toBean(updateReqVO, RoleDO.class); @@ -112,6 +122,7 @@ public class RoleServiceImpl implements RoleService { } @Override + @SneakyThrows @Transactional(rollbackFor = Exception.class) @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") @LogRecord(type = SYSTEM_ROLE_TYPE, subType = SYSTEM_ROLE_DELETE_SUB_TYPE, bizNo = "{{#id}}", @@ -120,6 +131,11 @@ public class RoleServiceImpl implements RoleService { // 1. 校验是否可以更新 RoleDO role = validateRoleForUpdate(id); + // 1.1 校验角色是否存在子角色,如果存在,则不允许删除 + if (roleMapper.selectCountByParentId(id) > 0) { + throw exception(ROLE_CAN_NOT_DELETE_HAS_CHILDREN , role.getName()); + } + // 2.1 标记删除 roleMapper.deleteById(id); // 2.2 删除相关数据 @@ -214,7 +230,7 @@ public class RoleServiceImpl implements RoleService { if (CollectionUtil.isEmpty(ids)) { return Collections.emptyList(); } - return roleMapper.selectBatchIds(ids); + return roleMapper.selectByIds(ids); } @Override @@ -262,7 +278,7 @@ public class RoleServiceImpl implements RoleService { return; } // 获得角色信息 - List roles = roleMapper.selectBatchIds(ids); + List roles = roleMapper.selectByIds(ids); Map roleMap = convertMap(roles, RoleDO::getId); // 校验 ids.forEach(id -> { @@ -276,6 +292,29 @@ public class RoleServiceImpl implements RoleService { }); } + @Override + public Set getAllParentAndSelfRoleIds(Collection roleIds) { + // 递归获取所有父角色id,最多递归5层,防止环 + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptySet(); + } + RoleServiceImpl self = getSelf(); + return roleIds.stream() + .flatMap(id -> { + Set chain = new LinkedHashSet<>(); + Long current = id; + for (int depth = 0; current != null && current > 0 && depth < 5 && chain.add(current); depth++) { + RoleDO role = self.getRoleFromCache(current); + if (role == null || role.getParentId() == null || role.getParentId() <= 0) { + break; + } + current = role.getParentId(); + } + return chain.stream(); + }) + .collect(Collectors.toSet()); + } + /** * 获得自身的代理对象,解决 AOP 生效问题 * @@ -285,4 +324,24 @@ public class RoleServiceImpl implements RoleService { return SpringUtil.getBean(getClass()); } + /** + * 判断 parentId 是否为 roleId 的子孙节点,递归最多5次 + */ + public boolean isChildRole(Long parentId, Long id) { + return isChildRole(parentId, id, 0); + } + public boolean isChildRole(Long parentId, Long id, int depth) { + if (parentId.equals(id)) { + return true; + } + if (depth >= 5) { + return false; + } + RoleDO parent = roleMapper.selectById(id); + if (parent == null || parent.getParentId() == null || parent.getParentId().equals(0L) || parent.getParentId().equals(-1L)) { + return false; + } + return isChildRole(parentId, parent.getParentId(), depth + 1); + } + } diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java index 924658b5..845a1f89 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -39,6 +40,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; @@ -284,9 +286,11 @@ public class AdminUserServiceImpl implements AdminUserService { @Override public AdminUserDO getUser(Long id) { AdminUserDO adminUserDO = userMapper.selectListByIds(singleton(id)).stream().findFirst().orElseThrow(() -> exception(USER_NOT_EXISTS)); - // 查询用户关联的部门编号 - List userDeptList = userDeptService.getValidUserDeptListByUserIds(singleton(id)); - adminUserDO.setDeptIds(convertSet(userDeptList, UserDeptDO::getDeptId)); + Set companyDeptInfoListByUserId = deptService.getCompanyDeptInfoListByUserId(id); + adminUserDO.setDeptIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getDeptId).collect(Collectors.toSet())); + adminUserDO.setCompanyIds(companyDeptInfoListByUserId.stream().map(CompanyDeptInfo::getCompanyId).collect(Collectors.toSet())); + adminUserDO.setCompanyDeptInfos(companyDeptInfoListByUserId); + // 设置用户的部门名称集合 return adminUserDO; } diff --git a/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml b/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml index f54a9a01..689136b3 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml +++ b/yudao-module-system/yudao-module-system-server/src/main/resources/application-local.yaml @@ -86,7 +86,7 @@ xxl: job: enabled: false # 是否开启调度中心,默认为 true 开启 admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java index 8ff80662..1215081b 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -81,7 +81,7 @@ public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { assertPojoEquals(accessTokenDO, dbAccessTokenDO, "expiresTime", "createTime", "updateTime", "deleted"); assertEquals(userId, accessTokenDO.getUserId()); assertEquals(userType, accessTokenDO.getUserType()); - assertEquals(2, accessTokenDO.getUserInfo().size()); + assertEquals(6, accessTokenDO.getUserInfo().size()); assertEquals(user.getNickname(), accessTokenDO.getUserInfo().get("nickname")); assertEquals(clientId, accessTokenDO.getClientId()); assertEquals(scopes, accessTokenDO.getScopes()); diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index 4daccf75..c82ac6dd 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -1,12 +1,16 @@ package cn.iocoder.yudao.module.system.service.permission; +import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleMenuDO; import cn.iocoder.yudao.module.system.dal.dataobject.permission.UserRoleDO; +import cn.iocoder.yudao.module.system.dal.dataobject.rolemenuexclusion.RoleMenuExclusionDO; import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.RoleMenuMapper; import cn.iocoder.yudao.module.system.dal.mysql.permission.UserRoleMapper; +import cn.iocoder.yudao.module.system.dal.mysql.rolemenuexclusion.RoleMenuExclusionMapper; import cn.iocoder.yudao.module.system.enums.permission.RoleTypeEnum; import cn.iocoder.yudao.module.system.service.dept.DeptService; import cn.iocoder.yudao.module.system.service.user.AdminUserService; @@ -17,6 +21,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -25,6 +30,9 @@ import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEq import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @Import({PermissionServiceImpl.class, RoleServiceImpl.class}) public class PermissionServiceTest extends BaseDbUnitTest { @@ -35,6 +43,8 @@ public class PermissionServiceTest extends BaseDbUnitTest { @Resource private RoleMenuMapper roleMenuMapper; @Resource + private RoleMenuExclusionMapper roleMenuExclusionMapper; + @Resource private UserRoleMapper userRoleMapper; @Resource @@ -263,5 +273,114 @@ public class PermissionServiceTest extends BaseDbUnitTest { // ========== 用户-部门的相关方法 ========== + // ========== 父子角色的相关方法 ========== + + @Test + public void testGetAllParentAndSelfRoleIds() { + // mock 3层父子关系 A->B->C + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + RoleDO roleC = randomPojo(RoleDO.class, o -> o.setParentId(roleB.getId())); + roleMapper.insert(roleC); + // 调用 PermissionService 的父子角色链路能力 + Set ids = roleService.getAllParentAndSelfRoleIds(asSet(roleC.getId())); + // 断言递归能拿到所有父节点和自身 + assertEquals(asSet(roleA.getId(), roleB.getId(), roleC.getId()), ids); + } + + @Test + public void testIsChildRole_trueAndFalse() { + // mock 3层父子关系 A->B->C + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + RoleDO roleC = randomPojo(RoleDO.class, o -> o.setParentId(roleB.getId())); + roleMapper.insert(roleC); + // 断言 C 是 A 的子孙节点 + assertTrue(roleService.isChildRole(roleA.getId(), roleC.getId())); + // 断言 A 不是 C 的子孙节点 + assertFalse(roleService.isChildRole(roleC.getId(), roleA.getId())); + } + + @Test + public void testUpdateRole_parentIsChildException() { + // mock 2层父子关系 A->B + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + // 尝试把A的父节点改为B(形成环) + RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(roleA.getId()).setParentId(roleB.getId())); + assertThrows(ServiceException.class, () -> roleService.updateRole(reqVO)); + } + + @Test + public void testChildRoleInheritParentRoleMenus() { + // mock 父子关系 A->B + RoleDO parentRole = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(parentRole); + RoleDO childRole = randomPojo(RoleDO.class, o -> o.setParentId(parentRole.getId())); + roleMapper.insert(childRole); + // 给父角色分配菜单 + RoleMenuDO parentMenu1 = randomPojo(RoleMenuDO.class).setRoleId(parentRole.getId()).setMenuId(101L); + roleMenuMapper.insert(parentMenu1); + RoleMenuDO parentMenu2 = randomPojo(RoleMenuDO.class).setRoleId(parentRole.getId()).setMenuId(102L); + roleMenuMapper.insert(parentMenu2); + // 给子角色分配部分菜单 + RoleMenuDO childMenu = randomPojo(RoleMenuDO.class).setRoleId(childRole.getId()).setMenuId(201L); + roleMenuMapper.insert(childMenu); + // 调用:获取子角色的所有菜单(应包含父角色的菜单) + Set menuIds = permissionService.getRoleMenuListByRoleId(childRole.getId()); + // 断言:包含父角色和子角色自己的菜单 + assertEquals(asSet(101L, 102L, 201L), menuIds); + } + + @Test + public void testChildRoleExcludeParentMenu() { + // mock 父子关系 A->B + RoleDO parentRole = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(parentRole); + RoleDO childRole = randomPojo(RoleDO.class, o -> o.setParentId(parentRole.getId())); + roleMapper.insert(childRole); + // 父角色分配菜单 + RoleMenuDO parentMenu = randomPojo(RoleMenuDO.class).setRoleId(parentRole.getId()).setMenuId(101L); + roleMenuMapper.insert(parentMenu); + // 子角色排除父菜单(模拟排除表) + RoleMenuExclusionDO exclusion = new RoleMenuExclusionDO(); + exclusion.setRoleId(childRole.getId()); + exclusion.setMenuId(101L); + roleMenuExclusionMapper.insert(exclusion); + // 调用:获取子角色菜单(应不包含父菜单) + Set menuIds = permissionService.getRoleMenuListByRoleId(childRole.getId()); + assertFalse(menuIds.contains(101L)); + } + + @Test + public void testChildRoleRemoveExclusionThenInheritMenu() { + // mock 父子关系 A->B + RoleDO parentRole = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(parentRole); + RoleDO childRole = randomPojo(RoleDO.class, o -> o.setParentId(parentRole.getId())); + roleMapper.insert(childRole); + // 父角色分配菜单 + RoleMenuDO parentMenu = randomPojo(RoleMenuDO.class).setRoleId(parentRole.getId()).setMenuId(101L); + roleMenuMapper.insert(parentMenu); + // 子角色排除父菜单 + RoleMenuExclusionDO exclusion = new RoleMenuExclusionDO(); + exclusion.setRoleId(childRole.getId()); + exclusion.setMenuId(101L); + roleMenuExclusionMapper.insert(exclusion); + // 先断言排除 + Set menuIds = permissionService.getRoleMenuListByRoleId(childRole.getId()); + assertFalse(menuIds.contains(101L)); + // 取消排除 + roleMenuExclusionMapper.deleteListByRoleIdAndMenuIds(childRole.getId(), Collections.singleton(101L)); + // 再次获取,应能继承 + Set menuIds2 = permissionService.getRoleMenuListByRoleId(childRole.getId()); + assertTrue(menuIds2.contains(101L)); + } } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java index 8268cd89..d11ad7f0 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/RoleServiceImplTest.java @@ -390,4 +390,122 @@ public class RoleServiceImplTest extends BaseDbUnitTest { // 调用, 并断言异常 assertServiceException(() -> roleService.validateRoleList(ids), ROLE_IS_DISABLE, RoleDO.getName()); } + + @Test + public void testIsChildRole_trueAndFalse() { + // mock 3层父子关系 A->B->C + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + RoleDO roleC = randomPojo(RoleDO.class, o -> o.setParentId(roleB.getId())); + roleMapper.insert(roleC); + // 断言 C 的 parentId 是 A 的子孙节点 + assertTrue(roleService.getClass().cast(roleService).isChildRole(roleA.getId(), roleC.getId())); + // 断言 A 不是 C 的子孙节点 + assertFalse(roleService.getClass().cast(roleService).isChildRole(roleC.getId(), roleA.getId())); + } + + @Test + public void testGetAllParentAndSelfRoleIds_multiLevelAndLoop() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + // mock 3层父子关系 A->B->C + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + RoleDO roleC = randomPojo(RoleDO.class, o -> o.setParentId(roleB.getId())); + roleMapper.insert(roleC); + // 断言递归能拿到所有父节点 + Set ids = roleService.getAllParentAndSelfRoleIds(singleton(roleC.getId())); + assertTrue(ids.contains(roleA.getId())); + assertTrue(ids.contains(roleB.getId())); + assertTrue(ids.contains(roleC.getId())); + // mock 环路(C->A) + roleC.setParentId(roleA.getId()); + roleMapper.updateById(roleC); + // 不会死循环 + Set idsLoop = roleService.getAllParentAndSelfRoleIds(singleton(roleC.getId())); + assertTrue(idsLoop.contains(roleA.getId())); + } + } + + @Test + public void testHasAnyAdmin_trueAndFalse() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + // mock admin + RoleDO adminRole = randomPojo(RoleDO.class).setCode("super_admin"); + roleMapper.insert(adminRole); + assertTrue(roleService.hasAnyAdmin(singletonList(adminRole.getId()))); + // mock 普通 + RoleDO normalRole = randomPojo(RoleDO.class).setCode("user"); + roleMapper.insert(normalRole); + assertFalse(roleService.hasAnyAdmin(singletonList(normalRole.getId()))); + } + } + + @Test + public void testUpdateRole_parentIsChildException() { + // mock 2层父子关系 A->B + RoleDO roleA = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(roleA); + RoleDO roleB = randomPojo(RoleDO.class, o -> o.setParentId(roleA.getId())); + roleMapper.insert(roleB); + // 尝试把A的父节点改为B(形成环) + RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class, o -> o.setId(roleA.getId()).setParentId(roleB.getId())); + assertServiceException(() -> roleService.updateRole(reqVO), ROLE_PARENT_IS_CHILD, reqVO.getName()); + } + + @Test + public void testGetRoleListFromCache_empty() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + // 空集合 + List list = roleService.getRoleListFromCache(List.of()); + assertTrue(list.isEmpty()); + } + } + + @Test + public void testValidateRoleList_empty() { + // 空集合 + roleService.validateRoleList(List.of()); // 不抛异常 + } + + @Test + public void testUpdateRoleDataScope_roleNotExist() { + assertServiceException(() -> roleService.updateRoleDataScope(randomLongId(), 1, Set.of(1L)), ROLE_NOT_EXISTS); + } + + @Test + public void testDeleteRole_hasChildrenException() { + // mock 父子关系 + RoleDO parent = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(parent); + RoleDO child = randomPojo(RoleDO.class, o -> o.setParentId(parent.getId())); + roleMapper.insert(child); + assertServiceException(() -> roleService.deleteRole(parent.getId()), ROLE_CAN_NOT_DELETE_HAS_CHILDREN, parent.getName()); + } + + @Test + public void testDeleteRole_roleNotExist() { + assertServiceException(() -> roleService.deleteRole(randomLongId()), ROLE_NOT_EXISTS); + } + + @Test + public void testCreateRole_superAdminCodeError() { + RoleSaveReqVO reqVO = randomPojo(RoleSaveReqVO.class).setCode("super_admin"); + assertServiceException(() -> roleService.createRole(reqVO, null), ROLE_ADMIN_CODE_ERROR, "super_admin"); + } + + @Test + public void testValidateRoleDuplicate_codeEmpty() { + // name 不重复,code 为空 + roleService.validateRoleDuplicate(randomString(), "", null); // 不抛异常 + } } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java index 5d05c7b5..9b387c7d 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java @@ -18,11 +18,13 @@ import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; import cn.iocoder.yudao.module.system.dal.dataobject.userdept.UserDeptDO; +import cn.iocoder.yudao.module.system.dal.mysql.dept.DeptMapper; import cn.iocoder.yudao.module.system.dal.mysql.dept.UserPostMapper; import cn.iocoder.yudao.module.system.dal.mysql.user.AdminUserMapper; import cn.iocoder.yudao.module.system.dal.mysql.userdept.UserDeptMapper; import cn.iocoder.yudao.module.system.enums.common.SexEnum; import cn.iocoder.yudao.module.system.service.dept.DeptService; +import cn.iocoder.yudao.module.system.service.dept.DeptServiceImpl; import cn.iocoder.yudao.module.system.service.dept.PostService; import cn.iocoder.yudao.module.system.service.permission.PermissionService; import cn.iocoder.yudao.module.system.service.tenant.TenantService; @@ -56,7 +58,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -@Import({AdminUserServiceImpl.class,UserDeptServiceImpl.class}) +@Import({AdminUserServiceImpl.class,UserDeptServiceImpl.class,DeptServiceImpl.class}) public class AdminUserServiceImplTest extends BaseDbUnitTest { @Resource @@ -69,10 +71,11 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { @Resource private UserDeptMapper userDeptMapper; @Resource + private DeptMapper deptMapper; + @Resource private UserDeptServiceImpl userDeptService; - - @MockBean - private DeptService deptService; + @Resource + private DeptServiceImpl deptService; @MockBean private PostService postService; @MockBean @@ -99,7 +102,12 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); o.setMobile(randomString()); o.setPostIds(asSet(1L, 2L)); + o.setDeptIds(asSet(1L, 2L)); }).setId(null); // 避免 id 被赋值 + // 新建对应的部门 + deptMapper.insert(randomPojo(DeptDO.class, o -> {o.setId(1L);o.setStatus(CommonStatusEnum.ENABLE.getStatus());})); + deptMapper.insert(randomPojo(DeptDO.class, o -> {o.setId(2L);o.setStatus(CommonStatusEnum.ENABLE.getStatus());})); + // mock 账户额度充足 TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(1)); doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { @@ -147,16 +155,20 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { @Test public void testUpdateUser_success() { // mock 数据 - AdminUserDO dbUser = randomAdminUserDO(o -> o.setPostIds(asSet(1L, 2L))); + AdminUserDO dbUser = randomAdminUserDO(o -> o.setPostIds(asSet(1L, 2L)).setDeptIds(asSet(1L, 2L))); userMapper.insert(dbUser); userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(1L)); userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(2L)); + // 新增对应的部门 + deptMapper.insert(randomPojo(DeptDO.class, o -> {o.setId(1L);o.setStatus(CommonStatusEnum.ENABLE.getStatus());})); + deptMapper.insert(randomPojo(DeptDO.class, o -> {o.setId(2L);o.setStatus(CommonStatusEnum.ENABLE.getStatus());})); // 准备参数 UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class, o -> { o.setId(dbUser.getId()); o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); o.setMobile(randomString()); o.setPostIds(asSet(2L, 3L)); + o.setDeptIds(asSet(1L, 2L)); }); // mock postService 的方法 List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> @@ -299,7 +311,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { // 调用 AdminUserDO user = userService.getUserByUsername(username); // 断言 - assertPojoEquals(dbUser, user,"deptIds"); + assertPojoEquals(dbUser, user,"deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -313,7 +325,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { // 调用 AdminUserDO user = userService.getUserByMobile(mobile); // 断言 - assertPojoEquals(dbUser, user,"deptIds"); + assertPojoEquals(dbUser, user,"deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -330,7 +342,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { // reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 // mock 方法 List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); - when(deptService.getChildDeptList(eq(reqVO.getDeptId()))).thenReturn(deptList); + deptService.getChildDeptList(reqVO.getDeptId()); // 新增 1L 和用户关联关系 userDeptMapper.insert(new UserDeptDO().setUserId(dbUser.getId()).setDeptId(1L)); // 调用 @@ -338,7 +350,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { // 断言 assertEquals(1, pageResult.getTotal()); assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbUser, pageResult.getList().get(0), "deptIds"); + assertPojoEquals(dbUser, pageResult.getList().get(0), "deptIds", "companyIds", "companyDeptInfos"); } /** @@ -376,7 +388,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { // 调用 AdminUserDO user = userService.getUser(userId); // 断言 - assertPojoEquals(dbUser, user, "deptIds"); + assertPojoEquals(dbUser, user, "deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -397,7 +409,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { List list = userService.getUserListByDeptIds(deptIds); // 断言 assertEquals(1, list.size()); - assertPojoEquals(dbUser, list.get(0), "deptIds"); + assertPojoEquals(dbUser, list.get(0), "deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -520,7 +532,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { List result = userService.getUserListByPostIds(postIds); // 断言 assertEquals(1, result.size()); - assertPojoEquals(user1, result.get(0), "deptIds"); + assertPojoEquals(user1, result.get(0), "deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -539,7 +551,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { List result = userService.getUserList(ids); // 断言 assertEquals(1, result.size()); - assertPojoEquals(user, result.get(0), "deptIds"); + assertPojoEquals(user, result.get(0), "deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -558,7 +570,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { Map result = userService.getUserMap(ids); // 断言 assertEquals(1, result.size()); - assertPojoEquals(user, result.get(user.getId()), "deptIds"); + assertPojoEquals(user, result.get(user.getId()), "deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -575,7 +587,7 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { List result = userService.getUserListByNickname(nickname); // 断言 assertEquals(1, result.size()); - assertPojoEquals(user, result.get(0),"deptIds"); + assertPojoEquals(user, result.get(0),"deptIds", "companyIds", "companyDeptInfos"); } @Test @@ -588,15 +600,12 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { userMapper.insert(user2); // 准备参数 Integer status = CommonStatusEnum.DISABLE.getStatus(); - // 新增 1L 和用户关联关系 未关联 部门的用户无法被正确查询 - userDeptMapper.insert(new UserDeptDO().setUserId(user2.getId()).setDeptId(1L)); - userDeptMapper.insert(new UserDeptDO().setUserId(user.getId()).setDeptId(1L)); // 调用 List result = userService.getUserListByStatus(status); // 断言 assertEquals(1, result.size()); AdminUserDO user1 = userService.getUser(result.get(0).getId()); - assertPojoEquals(user, user1, "deptIds"); + assertPojoEquals(user, user1, "deptIds","companyIds","companyDeptInfos"); } @Test @@ -640,7 +649,8 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { Consumer consumer = (o) -> { o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 o.setSex(randomEle(SexEnum.values()).getSex()); - o.setDeptIds(new HashSet<>(asSet(1L, 2L))); // 保证 deptIds 的范围 + o.setDeptIds(new HashSet<>(asSet(1L, 2L))); + o.setCompanyDeptInfos(null);// 保证 deptIds 的范围 }; return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers)); } @@ -651,4 +661,88 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { return randomPojo(UserDeptDO.class, ArrayUtils.append(consumer, consumers)); } + @Test + public void testCreateUser_withMultipleDepts() { + // 构造带多个部门的用户 + UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class, o -> { + o.setDeptIds(asSet(1L, 2L, 3L)); + o.setSex(1); + }).setId(null); + // 新建对应的部门 + deptMapper.insert(new DeptDO().setId(1L).setName("部门1").setStatus(CommonStatusEnum.ENABLE.getStatus())); + deptMapper.insert(new DeptDO().setId(2L).setName("部门2").setStatus(CommonStatusEnum.ENABLE.getStatus())); + deptMapper.insert(new DeptDO().setId(3L).setName("部门3").setStatus(CommonStatusEnum.ENABLE.getStatus())); + // mock 账户额度充足、部门可用 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(1)); + doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { + handler.handle(tenant); + return true; + })); + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + when(passwordEncoder.encode(eq(reqVO.getPassword()))).thenReturn("yudaoyuanma"); + + // 调用 + Long userId = userService.createUser(reqVO); + // 校验 user_dept 表有3条数据 + List userDepts = userDeptMapper.selectValidListByUserIds(singleton(userId)); + assertEquals(3, userDepts.size()); + assertTrue(userDepts.stream().anyMatch(ud -> ud.getDeptId().equals(1L))); + assertTrue(userDepts.stream().anyMatch(ud -> ud.getDeptId().equals(2L))); + assertTrue(userDepts.stream().anyMatch(ud -> ud.getDeptId().equals(3L))); + } + + @Test + public void testUpdateUser_changeDepts() { + // 先插入用户和2个部门 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setDeptIds(asSet(1L, 2L))); + userMapper.insert(dbUser); + userDeptMapper.insert(new UserDeptDO().setUserId(dbUser.getId()).setDeptId(1L)); + userDeptMapper.insert(new UserDeptDO().setUserId(dbUser.getId()).setDeptId(2L)); + // 新建对应的部门 + deptMapper.insert(new DeptDO().setId(1L).setName("部门1").setStatus(CommonStatusEnum.ENABLE.getStatus())); + deptMapper.insert(new DeptDO().setId(2L).setName("部门2").setStatus(CommonStatusEnum.ENABLE.getStatus())); + // 更新为3个新部门 + UserSaveReqVO reqVO = randomPojo(UserSaveReqVO.class, o -> { + o.setId(dbUser.getId()); + o.setSex(1); + o.setDeptIds(asSet(1L, 2L)); + }); + // mock postService 的方法 + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + + // 调用 + userService.updateUser(reqVO); + // 校验 user_dept 表 + List userDepts = userDeptMapper.selectValidListByUserIds(singleton(dbUser.getId())); + assertEquals(2, userDepts.size()); + assertTrue(userDepts.stream().anyMatch(ud -> ud.getDeptId().equals(2L))); + assertTrue(userDepts.stream().anyMatch(ud -> ud.getDeptId().equals(1L))); + } + + @Test + public void testGetUser_withMultipleDepts() { + // 插入用户和多个部门 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 插入用户部门关系 + userDeptMapper.insert(new UserDeptDO().setUserId(dbUser.getId()).setDeptId(1L)); + userDeptMapper.insert(new UserDeptDO().setUserId(dbUser.getId()).setDeptId(2L)); + // 插入部门 + deptMapper.insert(new DeptDO().setId(1L).setName("部门1").setStatus(CommonStatusEnum.ENABLE.getStatus())); + deptMapper.insert(new DeptDO().setId(2L).setName("部门2").setStatus(CommonStatusEnum.ENABLE.getStatus())); + + AdminUserDO user = userService.getUser(dbUser.getId()); + assertTrue(user.getDeptIds().contains(1L)); + assertTrue(user.getDeptIds().contains(2L)); + } } diff --git a/yudao-module-template/yudao-module-template-api/src/main/java/cn/iocoder/yudao/module/template/enums/ErrorCodeConstants.java b/yudao-module-template/yudao-module-template-api/src/main/java/cn/iocoder/yudao/module/template/enums/ErrorCodeConstants.java index 0bc202af..5beb60fe 100644 --- a/yudao-module-template/yudao-module-template-api/src/main/java/cn/iocoder/yudao/module/template/enums/ErrorCodeConstants.java +++ b/yudao-module-template/yudao-module-template-api/src/main/java/cn/iocoder/yudao/module/template/enums/ErrorCodeConstants.java @@ -10,5 +10,7 @@ public interface ErrorCodeConstants { // ========== 模板样例 1_100_000_000 ========== ErrorCode EXAMPLE_NOT_EXISTS = new ErrorCode(1_100_000_000, "模板样例不存在"); + // ========== 合同 补充编号 ========== + ErrorCode DEMO_CONTRACT_NOT_EXISTS = new ErrorCode(2_100_000_000, "合同不存在"); } diff --git a/yudao-module-template/yudao-module-template-server/pom.xml b/yudao-module-template/yudao-module-template-server/pom.xml index 94a24185..ed3c29b0 100644 --- a/yudao-module-template/yudao-module-template-server/pom.xml +++ b/yudao-module-template/yudao-module-template-server/pom.xml @@ -121,6 +121,11 @@ cn.iocoder.cloud yudao-spring-boot-starter-monitor + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-business + ${revision} + diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java index 7ad6a4b7..4ae999b0 100644 --- a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java @@ -1,7 +1,10 @@ package cn.iocoder.yudao.module.template; +import cn.iocoder.yudao.framework.business.framework.BusinessDataPermissionConfiguration; +import cn.iocoder.yudao.framework.datapermission.config.YudaoCompanyDataPermissionAutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; /** * 项目的启动类 @@ -9,6 +12,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; * @author 周迪 */ @SpringBootApplication +@Import(BusinessDataPermissionConfiguration.class) public class TemplateServerApplication { public static void main(String[] args) { diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/DemoContractController.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/DemoContractController.java new file mode 100644 index 00000000..7c9b9f4a --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/DemoContractController.java @@ -0,0 +1,106 @@ +package cn.iocoder.yudao.module.template.controller.admin.contract; + +import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; +import cn.iocoder.yudao.framework.business.interceptor.BusinessControllerMarker; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.DemoContractPageReqVO; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.DemoContractRespVO; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.DemoContractSaveReqVO; +import cn.iocoder.yudao.module.template.dal.dataobject.contract.DemoContractDO; +import cn.iocoder.yudao.module.template.service.contract.DemoContractService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * @author chenbowen + */ +@Tag(name = "管理后台 - 合同") +@RestController +@RequestMapping("/template/demo-contract") +@Validated +public class DemoContractController implements BusinessControllerMarker { + + @Resource + private DemoContractService demoContractService; + + @PostMapping("/create") + @Operation(summary = "创建合同") + @PreAuthorize("@ss.hasPermission('template:demo-contract:create')") + public CommonResult createDemoContract(@Valid @RequestBody DemoContractSaveReqVO createReqVO) { + return success(demoContractService.createDemoContract(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新合同") + @PreAuthorize("@ss.hasPermission('template:demo-contract:update')") + public CommonResult updateDemoContract(@Valid @RequestBody DemoContractSaveReqVO updateReqVO) { + demoContractService.updateDemoContract(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除合同") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('template:demo-contract:delete')") + public CommonResult deleteDemoContract(@RequestParam("id") Long id) { + demoContractService.deleteDemoContract(id); + return success(true); + } + + @DeleteMapping("/delete-list") + @Parameter(name = "ids", description = "编号", required = true) + @Operation(summary = "批量删除合同") + @PreAuthorize("@ss.hasPermission('template:demo-contract:delete')") + public CommonResult deleteDemoContractList(@RequestParam("ids") List ids) { + demoContractService.deleteDemoContractListByIds(ids); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得合同") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('template:demo-contract:query')") + public CommonResult getDemoContract(@RequestParam("id") Long id) { + DemoContractDO demoContract = demoContractService.getDemoContract(id); + return success(BeanUtils.toBean(demoContract, DemoContractRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得合同分页") + @PreAuthorize("@ss.hasPermission('template:demo-contract:query')") + public CommonResult> getDemoContractPage(@Valid DemoContractPageReqVO pageReqVO) { + PageResult pageResult = demoContractService.getDemoContractPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, DemoContractRespVO.class)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出合同 Excel") + @PreAuthorize("@ss.hasPermission('template:demo-contract:export')") + @ApiAccessLog(operateType = EXPORT) + public void exportDemoContractExcel(@Valid DemoContractPageReqVO pageReqVO, + HttpServletResponse response) throws IOException { + pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE); + List list = demoContractService.getDemoContractPage(pageReqVO).getList(); + // 导出 Excel + ExcelUtils.write(response, "合同.xls", "数据", DemoContractRespVO.class, + BeanUtils.toBean(list, DemoContractRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractPageReqVO.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractPageReqVO.java new file mode 100644 index 00000000..6be5c5ca --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractPageReqVO.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.module.template.controller.admin.contract.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import java.math.BigDecimal; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 合同分页 Request VO") +@Data +public class DemoContractPageReqVO extends PageParam { + + @Schema(description = "合同编号") + private String code; + + @Schema(description = "合同名称", example = "李四") + private String name; + + @Schema(description = "合同状态", example = "1") + private Short status; + + @Schema(description = "流程实例ID", example = "24962") + private Long processInstanceId; + + @Schema(description = "签订日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] signDate; + + @Schema(description = "合同开始日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startDate; + + @Schema(description = "合同结束日期") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] endDate; + + @Schema(description = "合同金额") + private BigDecimal amount; + + @Schema(description = "备注", example = "随便") + private String remark; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "公司ID", example = "4180") + private Long companyId; + + @Schema(description = "公司名称", example = "张三") + private String companyName; + + @Schema(description = "部门ID", example = "1707") + private Long deptId; + + @Schema(description = "部门名称", example = "张三") + private String deptName; + + @Schema(description = "岗位ID", example = "26779") + private Long postId; + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractRespVO.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractRespVO.java new file mode 100644 index 00000000..8680fa21 --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractRespVO.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.template.controller.admin.contract.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import java.math.BigDecimal; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import com.alibaba.excel.annotation.*; + +@Schema(description = "管理后台 - 合同 Response VO") +@Data +@ExcelIgnoreUnannotated +public class DemoContractRespVO { + + @Schema(description = "合同ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "9541") + @ExcelProperty("合同ID") + private Long id; + + @Schema(description = "合同编号", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("合同编号") + private String code; + + @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @ExcelProperty("合同名称") + private String name; + + @Schema(description = "合同状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @ExcelProperty("合同状态") + private Short status; + + @Schema(description = "流程实例ID", example = "24962") + @ExcelProperty("流程实例ID") + private Long processInstanceId; + + @Schema(description = "签订日期") + @ExcelProperty("签订日期") + private LocalDateTime signDate; + + @Schema(description = "合同开始日期") + @ExcelProperty("合同开始日期") + private LocalDateTime startDate; + + @Schema(description = "合同结束日期") + @ExcelProperty("合同结束日期") + private LocalDateTime endDate; + + @Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("合同金额") + private BigDecimal amount; + + @Schema(description = "备注", example = "随便") + @ExcelProperty("备注") + private String remark; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @Schema(description = "公司ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "4180") + @ExcelProperty("公司ID") + private Long companyId; + + @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @ExcelProperty("公司名称") + private String companyName; + + @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1707") + @ExcelProperty("部门ID") + private Long deptId; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @ExcelProperty("部门名称") + private String deptName; + + @Schema(description = "岗位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "26779") + @ExcelProperty("岗位ID") + private Long postId; + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java new file mode 100644 index 00000000..84395532 --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.template.controller.admin.contract.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 合同新增/修改 Request VO") +@Data +public class DemoContractSaveReqVO { + + @Schema(description = "合同ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "9541") + private Long id; + + @Schema(description = "合同名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "合同名称不能为空") + private String name; + + @Schema(description = "签订日期") + private LocalDateTime signDate; + + @Schema(description = "合同开始日期") + private LocalDateTime startDate; + + @Schema(description = "合同结束日期") + private LocalDateTime endDate; + + @Schema(description = "合同金额", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "合同金额不能为空") + private BigDecimal amount; + + @Schema(description = "备注", example = "随便") + private String remark; + + @Schema(description = "公司ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "4180") + @NotNull(message = "公司ID不能为空") + private Long companyId; + + @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "公司名称不能为空") + private String companyName; + + @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1707") + @NotNull(message = "部门ID不能为空") + private Long deptId; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "部门名称不能为空") + private String deptName; + + @Schema(description = "岗位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "26779") + @NotNull(message = "岗位ID不能为空") + private Long postId; + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/dataobject/contract/DemoContractDO.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/dataobject/contract/DemoContractDO.java new file mode 100644 index 00000000..48253bbf --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/dataobject/contract/DemoContractDO.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.template.dal.dataobject.contract; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BusinessBaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 合同 DO + * + * @author 后台管理 + */ +@TableName("demo_contract") +@KeySequence("demo_contract_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DemoContractDO extends BusinessBaseDO { + + /** + * 合同ID + */ + @TableId(type = IdType.ASSIGN_ID) + private Long id; + /** + * 合同编号 + */ + private String code; + /** + * 合同名称 + */ + private String name; + /** + * 合同状态 + */ + private Short status; + /** + * 流程实例ID + */ + private Long processInstanceId; + /** + * 签订日期 + */ + private LocalDateTime signDate; + /** + * 合同开始日期 + */ + private LocalDateTime startDate; + /** + * 合同结束日期 + */ + private LocalDateTime endDate; + /** + * 合同金额 + */ + private BigDecimal amount; + /** + * 备注 + */ + private String remark; +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/mysql/contract/DemoContractMapper.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/mysql/contract/DemoContractMapper.java new file mode 100644 index 00000000..8e8f4ed0 --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/dal/mysql/contract/DemoContractMapper.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.template.dal.mysql.contract; + +import java.util.*; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.template.dal.dataobject.contract.DemoContractDO; +import org.apache.ibatis.annotations.Mapper; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.*; + +/** + * 合同 Mapper + * + * @author 后台管理 + */ +@Mapper +public interface DemoContractMapper extends BaseMapperX { + + default PageResult selectPage(DemoContractPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(DemoContractDO::getCode, reqVO.getCode()) + .likeIfPresent(DemoContractDO::getName, reqVO.getName()) + .eqIfPresent(DemoContractDO::getStatus, reqVO.getStatus()) + .eqIfPresent(DemoContractDO::getProcessInstanceId, reqVO.getProcessInstanceId()) + .betweenIfPresent(DemoContractDO::getSignDate, reqVO.getSignDate()) + .betweenIfPresent(DemoContractDO::getStartDate, reqVO.getStartDate()) + .betweenIfPresent(DemoContractDO::getEndDate, reqVO.getEndDate()) + .eqIfPresent(DemoContractDO::getAmount, reqVO.getAmount()) + .eqIfPresent(DemoContractDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(DemoContractDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(DemoContractDO::getCompanyId, reqVO.getCompanyId()) + .likeIfPresent(DemoContractDO::getCompanyName, reqVO.getCompanyName()) + .eqIfPresent(DemoContractDO::getDeptId, reqVO.getDeptId()) + .likeIfPresent(DemoContractDO::getDeptName, reqVO.getDeptName()) + .eqIfPresent(DemoContractDO::getPostId, reqVO.getPostId()) + .orderByDesc(DemoContractDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractService.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractService.java new file mode 100644 index 00000000..95c2f12c --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractService.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.template.service.contract; + +import java.util.*; +import jakarta.validation.*; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.*; +import cn.iocoder.yudao.module.template.dal.dataobject.contract.DemoContractDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +/** + * 合同 Service 接口 + * + * @author 后台管理 + */ +public interface DemoContractService { + + /** + * 创建合同 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoContract(@Valid DemoContractSaveReqVO createReqVO); + + /** + * 更新合同 + * + * @param updateReqVO 更新信息 + */ + void updateDemoContract(@Valid DemoContractSaveReqVO updateReqVO); + + /** + * 删除合同 + * + * @param id 编号 + */ + void deleteDemoContract(Long id); + + /** + * 批量删除合同 + * + * @param ids 编号 + */ + void deleteDemoContractListByIds(List ids); + + /** + * 获得合同 + * + * @param id 编号 + * @return 合同 + */ + DemoContractDO getDemoContract(Long id); + + /** + * 获得合同分页 + * + * @param pageReqVO 分页查询 + * @return 合同分页 + */ + PageResult getDemoContractPage(DemoContractPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractServiceImpl.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractServiceImpl.java new file mode 100644 index 00000000..9cbf56ee --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/service/contract/DemoContractServiceImpl.java @@ -0,0 +1,93 @@ +package cn.iocoder.yudao.module.template.service.contract; + +import cn.hutool.core.collection.CollUtil; +import org.springframework.stereotype.Service; +import jakarta.annotation.Resource; +import org.springframework.validation.annotation.Validated; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import cn.iocoder.yudao.module.template.controller.admin.contract.vo.*; +import cn.iocoder.yudao.module.template.dal.dataobject.contract.DemoContractDO; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; + +import cn.iocoder.yudao.module.template.dal.mysql.contract.DemoContractMapper; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.diffList; +import static cn.iocoder.yudao.module.template.enums.ErrorCodeConstants.*; + +/** + * 合同 Service 实现类 + * + * @author 后台管理 + */ +@Service +@Validated +public class DemoContractServiceImpl implements DemoContractService { + + @Resource + private DemoContractMapper demoContractMapper; + + @Override + public Long createDemoContract(DemoContractSaveReqVO createReqVO) { + // 插入 + DemoContractDO demoContract = BeanUtils.toBean(createReqVO, DemoContractDO.class); + demoContract.setCode("0"); + demoContractMapper.insert(demoContract); + // 返回 + return demoContract.getId(); + } + + @Override + public void updateDemoContract(DemoContractSaveReqVO updateReqVO) { + // 校验存在 + validateDemoContractExists(updateReqVO.getId()); + // 更新 + DemoContractDO updateObj = BeanUtils.toBean(updateReqVO, DemoContractDO.class); + demoContractMapper.updateById(updateObj); + } + + @Override + public void deleteDemoContract(Long id) { + // 校验存在 + validateDemoContractExists(id); + // 删除 + demoContractMapper.deleteById(id); + } + + @Override + public void deleteDemoContractListByIds(List ids) { + // 校验存在 + validateDemoContractExists(ids); + // 删除 + demoContractMapper.deleteByIds(ids); + } + + private void validateDemoContractExists(List ids) { + List list = demoContractMapper.selectByIds(ids); + if (CollUtil.isEmpty(list) || list.size() != ids.size()) { + throw exception(DEMO_CONTRACT_NOT_EXISTS); + } + } + + private void validateDemoContractExists(Long id) { + if (demoContractMapper.selectById(id) == null) { + throw exception(DEMO_CONTRACT_NOT_EXISTS); + } + } + + @Override + public DemoContractDO getDemoContract(Long id) { + return demoContractMapper.selectById(id); + } + + @Override + public PageResult getDemoContractPage(DemoContractPageReqVO pageReqVO) { + return demoContractMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/yudao-module-template/yudao-module-template-server/src/main/resources/application-local.yaml b/yudao-module-template/yudao-module-template-server/src/main/resources/application-local.yaml index 448db6e5..f3e02546 100644 --- a/yudao-module-template/yudao-module-template-server/src/main/resources/application-local.yaml +++ b/yudao-module-template/yudao-module-template-server/src/main/resources/application-local.yaml @@ -81,7 +81,7 @@ spring: xxl: job: admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 服务保障相关配置 #################### diff --git a/yudao-module-template/yudao-module-template-server/src/main/resources/mapper/contract/DemoContractMapper.xml b/yudao-module-template/yudao-module-template-server/src/main/resources/mapper/contract/DemoContractMapper.xml new file mode 100644 index 00000000..bfbb6357 --- /dev/null +++ b/yudao-module-template/yudao-module-template-server/src/main/resources/mapper/contract/DemoContractMapper.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 1ee2da10..970d03dd 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -91,7 +91,7 @@ xxl: job: enabled: false # 是否开启调度中心,默认为 true 开启 admin: - addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址 --- #################### 消息队列相关 #################### From eaea76e9553cd57034d10c7d7153c12c931f4b49 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Tue, 15 Jul 2025 10:01:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20=E9=83=A8=E9=97=A8=20=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90?= =?UTF-8?q?=E8=A7=84=E5=88=99=E6=94=AF=E6=8C=81=202.=20=E8=A1=A5=E5=85=A8?= =?UTF-8?q?=E5=AD=90=E8=A7=92=E8=89=B2=E6=8E=92=E9=99=A4=E7=88=B6=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=AE=A1=E7=90=86=E8=8F=9C=E5=8D=95=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pom.xml | 6 + .../BusinessDataPermissionConfiguration.java | 11 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../BusinessHeaderInterceptorTest.java | 238 ++++++++++++++++++ ...inessDataPermissionAutoConfiguration.java} | 22 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../system/enums/ErrorCodeConstants.java | 2 +- .../permission/PermissionServiceImpl.java | 53 ++-- .../permission/PermissionServiceTest.java | 25 ++ .../yudao-module-template-api/pom.xml | 20 ++ .../template/TemplateServerApplication.java | 4 - 11 files changed, 360 insertions(+), 26 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-biz-business/src/test/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptorTest.java rename yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/{YudaoCompanyDataPermissionAutoConfiguration.java => YudaoBusinessDataPermissionAutoConfiguration.java} (59%) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml index 958502de..e48b4d0b 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/pom.xml @@ -31,6 +31,12 @@ yudao-spring-boot-starter-biz-data-permission ${revision} + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + test + \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java index c09920d0..4a0b19d5 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.framework.business.framework; import cn.iocoder.yudao.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer; +import cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -10,10 +11,18 @@ import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) public class BusinessDataPermissionConfiguration { @Bean - public CompanyDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + public CompanyDataPermissionRuleCustomizer sysCompanyDataPermissionRuleCustomizer() { return rule -> { // companyId rule.addCompanyColumn("demo_contract", "company_id"); }; } + + @Bean + public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + return rule -> { + // dept + rule.addDeptColumn("demo_contract", "dept_id"); + }; + } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e476ed4c..f9e25597 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ -cn.iocoder.yudao.framework.business.config.YudaoBusinessAutoConfiguration \ No newline at end of file +cn.iocoder.yudao.framework.business.config.YudaoBusinessAutoConfiguration +cn.iocoder.yudao.framework.business.framework.BusinessDataPermissionConfiguration \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/test/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptorTest.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/test/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptorTest.java new file mode 100644 index 00000000..9abb63c8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/test/java/cn/iocoder/yudao/framework/business/interceptor/BusinessHeaderInterceptorTest.java @@ -0,0 +1,238 @@ +package cn.iocoder.yudao.framework.business.interceptor; + +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.pojo.CompanyDeptInfo; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.method.HandlerMethod; + +import java.io.PrintWriter; +import java.util.*; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.core.context.SecurityContextHolder.getContext; + +class BusinessHeaderInterceptorTest { + + private BusinessHeaderInterceptor interceptor; + private HttpServletRequest request; + private HttpServletResponse response; + private HandlerMethod handlerMethod; + private PrintWriter writer; + + @BeforeEach + void setUp() throws Exception { + interceptor = new BusinessHeaderInterceptor(); + request = mock(HttpServletRequest.class); + response = mock(HttpServletResponse.class); + handlerMethod = mock(HandlerMethod.class); + writer = mock(PrintWriter.class); + when(response.getWriter()).thenReturn(writer); + } + + /** + * 用例:传入的 handler 不是 HandlerMethod,应该直接返回 true + */ + @Test + void testPreHandle_NotHandlerMethod() throws Exception { + boolean result = interceptor.preHandle(request, response, new Object()); + assertTrue(result); + } + + /** + * 用例:handlerMethod.getBean() 不是 BusinessControllerMarker,应该直接返回 true + */ + @Test + void testPreHandle_NotBusinessControllerMarker() throws Exception { + when(handlerMethod.getBean()).thenReturn(new Object()); + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertTrue(result); + } + + /** + * 用例:handlerMethod.getBean() 是普通 Controller(未实现 marker 接口),应直接返回 true + */ + @Test + void testPreHandle_NormalController() throws Exception { + class NormalController {} + when(handlerMethod.getBean()).thenReturn(new NormalController()); + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertTrue(result); + } + + /** + * 用例:marker controller,且 header 无 companyId/deptId,loginUser 有多个公司部门,应该返回 false 并提示 NEED_ADJUST + */ + @Test + void testPreHandle_NoCompanyId_MultiCompanyDept() throws Exception { + class TestBusinessController implements BusinessControllerMarker {} + when(handlerMethod.getBean()).thenReturn(new TestBusinessController()); + when(request.getHeader("visit-company-id")).thenReturn(null); + when(request.getHeader("visit-dept-id")).thenReturn(null); + + // 构造 loginUser,包含多个公司部门 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + Map infoMap = new HashMap<>(); + infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, "[{\"companyId\":1,\"deptId\":2},{\"companyId\":2,\"deptId\":3}]"); + loginUser.setInfo(infoMap); + + // 通过反射或包可见性设置 getLoginUser 返回 + setLoginUserForTest(loginUser); + + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertFalse(result); + verify(writer).write(contains("400")); + } + + /** + * 用例:header 有 companyId/deptId,loginUser 有多个公司部门,应该正常通过 + */ + @Test + void testPreHandle_WithCompanyIdDeptId_MultiCompanyDept() throws Exception { + class TestBusinessController implements BusinessControllerMarker {} + when(handlerMethod.getBean()).thenReturn(new TestBusinessController()); + when(request.getHeader("visit-company-id")).thenReturn("1"); + when(request.getHeader("visit-dept-id")).thenReturn("2"); + + // 构造 loginUser,包含多个公司部门 + CompanyDeptInfo deptInfo1 = new CompanyDeptInfo(); + deptInfo1.setCompanyId(1L); + deptInfo1.setDeptId(2L); + CompanyDeptInfo deptInfo2 = new CompanyDeptInfo(); + deptInfo2.setCompanyId(2L); + deptInfo2.setDeptId(3L); + Set deptSet = new HashSet<>(); + deptSet.add(deptInfo1); + deptSet.add(deptInfo2); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + String deptSetJson = "[" + + "{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," + + "{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]"; + Map infoMap = new HashMap<>(); + infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson); + loginUser.setInfo(infoMap); + setLoginUserForTest(loginUser); + + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertTrue(result); + } + + /** + * 用例:header 无 companyId/deptId,loginUser 只有一个公司部门,应该自动填充 header 并通过 + */ + @Test + void testPreHandle_NoHeader_SingleCompanyDept() throws Exception { + class TestBusinessController implements BusinessControllerMarker {} + when(handlerMethod.getBean()).thenReturn(new TestBusinessController()); + when(request.getHeader("visit-company-id")).thenReturn(null); + when(request.getHeader("visit-dept-id")).thenReturn(null); + + // 构造 loginUser,只有一个公司且公司下只有一个部门 + CompanyDeptInfo deptInfo = new CompanyDeptInfo(); + deptInfo.setCompanyId(100L); + deptInfo.setDeptId(200L); + Set deptSet = new HashSet<>(); + deptSet.add(deptInfo); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + // 只放一个公司部门 + String deptSetJson = "[{\"companyId\":" + deptInfo.getCompanyId() + ",\"deptId\":" + deptInfo.getDeptId() + "}]"; + Map infoMap = new HashMap<>(); + infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson); + loginUser.setInfo(infoMap); + setLoginUserForTest(loginUser); + + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertFalse(result); + // 可选:verify(request).setAttribute("visit-company-id", String.valueOf(deptInfo.getCompanyId())); + // 可选:verify(request).setAttribute("visit-dept-id", String.valueOf(deptInfo.getDeptId())); + } + + /** + * 用例:header 无 companyId/deptId,loginUser 有多个公司部门,应该返回 false 并提示 400 + */ + @Test + void testPreHandle_NoHeader_MultiCompanyDept() throws Exception { + class TestBusinessController implements BusinessControllerMarker {} + when(handlerMethod.getBean()).thenReturn(new TestBusinessController()); + when(request.getHeader("visit-company-id")).thenReturn(null); + when(request.getHeader("visit-dept-id")).thenReturn(null); + + // 构造 loginUser,多个公司部门 + CompanyDeptInfo deptInfo1 = new CompanyDeptInfo(); + deptInfo1.setCompanyId(1L); + deptInfo1.setDeptId(2L); + CompanyDeptInfo deptInfo2 = new CompanyDeptInfo(); + deptInfo2.setCompanyId(2L); + deptInfo2.setDeptId(3L); + Set deptSet = new HashSet<>(); + deptSet.add(deptInfo1); + deptSet.add(deptInfo2); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + String deptSetJson = "[" + + "{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," + + "{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]"; + Map infoMap = new HashMap<>(); + infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson); + loginUser.setInfo(infoMap); + setLoginUserForTest(loginUser); + + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertFalse(result); + verify(writer).write(contains("400")); + } + + /** + * 用例:header 有错误的 companyId/deptId,loginUser 不包含该公司部门,应该返回 false 并提示 400 + */ + @Test + void testPreHandle_HeaderNotMatchUserCompanyDept() throws Exception { + class TestBusinessController implements BusinessControllerMarker {} + when(handlerMethod.getBean()).thenReturn(new TestBusinessController()); + when(request.getHeader("visit-company-id")).thenReturn("999"); + when(request.getHeader("visit-dept-id")).thenReturn("888"); + + // 构造 loginUser,只有其他公司部门 + CompanyDeptInfo deptInfo1 = new CompanyDeptInfo(); + deptInfo1.setCompanyId(1L); + deptInfo1.setDeptId(2L); + CompanyDeptInfo deptInfo2 = new CompanyDeptInfo(); + deptInfo2.setCompanyId(2L); + deptInfo2.setDeptId(3L); + Set deptSet = new HashSet<>(); + deptSet.add(deptInfo1); + deptSet.add(deptInfo2); + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + String deptSetJson = "[" + + "{\"companyId\":" + deptInfo1.getCompanyId() + ",\"deptId\":" + deptInfo1.getDeptId() + "}," + + "{\"companyId\":" + deptInfo2.getCompanyId() + ",\"deptId\":" + deptInfo2.getDeptId() + "}]"; + Map infoMap = new HashMap<>(); + infoMap.put(LoginUser.INFO_KEY_COMPANY_DEPT_SET, deptSetJson); + loginUser.setInfo(infoMap); + setLoginUserForTest(loginUser); + + boolean result = interceptor.preHandle(request, response, handlerMethod); + assertFalse(result); + verify(writer).write(contains("400")); + } + + // 工具方法:通过 Spring Security 设置当前登录用户,仅测试环境使用 + private void setLoginUserForTest(LoginUser loginUser) { + // 使用 Spring Security 的 SecurityContextHolder 设置 Authentication + getContext() + .setAuthentication(new UsernamePasswordAuthenticationToken( + loginUser, null, null + )); + } +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java similarity index 59% rename from yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java rename to yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java index bef0e962..1a806c43 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoCompanyDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java @@ -21,8 +21,8 @@ import java.util.List; */ @AutoConfiguration @ConditionalOnClass(LoginUser.class) -@ConditionalOnBean(value = {CompanyDataPermissionRuleCustomizer.class}) -public class YudaoCompanyDataPermissionAutoConfiguration { +@ConditionalOnBean(value = {CompanyDataPermissionRuleCustomizer.class, DeptDataPermissionRuleCustomizer.class}) +public class YudaoBusinessDataPermissionAutoConfiguration { @Bean public CompanyDataPermissionRule companyDataPermissionRule(List customizers) { @@ -33,4 +33,22 @@ public class YudaoCompanyDataPermissionAutoConfiguration { customizers.forEach(customizer -> customizer.customize(rule)); return rule; } + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, List customizers) { + // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用 + // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误 + try { + PermissionCommonApi permissionApiImpl = SpringUtil.getBean("permissionApiImpl", PermissionCommonApi.class); + if (permissionApiImpl != null) { + permissionApi = permissionApiImpl; + } + } catch (Exception ignored) {} + + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 81ba9b1a..b2d320cc 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,4 +1,4 @@ cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionAutoConfiguration cn.iocoder.yudao.framework.datapermission.config.YudaoDeptDataPermissionAutoConfiguration -cn.iocoder.yudao.framework.datapermission.config.YudaoCompanyDataPermissionAutoConfiguration +cn.iocoder.yudao.framework.datapermission.config.YudaoBusinessDataPermissionAutoConfiguration cn.iocoder.yudao.framework.datapermission.config.YudaoDataPermissionRpcAutoConfiguration diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java index 62073153..2d66a7f8 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java @@ -34,7 +34,7 @@ public interface ErrorCodeConstants { ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1_002_002_003, "不能操作类型为系统内置的角色"); ErrorCode ROLE_IS_DISABLE = new ErrorCode(1_002_002_004, "名字为【{}】的角色已被禁用"); ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1_002_002_005, "标识【{}】不能使用"); - ErrorCode ROLE_CAN_NOT_UPDATE_NORMAL_TYPE_ROLE = new ErrorCode(1_002_002_006, "不能操作类型为标准的角色,除非是管理员角色"); + ErrorCode ROLE_CAN_NOT_UPDATE_NORMAL_TYPE_ROLE = new ErrorCode(1_002_002_006, "非管理员,不能操作类型为标准的角色"); ErrorCode ROLE_CAN_NOT_DELETE_HAS_CHILDREN = new ErrorCode(1_002_002_007, " 角色【{}】存在子角色,不允许删除"); ErrorCode ROLE_PARENT_IS_CHILD = new ErrorCode(1_002_002_008, "不能设置自己的子角色为父角色"); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java index 45ff2d28..6b3826d7 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceImpl.java @@ -157,29 +157,50 @@ public class PermissionServiceImpl implements PermissionService { allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 }) public void assignRoleMenu(Long roleId, Set menuIds) { - RoleDO role = roleService.getRole(roleId); - Set userRoleIdListByUserId = permissionService.getUserRoleIdListByUserId(getLoginUserId()); - // 如果为标准角色,只允许管理员修改菜单权限 - if (RoleTypeEnum.NORMAL.getType().equals(role.getType()) && !roleService.hasAnySuperAdmin(userRoleIdListByUserId)) { - throw exception(ROLE_CAN_NOT_UPDATE_NORMAL_TYPE_ROLE); - } // 获得角色拥有菜单编号 - Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); + Set dbMenuIds = convertSet(getRoleMenuListByRoleId(roleId)); + // 获取父级角色拥有的菜单编号 + Set parentRoleIds = roleService.getAllParentAndSelfRoleIds(singleton(roleId)); + // 移除自身角色编号 + parentRoleIds.remove(roleId); + Set dbInheritedMenuIds = convertSet(roleMenuMapper.selectListByRoleId(parentRoleIds), RoleMenuDO::getMenuId); // 计算新增和删除的菜单编号 Set menuIdList = CollUtil.emptyIfNull(menuIds); Collection createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds); Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList); - // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + // 执行新增和删除。对于已经授权的菜单,不用进行新增和删除,处理排除关系即可 if (CollUtil.isNotEmpty(createMenuIds)) { - roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { - RoleMenuDO entity = new RoleMenuDO(); - entity.setRoleId(roleId); - entity.setMenuId(menuId); - return entity; - })); + Set inheritedCreateMenuIds = new HashSet<>(dbInheritedMenuIds); + inheritedCreateMenuIds.retainAll(createMenuIds); + if (CollUtil.isNotEmpty(inheritedCreateMenuIds)) { + // 不需要新增,只需要检查是否存在排除关系,如果存在,则标记排除关系失效 + roleMenuExclusionMapper.deleteListByRoleIdAndMenuIds(roleId, inheritedCreateMenuIds); + createMenuIds.removeAll(inheritedCreateMenuIds); + } + if (CollUtil.isNotEmpty(createMenuIds)) { + roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { + RoleMenuDO entity = new RoleMenuDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + return entity; + })); + } } if (CollUtil.isNotEmpty(deleteMenuIds)) { - roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); + Set inheritedDeleteMenuIds = new HashSet<>(dbInheritedMenuIds); + inheritedDeleteMenuIds.retainAll(deleteMenuIds); + if (CollUtil.isNotEmpty(inheritedDeleteMenuIds)) { + // 标记排除 + roleMenuExclusionMapper.insertBatch(CollectionUtils.convertList(inheritedDeleteMenuIds, menuId -> { + RoleMenuExclusionDO entity = new RoleMenuExclusionDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + return entity; + })); + } + if (CollUtil.isNotEmpty(deleteMenuIds)) { + roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); + } } } @@ -303,7 +324,7 @@ public class PermissionServiceImpl implements PermissionService { Set userRoleIdListByUserId = permissionService.getUserRoleIdListByUserId(getLoginUserId()); // 如果为标准角色,只允许管理员修改数据权限 if (RoleTypeEnum.NORMAL.getType().equals(role.getType()) && !roleService.hasAnySuperAdmin(userRoleIdListByUserId)) { - throw exception(ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + throw exception(ROLE_CAN_NOT_UPDATE_NORMAL_TYPE_ROLE); } roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); } diff --git a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java index c82ac6dd..05c1dd0e 100644 --- a/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java +++ b/yudao-module-system/yudao-module-system-server/src/test/java/cn/iocoder/yudao/module/system/service/permission/PermissionServiceTest.java @@ -383,4 +383,29 @@ public class PermissionServiceTest extends BaseDbUnitTest { assertTrue(menuIds2.contains(101L)); } + + /** + * 测试子角色排除父角色菜单 + * 通过 Service 方法排除,确保子角色不继承父角色的菜单 + */ + @Test + public void testExcludeParentRoleMenu() { + // mock 父子关系 A->B + RoleDO parentRole = randomPojo(RoleDO.class, o -> o.setParentId(0L)); + roleMapper.insert(parentRole); + RoleDO childRole = randomPojo(RoleDO.class, o -> o.setParentId(parentRole.getId())); + roleMapper.insert(childRole); + // 父角色分配菜单 + RoleMenuDO parentMenu = randomPojo(RoleMenuDO.class).setRoleId(parentRole.getId()).setMenuId(101L); + roleMenuMapper.insert(parentMenu); + // 子角色排除父菜单(通过 Service 方法排除) + permissionService.assignRoleMenu(childRole.getId(), Collections.emptySet()); + // 调用:获取子角色菜单(应不包含父菜单) + Set menuIds = permissionService.getRoleMenuListByRoleId(childRole.getId()); + assertFalse(menuIds.contains(101L)); + // 新增了子角色的排除菜单记录 + List exclusionDOS = roleMenuExclusionMapper.selectMenuIdListByRoleId(Collections.singleton(childRole.getId())); + assertEquals(1, exclusionDOS.size()); + assertEquals(101L, exclusionDOS.get(0).getMenuId()); + } } diff --git a/yudao-module-template/yudao-module-template-api/pom.xml b/yudao-module-template/yudao-module-template-api/pom.xml index 4ae34fec..173dcb94 100644 --- a/yudao-module-template/yudao-module-template-api/pom.xml +++ b/yudao-module-template/yudao-module-template-api/pom.xml @@ -21,6 +21,26 @@ cn.iocoder.cloud yudao-common + + + org.springdoc + springdoc-openapi-starter-webmvc-api + provided + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java index 4ae999b0..7ad6a4b7 100644 --- a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/TemplateServerApplication.java @@ -1,10 +1,7 @@ package cn.iocoder.yudao.module.template; -import cn.iocoder.yudao.framework.business.framework.BusinessDataPermissionConfiguration; -import cn.iocoder.yudao.framework.datapermission.config.YudaoCompanyDataPermissionAutoConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Import; /** * 项目的启动类 @@ -12,7 +9,6 @@ import org.springframework.context.annotation.Import; * @author 周迪 */ @SpringBootApplication -@Import(BusinessDataPermissionConfiguration.class) public class TemplateServerApplication { public static void main(String[] args) {