feat(system): 新增代理租户续费功能及多级代理支持

This commit is contained in:
2025-11-26 13:38:53 +08:00
parent d99bb17419
commit 42ec325815
12 changed files with 251 additions and 6 deletions

View File

@@ -9,10 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantLevelRespVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRespVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.*;
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;
@@ -111,7 +108,11 @@ public class TenantController {
@PreAuthorize("@ss.hasPermission('system:tenant:query')")
public CommonResult<PageResult<TenantRespVO>> getTenantPage(@Valid TenantPageReqVO pageVO) {
PageResult<TenantDO> pageResult = tenantService.getTenantPage(pageVO);
return success(BeanUtils.toBean(pageResult, TenantRespVO.class));
PageResult<TenantRespVO> bean = BeanUtils.toBean(pageResult, TenantRespVO.class);
for (TenantRespVO tenantRespVO : bean.getList()) {
tenantRespVO.setHasChildren(tenantRespVO.getTenantType().equals("代理"));
}
return success(bean);
}
@GetMapping("/export-excel")
@@ -135,7 +136,11 @@ public class TenantController {
@PreAuthorize("@ss.hasPermission('system:tenant:query-self')")
public CommonResult<PageResult<TenantRespVO>> getSelfTenantPage(@Valid TenantPageReqVO pageVO) {
PageResult<TenantDO> pageResult = tenantService.getSelfTenantPage(pageVO);
return success(BeanUtils.toBean(pageResult, TenantRespVO.class));
PageResult<TenantRespVO> bean = BeanUtils.toBean(pageResult, TenantRespVO.class);
for (TenantRespVO tenantRespVO : bean.getList()) {
tenantRespVO.setHasChildren(tenantRespVO.getTenantType().equals("代理"));
}
return success(bean);
}
@GetMapping("/getSelfTenantLevel")
@@ -147,4 +152,28 @@ public class TenantController {
return success(BeanUtils.toBean(tenant, TenantLevelRespVO.class));
}
@GetMapping("/get-children")
@Operation(summary = "获得代理租户下级")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('system:tenant:query-children')")
public CommonResult<List<TenantRespVO>> getChildrenTenant(@RequestParam("id") Long id) {
List<TenantDO> tenantList = tenantService.getChildrenTenant(id);
List<TenantRespVO> bean = BeanUtils.toBean(tenantList, TenantRespVO.class);
for (TenantRespVO tenantRespVO : bean) {
tenantRespVO.setHasChildren(tenantRespVO.getTenantType().equals("代理"));
}
return success(bean);
}
@PutMapping("/renewal")
@Operation(summary = "租户续费")
@PreAuthorize("@ss.hasPermission('system:tenant:renewal')")
public CommonResult<Boolean> renewalTenant(@Valid @RequestBody TenantRenewalReqVO renewalReqVO) {
tenantService.renewalTenant(renewalReqVO);
return success(true);
}
}

View File

@@ -0,0 +1,31 @@
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 javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
/*
* @author: ziin
* @date: 2025/11/25 20:54
*/
@Schema(description = "管理后台 - 租户续费 Request VO")
@Data
public class TenantRenewalReqVO {
@Schema(description = "租户编号", example = "1024")
private Long id;
@Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "租户套餐编号不能为空")
private Long packageId;
@Schema(description = "备注", example = "备注")
private String remark;
}

View File

@@ -72,4 +72,7 @@ public class TenantRespVO {
@Schema(description = "租户等级",example = "1")
private String tenantLevel;
@Schema(description = "是否存在下级",example = "true")
private Boolean hasChildren;
}

View File

@@ -111,4 +111,6 @@ public class TenantDO extends BaseDO {
* 代理级别
*/
private Integer tenantLevel;
private String initialUser;
}

View File

@@ -78,4 +78,8 @@ public interface TenantMapper extends BaseMapperX<TenantDO> {
.likeIfPresent(TenantDO::getRemark, reqVO.getRemark())
.orderByDesc(TenantDO::getId));
}
default List<TenantDO> selectTenantByParentId(Long id){
return selectList(TenantDO::getParentId, id);
}
}

