feat(tenant): 租户-项目两级架构 Phase 2 — IoT + Ops 业务迁移

DO 迁移 (15个 TenantBaseDO → ProjectBaseDO):
- IoT: IotDeviceDO
- Ops 核心: OpsOrderDO, OpsOrderEventDO, OpsOrderDispatchDO, OpsOrderQueueDO,
  OpsBusAreaDO, OpsAreaDeviceRelationDO, OpsDeviceTrajectoryDO
- Ops 保洁: OpsOrderCleanExtDO, OpsCleanerStatusDO, OpsCleanerPerformanceMonthlyDO,
  OpsInspectionRecordDO, OpsInspectionRecordItemDO
- Ops 安保: OpsOrderSecurityExtDO, OpsAreaSecurityUserDO

IoT 适配:
- IotDeviceRespDTO 新增 projectId 字段
- IotDeviceMessage 新增 projectId 字段
- IotDeviceMessageServiceImpl.appendDeviceMessage() 设置 projectId
- IotCleanRuleMessageHandler 嵌套 ProjectUtils.execute() 设置项目上下文

缓存改造:
- ProjectRedisCacheManager extends TenantRedisCacheManager,追加 :projectId 后缀
- ViewshTenantAutoConfiguration 替换为 ProjectRedisCacheManager

SQL 迁移脚本 (sql/mysql/project/):
- 01-create-tables.sql: system_project + system_user_project 建表
- 02-default-data.sql: 默认项目 + 用户关联回填
- 03-alter-business-tables.sql: 15 张表添加 project_id (NULL → 回填 → NOT NULL → 索引)
- 04-index-audit.sql: 现有索引审计 + project_id 补充建议
- 99-rollback.sql: 完整回滚方案

附带修复:
- fix(ops): UserDispatchStatusServiceImpl 添加缺失的 KEY_PREFIX 常量

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-16 22:27:34 +08:00
parent 87beb1228e
commit a2f500fa20
27 changed files with 648 additions and 176 deletions

View File

@@ -17,6 +17,7 @@ import com.viewsh.framework.tenant.core.job.TenantJobAspect;
import com.viewsh.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer;
import com.viewsh.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor;
import com.viewsh.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer;
import com.viewsh.framework.tenant.core.redis.ProjectRedisCacheManager;
import com.viewsh.framework.tenant.core.redis.TenantRedisCacheManager;
import com.viewsh.framework.tenant.core.security.TenantSecurityWebFilter;
import com.viewsh.framework.tenant.core.service.ProjectFrameworkService;
@@ -266,8 +267,9 @@ public class ViewshTenantAutoConfiguration {
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(viewshCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象
return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
// 创建 ProjectRedisCacheManager 对象(在租户隔离基础上叠加项目隔离)
return new ProjectRedisCacheManager(cacheWriter, redisCacheConfiguration,
tenantProperties.getIgnoreCaches(), tenantProperties.getIgnoreProjectCaches());
}
}

View File

@@ -0,0 +1,51 @@
package com.viewsh.framework.tenant.core.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.viewsh.framework.tenant.core.context.ProjectContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.util.Set;
/**
* 多项目的 {@link org.springframework.data.redis.cache.RedisCacheManager} 实现类
*
* 在租户隔离的基础上,追加项目隔离后缀,格式为 name + ":" + tenantId + ":" + projectId
*
* @author lzh
*/
@Slf4j
public class ProjectRedisCacheManager extends TenantRedisCacheManager {
private static final String SPLIT = "#";
private final Set<String> ignoreProjectCaches;
public ProjectRedisCacheManager(RedisCacheWriter cacheWriter,
RedisCacheConfiguration defaultCacheConfiguration,
Set<String> ignoreTenantCaches,
Set<String> ignoreProjectCaches) {
super(cacheWriter, defaultCacheConfiguration, ignoreTenantCaches);
this.ignoreProjectCaches = ignoreProjectCaches;
}
@Override
public Cache getCache(String name) {
// 获取原始 cache name去掉 # 后缀部分,# 后面是超时配置)
String[] names = StrUtil.splitToArray(name, SPLIT);
// 如果开启项目隔离,则在 name 上追加 projectId 后缀
// 父类 TenantRedisCacheManager.getCache 会继续追加 tenantId
// 最终 key 格式name[:projectId]:tenantId
if (!ProjectContextHolder.isIgnore()
&& ProjectContextHolder.getProjectId() != null
&& !CollUtil.contains(ignoreProjectCaches, names[0])) {
name = name + ":" + ProjectContextHolder.getProjectId();
}
// 继续基于父方法(父类追加租户后缀)
return super.getCache(name);
}
}