feat(tenant): 租户-项目两级架构 Phase 0+1 — 基础框架层
Phase 0 技术验证: - ProjectBaseDO extends TenantBaseDO,新增 projectId 字段 - ProjectContextHolder (TransmittableThreadLocal) 项目上下文管理 - ProjectDatabaseInterceptor 实现 TenantLineHandler,返回 project_id 列 - 注册第二个 TenantLineInnerInterceptor,通过 @Qualifier 保证初始化顺序 - DualInterceptorTest 11 个用例验证双拦截器 SQL 注入(SELECT/INSERT/UPDATE/DELETE + JOIN + 子查询) Phase 1 基础框架层: - @ProjectIgnore 注解 + ProjectIgnoreAspect (SpEL 条件支持) - ProjectUtils 工具类 (execute/executeIgnore) - ProjectContextWebFilter 从请求 Header 解析 project-id - WebFrameworkUtils 扩展 HEADER_PROJECT_ID + getProjectId() - WebFilterOrderEnum 新增 PROJECT_CONTEXT_FILTER、PROJECT_SECURITY_FILTER - RPC: TenantRequestInterceptor 自动透传 project-id - MQ: Kafka/RocketMQ/RabbitMQ/Redis 全部支持 project-id 发送与消费 - @ProjectJob + ProjectJobAspect (@Order(2) 内层,配合 @TenantJob 使用) - TenantJobAspect 补充 @Order(1) 外层标记 - ProjectDO + UserProjectDO + Mapper + ProjectService + ProjectController - ProjectCommonApi (Feign) + ProjectApiImpl + ProjectFrameworkServiceImpl (Guava 缓存) - TenantServiceImpl.createTenant() 联动创建默认项目 - ErrorCodeConstants 新增 1-002-030-xxx 项目错误码 Review 修复: - Bean 初始化顺序: projectLineInnerInterceptor 依赖 @Qualifier 确保顺序 - computeIgnoreTable: @ProjectIgnore 检查优先于 isAssignableFrom - ProjectFrameworkServiceImpl 注册为 Spring Bean - RocketMQ SendHook: project-id 独立于 tenantId 传播 - createDefaultProject 移入 TenantUtils.execute 事务块内 - 全部 MQ/RPC 统一使用 HEADER_PROJECT_ID 常量 - ProjectJobAspect 增加租户上下文防御校验 - 移除 ProjectDO/UserProjectDO 无效的 @KeySequence - ProjectServiceImpl/ProjectApiImpl 移除冗余 TenantUtils.execute 嵌套 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package com.viewsh.module.system.api.project;
|
||||
|
||||
import com.viewsh.framework.common.biz.system.project.ProjectCommonApi;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.system.service.project.ProjectService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@RestController // 提供 RESTful API 接口,给 Feign 调用
|
||||
@Validated
|
||||
public class ProjectApiImpl implements ProjectCommonApi {
|
||||
|
||||
@Resource
|
||||
private ProjectService projectService;
|
||||
|
||||
@Override
|
||||
public CommonResult<List<Long>> getProjectIdList() {
|
||||
// 依赖框架的 TenantContextWebFilter 已设置的租户上下文
|
||||
List<Long> projectIds = projectService.getProjectIdsByTenantId(null);
|
||||
return success(projectIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> validProject(Long id) {
|
||||
projectService.validProject(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.viewsh.module.system.controller.admin.project;
|
||||
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectPageReqVO;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectRespVO;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectSaveReqVO;
|
||||
import com.viewsh.module.system.dal.dataobject.project.ProjectDO;
|
||||
import com.viewsh.module.system.service.project.ProjectService;
|
||||
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.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
import static com.viewsh.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
@Tag(name = "管理后台 - 项目管理")
|
||||
@RestController
|
||||
@RequestMapping("/system/project")
|
||||
public class ProjectController {
|
||||
|
||||
@Resource
|
||||
private ProjectService projectService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建项目")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:create')")
|
||||
public CommonResult<Long> createProject(@Valid @RequestBody ProjectSaveReqVO createReqVO) {
|
||||
return success(projectService.createProject(createReqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新项目")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:update')")
|
||||
public CommonResult<Boolean> updateProject(@Valid @RequestBody ProjectSaveReqVO updateReqVO) {
|
||||
projectService.updateProject(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除项目")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:delete')")
|
||||
public CommonResult<Boolean> deleteProject(@RequestParam("id") Long id) {
|
||||
projectService.deleteProject(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得项目")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:query')")
|
||||
public CommonResult<ProjectRespVO> getProject(@RequestParam("id") Long id) {
|
||||
ProjectDO project = projectService.getProject(id);
|
||||
return success(BeanUtils.toBean(project, ProjectRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得项目分页")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:query')")
|
||||
public CommonResult<PageResult<ProjectRespVO>> getProjectPage(@Valid ProjectPageReqVO pageVO) {
|
||||
PageResult<ProjectDO> pageResult = projectService.getProjectPage(pageVO);
|
||||
return success(BeanUtils.toBean(pageResult, ProjectRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/simple-list")
|
||||
@Operation(summary = "获取项目精简信息列表", description = "只包含被开启的项目,用于下拉选择")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:query')")
|
||||
public CommonResult<List<ProjectRespVO>> getProjectSimpleList() {
|
||||
List<ProjectDO> list = projectService.getProjectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
return success(convertList(list, project ->
|
||||
new ProjectRespVO().setId(project.getId()).setName(project.getName()).setCode(project.getCode())));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.viewsh.module.system.controller.admin.project.vo;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
@Schema(description = "管理后台 - 项目分页 Request VO")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class ProjectPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "项目名称", example = "某某大厦")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "项目编码", example = "PROJ_001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "项目状态(0正常 1停用)", example = "0")
|
||||
private Integer status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.viewsh.module.system.controller.admin.project.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "管理后台 - 项目 Response VO")
|
||||
@Data
|
||||
public class ProjectRespVO {
|
||||
|
||||
@Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "某某大厦")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "项目编码", example = "PROJ_001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "项目状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "联系人姓名", example = "张三")
|
||||
private String contactName;
|
||||
|
||||
@Schema(description = "联系手机", example = "15601691300")
|
||||
private String contactMobile;
|
||||
|
||||
@Schema(description = "项目地址", example = "北京市朝阳区某某街道")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "备注", example = "这是备注信息")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.viewsh.module.system.controller.admin.project.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "管理后台 - 项目创建/修改 Request VO")
|
||||
@Data
|
||||
public class ProjectSaveReqVO {
|
||||
|
||||
@Schema(description = "项目编号", example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "某某大厦")
|
||||
@NotBlank(message = "项目名称不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "项目编码", example = "PROJ_001")
|
||||
private String code;
|
||||
|
||||
@Schema(description = "项目状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
@NotNull(message = "项目状态不能为空")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "联系人姓名", example = "张三")
|
||||
private String contactName;
|
||||
|
||||
@Schema(description = "联系手机", example = "15601691300")
|
||||
private String contactMobile;
|
||||
|
||||
@Schema(description = "项目地址", example = "北京市朝阳区某某街道")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "备注", example = "这是备注信息")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.viewsh.module.system.dal.dataobject.project;
|
||||
|
||||
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 项目 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("system_project")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ProjectDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 默认项目编码
|
||||
*/
|
||||
public static final String CODE_DEFAULT = "DEFAULT";
|
||||
|
||||
/**
|
||||
* 项目编号,自增
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 项目名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 项目编码,租户内唯一
|
||||
*/
|
||||
private String code;
|
||||
/**
|
||||
* 项目状态
|
||||
*
|
||||
* 枚举 {@link com.viewsh.framework.common.enums.CommonStatusEnum}
|
||||
*/
|
||||
private Integer status;
|
||||
/**
|
||||
* 联系人姓名
|
||||
*/
|
||||
private String contactName;
|
||||
/**
|
||||
* 联系手机
|
||||
*/
|
||||
private String contactMobile;
|
||||
/**
|
||||
* 项目地址
|
||||
*/
|
||||
private String address;
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.viewsh.module.system.dal.dataobject.project;
|
||||
|
||||
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 用户项目关联 DO
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@TableName("system_user_project")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UserProjectDO extends TenantBaseDO {
|
||||
|
||||
/**
|
||||
* 编号,自增
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 用户编号
|
||||
*
|
||||
* 关联 {@link com.viewsh.module.system.dal.dataobject.user.AdminUserDO#getId()}
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 项目编号
|
||||
*
|
||||
* 关联 {@link ProjectDO#getId()}
|
||||
*/
|
||||
private Long projectId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.viewsh.module.system.dal.mysql.project;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectPageReqVO;
|
||||
import com.viewsh.module.system.dal.dataobject.project.ProjectDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ProjectMapper extends BaseMapperX<ProjectDO> {
|
||||
|
||||
default PageResult<ProjectDO> selectPage(ProjectPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<ProjectDO>()
|
||||
.likeIfPresent(ProjectDO::getName, reqVO.getName())
|
||||
.likeIfPresent(ProjectDO::getCode, reqVO.getCode())
|
||||
.eqIfPresent(ProjectDO::getStatus, reqVO.getStatus())
|
||||
.orderByDesc(ProjectDO::getId));
|
||||
}
|
||||
|
||||
default ProjectDO selectByName(String name) {
|
||||
return selectOne(ProjectDO::getName, name);
|
||||
}
|
||||
|
||||
default ProjectDO selectByCode(String code) {
|
||||
return selectOne(ProjectDO::getCode, code);
|
||||
}
|
||||
|
||||
default List<ProjectDO> selectListByStatus(Integer status) {
|
||||
return selectList(ProjectDO::getStatus, status);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.viewsh.module.system.dal.mysql.project;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.system.dal.dataobject.project.UserProjectDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface UserProjectMapper extends BaseMapperX<UserProjectDO> {
|
||||
|
||||
default List<UserProjectDO> selectListByUserId(Long userId) {
|
||||
return selectList(UserProjectDO::getUserId, userId);
|
||||
}
|
||||
|
||||
default List<UserProjectDO> selectListByProjectId(Long projectId) {
|
||||
return selectList(UserProjectDO::getProjectId, projectId);
|
||||
}
|
||||
|
||||
default void deleteByUserIdAndProjectId(Long userId, Long projectId) {
|
||||
delete(new LambdaQueryWrapperX<UserProjectDO>()
|
||||
.eq(UserProjectDO::getUserId, userId)
|
||||
.eq(UserProjectDO::getProjectId, projectId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.viewsh.module.system.service.project;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectPageReqVO;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectSaveReqVO;
|
||||
import com.viewsh.module.system.dal.dataobject.project.ProjectDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface ProjectService {
|
||||
|
||||
/**
|
||||
* 创建项目
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 项目编号
|
||||
*/
|
||||
Long createProject(@Valid ProjectSaveReqVO createReqVO);
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
*
|
||||
* @param updateReqVO 更新信息
|
||||
*/
|
||||
void updateProject(@Valid ProjectSaveReqVO updateReqVO);
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteProject(Long id);
|
||||
|
||||
/**
|
||||
* 获得项目
|
||||
*
|
||||
* @param id 编号
|
||||
* @return 项目
|
||||
*/
|
||||
ProjectDO getProject(Long id);
|
||||
|
||||
/**
|
||||
* 获得项目分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 项目分页
|
||||
*/
|
||||
PageResult<ProjectDO> getProjectPage(ProjectPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 获得指定状态的项目列表
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 项目列表
|
||||
*/
|
||||
List<ProjectDO> getProjectListByStatus(Integer status);
|
||||
|
||||
/**
|
||||
* 根据编码获得项目
|
||||
*
|
||||
* @param code 项目编码
|
||||
* @return 项目
|
||||
*/
|
||||
ProjectDO getProjectByCode(String code);
|
||||
|
||||
/**
|
||||
* 校验项目是否合法(存在且启用)
|
||||
*
|
||||
* @param id 项目编号
|
||||
*/
|
||||
void validProject(Long id);
|
||||
|
||||
/**
|
||||
* 获得租户下所有项目编号
|
||||
*
|
||||
* @param tenantId 租户编号
|
||||
* @return 项目编号列表
|
||||
*/
|
||||
List<Long> getProjectIdsByTenantId(Long tenantId);
|
||||
|
||||
/**
|
||||
* 创建默认项目(租户创建时自动调用)
|
||||
*
|
||||
* @param tenantId 租户编号
|
||||
* @param tenantName 租户名称(用于默认项目名称)
|
||||
* @return 项目编号
|
||||
*/
|
||||
Long createDefaultProject(Long tenantId, String tenantName);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.viewsh.module.system.service.project;
|
||||
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.collection.CollectionUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectPageReqVO;
|
||||
import com.viewsh.module.system.controller.admin.project.vo.ProjectSaveReqVO;
|
||||
import com.viewsh.module.system.dal.dataobject.project.ProjectDO;
|
||||
import com.viewsh.module.system.dal.dataobject.project.UserProjectDO;
|
||||
import com.viewsh.module.system.dal.dataobject.user.AdminUserDO;
|
||||
import com.viewsh.module.system.dal.mysql.project.ProjectMapper;
|
||||
import com.viewsh.module.system.dal.mysql.project.UserProjectMapper;
|
||||
import com.viewsh.module.system.service.user.AdminUserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.system.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 项目 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
@Resource
|
||||
private ProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
private UserProjectMapper userProjectMapper;
|
||||
|
||||
@Resource
|
||||
@Lazy // 延迟,避免循环依赖报错
|
||||
private AdminUserService userService;
|
||||
|
||||
@Override
|
||||
public Long createProject(ProjectSaveReqVO createReqVO) {
|
||||
// 校验项目名称是否重复
|
||||
validProjectNameDuplicate(createReqVO.getName(), null);
|
||||
// 校验项目编码是否重复
|
||||
if (createReqVO.getCode() != null) {
|
||||
validProjectCodeDuplicate(createReqVO.getCode(), null);
|
||||
}
|
||||
// 插入
|
||||
ProjectDO project = BeanUtils.toBean(createReqVO, ProjectDO.class);
|
||||
projectMapper.insert(project);
|
||||
return project.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateProject(ProjectSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateProjectExists(updateReqVO.getId());
|
||||
// 校验项目名称是否重复
|
||||
validProjectNameDuplicate(updateReqVO.getName(), updateReqVO.getId());
|
||||
// 校验项目编码是否重复
|
||||
if (updateReqVO.getCode() != null) {
|
||||
validProjectCodeDuplicate(updateReqVO.getCode(), updateReqVO.getId());
|
||||
}
|
||||
// 更新
|
||||
ProjectDO updateObj = BeanUtils.toBean(updateReqVO, ProjectDO.class);
|
||||
projectMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteProject(Long id) {
|
||||
// 校验存在
|
||||
validateProjectExists(id);
|
||||
// 删除
|
||||
projectMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectDO getProject(Long id) {
|
||||
return projectMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<ProjectDO> getProjectPage(ProjectPageReqVO pageReqVO) {
|
||||
return projectMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProjectDO> getProjectListByStatus(Integer status) {
|
||||
return projectMapper.selectListByStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectDO getProjectByCode(String code) {
|
||||
return projectMapper.selectByCode(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validProject(Long id) {
|
||||
ProjectDO project = getProject(id);
|
||||
if (project == null) {
|
||||
throw exception(PROJECT_NOT_EXISTS);
|
||||
}
|
||||
if (CommonStatusEnum.isDisable(project.getStatus())) {
|
||||
throw exception(PROJECT_DISABLE, project.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getProjectIdsByTenantId(Long tenantId) {
|
||||
// 依赖框架已设置的租户上下文,直接查询当前租户下所有项目
|
||||
return CollectionUtils.convertList(projectMapper.selectList(), ProjectDO::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createDefaultProject(Long tenantId, String tenantName) {
|
||||
// 注意:调用方(TenantServiceImpl.createTenant)已在 TenantUtils.execute 块内,
|
||||
// 此处无需再次切换租户上下文
|
||||
// 创建默认项目
|
||||
ProjectDO project = new ProjectDO();
|
||||
project.setName(tenantName);
|
||||
project.setCode(ProjectDO.CODE_DEFAULT);
|
||||
project.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
project.setRemark("系统自动生成默认项目");
|
||||
projectMapper.insert(project);
|
||||
|
||||
// 将租户下所有用户授权到默认项目
|
||||
List<AdminUserDO> users = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
users.forEach(user -> {
|
||||
UserProjectDO userProject = new UserProjectDO();
|
||||
userProject.setUserId(user.getId());
|
||||
userProject.setProjectId(project.getId());
|
||||
userProjectMapper.insert(userProject);
|
||||
});
|
||||
return project.getId();
|
||||
}
|
||||
|
||||
private void validateProjectExists(Long id) {
|
||||
if (projectMapper.selectById(id) == null) {
|
||||
throw exception(PROJECT_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
private void validProjectNameDuplicate(String name, Long id) {
|
||||
ProjectDO project = projectMapper.selectByName(name);
|
||||
if (project == null) {
|
||||
return;
|
||||
}
|
||||
if (id == null) {
|
||||
throw exception(PROJECT_NAME_DUPLICATE, name);
|
||||
}
|
||||
if (!project.getId().equals(id)) {
|
||||
throw exception(PROJECT_NAME_DUPLICATE, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void validProjectCodeDuplicate(String code, Long id) {
|
||||
ProjectDO project = projectMapper.selectByCode(code);
|
||||
if (project == null) {
|
||||
return;
|
||||
}
|
||||
if (id == null) {
|
||||
throw exception(PROJECT_CODE_DUPLICATE, code);
|
||||
}
|
||||
if (!project.getId().equals(id)) {
|
||||
throw exception(PROJECT_CODE_DUPLICATE, code);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,320 +1,326 @@
|
||||
package com.viewsh.module.system.service.tenant;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.collection.CollectionUtils;
|
||||
import com.viewsh.framework.common.util.date.DateUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.viewsh.framework.tenant.config.TenantProperties;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
import com.viewsh.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
|
||||
import com.viewsh.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
|
||||
import com.viewsh.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
|
||||
import com.viewsh.module.system.convert.tenant.TenantConvert;
|
||||
import com.viewsh.module.system.dal.dataobject.permission.MenuDO;
|
||||
import com.viewsh.module.system.dal.dataobject.permission.RoleDO;
|
||||
import com.viewsh.module.system.dal.dataobject.tenant.TenantDO;
|
||||
import com.viewsh.module.system.dal.dataobject.tenant.TenantPackageDO;
|
||||
import com.viewsh.module.system.dal.mysql.tenant.TenantMapper;
|
||||
import com.viewsh.module.system.enums.permission.RoleCodeEnum;
|
||||
import com.viewsh.module.system.enums.permission.RoleTypeEnum;
|
||||
import com.viewsh.module.system.service.permission.MenuService;
|
||||
import com.viewsh.module.system.service.permission.PermissionService;
|
||||
import com.viewsh.module.system.service.permission.RoleService;
|
||||
import com.viewsh.module.system.service.tenant.handler.TenantInfoHandler;
|
||||
import com.viewsh.module.system.service.tenant.handler.TenantMenuHandler;
|
||||
import com.viewsh.module.system.service.user.AdminUserService;
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.system.enums.ErrorCodeConstants.*;
|
||||
import static java.util.Collections.singleton;
|
||||
|
||||
/**
|
||||
* 租户 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class TenantServiceImpl implements TenantService {
|
||||
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 viewsh.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入
|
||||
private TenantProperties tenantProperties;
|
||||
|
||||
@Resource
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Resource
|
||||
private TenantPackageService tenantPackageService;
|
||||
@Resource
|
||||
@Lazy // 延迟,避免循环依赖报错
|
||||
private AdminUserService userService;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private MenuService menuService;
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
|
||||
@Override
|
||||
public List<Long> getTenantIdList() {
|
||||
List<TenantDO> tenants = tenantMapper.selectList();
|
||||
return CollectionUtils.convertList(tenants, TenantDO::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validTenant(Long id) {
|
||||
TenantDO tenant = getTenant(id);
|
||||
if (tenant == null) {
|
||||
throw exception(TENANT_NOT_EXISTS);
|
||||
}
|
||||
if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
|
||||
throw exception(TENANT_DISABLE, tenant.getName());
|
||||
}
|
||||
if (DateUtils.isExpired(tenant.getExpireTime())) {
|
||||
throw exception(TENANT_EXPIRE, tenant.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
||||
@DataPermission(enable = false) // 参见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1154 说明
|
||||
public Long createTenant(TenantSaveReqVO createReqVO) {
|
||||
// 校验租户名称是否重复
|
||||
validTenantNameDuplicate(createReqVO.getName(), null);
|
||||
// 校验租户域名是否重复
|
||||
validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
|
||||
// 校验套餐被禁用
|
||||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
|
||||
|
||||
// 创建租户
|
||||
TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class);
|
||||
tenantMapper.insert(tenant);
|
||||
// 创建租户的管理员
|
||||
TenantUtils.execute(tenant.getId(), () -> {
|
||||
// 创建角色
|
||||
Long roleId = createRole(tenantPackage);
|
||||
// 创建用户,并分配角色
|
||||
Long userId = createUser(roleId, createReqVO);
|
||||
// 修改租户的管理员
|
||||
tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId));
|
||||
});
|
||||
return tenant.getId();
|
||||
}
|
||||
|
||||
private Long createUser(Long roleId, TenantSaveReqVO createReqVO) {
|
||||
// 创建用户
|
||||
Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO));
|
||||
// 分配角色
|
||||
permissionService.assignUserRole(userId, singleton(roleId));
|
||||
return userId;
|
||||
}
|
||||
|
||||
private Long createRole(TenantPackageDO tenantPackage) {
|
||||
// 创建角色
|
||||
RoleSaveReqVO reqVO = new RoleSaveReqVO();
|
||||
reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())
|
||||
.setSort(0).setRemark("系统自动生成");
|
||||
Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType());
|
||||
// 分配权限
|
||||
permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds());
|
||||
return roleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
||||
public void updateTenant(TenantSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
TenantDO tenant = validateUpdateTenant(updateReqVO.getId());
|
||||
// 校验租户名称是否重复
|
||||
validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId());
|
||||
// 校验租户域名是否重复
|
||||
validTenantWebsiteDuplicate(updateReqVO.getWebsites(), updateReqVO.getId());
|
||||
// 校验套餐被禁用
|
||||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
|
||||
|
||||
// 更新租户
|
||||
TenantDO updateObj = BeanUtils.toBean(updateReqVO, TenantDO.class);
|
||||
tenantMapper.updateById(updateObj);
|
||||
// 如果套餐发生变化,则修改其角色的权限
|
||||
if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) {
|
||||
updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds());
|
||||
}
|
||||
}
|
||||
|
||||
private void validTenantNameDuplicate(String name, Long id) {
|
||||
TenantDO tenant = tenantMapper.selectByName(name);
|
||||
if (tenant == null) {
|
||||
return;
|
||||
}
|
||||
// 如果 id 为空,说明不用比较是否为相同名字的租户
|
||||
if (id == null) {
|
||||
throw exception(TENANT_NAME_DUPLICATE, name);
|
||||
}
|
||||
if (!tenant.getId().equals(id)) {
|
||||
throw exception(TENANT_NAME_DUPLICATE, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void validTenantWebsiteDuplicate(List<String> websites, Long excludeId) {
|
||||
if (CollUtil.isEmpty(websites)) {
|
||||
return;
|
||||
}
|
||||
websites.forEach(website -> {
|
||||
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
|
||||
if (excludeId != null) {
|
||||
tenants.removeIf(tenant -> tenant.getId().equals(excludeId));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(tenants)) {
|
||||
throw exception(TENANT_WEBSITE_DUPLICATE, website);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) {
|
||||
TenantUtils.execute(tenantId, () -> {
|
||||
// 获得所有角色
|
||||
List<RoleDO> roles = roleService.getRoleList();
|
||||
roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配",
|
||||
role.getId(), role.getTenantId(), tenantId)); // 兜底校验
|
||||
// 重新分配每个角色的权限
|
||||
roles.forEach(role -> {
|
||||
// 如果是租户管理员,重新分配其权限为租户套餐的权限
|
||||
if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
|
||||
permissionService.assignRoleMenu(role.getId(), menuIds);
|
||||
log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds);
|
||||
return;
|
||||
}
|
||||
// 如果是其他角色,则去掉超过套餐的权限
|
||||
Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId());
|
||||
roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
|
||||
permissionService.assignRoleMenu(role.getId(), roleMenuIds);
|
||||
log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTenant(Long id) {
|
||||
// 校验存在
|
||||
validateUpdateTenant(id);
|
||||
// 删除
|
||||
tenantMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTenantList(List<Long> ids) {
|
||||
// 1. 校验存在
|
||||
ids.forEach(this::validateUpdateTenant);
|
||||
|
||||
// 2. 批量删除
|
||||
tenantMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
private TenantDO validateUpdateTenant(Long id) {
|
||||
TenantDO tenant = tenantMapper.selectById(id);
|
||||
if (tenant == null) {
|
||||
throw exception(TENANT_NOT_EXISTS);
|
||||
}
|
||||
// 内置租户,不允许删除
|
||||
if (isSystemTenant(tenant)) {
|
||||
throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM);
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenant(Long id) {
|
||||
return tenantMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<TenantDO> getTenantPage(TenantPageReqVO pageReqVO) {
|
||||
return tenantMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenantByName(String name) {
|
||||
return tenantMapper.selectByName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenantByWebsite(String website) {
|
||||
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
|
||||
return CollUtil.getFirst(tenants);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTenantCountByPackageId(Long packageId) {
|
||||
return tenantMapper.selectCountByPackageId(packageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TenantDO> getTenantListByPackageId(Long packageId) {
|
||||
return tenantMapper.selectListByPackageId(packageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TenantDO> getTenantListByStatus(Integer status) {
|
||||
return tenantMapper.selectListByStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTenantInfo(TenantInfoHandler handler) {
|
||||
// 如果禁用,则不执行逻辑
|
||||
if (isTenantDisable()) {
|
||||
return;
|
||||
}
|
||||
// 获得租户
|
||||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId());
|
||||
// 执行处理器
|
||||
handler.handle(tenant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTenantMenu(TenantMenuHandler handler) {
|
||||
// 如果禁用,则不执行逻辑
|
||||
if (isTenantDisable()) {
|
||||
return;
|
||||
}
|
||||
// 获得租户,然后获得菜单
|
||||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId());
|
||||
Set<Long> menuIds;
|
||||
if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的
|
||||
menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId);
|
||||
} else {
|
||||
menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds();
|
||||
}
|
||||
// 执行处理器
|
||||
handler.handle(menuIds);
|
||||
}
|
||||
|
||||
private static boolean isSystemTenant(TenantDO tenant) {
|
||||
return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM);
|
||||
}
|
||||
|
||||
private boolean isTenantDisable() {
|
||||
return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable());
|
||||
}
|
||||
|
||||
}
|
||||
package com.viewsh.module.system.service.tenant;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.viewsh.framework.common.enums.CommonStatusEnum;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.collection.CollectionUtils;
|
||||
import com.viewsh.framework.common.util.date.DateUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.viewsh.framework.tenant.config.TenantProperties;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
import com.viewsh.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
|
||||
import com.viewsh.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
|
||||
import com.viewsh.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
|
||||
import com.viewsh.module.system.convert.tenant.TenantConvert;
|
||||
import com.viewsh.module.system.dal.dataobject.permission.MenuDO;
|
||||
import com.viewsh.module.system.dal.dataobject.permission.RoleDO;
|
||||
import com.viewsh.module.system.dal.dataobject.tenant.TenantDO;
|
||||
import com.viewsh.module.system.dal.dataobject.tenant.TenantPackageDO;
|
||||
import com.viewsh.module.system.dal.mysql.tenant.TenantMapper;
|
||||
import com.viewsh.module.system.enums.permission.RoleCodeEnum;
|
||||
import com.viewsh.module.system.enums.permission.RoleTypeEnum;
|
||||
import com.viewsh.module.system.service.permission.MenuService;
|
||||
import com.viewsh.module.system.service.permission.PermissionService;
|
||||
import com.viewsh.module.system.service.permission.RoleService;
|
||||
import com.viewsh.module.system.service.project.ProjectService;
|
||||
import com.viewsh.module.system.service.tenant.handler.TenantInfoHandler;
|
||||
import com.viewsh.module.system.service.tenant.handler.TenantMenuHandler;
|
||||
import com.viewsh.module.system.service.user.AdminUserService;
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.system.enums.ErrorCodeConstants.*;
|
||||
import static java.util.Collections.singleton;
|
||||
|
||||
/**
|
||||
* 租户 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class TenantServiceImpl implements TenantService {
|
||||
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 viewsh.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入
|
||||
private TenantProperties tenantProperties;
|
||||
|
||||
@Resource
|
||||
private TenantMapper tenantMapper;
|
||||
|
||||
@Resource
|
||||
private TenantPackageService tenantPackageService;
|
||||
@Resource
|
||||
@Lazy // 延迟,避免循环依赖报错
|
||||
private AdminUserService userService;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private MenuService menuService;
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
@Resource
|
||||
@Lazy // 延迟,避免循环依赖报错
|
||||
private ProjectService projectService;
|
||||
|
||||
@Override
|
||||
public List<Long> getTenantIdList() {
|
||||
List<TenantDO> tenants = tenantMapper.selectList();
|
||||
return CollectionUtils.convertList(tenants, TenantDO::getId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validTenant(Long id) {
|
||||
TenantDO tenant = getTenant(id);
|
||||
if (tenant == null) {
|
||||
throw exception(TENANT_NOT_EXISTS);
|
||||
}
|
||||
if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) {
|
||||
throw exception(TENANT_DISABLE, tenant.getName());
|
||||
}
|
||||
if (DateUtils.isExpired(tenant.getExpireTime())) {
|
||||
throw exception(TENANT_EXPIRE, tenant.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
||||
@DataPermission(enable = false) // 参见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1154 说明
|
||||
public Long createTenant(TenantSaveReqVO createReqVO) {
|
||||
// 校验租户名称是否重复
|
||||
validTenantNameDuplicate(createReqVO.getName(), null);
|
||||
// 校验租户域名是否重复
|
||||
validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
|
||||
// 校验套餐被禁用
|
||||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
|
||||
|
||||
// 创建租户
|
||||
TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class);
|
||||
tenantMapper.insert(tenant);
|
||||
// 创建租户的管理员
|
||||
TenantUtils.execute(tenant.getId(), () -> {
|
||||
// 创建角色
|
||||
Long roleId = createRole(tenantPackage);
|
||||
// 创建用户,并分配角色
|
||||
Long userId = createUser(roleId, createReqVO);
|
||||
// 修改租户的管理员
|
||||
tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId));
|
||||
// 创建默认项目
|
||||
projectService.createDefaultProject(tenant.getId(), tenant.getName());
|
||||
});
|
||||
return tenant.getId();
|
||||
}
|
||||
|
||||
private Long createUser(Long roleId, TenantSaveReqVO createReqVO) {
|
||||
// 创建用户
|
||||
Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO));
|
||||
// 分配角色
|
||||
permissionService.assignUserRole(userId, singleton(roleId));
|
||||
return userId;
|
||||
}
|
||||
|
||||
private Long createRole(TenantPackageDO tenantPackage) {
|
||||
// 创建角色
|
||||
RoleSaveReqVO reqVO = new RoleSaveReqVO();
|
||||
reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())
|
||||
.setSort(0).setRemark("系统自动生成");
|
||||
Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType());
|
||||
// 分配权限
|
||||
permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds());
|
||||
return roleId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
|
||||
public void updateTenant(TenantSaveReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
TenantDO tenant = validateUpdateTenant(updateReqVO.getId());
|
||||
// 校验租户名称是否重复
|
||||
validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId());
|
||||
// 校验租户域名是否重复
|
||||
validTenantWebsiteDuplicate(updateReqVO.getWebsites(), updateReqVO.getId());
|
||||
// 校验套餐被禁用
|
||||
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
|
||||
|
||||
// 更新租户
|
||||
TenantDO updateObj = BeanUtils.toBean(updateReqVO, TenantDO.class);
|
||||
tenantMapper.updateById(updateObj);
|
||||
// 如果套餐发生变化,则修改其角色的权限
|
||||
if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) {
|
||||
updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds());
|
||||
}
|
||||
}
|
||||
|
||||
private void validTenantNameDuplicate(String name, Long id) {
|
||||
TenantDO tenant = tenantMapper.selectByName(name);
|
||||
if (tenant == null) {
|
||||
return;
|
||||
}
|
||||
// 如果 id 为空,说明不用比较是否为相同名字的租户
|
||||
if (id == null) {
|
||||
throw exception(TENANT_NAME_DUPLICATE, name);
|
||||
}
|
||||
if (!tenant.getId().equals(id)) {
|
||||
throw exception(TENANT_NAME_DUPLICATE, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void validTenantWebsiteDuplicate(List<String> websites, Long excludeId) {
|
||||
if (CollUtil.isEmpty(websites)) {
|
||||
return;
|
||||
}
|
||||
websites.forEach(website -> {
|
||||
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
|
||||
if (excludeId != null) {
|
||||
tenants.removeIf(tenant -> tenant.getId().equals(excludeId));
|
||||
}
|
||||
if (CollUtil.isNotEmpty(tenants)) {
|
||||
throw exception(TENANT_WEBSITE_DUPLICATE, website);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@DSTransactional
|
||||
public void updateTenantRoleMenu(Long tenantId, Set<Long> menuIds) {
|
||||
TenantUtils.execute(tenantId, () -> {
|
||||
// 获得所有角色
|
||||
List<RoleDO> roles = roleService.getRoleList();
|
||||
roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配",
|
||||
role.getId(), role.getTenantId(), tenantId)); // 兜底校验
|
||||
// 重新分配每个角色的权限
|
||||
roles.forEach(role -> {
|
||||
// 如果是租户管理员,重新分配其权限为租户套餐的权限
|
||||
if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) {
|
||||
permissionService.assignRoleMenu(role.getId(), menuIds);
|
||||
log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds);
|
||||
return;
|
||||
}
|
||||
// 如果是其他角色,则去掉超过套餐的权限
|
||||
Set<Long> roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId());
|
||||
roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds);
|
||||
permissionService.assignRoleMenu(role.getId(), roleMenuIds);
|
||||
log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTenant(Long id) {
|
||||
// 校验存在
|
||||
validateUpdateTenant(id);
|
||||
// 删除
|
||||
tenantMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTenantList(List<Long> ids) {
|
||||
// 1. 校验存在
|
||||
ids.forEach(this::validateUpdateTenant);
|
||||
|
||||
// 2. 批量删除
|
||||
tenantMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
private TenantDO validateUpdateTenant(Long id) {
|
||||
TenantDO tenant = tenantMapper.selectById(id);
|
||||
if (tenant == null) {
|
||||
throw exception(TENANT_NOT_EXISTS);
|
||||
}
|
||||
// 内置租户,不允许删除
|
||||
if (isSystemTenant(tenant)) {
|
||||
throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM);
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenant(Long id) {
|
||||
return tenantMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<TenantDO> getTenantPage(TenantPageReqVO pageReqVO) {
|
||||
return tenantMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenantByName(String name) {
|
||||
return tenantMapper.selectByName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TenantDO getTenantByWebsite(String website) {
|
||||
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
|
||||
return CollUtil.getFirst(tenants);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getTenantCountByPackageId(Long packageId) {
|
||||
return tenantMapper.selectCountByPackageId(packageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TenantDO> getTenantListByPackageId(Long packageId) {
|
||||
return tenantMapper.selectListByPackageId(packageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TenantDO> getTenantListByStatus(Integer status) {
|
||||
return tenantMapper.selectListByStatus(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTenantInfo(TenantInfoHandler handler) {
|
||||
// 如果禁用,则不执行逻辑
|
||||
if (isTenantDisable()) {
|
||||
return;
|
||||
}
|
||||
// 获得租户
|
||||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId());
|
||||
// 执行处理器
|
||||
handler.handle(tenant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTenantMenu(TenantMenuHandler handler) {
|
||||
// 如果禁用,则不执行逻辑
|
||||
if (isTenantDisable()) {
|
||||
return;
|
||||
}
|
||||
// 获得租户,然后获得菜单
|
||||
TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId());
|
||||
Set<Long> menuIds;
|
||||
if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的
|
||||
menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId);
|
||||
} else {
|
||||
menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds();
|
||||
}
|
||||
// 执行处理器
|
||||
handler.handle(menuIds);
|
||||
}
|
||||
|
||||
private static boolean isSystemTenant(TenantDO tenant) {
|
||||
return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM);
|
||||
}
|
||||
|
||||
private boolean isTenantDisable() {
|
||||
return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user