View File

@@ -3,10 +3,12 @@ package cn.iocoder.yudao.module.system.dal.mysql.user;
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.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
@@ -18,6 +20,7 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
return selectOne(AdminUserDO::getUsername, username);
}
default AdminUserDO selectByUsernameAndTenantId(String username,Long tenantId) {
return selectOne(AdminUserDO::getUsername, username
,AdminUserDO::getTenantId, tenantId);
@@ -62,4 +65,10 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
}
List<UserRespVO> selectTenantUserById(Long tenantId);
@TenantIgnore
AdminUserDO selectByUsernameAndTenantIdWithAgencyRenewal(@Param("initialUser") String initialUser,@Param("targetTenantId") Long targetTenantId);
@TenantIgnore
int updateByIdWithRenwal(AdminUserDO targetTenantUser);
}

View File

@@ -111,11 +111,13 @@ public interface ErrorCodeConstants {
ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在");
ErrorCode TENANT_LEVEL_CANT_CREATE_AGENCY = new ErrorCode(1_002_015_006, "租户级别不能创建代理租户");
// ========== 租户套餐 1-002-016-000 ==========
ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在");
ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1_002_016_001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除");
ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1_002_016_002, "名字为【{}】的租户套餐已被禁用");
ErrorCode TENANT_PACKAGE_NAME_DUPLICATE = new ErrorCode(1_002_016_003, "已经存在该名字的租户套餐");
ErrorCode TENANT_USER_NOT_EXISTS = new ErrorCode(1_002_016_004, "租户初始用户不存在");
// ========== 租户套餐 1-002-017-000 ==========
ErrorCode TENANT_POINTS_NOT_EXISTS = new ErrorCode(1_002_017_000, "租户积分不存在");
@@ -138,6 +140,9 @@ public interface ErrorCodeConstants {
ErrorCode TENANT_CREATE_FAIL = new ErrorCode(1_003_017_014, "租户创建失败");
ErrorCode TENANT_BALANCE_TEST_ACCOUNT_NUM_NOT_ENOUGH = new ErrorCode(1_003_017_015, "当前账户测试账户数量不足");
ErrorCode TENANT_BALANCE_TEST_ACCOUNT_NUM_OPERATION_ERROR = new ErrorCode(1_003_017_016, "租户测试账户数量操作错误");
ErrorCode TENANT_IS_AGENCY = new ErrorCode(1_003_017_017, "代理租户,不能进行续费操作");
ErrorCode TENANT_UPDATE_INITIAL_USER_INFO_FAIL = new ErrorCode(1_003_017_018, "更新租户初始用户信息失败");
ErrorCode TENANT_RENEWAL_FAIL = new ErrorCode(1_003_017_019, "租户续费失败");
// ================= 租户套餐 1-003-018-000 ==================
ErrorCode TENANT_AGENCY_PACKAGE_NOT_EXISTS = new ErrorCode(1_003_018_000, "代理租户套餐不存在");

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.service.tenant;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRenewalReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
import cn.iocoder.yudao.module.system.dal.dataobject.tenant.TenantDO;
import cn.iocoder.yudao.module.system.service.tenant.handler.TenantInfoHandler;
@@ -136,4 +137,8 @@ public interface TenantService {
void validTenant(Long id);
PageResult<TenantDO> getSelfTenantPage(@Valid TenantPageReqVO pageVO);
List<TenantDO> getChildrenTenant(Long id);
void renewalTenant( TenantRenewalReqVO updateReqVO);
}

View File

@@ -14,10 +14,12 @@ import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantRenewalReqVO;
import cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant.TenantSaveReqVO;
import cn.iocoder.yudao.module.system.convert.tenant.TenantConvert;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
@@ -48,6 +50,7 @@ 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.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
@@ -97,6 +100,9 @@ public class TenantServiceImpl implements TenantService {
private AdminUserMapper userMapper;
@Resource
private TenantBalanceService balanceService;
@Autowired
private TenantBalanceService tenantBalanceService;
@Override
public List<Long> getTenantIdList() {
List<TenantDO> tenants = tenantMapper.selectList();
@@ -124,6 +130,84 @@ public class TenantServiceImpl implements TenantService {
return tenantMapper.selectSelfPage(pageVO);
}
@Override
public List<TenantDO> getChildrenTenant(Long id) {
return tenantMapper.selectTenantByParentId(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
@TenantIgnore
public void renewalTenant(TenantRenewalReqVO updateReqVO) {
// 校验租户是否存在
TenantDO targetTenant = getTenant(updateReqVO.getId());
Long currentTenantId = TenantContextHolder.getTenantId();
if (targetTenant == null) {
throw exception(TENANT_NOT_EXISTS);
}
// 校验租户是否为代理类型
if (targetTenant.getTenantType().equals(TenantEnum.AGENCY.getTenantType())) {
throw exception(TENANT_IS_AGENCY);
}
TenantAgencyPackageDO tenantAgencyPackage = tenantAgencyPackageService.getTenantAgencyPackage(updateReqVO.getPackageId());
TenantBalanceDO tenantBalance = tenantBalanceService.getTenantBalance(currentTenantId);
AdminUserDO targetTenantUser = userService.getUserByTenantIdAndUserName(updateReqVO.getId(), targetTenant.getInitialUser());
if (targetTenantUser == null) {
throw exception(TENANT_USER_NOT_EXISTS);
}
if (tenantBalance.getBalance()<=tenantAgencyPackage.getPrice()){
throw exception(TENANT_BALANCE_NOT_ENOUGH);
}
if (balanceService.consumption(tenantAgencyPackage.getId(), targetTenant.getId(), updateReqVO.getRemark())) {
log.info("代理: {} 续费租户:{} 成功,套餐 Id:{}", currentTenantId,targetTenant.getId(),updateReqVO.getPackageId());
}
if (targetTenant.getExpireTime().isBefore(LocalDateTime.now())){
targetTenant.setExpireTime(LocalDateTime.now().plusDays(tenantAgencyPackage.getDays()));
targetTenant.setStatus(CommonStatusEnum.ENABLE.getStatus());
targetTenantUser.setCrawl((byte) 1);
}else {
targetTenant.setExpireTime(targetTenant.getExpireTime().plusDays(tenantAgencyPackage.getDays()));
}
if (tenantAgencyPackage.getAiClient()==1){
if (targetTenant.getAiExpireTime().isBefore(LocalDateTime.now())){
targetTenant.setAiExpireTime(LocalDateTime.now().plusDays(tenantAgencyPackage.getDays()));
targetTenantUser.setAiReplay((byte) 1);
targetTenantUser.setAiChat((byte) 1);
}else {
targetTenant.setAiExpireTime(targetTenant.getAiExpireTime().plusDays(tenantAgencyPackage.getDays()));
}
}
if (tenantAgencyPackage.getBrotherClient() ==1 ){
if (targetTenant.getBrotherExpireTime().isBefore(LocalDateTime.now())){
targetTenant.setBrotherExpireTime(LocalDateTime.now().plusDays(tenantAgencyPackage.getDays()));
targetTenantUser.setBigBrother((byte) 1);
}else {
targetTenant.setBrotherExpireTime(targetTenant.getBrotherExpireTime().plusDays(tenantAgencyPackage.getDays()));
}
}
int updateInitialUserCount = userMapper.updateByIdWithRenwal(targetTenantUser);
if (updateInitialUserCount <= 0) {
throw exception(TENANT_UPDATE_INITIAL_USER_INFO_FAIL);
}
int updateTenantCount = tenantMapper.updateById(targetTenant);
if (updateTenantCount <= 0) {
throw exception(TENANT_RENEWAL_FAIL);
}
}
@Override
@DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
@DataPermission(enable = false) // 参见 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1154 说明
@@ -157,6 +241,7 @@ public class TenantServiceImpl implements TenantService {
LocalDateTime localDateTime = LocalDateTimeUtil.of(dt);
LocalDateTime offset = LocalDateTimeUtil.offset(localDateTime, tenantAgencyPackageDO.getDays(), ChronoUnit.DAYS);
tenant.setExpireTime(offset);
tenant.setInitialUser(createReqVO.getUsername());
if (tenantAgencyPackageDO.getBrotherClient() == 1){
tenant.setBrotherExpireTime(offset);
}
@@ -274,6 +359,7 @@ public class TenantServiceImpl implements TenantService {
tenantBalance.setId(tenant.getId()); // 钱包ID与租户ID一致
tenantBalance.setBalance(0); // 初始余额设为0
tenantBalance.setVersion(0); // 初始版本号设为0
tenantBalance.setTestAccountNum(15);
tenantBalanceMapper.insert(tenantBalance); // 插入钱包记录
}

View File

@@ -209,4 +209,6 @@ public interface AdminUserService {
List<UserRespVO> getTenantUserById(Long tenantId);
void updateUserWithClientRole(@Valid UserClientSaveReqVO reqVO);
AdminUserDO getUserByTenantIdAndUserName(Long targetTenantId, String initialUser);
}

View File

@@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.AuthRegisterReqVO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
@@ -552,6 +553,11 @@ public class AdminUserServiceImpl implements AdminUserService {
userMapper.updateById(updateObj);
}
@Override
public AdminUserDO getUserByTenantIdAndUserName(Long targetTenantId, String initialUser) {
return userMapper.selectByUsernameAndTenantIdWithAgencyRenewal(initialUser,targetTenantId);
}
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username,Long tenantId) {
return DataPermissionUtils.executeIgnore(() -> {

View File

@@ -16,4 +16,67 @@
select * from system_users where tenant_id=#{tenantId,jdbcType=BIGINT}
</select>
<select id="selectByUsernameAndTenantIdWithAgencyRenewal"
resultType="cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO">
select * from system_users
where username=#{initialUser,jdbcType=VARCHAR} and tenant_id=#{targetTenantId,jdbcType=BIGINT}
</select>
<update id="updateByIdWithRenwal">
update system_users set
<if test="username != null">
username = #{username,jdbcType=VARCHAR},
</if>
<if test="password != null">
password = #{password,jdbcType=VARCHAR},
</if>
<if test="nickname != null">
nickname = #{nickname,jdbcType=VARCHAR},
</if>
<if test="remark != null">
remark = #{remark,jdbcType=VARCHAR},
</if>
<if test="deptId != null">
dept_id = #{deptId,jdbcType=BIGINT},
</if>
<if test="postIds != null">
post_ids = #{postIds,jdbcType=VARCHAR,typeHandler=com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler},
</if>
<if test="email != null">
email = #{email,jdbcType=VARCHAR},
</if>
<if test="mobile != null">
mobile = #{mobile,jdbcType=VARCHAR},
</if>
<if test="sex != null">
sex = #{sex,jdbcType=INTEGER},
</if>
<if test="avatar != null">
avatar = #{avatar,jdbcType=VARCHAR},
</if>
<if test="status != null">
status = #{status,jdbcType=INTEGER},
</if>
<if test="loginIp != null">
login_ip = #{loginIp,jdbcType=VARCHAR},
</if>
<if test="loginDate != null">
login_date = #{loginDate,jdbcType=TIMESTAMP},
</if>
<if test="crawl != null">
crawl = #{crawl,jdbcType=TINYINT},
</if>
<if test="bigBrother != null">
big_brother = #{bigBrother,jdbcType=TINYINT},
</if>
<if test="aiChat != null">
ai_chat = #{aiChat,jdbcType=TINYINT},
</if>
<if test="aiReplay != null">
ai_replay = #{aiReplay,jdbcType=TINYINT},
</if>
update_time = NOW()
where id = #{id,jdbcType=BIGINT}
</update>
</mapper>