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

@@ -0,0 +1,38 @@
-- =============================================
-- 租户-项目两级架构 - 建表脚本
-- =============================================
-- 1. 项目表
CREATE TABLE IF NOT EXISTS system_project (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '项目编号',
tenant_id BIGINT NOT NULL COMMENT '所属租户编号',
name VARCHAR(100) NOT NULL COMMENT '项目名称',
code VARCHAR(50) NOT NULL COMMENT '项目编码(如楼盘编号)',
status TINYINT DEFAULT 0 COMMENT '状态0=正常, 1=禁用)',
contact_name VARCHAR(30) COMMENT '项目联系人',
contact_mobile VARCHAR(30) COMMENT '联系手机',
address VARCHAR(500) COMMENT '项目地址',
remark VARCHAR(500) COMMENT '备注',
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted BIT DEFAULT 0 COMMENT '是否删除',
UNIQUE KEY uk_tenant_code (tenant_id, code, deleted),
INDEX idx_tenant (tenant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='项目表';
-- 2. 用户-项目关联表
CREATE TABLE IF NOT EXISTS system_user_project (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '编号',
user_id BIGINT NOT NULL COMMENT '用户编号',
project_id BIGINT NOT NULL COMMENT '项目编号',
tenant_id BIGINT NOT NULL COMMENT '租户编号',
creator VARCHAR(64) DEFAULT '' COMMENT '创建者',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updater VARCHAR(64) DEFAULT '' COMMENT '更新者',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted BIT DEFAULT 0 COMMENT '是否删除',
UNIQUE KEY uk_user_project (user_id, project_id, deleted),
INDEX idx_project (project_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-项目关联表';

View File

@@ -0,0 +1,21 @@
-- =============================================
-- 租户-项目两级架构 - 默认数据迁移
-- 为每个现有租户创建默认项目,为所有用户关联默认项目
-- =============================================
-- Step 1: 为每个现有租户创建默认项目
INSERT INTO system_project (tenant_id, name, code, status, remark, creator, updater, deleted)
SELECT id, name, 'DEFAULT', 0, '系统自动生成默认项目', 'system', 'system', 0
FROM system_tenant WHERE deleted = 0;
-- Step 2: 为所有用户关联默认项目
INSERT INTO system_user_project (user_id, project_id, tenant_id, creator, updater, deleted)
SELECT u.id, p.id, u.tenant_id, 'system', 'system', 0
FROM system_users u
JOIN system_project p ON p.tenant_id = u.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
WHERE u.deleted = 0;
-- 验证
SELECT '默认项目数量' AS metric, COUNT(*) AS value FROM system_project WHERE code = 'DEFAULT' AND deleted = 0
UNION ALL
SELECT '用户-项目关联数量', COUNT(*) FROM system_user_project WHERE deleted = 0;

View File

@@ -0,0 +1,215 @@
-- =============================================
-- 租户-项目两级架构 - 业务表增加 project_id 字段
-- 执行顺序ADD NULL → UPDATE 回填 → MODIFY NOT NULL → ADD INDEX
-- 前置条件02-default-data.sql 已执行system_project 中已有默认项目数据
-- =============================================
-- -----------------------------------------------
-- 1. iot_device
-- -----------------------------------------------
ALTER TABLE iot_device ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE iot_device d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE iot_device MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE iot_device ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 2. ops_order
-- -----------------------------------------------
ALTER TABLE ops_order ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 3. ops_order_event
-- -----------------------------------------------
ALTER TABLE ops_order_event ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order_event d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order_event MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order_event ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 4. ops_order_dispatch
-- -----------------------------------------------
ALTER TABLE ops_order_dispatch ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order_dispatch d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order_dispatch MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order_dispatch ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 5. ops_order_queue
-- -----------------------------------------------
ALTER TABLE ops_order_queue ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order_queue d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order_queue MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order_queue ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 6. ops_bus_area
-- -----------------------------------------------
ALTER TABLE ops_bus_area ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_bus_area d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_bus_area MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_bus_area ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 7. ops_area_device_relation
-- -----------------------------------------------
ALTER TABLE ops_area_device_relation ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_area_device_relation d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_area_device_relation MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_area_device_relation ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 8. ops_order_security_ext
-- -----------------------------------------------
ALTER TABLE ops_order_security_ext ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order_security_ext d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order_security_ext MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order_security_ext ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 9. ops_area_security_user
-- -----------------------------------------------
ALTER TABLE ops_area_security_user ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_area_security_user d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_area_security_user MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_area_security_user ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 10. ops_order_clean_ext
-- -----------------------------------------------
ALTER TABLE ops_order_clean_ext ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_order_clean_ext d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_order_clean_ext MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_order_clean_ext ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 11. ops_cleaner_status
-- -----------------------------------------------
ALTER TABLE ops_cleaner_status ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_cleaner_status d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_cleaner_status MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_cleaner_status ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 12. ops_cleaner_performance_monthly
-- -----------------------------------------------
ALTER TABLE ops_cleaner_performance_monthly ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_cleaner_performance_monthly d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_cleaner_performance_monthly MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_cleaner_performance_monthly ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 13. ops_device_trajectory
-- -----------------------------------------------
ALTER TABLE ops_device_trajectory ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_device_trajectory d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_device_trajectory MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_device_trajectory ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 14. ops_inspection_record
-- -----------------------------------------------
ALTER TABLE ops_inspection_record ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_inspection_record d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_inspection_record MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_inspection_record ADD INDEX idx_project_id (project_id);
-- -----------------------------------------------
-- 15. ops_inspection_record_item
-- -----------------------------------------------
ALTER TABLE ops_inspection_record_item ADD COLUMN project_id BIGINT NULL COMMENT '项目编号';
UPDATE ops_inspection_record_item d
JOIN system_project p ON p.tenant_id = d.tenant_id AND p.code = 'DEFAULT' AND p.deleted = 0
SET d.project_id = p.id
WHERE d.project_id IS NULL;
ALTER TABLE ops_inspection_record_item MODIFY COLUMN project_id BIGINT NOT NULL COMMENT '项目编号';
ALTER TABLE ops_inspection_record_item ADD INDEX idx_project_id (project_id);

View File

@@ -0,0 +1,86 @@
-- =============================================
-- 租户-项目两级架构 - 唯一索引审计脚本
-- 检查各业务表现有唯一键,评估是否需要将 project_id 纳入唯一约束
-- 执行顺序:在 03-alter-business-tables.sql 之后执行
-- =============================================
-- -----------------------------------------------
-- 查看各表当前的索引情况(审计用,执行前先 SHOW INDEX 确认)
-- -----------------------------------------------
SHOW INDEX FROM iot_device;
SHOW INDEX FROM ops_order;
SHOW INDEX FROM ops_order_event;
SHOW INDEX FROM ops_order_dispatch;
SHOW INDEX FROM ops_order_queue;
SHOW INDEX FROM ops_bus_area;
SHOW INDEX FROM ops_area_device_relation;
SHOW INDEX FROM ops_order_security_ext;
SHOW INDEX FROM ops_area_security_user;
SHOW INDEX FROM ops_order_clean_ext;
SHOW INDEX FROM ops_cleaner_status;
SHOW INDEX FROM ops_cleaner_performance_monthly;
SHOW INDEX FROM ops_device_trajectory;
SHOW INDEX FROM ops_inspection_record;
SHOW INDEX FROM ops_inspection_record_item;
-- -----------------------------------------------
-- 唯一键改造建议(根据实际业务语义决定是否执行)
-- 说明:若某张表的唯一键仅含 tenant_id则需要将 project_id 也纳入,
-- 以确保在同一租户下不同项目之间的数据互不干扰。
-- -----------------------------------------------
-- ops_bus_area区域编码在项目内唯一原唯一键可能仅含 tenant_id + code
-- 示例DROP INDEX uk_tenant_code ON ops_bus_area;
-- 示例ALTER TABLE ops_bus_area ADD UNIQUE KEY uk_project_code (tenant_id, project_id, code, deleted);
-- ops_area_device_relation设备-区域关联在项目内唯一
-- 示例DROP INDEX uk_area_device ON ops_area_device_relation;
-- 示例ALTER TABLE ops_area_device_relation ADD UNIQUE KEY uk_project_area_device (project_id, area_id, device_id, deleted);
-- ops_area_security_user安保用户-区域关联在项目内唯一
-- 示例DROP INDEX uk_area_user ON ops_area_security_user;
-- 示例ALTER TABLE ops_area_security_user ADD UNIQUE KEY uk_project_area_user (project_id, area_id, user_id, deleted);
-- ops_cleaner_status保洁员状态在项目内唯一每个项目内每个用户仅一条状态记录
-- 示例DROP INDEX uk_tenant_user ON ops_cleaner_status;
-- 示例ALTER TABLE ops_cleaner_status ADD UNIQUE KEY uk_project_user (project_id, user_id, deleted);
-- ops_cleaner_performance_monthly月度绩效在项目内唯一project + user + year_month
-- 示例DROP INDEX uk_tenant_user_month ON ops_cleaner_performance_monthly;
-- 示例ALTER TABLE ops_cleaner_performance_monthly ADD UNIQUE KEY uk_project_user_month (project_id, user_id, stat_year, stat_month, deleted);
-- iot_device设备标识符在项目内唯一device_key / product_key + device_name
-- 示例:根据实际唯一键定义决定是否改造。
-- -----------------------------------------------
-- 验证:各表 project_id 回填情况
-- -----------------------------------------------
SELECT 'iot_device' AS tbl, COUNT(*) AS total, SUM(project_id IS NULL) AS null_count FROM iot_device
UNION ALL
SELECT 'ops_order', COUNT(*), SUM(project_id IS NULL) FROM ops_order
UNION ALL
SELECT 'ops_order_event', COUNT(*), SUM(project_id IS NULL) FROM ops_order_event
UNION ALL
SELECT 'ops_order_dispatch', COUNT(*), SUM(project_id IS NULL) FROM ops_order_dispatch
UNION ALL
SELECT 'ops_order_queue', COUNT(*), SUM(project_id IS NULL) FROM ops_order_queue
UNION ALL
SELECT 'ops_bus_area', COUNT(*), SUM(project_id IS NULL) FROM ops_bus_area
UNION ALL
SELECT 'ops_area_device_relation', COUNT(*), SUM(project_id IS NULL) FROM ops_area_device_relation
UNION ALL
SELECT 'ops_order_security_ext', COUNT(*), SUM(project_id IS NULL) FROM ops_order_security_ext
UNION ALL
SELECT 'ops_area_security_user', COUNT(*), SUM(project_id IS NULL) FROM ops_area_security_user
UNION ALL
SELECT 'ops_order_clean_ext', COUNT(*), SUM(project_id IS NULL) FROM ops_order_clean_ext
UNION ALL
SELECT 'ops_cleaner_status', COUNT(*), SUM(project_id IS NULL) FROM ops_cleaner_status
UNION ALL
SELECT 'ops_cleaner_performance_monthly', COUNT(*), SUM(project_id IS NULL) FROM ops_cleaner_performance_monthly
UNION ALL
SELECT 'ops_device_trajectory', COUNT(*), SUM(project_id IS NULL) FROM ops_device_trajectory
UNION ALL
SELECT 'ops_inspection_record', COUNT(*), SUM(project_id IS NULL) FROM ops_inspection_record
UNION ALL
SELECT 'ops_inspection_record_item', COUNT(*), SUM(project_id IS NULL) FROM ops_inspection_record_item;

View File

@@ -0,0 +1,46 @@
-- =============================================
-- 租户-项目两级架构 - 回滚脚本
-- 执行顺序与正向脚本相反:业务表 → 默认数据 → 建表
-- =============================================
-- -----------------------------------------------
-- Step 1: 回滚业务表(删除 project_id 字段及索引)
-- -----------------------------------------------
ALTER TABLE iot_device DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order_event DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order_dispatch DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order_queue DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_bus_area DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_area_device_relation DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order_security_ext DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_area_security_user DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_order_clean_ext DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_cleaner_status DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_cleaner_performance_monthly DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_device_trajectory DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_inspection_record DROP INDEX idx_project_id, DROP COLUMN project_id;
ALTER TABLE ops_inspection_record_item DROP INDEX idx_project_id, DROP COLUMN project_id;
-- -----------------------------------------------
-- Step 2: 回滚默认数据(删除用户-项目关联 和 默认项目)
-- -----------------------------------------------
-- 删除系统自动生成的用户-项目关联
DELETE FROM system_user_project WHERE creator = 'system' AND deleted = 0;
-- 删除系统自动生成的默认项目
DELETE FROM system_project WHERE code = 'DEFAULT' AND creator = 'system' AND deleted = 0;
-- -----------------------------------------------
-- Step 3: 删除核心表
-- -----------------------------------------------
DROP TABLE IF EXISTS system_user_project;
DROP TABLE IF EXISTS system_project;
-- -----------------------------------------------
-- 验证
-- -----------------------------------------------
SELECT '回滚完成' AS status;

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

View File

@@ -26,6 +26,10 @@ public class IotDeviceRespDTO {
* 租户编号
*/
private Long tenantId;
/**
* 项目编号
*/
private Long projectId;
// ========== 产品相关字段 ==========

View File

@@ -54,6 +54,10 @@ public class IotDeviceMessage {
* 租户编号
*/
private Long tenantId;
/**
* 项目编号
*/
private Long projectId;
/**
* 服务编号,该消息由哪个 server 发送

View File

@@ -1,138 +1,139 @@
package com.viewsh.module.iot.gateway.service.device.message;
import cn.hutool.core.util.StrUtil;
import com.viewsh.framework.common.util.collection.CollectionUtils;
import com.viewsh.module.iot.core.biz.dto.IotDeviceRespDTO;
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
import com.viewsh.module.iot.core.mq.producer.IotDeviceMessageProducer;
import com.viewsh.module.iot.core.util.IotDeviceMessageUtils;
import com.viewsh.module.iot.gateway.codec.IotDeviceMessageCodec;
import com.viewsh.module.iot.gateway.service.device.IotDeviceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.viewsh.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
/**
* IoT 设备消息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Slf4j
public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
/**
* 编解码器
*/
private final Map<String, IotDeviceMessageCodec> codes;
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageProducer deviceMessageProducer;
public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) {
this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type);
}
@Override
public byte[] encodeDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName) {
// 1.1 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 1.2 获取编解码器
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
}
// 2. 编码消息
return codec.encode(message);
}
@Override
public byte[] encodeDeviceMessage(IotDeviceMessage message,
String codecType) {
// 1. 获取编解码器
IotDeviceMessageCodec codec = codes.get(codecType);
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
}
// 2. 编码消息
return codec.encode(message);
}
@Override
public IotDeviceMessage decodeDeviceMessage(byte[] bytes,
String productKey, String deviceName) {
// 1.1 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 1.2 获取编解码器
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
}
// 2. 解码消息
return codec.decode(bytes);
}
@Override
public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
// 1. 获取编解码器
IotDeviceMessageCodec codec = codes.get(codecType);
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
}
// 2. 解码消息
return codec.decode(bytes);
}
@Override
public void sendDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName, String serverId) {
// 1. 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 2. 发送消息
appendDeviceMessage(message, device, serverId);
deviceMessageProducer.sendDeviceMessage(message);
}
/**
* 补充消息的后端字段
*
* @param message 消息
* @param device 设备信息
* @param serverId 设备连接的 serverId
*/
private void appendDeviceMessage(IotDeviceMessage message,
IotDeviceRespDTO device, String serverId) {
message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now())
.setDeviceId(device.getId()).setTenantId(device.getTenantId()).setServerId(serverId);
// 特殊:如果设备没有指定 requestId则使用 messageId
if (StrUtil.isEmpty(message.getRequestId())) {
message.setRequestId(message.getId());
}
}
}
package com.viewsh.module.iot.gateway.service.device.message;
import cn.hutool.core.util.StrUtil;
import com.viewsh.framework.common.util.collection.CollectionUtils;
import com.viewsh.module.iot.core.biz.dto.IotDeviceRespDTO;
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
import com.viewsh.module.iot.core.mq.producer.IotDeviceMessageProducer;
import com.viewsh.module.iot.core.util.IotDeviceMessageUtils;
import com.viewsh.module.iot.gateway.codec.IotDeviceMessageCodec;
import com.viewsh.module.iot.gateway.service.device.IotDeviceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.viewsh.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
/**
* IoT 设备消息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Slf4j
public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
/**
* 编解码器
*/
private final Map<String, IotDeviceMessageCodec> codes;
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageProducer deviceMessageProducer;
public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) {
this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type);
}
@Override
public byte[] encodeDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName) {
// 1.1 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 1.2 获取编解码器
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
}
// 2. 编码消息
return codec.encode(message);
}
@Override
public byte[] encodeDeviceMessage(IotDeviceMessage message,
String codecType) {
// 1. 获取编解码器
IotDeviceMessageCodec codec = codes.get(codecType);
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
}
// 2. 编码消息
return codec.encode(message);
}
@Override
public IotDeviceMessage decodeDeviceMessage(byte[] bytes,
String productKey, String deviceName) {
// 1.1 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 1.2 获取编解码器
IotDeviceMessageCodec codec = codes.get(device.getCodecType());
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType()));
}
// 2. 解码消息
return codec.decode(bytes);
}
@Override
public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) {
// 1. 获取编解码器
IotDeviceMessageCodec codec = codes.get(codecType);
if (codec == null) {
throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType));
}
// 2. 解码消息
return codec.decode(bytes);
}
@Override
public void sendDeviceMessage(IotDeviceMessage message,
String productKey, String deviceName, String serverId) {
// 1. 获取设备信息
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
if (device == null) {
throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
}
// 2. 发送消息
appendDeviceMessage(message, device, serverId);
deviceMessageProducer.sendDeviceMessage(message);
}
/**
* 补充消息的后端字段
*
* @param message 消息
* @param device 设备信息
* @param serverId 设备连接的 serverId
*/
private void appendDeviceMessage(IotDeviceMessage message,
IotDeviceRespDTO device, String serverId) {
message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now())
.setDeviceId(device.getId()).setTenantId(device.getTenantId())
.setProjectId(device.getProjectId()).setServerId(serverId);
// 特殊:如果设备没有指定 requestId则使用 messageId
if (StrUtil.isEmpty(message.getRequestId())) {
message.setRequestId(message.getId());
}
}
}

