feat:【system 系统管理】租户支持匹配多域名、微信小程序 appid 等

This commit is contained in:
YunaiV
2025-08-19 22:58:44 +08:00
parent 97a981c294
commit df2ef43f85
17 changed files with 138 additions and 77 deletions

View File

@@ -1,14 +1,15 @@
package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.system.enums.DictTypeConstants;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 租户 Response VO")
@Data
@@ -36,8 +37,8 @@ public class TenantRespVO {
@DictFormat(DictTypeConstants.COMMON_STATUS)
private Integer status;
@Schema(description = "绑定域名", example = "https://www.iocoder.cn")
private String website;
@Schema(description = "绑定域名数组", example = "https://www.iocoder.cn")
private List<String> websites;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long packageId;

View File

@@ -3,14 +3,15 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 租户创建/修改 Request VO")
@Data
@@ -34,8 +35,8 @@ public class TenantSaveReqVO {
@NotNull(message = "租户状态")
private Integer status;
@Schema(description = "绑定域名", example = "https://www.iocoder.cn")
private String website;
@Schema(description = "绑定域名数组", example = "https://www.iocoder.cn")
private List<String> websites;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "租户套餐编号不能为空")

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.module.system.controller.app.tenant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.controller.app.tenant.vo.AppTenantRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.service.tenant.TenantService;
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.annotation.security.PermitAll;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 App - 租户")
@RestController
@RequestMapping("/system/tenant")
public class AppTenantController {
@Resource
private TenantService tenantService;
@GetMapping("/get-by-website")
@PermitAll
@TenantIgnore
@Operation(summary = "使用域名,获得租户信息", description = "根据用户的域名,获得租户信息")
@Parameter(name = "website", description = "域名", required = true, example = "www.iocoder.cn")
public CommonResult<AppTenantRespVO> getTenantByWebsite(@RequestParam("website") String website) {
TenantDO tenant = tenantService.getTenantByWebsite(website);
if (tenant == null || CommonStatusEnum.isDisable(tenant.getStatus())) {
return success(null);
}
return success(BeanUtils.toBean(tenant, AppTenantRespVO.class));
}
}

View File

@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.controller.app.tenant.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户 App - 租户 Response VO")
@Data
public class AppTenantRespVO {
@Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
}

View File

@@ -2,13 +2,16 @@ package cn.iocoder.yudao.module.system.dal.dataobject.tenant;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 租户 DO
@@ -60,9 +63,13 @@ public class TenantDO extends BaseDO {
*/
private Integer status;
/**
* 绑定域名
* 绑定域名列表
*
* 1. 考虑到对微信小程序的兼容,也允许传递 appid
* 2. 为什么是数组,考虑到管理后台、会员前台都有独立的域名,又或者多个管理后台
*/
private String website;
@TableField(typeHandler = StringListTypeHandler.class)
private List<String> websites;
/**
* 租户套餐编号
*

View File

@@ -3,17 +3,13 @@ package cn.iocoder.yudao.module.system.dal.mysql.tenant;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 租户 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface TenantMapper extends BaseMapperX<TenantDO> {
@@ -31,8 +27,9 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
return selectOne(TenantDO::getName, name);
}
default TenantDO selectByWebsite(String website) {
return selectOne(TenantDO::getWebsite, website);
default List<TenantDO> selectListByWebsite(String website) {
return selectList(new LambdaQueryWrapperX<TenantDO>()
.apply(MyBatisUtils.findInSet("websites", website)));
}
default Long selectCountByPackageId(Long packageId) {

View File

@@ -9,11 +9,6 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 租户套餐 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface TenantPackageMapper extends BaseMapperX<TenantPackageDO> {

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.system.service.tenant;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -102,7 +101,7 @@ public class TenantServiceImpl implements TenantService {
// 校验租户名称是否重复
validTenantNameDuplicate(createReqVO.getName(), null);
// 校验租户域名是否重复
validTenantWebsiteDuplicate(createReqVO.getWebsite(), null);
validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
// 校验套餐被禁用
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
@@ -148,7 +147,7 @@ public class TenantServiceImpl implements TenantService {
// 校验租户名称是否重复
validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId());
// 校验租户域名是否重复
validTenantWebsiteDuplicate(updateReqVO.getWebsite(), updateReqVO.getId());
validTenantWebsiteDuplicate(updateReqVO.getWebsites(), updateReqVO.getId());
// 校验套餐被禁用
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId());
@@ -175,21 +174,19 @@ public class TenantServiceImpl implements TenantService {
}
}
private void validTenantWebsiteDuplicate(String website, Long id) {
if (StrUtil.isEmpty(website)) {
private void validTenantWebsiteDuplicate(List<String> websites, Long excludeId) {
if (CollUtil.isEmpty(websites)) {
return;
}
TenantDO tenant = tenantMapper.selectByWebsite(website);
if (tenant == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同名字的租户
if (id == null) {
throw exception(TENANT_WEBSITE_DUPLICATE, website);
}
if (!tenant.getId().equals(id)) {
throw exception(TENANT_WEBSITE_DUPLICATE, website);
}
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
@@ -263,7 +260,8 @@ public class TenantServiceImpl implements TenantService {
@Override
public TenantDO getTenantByWebsite(String website) {
return tenantMapper.selectByWebsite(website);
List<TenantDO> tenants = tenantMapper.selectListByWebsite(website);
return CollUtil.getFirst(tenants);
}
@Override

View File

@@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.system.service.tenant.handler.TenantMenuHandler;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@@ -42,6 +43,7 @@ import static cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO.PACK
import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@@ -155,7 +157,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
o.setContactMobile("15601691300");
o.setPackageId(100L);
o.setStatus(randomCommonStatus());
o.setWebsite("https://www.iocoder.cn");
o.setWebsites(singletonList("https://www.iocoder.cn"));
o.setUsername("yunai");
o.setPassword("yuanma");
}).setId(null); // 设置为 null方便后面校验
@@ -183,7 +185,7 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
TenantSaveReqVO reqVO = randomPojo(TenantSaveReqVO.class, o -> {
o.setId(dbTenant.getId()); // 设置更新的 ID
o.setStatus(randomCommonStatus());
o.setWebsite(randomString());
o.setWebsites(singletonList(randomString()));
});
// mock 套餐
@@ -332,9 +334,10 @@ public class TenantServiceImplTest extends BaseDbUnitTest {
}
@Test
@Disabled // H2 不支持 find_in_set 函数
public void testGetTenantByWebsite() {
// mock 数据
TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setWebsite("https://www.iocoder.cn"));
TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setWebsites(singletonList("https://www.iocoder.cn")));
tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据
// 调用

View File

@@ -390,7 +390,7 @@ CREATE TABLE IF NOT EXISTS "system_tenant" (
"contact_name" varchar(255) NOT NULL,
"contact_mobile" varchar(255),
"status" tinyint NOT NULL,
"website" varchar(63) DEFAULT '',
"websites" varchar(1024) DEFAULT '',
"package_id" bigint NOT NULL,
"expire_time" timestamp NOT NULL,
"account_count" int NOT NULL,