RuoYi-Vue-Pro 中为多租户系统添加包月付费功能并展示剩余费用

结合权限管理、数据隔离、定时任务等模块进行改造。


一、数据库设计

1. 新增费用相关表
-- 租户套餐表(存储不同套餐价格)
CREATE TABLE `sys_tenant_package` (
  `package_id` BIGINT PRIMARY KEY COMMENT '套餐ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `package_name` VARCHAR(50) NOT NULL COMMENT '套餐名称',
  `price` DECIMAL(10,2) NOT NULL COMMENT '月费(元)',
  `valid_period` INT DEFAULT 30 COMMENT '有效期(天)',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP
) COMMENT '租户套餐表';

-- 租户订阅记录表
CREATE TABLE `sys_tenant_subscription` (
  `subscription_id` BIGINT PRIMARY KEY COMMENT '订阅ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `package_id` BIGINT NOT NULL COMMENT '关联套餐ID',
  `start_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '生效时间',
  `end_time` DATETIME GENERATED ALWAYS AS (start_time + INTERVAL valid_period DAY) COMMENT '到期时间',
  `status` TINYINT DEFAULT 1 COMMENT '状态(1:有效 0:过期)',
  `is_auto_renew` TINYINT DEFAULT 1 COMMENT '是否自动续费'
) COMMENT '租户订阅记录表';
2. 修改租户主表
-- 在 sys_tenant 表中添加字段
ALTER TABLE `sys_tenant`
ADD COLUMN `remaining_fee` DECIMAL(10,2) DEFAULT 0 COMMENT '剩余费用',
ADD COLUMN `current_package_id` BIGINT COMMENT '当前套餐ID';

二、后端实现

1. 多租户数据隔离配置

MyBatisPlusConfig 中启用租户过滤器(参考):

@Configuration
public class MyBatisPlusConfig {
    @Bean
    public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
        return new TenantLineInnerInterceptor(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                Long tenantId = SecurityUtils.getLoginUser().getTenantId();
                return new LongValue(tenantId);
            }
        });
    }
}
2. 费用管理 Service
@Service
public class TenantSubscriptionService {
    @Autowired
    private SysTenantSubscriptionMapper subscriptionMapper;

    // 计算剩余费用
    public BigDecimal calculateRemainingFee(Long tenantId) {
        SysTenantSubscription subscription = subscriptionMapper.selectByTenantId(tenantId);
        if (subscription == null || subscription.getStatus() == 0) {
            return BigDecimal.ZERO;
        }
        LocalDateTime now = LocalDateTime.now();
        long daysUsed = ChronoUnit.DAYS.between(subscription.getStartTime(), now);
        BigDecimal dailyCost = subscription.getPrice().divide(BigDecimal.valueOf(subscription.getValidPeriod()), 2, RoundingMode.DOWN);
        return subscription.getPrice().subtract(dailyCost.multiply(BigDecimal.valueOf(daysUsed)));
    }

    // 自动续费定时任务
    @Scheduled(cron = "0 0 0 * * ?") // 每天0点执行
    public void autoRenew() {
        List<SysTenantSubscription> list = subscriptionMapper.selectAutoRenewList();
        list.forEach(sub -> {
            if (sub.isAutoRenew() && sub.getStatus() == 1) {
                // 调用支付接口扣款逻辑
                // 更新 end_time 和 remaining_fee
            }
        });
    }
}

三、前端实现

1. 租户信息页面改造
<!-- src/views/tenant/tenant-info.vue -->
<template>
  <el-card>
    <template #header>
      <div class="card-header">租户套餐信息</div>
    </template>
    <el-descriptions :column="1" border>
      <el-descriptions-item label="当前套餐">
        {{ currentPackage.packageName }}
      </el-descriptions-item>
      <el-descriptions-item label="剩余费用">
        ¥{{ remainingFee }}
      </el-descriptions-item>
      <el-descriptions-item label="有效期至">
        {{ formatTime(currentPackage.endTime) }}
      </el-descriptions-item>
      <el-descriptions-item label="自动续费">
        {{ currentPackage.autoRenew ? '是' : '否' }}
      </el-descriptions-item>
    </el-descriptions>
  </el-card>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { getTenantSubscription } from '@/api/tenant';

const remainingFee = ref(0);
const currentPackage = ref({});

onMounted(async () => {
  const res = await getTenantSubscription();
  remainingFee.value = res.remainingFee;
  currentPackage.value = res.subscription;
});
</script>

四、关键功能实现

1. 支付回调处理
@RestController
@RequestMapping("/api/tenant/pay")
public class PayController {
    @PostMapping("/callback")
    public AjaxResult payCallback(@RequestBody PayCallbackDTO dto) {
        // 验证签名
        if (!verifySignature(dto)) {
            return AjaxResult.error("非法请求");
        }
        // 更新订阅状态
        sysTenantSubscriptionService.updateStatus(dto.getTenantId(), true);
        return AjaxResult.success();
    }
}
2. 租户套餐管理 API
@RestController
@RequestMapping("/api/tenant/package")
public class PackageController {
    @PostMapping("/subscribe")
    public AjaxResult subscribe(@RequestBody SubscribeDTO dto) {
        // 校验租户状态
        SysTenant tenant = sysTenantService.getById(dto.getTenantId());
        if (tenant == null) {
            return AjaxResult.error("租户不存在");
        }
        // 创建订阅记录
        SysTenantSubscription subscription = new SysTenantSubscription();
        subscription.setTenantId(dto.getTenantId());
        subscription.setPackageId(dto.getPackageId());
        subscription.setStartTime(LocalDateTime.now());
        subscriptionMapper.insert(subscription);
        return AjaxResult.success();
    }
}

五、数据隔离与安全

  1. ​MyBatis-Plus 多租户拦截​
    确保所有租户数据查询自动添加 tenant_id 过滤条件(参考):

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor());
        return interceptor;
    }
  2. ​敏感字段加密​
    tenant_id 和支付信息使用 AES 加密存储:

    public class EncryptUtils {
        private static final String KEY = "ruoyi-tenant-pay-2025";
        
        public static String encrypt(Long tenantId) {
            return AESUtil.encrypt(tenantId.toString(), KEY);
        }
    }

六、测试用例

@SpringBootTest
public class TenantSubscriptionTest {
    @Autowired
    private TenantSubscriptionService service;

    @Test
    public void testCalculateRemainingFee() {
        BigDecimal fee = service.calculateRemainingFee(1L);
        Assert.assertTrue(fee.compareTo(BigDecimal.ZERO) >= 0);
    }

    @Test
    public void testAutoRenew() {
        service.autoRenew();
        // 验证续费后的 end_time 是否延长
    }
}

七、部署注意事项

  1. ​数据库迁移​
    使用 Flyway 执行 SQL 脚本:

    flyway migrate -url=jdbc:mysql://localhost:3306/ruoyi_v6 -user=root -password=123456
  2. ​Redis 缓存优化​
    对高频访问的租户费用信息添加缓存:

    @Cacheable(value = "tenant_fee", key = "#tenantId")
    public BigDecimal getRemainingFee(Long tenantId) {
        // 数据库查询逻辑
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex艾力的IT数字空间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值