View File

@@ -1,7 +1,7 @@
package com.viewsh.module.iot.dal.dataobject.device;
import com.viewsh.framework.mybatis.core.type.LongSetTypeHandler;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.viewsh.module.iot.dal.dataobject.ota.IotOtaFirmwareDO;
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
import com.viewsh.module.iot.core.enums.IotDeviceStateEnum;
@@ -26,7 +26,7 @@ import java.util.Set;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IotDeviceDO extends TenantBaseDO {
public class IotDeviceDO extends ProjectBaseDO {
/**
* 设备编号 - 全部设备

View File

@@ -1,5 +1,6 @@
package com.viewsh.module.iot.mq.consumer.rule;
import com.viewsh.framework.tenant.core.util.ProjectUtils;
import com.viewsh.framework.tenant.core.util.TenantUtils;
import com.viewsh.module.iot.core.messagebus.core.IotMessageBus;
import com.viewsh.module.iot.core.messagebus.core.IotMessageSubscriber;
@@ -50,12 +51,14 @@ public class IotCleanRuleMessageHandler implements IotMessageSubscriber<IotDevic
@Override
public void onMessage(IotDeviceMessage message) {
TenantUtils.execute(message.getTenantId(), () -> {
try {
cleanRuleProcessorManager.processMessage(message);
} catch (Exception e) {
// 规则处理异常不影响其他消息处理
log.error("[onMessage][消息({}) 保洁规则处理异常]", message.getRequestId(), e);
}
ProjectUtils.execute(message.getProjectId(), () -> {
try {
cleanRuleProcessorManager.processMessage(message);
} catch (Exception e) {
// 规则处理异常不影响其他消息处理
log.error("[onMessage][消息({}) 保洁规则处理异常]", message.getRequestId(), e);
}
});
});
}

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.environment.dal.dataobject.cleaner;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -21,7 +21,7 @@ import java.math.BigDecimal;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsCleanerPerformanceMonthlyDO extends TenantBaseDO {
public class OpsCleanerPerformanceMonthlyDO extends ProjectBaseDO {
/**
* 汇总ID

View File

@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.viewsh.module.ops.enums.CleanerStatusEnum;
import lombok.*;
@@ -22,7 +22,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsCleanerStatusDO extends TenantBaseDO {
public class OpsCleanerStatusDO extends ProjectBaseDO {
/**
* 主键ID

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.environment.dal.dataobject.inspection;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -21,7 +21,7 @@ import java.util.List;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsInspectionRecordDO extends TenantBaseDO {
public class OpsInspectionRecordDO extends ProjectBaseDO {
/**
* 巡检记录ID

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.environment.dal.dataobject.inspection;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -21,7 +21,7 @@ import java.util.List;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsInspectionRecordItemDO extends TenantBaseDO {
public class OpsInspectionRecordItemDO extends ProjectBaseDO {
/**
* 明细ID

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.environment.dal.dataobject.workorder;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderCleanExtDO extends TenantBaseDO {
public class OpsOrderCleanExtDO extends ProjectBaseDO {
/**
* 主键

View File

@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import lombok.*;
import java.util.Map;
@@ -27,7 +27,7 @@ import java.util.Map;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsAreaDeviceRelationDO extends TenantBaseDO {
public class OpsAreaDeviceRelationDO extends ProjectBaseDO {
/**
* 主键

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.dal.dataobject.area;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -19,7 +19,7 @@ import lombok.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsBusAreaDO extends TenantBaseDO {
public class OpsBusAreaDO extends ProjectBaseDO {
/**
* 区域ID主键

View File

@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.viewsh.module.ops.enums.OrderQueueStatusEnum;
import com.viewsh.module.ops.enums.PriorityEnum;
import lombok.*;
@@ -24,7 +24,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderQueueDO extends TenantBaseDO {
public class OpsOrderQueueDO extends ProjectBaseDO {
/**
* 队列ID主键

View File

@@ -3,7 +3,7 @@ package com.viewsh.module.ops.dal.dataobject.trajectory;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import lombok.*;
import java.time.LocalDateTime;
@@ -24,7 +24,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsDeviceTrajectoryDO extends TenantBaseDO {
public class OpsDeviceTrajectoryDO extends ProjectBaseDO {
/**
* 主键

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.dal.dataobject.workorder;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -21,7 +21,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderDO extends TenantBaseDO {
public class OpsOrderDO extends ProjectBaseDO {
/**
* 工单ID

View File

@@ -1,7 +1,7 @@
package com.viewsh.module.ops.dal.dataobject.workorder;
import com.baomidou.mybatisplus.annotation.TableField;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -24,7 +24,7 @@ import java.util.Map;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderDispatchDO extends TenantBaseDO {
public class OpsOrderDispatchDO extends ProjectBaseDO {
/**
* 派单ID

View File

@@ -1,7 +1,7 @@
package com.viewsh.module.ops.dal.dataobject.workorder;
import com.baomidou.mybatisplus.annotation.IdType;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderEventDO extends TenantBaseDO {
public class OpsOrderEventDO extends ProjectBaseDO {
/**
* 事件ID使用<E4BDBF><E794A8>花算法生成

View File

@@ -31,6 +31,7 @@ import java.util.stream.Collectors;
public class UserDispatchStatusServiceImpl implements UserDispatchStatusService {
private static final long TTL_SECONDS = 24 * 3600; // 24h
private static final String KEY_PREFIX = "ops:user:dispatch:";
@Resource
private StringRedisTemplate stringRedisTemplate;

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.security.dal.dataobject.area;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -17,7 +17,7 @@ import lombok.*;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsAreaSecurityUserDO extends TenantBaseDO {
public class OpsAreaSecurityUserDO extends ProjectBaseDO {
/**
* 主键

View File

@@ -1,6 +1,6 @@
package com.viewsh.module.ops.security.dal.dataobject.workorder;
import com.viewsh.framework.tenant.core.db.TenantBaseDO;
import com.viewsh.framework.tenant.core.db.ProjectBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OpsOrderSecurityExtDO extends TenantBaseDO {
public class OpsOrderSecurityExtDO extends ProjectBaseDO {
/**
* 主键