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:
lzh
2026-04-16 19:22:57 +08:00
parent 73e67dd3ec
commit 87beb1228e
37 changed files with 2724 additions and 1122 deletions

View File

@@ -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);
}
}

View File

@@ -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())));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}