version(2.1):11月代码合并,保持与企业版和演示环境一致,补充docker镜像和SQL;增加wvp的集成开源内容

This commit is contained in:
PhoenixHai
2025-12-09 16:02:45 +08:00
parent c4a396f587
commit dff69dc131
924 changed files with 136442 additions and 19119 deletions
+127 -121
View File
@@ -1,131 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cn-universal</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cn-universal</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cn-universal-admin</artifactId>
<dependencies>
<artifactId>cn-universal-admin</artifactId>
<dependencies>
<!--velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-rule</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-dm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-oss</artifactId>
<version>${project.version}</version>
</dependency>
<!--velocity代码生成使用模板 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-rule</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-dm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<!--排除这个slf4j-log4j12-->
<groupId>org.springframework.boot</groupId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-oss</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<!--排除这个slf4j-log4j12-->
<groupId>org.springframework.boot</groupId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- Validation依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- OAuth2 Resource Server (JWT support) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-mqtt-protocol</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-notice</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-security</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- Validation依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- OAuth2 Resource Server (JWT support) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-mqtt-protocol</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-notice</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-security</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -10,7 +10,7 @@ import org.springframework.stereotype.Component;
public class ConnectionConfig {
// TCP配置 - 复用现有配置
@Value("${tcp.protocol.sni.host.out:#productKey.tcp.nexiot.xyz}")
@Value("${tcp.protocol.sni.host.out:#productKey.tcp.nexiot.cc}")
private String tcpSniHostTemplate;
@Value("${tcp.protocol.sni.port.out:38883}")
@@ -53,6 +53,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@@ -291,7 +292,7 @@ public class NetworkServiceImpl implements INetworkService {
}
// 启用/停用状态
vo.setEnableName(Boolean.TRUE.equals(network.getState()) ? "已启用" : "已停用");
// 实际运行状态
// 实际运行状态和绑定产品
String type = network.getType();
String productKey = network.getProductKey();
String unionId = network.getUnionId();
@@ -310,17 +311,29 @@ public class NetworkServiceImpl implements INetworkService {
vo.setStateName("未启动");
vo.setRunning(Boolean.FALSE);
}
} else if (NetworkType.MQTT_CLIENT.getId().equals(type)
|| NetworkType.MQTT_SERVER.getId().equals(type)) {
if (mqttServerManager != null && mqttServerManager.isConnected(unionId)) {
vo.setStateName("已启动");
vo.setRunning(Boolean.TRUE);
} else {
vo.setStateName("未启动");
vo.setRunning(Boolean.FALSE);
// TCP类型查询绑定产品
if (NetworkType.TCP_SERVER.getId().equals(type)) {
IoTProductBO ioTProductBO =
ioTProductMapper.selectTcpProductsUseNetwork(productKey);
vo.setBindTcpServerProductCount(ioTProductBO == null ? 0 : 1);
vo.setBindTcpServerProducts(ioTProductBO);
}
} else if (NetworkType.UDP.getId().equals(type)) {
} else if (NetworkType.MQTT_CLIENT.getId().equals(type)
|| NetworkType.MQTT_SERVER.getId().equals(type)) {
// 查询MQTT绑定产品
List<IoTProductBO> ioTProductBOS = ioTProductMapper.selectMqttProductsUseNetwork(unionId);
vo.setBindMqttServerProductCount(CollUtil.size(ioTProductBOS));
vo.setBindMqttServerProducts(ioTProductBOS);
// 查询运行状态
if (mqttServerManager != null && mqttServerManager.isConnected(unionId)) {
vo.setStateName("已启动");
vo.setRunning(Boolean.TRUE);
} else {
vo.setStateName("未启动");
vo.setRunning(Boolean.FALSE);
}
} else if (NetworkType.UDP.getId().equals(type)) {
if (udpClusterService != null && udpClusterService.isProductServerAlive(productKey)) {
vo.setStateName("已启动");
vo.setRunning(Boolean.TRUE);
@@ -480,13 +493,26 @@ public class NetworkServiceImpl implements INetworkService {
}
}
}
return networkMapper.deleteNetworkById(id);
int ct = networkMapper.deleteNetworkById(id);
if (ct <= 0) {
throw new RuntimeException("删除失败,请重试");
}
ioTProductMapper.updateNetworkProductsUseNetwork(network.getUnionId());
return ct;
}
@Override
@Transactional
public int deleteNetworkByIds(Integer[] ids) {
return networkMapper.deleteNetworkByIds(ids);
if (ids == null || ids.length == 0) {
return 0;
}
int count = 0;
for (Integer id : ids) {
count= ((INetworkService) AopContext.currentProxy()).deleteNetworkById(id);
count++;
}
return count;
}
@Override
@@ -172,12 +172,12 @@ public class NetworkConfigUtil {
config.set("readerIdleTime", 360);
config.set("writerIdleTime", 0);
config.set("decoderType", "STRING");
config.set("parserType", "DELIMITED");
config.set("parserType", "DELIMITER");
config.set(
"parserConfiguration",
new JSONObject()
.set("byteOrderLittle", true)
.set("delimited", "]")
.set("delimiter", "]")
.set("delimitedMaxlength", 1024)
.set("failFast", true));
return config.toString();
@@ -13,12 +13,15 @@
package cn.universal.admin.platform.scheduler;
import cn.universal.admin.platform.service.IGatewayPollingService;
import cn.universal.common.constant.IoTConstant;
import cn.universal.persistence.entity.GatewayPollingConfig;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled;
@@ -26,70 +29,126 @@ import org.springframework.stereotype.Component;
/**
* 网关轮询定时任务调度器
*
*
* <p>使用固定轮询间隔(30s, 60s, 120s, 300s, 600s),通过分布式锁保证集群环境下只有一个节点执行
*
*
* @author Aleo
* @date 2025-10-26
*/
@Component
@Slf4j
@ConditionalOnProperty(
prefix = "gateway.polling",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@ConditionalOnProperty(prefix = "gateway.polling", name = "enabled", havingValue = "true", matchIfMissing = true)
public class GatewayPollingScheduler {
@Autowired private IGatewayPollingService gatewayPollingService;
@Autowired
private IGatewayPollingService gatewayPollingService;
@Autowired private RedissonClient redissonClient;
@Autowired
private RedissonClient redissonClient;
/** 30秒轮询任务 */
/**
* 30秒轮询任务
*/
@Scheduled(cron = "0/30 * * * * ?")
public void polling30s() {
executePollingWithLock(30);
String traceId = generateTraceId("polling-30s");
MDC.put(IoTConstant.TRACE_ID, traceId);
try {
executePollingWithLock(30);
} finally {
MDC.remove(IoTConstant.TRACE_ID);
}
}
/** 60秒轮询任务 */
/**
* 60秒轮询任务
*/
@Scheduled(cron = "0 0/1 * * * ?")
public void polling60s() {
executePollingWithLock(60);
String traceId = generateTraceId("polling-60s");
MDC.put(IoTConstant.TRACE_ID, traceId);
try {
executePollingWithLock(60);
} finally {
MDC.remove(IoTConstant.TRACE_ID);
}
}
/** 120秒轮询任务 */
/**
* 120秒轮询任务
*/
@Scheduled(cron = "0 0/2 * * * ?")
public void polling120s() {
executePollingWithLock(120);
String traceId = generateTraceId("polling-120s");
MDC.put(IoTConstant.TRACE_ID, traceId);
try {
executePollingWithLock(120);
} finally {
MDC.remove(IoTConstant.TRACE_ID);
}
}
/** 300秒轮询任务 (5分钟) */
/**
* 300秒轮询任务 (5分钟)
*/
@Scheduled(cron = "0 0/5 * * * ?")
public void polling300s() {
executePollingWithLock(300);
String traceId = generateTraceId("polling-300s");
MDC.put(IoTConstant.TRACE_ID, traceId);
try {
executePollingWithLock(300);
} finally {
MDC.remove(IoTConstant.TRACE_ID);
}
}
/** 600秒轮询任务 (10分钟) */
/**
* 600秒轮询任务 (10分钟)
*/
@Scheduled(cron = "0 0/10 * * * ?")
public void polling600s() {
executePollingWithLock(600);
String traceId = generateTraceId("polling-600s");
MDC.put(IoTConstant.TRACE_ID, traceId);
try {
executePollingWithLock(600);
} finally {
MDC.remove(IoTConstant.TRACE_ID);
}
}
/**
* 生成 traceId
* 格式: {prefix}-{timestamp}-{random}
* 示例: polling-30s-1699344000-a1b2c3
*/
private String generateTraceId(String prefix) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String random = UUID.randomUUID().toString().substring(0, 6);
return String.format("%s-%s-%s", prefix, timestamp, random);
}
/**
* 使用分布式锁执行轮询任务
*
*
* @param intervalSeconds 轮询间隔(秒)
*/
private void executePollingWithLock(int intervalSeconds) {
String lockKey = "gateway:polling:lock:" + intervalSeconds;
RLock lock = redissonClient.getLock(lockKey);
// 打印线程信息用于验证虚拟线程
Thread currentThread = Thread.currentThread();
log.debug("当前线程: name={}, isVirtual={}, id={}",
currentThread.getName(),
currentThread.isVirtual(),
currentThread.threadId());
try {
// 尝试获取锁,最多等待0秒,锁自动释放时间为任务执行周期的80%
boolean acquired = lock.tryLock(0, (long) (intervalSeconds * 0.8), TimeUnit.SECONDS);
if (acquired) {
log.info("✓ 节点获得 {}s 轮询任务锁,开始执行", intervalSeconds);
log.debug("✓ 节点获得 {}s 轮询任务锁,开始执行", intervalSeconds);
executePollingTask(intervalSeconds);
} else {
log.debug("✗ 节点未获得 {}s 轮询任务锁,跳过执行", intervalSeconds);
@@ -110,22 +169,21 @@ public class GatewayPollingScheduler {
/**
* 执行轮询任务
*
*
* @param intervalSeconds 轮询间隔(秒)
*/
private void executePollingTask(int intervalSeconds) {
long startTime = System.currentTimeMillis();
// 1. 查询待轮询的网关设备
List<GatewayPollingConfig> configs =
gatewayPollingService.getDuePollingDevices(intervalSeconds);
List<GatewayPollingConfig> configs = gatewayPollingService.getDuePollingDevices(intervalSeconds);
if (configs == null || configs.isEmpty()) {
log.debug("本次{}s轮询无待执行设备", intervalSeconds);
return;
}
log.info("本次{}s轮询共 {} 个网关设备", intervalSeconds, configs.size());
log.debug("本次{}s轮询共 {} 个网关设备", intervalSeconds, configs.size());
// 2. 并发执行轮询
int successCount = 0;
@@ -142,12 +200,7 @@ public class GatewayPollingScheduler {
}
long costTime = System.currentTimeMillis() - startTime;
log.info(
"{}s轮询任务完成: 总数={}, 成功={}, 失败={}, 耗时={}ms",
intervalSeconds,
configs.size(),
successCount,
failCount,
costTime);
log.debug("{}s轮询任务完成: 总数={}, 成功={}, 失败={}, 耗时={}ms",
intervalSeconds, configs.size(), successCount, failCount, costTime);
}
}
@@ -19,7 +19,7 @@ import java.util.List;
/**
* 网关轮询服务接口
*
*
* @author Aleo
* @date 2025-10-26
*/
@@ -27,7 +27,7 @@ public interface IGatewayPollingService {
/**
* 保存或更新网关轮询配置
*
*
* @param dto 配置DTO
* @return 操作结果
*/
@@ -35,7 +35,7 @@ public interface IGatewayPollingService {
/**
* 获取网关轮询配置
*
*
* @param productKey 产品Key
* @param deviceId 设备ID
* @return 配置信息
@@ -44,7 +44,7 @@ public interface IGatewayPollingService {
/**
* 删除网关轮询配置
*
*
* @param productKey 产品Key
* @param deviceId 设备ID
* @return 操作结果
@@ -53,14 +53,14 @@ public interface IGatewayPollingService {
/**
* 执行单个网关的轮询任务
*
*
* @param config 轮询配置
*/
void pollGatewayDevice(GatewayPollingConfig config);
/**
* 查询待轮询的网关设备列表
*
*
* @param intervalSeconds 轮询间隔
* @return 待轮询设备列表
*/
@@ -68,7 +68,7 @@ public interface IGatewayPollingService {
/**
* 测试轮询 - 立即执行一次轮询
*
*
* @param productKey 产品Key
* @param deviceId 设备ID
* @return 操作结果
@@ -1,11 +1,13 @@
package cn.universal.admin.platform.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.universal.admin.config.ConnectionConfig;
import cn.universal.admin.platform.dto.ConnectionInfoDTO;
import cn.universal.admin.platform.service.ConnectionInfoService;
import cn.universal.common.constant.IoTConstant;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.Network;
import cn.universal.persistence.mapper.IoTProductMapper;
@@ -139,7 +141,7 @@ public class ConnectionInfoServiceImpl implements ConnectionInfoService {
// 处理MQTT主题信息
if ("mqtt".equals(product.getThirdPlatform())) {
ConnectionInfoDTO.TopicInfo subscribeTopics =
buildSubscribeTopics(config, product.getProductKey());
buildThirdMQTTSubscribeTopics(config, product);
builder.subscribeTopics(subscribeTopics);
}
@@ -214,45 +216,299 @@ public class ConnectionInfoServiceImpl implements ConnectionInfoService {
.passthroughTopics(passthroughTopics)
.build();
}
/** 构建订阅主题信息 */
private ConnectionInfoDTO.TopicInfo buildSubscribeTopics(JSONObject config, String productKey) {
String subscribe = config.getStr("subscribeTopics");
;
if (!JSONUtil.isTypeJSON(subscribe)) {
private ConnectionInfoDTO.TopicInfo buildThirdMQTTSubscribeTopics(JSONObject config, IoTProduct product) {
String productKey = product.getProductKey();
Object subscribeTopicsObj = config.get("subscribeTopics");
if (subscribeTopicsObj == null) {
return buildDefaultTopics(productKey);
}
JSONObject subscribeTopics = config.getJSONObject("subscribeTopics");
if (subscribeTopics == null || subscribeTopics.isEmpty()) {
// 使用默认主题
// 检测是否为数组格式(第三方MQTT新格式)
if (subscribeTopicsObj instanceof List || subscribeTopicsObj instanceof JSONArray) {
return buildThirdPartyTopics(subscribeTopicsObj, product);
}
// 检测是否为对象格式(旧格式)
if (subscribeTopicsObj instanceof JSONObject) {
JSONObject subscribeTopics = (JSONObject) subscribeTopicsObj;
if (subscribeTopics.isEmpty()) {
return buildDefaultTopics(productKey);
}
ConnectionInfoDTO.ThingTopics thingTopics = null;
ConnectionInfoDTO.PassthroughTopics passthroughTopics = null;
JSONObject thingTopicsConfig = subscribeTopics.getJSONObject("thingTopics");
if (thingTopicsConfig != null) {
thingTopics =
ConnectionInfoDTO.ThingTopics.builder()
.propertyUp(thingTopicsConfig.getStr("propertyUp"))
.eventUp(thingTopicsConfig.getStr("eventUp"))
.commandDown(thingTopicsConfig.getStr("commandDown"))
.build();
}
JSONObject passthroughTopicsConfig = subscribeTopics.getJSONObject("passthroughTopics");
if (passthroughTopicsConfig != null) {
passthroughTopics =
ConnectionInfoDTO.PassthroughTopics.builder()
.dataUp(passthroughTopicsConfig.getStr("dataUp"))
.commandDown(passthroughTopicsConfig.getStr("commandDown"))
.build();
}
return ConnectionInfoDTO.TopicInfo.builder()
.thingTopics(thingTopics)
.passthroughTopics(passthroughTopics)
.build();
}
// 其他情况,回退到默认主题
return buildDefaultTopics(productKey);
}
/**
* 构建第三方MQTT主题信息(数组格式)
*
* @param subscribeTopicsObj 订阅主题配置(数组格式)
* @param product 产品对象,用于过滤匹配的主题和获取下行主题配置
* @return 主题信息
*/
@SuppressWarnings("unchecked")
private ConnectionInfoDTO.TopicInfo buildThirdPartyTopics(Object subscribeTopicsObj, IoTProduct product) {
String productKey = product.getProductKey();
List<Object> topicList;
// 转换为List
if (subscribeTopicsObj instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) subscribeTopicsObj;
topicList = jsonArray.toList(Object.class);
} else if (subscribeTopicsObj instanceof List) {
topicList = (List<Object>) subscribeTopicsObj;
} else {
return buildDefaultTopics(productKey);
}
ConnectionInfoDTO.ThingTopics thingTopics = null;
ConnectionInfoDTO.PassthroughTopics passthroughTopics = null;
JSONObject thingTopicsConfig = subscribeTopics.getJSONObject("thingTopics");
if (thingTopicsConfig != null) {
thingTopics =
ConnectionInfoDTO.ThingTopics.builder()
.propertyUp(thingTopicsConfig.getStr("propertyUp"))
.eventUp(thingTopicsConfig.getStr("eventUp"))
.commandDown(thingTopicsConfig.getStr("commandDown"))
.build();
if (topicList == null || topicList.isEmpty()) {
return buildDefaultTopics(productKey);
}
JSONObject passthroughTopicsConfig = subscribeTopics.getJSONObject("passthroughTopics");
if (passthroughTopicsConfig != null) {
passthroughTopics =
ConnectionInfoDTO.PassthroughTopics.builder()
.dataUp(passthroughTopicsConfig.getStr("dataUp"))
.commandDown(passthroughTopicsConfig.getStr("commandDown"))
.build();
// 使用List收集相同类型的主题,最后用逗号连接
List<String> propertyUpList = new java.util.ArrayList<>();
List<String> eventUpList = new java.util.ArrayList<>();
List<String> thingCommandDownList = new java.util.ArrayList<>();
List<String> passthroughDataUpList = new java.util.ArrayList<>();
List<String> passthroughCommandDownList = new java.util.ArrayList<>();
boolean hasThingTopics = false;
boolean hasPassthroughTopics = false;
// 遍历主题配置,过滤出与当前productKey匹配的主题
for (Object item : topicList) {
if (!(item instanceof JSONObject)) {
continue;
}
JSONObject topicConfig = (JSONObject) item;
// 检查是否启用
if (!topicConfig.getBool("enabled", true)) {
continue;
}
// 检查productKey匹配(如果配置的productKey为空,视为通用主题)
String configProductKey = topicConfig.getStr("productKey");
if (StrUtil.isNotBlank(configProductKey) && !configProductKey.equals(productKey)) {
continue; // 不匹配,跳过
}
String topicPattern = topicConfig.getStr("topicPattern");
if (StrUtil.isBlank(topicPattern)) {
continue;
}
// 根据topicCategory分类
String topicCategory = topicConfig.getStr("topicCategory");
if ("THING_MODEL".equals(topicCategory)) {
// 物模型主题:根据topic pattern判断是propertyUp、eventUp还是commandDown
String topicType = inferThingTopicType(topicPattern);
switch (topicType) {
case "propertyUp":
propertyUpList.add(topicPattern);
hasThingTopics = true;
break;
case "eventUp":
eventUpList.add(topicPattern);
hasThingTopics = true;
break;
case "commandDown":
thingCommandDownList.add(topicPattern);
hasThingTopics = true;
break;
default:
// 如果无法判断,默认作为propertyUp
propertyUpList.add(topicPattern);
hasThingTopics = true;
break;
}
} else if ("PASSTHROUGH".equals(topicCategory)) {
// 透传主题:根据topic pattern判断是dataUp还是commandDown
String topicType = inferPassthroughTopicType(topicPattern);
switch (topicType) {
case "dataUp":
passthroughDataUpList.add(topicPattern);
hasPassthroughTopics = true;
break;
case "commandDown":
passthroughCommandDownList.add(topicPattern);
hasPassthroughTopics = true;
break;
default:
// 如果无法判断,默认作为dataUp
passthroughDataUpList.add(topicPattern);
hasPassthroughTopics = true;
break;
}
} else {
// 未配置topicCategory,尝试自动判断
// 如果包含/down/,可能是下行主题;否则默认为上行主题
if (topicPattern.contains("/down/") || topicPattern.contains("/down")) {
// 无法确定是物模型还是透传,优先作为物模型的commandDown
thingCommandDownList.add(topicPattern);
hasThingTopics = true;
} else {
// 上行主题,无法确定类型,优先作为透传的dataUp
passthroughDataUpList.add(topicPattern);
hasPassthroughTopics = true;
}
}
}
return ConnectionInfoDTO.TopicInfo.builder()
.thingTopics(thingTopics)
.passthroughTopics(passthroughTopics)
.build();
// 构建物模型主题
ConnectionInfoDTO.ThingTopics.ThingTopicsBuilder thingTopicsBuilder =
ConnectionInfoDTO.ThingTopics.builder();
if (!propertyUpList.isEmpty()) {
thingTopicsBuilder.propertyUp(String.join(",", propertyUpList));
}
if (!eventUpList.isEmpty()) {
thingTopicsBuilder.eventUp(String.join(",", eventUpList));
}
// 处理下行主题:优先从产品配置中读取downTopic
String commandDownTopic = getProductDownTopic(product);
if (StrUtil.isNotBlank(commandDownTopic)) {
// 产品配置中有downTopic,直接使用
thingTopicsBuilder.commandDown(commandDownTopic);
} else if (!thingCommandDownList.isEmpty()) {
// 如果产品配置中没有,使用收集到的下行主题(用逗号连接)
thingTopicsBuilder.commandDown(String.join(",", thingCommandDownList));
} else {
// 如果都没有,使用默认的物模型下行主题格式(与BaseReplyProcessor中的规则一致)
// 格式:$thing/down/${productKey}/${deviceId}
String defaultDownTopic = "$thing/down/${productKey}/${deviceId}";
thingTopicsBuilder.commandDown(defaultDownTopic);
}
// 构建透传主题
ConnectionInfoDTO.PassthroughTopics.PassthroughTopicsBuilder passthroughTopicsBuilder =
ConnectionInfoDTO.PassthroughTopics.builder();
if (!passthroughDataUpList.isEmpty()) {
passthroughTopicsBuilder.dataUp(String.join(",", passthroughDataUpList));
}
if (!passthroughCommandDownList.isEmpty()) {
passthroughTopicsBuilder.commandDown(String.join(",", passthroughCommandDownList));
}
ConnectionInfoDTO.TopicInfo.TopicInfoBuilder builder = ConnectionInfoDTO.TopicInfo.builder();
if (hasThingTopics) {
builder.thingTopics(thingTopicsBuilder.build());
}
if (hasPassthroughTopics) {
builder.passthroughTopics(passthroughTopicsBuilder.build());
}
return builder.build();
}
/**
* 从产品配置中获取下行主题
* 如果产品配置中没有downTopic,返回null,使用默认规则
*
* @param product 产品对象
* @return 下行主题,如果不存在返回null
*/
private String getProductDownTopic(IoTProduct product) {
if (product == null || StrUtil.isBlank(product.getConfiguration())) {
return null;
}
try {
JSONObject config = JSONUtil.parseObj(product.getConfiguration());
String downTopic = config.getStr("downTopic");
return StrUtil.isNotBlank(downTopic) ? downTopic : null;
} catch (Exception e) {
log.warn("解析产品配置失败,productKey={}", product.getProductKey(), e);
return null;
}
}
/**
* 推断物模型主题类型(propertyUp、eventUp、commandDown)
*/
private String inferThingTopicType(String topicPattern) {
if (StrUtil.isBlank(topicPattern)) {
return "propertyUp";
}
String lowerPattern = topicPattern.toLowerCase();
// 判断是否为下行主题
if (lowerPattern.contains("/down/") || lowerPattern.contains("/down")
|| lowerPattern.contains("/set/") || lowerPattern.contains("/set")) {
return "commandDown";
}
// 判断是否为事件主题
if (lowerPattern.contains("/event/") || lowerPattern.contains("/event")) {
return "eventUp";
}
// 判断是否为属性主题
if (lowerPattern.contains("/property/") || lowerPattern.contains("/property")) {
return "propertyUp";
}
// 默认作为属性上报
return "propertyUp";
}
/**
* 推断透传主题类型(dataUp、commandDown)
*/
private String inferPassthroughTopicType(String topicPattern) {
if (StrUtil.isBlank(topicPattern)) {
return "dataUp";
}
String lowerPattern = topicPattern.toLowerCase();
// 判断是否为下行主题
if (lowerPattern.contains("/down/") || lowerPattern.contains("/down")
|| lowerPattern.contains("/set/") || lowerPattern.contains("/set")
|| lowerPattern.contains("/command/") || lowerPattern.contains("/command")) {
return "commandDown";
}
// 默认作为数据上报
return "dataUp";
}
/** 替换主题中的产品Key占位符 */
@@ -15,6 +15,9 @@ package cn.universal.admin.platform.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.universal.admin.platform.service.DeviceCommandDispatcher;
import cn.universal.admin.platform.service.IGatewayPollingService;
import cn.universal.common.constant.IoTConstant.DeviceNode;
@@ -29,6 +32,8 @@ import cn.universal.persistence.mapper.GatewayPollingConfigMapper;
import cn.universal.persistence.mapper.IoTDeviceMapper;
import java.util.Date;
import java.util.List;
import cn.universal.persistence.query.IoTAPIQuery;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -188,12 +193,11 @@ public class GatewayPollingServiceImpl implements IGatewayPollingService {
IoTDevice gateway =
deviceMapper.selectIoTDevice(config.getProductKey(), config.getDeviceId());
// if (gateway == null || !Boolean.TRUE.equals(gateway.getState())) {
// log.warn(
// "网关离线,跳过轮询: productKey={}, deviceId={}", config.getProductKey(),
// config.getDeviceId());
// return;
// }
// if (gateway == null || !Boolean.TRUE.equals(gateway.getState())) {
// log.warn(
// "网关离线,跳过轮询: productKey={}, deviceId={}", config.getProductKey(), config.getDeviceId());
// return;
// }
// 2. 查询轮询指令 (使用 productKey + deviceId)
List<GatewayPollingCommand> commands =
@@ -215,9 +219,8 @@ public class GatewayPollingServiceImpl implements IGatewayPollingService {
boolean allSuccess = true;
int executedCount = 0;
// 获取配置的指令间隔,默认300ms
int commandIntervalMs =
config.getCommandIntervalMs() != null ? config.getCommandIntervalMs() : 300;
int commandIntervalMs = config.getCommandIntervalMs() != null ? config.getCommandIntervalMs() : 300;
for (GatewayPollingCommand command : commands) {
if (Boolean.TRUE.equals(command.getEnabled())) {
// 非第一条指令先等待,给设备响应时间
@@ -225,13 +228,15 @@ public class GatewayPollingServiceImpl implements IGatewayPollingService {
try {
Thread.sleep(commandIntervalMs);
log.debug(
"[指令间隔] 等待{}ms,防止设备缓冲区溢出: deviceId={}", commandIntervalMs, config.getDeviceId());
"[指令间隔] 等待{}ms,防止设备缓冲区溢出: deviceId={}",
commandIntervalMs,
config.getDeviceId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("指令间隔等待被中断", e);
}
}
boolean success = executeCommand(gateway, command);
if (!success) {
allSuccess = false;
@@ -280,7 +285,10 @@ public class GatewayPollingServiceImpl implements IGatewayPollingService {
IoTDeviceDTO device =
deviceMapper.selectIoTDeviceBO(
BeanUtil.beanToMap(
IoTDevice.builder().productKey(productKey).deviceId(deviceId).build()));
IoTDevice.builder()
.productKey(productKey)
.deviceId(deviceId)
.build()));
if (device == null) {
return R.error("设备不存在");
@@ -298,16 +306,13 @@ public class GatewayPollingServiceImpl implements IGatewayPollingService {
return R.error("未配置轮询指令,请先添加轮询指令");
}
log.info(
"开始测试轮询: productKey={}, deviceId={}, commands={}", productKey, deviceId, commands.size());
log.info("开始测试轮询: productKey={}, deviceId={}, commands={}",
productKey, deviceId, commands.size());
// 4. 立即执行轮询
pollGatewayDevice(config);
return R.ok(
"测试轮询已执行,共发送 "
+ commands.stream().filter(cmd -> Boolean.TRUE.equals(cmd.getEnabled())).count()
+ " 条指令");
return R.ok("测试轮询已执行,共发送 " + commands.stream().filter(cmd -> Boolean.TRUE.equals(cmd.getEnabled())).count() + " 条指令");
} catch (Exception e) {
log.error("测试轮询失败: productKey={}, deviceId={}", productKey, deviceId, e);
@@ -546,8 +546,7 @@ public class IoTDeviceServiceImpl extends BaseServiceImpl implements IIoTDeviceS
IoTProduct ioTProduct = ioTProductMapper.getProductByProductKey(productKey);
// 转换为UnifiedDownlinkCommand(保持所有参数)
UnifiedDownlinkCommand command =
UnifiedDownlinkCommand.fromJson(JSONUtil.parseObj(downRequest));
UnifiedDownlinkCommand command = UnifiedDownlinkCommand.fromJson(JSONUtil.parseObj(downRequest));
R result = IoTDownlFactory.getIDown(ioTProduct.getThirdPlatform()).doAction(command);
return result;
}
@@ -487,7 +487,9 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
"supportMQTTNetwork",
"selectDevProductV4ListNoPage",
"countDevNumberByProductKey",
"iot_product_device"
"iot_product_device",
"getProductEncoderType",
"getProductDecoderType"
},
allEntries = true)
public int updateDevProduct(IoTProduct ioTProduct) {
@@ -529,7 +531,11 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
},
allEntries = true)
public int deleteDevProductByIds(String[] ids) {
String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId();
IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId());
if (iotUser == null) {
throw new IoTException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED);
}
String appUnionId = iotUser.getUnionId();
int count = 0;
for (String id : ids) {
IoTProduct ioTProduct = ioTProductMapper.selectDevProductById(id);
@@ -595,17 +601,22 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
"selectDevCount",
"supportMQTTNetwork",
"iot_dev_product_list",
"getProductConfiguration"
"getProductConfiguration",
"getProductEncoderType",
"getProductDecoderType"
},
allEntries = true)
public int updateDevProductConfig(IoTProductVO devProduct) {
String appUnionId = queryIotUser(SecurityUtils.getUnionId()).getUnionId();
IoTUser iotUser = queryIotUser(SecurityUtils.getUnionId());
if (iotUser == null || iotUser.getUnionId() == null) {
throw new IoTException("获取用户信息异常", HttpStatus.HTTP_UNAUTHORIZED);
}
// 根据id获取产品信息
IoTProduct product = ioTProductMapper.selectDevProductById(String.valueOf(devProduct.getId()));
if (devProduct == null) {
throw new IoTException("产品不存在");
}
if (!appUnionId.equals(product.getCreatorId())) {
if (!iotUser.getUnionId().equals(product.getCreatorId()) && !iotUser.isAdmin()) {
throw new IoTException("无产品权限");
}
// 获取新的产品配置信息
@@ -627,67 +638,6 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
throw new IoTException(port + "端口" + "已被占用");
}
}
Network network =
networkMapper.selectOne(Network.builder().productKey(product.getProductKey()).build());
int updateCount;
try {
// 获取配置
JSONObject tcpConfig = new JSONObject();
JSONObject parserConfiguration = new JSONObject();
parserConfiguration.set("cert", newConfig.getStr("cert"));
parserConfiguration.set("key", newConfig.getStr("key"));
parserConfiguration.set("rootCa", newConfig.getStr("rootCa"));
parserConfiguration.set("delimited", newConfig.getStr("delimited"));
parserConfiguration.set("lineMaxLength", newConfig.getInt("lineMaxLength"));
parserConfiguration.set("delimitedMaxlength", newConfig.getInt("delimitedMaxlength"));
parserConfiguration.set("fixedLength", newConfig.getInt("fixedLength"));
parserConfiguration.set("byteOrderLittle", newConfig.getBool("byteOrderLittle", false));
parserConfiguration.set("maxFrameLength", newConfig.getInt("maxFrameLength"));
parserConfiguration.set("lengthFieldOffset", newConfig.getInt("lengthFieldOffset"));
parserConfiguration.set("lengthFieldLength", newConfig.getInt("lengthFieldLength"));
parserConfiguration.set("lengthAdjustment", newConfig.getInt("lengthAdjustment"));
parserConfiguration.set("initialBytesToStrip", newConfig.getInt("initialBytesToStrip"));
parserConfiguration.set("failFast", newConfig.getBool("failFast", false));
tcpConfig.set("parserConfiguration", parserConfiguration);
tcpConfig.set("ssl", newConfig.getBool("ssl", false));
tcpConfig.set("port", port);
tcpConfig.set("onlyCache", newConfig.getBool("onlyCache", false));
tcpConfig.set("productKey", product.getProductKey());
tcpConfig.set("host", newConfig.getStr("host"));
tcpConfig.set(IoTConstant.ALLOW_INSERT, newConfig.getBool(IoTConstant.ALLOW_INSERT, false));
tcpConfig.set("preStore", newConfig.getBool("preStore", false));
tcpConfig.set("alwaysPreDecode", newConfig.getBool("alwaysPreDecode", false));
tcpConfig.set("decoderType", newConfig.getStr("decoderType"));
tcpConfig.set("parserType", newConfig.getStr("parserType"));
tcpConfig.set("allIdleTime", newConfig.getInt("allIdleTime"));
tcpConfig.set("readerIdleTime", newConfig.getInt("readerIdleTime"));
tcpConfig.set("writerIdleTime", newConfig.getInt("writerIdleTime"));
tcpConfig.set("readTimeout", newConfig.getInt("readTimeout"));
tcpConfig.set("sendTimeout", newConfig.getInt("sendTimeout"));
tcpConfig.set("idleInterval", newConfig.getInt("idleInterval"));
// if (Objects.isNull(network)) {
// // 新增
// updateCount = networkMapper.insert(
// Network.builder().type("TCP_SERVER").productKey(product.getProductKey())
// .description("tcp_server").createDate(new Date()).createUser(appUnionId)
//
// .state(newConfig.getBool("enabled")).configuration(tcpConfig.toString()).build());
// } else {
// // 更新
// network.setConfiguration(tcpConfig.toString());
// network.setState(newConfig.getBool("enabled"));
// updateCount = networkMapper.updateByPrimaryKey(network);
// }
// 更改配置后重启tcp监听
// if (updateCount > 0) {
// 暂时不重启
// flushNettyServer(JSONUtil.toJsonStr(tcpConfig), product.getProductKey(),
// TcpFlushType.reload);
// }
} catch (Exception e) {
log.error("tcp网络组件配置失败", e);
throw new IoTException("添加失败");
}
}
// 获取旧的产品配置信息
JSONObject oldConfig = JSONUtil.parseObj(product.getConfiguration());
@@ -703,7 +653,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
"更新产品={},更新状态={},更新人={}",
devProduct.getProductKey() + devProduct.getName(),
updateCount,
appUnionId);
iotUser.getUnionId());
}
return updateCount;
@@ -1139,6 +1089,7 @@ public class IoTProductServiceImpl extends BaseServiceImpl implements IIoTProduc
}
@Override
@CacheEvict(cacheNames = {"iot_product_log_store_policy"},allEntries = true)
public int updateDevProductStoreConfig(IoTProductVO storeConfig) {
// 获取产品信息
IoTProduct product = ioTProductMapper.selectDevProductById(String.valueOf(storeConfig.getId()));
@@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.*;
/**
* 网关轮询配置控制器
*
*
* @author Aleo
* @date 2025-10-26
*/
@@ -34,9 +34,12 @@ import org.springframework.web.bind.annotation.*;
@Tag(name = "网关轮询管理", description = "网关云端轮询配置管理接口")
public class GatewayPollingController extends BaseController {
@Autowired private IGatewayPollingService gatewayPollingService;
@Autowired
private IGatewayPollingService gatewayPollingService;
/** 保存网关轮询配置 */
/**
* 保存网关轮询配置
*/
@Operation(summary = "保存轮询配置", description = "保存或更新网关轮询配置及指令")
@PostMapping("/save")
@PreAuthorize("@ss.hasPermi('platform:device:edit')")
@@ -44,33 +47,45 @@ public class GatewayPollingController extends BaseController {
return gatewayPollingService.savePollingConfig(dto);
}
/** 获取网关轮询配置 */
/**
* 获取网关轮询配置
*/
@Operation(summary = "获取轮询配置", description = "根据产品Key和设备ID获取轮询配置")
@GetMapping("/get/{productKey}/{deviceId}")
@PreAuthorize("@ss.hasPermi('platform:device:query')")
public R getConfig(
@Parameter(description = "产品Key", required = true) @PathVariable String productKey,
@Parameter(description = "设备ID", required = true) @PathVariable String deviceId) {
@Parameter(description = "产品Key", required = true)
@PathVariable String productKey,
@Parameter(description = "设备ID", required = true)
@PathVariable String deviceId) {
return gatewayPollingService.getPollingConfig(productKey, deviceId);
}
/** 删除网关轮询配置 */
/**
* 删除网关轮询配置
*/
@Operation(summary = "删除轮询配置", description = "删除指定设备的轮询配置")
@DeleteMapping("/delete/{productKey}/{deviceId}")
@PreAuthorize("@ss.hasPermi('platform:device:remove')")
public R deleteConfig(
@Parameter(description = "产品Key", required = true) @PathVariable String productKey,
@Parameter(description = "设备ID", required = true) @PathVariable String deviceId) {
@Parameter(description = "产品Key", required = true)
@PathVariable String productKey,
@Parameter(description = "设备ID", required = true)
@PathVariable String deviceId) {
return gatewayPollingService.deletePollingConfig(productKey, deviceId);
}
/** 测试轮询指令 */
/**
* 测试轮询指令
*/
@Operation(summary = "测试轮询指令", description = "立即执行一次轮询测试")
@PostMapping("/test/{productKey}/{deviceId}")
@PreAuthorize("@ss.hasPermi('platform:device:edit')")
public R testPolling(
@Parameter(description = "产品Key", required = true) @PathVariable String productKey,
@Parameter(description = "设备ID", required = true) @PathVariable String deviceId) {
@Parameter(description = "产品Key", required = true)
@PathVariable String productKey,
@Parameter(description = "设备ID", required = true)
@PathVariable String deviceId) {
return gatewayPollingService.testPolling(productKey, deviceId);
}
}
@@ -26,6 +26,7 @@ import cn.universal.common.exception.CodecException;
import cn.universal.common.exception.IoTException;
import cn.universal.ossm.service.ISysOssService;
import cn.universal.persistence.entity.IoTDeviceProtocol;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO;
import cn.universal.persistence.entity.vo.IoTProtocolVO;
@@ -46,6 +47,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
@@ -57,11 +59,16 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/admin/v1/protocol")
public class IoTDeviceProtocolController extends BaseController {
@Autowired private IoTDeviceProtocolService devProtocolService;
@Resource private IoTDeviceMessageCodecService ioTDeviceMessageCodecService;
@Resource private ISysOssService ossService;
@Autowired
private IoTDeviceProtocolService devProtocolService;
@Resource
private IoTDeviceMessageCodecService ioTDeviceMessageCodecService;
@Resource
private ISysOssService ossService;
/** 查询设备协议列表 */
/**
* 查询设备协议列表
*/
@GetMapping("/list")
public TableDataInfo list(IoTDeviceProtocol ioTDeviceProtocol) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
@@ -71,7 +78,9 @@ public class IoTDeviceProtocolController extends BaseController {
return getDataTable(list);
}
/** 查询设备协议列表 */
/**
* 查询设备协议列表
*/
@GetMapping("/list/ids")
public AjaxResult<List<String>> idList(IoTDeviceProtocol ioTDeviceProtocol) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
@@ -81,7 +90,9 @@ public class IoTDeviceProtocolController extends BaseController {
return AjaxResult.success(list1);
}
/** 导出设备协议列表 */
/**
* 导出设备协议列表
*/
@PostMapping("/export")
@Log(title = "导出设备协议列表", businessType = BusinessType.EXPORT)
public void export(HttpServletResponse response, IoTDeviceProtocol ioTDeviceProtocol) {
@@ -92,7 +103,9 @@ public class IoTDeviceProtocolController extends BaseController {
util.exportExcel(response, list, "设备协议数据");
}
/** 获取设备协议详细信息 */
/**
* 获取设备协议详细信息
*/
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
if (StrUtil.isBlank(id)) {
@@ -115,7 +128,9 @@ public class IoTDeviceProtocolController extends BaseController {
return AjaxResult.success(devProductVO);
}
/** 新增设备协议 */
/**
* 新增设备协议
*/
@PostMapping
@Log(title = "新增设备协议", businessType = BusinessType.INSERT)
public AjaxResult add(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) {
@@ -144,7 +159,9 @@ public class IoTDeviceProtocolController extends BaseController {
return toAjax(devProtocolService.insertDevProtocol(ioTDeviceProtocol));
}
/** 修改设备协议 */
/**
* 修改设备协议
*/
@PutMapping
@Log(title = "修改设备协议", businessType = BusinessType.UPDATE)
public AjaxResult edit(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) {
@@ -152,7 +169,9 @@ public class IoTDeviceProtocolController extends BaseController {
return toAjax(devProtocolService.updateDevProtocol(ioTDeviceProtocolBO, iotUser));
}
/** 删除设备协议 */
/**
* 删除设备协议
*/
@DeleteMapping("/{ids}")
@Log(title = "删除设备协议", businessType = BusinessType.DELETE)
public AjaxResult remove(@PathVariable String[] ids) {
@@ -165,7 +184,9 @@ public class IoTDeviceProtocolController extends BaseController {
return toAjax(devProtocolService.deleteDevProtocolByIds(ids));
}
/** 编解码测试 */
/**
* 编解码测试
*/
@PostMapping("/codec")
public AjaxResult codec(@RequestBody IoTDeviceProtocolBO ioTDeviceProtocolBO) {
@@ -183,4 +204,23 @@ public class IoTDeviceProtocolController extends BaseController {
return AjaxResult.success(e.getMsg());
}
}
/**
* 查询未创建协议的产品列表(归属人为当前用户)
*
* @param searchKey 搜索关键词(产品名称或productKey,可选)
* @return 产品列表
*/
@GetMapping("/products/without-protocol")
public AjaxResult<List<IoTProduct>> getProductsWithoutProtocol(
@RequestParam(value = "searchKey", required = false) String searchKey) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 获取当前用户的unionId,如果是管理员则使用parentUnionId
String unionId = iotUser.isAdmin() ? iotUser.getParentUnionId() : iotUser.getUnionId();
if (unionId == null) {
unionId = iotUser.getUnionId();
}
List<IoTProduct> list = devProtocolService.selectProductsWithoutProtocol(unionId, searchKey);
return AjaxResult.success(list);
}
}
@@ -45,6 +45,7 @@ import cn.universal.persistence.entity.admin.SysDictData;
import cn.universal.persistence.entity.bo.IoTProductBO;
import cn.universal.persistence.entity.bo.IoTProductImportBO;
import cn.universal.persistence.entity.vo.IoTDeviceModelVO;
import cn.universal.persistence.entity.vo.IoTProductExportVO;
import cn.universal.persistence.entity.vo.IoTProductVO;
import cn.universal.persistence.mapper.IoTProductMapper;
import cn.universal.persistence.page.TableDataInfo;
@@ -572,9 +573,8 @@ public class IoTProductController extends BaseController {
/**
* 导出设备产品列表
*
* @deprecated 请使用 ProductImportExportController.exportProductPackages() 代替 该接口仅保留用于向后兼容,建议使用
* /admin/v1/product/transfer/export
* @deprecated 请使用 ProductImportExportController.exportProductPackages() 代替
* 该接口仅保留用于向后兼容,建议使用 /admin/v1/product/transfer/export
*/
@Deprecated
@PostMapping("/export")
@@ -590,17 +590,17 @@ public class IoTProductController extends BaseController {
}
// 使用新的导出包方法
List<cn.universal.persistence.entity.dto.ProductExportPackageDTO> packageList =
List<cn.universal.persistence.entity.dto.ProductExportPackageDTO> packageList =
devProductService.exportProductPackages(query);
// 设置响应头为文件下载格式
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
// 生成文件名:产品导出_时间戳.json
String fileName = String.format("products_export_%d.json", System.currentTimeMillis());
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 添加缓存控制头,防止浏览器缓存
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
@@ -629,9 +629,8 @@ public class IoTProductController extends BaseController {
/**
* 批量导入产品
*
* @deprecated 请使用 ProductImportExportController.importProductPackages() 代替 该接口仅保留用于向后兼容,建议使用
* /admin/v1/product/transfer/import
* @deprecated 请使用 ProductImportExportController.importProductPackages() 代替
* 该接口仅保留用于向后兼容,建议使用 /admin/v1/product/transfer/import
*/
@Deprecated
@PostMapping("/import")
@@ -646,8 +645,7 @@ public class IoTProductController extends BaseController {
// 解析JSON数据 - 尝试解析为新格式(完整包)
try {
List<cn.universal.persistence.entity.dto.ProductExportPackageDTO> packages =
JSONUtil.toList(
jsonContent, cn.universal.persistence.entity.dto.ProductExportPackageDTO.class);
JSONUtil.toList(jsonContent, cn.universal.persistence.entity.dto.ProductExportPackageDTO.class);
if (packages != null && !packages.isEmpty()) {
// 使用新的导入方法
@@ -220,9 +220,9 @@ public class IoTUserAppController extends BaseController {
@Log(title = "重置密钥", businessType = BusinessType.UPDATE)
public AjaxResult<Void> resetSecret(@RequestBody IoTUserApplication iotUserApplication) {
IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId());
// 权限检查
IoTUserApplication oldApplication =
IoTUserApplication oldApplication =
iotUserApplicationService.selectIotUserApplicationById(iotUserApplication.getAppUniqueId());
if (!user.isAdmin()
&& ObjectUtil.isNotEmpty(oldApplication)
@@ -168,12 +168,8 @@ public class IoTUserController extends BaseController {
userbo.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
// userbo.setPassword(user.getPassword());
userbo.setUnionId(userbo.getUsername());
userbo.setRegisterFrom("admin");
userbo.setDeleted(0);
if (parentUser.isAdmin()) {
userbo.setLicense(5000);
userbo.setLicenseTotalAmount(5000);
}
return toAjax(ioTUserService.insertUser(userbo));
}
@@ -36,8 +36,9 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 产品导入导出控制器 专门处理产品的完整导出导入功能
*
* 产品导入导出控制器
* 专门处理产品的完整导出导入功能
*
* @author NexIoT
* @since 2025-10-27
*/
@@ -49,7 +50,11 @@ public class ProductImportExportController extends BaseController {
@Autowired private IIoTProductService devProductService;
/** 导出产品完整包 包含产品基本信息、设备协议、网络配置、物模型等所有配置 导出后可直接在其他环境导入使用,无需额外配置 */
/**
* 导出产品完整包
* 包含产品基本信息、设备协议、网络配置、物模型等所有配置
* 导出后可直接在其他环境导入使用,无需额外配置
*/
@PostMapping("/export")
@Operation(summary = "导出产品完整包", description = "导出产品及其关联的协议、网络配置、物模型等完整信息")
@Log(title = "产品完整包导出", businessType = BusinessType.EXPORT)
@@ -57,7 +62,7 @@ public class ProductImportExportController extends BaseController {
try {
// 获取当前用户信息
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 权限控制:非管理员只能导出自己创建的产品
query.setSelf(!iotUser.isAdmin());
if (query.isSelf()) {
@@ -73,11 +78,11 @@ public class ProductImportExportController extends BaseController {
// 设置响应头
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
// 生成文件名:产品导出包_时间戳.json
String fileName = String.format("product_packages_%d.json", System.currentTimeMillis());
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
// 缓存控制
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
@@ -95,7 +100,7 @@ public class ProductImportExportController extends BaseController {
out.write(bytes);
out.flush();
}
log.info("产品导出成功: 用户={}, 导出数量={}", iotUser.getUnionId(), packageList.size());
} catch (Exception e) {
log.error("产品导出失败", e);
@@ -103,7 +108,11 @@ public class ProductImportExportController extends BaseController {
}
}
/** 导入产品完整包 支持导入完整的产品配置,包括协议、网络、物模型等 自动处理ProductKey冲突、协议依赖等问题 */
/**
* 导入产品完整包
* 支持导入完整的产品配置,包括协议、网络、物模型等
* 自动处理ProductKey冲突、协议依赖等问题
*/
@PostMapping("/import")
@Operation(summary = "导入产品完整包", description = "导入产品完整配置包,自动处理所有依赖关系")
@Log(title = "产品完整包导入", businessType = BusinessType.IMPORT)
@@ -112,7 +121,7 @@ public class ProductImportExportController extends BaseController {
try {
// 获取当前用户ID
String unionId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId();
// 读取上传的JSON文件
String jsonContent = new String(file.getBytes(), "UTF-8");
@@ -126,7 +135,7 @@ public class ProductImportExportController extends BaseController {
// 执行导入
String result = devProductService.importProductPackages(packages, unionId);
log.info("产品导入完成: 用户={}, 导入数量={}", unionId, packages.size());
return AjaxResult.success(result);
} catch (Exception e) {
@@ -6,12 +6,14 @@ import cn.universal.manager.notice.dto.NoticeChannelDTO;
import cn.universal.manager.notice.model.NoticeChannel;
import cn.universal.manager.notice.service.NoticeChannelService;
import cn.universal.manager.notice.util.JsonDesensitizationUtil;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.page.TableDataInfo;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -40,8 +42,18 @@ public class NoticeChannelController extends BaseController {
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String channelType) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
String currentUser = SecurityUtils.getUnionId();
startPage();
List<NoticeChannel> list = noticeChannelService.search(name, channelType, null);
// 如果不是管理员,只显示该用户创建的渠道
if (!iotUser.isAdmin()) {
list = list.stream()
.filter(channel -> currentUser.equals(channel.getCreator()))
.collect(Collectors.toList());
}
List<NoticeChannelDTO> dtoList =
list.stream()
@@ -82,6 +94,15 @@ public class NoticeChannelController extends BaseController {
if (channel == null) {
return null;
}
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员且不是创建者,无权限访问
if (!iotUser.isAdmin() && !currentUser.equals(channel.getCreator())) {
return null;
}
NoticeChannelDTO dto = new NoticeChannelDTO();
dto.setId(channel.getId());
dto.setName(channel.getName());
@@ -107,14 +128,37 @@ public class NoticeChannelController extends BaseController {
@PostMapping("/save")
public Map<String, Object> save(@RequestBody NoticeChannelDTO channelDTO) {
try {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
NoticeChannel existingChannel = null;
// 如果是修改操作,检查权限
if (channelDTO.getId() != null) {
existingChannel = noticeChannelService.getById(channelDTO.getId());
if (existingChannel == null) {
return Map.of("code", 1, "msg", "渠道不存在");
}
// 如果不是管理员且不是创建者,无权限修改
if (!iotUser.isAdmin() && !currentUser.equals(existingChannel.getCreator())) {
return Map.of("code", 1, "msg", "无权限修改该渠道");
}
}
NoticeChannel channel = new NoticeChannel();
channel.setId(channelDTO.getId());
channel.setName(channelDTO.getName());
channel.setChannelType(channelDTO.getChannelType());
channel.setStatus(channelDTO.getStatus());
channel.setRemark(channelDTO.getRemark());
channel.setCreator(SecurityUtils.getUnionId());
channel.setCreateTime(channelDTO.getCreateTime());
// 如果是新增,设置创建者;如果是修改,保持原有创建者
if (channelDTO.getId() == null) {
channel.setCreator(currentUser);
} else if (existingChannel != null) {
channel.setCreator(existingChannel.getCreator());
channel.setCreateTime(existingChannel.getCreateTime());
}
channel.setUpdateTime(channelDTO.getUpdateTime());
// 将config对象转换为JSON字符串
if (channelDTO.getConfig() != null) {
@@ -135,6 +179,19 @@ public class NoticeChannelController extends BaseController {
public Map<String, Object> delete(@RequestBody List<Long> ids) {
try {
if (ids != null && !ids.isEmpty()) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员,检查是否有权限删除
if (!iotUser.isAdmin()) {
for (Long id : ids) {
NoticeChannel channel = noticeChannelService.getById(id);
if (channel != null && !currentUser.equals(channel.getCreator())) {
return Map.of("code", 1, "msg", "无权限删除该渠道");
}
}
}
noticeChannelService.deleteBatch(ids);
return Map.of("code", 0, "msg", "删除成功");
} else {
@@ -1,9 +1,11 @@
package cn.universal.admin.system.notice;
import cn.universal.admin.system.service.ISysNoticeService;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.entity.admin.SysNotice;
import cn.universal.persistence.page.TableDataInfo;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -17,45 +19,108 @@ import org.springframework.web.bind.annotation.RestController;
/**
* 系统通知Controller
*
*
* @author universal
* @date 2023-01-01
*/
@RestController
@RequestMapping("/system/notice")
public class NoticeController extends BaseController {
@Autowired
private ISysNoticeService noticeService;
@Autowired private ISysNoticeService noticeService;
/**
* 查询系统通知列表
*/
@GetMapping("/list")
public TableDataInfo<SysNotice> list(SysNotice notice) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
String currentUser = SecurityUtils.getUnionId();
// 如果不是管理员,只显示该用户创建的通知
if (!iotUser.isAdmin()) {
notice.setCreateBy(currentUser);
}
startPage();
List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list);
}
/** 查询系统通知列表 */
@GetMapping("/list")
public TableDataInfo<SysNotice> list(SysNotice notice) {
startPage();
List<SysNotice> list = noticeService.selectNoticeList(notice);
return getDataTable(list);
}
/**
* 获取系统通知详细信息
*/
@GetMapping(value = "/{noticeId}")
public SysNotice getInfo(@PathVariable("noticeId") Long noticeId) {
SysNotice notice = noticeService.selectNoticeById(noticeId);
if (notice == null) {
return null;
}
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员且不是创建者,无权限访问
if (!iotUser.isAdmin() && !currentUser.equals(notice.getCreateBy())) {
return null;
}
return notice;
}
/** 获取系统通知详细信息 */
@GetMapping(value = "/{noticeId}")
public SysNotice getInfo(@PathVariable("noticeId") Long noticeId) {
return noticeService.selectNoticeById(noticeId);
}
/**
* 新增系统通知
*/
@PostMapping
public void add(@RequestBody SysNotice notice) {
// 设置创建者
notice.setCreateBy(SecurityUtils.getUnionId());
noticeService.insertNotice(notice);
}
/** 新增系统通知 */
@PostMapping
public void add(@RequestBody SysNotice notice) {
noticeService.insertNotice(notice);
}
/**
* 修改系统通知
*/
@PutMapping
public void edit(@RequestBody SysNotice notice) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 检查权限
SysNotice existingNotice = noticeService.selectNoticeById(notice.getNoticeId());
if (existingNotice == null) {
throw new RuntimeException("通知不存在");
}
// 如果不是管理员且不是创建者,无权限修改
if (!iotUser.isAdmin() && !currentUser.equals(existingNotice.getCreateBy())) {
throw new RuntimeException("无权限修改该通知");
}
// 设置更新者
notice.setUpdateBy(currentUser);
noticeService.updateNotice(notice);
}
/** 修改系统通知 */
@PutMapping
public void edit(@RequestBody SysNotice notice) {
noticeService.updateNotice(notice);
}
/** 删除系统通知 */
@DeleteMapping("/{noticeIds}")
public void remove(@PathVariable Long[] noticeIds) {
noticeService.deleteNoticeByIds(noticeIds);
}
/**
* 删除系统通知
*/
@DeleteMapping("/{noticeIds}")
public void remove(@PathVariable Long[] noticeIds) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员,检查是否有权限删除
if (!iotUser.isAdmin()) {
for (Long noticeId : noticeIds) {
SysNotice notice = noticeService.selectNoticeById(noticeId);
if (notice != null && !currentUser.equals(notice.getCreateBy())) {
throw new RuntimeException("无权限删除该通知");
}
}
}
noticeService.deleteNoticeByIds(noticeIds);
}
}
@@ -2,8 +2,10 @@ package cn.universal.admin.system.notice;
import cn.universal.manager.notice.model.NoticeSendRecord;
import cn.universal.manager.notice.service.NoticeRecordService;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.page.TableDataInfo;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
@@ -28,8 +30,15 @@ public class NoticeRecordController extends BaseController {
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String type,
@RequestParam(required = false) String status) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
String currentUser = SecurityUtils.getUnionId();
// 如果不是管理员,只显示该用户创建的记录
String creator = iotUser.isAdmin() ? null : currentUser;
startPage();
List<NoticeSendRecord> list = recordService.search(keyword, type, status);
List<NoticeSendRecord> list = recordService.search(keyword, type, status, creator);
return getDataTable(list);
}
}
@@ -1,7 +1,12 @@
package cn.universal.admin.system.notice;
import cn.universal.manager.notice.model.NoticeSendRequest;
import cn.universal.manager.notice.model.NoticeTemplate;
import cn.universal.manager.notice.service.NoticeService;
import cn.universal.manager.notice.service.NoticeTemplateService;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
@@ -12,13 +17,16 @@ import org.springframework.web.bind.annotation.RestController;
/** 通知发送 */
@RestController
@RequestMapping("/admin/v1/notice")
public class NoticeSendController {
public class NoticeSendController extends BaseController {
private final NoticeService noticeService;
private final NoticeTemplateService templateService;
@Autowired
public NoticeSendController(NoticeService noticeService) {
public NoticeSendController(
NoticeService noticeService, NoticeTemplateService templateService) {
this.noticeService = noticeService;
this.templateService = templateService;
}
@PostMapping("/send")
@@ -31,9 +39,24 @@ public class NoticeSendController {
throw new IllegalArgumentException("模板ID不能为空");
}
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 检查模板是否存在及权限
NoticeTemplate template = templateService.getById(templateId);
if (template == null) {
throw new IllegalArgumentException("模板不存在");
}
// 如果不是管理员且不是创建者,无权限发送
if (!iotUser.isAdmin() && !currentUser.equals(template.getCreator())) {
throw new IllegalArgumentException("无权限使用该模板发送通知");
}
NoticeSendRequest req = new NoticeSendRequest();
req.setTemplateId(templateId);
req.setReceivers(receivers);
req.setCreator(currentUser); // 设置创建者为当前用户
if (params instanceof Map) {
req.setParams((Map<String, Object>) params);
}
@@ -3,12 +3,14 @@ package cn.universal.admin.system.notice;
import cn.universal.manager.notice.dto.NoticeTemplateDTO;
import cn.universal.manager.notice.model.NoticeTemplate;
import cn.universal.manager.notice.service.NoticeTemplateService;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.page.TableDataInfo;
import cn.universal.persistence.query.AjaxResult;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -34,8 +36,18 @@ public class NoticeTemplateController extends BaseController {
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String name,
@RequestParam(required = false) String channelType) {
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
String currentUser = SecurityUtils.getUnionId();
startPage();
List<NoticeTemplate> list = noticeTemplateService.search(name, channelType, null);
// 如果不是管理员,只显示该用户创建的模板
if (!iotUser.isAdmin()) {
list = list.stream()
.filter(template -> currentUser.equals(template.getCreator()))
.collect(Collectors.toList());
}
List<NoticeTemplateDTO> dtoList =
list.stream()
@@ -63,7 +75,22 @@ public class NoticeTemplateController extends BaseController {
@PostMapping("/save")
public AjaxResult save(@RequestBody NoticeTemplateDTO templateDTO) {
try {
noticeTemplateService.saveTemplate(templateDTO, SecurityUtils.getUnionId());
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果是修改操作,检查权限
if (templateDTO.getId() != null) {
NoticeTemplate existingTemplate = noticeTemplateService.getById(templateDTO.getId());
if (existingTemplate == null) {
return AjaxResult.error("模板不存在");
}
// 如果不是管理员且不是创建者,无权限修改
if (!iotUser.isAdmin() && !currentUser.equals(existingTemplate.getCreator())) {
return AjaxResult.error("无权限修改该模板");
}
}
noticeTemplateService.saveTemplate(templateDTO, currentUser);
return AjaxResult.success("操作成功");
} catch (Exception e) {
return AjaxResult.error("操作失败: " + e.getMessage());
@@ -74,6 +101,19 @@ public class NoticeTemplateController extends BaseController {
public AjaxResult delete(@RequestBody List<Long> ids) {
try {
if (ids != null && !ids.isEmpty()) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员,检查是否有权限删除
if (!iotUser.isAdmin()) {
for (Long id : ids) {
NoticeTemplate template = noticeTemplateService.getById(id);
if (template != null && !currentUser.equals(template.getCreator())) {
return AjaxResult.error("无权限删除该模板");
}
}
}
noticeTemplateService.deleteBatch(ids);
return AjaxResult.success("删除成功");
} else {
@@ -89,6 +129,19 @@ public class NoticeTemplateController extends BaseController {
public AjaxResult enable(@RequestBody List<Long> ids) {
try {
if (ids != null && !ids.isEmpty()) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员,检查是否有权限启用
if (!iotUser.isAdmin()) {
for (Long id : ids) {
NoticeTemplate template = noticeTemplateService.getById(id);
if (template != null && !currentUser.equals(template.getCreator())) {
return AjaxResult.error("无权限启用该模板");
}
}
}
noticeTemplateService.enableBatch(ids);
return AjaxResult.success("启用成功");
} else {
@@ -104,6 +157,19 @@ public class NoticeTemplateController extends BaseController {
public AjaxResult disable(@RequestBody List<Long> ids) {
try {
if (ids != null && !ids.isEmpty()) {
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员,检查是否有权限停用
if (!iotUser.isAdmin()) {
for (Long id : ids) {
NoticeTemplate template = noticeTemplateService.getById(id);
if (template != null && !currentUser.equals(template.getCreator())) {
return AjaxResult.error("无权限停用该模板");
}
}
}
noticeTemplateService.disableBatch(ids);
return AjaxResult.success("停用成功");
} else {
@@ -119,8 +185,17 @@ public class NoticeTemplateController extends BaseController {
try {
NoticeTemplate template = noticeTemplateService.getById(id);
if (template == null) {
return null;
return AjaxResult.error("模板不存在");
}
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 如果不是管理员且不是创建者,无权限访问
if (!iotUser.isAdmin() && !currentUser.equals(template.getCreator())) {
return AjaxResult.error("无权限访问该模板");
}
NoticeTemplateDTO dto = new NoticeTemplateDTO();
dto.setId(template.getId());
dto.setName(template.getName());
@@ -135,16 +210,35 @@ public class NoticeTemplateController extends BaseController {
dto.setUpdateTime(template.getUpdateTime());
return AjaxResult.success(dto);
} catch (Exception e) {
return null;
return AjaxResult.error("查询失败: " + e.getMessage());
}
}
@PostMapping("/test")
public AjaxResult test(@RequestBody Map<String, Object> request) {
Long templateId = Long.valueOf(request.get("templateId").toString());
String receivers = (String) request.get("receivers");
Object params = request.get("params");
noticeTemplateService.testTemplate(templateId, receivers, params);
return AjaxResult.success("成功");
try {
Long templateId = Long.valueOf(request.get("templateId").toString());
String receivers = (String) request.get("receivers");
Object params = request.get("params");
String currentUser = SecurityUtils.getUnionId();
IoTUser iotUser = loginIoTUnionUser(SecurityUtils.getUnionId());
// 检查模板是否存在及权限
NoticeTemplate template = noticeTemplateService.getById(templateId);
if (template == null) {
return AjaxResult.error("模板不存在");
}
// 如果不是管理员且不是创建者,无权限测试
if (!iotUser.isAdmin() && !currentUser.equals(template.getCreator())) {
return AjaxResult.error("无权限测试该模板");
}
noticeTemplateService.testTemplate(templateId, receivers, params);
return AjaxResult.success("成功");
} catch (Exception e) {
return AjaxResult.error("测试失败: " + e.getMessage());
}
}
}
@@ -80,6 +80,7 @@ public interface ISysDictDataService {
*/
Map<String, Object> selectDictDataByTypes(String[] dictType);
/**
* 根据label标签,查找白名单
*
@@ -5,56 +5,56 @@ import java.util.List;
/**
* 系统通知Service接口
*
*
* @author universal
* @date 2023-01-01
*/
public interface ISysNoticeService {
/**
* 查询系统通知
*
* @param noticeId 系统通知主键
* @return 系统通知
*/
public SysNotice selectNoticeById(Long noticeId);
/**
* 查询系统通知
*
* @param noticeId 系统通知主键
* @return 系统通知
*/
public SysNotice selectNoticeById(Long noticeId);
/**
* 查询系统通知列表
*
* @param notice 系统通知
* @return 系统通知集合
*/
public List<SysNotice> selectNoticeList(SysNotice notice);
/**
* 查询系统通知列表
*
* @param notice 系统通知
* @return 系统通知集合
*/
public List<SysNotice> selectNoticeList(SysNotice notice);
/**
* 新增系统通知
*
* @param notice 系统通知
* @return 结果
*/
public int insertNotice(SysNotice notice);
/**
* 新增系统通知
*
* @param notice 系统通知
* @return 结果
*/
public int insertNotice(SysNotice notice);
/**
* 修改系统通知
*
* @param notice 系统通知
* @return 结果
*/
public int updateNotice(SysNotice notice);
/**
* 修改系统通知
*
* @param notice 系统通知
* @return 结果
*/
public int updateNotice(SysNotice notice);
/**
* 批量删除系统通知
*
* @param noticeIds 需要删除的系统通知主键集合
* @return 结果
*/
public int deleteNoticeByIds(Long[] noticeIds);
/**
* 批量删除系统通知
*
* @param noticeIds 需要删除的系统通知主键集合
* @return 结果
*/
public int deleteNoticeByIds(Long[] noticeIds);
/**
* 删除系统通知信息
*
* @param noticeId 系统通知主键
* @return 结果
*/
public int deleteNoticeById(Long noticeId);
/**
* 删除系统通知信息
*
* @param noticeId 系统通知主键
* @return 结果
*/
public int deleteNoticeById(Long noticeId);
}
@@ -14,6 +14,7 @@ package cn.universal.admin.system.service;
import cn.universal.admin.common.service.BaseService;
import cn.universal.persistence.entity.IoTDeviceProtocol;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO;
import java.util.List;
@@ -92,4 +93,13 @@ public interface IoTDeviceProtocolService extends BaseService {
* @return 结果
*/
public int deleteDevProtocolById(String id);
/**
* 查询未创建协议的产品列表(归属人为当前用户)
*
* @param unionId 用户unionId
* @param searchKey 搜索关键词(产品名称或productKey)
* @return 产品列表
*/
List<IoTProduct> selectProductsWithoutProtocol(String unionId, String searchKey);
}
@@ -43,7 +43,7 @@ public class IoTDeviceMessageCodecService {
return null;
}
try {
Object result = codecService.preDecode(productKey, JSONUtil.toJsonStr(payload));
Object result = codecService.preDecode(productKey, JSONUtil.toJsonStr(payload), null);
log.info("产品编号={} 原始报文={} , preDecode解码={}", productKey, payload, JSONUtil.toJsonStr(result));
return result;
} catch (Exception e) {
@@ -32,6 +32,7 @@ import cn.universal.core.protocol.magic.ProtocolCodecMagic;
import cn.universal.dm.device.service.impl.IoTCacheRemoveService;
import cn.universal.ossm.service.ISysOssService;
import cn.universal.persistence.entity.IoTDeviceProtocol;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.persistence.entity.bo.IoTDeviceProtocolBO;
import cn.universal.persistence.mapper.IoTDeviceProtocolMapper;
@@ -280,4 +281,16 @@ public class IoTDeviceProtocolServiceImpl extends BaseServiceImpl
public int deleteDevProtocolById(String id) {
return ioTDeviceProtocolMapper.deleteDevProtocolById(id);
}
/**
* 查询未创建协议的产品列表(归属人为当前用户)
*
* @param unionId 用户unionId
* @param searchKey 搜索关键词(产品名称或productKey)
* @return 产品列表
*/
@Override
public List<IoTProduct> selectProductsWithoutProtocol(String unionId, String searchKey) {
return ioTDeviceProtocolMapper.selectProductsWithoutProtocol(unionId, searchKey);
}
}
@@ -270,9 +270,11 @@ public class IoTUserApplicationServiceImpl extends BaseServiceImpl
if (isEnable) {
// HTTP启用:设置enable和support为true
JSONObject httpConfig = pushCfg.getJSONObject("http");
httpConfig.set("enable", true);
httpConfig.set("support", true);
pushCfg.set("http", httpConfig);
if(httpConfig!=null){
httpConfig.set("enable", true);
httpConfig.set("support", true);
pushCfg.set("http", httpConfig);
}
} else {
// HTTP禁用:设置enable为false
if (pushCfg.containsKey("http")) {
@@ -105,9 +105,10 @@ public class SysDictDataServiceImpl implements ISysDictDataService {
"selectDictDataById",
"selectDictLabel",
"selectDictDataByTypes",
"selectDictDataByType",
"selectWhitelist",
"selectDictTypeByType"
})
},allEntries = true)
public void deleteDictDataByIds(Long[] dictCodes) {
for (Long dictCode : dictCodes) {
SysDictData data = selectDictDataById(dictCode);
@@ -130,7 +131,7 @@ public class SysDictDataServiceImpl implements ISysDictDataService {
"selectDictDataByTypes",
"selectWhitelist",
"selectDictTypeByType"
})
},allEntries = true)
public int insertDictData(SysDictData data) {
int row = sysDictDataMapper.insert(data);
return row;
@@ -148,9 +149,11 @@ public class SysDictDataServiceImpl implements ISysDictDataService {
"selectDictDataById",
"selectDictLabel",
"selectDictDataByTypes",
"selectDictDataByType",
"selectWhitelist",
"selectDictTypeByType"
})
},
allEntries = true)
public int updateDictData(SysDictData data) {
int row = sysDictDataMapper.updateByPrimaryKey(data);
return row;
@@ -142,7 +142,7 @@ public class SysDictTypeServiceImpl
"selectDictTypeById",
"selectDictTypeByType",
"selectPageDictTypeList"
})
},allEntries = true)
public void deleteDictTypeByIds(Long[] dictIds) {
for (Long dictId : dictIds) {
SysDictType dictType = selectDictTypeById(dictId);
@@ -177,7 +177,7 @@ public class SysDictTypeServiceImpl
"selectDictTypeById",
"selectDictTypeByType",
"selectPageDictTypeList"
})
},allEntries = true)
public void clearDictCache() {}
/** 重置字典缓存数据 */
@@ -190,7 +190,7 @@ public class SysDictTypeServiceImpl
"selectDictTypeById",
"selectDictTypeByType",
"selectPageDictTypeList"
})
},allEntries = true)
public void resetDictCache() {}
/**
@@ -208,7 +208,7 @@ public class SysDictTypeServiceImpl
"selectDictTypeById",
"selectDictTypeByType",
"selectPageDictTypeList"
})
},allEntries = true)
public int insertDictType(SysDictType dict) {
int row = dictTypeMapper.insert(dict);
return row;
@@ -230,7 +230,7 @@ public class SysDictTypeServiceImpl
"selectDictTypeById",
"selectDictTypeByType",
"selectPageDictTypeList"
})
},allEntries = true)
public int updateDictType(SysDictType dict) {
SysDictType oldDict = dictTypeMapper.selectByPrimaryKey(dict.getDictId());
Example ex = new Example(SysDictData.class);
@@ -133,8 +133,7 @@ public class RulegoChainController extends BaseController {
@Operation(summary = "获取设计器URL")
@GetMapping("/chain/{id}/designer")
public AjaxResult<String> getDesignerUrl(
@PathVariable @Parameter(description = "规则链ID") Long id) {
public AjaxResult<String> getDesignerUrl(@PathVariable @Parameter(description = "规则链ID") Long id) {
String creatorId = loginIoTUnionUser(SecurityUtils.getUnionId()).getUnionId();
RulegoChainVO rulegoChain = rulegoChainService.queryRulegoChainById(id, creatorId);
String designerUrl = rulegoChainService.getDesignerUrl(rulegoChain.getRulegoId());
@@ -87,6 +87,7 @@ public class SysDictDataController extends BaseController {
return toAjax(dictDataService.insertDictData(dict));
}
/** 修改保存字典类型 */
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysDictData dict) {
@@ -84,6 +84,9 @@ public class SystemProtocolController extends BaseController {
details.put("category", protocol.getCategory().name());
details.put("available", true); // 运行时注册表中的都是可用的
details.put("status", "已启用");
// 添加是否支持手动创建产品的信息
details.put("isManualCreatable", protocol.isManualCreatable());
details.put("notCreatableReason", protocol.getNotCreatableReason());
runtimeProtocols.put(protocol.getCode(), details);
});
@@ -0,0 +1,38 @@
/*
*
* Copyright (c) 2025, NexIoT. All Rights Reserved.
*
* @Description: 视频设备导入服务
* @Author: gitee.com/NexIoT
*
*/
package cn.universal.admin.video.service;
import java.util.List;
import java.util.Map;
/**
* 视频设备导入服务
* 从三方平台拉取设备后,勾选使用时落库到IoTDevice
*/
public interface VideoDeviceImportService {
/**
* 批量导入设备(勾选使用流程)
* @param instanceKey 平台实例Key
* @param devices 设备列表(从适配器返回的原始设备数据)
* @param productKey 指定产品Key(可选,为空则自动创建/选择平台产品)
* @return 导入结果 {success: 成功数, failed: 失败数, exists: 已存在数}
*/
Map<String, Integer> importDevices(
String instanceKey, List<Map<String, Object>> devices, String productKey);
/**
* 单个设备导入
* @param instanceKey 平台实例Key
* @param device 设备数据
* @param productKey 指定产品Key
* @return 导入成功的设备ID
*/
String importDevice(String instanceKey, Map<String, Object> device, String productKey);
}
@@ -0,0 +1,174 @@
/*
*
* Copyright (c) 2025, NexIoT. All Rights Reserved.
*
* @Description: 视频平台实例服务接口
* @Author: gitee.com/NexIoT
*
*/
package cn.universal.admin.video.service;
import cn.universal.persistence.entity.VideoPlatformInstance;
import cn.universal.persistence.entity.VideoPlatformDevice;
import java.util.List;
import java.util.Map;
public interface VideoPlatformInstanceService {
/**
* 查询所有平台实例
*/
List<VideoPlatformInstance> listAll();
/**
* 根据平台类型查询
*/
List<VideoPlatformInstance> listByType(String platformType);
/**
* 根据instanceKey查询
*/
VideoPlatformInstance getByInstanceKey(String instanceKey);
/** 根据ID查询 */
VideoPlatformInstance getById(Long id);
/**
* 创建平台实例
*/
VideoPlatformInstance create(VideoPlatformInstance instance);
/**
* 更新平台实例
*/
VideoPlatformInstance update(VideoPlatformInstance instance);
/**
* 删除平台实例
*/
void delete(Long id);
void delete(Long id, boolean force);
/**
* 测试连接(调用适配器真实测试)
*/
Map<String, Object> testConnection(String instanceKey);
/**
* 实时拉取设备目录(调用适配器)
*/
List<Map<String, Object>> listDevices(String instanceKey, Map<String, Object> filters);
/**
* 获取组织树(调用适配器,部分平台支持)
*/
List<Map<String, Object>> listOrganizations(String instanceKey, Map<String, Object> filters);
/**
* 应用订阅(根据实例配置)
*/
void applySubscription(String instanceKey);
/**
* 取消订阅(根据实例配置)
*/
void cancelSubscription(String instanceKey);
/**
* 获取设备通道列表(从中间表)
*/
List<Map<String, Object>> listDeviceChannels(String instanceKey, String deviceId);
/**
* 查询通道列表(从通道表)
* @param instanceKey 平台实例Key
* @param deviceId 设备ID(可选)
* @param filters 筛选条件: keyword(通道名称), status(通道状态), ptzType(PTZ类型)
*/
List<Map<String, Object>> listChannels(String instanceKey, String deviceId, Map<String, Object> filters);
/**
* 获取通道的流地址(预览流)
*/
Map<String, String> getStreamUrl(String instanceKey, String deviceId, String channelId, String streamType);
/**
* 获取通道的回放流地址
*/
Map<String, Object> getPlaybackUrl(String instanceKey, String deviceId, String channelId, long startTime, long endTime);
/**
* PTZ控制
*/
void controlPTZ(String instanceKey, String deviceId, String channelId, String command, int speed);
/**
* 查询录像记录
*/
List<Map<String, Object>> queryRecords(String instanceKey, String deviceId, String channelId, long startTime, long endTime);
// ==================== 国标录像下载 ====================
/**
* 开始国标录像下载
*/
cn.universal.common.domain.R<Map<String, Object>> startGBRecordDownload(String instanceKey, String deviceId, String channelId, Map<String, Object> data);
/**
* 停止国标录像下载
*/
cn.universal.common.domain.R<Void> stopGBRecordDownload(String instanceKey, String deviceId, String channelId, Map<String, Object> data);
/**
* 获取国标录像下载进度
*/
cn.universal.common.domain.R<Map<String, Object>> getGBRecordDownloadProgress(String instanceKey, String deviceId, String channelId, String stream);
// ==================== 云端录像 ====================
/**
* 查询云端录像日期列表
*/
cn.universal.common.domain.R<List<String>> queryCloudRecordDates(String instanceKey, String deviceId, String channelId, Map<String, String> params);
/**
* 查询云端录像列表
*/
cn.universal.common.domain.R<Map<String, Object>> queryCloudRecords(String instanceKey, String deviceId, String channelId, Map<String, String> params);
/**
* 加载云端录像文件
*/
cn.universal.common.domain.R<Map<String, Object>> loadCloudRecord(String instanceKey, String deviceId, String channelId, Map<String, Object> data);
/**
* 云端录像定位
*/
cn.universal.common.domain.R<Void> seekCloudRecord(String instanceKey, String deviceId, String channelId, Map<String, Object> data);
/**
* 设置云端录像倍速
*/
cn.universal.common.domain.R<Void> setCloudRecordSpeed(String instanceKey, String deviceId, String channelId, Map<String, Object> data);
/**
* 获取云端录像下载地址
*/
cn.universal.common.domain.R<Map<String, Object>> getCloudRecordPlayPath(String instanceKey, Integer recordId);
/**
* 获取设备完整信息(跨主表和扩展表查询)
* @param instanceKey 平台实例Key
* @param deviceId 设备ID
* @return 设备完整信息(主表+扩展表)
*/
Map<String, Object> getDeviceDetail(String instanceKey, String deviceId);
/**
* 更新设备基本信息(remark/gps/org/enabled等)
*/
Map<String, Object> updateDevice(String instanceKey, String deviceId, Map<String, Object> updates);
/** 删除设备(级联删除扩展与通道) */
void deleteDevice(String instanceKey, String deviceId);
}
@@ -0,0 +1,40 @@
/*
*
* Copyright (c) 2025, NexIoT. All Rights Reserved.
*
* @Description: 视频平台产品自动创建服务
* @Author: gitee.com/NexIoT
*
*/
package cn.universal.admin.video.service;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.VideoPlatformInstance;
/**
* 视频平台产品创建策略服务
*/
public interface VideoPlatformProductService {
/**
* 为WVP平台自动创建GB/级联两个产品(如果autoCreateProducts=1且尚未创建)
* @param instance 平台实例
*/
void autoCreateWvpProducts(VideoPlatformInstance instance);
/**
* 懒创建平台产品(HIK/ICC/WVP通用)
* @param instance 平台实例
* @param deviceNode 设备节点类型:VIDEO_GATEWAY/VIDEO_DEVICE等
* @return 平台产品(用于后续设备落库时填充gwProductKey)
*/
IoTProduct ensurePlatformProduct(VideoPlatformInstance instance, String deviceNode);
/**
* 根据instanceKey获取或创建平台产品
* @param instanceKey 平台实例Key
* @param deviceNode 设备节点类型
* @return 平台产品
*/
IoTProduct getOrCreatePlatformProduct(String instanceKey, String deviceNode);
}
@@ -0,0 +1,240 @@
/*
*
* Copyright (c) 2025, NexIoT. All Rights Reserved.
*
* @Description: 视频设备导入服务实现
* @Author: gitee.com/NexIoT
*
*/
package cn.universal.admin.video.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.universal.admin.video.service.VideoDeviceImportService;
import cn.universal.admin.video.service.VideoPlatformProductService;
import cn.universal.common.constant.IoTConstant;
import cn.universal.common.exception.BaseException;
import cn.universal.common.utils.StringUtils;
import cn.universal.persistence.entity.IoTDevice;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.VideoPlatformInstance;
import cn.universal.persistence.mapper.IoTDeviceMapper;
import cn.universal.persistence.mapper.IoTProductMapper;
import cn.universal.persistence.mapper.VideoPlatformInstanceMapper;
import cn.universal.security.utils.SecurityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
/**
* 视频设备导入服务
* 负责将三方平台设备落库到IoTDevice
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoDeviceImportServiceImpl implements VideoDeviceImportService {
private final VideoPlatformInstanceMapper instanceMapper;
private final VideoPlatformProductService productService;
private final IoTDeviceMapper deviceMapper;
private final IoTProductMapper productMapper;
private final ObjectMapper objectMapper;
@Override
@Transactional
public Map<String, Integer> importDevices(
String instanceKey, List<Map<String, Object>> devices, String productKey) {
Map<String, Integer> result = new HashMap<>();
int success = 0, failed = 0, exists = 0;
for (Map<String, Object> device : devices) {
try {
String deviceId = importDevice(instanceKey, device, productKey);
if (deviceId != null) {
if (deviceId.startsWith("EXISTS:")) {
exists++;
} else {
success++;
}
} else {
failed++;
}
} catch (Exception e) {
log.error("导入设备失败:{}", e.getMessage(), e);
failed++;
}
}
result.put("success", success);
result.put("failed", failed);
result.put("exists", exists);
return result;
}
@Override
@Transactional
public String importDevice(String instanceKey, Map<String, Object> device, String productKey) {
// 1. 获取平台实例
Example example = new Example(VideoPlatformInstance.class);
example.createCriteria().andEqualTo("instanceKey", instanceKey);
VideoPlatformInstance instance =
instanceMapper.selectByExample(example).stream().findFirst().orElse(null);
if (instance == null) {
throw new BaseException("平台实例不存在:" + instanceKey);
}
// 2. 获取或创建平台产品
String deviceId = (String) device.get("deviceId");
if (StringUtils.isEmpty(deviceId)) {
throw new BaseException("设备DeviceId不能为空");
}
// 判断设备节点类型(优先从设备数据获取,否则默认VIDEO_DEVICE)
String deviceNode = (String) device.getOrDefault("deviceNode", "VIDEO_DEVICE");
IoTProduct product;
if (StringUtils.isEmpty(productKey)) {
// 自动创建/选择平台产品
product = productService.ensurePlatformProduct(instance, deviceNode);
} else {
// 使用指定产品
product = productMapper.getProductByProductKey(productKey);
if (product == null) {
throw new BaseException("指定产品不存在:" + productKey);
}
}
String deviceName = (String) device.getOrDefault("deviceName", "");
String deviceIp = (String) device.getOrDefault("deviceIp", "");
// 3. 检查设备是否已存在(幂等性校验:productKey + deviceId)
Example deviceExample = new Example(IoTDevice.class);
deviceExample
.createCriteria()
.andEqualTo("productKey", product.getProductKey())
.andEqualTo("deviceId", deviceId);
IoTDevice existingDevice = deviceMapper.selectByExample(deviceExample).stream().findFirst().orElse(null);
if (existingDevice != null) {
log.info("设备已存在,跳过导入:{}/{}", product.getProductKey(), deviceId);
return "EXISTS:" + existingDevice.getIotId();
}
// 4. 创建IoTDevice
IoTDevice iotDevice = new IoTDevice();
iotDevice.setIotId(product.getProductKey()+deviceId);
iotDevice.setProductKey(product.getProductKey());
iotDevice.setDeviceId(deviceId);
iotDevice.setDeviceName(StrUtil.isBlank(deviceName)?deviceIp:deviceName);
iotDevice.setProductName(product.getName());
iotDevice.setState(parseDeviceStatus(device));
iotDevice.setCreatorId(SecurityUtils.getUnionId());
iotDevice.setCreateTime(System.currentTimeMillis() / 1000);
// 填充网关产品Key(平台实例作为网关)
iotDevice.setGwProductKey(product.getProductKey());
// 填充配置字段(包含通道列表等)
iotDevice.setConfiguration(buildConfiguration(device));
// 填充派生元数据(媒体能力、通道信息等)
iotDevice.setDeriveMetadata(buildDeriveMetadata(device));
// 填充事件元数据到ext1(遵循规范)
iotDevice.setExt1(buildEventMetadata(device));
deviceMapper.insertSelective(iotDevice);
log.info(
"设备导入成功:{}/{} -> {}", product.getProductKey(), deviceId, iotDevice.getIotId());
return iotDevice.getIotId();
}
/**
* 解析设备状态
*/
private Boolean parseDeviceStatus(Map<String, Object> device) {
Object status = device.get("status");
if (status == null) return false;
if (status instanceof Boolean) return (Boolean) status;
String statusStr = status.toString().toLowerCase();
return "online".equals(statusStr) || "1".equals(statusStr) || "true".equals(statusStr);
}
/**
* 构建配置字段(保存轻量配置,如通道列表)
*/
private String buildConfiguration(Map<String, Object> device) {
try {
Map<String, Object> config = new HashMap<>();
// 保存厂商、型号等基础信息
if (device.containsKey("manufacturer")) {
config.put("manufacturer", device.get("manufacturer"));
}
if (device.containsKey("model")) {
config.put("model", device.get("model"));
}
// 保存通道列表(如果有)
if (device.containsKey("channels")) {
config.put("channelList", device.get("channels"));
}
if (device.containsKey("channelCount")) {
config.put("channelCount", device.get("channelCount"));
}
return objectMapper.writeValueAsString(config);
} catch (Exception e) {
log.warn("构建配置失败:{}", e.getMessage());
return "{}";
}
}
/**
* 构建派生元数据(媒体能力、编解码等)
*/
private String buildDeriveMetadata(Map<String, Object> device) {
try {
Map<String, Object> metadata = new HashMap<>();
// 媒体能力(预览URL、回放能力等)
if (device.containsKey("capabilities")) {
metadata.put("capabilities", device.get("capabilities"));
}
// PTZ能力
if (device.containsKey("ptzSupport")) {
metadata.put("ptzSupport", device.get("ptzSupport"));
}
// 编解码信息
if (device.containsKey("videoCodec")) {
metadata.put("videoCodec", device.get("videoCodec"));
}
if (device.containsKey("audioCodec")) {
metadata.put("audioCodec", device.get("audioCodec"));
}
return objectMapper.writeValueAsString(metadata);
} catch (Exception e) {
log.warn("构建派生元数据失败:{}", e.getMessage());
return "{}";
}
}
/**
* 构建事件元数据(仅用ext1,遵循规范)
*/
private String buildEventMetadata(Map<String, Object> device) {
try {
Map<String, Object> eventMeta = new HashMap<>();
// 保存原始三方设备ID
eventMeta.put("thirdPartyDeviceId", device.get("extDeviceId"));
// 设备类型(IPC/NVR等)
if (device.containsKey("deviceType")) {
eventMeta.put("deviceType", device.get("deviceType"));
}
return objectMapper.writeValueAsString(eventMeta);
} catch (Exception e) {
log.warn("构建事件元数据失败:{}", e.getMessage());
return "{}";
}
}
}
@@ -0,0 +1,150 @@
/*
*
* Copyright (c) 2025, NexIoT. All Rights Reserved.
*
* @Description: 视频平台产品自动创建服务实现
* @Author: gitee.com/NexIoT
*
*/
package cn.universal.admin.video.service.impl;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import cn.universal.admin.video.service.VideoPlatformProductService;
import cn.universal.common.constant.IoTConstant;
import cn.universal.common.exception.BaseException;
import cn.universal.persistence.entity.IoTProduct;
import cn.universal.persistence.entity.VideoPlatformInstance;
import cn.universal.persistence.mapper.IoTProductMapper;
import cn.universal.persistence.mapper.VideoPlatformInstanceMapper;
import cn.universal.security.utils.SecurityUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
/** 视频平台产品创建策略 WVP:自动创建GB/级联两个产品(autoCreateProducts=1) HIK/ICC:懒创建,首次勾选使用设备时创建平台产品 */
@Slf4j
@Service
@RequiredArgsConstructor
public class VideoPlatformProductServiceImpl implements VideoPlatformProductService {
private String metadata =
"""
{"tags":[],"events":[{"id":"online","name":"设备上线","valueType":{"type":"string"}},{"id":"offline","name":"设备下线","valueType":{"type":"string"}}],"functions":[{"id":"cameraLiveStream","name":"实时预览","config":false,"inputs":[{"id":"deviceId","name":"设备ID","valueType":{"type":"string"}},{"id":"channelId","name":"通道ID","valueType":{"type":"string"}},{"id":"streamType","name":"码流类型","valueType":{"type":"enum","elements":[{"text":"主码流","value":"main"},{"text":"子码流","value":"sub"}]}}],"output":{}},{"id":"cameraTurn","name":"云台控制","config":false,"inputs":[{"id":"deviceId","name":"设备ID","valueType":{"type":"string"}},{"id":"channelId","name":"通道ID","valueType":{"type":"string"}},{"id":"command","name":"控制命令","valueType":{"type":"string"}},{"id":"speed","name":"速度","valueType":{"type":"int"}}],"output":{}},{"id":"cameraPlayback","name":"录像回放","config":false,"inputs":[{"id":"deviceId","name":"设备ID","valueType":{"type":"string"}},{"id":"channelId","name":"通道ID","valueType":{"type":"string"}},{"id":"startTime","name":"开始时间","valueType":{"type":"long"}},{"id":"endTime","name":"结束时间","valueType":{"type":"long"}}],"output":{}},{"id":"cameraSnapshot","name":"抓拍","config":false,"inputs":[{"id":"deviceId","name":"设备ID","valueType":{"type":"string"}},{"id":"channelId","name":"通道ID","valueType":{"type":"string"}}],"output":{}},{"id":"queryRecords","name":"查询录像","config":false,"inputs":[{"id":"deviceId","name":"设备ID","valueType":{"type":"string"}},{"id":"channelId","name":"通道ID","valueType":{"type":"string"}},{"id":"startTime","name":"开始时间","valueType":{"type":"string"}},{"id":"endTime","name":"结束时间","valueType":{"type":"string"}}],"output":{}},{"id":"queryCloudRecords","name":"查询云端录像","config":false,"inputs":[{"id":"app","name":"应用名","valueType":{"type":"string"}},{"id":"stream","name":"流ID","valueType":{"type":"string"}},{"id":"page","name":"页码","valueType":{"type":"int"}},{"id":"count","name":"每页数量","valueType":{"type":"int"}}],"output":{}},{"id":"loadCloudRecord","name":"加载云端录像","config":false,"inputs":[{"id":"app","name":"应用名","valueType":{"type":"string"}},{"id":"stream","name":"流ID","valueType":{"type":"string"}},{"id":"cloudRecordId","name":"录像ID","valueType":{"type":"int"}}],"output":{}}],"properties":[{"id":"deviceStatus","mode":"r","name":"设备状态","config":false,"source":"device","valueType":{"type":"enum","elements":[{"text":"在线","value":"online"},{"text":"离线","value":"offline"}]}},{"id":"manufacturer","mode":"r","name":"厂商","config":false,"source":"device","valueType":{"type":"string"}},{"id":"model","mode":"r","name":"型号","config":false,"source":"device","valueType":{"type":"string"}}]}""";
private final IoTProductMapper productMapper;
private final VideoPlatformInstanceMapper instanceMapper;
private final ObjectMapper objectMapper;
@Override
@Transactional
public void autoCreateWvpProducts(VideoPlatformInstance instance) {
if (!"wvp".equals(instance.getPlatformType())) {
log.warn("仅WVP平台支持自动创建产品,当前平台类型:{}", instance.getPlatformType());
return;
}
if (instance.getAutoCreateProducts() != 1) {
log.info("WVP平台实例未开启自动创建产品,跳过");
return;
}
// 检查是否已创建GB产品
String gbProductKey = instance.getInstanceKey() + "_GB";
IoTProduct gbProduct = productMapper.getProductByProductKey(gbProductKey);
if (gbProduct == null) {
createPlatformProduct(instance, "VIDEO_DEVICE", gbProductKey, instance.getName() + "-GB设备");
log.info("WVP平台自动创建GB产品:{}", gbProductKey);
}
// 检查是否已创建级联产品
// String cascadeProductKey = instance.getInstanceKey() + "_CASCADE";
// IoTProduct cascadeProduct = productMapper.getProductByProductKey(cascadeProductKey);
// if (cascadeProduct == null) {
// createPlatformProduct(
// instance, "VIDEO_GATEWAY", cascadeProductKey, instance.getName() + "-级联平台");
// log.info("WVP平台自动创建级联产品:{}", cascadeProductKey);
// }
}
@Override
@Transactional
public IoTProduct ensurePlatformProduct(VideoPlatformInstance instance, String deviceNode) {
String productKey = buildPlatformProductKey(instance, deviceNode);
IoTProduct product = productMapper.getProductByProductKey(productKey);
if (product == null) {
product = createPlatformProduct(instance, deviceNode, productKey, instance.getName());
log.info("懒创建平台产品:{} [{}]", productKey, deviceNode);
}
return product;
}
@Override
public IoTProduct getOrCreatePlatformProduct(String instanceKey, String deviceNode) {
Example example = new Example(VideoPlatformInstance.class);
example.createCriteria().andEqualTo("instanceKey", instanceKey);
VideoPlatformInstance instance =
instanceMapper.selectByExample(example).stream().findFirst().orElse(null);
if (instance == null) {
throw new BaseException("平台实例不存在:" + instanceKey);
}
return ensurePlatformProduct(instance, deviceNode);
}
/** 创建产品*/
private IoTProduct createPlatformProduct(
VideoPlatformInstance instance, String deviceNode, String productKey, String productName) {
try {
IoTProduct product = new IoTProduct();
product.setProductKey(productKey);
product.setProductId(productKey); // 与productKey一致
product.setName(productName);
product.setThirdPlatform(instance.getPlatformType());
product.setDeviceNode(deviceNode);
product.setState((IoTConstant.NORMAL.byteValue())); // 启用
product.setCreatorId(SecurityUtils.getUnionId());
product.setCreateTime(new Date());
product.setUpdateTime(new Date());
product.setStorePolicy("mysql");
product.setTransportProtocol("以太网");
product.setIsDeleted(0);
// 将平台实例配置写入thirdConfiguration
Map<String, Object> thirdConfig = new HashMap<>();
thirdConfig.put("instanceKey", instance.getInstanceKey());
thirdConfig.put("endpoint", instance.getEndpoint());
thirdConfig.put("auth", instance.getAuth());
thirdConfig.put("version", instance.getVersion());
thirdConfig.put("options", instance.getOptions());
thirdConfig.put("customField", new ArrayList<String>());
product.setThirdConfiguration(objectMapper.writeValueAsString(thirdConfig));
// 默认物模型(仅包含online/offline事件)
product.setMetadata(metadata);
productMapper.insertSelective(product);
return product;
} catch (Exception e) {
log.error("创建平台产品失败:{}", e.getMessage(), e);
throw new BaseException("创建平台产品失败:" + e.getMessage());
}
}
/** 根据平台类型与设备节点构建产品Key */
private String buildPlatformProductKey(VideoPlatformInstance instance, String deviceNode) {
// // WVP有特殊规则(已在autoCreateWvpProducts中处理),这里是通用规则
// if ("wvp".equals(instance.getPlatformType())) {
// if ("VIDEO_DEVICE".equals(deviceNode)) {
// return instance.getInstanceKey() + "_GB";
// } else if ("VIDEO_GATEWAY".equals(deviceNode)) {
// return instance.getInstanceKey() + "_CASCADE";
// }
// }
// HIK/ICC/其他平台:instanceKey_deviceNode
return instance.getInstanceKey();
}
}
@@ -0,0 +1,484 @@
package cn.universal.admin.video.web;
import cn.universal.admin.video.service.VideoDeviceImportService;
import cn.universal.admin.video.service.VideoPlatformInstanceService;
import cn.universal.common.annotation.Log;
import cn.universal.common.domain.R;
import cn.universal.common.enums.BusinessType;
import cn.universal.common.exception.BaseException;
import cn.universal.persistence.entity.VideoPlatformInstance;
import cn.universal.persistence.entity.VideoPlatformDevice;
import cn.universal.persistence.entity.IoTUser;
import cn.universal.security.BaseController;
import cn.universal.security.utils.SecurityUtils;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 视频平台实例与设备接口(先行联调,逐步接入真实适配器) */
@RestController
@RequestMapping("/api/video/platforms")
@RequiredArgsConstructor
public class VideoPlatformController extends BaseController {
private final VideoPlatformInstanceService instanceService;
private final VideoDeviceImportService deviceImportService;
// 同步限频缓存:记录每个实例最后一次强制同步时间戳
private static final java.util.concurrent.ConcurrentHashMap<String, Long> SYNC_RATE_LIMIT =
new java.util.concurrent.ConcurrentHashMap<>();
/** 平台实例列表 */
@GetMapping("")
public R<List<VideoPlatformInstance>> listPlatforms() {
IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId());
List<VideoPlatformInstance> all = instanceService.listAll();
if (user != null && !user.isAdmin()) {
String uid = user.getUnionId();
all = all.stream().filter(i -> uid != null && uid.equals(i.getCreatorId()))
.collect(java.util.stream.Collectors.toList());
}
return R.ok(all);
}
/** 根据instanceKey查询实例 */
@GetMapping("/{instanceKey}")
public R<VideoPlatformInstance> getInstance(@PathVariable("instanceKey") String instanceKey) {
assertInstancePerm(instanceKey);
return R.ok(instanceService.getByInstanceKey(instanceKey));
}
/** 创建/更新平台实例(若id为空则创建,否则更新) */
@PostMapping("")
@Log(title = "创建/更新视频平台实例", businessType = BusinessType.INSERT)
public R<VideoPlatformInstance> savePlatform(@RequestBody VideoPlatformInstance body) {
IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId());
VideoPlatformInstance saved;
if (body.getId() == null) {
if (user != null) {
body.setCreatorId(user.getUnionId());
}
saved = instanceService.create(body);
} else {
VideoPlatformInstance existing = instanceService.getByInstanceKey(body.getInstanceKey());
if (existing == null) throw new BaseException("平台实例不存在");
if (user != null && !user.isAdmin()) {
if (existing.getCreatorId() == null || !existing.getCreatorId().equals(user.getUnionId())) {
throw new BaseException("无权更新该平台实例");
}
}
saved = instanceService.update(body);
}
// 尝试应用订阅配置(若启用)
try {
instanceService.applySubscription(saved.getInstanceKey());
} catch (Exception ignore) {
}
return R.ok(saved);
}
/** 删除平台实例 */
@DeleteMapping("/{id}")
@Log(title = "删除视频平台实例", businessType = BusinessType.DELETE)
public R<Void> deletePlatform(@PathVariable("id") Long id, @RequestParam(value = "force", required = false, defaultValue = "false") boolean force) {
IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId());
VideoPlatformInstance inst = instanceService.getById(id);
if (inst == null) throw new BaseException("平台实例不存在");
if (user != null && !user.isAdmin()) {
if (inst.getCreatorId() == null || !inst.getCreatorId().equals(user.getUnionId())) {
throw new BaseException("无权删除该平台实例");
}
}
instanceService.delete(id, force);
return R.ok();
}
/** 测试连接(占位,后续接入适配器) */
@PostMapping("/test")
public R<Map<String, Object>> testConnection(@RequestBody Map<String, Object> body) {
String instanceKey = (String) body.get("instanceKey");
assertInstancePerm(instanceKey);
return R.ok(instanceService.testConnection(instanceKey));
}
/** 获取组织树(部分平台支持) */
@GetMapping("/{instanceKey}/orgs")
public R<List<Map<String, Object>>> listOrganizations(
@PathVariable("instanceKey") String instanceKey, @RequestParam Map<String, String> params) {
Map<String, Object> filters = new java.util.HashMap<>(params);
assertInstancePerm(instanceKey);
return R.ok(instanceService.listOrganizations(instanceKey, filters));
}
/** 实时拉取设备目录(占位),用于前端页面测试 */
@GetMapping("/{instanceKey}/devices")
public R<List<Map<String, Object>>> listDevices(
@PathVariable("instanceKey") String instanceKey, @RequestParam Map<String, String> params) {
Map<String, Object> filters = new java.util.HashMap<>(params);
assertInstancePerm(instanceKey);
return R.ok(instanceService.listDevices(instanceKey, filters));
}
@PostMapping("/{instanceKey}/subscribe")
public R<Void> subscribe(@PathVariable("instanceKey") String instanceKey) {
assertInstancePerm(instanceKey);
instanceService.applySubscription(instanceKey);
return R.ok();
}
@PostMapping("/{instanceKey}/unsubscribe")
public R<Void> unsubscribe(@PathVariable("instanceKey") String instanceKey) {
assertInstancePerm(instanceKey);
instanceService.cancelSubscription(instanceKey);
return R.ok();
}
/** 强制同步组织与设备缓存(限频:每实例5分钟一次) */
@PostMapping("/{instanceKey}/sync")
@Log(title = "强制同步视频平台组织与设备缓存", businessType = BusinessType.OTHER)
public R<?> forceSync(@PathVariable("instanceKey") String instanceKey) {
assertInstancePerm(instanceKey);
long now = System.currentTimeMillis();
Long last = SYNC_RATE_LIMIT.get(instanceKey);
if (last != null && (now - last) < 5 * 60 * 1000) {
long remaining = 5 * 60 * 1000 - (now - last);
return R.error("操作过于频繁,请在" + (remaining / 1000) + "秒后重试");
}
SYNC_RATE_LIMIT.put(instanceKey, now);
// 分页拉取所有设备(循环直到没有下一页)
int totalDeviceCount = 0;
int currentPage = 1;
int pageSize = 100; // 每页拉取100条
while (true) {
Map<String, Object> forceParam = new java.util.HashMap<>();
forceParam.put("forceSync", "true");
forceParam.put("page", currentPage);
forceParam.put("count", pageSize);
// 触发设备同步(具体落库由Service实现到中间表)
List<Map<String, Object>> devices = instanceService.listDevices(instanceKey, forceParam);
if (devices == null || devices.isEmpty()) {
break; // 没有更多数据,退出循环
}
totalDeviceCount += devices.size();
// 如果返回数量小于pageSize,说明已经是最后一页
if (devices.size() < pageSize) {
break;
}
currentPage++;
}
// 同步组织信息(如果有)
Map<String, Object> orgParam = new java.util.HashMap<>();
orgParam.put("forceSync", "true");
List<Map<String, Object>> orgs = instanceService.listOrganizations(instanceKey, orgParam);
Map<String, Object> result = new java.util.HashMap<>();
result.put("orgCount", orgs == null ? 0 : orgs.size());
result.put("deviceCount", totalDeviceCount);
result.put("instanceKey", instanceKey);
result.put("pages", currentPage);
return R.ok(result);
}
/**
* 获取设备通道列表(从中间表返回)
*
* @deprecated 请使用 listChannels 从通道表查询
*/
@Deprecated
@GetMapping("/{instanceKey}/devices/{deviceId}/channels")
public R<List<Map<String, Object>>> listDeviceChannels(
@PathVariable("instanceKey") String instanceKey, @PathVariable("deviceId") String deviceId) {
assertInstancePerm(instanceKey);
return R.ok(instanceService.listDeviceChannels(instanceKey, deviceId));
}
/** 查询通道列表(从通道表查询) 支持筛选: keyword(通道名称/ID), status(通道状态), ptzType(PTZ类型) */
@GetMapping("/{instanceKey}/channels")
public R<List<Map<String, Object>>> listChannels(
@PathVariable("instanceKey") String instanceKey,
@RequestParam(required = false) String deviceId,
@RequestParam Map<String, String> params) {
Map<String, Object> filters = new java.util.HashMap<>(params);
assertInstancePerm(instanceKey);
return R.ok(instanceService.listChannels(instanceKey, deviceId, filters));
}
/** 获取通道流地址(预览/回放) */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/stream")
public R<Map<String, String>> getStreamUrl(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam Map<String, String> params) {
String streamType = params.getOrDefault("streamType", "main");
assertInstancePerm(instanceKey);
return R.ok(instanceService.getStreamUrl(instanceKey, deviceId, channelId, streamType));
}
/** PTZ控制 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/ptz")
@Log(title = "视频设备PTZ控制", businessType = BusinessType.OTHER)
public R<Void> controlPTZ(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> body) {
String command = String.valueOf(body.getOrDefault("command", "stop"));
int speed = 50;
try {
Object s = body.get("speed");
if (s != null) speed = Integer.parseInt(String.valueOf(s));
} catch (Exception ignore) {
}
assertInstancePerm(instanceKey);
instanceService.controlPTZ(instanceKey, deviceId, channelId, command, speed);
return R.ok();
}
/** 批量导入设备(勾选使用流程) */
@PostMapping("/{instanceKey}/devices/import")
@Log(title = "批量导入视频设备", businessType = BusinessType.IMPORT)
public R<Map<String, Integer>> importDevices(
@PathVariable("instanceKey") String instanceKey, @RequestBody Map<String, Object> body) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> devices = (List<Map<String, Object>>) body.get("devices");
String productKey = (String) body.get("productKey");
assertInstancePerm(instanceKey);
Map<String, Integer> result =
deviceImportService.importDevices(instanceKey, devices, productKey);
return R.ok(result);
}
/** 获取预览流地址 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/preview")
public R<Map<String, String>> getPreviewUrl(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam(defaultValue = "main") String streamType) {
assertInstancePerm(instanceKey);
return R.ok(instanceService.getStreamUrl(instanceKey, deviceId, channelId, streamType));
}
/** 获取回放流地址 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/playback")
public R<Map<String, Object>> getPlaybackUrl(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam Map<String, String> params) {
assertInstancePerm(instanceKey);
// 从参数中获取开始时间和结束时间
String startTimeStr = params.get("startTime");
String endTimeStr = params.get("endTime");
if (startTimeStr == null || endTimeStr == null) {
return R.error("缺少参数: startTime 或 endTime", null);
}
// 解析时间字符串为时间戳(毫秒)
long startTime;
long endTime;
try {
// 支持格式:yyyy-MM-dd HH:mm:ss 或时间戳(毫秒)
if (startTimeStr.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
startTime = sdf.parse(startTimeStr).getTime();
} else {
startTime = Long.parseLong(startTimeStr);
// 如果是10位秒级时间戳,转换为毫秒
if (startTimeStr.length() == 10) {
startTime = startTime * 1000;
}
}
if (endTimeStr.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
endTime = sdf.parse(endTimeStr).getTime();
} else {
endTime = Long.parseLong(endTimeStr);
// 如果是10位秒级时间戳,转换为毫秒
if (endTimeStr.length() == 10) {
endTime = endTime * 1000;
}
}
} catch (Exception e) {
return R.error("时间格式错误: " + e.getMessage(), null);
}
return R.ok(instanceService.getPlaybackUrl(instanceKey, deviceId, channelId, startTime, endTime));
}
/** 查询录像记录 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/records")
public R<List<Map<String, Object>>> queryRecords(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam Map<String, String> params) {
long startTime = Long.parseLong(params.getOrDefault("startTime", "0"));
long endTime =
Long.parseLong(params.getOrDefault("endTime", String.valueOf(System.currentTimeMillis())));
assertInstancePerm(instanceKey);
return R.ok(instanceService.queryRecords(instanceKey, deviceId, channelId, startTime, endTime));
}
// ==================== 国标录像下载 ====================
/** 开始国标录像下载 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/gb-records/download/start")
public R<Map<String, Object>> startGBRecordDownload(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> data) {
assertInstancePerm(instanceKey);
return instanceService.startGBRecordDownload(instanceKey, deviceId, channelId, data);
}
/** 停止国标录像下载 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/gb-records/download/stop")
public R<Void> stopGBRecordDownload(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> data) {
assertInstancePerm(instanceKey);
return instanceService.stopGBRecordDownload(instanceKey, deviceId, channelId, data);
}
/** 获取国标录像下载进度 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/gb-records/download/progress")
public R<Map<String, Object>> getGBRecordDownloadProgress(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam String stream) {
assertInstancePerm(instanceKey);
return instanceService.getGBRecordDownloadProgress(instanceKey, deviceId, channelId, stream);
}
// ==================== 云端录像 ====================
/** 查询云端录像日期列表 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/cloud-records/dates")
public R<List<String>> queryCloudRecordDates(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam Map<String, String> params) {
assertInstancePerm(instanceKey);
return instanceService.queryCloudRecordDates(instanceKey, deviceId, channelId, params);
}
/** 查询云端录像列表 */
@GetMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/cloud-records")
public R<Map<String, Object>> queryCloudRecords(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestParam Map<String, String> params) {
assertInstancePerm(instanceKey);
return instanceService.queryCloudRecords(instanceKey, deviceId, channelId, params);
}
/** 加载云端录像文件 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/cloud-records/load")
public R<Map<String, Object>> loadCloudRecord(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> data) {
assertInstancePerm(instanceKey);
return instanceService.loadCloudRecord(instanceKey, deviceId, channelId, data);
}
/** 云端录像定位 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/cloud-records/seek")
public R<Void> seekCloudRecord(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> data) {
assertInstancePerm(instanceKey);
return instanceService.seekCloudRecord(instanceKey, deviceId, channelId, data);
}
/** 设置云端录像倍速 */
@PostMapping("/{instanceKey}/devices/{deviceId}/channels/{channelId}/cloud-records/speed")
public R<Void> setCloudRecordSpeed(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@PathVariable("channelId") String channelId,
@RequestBody Map<String, Object> data) {
assertInstancePerm(instanceKey);
return instanceService.setCloudRecordSpeed(instanceKey, deviceId, channelId, data);
}
/** 获取云端录像下载地址 */
@GetMapping("/{instanceKey}/cloud-records/play/path")
public R<Map<String, Object>> getCloudRecordPlayPath(
@PathVariable("instanceKey") String instanceKey, @RequestParam Integer recordId) {
assertInstancePerm(instanceKey);
return instanceService.getCloudRecordPlayPath(instanceKey, recordId);
}
/** 获取设备完整信息(跨主表和扩展表查询) */
@GetMapping("/{instanceKey}/devices/{deviceId}/detail")
public R<Map<String, Object>> getDeviceDetail(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId) {
assertInstancePerm(instanceKey);
return R.ok(instanceService.getDeviceDetail(instanceKey, deviceId));
}
@PutMapping("/{instanceKey}/devices/{deviceId}")
@Log(title = "更新视频设备信息", businessType = BusinessType.UPDATE)
public R<Map<String, Object>> updateDevice(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId,
@RequestBody Map<String, Object> updates) {
assertInstancePerm(instanceKey);
Map<String, Object> detail = instanceService.updateDevice(instanceKey, deviceId, updates);
return R.ok(detail);
}
@DeleteMapping("/{instanceKey}/devices/{deviceId}")
@Log(title = "删除视频设备", businessType = BusinessType.DELETE)
public R<Void> deleteDevice(
@PathVariable("instanceKey") String instanceKey,
@PathVariable("deviceId") String deviceId) {
assertInstancePerm(instanceKey);
instanceService.deleteDevice(instanceKey, deviceId);
return R.ok();
}
private void assertInstancePerm(String instanceKey) {
IoTUser user = loginIoTUnionUser(SecurityUtils.getUnionId());
if (user == null || user.isAdmin()) return;
VideoPlatformInstance inst = instanceService.getByInstanceKey(instanceKey);
if (inst == null) throw new BaseException("平台实例不存在");
String uid = user.getUnionId();
if (uid == null || !uid.equals(inst.getCreatorId())) {
throw new BaseException("无权操作该平台实例");
}
}
}
@@ -1,161 +1,161 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.universal.admin.generator.mapper.GenTableColumnMapper">
<resultMap type="cn.universal.admin.generator.domain.GenTableColumn" id="GenTableColumnResult">
<id property="columnId" column="column_id"/>
<result property="tableId" column="table_id"/>
<result property="columnName" column="column_name"/>
<result property="columnComment" column="column_comment"/>
<result property="columnType" column="column_type"/>
<result property="javaType" column="java_type"/>
<result property="javaField" column="java_field"/>
<result property="isPk" column="is_pk"/>
<result property="isIncrement" column="is_increment"/>
<result property="isRequired" column="is_required"/>
<result property="isInsert" column="is_insert"/>
<result property="isEdit" column="is_edit"/>
<result property="isList" column="is_list"/>
<result property="isQuery" column="is_query"/>
<result property="queryType" column="query_type"/>
<result property="htmlType" column="html_type"/>
<result property="dictType" column="dict_type"/>
<result property="sort" column="sort"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<resultMap type="cn.universal.admin.generator.domain.GenTableColumn" id="GenTableColumnResult">
<id property="columnId" column="column_id"/>
<result property="tableId" column="table_id"/>
<result property="columnName" column="column_name"/>
<result property="columnComment" column="column_comment"/>
<result property="columnType" column="column_type"/>
<result property="javaType" column="java_type"/>
<result property="javaField" column="java_field"/>
<result property="isPk" column="is_pk"/>
<result property="isIncrement" column="is_increment"/>
<result property="isRequired" column="is_required"/>
<result property="isInsert" column="is_insert"/>
<result property="isEdit" column="is_edit"/>
<result property="isList" column="is_list"/>
<result property="isQuery" column="is_query"/>
<result property="queryType" column="query_type"/>
<result property="htmlType" column="html_type"/>
<result property="dictType" column="dict_type"/>
<result property="sort" column="sort"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectGenTableColumnVo">
select column_id,
table_id,
column_name,
column_comment,
column_type,
java_type,
java_field,
is_pk,
is_increment,
is_required,
is_insert,
is_edit,
is_list,
is_query,
query_type,
html_type,
dict_type,
sort,
create_by,
create_time,
update_by,
update_time
from gen_table_column
</sql>
<sql id="selectGenTableColumnVo">
select column_id,
table_id,
column_name,
column_comment,
column_type,
java_type,
java_field,
is_pk,
is_increment,
is_required,
is_insert,
is_edit,
is_list,
is_query,
query_type,
html_type,
dict_type,
sort,
create_by,
create_time,
update_by,
update_time
from gen_table_column
</sql>
<select id="selectGenTableColumnListByTableId" parameterType="Long"
resultMap="GenTableColumnResult">
<include refid="selectGenTableColumnVo"/>
where table_id = #{tableId}
order by sort
</select>
<select id="selectGenTableColumnListByTableId" parameterType="Long"
resultMap="GenTableColumnResult">
<include refid="selectGenTableColumnVo"/>
where table_id = #{tableId}
order by sort
</select>
<select id="selectDbTableColumnsByName" parameterType="String" resultMap="GenTableColumnResult">
select column_name,
(case when (is_nullable = 'no' <![CDATA[ && ]]> column_key != 'PRI') then '1' else null end) as is_required,
(case when column_key = 'PRI' then '1' else '0' end) as is_pk,
ordinal_position as sort,
column_comment,
(case when extra = 'auto_increment' then '1' else '0' end) as is_increment,
column_type
from information_schema.columns
where table_schema = (select database())
and table_name = (#{tableName})
order by ordinal_position
</select>
<select id="selectDbTableColumnsByName" parameterType="String" resultMap="GenTableColumnResult">
select column_name,
(case when (is_nullable = 'no' <![CDATA[ && ]]> column_key != 'PRI') then '1' else null end) as is_required,
(case when column_key = 'PRI' then '1' else '0' end) as is_pk,
ordinal_position as sort,
column_comment,
(case when extra = 'auto_increment' then '1' else '0' end) as is_increment,
column_type
from information_schema.columns
where table_schema = (select database())
and table_name = (#{tableName})
order by ordinal_position
</select>
<insert id="insertGenTableColumn"
parameterType="cn.universal.admin.generator.domain.GenTableColumn" useGeneratedKeys="true"
keyProperty="columnId">
insert into gen_table_column (
<if test="tableId != null and tableId != ''">table_id,</if>
<if test="columnName != null and columnName != ''">column_name,</if>
<if test="columnComment != null and columnComment != ''">column_comment,</if>
<if test="columnType != null and columnType != ''">column_type,</if>
<if test="javaType != null and javaType != ''">java_type,</if>
<if test="javaField != null and javaField != ''">java_field,</if>
<if test="isPk != null and isPk != ''">is_pk,</if>
<if test="isIncrement != null and isIncrement != ''">is_increment,</if>
<if test="isRequired != null and isRequired != ''">is_required,</if>
<if test="isInsert != null and isInsert != ''">is_insert,</if>
<if test="isEdit != null and isEdit != ''">is_edit,</if>
<if test="isList != null and isList != ''">is_list,</if>
<if test="isQuery != null and isQuery != ''">is_query,</if>
<if test="queryType != null and queryType != ''">query_type,</if>
<if test="htmlType != null and htmlType != ''">html_type,</if>
<if test="dictType != null and dictType != ''">dict_type,</if>
<if test="sort != null">sort,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tableId != null and tableId != ''">#{tableId},</if>
<if test="columnName != null and columnName != ''">#{columnName},</if>
<if test="columnComment != null and columnComment != ''">#{columnComment},</if>
<if test="columnType != null and columnType != ''">#{columnType},</if>
<if test="javaType != null and javaType != ''">#{javaType},</if>
<if test="javaField != null and javaField != ''">#{javaField},</if>
<if test="isPk != null and isPk != ''">#{isPk},</if>
<if test="isIncrement != null and isIncrement != ''">#{isIncrement},</if>
<if test="isRequired != null and isRequired != ''">#{isRequired},</if>
<if test="isInsert != null and isInsert != ''">#{isInsert},</if>
<if test="isEdit != null and isEdit != ''">#{isEdit},</if>
<if test="isList != null and isList != ''">#{isList},</if>
<if test="isQuery != null and isQuery != ''">#{isQuery},</if>
<if test="queryType != null and queryType != ''">#{queryType},</if>
<if test="htmlType != null and htmlType != ''">#{htmlType},</if>
<if test="dictType != null and dictType != ''">#{dictType},</if>
<if test="sort != null">#{sort},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<insert id="insertGenTableColumn"
parameterType="cn.universal.admin.generator.domain.GenTableColumn" useGeneratedKeys="true"
keyProperty="columnId">
insert into gen_table_column (
<if test="tableId != null and tableId != ''">table_id,</if>
<if test="columnName != null and columnName != ''">column_name,</if>
<if test="columnComment != null and columnComment != ''">column_comment,</if>
<if test="columnType != null and columnType != ''">column_type,</if>
<if test="javaType != null and javaType != ''">java_type,</if>
<if test="javaField != null and javaField != ''">java_field,</if>
<if test="isPk != null and isPk != ''">is_pk,</if>
<if test="isIncrement != null and isIncrement != ''">is_increment,</if>
<if test="isRequired != null and isRequired != ''">is_required,</if>
<if test="isInsert != null and isInsert != ''">is_insert,</if>
<if test="isEdit != null and isEdit != ''">is_edit,</if>
<if test="isList != null and isList != ''">is_list,</if>
<if test="isQuery != null and isQuery != ''">is_query,</if>
<if test="queryType != null and queryType != ''">query_type,</if>
<if test="htmlType != null and htmlType != ''">html_type,</if>
<if test="dictType != null and dictType != ''">dict_type,</if>
<if test="sort != null">sort,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tableId != null and tableId != ''">#{tableId},</if>
<if test="columnName != null and columnName != ''">#{columnName},</if>
<if test="columnComment != null and columnComment != ''">#{columnComment},</if>
<if test="columnType != null and columnType != ''">#{columnType},</if>
<if test="javaType != null and javaType != ''">#{javaType},</if>
<if test="javaField != null and javaField != ''">#{javaField},</if>
<if test="isPk != null and isPk != ''">#{isPk},</if>
<if test="isIncrement != null and isIncrement != ''">#{isIncrement},</if>
<if test="isRequired != null and isRequired != ''">#{isRequired},</if>
<if test="isInsert != null and isInsert != ''">#{isInsert},</if>
<if test="isEdit != null and isEdit != ''">#{isEdit},</if>
<if test="isList != null and isList != ''">#{isList},</if>
<if test="isQuery != null and isQuery != ''">#{isQuery},</if>
<if test="queryType != null and queryType != ''">#{queryType},</if>
<if test="htmlType != null and htmlType != ''">#{htmlType},</if>
<if test="dictType != null and dictType != ''">#{dictType},</if>
<if test="sort != null">#{sort},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<update id="updateGenTableColumn"
parameterType="cn.universal.admin.generator.domain.GenTableColumn">
update gen_table_column
<set>
column_comment = #{columnComment},
java_type = #{javaType},
java_field = #{javaField},
is_insert = #{isInsert},
is_edit = #{isEdit},
is_list = #{isList},
is_query = #{isQuery},
is_required = #{isRequired},
query_type = #{queryType},
html_type = #{htmlType},
dict_type = #{dictType},
sort = #{sort},
update_by = #{updateBy},
update_time = sysdate()
</set>
where column_id = #{columnId}
</update>
<update id="updateGenTableColumn"
parameterType="cn.universal.admin.generator.domain.GenTableColumn">
update gen_table_column
<set>
column_comment = #{columnComment},
java_type = #{javaType},
java_field = #{javaField},
is_insert = #{isInsert},
is_edit = #{isEdit},
is_list = #{isList},
is_query = #{isQuery},
is_required = #{isRequired},
query_type = #{queryType},
html_type = #{htmlType},
dict_type = #{dictType},
sort = #{sort},
update_by = #{updateBy},
update_time = sysdate()
</set>
where column_id = #{columnId}
</update>
<delete id="deleteGenTableColumnByIds" parameterType="Long">
delete from gen_table_column where table_id in
<foreach collection="array" item="tableId" open="(" separator="," close=")">
#{tableId}
</foreach>
</delete>
<delete id="deleteGenTableColumnByIds" parameterType="Long">
delete from gen_table_column where table_id in
<foreach collection="array" item="tableId" open="(" separator="," close=")">
#{tableId}
</foreach>
</delete>
<delete id="deleteGenTableColumns">
delete from gen_table_column where column_id in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.columnId}
</foreach>
</delete>
<delete id="deleteGenTableColumns">
delete from gen_table_column where column_id in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.columnId}
</foreach>
</delete>
</mapper>
@@ -1,323 +1,323 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.universal.admin.generator.mapper.GenTableMapper">
<resultMap type="cn.universal.admin.generator.domain.GenTable" id="GenTableResult">
<id property="tableId" column="table_id"/>
<result property="tableName" column="table_name"/>
<result property="tableComment" column="table_comment"/>
<result property="subTableName" column="sub_table_name"/>
<result property="subTableFkName" column="sub_table_fk_name"/>
<result property="className" column="class_name"/>
<result property="tplCategory" column="tpl_category"/>
<result property="packageName" column="package_name"/>
<result property="moduleName" column="module_name"/>
<result property="businessName" column="business_name"/>
<result property="functionName" column="function_name"/>
<result property="functionAuthor" column="function_Author"/>
<result property="genType" column="gen_type"/>
<result property="genPath" column="gen_path"/>
<result property="options" column="options"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<collection property="columns" javaType="java.util.List" resultMap="GenTableColumnResult"/>
</resultMap>
<resultMap type="cn.universal.admin.generator.domain.GenTable" id="GenTableResult">
<id property="tableId" column="table_id"/>
<result property="tableName" column="table_name"/>
<result property="tableComment" column="table_comment"/>
<result property="subTableName" column="sub_table_name"/>
<result property="subTableFkName" column="sub_table_fk_name"/>
<result property="className" column="class_name"/>
<result property="tplCategory" column="tpl_category"/>
<result property="packageName" column="package_name"/>
<result property="moduleName" column="module_name"/>
<result property="businessName" column="business_name"/>
<result property="functionName" column="function_name"/>
<result property="functionAuthor" column="function_Author"/>
<result property="genType" column="gen_type"/>
<result property="genPath" column="gen_path"/>
<result property="options" column="options"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<collection property="columns" javaType="java.util.List" resultMap="GenTableColumnResult"/>
</resultMap>
<resultMap type="cn.universal.admin.generator.domain.GenTableColumn" id="GenTableColumnResult">
<id property="columnId" column="column_id"/>
<result property="tableId" column="table_id"/>
<result property="columnName" column="column_name"/>
<result property="columnComment" column="column_comment"/>
<result property="columnType" column="column_type"/>
<result property="javaType" column="java_type"/>
<result property="javaField" column="java_field"/>
<result property="isPk" column="is_pk"/>
<result property="isIncrement" column="is_increment"/>
<result property="isRequired" column="is_required"/>
<result property="isInsert" column="is_insert"/>
<result property="isEdit" column="is_edit"/>
<result property="isList" column="is_list"/>
<result property="isQuery" column="is_query"/>
<result property="queryType" column="query_type"/>
<result property="htmlType" column="html_type"/>
<result property="dictType" column="dict_type"/>
<result property="sort" column="sort"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<resultMap type="cn.universal.admin.generator.domain.GenTableColumn" id="GenTableColumnResult">
<id property="columnId" column="column_id"/>
<result property="tableId" column="table_id"/>
<result property="columnName" column="column_name"/>
<result property="columnComment" column="column_comment"/>
<result property="columnType" column="column_type"/>
<result property="javaType" column="java_type"/>
<result property="javaField" column="java_field"/>
<result property="isPk" column="is_pk"/>
<result property="isIncrement" column="is_increment"/>
<result property="isRequired" column="is_required"/>
<result property="isInsert" column="is_insert"/>
<result property="isEdit" column="is_edit"/>
<result property="isList" column="is_list"/>
<result property="isQuery" column="is_query"/>
<result property="queryType" column="query_type"/>
<result property="htmlType" column="html_type"/>
<result property="dictType" column="dict_type"/>
<result property="sort" column="sort"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectGenTableVo">
select table_id,
table_name,
table_comment,
sub_table_name,
sub_table_fk_name,
class_name,
tpl_category,
package_name,
module_name,
business_name,
function_name,
function_Author,
gen_type,
gen_path,
options,
create_by,
create_time,
update_by,
update_time,
remark
from gen_table
</sql>
<sql id="selectGenTableVo">
select table_id,
table_name,
table_comment,
sub_table_name,
sub_table_fk_name,
class_name,
tpl_category,
package_name,
module_name,
business_name,
function_name,
function_Author,
gen_type,
gen_path,
options,
create_by,
create_time,
update_by,
update_time,
remark
from gen_table
</sql>
<select id="selectGenTableList" parameterType="cn.universal.admin.generator.domain.GenTable"
resultMap="GenTableResult">
<include refid="selectGenTableVo"/>
<where>
<if test="tableName != null and tableName != ''">
AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
</if>
<if test="tableComment != null and tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
</select>
<select id="selectGenTableList" parameterType="cn.universal.admin.generator.domain.GenTable"
resultMap="GenTableResult">
<include refid="selectGenTableVo"/>
<where>
<if test="tableName != null and tableName != ''">
AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
</if>
<if test="tableComment != null and tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
</where>
</select>
<select id="selectDbTableList" parameterType="cn.universal.admin.generator.domain.GenTable"
resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_schema = (select database())
AND table_name NOT LIKE 'qrtz_%' AND table_name NOT LIKE 'gen_%'
AND table_name NOT IN (select table_name from gen_table)
<if test="tableName != null and tableName != ''">
AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
</if>
<if test="tableComment != null and tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
order by create_time desc
</select>
<select id="selectDbTableList" parameterType="cn.universal.admin.generator.domain.GenTable"
resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_schema = (select database())
AND table_name NOT LIKE 'qrtz_%' AND table_name NOT LIKE 'gen_%'
AND table_name NOT IN (select table_name from gen_table)
<if test="tableName != null and tableName != ''">
AND lower(table_name) like lower(concat('%', #{tableName}, '%'))
</if>
<if test="tableComment != null and tableComment != ''">
AND lower(table_comment) like lower(concat('%', #{tableComment}, '%'))
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
AND date_format(create_time,'%y%m%d') &gt;= date_format(#{params.beginTime},'%y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''"><!-- 结束时间检索 -->
AND date_format(create_time,'%y%m%d') &lt;= date_format(#{params.endTime},'%y%m%d')
</if>
order by create_time desc
</select>
<select id="selectDbTableListByNames" resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_name NOT LIKE 'qrtz_%' and table_name NOT LIKE 'gen_%' and table_schema = (select
database())
and table_name in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</select>
<select id="selectDbTableListByNames" resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time from information_schema.tables
where table_name NOT LIKE 'qrtz_%' and table_name NOT LIKE 'gen_%' and table_schema = (select
database())
and table_name in
<foreach collection="array" item="name" open="(" separator="," close=")">
#{name}
</foreach>
</select>
<select id="selectTableByName" parameterType="String" resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time
from information_schema.tables
where table_comment <![CDATA[ <> ]]> ''
and table_schema = (select database())
and table_name = #{tableName}
</select>
<select id="selectTableByName" parameterType="String" resultMap="GenTableResult">
select table_name, table_comment, create_time, update_time
from information_schema.tables
where table_comment <![CDATA[ <> ]]> ''
and table_schema = (select database())
and table_name = #{tableName}
</select>
<select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.gen_type,
t.gen_path,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
where t.table_id = #{tableId}
order by c.sort
</select>
<select id="selectGenTableById" parameterType="Long" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.gen_type,
t.gen_path,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
where t.table_id = #{tableId}
order by c.sort
</select>
<select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.gen_type,
t.gen_path,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
where t.table_name = #{tableName}
order by c.sort
</select>
<select id="selectGenTableByName" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.gen_type,
t.gen_path,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
where t.table_name = #{tableName}
order by c.sort
</select>
<select id="selectGenTableAll" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
order by c.sort
</select>
<select id="selectGenTableAll" parameterType="String" resultMap="GenTableResult">
SELECT t.table_id,
t.table_name,
t.table_comment,
t.sub_table_name,
t.sub_table_fk_name,
t.class_name,
t.tpl_category,
t.package_name,
t.module_name,
t.business_name,
t.function_name,
t.function_Author,
t.options,
t.remark,
c.column_id,
c.column_name,
c.column_comment,
c.column_type,
c.java_type,
c.java_field,
c.is_pk,
c.is_increment,
c.is_required,
c.is_insert,
c.is_edit,
c.is_list,
c.is_query,
c.query_type,
c.html_type,
c.dict_type,
c.sort
FROM gen_table t
LEFT JOIN gen_table_column c ON t.table_id = c.table_id
order by c.sort
</select>
<insert id="insertGenTable" parameterType="cn.universal.admin.generator.domain.GenTable"
useGeneratedKeys="true" keyProperty="tableId">
insert into gen_table (
<if test="tableName != null">table_name,</if>
<if test="tableComment != null and tableComment != ''">table_comment,</if>
<if test="className != null and className != ''">class_name,</if>
<if test="tplCategory != null and tplCategory != ''">tpl_category,</if>
<if test="packageName != null and packageName != ''">package_name,</if>
<if test="moduleName != null and moduleName != ''">module_name,</if>
<if test="businessName != null and businessName != ''">business_name,</if>
<if test="functionName != null and functionName != ''">function_name,</if>
<if test="functionAuthor != null and functionAuthor != ''">function_Author,</if>
<if test="genType != null and genType != ''">gen_type,</if>
<if test="genPath != null and genPath != ''">gen_path,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tableName != null">#{tableName},</if>
<if test="tableComment != null and tableComment != ''">#{tableComment},</if>
<if test="className != null and className != ''">#{className},</if>
<if test="tplCategory != null and tplCategory != ''">#{tplCategory},</if>
<if test="packageName != null and packageName != ''">#{packageName},</if>
<if test="moduleName != null and moduleName != ''">#{moduleName},</if>
<if test="businessName != null and businessName != ''">#{businessName},</if>
<if test="functionName != null and functionName != ''">#{functionName},</if>
<if test="functionAuthor != null and functionAuthor != ''">#{functionAuthor},</if>
<if test="genType != null and genType != ''">#{genType},</if>
<if test="genPath != null and genPath != ''">#{genPath},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<insert id="insertGenTable" parameterType="cn.universal.admin.generator.domain.GenTable"
useGeneratedKeys="true" keyProperty="tableId">
insert into gen_table (
<if test="tableName != null">table_name,</if>
<if test="tableComment != null and tableComment != ''">table_comment,</if>
<if test="className != null and className != ''">class_name,</if>
<if test="tplCategory != null and tplCategory != ''">tpl_category,</if>
<if test="packageName != null and packageName != ''">package_name,</if>
<if test="moduleName != null and moduleName != ''">module_name,</if>
<if test="businessName != null and businessName != ''">business_name,</if>
<if test="functionName != null and functionName != ''">function_name,</if>
<if test="functionAuthor != null and functionAuthor != ''">function_Author,</if>
<if test="genType != null and genType != ''">gen_type,</if>
<if test="genPath != null and genPath != ''">gen_path,</if>
<if test="remark != null and remark != ''">remark,</if>
<if test="createBy != null and createBy != ''">create_by,</if>
create_time
)values(
<if test="tableName != null">#{tableName},</if>
<if test="tableComment != null and tableComment != ''">#{tableComment},</if>
<if test="className != null and className != ''">#{className},</if>
<if test="tplCategory != null and tplCategory != ''">#{tplCategory},</if>
<if test="packageName != null and packageName != ''">#{packageName},</if>
<if test="moduleName != null and moduleName != ''">#{moduleName},</if>
<if test="businessName != null and businessName != ''">#{businessName},</if>
<if test="functionName != null and functionName != ''">#{functionName},</if>
<if test="functionAuthor != null and functionAuthor != ''">#{functionAuthor},</if>
<if test="genType != null and genType != ''">#{genType},</if>
<if test="genPath != null and genPath != ''">#{genPath},</if>
<if test="remark != null and remark != ''">#{remark},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if>
sysdate()
)
</insert>
<update id="updateGenTable" parameterType="cn.universal.admin.generator.domain.GenTable">
update gen_table
<set>
<if test="tableName != null">table_name = #{tableName},</if>
<if test="tableComment != null and tableComment != ''">table_comment = #{tableComment},</if>
<if test="subTableName != null">sub_table_name = #{subTableName},</if>
<if test="subTableFkName != null">sub_table_fk_name = #{subTableFkName},</if>
<if test="className != null and className != ''">class_name = #{className},</if>
<if test="functionAuthor != null and functionAuthor != ''">function_Author =
#{functionAuthor},
</if>
<if test="genType != null and genType != ''">gen_type = #{genType},</if>
<if test="genPath != null and genPath != ''">gen_path = #{genPath},</if>
<if test="tplCategory != null and tplCategory != ''">tpl_category = #{tplCategory},</if>
<if test="packageName != null and packageName != ''">package_name = #{packageName},</if>
<if test="moduleName != null and moduleName != ''">module_name = #{moduleName},</if>
<if test="businessName != null and businessName != ''">business_name = #{businessName},</if>
<if test="functionName != null and functionName != ''">function_name = #{functionName},</if>
<if test="options != null and options != ''">options = #{options},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where table_id = #{tableId}
</update>
<update id="updateGenTable" parameterType="cn.universal.admin.generator.domain.GenTable">
update gen_table
<set>
<if test="tableName != null">table_name = #{tableName},</if>
<if test="tableComment != null and tableComment != ''">table_comment = #{tableComment},</if>
<if test="subTableName != null">sub_table_name = #{subTableName},</if>
<if test="subTableFkName != null">sub_table_fk_name = #{subTableFkName},</if>
<if test="className != null and className != ''">class_name = #{className},</if>
<if test="functionAuthor != null and functionAuthor != ''">function_Author =
#{functionAuthor},
</if>
<if test="genType != null and genType != ''">gen_type = #{genType},</if>
<if test="genPath != null and genPath != ''">gen_path = #{genPath},</if>
<if test="tplCategory != null and tplCategory != ''">tpl_category = #{tplCategory},</if>
<if test="packageName != null and packageName != ''">package_name = #{packageName},</if>
<if test="moduleName != null and moduleName != ''">module_name = #{moduleName},</if>
<if test="businessName != null and businessName != ''">business_name = #{businessName},</if>
<if test="functionName != null and functionName != ''">function_name = #{functionName},</if>
<if test="options != null and options != ''">options = #{options},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="remark != null">remark = #{remark},</if>
update_time = sysdate()
</set>
where table_id = #{tableId}
</update>
<delete id="deleteGenTableByIds" parameterType="Long">
delete from gen_table where table_id in
<foreach collection="array" item="tableId" open="(" separator="," close=")">
#{tableId}
</foreach>
</delete>
<delete id="deleteGenTableByIds" parameterType="Long">
delete from gen_table where table_id in
<foreach collection="array" item="tableId" open="(" separator="," close=")">
#{tableId}
</foreach>
</delete>
</mapper>
@@ -29,79 +29,79 @@ import io.swagger.annotations.Api;
* @date ${datetime}
*/
@RestController
@Api(value = "${functionName}管理", tags = {"${functionName}管理"})
@RequestMapping("/admin/${moduleName}/${businessName}")
@Api(value = "${functionName}管理" , tags = {"${functionName}管理" })
@RequestMapping("/admin/${moduleName}/${businessName}" )
public class ${ClassName}Controller extends BaseController {
@Autowired
private I${ClassName}Service ${className}Service;
@Autowired
private I${ClassName}Service ${className}Service;
/**
* 查询${functionName}列表
*/
@ApiOperation("查询${functionName}列表")
@GetMapping("/list")
@ApiOperation("查询${functionName}列表" )
@GetMapping("/list" )
#if($table.crud || $table.sub)
public TableDataInfo list(${ClassName} ${className}) {
startPage();
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
return getDataTable(list);
startPage();
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
return getDataTable(list);
}
#elseif($table.tree)
public AjaxResult list(${ClassName} ${className}) {
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
return AjaxResult.success(list);
}
public AjaxResult list(${ClassName} ${className}) {
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
return AjaxResult.success(list);
}
#end
/**
* 导出${functionName}列表
*/
@ApiOperation("导出${functionName}列表")
@PostMapping("/export")
public void export(HttpServletResponse response, ${ClassName} ${className}) {
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}. class);
util.exportExcel(response, list, "${functionName}数据");
}
/**
* 导出${functionName}列表
*/
@ApiOperation("导出${functionName}列表" )
@PostMapping("/export" )
public void export(HttpServletResponse response, ${ClassName} ${className}) {
List<${ClassName}> list = ${className}Service.select${ClassName}List(${className});
ExcelUtil<${ClassName}> util = new ExcelUtil<${ClassName}>(${ClassName}. class);
util.exportExcel(response, list, "${functionName}数据" );
}
/**
* 获取${functionName}详细信息
*/
@ApiOperation("获取${functionName}详细信息")
@GetMapping(value = "/{${pkColumn.javaField}}")
public AjaxResult<${ClassName}> getInfo(
@PathVariable("${pkColumn.javaField}") ${pkColumn.javaType} ${pkColumn.javaField}) {
return AjaxResult.success(${className}Service.select${ClassName}ById(${pkColumn.javaField}));
}
/**
* 获取${functionName}详细信息
*/
@ApiOperation("获取${functionName}详细信息" )
@GetMapping(value = "/{${pkColumn.javaField}}" )
public AjaxResult<${ClassName}> getInfo(
@PathVariable("${pkColumn.javaField}" ) ${pkColumn.javaType} ${pkColumn.javaField}) {
return AjaxResult.success(${className}Service.select${ClassName}ById(${pkColumn.javaField}));
}
/**
* 新增${functionName}
*/
@ApiOperation("新增${functionName}")
@PostMapping
public AjaxResult<Void> add(@RequestBody ${ClassName} ${className}) {
${className}Service.insert${ClassName}(${className});
return AjaxResult.success();
}
/**
* 新增${functionName}
*/
@ApiOperation("新增${functionName}" )
@PostMapping
public AjaxResult<Void> add(@RequestBody ${ClassName} ${className}) {
${className}Service.insert${ClassName}(${className});
return AjaxResult.success();
}
/**
* 修改${functionName}
*/
@ApiOperation("修改${functionName}")
@PutMapping
public AjaxResult<Void> edit(@RequestBody ${ClassName} ${className}) {
${className}Service.update${ClassName}(${className});
return AjaxResult.success();
}
/**
* 修改${functionName}
*/
@ApiOperation("修改${functionName}" )
@PutMapping
public AjaxResult<Void> edit(@RequestBody ${ClassName} ${className}) {
${className}Service.update${ClassName}(${className});
return AjaxResult.success();
}
/**
* 删除${functionName}
*/
@ApiOperation("删除${functionName}")
@DeleteMapping("/{${pkColumn.javaField}s}")
public AjaxResult<Void> remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) {
${className}Service.delete${ClassName}ByIds(${pkColumn.javaField}s);
return AjaxResult.success();
}
/**
* 删除${functionName}
*/
@ApiOperation("删除${functionName}" )
@DeleteMapping("/{${pkColumn.javaField}s}" )
public AjaxResult<Void> remove(@PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) {
${className}Service.delete${ClassName}ByIds(${pkColumn.javaField}s);
return AjaxResult.success();
}
}
@@ -26,38 +26,38 @@ import ${import};
#elseif($table.tree)
#set($Entity="TreeEntity")
#end
@Table(name = "${tableName}")
@Data
public class ${ClassName} extends
@Table(name = "${tableName}" )
@Data
public class ${ClassName} extends
BaseEntity {
private static final long serialVersionUID = 1L;
BaseEntity {
private static final long serialVersionUID = 1L;
#foreach ($column in $columns)
#if(!$column.superColumn)
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf("("))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#foreach ($column in $columns)
#if(!$column.superColumn)
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf("("))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($parentheseIndex != -1)
@Excel(name = "${comment}" , readConverterExp = "$column.readConverterExp()" )
#elseif($column.javaType == 'Date')
@Excel(name = "${comment}" , width = 30, dateFormat = "yyyy-MM-dd" )
#else
@Excel(name = "${comment}" )
#end
#end
#if($parentheseIndex != -1)
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
#elseif($column.javaType == 'Date')
@Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
#if($column.isPk==1)
@Id
#else
@Excel(name = "${comment}")
@Column(name = "${column.columnName}" )
#end
#end
#if($column.isPk==1)
@Id
#else
@Column(name = "${column.columnName}")
#end
private $column.javaType $column.javaField;
private $column.javaType $column.javaField;
#end
#end
#end
}
}
@@ -13,51 +13,51 @@ import cn.universal.persistence.common.BaseMapper;
*/
public interface ${ClassName}Mapper extends BaseMapper<${ClassName}> {
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}集合
*/
List<${ClassName}> select${ClassName}List(${ClassName} ${className});
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}集合
*/
List<${ClassName}> select${ClassName}List(${ClassName} ${className});
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int insert${ClassName}(${ClassName} ${className});
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int insert${ClassName}(${ClassName} ${className});
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int update${ClassName}(${ClassName} ${className});
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int update${ClassName}(${ClassName} ${className});
/**
* 删除${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 删除${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
}
@@ -14,60 +14,60 @@ import com.zebra.common.core.domain.Ztree;
*/
public interface I${ClassName}Service {
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}集合
*/
List<${ClassName}> select${ClassName}List(${ClassName} ${className});
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}集合
*/
List<${ClassName}> select${ClassName}List(${ClassName} ${className});
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int insert${ClassName}(${ClassName} ${className});
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int insert${ClassName}(${ClassName} ${className});
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int update${ClassName}(${ClassName} ${className});
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
int update${ClassName}(${ClassName} ${className});
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
/**
* 批量删除${functionName}
*
* @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s);
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField});
#if($table.tree)
/**
* 查询${functionName}树列表
*
* @return 所有${functionName}信息
*/
List<Ztree> select${ClassName}Tree();
/**
* 查询${functionName}树列表
*
* @return 所有${functionName}信息
*/
List<Ztree> select${ClassName}Tree();
#end
}
@@ -25,118 +25,118 @@ import ${packageName}.service.I${ClassName}Service;
@Service
public class ${ClassName}ServiceImpl implements I${ClassName}Service {
@Resource
private ${ClassName}Mapper ${className}Mapper;
@Resource
private ${ClassName}Mapper ${className}Mapper;
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
@Override
public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) {
return ${className}Mapper.select${ClassName}ById(${pkColumn.javaField});
}
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return ${functionName}
*/
@Override
public ${ClassName} select${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) {
return ${className}Mapper.select${ClassName}ById(${pkColumn.javaField});
}
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}
*/
@Override
public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) {
return ${className}Mapper.select${ClassName}List(${className});
}
/**
* 查询${functionName}列表
*
* @param ${className} ${functionName}
* @return ${functionName}
*/
@Override
public List<${ClassName}> select${ClassName}List(${ClassName} ${className}) {
return ${className}Mapper.select${ClassName}List(${className});
}
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
@Override
public int insert${ClassName}(${ClassName} ${className}) {
#foreach ($column in $columns)
#if($column.javaField == 'createTime')
${className}.setCreateTime(new Date());
#end
#if($column.javaField == 'updateTime')
${className}.setUpdateTime(new Date());
#end
#if($column.javaField == 'updateBy')
${className}.setUpdateBy(SecurityUtils.getUnionId());
#end
#end
return ${className}Mapper.insert${ClassName}(${className});
}
/**
* 新增${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
@Override
public int insert${ClassName}(${ClassName} ${className}) {
#foreach ($column in $columns)
#if($column.javaField == 'createTime')
${className}.setCreateTime(new Date());
#end
#if($column.javaField == 'updateTime')
${className}.setUpdateTime(new Date());
#end
#if($column.javaField == 'updateBy')
${className}.setUpdateBy(SecurityUtils.getUnionId());
#end
#end
return ${className}Mapper.insert${ClassName}(${className});
}
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
@Override
public int update${ClassName}(${ClassName} ${className}) {
#foreach ($column in $columns)
#if($column.javaField == 'updateTime')
${className}.setUpdateTime(new Date());
#end
#if($column.javaField == 'updateBy')
${className}.setUpdateBy(SecurityUtils.getUnionId());
#end
#end
return ${className}Mapper.update${ClassName}(${className});
}
/**
* 修改${functionName}
*
* @param ${className} ${functionName}
* @return 结果
*/
@Override
public int update${ClassName}(${ClassName} ${className}) {
#foreach ($column in $columns)
#if($column.javaField == 'updateTime')
${className}.setUpdateTime(new Date());
#end
#if($column.javaField == 'updateBy')
${className}.setUpdateBy(SecurityUtils.getUnionId());
#end
#end
return ${className}Mapper.update${ClassName}(${className});
}
/**
* 删除${functionName}对象
*
* @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
@Override
public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s) {
return ${className}Mapper.delete${ClassName}ByIds(${pkColumn.javaType}[]${pkColumn.javaField}s);
}
/**
* 删除${functionName}对象
*
* @param ${pkColumn.javaType}[] ${pkColumn.javaField}s 需要删除的数据ID
* @return 结果
*/
@Override
public int delete${ClassName}ByIds(${pkColumn.javaType}[] ${pkColumn.javaField}s) {
return ${className}Mapper.delete${ClassName}ByIds(${pkColumn.javaType}[]${pkColumn.javaField}s);
}
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
@Override
public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) {
return ${className}Mapper.delete${ClassName}ById(${pkColumn.javaField});
}
/**
* 删除${functionName}信息
*
* @param ${pkColumn.javaField} ${functionName}ID
* @return 结果
*/
@Override
public int delete${ClassName}ById(${pkColumn.javaType} ${pkColumn.javaField}) {
return ${className}Mapper.delete${ClassName}ById(${pkColumn.javaField});
}
#if($table.tree)
/**
* 查询${functionName}树列表
*
* @return 所有${functionName}信息
*/
@Override
public List<Ztree> select${ClassName}Tree() {
List<${ClassName}> ${className}List = ${className}Mapper.select${ClassName}List(
new ${ClassName}());
List<Ztree> ztrees = new ArrayList<Ztree>();
for (${ClassName} ${className} :${className}List)
{
Ztree ztree = new Ztree();
#set($TreeCode=$treeCode.substring(0,1).toUpperCase() + ${treeCode.substring(1)})
#set($TreeParentCode=$treeParentCode.substring(0,1).toUpperCase() + ${treeParentCode.substring(1)})
#set($TreeName=$treeName.substring(0,1).toUpperCase() + ${treeName.substring(1)})
ztree.setId(${className}.get${TreeCode}());
ztree.setpId(${className}.get${TreeParentCode}());
ztree.setName(${className}.get${TreeName}());
ztree.setTitle(${className}.get${TreeName}());
ztrees.add(ztree);
/**
* 查询${functionName}树列表
*
* @return 所有${functionName}信息
*/
@Override
public List<Ztree> select${ClassName}Tree() {
List<${ClassName}> ${className}List = ${className}Mapper.select${ClassName}List(
new ${ClassName}());
List<Ztree> ztrees = new ArrayList<Ztree>();
for (${ClassName} ${className} :${className}List)
{
Ztree ztree = new Ztree();
#set($TreeCode=$treeCode.substring(0,1).toUpperCase() + ${treeCode.substring(1)})
#set($TreeParentCode=$treeParentCode.substring(0,1).toUpperCase() + ${treeParentCode.substring(1)})
#set($TreeName=$treeName.substring(0,1).toUpperCase() + ${treeName.substring(1)})
ztree.setId(${className}.get${TreeCode}());
ztree.setpId(${className}.get${TreeParentCode}());
ztree.setName(${className}.get${TreeName}());
ztree.setTitle(${className}.get${TreeName}());
ztrees.add(ztree);
}
return ztrees;
}
return ztrees;
}
#end
}
@@ -15,31 +15,31 @@ import com.ruoyi.common.annotation.Excel;
@Data
public class ${subClassName} extends
BaseEntity {
private static final long serialVersionUID = 1L;
BaseEntity {
private static final long serialVersionUID = 1L;
#foreach ($column in $subTable.columns)
#if(!$table.isSuperColumn($column.javaField))
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf("("))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($parentheseIndex != -1)
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
#elseif($column.javaType == 'Date')
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
#else
@Excel(name = "${comment}")
#foreach ($column in $subTable.columns)
#if(!$table.isSuperColumn($column.javaField))
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf("("))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($parentheseIndex != -1)
@Excel(name = "${comment}" , readConverterExp = "$column.readConverterExp()" )
#elseif($column.javaType == 'Date')
@JsonFormat(pattern = "yyyy-MM-dd" )
@Excel(name = "${comment}" , width = 30, dateFormat = "yyyy-MM-dd" )
#else
@Excel(name = "${comment}" )
#end
#end
private $column.javaType $column.javaField;
#end
private $column.javaType $column.javaField;
#end
#end
}
}
@@ -2,43 +2,43 @@ import request from '@/utils/request'
// 查询${functionName}列表
export function list${BusinessName}(query) {
return request({
url: '/admin/${moduleName}/${businessName}/list',
method: 'get',
params: query
})
return request({
url: '/admin/${moduleName}/${businessName}/list',
method: 'get',
params: query
})
}
// 查询${functionName}详细
export function get${BusinessName}(${pkColumn.javaField}) {
return request({
url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
})
return request({
url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'get'
})
}
// 新增${functionName}
export function add${BusinessName}(data) {
return request({
url: '/admin/${moduleName}/${businessName}',
method: 'post',
data: data
})
return request({
url: '/admin/${moduleName}/${businessName}',
method: 'post',
data: data
})
}
// 修改${functionName}
export function update${BusinessName}(data) {
return request({
url: '/admin/${moduleName}/${businessName}',
method: 'put',
data: data
})
return request({
url: '/admin/${moduleName}/${businessName}',
method: 'put',
data: data
})
}
// 删除${functionName}
export function del${BusinessName}(${pkColumn.javaField}) {
return request({
url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
})
return request({
url: '/admin/${moduleName}/${businessName}/' + ${pkColumn.javaField},
method: 'delete'
})
}
@@ -70,8 +70,7 @@
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索
</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}
</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}</el-button>
</el-form-item>
</el-form>
@@ -68,11 +68,9 @@
#end
#end
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t
('button.search') }}
</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('button.search') }}
</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}</el-button>
</el-form-item>
</el-form>
@@ -68,11 +68,8 @@
#end
#end
<el-form-item>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">{{ $t
('button.edit') }}
</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}
</el-button>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">{{ $t('button.edit') }}</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}</el-button>
</el-form-item>
</el-form>
@@ -68,11 +68,8 @@
#end
#end
<el-form-item>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">{{ $t
('button.search') }}
</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}
</el-button>
<el-button type="primary" icon="Search" size="mini" @click="handleQuery">{{ $t('button.search') }}</el-button>
<el-button icon="Refresh" size="mini" @click="resetQuery">{{ $t('button.reset') }}</el-button>
</el-form-item>
</el-form>
@@ -1,139 +1,139 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
<resultMap type="${ClassName}" id="${ClassName}Result">
#foreach ($column in $columns)
<result property="${column.javaField}" column="${column.columnName}"/>
#end
#if($table.tree)
<result property="parentName" column="parent_name"/>
#end
</resultMap>
<resultMap type="${ClassName}" id="${ClassName}Result">
#foreach ($column in $columns)
<result property="${column.javaField}" column="${column.columnName}"/>
#end
#if($table.tree)
<result property="parentName" column="parent_name"/>
#end
</resultMap>
<sql id="select${ClassName}Vo">
select#foreach($column in $columns) $column.columnName#if($velocityCount != $columns.size()),#end#end
from ${tableName}
</sql>
<sql id="select${ClassName}Vo">
select#foreach($column in $columns) $column.columnName#if($velocityCount != $columns.size()),#end#end
from ${tableName}
</sql>
<select id="select${ClassName}List" parameterType="${ClassName}" resultMap="${ClassName}Result">
<include refid="select${ClassName}Vo"/>
<where>
#foreach($column in $columns)
#set($queryType=$column.queryType)
#set($javaField=$column.javaField)
#set($javaType=$column.javaType)
#set($columnName=$column.columnName)
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#if($column.query)
#if($column.queryType == "EQ")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName = #{$javaField}
</if>
#elseif($queryType == "NE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName != #{$javaField}
</if>
#elseif($queryType == "GT")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &gt; #{$javaField}
</if>
#elseif($queryType == "GTE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &gt;= #{$javaField
}
</if>
#elseif($queryType == "LT")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &lt; #{$javaField}
</if>
#elseif($queryType == "LTE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &lt;= #{$javaField
}
</if>
#elseif($queryType == "LIKE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName like concat('%',
#{$javaField}, '%')
</if>
#elseif($queryType == "BETWEEN")
<if
test="params.begin$AttrName != null and params.begin$AttrName != '' and params.end$AttrName != null and params.end$AttrName != ''">
and $columnName between #{params.begin$AttrName} and #{params.end$AttrName}
</if>
<select id="select${ClassName}List" parameterType="${ClassName}" resultMap="${ClassName}Result">
<include refid="select${ClassName}Vo"/>
<where>
#foreach($column in $columns)
#set($queryType=$column.queryType)
#set($javaField=$column.javaField)
#set($javaType=$column.javaType)
#set($columnName=$column.columnName)
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#if($column.query)
#if($column.queryType == "EQ")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName = #{$javaField}
</if>
#elseif($queryType == "NE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName != #{$javaField}
</if>
#elseif($queryType == "GT")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &gt; #{$javaField}
</if>
#elseif($queryType == "GTE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &gt;= #{$javaField
}
</if>
#elseif($queryType == "LT")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &lt; #{$javaField}
</if>
#elseif($queryType == "LTE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName &lt;= #{$javaField
}
</if>
#elseif($queryType == "LIKE")
<if test="$javaField != null #if($javaType ==
'String' ) and $javaField.trim() != ''#end">and $columnName like concat('%',
#{$javaField}, '%')
</if>
#elseif($queryType == "BETWEEN")
<if
test="params.begin$AttrName != null and params.begin$AttrName != '' and params.end$AttrName != null and params.end$AttrName != ''">
and $columnName between #{params.begin$AttrName} and #{params.end$AttrName}
</if>
#end
#end
#end
</where>
#if($table.tree)
order by ${tree_parent_code}
#end
</where>
#if($table.tree)
order by ${tree_parent_code}
#end
</select>
</select>
<select id="select${ClassName}ById" parameterType="${pkColumn.javaType}"
resultMap="${ClassName}Result">
#if($table.crud)
<include refid="select${ClassName}Vo"/>
<select id="select${ClassName}ById" parameterType="${pkColumn.javaType}"
resultMap="${ClassName}Result">
#if($table.crud)
<include refid="select${ClassName}Vo"/>
where ${pkColumn.columnName} = #{${pkColumn.javaField}}
#elseif($table.tree)
select#foreach($column in $columns) t.$column.columnName,#end p.${tree_name} as parent_name
from ${tableName} t
left join ${tableName} p on p.${pkColumn.columnName} = t.${tree_parent_code}
where t.${pkColumn.columnName} = #{${pkColumn.javaField}}
#end
</select>
<insert id="insert${ClassName}" parameterType="${ClassName}"#if($pkColumn.increment)
useGeneratedKeys="true" keyProperty="$pkColumn.javaField"#end>
insert into ${tableName}
<trim prefix="(" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">$column.columnName,
</if>
#end
#end
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">#{$column.javaField},
</if>
#end
#end
</trim>
</insert>
<update id="update${ClassName}" parameterType="${ClassName}">
update ${tableName}
<trim prefix="SET" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">$column.columnName =
#{$column.javaField},
</if>
#end
#end
</trim>
where ${pkColumn.columnName} = #{${pkColumn.javaField}}
#elseif($table.tree)
select#foreach($column in $columns) t.$column.columnName,#end p.${tree_name} as parent_name
from ${tableName} t
left join ${tableName} p on p.${pkColumn.columnName} = t.${tree_parent_code}
where t.${pkColumn.columnName} = #{${pkColumn.javaField}}
#end
</select>
</update>
<insert id="insert${ClassName}" parameterType="${ClassName}"#if($pkColumn.increment)
useGeneratedKeys="true" keyProperty="$pkColumn.javaField"#end>
insert into ${tableName}
<trim prefix="(" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">$column.columnName,
</if>
#end
#end
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName || !$pkColumn.increment)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">#{$column.javaField},
</if>
#end
#end
</trim>
</insert>
<delete id="delete${ClassName}ById" parameterType="${pkColumn.javaType}">
delete
from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
</delete>
<update id="update${ClassName}" parameterType="${ClassName}">
update ${tableName}
<trim prefix="SET" suffixOverrides=",">
#foreach($column in $columns)
#if($column.columnName != $pkColumn.columnName)
<if test="$column.javaField != null #if($column.javaType ==
'String' ) and $column.javaField != ''#end">$column.columnName =
#{$column.javaField},
</if>
#end
#end
</trim>
where ${pkColumn.columnName} = #{${pkColumn.javaField}}
</update>
<delete id="delete${ClassName}ById" parameterType="${pkColumn.javaType}">
delete
from ${tableName} where ${pkColumn.columnName} = #{${pkColumn.javaField}}
</delete>
<delete id="delete${ClassName}ByIds" parameterType="${pkColumn.javaType}">
delete from ${tableName} where ${pkColumn.columnName} in
<foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
#{${pkColumn.javaField}}
</foreach>
</delete>
<delete id="delete${ClassName}ByIds" parameterType="${pkColumn.javaType}">
delete from ${tableName} where ${pkColumn.columnName} in
<foreach item="${pkColumn.javaField}" collection="array" open="(" separator="," close=")">
#{${pkColumn.javaField}}
</foreach>
</delete>
</mapper>
@@ -1,88 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-core</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-core</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-core</name>
<description>数据桥接核心模块</description>
<name>cn-universal-data-bridge-core</name>
<description>数据桥接核心模块</description>
<dependencies>
<!-- 父项目依赖 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 父项目依赖 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-persistence</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Kafka Client -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<!-- Kafka Client -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</dependency>
<!-- MQTT Client -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
</dependency>
<!-- MQTT Client -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
</dependency>
<!-- IoTDB JDBC Driver -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-jdbc</artifactId>
</dependency>
<!-- IoTDB JDBC Driver -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-jdbc</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-cache</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-cache</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,26 @@
package cn.universal.databridge.engine;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 封装参数化SQL结果:SQL文本 + 有序参数列表
*/
public class ParamSql {
private final String sql;
private final List<Object> params;
public ParamSql(String sql, List<Object> params) {
this.sql = sql;
this.params = params != null ? new ArrayList<>(params) : new ArrayList<>();
}
public String getSql() {
return sql;
}
public List<Object> getParams() {
return Collections.unmodifiableList(params);
}
}
@@ -0,0 +1,107 @@
package cn.universal.databridge.engine;
import cn.hutool.core.util.StrUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 参数化模板引擎:支持 #{path} 值占位
* 所有值均以 '?' 参数占位绑定,避免引号与转义问题
*/
public class ParamTemplateEngine {
private static final Pattern VAR_PATTERN = Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
public ParamSql process(String template, Map<String, Object> variables, SqlDialectAdapter adapter) {
if (StrUtil.isBlank(template)) {
return new ParamSql("", List.of());
}
String sql = template;
List<Object> params = new ArrayList<>();
// 处理 #{var.path} 占位符
Matcher m = VAR_PATTERN.matcher(sql);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String path = m.group(1);
Object value = getNestedValue(variables, path);
// 将值转换为适合数据库的格式
Object paramValue = convertValueForDatabase(value, path);
params.add(paramValue);
// properties 和 data 字段如果是 Map 序列化的 JSON 字符串,需要特殊处理
String placeholder = "?";
if (("properties".equals(path) || "data".equals(path)) && paramValue instanceof String) {
String strValue = (String) paramValue;
// 检查是否是 JSON 对象格式(以 { 开头且以 } 结尾)
if (strValue.trim().startsWith("{") && strValue.trim().endsWith("}")) {
placeholder = adapter.wrapJsonParameter();
}
}
m.appendReplacement(sb, Matcher.quoteReplacement(placeholder));
}
m.appendTail(sb);
sql = sb.toString();
return new ParamSql(sql, params);
}
@SuppressWarnings("unchecked")
private Object getNestedValue(Map<String, Object> variables, String path) {
if (path == null || path.isEmpty()) return null;
String[] keys = path.split("\\.");
Object curr = variables;
for (String k : keys) {
if (curr == null) return null;
if (curr instanceof Map) {
curr = ((Map<String, Object>) curr).get(k);
} else {
return null;
}
}
// 若直接访问 properties 或 data,返回 JSON 字符串,避免各库的 JSON 绑定差异
if (curr instanceof Map && ("properties".equals(path) || "data".equals(path))) {
return cn.hutool.json.JSONUtil.toJsonStr(curr);
}
return curr;
}
/**
* 将值转换为适合数据库存储的格式
* @param value 原始值
* @param path 变量路径
* @return 转换后的值
*/
private Object convertValueForDatabase(Object value, String path) {
if (value == null) {
return null;
}
// 如果已经是字符串,直接返回
if (value instanceof String) {
return value;
}
// 其他类型转为字符串,避免类型不匹配问题
if (value instanceof Number || value instanceof Boolean) {
return value.toString();
}
if (value instanceof java.util.Date) {
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);
}
// Map 类型序列化为 JSON
if (value instanceof Map) {
return cn.hutool.json.JSONUtil.toJsonStr(value);
}
// 其他类型默认转为字符串
return value.toString();
}
}
@@ -0,0 +1,33 @@
package cn.universal.databridge.engine;
import java.util.List;
/**
* SQL方言适配器接口:不同数据库的JSON宏、文本转换等差异在此适配
*/
public interface SqlDialectAdapter {
/**
* 生成 JSON 片段(返回 JSON 类型/对象),参数位置使用占位符 '?'
* @param keys 键名(静态字符串,已安全)
* @param valueCount 值数量(等于 keys.size())
* @return SQL 片段,如 JSON_OBJECT('k1', ?, 'k2', ?)
*/
String jsonFragment(List<String> keys, int valueCount);
/**
* 生成 JSON 文本片段(返回文本),参数位置使用占位符 '?'
* @param keys 键名(静态字符串,已安全)
* @param valueCount 值数量(等于 keys.size())
* @return SQL 片段,如 json_build_object(...)::TEXT
*/
String jsonTextFragment(List<String> keys, int valueCount);
/**
* 包装JSON字符串参数以转换为特定数据库的JSON类型
* @return 转换表达式,默认不转换直接返回 ?
*/
default String wrapJsonParameter() {
return "?";
}
}
@@ -0,0 +1,25 @@
package cn.universal.databridge.engine.dialect;
import cn.universal.databridge.engine.SqlDialectAdapter;
import java.util.List;
import java.util.stream.Collectors;
/** MySQL 方言适配 */
public class MySqlDialectAdapter implements SqlDialectAdapter {
private String buildPairs(List<String> keys) {
return keys.stream()
.map(k -> "'" + k.replace("'", "''") + "', ?")
.collect(Collectors.joining(", "));
}
@Override
public String jsonFragment(List<String> keys, int valueCount) {
return "JSON_OBJECT(" + buildPairs(keys) + ")";
}
@Override
public String jsonTextFragment(List<String> keys, int valueCount) {
// MySQL 的 JSON_OBJECT 返回 JSON 文本即可
return jsonFragment(keys, valueCount);
}
}
@@ -0,0 +1,26 @@
package cn.universal.databridge.engine.dialect;
import cn.universal.databridge.engine.SqlDialectAdapter;
import java.util.List;
import java.util.stream.Collectors;
/** Oracle 方言适配 */
public class OracleDialectAdapter implements SqlDialectAdapter {
private String buildPairs(List<String> keys) {
// Oracle: JSON_OBJECT('k1' VALUE ?, 'k2' VALUE ?)
return keys.stream()
.map(k -> "'" + k.replace("'", "''") + "' VALUE ?")
.collect(Collectors.joining(", "));
}
@Override
public String jsonFragment(List<String> keys, int valueCount) {
return "JSON_OBJECT(" + buildPairs(keys) + ")";
}
@Override
public String jsonTextFragment(List<String> keys, int valueCount) {
// Oracle 的 JSON_OBJECT 返回 JSON 文本字符串
return jsonFragment(keys, valueCount);
}
}
@@ -0,0 +1,30 @@
package cn.universal.databridge.engine.dialect;
import cn.universal.databridge.engine.SqlDialectAdapter;
import java.util.List;
import java.util.stream.Collectors;
/** PostgreSQL 方言适配 */
public class PostgresDialectAdapter implements SqlDialectAdapter {
private String buildPairs(List<String> keys) {
return keys.stream()
.map(k -> "'" + k.replace("'", "''") + "', ?")
.collect(Collectors.joining(", "));
}
@Override
public String jsonFragment(List<String> keys, int valueCount) {
return "json_build_object(" + buildPairs(keys) + ")";
}
@Override
public String jsonTextFragment(List<String> keys, int valueCount) {
return jsonFragment(keys, valueCount) + "::TEXT";
}
@Override
public String wrapJsonParameter() {
// PostgreSQL: 将 JSON 字符串转为 jsonb 类型
return "?::jsonb";
}
}
@@ -0,0 +1,36 @@
package cn.universal.databridge.engine.dialect;
import cn.universal.databridge.engine.SqlDialectAdapter;
import java.util.List;
import java.util.stream.Collectors;
/**
* SQL Server 方言适配
* 注意:SQL Server 2016之前不支持JSON_OBJECT,2016+语法也不同
* 这里使用字符串拼接方式构造JSON(兼容所有版本)
*/
public class SqlServerDialectAdapter implements SqlDialectAdapter {
@Override
public String jsonFragment(List<String> keys, int valueCount) {
// SQL Server 使用 CONCAT 函数拼接 JSON 字符串
// 简化处理:所有值都当作字符串处理(加引号)
// 如果需要精确区分数值,应在应用层处理
StringBuilder sb = new StringBuilder("CONCAT('{'");
for (int i = 0; i < keys.size(); i++) {
if (i > 0) {
sb.append(", ','");
}
String key = keys.get(i).replace("'", "''");
sb.append(", '\"" + key + "\":', COALESCE('\"' + CAST(? AS NVARCHAR(MAX)) + '\"', 'null')");
}
sb.append(", '}'");
return sb.toString() + ")"; // 添加 CONCAT 的闭合括号
}
@Override
public String jsonTextFragment(List<String> keys, int valueCount) {
// SQL Server 的 JSON 构造本身就是文本
return jsonFragment(keys, valueCount);
}
}
@@ -34,61 +34,63 @@ import lombok.NoArgsConstructor;
@Table(name = "iot_data_input_log")
public class DataInputLog implements Serializable {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 配置ID */
@Column(name = "config_id")
private Long configId;
/** 配置ID */
@Column(name = "config_id")
private Long configId;
/** 配置名称 */
@Column(name = "config_name")
private String configName;
/** 配置名称 */
@Column(name = "config_name")
private String configName;
/** 源系统 */
@Column(name = "source_system")
private String sourceSystem;
/** 源系统 */
@Column(name = "source_system")
private String sourceSystem;
/** 消息数量 */
@Column(name = "message_count")
private Integer messageCount;
/** 消息数量 */
@Column(name = "message_count")
private Integer messageCount;
/** 成功数量 */
@Column(name = "success_count")
private Integer successCount;
/** 成功数量 */
@Column(name = "success_count")
private Integer successCount;
/** 失败数量 */
@Column(name = "failed_count")
private Integer failedCount;
/** 失败数量 */
@Column(name = "failed_count")
private Integer failedCount;
/** 错误信息 */
@Column(name = "error_message")
private String errorMessage;
/** 错误信息 */
@Column(name = "error_message")
private String errorMessage;
/** 执行时间(毫秒) */
@Column(name = "execution_time")
private Long executionTime;
/** 执行时间(毫秒) */
@Column(name = "execution_time")
private Long executionTime;
/** 状态 */
@Column(name = "status")
@Enumerated(EnumType.STRING)
private Status status;
/** 状态 */
@Column(name = "status")
@Enumerated(EnumType.STRING)
private Status status;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 状态枚举 */
public enum Status {
SUCCESS, // 成功
FAILED, // 失败
RUNNING // 运行中
}
/**
* 状态枚举
*/
public enum Status {
SUCCESS, // 成功
FAILED, // 失败
RUNNING // 运行中
}
}
@@ -34,72 +34,76 @@ import lombok.NoArgsConstructor;
@Table(name = "iot_data_input_task")
public class DataInputTask implements Serializable {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 配置ID */
@Column(name = "config_id")
private Long configId;
/** 配置ID */
@Column(name = "config_id")
private Long configId;
/** 任务名称 */
@Column(name = "task_name")
private String taskName;
/** 任务名称 */
@Column(name = "task_name")
private String taskName;
/** 任务类型 */
@Column(name = "task_type")
@Enumerated(EnumType.STRING)
private TaskType taskType;
/** 任务类型 */
@Column(name = "task_type")
@Enumerated(EnumType.STRING)
private TaskType taskType;
/** Cron表达式 */
@Column(name = "cron_expression")
private String cronExpression;
/** Cron表达式 */
@Column(name = "cron_expression")
private String cronExpression;
/** 最后执行时间 */
@Column(name = "last_execution_time")
private LocalDateTime lastExecutionTime;
/** 最后执行时间 */
@Column(name = "last_execution_time")
private LocalDateTime lastExecutionTime;
/** 下次执行时间 */
@Column(name = "next_execution_time")
private LocalDateTime nextExecutionTime;
/** 下次执行时间 */
@Column(name = "next_execution_time")
private LocalDateTime nextExecutionTime;
/** 状态 */
@Column(name = "status")
@Enumerated(EnumType.STRING)
private Status status;
/** 状态 */
@Column(name = "status")
@Enumerated(EnumType.STRING)
private Status status;
/** 错误信息 */
@Column(name = "error_message")
private String errorMessage;
/** 错误信息 */
@Column(name = "error_message")
private String errorMessage;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 更新时间 */
@Column(name = "update_time")
private LocalDateTime updateTime;
/** 更新时间 */
@Column(name = "update_time")
private LocalDateTime updateTime;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 更新者 */
@Column(name = "update_by")
private String updateBy;
/** 更新者 */
@Column(name = "update_by")
private String updateBy;
/** 任务类型枚举 */
public enum TaskType {
SCHEDULED, // 定时任务
REALTIME // 实时任务
}
/**
* 任务类型枚举
*/
public enum TaskType {
SCHEDULED, // 定时任务
REALTIME // 实时任务
}
/** 状态枚举 */
public enum Status {
RUNNING, // 运行中
STOPPED, // 已停止
ERROR // 错误状态
}
/**
* 状态枚举
*/
public enum Status {
RUNNING, // 运行中
STOPPED, // 已停止
ERROR // 错误状态
}
}
@@ -34,112 +34,109 @@ import lombok.NoArgsConstructor;
@Table(name = "iot_resource_connection")
public class ResourceConnection implements Serializable {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 资源名称 */
@Column(name = "name")
private String name;
/** 资源名称 */
@Column(name = "name")
private String name;
/** 资源类型 */
@Column(name = "type")
@Enumerated(EnumType.STRING)
private ResourceType type;
/** 资源类型 */
@Column(name = "type")
@Enumerated(EnumType.STRING)
private ResourceType type;
/** 插件类型 */
@Column(name = "plugin_type")
private String pluginType;
/** 插件类型 */
@Column(name = "plugin_type")
private String pluginType;
/** 主机地址 */
@Column(name = "host")
private String host;
/** 主机地址 */
@Column(name = "host")
private String host;
/** 端口号 */
@Column(name = "port")
private Integer port;
/** 端口号 */
@Column(name = "port")
private Integer port;
/** 用户名 */
@Column(name = "username")
private String username;
/** 用户名 */
@Column(name = "username")
private String username;
/** 密码 */
@Column(name = "password")
private String password;
/** 密码 */
@Column(name = "password")
private String password;
/** 数据库名 */
@Column(name = "database_name")
private String databaseName;
/** 数据库名 */
@Column(name = "database_name")
private String databaseName;
/** 扩展配置JSON */
@Column(name = "extra_config")
private String extraConfig;
/** 扩展配置JSON */
@Column(name = "extra_config")
private String extraConfig;
/** 状态:0禁用,1启用 */
@Column(name = "status")
private Integer status;
/** 状态:0禁用,1启用 */
@Column(name = "status")
private Integer status;
/** 描述 */
@Column(name = "description")
private String description;
/** 描述 */
@Column(name = "description")
private String description;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 创建者 */
@Column(name = "create_by")
private String createBy;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 创建时间 */
@Column(name = "create_time")
private LocalDateTime createTime;
/** 更新者 */
@Column(name = "update_by")
private String updateBy;
/** 更新者 */
@Column(name = "update_by")
private String updateBy;
/** 更新时间 */
@Column(name = "update_time")
private LocalDateTime updateTime;
/** 更新时间 */
@Column(name = "update_time")
private LocalDateTime updateTime;
/** 方向:IN-输入,OUT-输出,BOTH-双向 */
@Column(name = "direction")
@Enumerated(EnumType.STRING)
private Direction direction;
/** 方向:IN-输入,OUT-输出,BOTH-双向 */
@Column(name = "direction")
@Enumerated(EnumType.STRING)
private Direction direction;
/** 数据流向:INPUT-输入,OUTPUT-输出,BIDIRECTIONAL-双向 */
@Column(name = "data_direction")
@Enumerated(EnumType.STRING)
private DataDirection dataDirection;
/** 数据流向:INPUT-输入,OUTPUT-输出,BIDIRECTIONAL-双向 */
@Column(name = "data_direction")
@Enumerated(EnumType.STRING)
private DataDirection dataDirection;
/** 动态配置JSON - 使用现有的extra_config字段存储 */
@Transient private String dynamicConfig;
/** 动态配置JSON - 使用现有的extra_config字段存储 */
@Transient
private String dynamicConfig;
/** 资源类型枚举 */
public enum ResourceType {
MYSQL,
KAFKA,
IOTDB,
INFLUXDB,
MQTT,
HTTP,
REDIS,
ELASTICSEARCH,
ALIYUN_IOT,
TENCENT_IOT,
HUAWEI_IOT
}
/**
* 资源类型枚举
*/
public enum ResourceType {
MYSQL, POSTGRESQL, H2, ORACLE, SQLSERVER, KAFKA, IOTDB, INFLUXDB, MQTT, HTTP, REDIS, ELASTICSEARCH, ALIYUN_IOT, TENCENT_IOT, HUAWEI_IOT
}
/** 方向枚举 */
public enum Direction {
IN, // 输入
OUT, // 输出
BOTH // 双向
}
/**
* 方向枚举
*/
public enum Direction {
IN, // 输入
OUT, // 输出
BOTH // 双向
}
/** 数据流向枚举 */
public enum DataDirection {
INPUT, // 数据输入
OUTPUT, // 数据输出
BIDIRECTIONAL // 双向流转
}
/**
* 数据流向枚举
*/
public enum DataDirection {
INPUT, // 数据输入
OUTPUT, // 数据输出
BIDIRECTIONAL // 双向流转
}
}
@@ -60,7 +60,7 @@ public class DataBridgeManager {
@Resource private TemplateEngine templateEngine;
/** 处理设备数据 */
@Async("dataBridgeExecutor")
@Async("taskExecutor")
public void processDeviceData(List<BaseUPRequest> requests) {
if (CollectionUtil.isEmpty(requests)) {
return;
@@ -44,219 +44,224 @@ import org.springframework.stereotype.Component;
@Slf4j
public class DataInputManager {
@Autowired(required = false)
private Map<String, DataInputPlugin> inputPlugins;
@Autowired(required = false)
private Map<String, DataInputPlugin> inputPlugins;
@Resource private ResourceConnectionService resourceConnectionService;
@Resource
private ResourceConnectionService resourceConnectionService;
@Resource private DataBridgeConfigService dataBridgeConfigService;
@Resource
private DataBridgeConfigService dataBridgeConfigService;
@Resource private DataInputLogService dataInputLogService;
@Resource
private DataInputLogService dataInputLogService;
@Resource private DataBridgeLogger dataBridgeLogger;
@Resource
private DataBridgeLogger dataBridgeLogger;
// 存储正在运行的输入任务
private final Map<Long, Boolean> runningTasks = new ConcurrentHashMap<>();
// 存储正在运行的输入任务
private final Map<Long, Boolean> runningTasks = new ConcurrentHashMap<>();
/** 启动数据输入任务 */
public void startInputTask(Long configId) {
try {
DataBridgeConfig config = dataBridgeConfigService.getById(configId);
if (config == null) {
throw new RuntimeException("配置不存在");
}
// TODO: 需要实现新的输入配置判断逻辑,因为Direction枚举已被移除
ResourceConnection connection =
resourceConnectionService.getById(config.getTargetResourceId());
if (connection == null || connection.getStatus() != 1) {
throw new RuntimeException("资源连接不存在或已禁用");
}
DataInputPlugin plugin = inputPlugins.get(config.getBridgeType().name().toUpperCase());
if (plugin == null) {
throw new RuntimeException("未找到对应的输入插件: " + config.getBridgeType());
}
// 标记任务为运行状态
runningTasks.put(configId, true);
// 创建数据消费者回调
Consumer<List<BaseUPRequest>> dataConsumer = data -> processInputData(data, config);
// 启动数据输入
// TODO: 需要重新设计输入插件的启动机制,因为相关方法已被移除
log.info("数据输入任务启动逻辑需要重新实现,配置ID: {}", configId);
} catch (Exception e) {
runningTasks.remove(configId);
log.error("启动数据输入任务失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
throw e;
}
}
/** 停止数据输入任务 */
public void stopInputTask(Long configId) {
try {
DataBridgeConfig config = dataBridgeConfigService.getById(configId);
if (config == null) {
return;
}
ResourceConnection connection =
resourceConnectionService.getById(config.getTargetResourceId());
if (connection == null) {
return;
}
DataInputPlugin plugin = inputPlugins.get(config.getBridgeType().name().toUpperCase());
if (plugin != null) {
// TODO: 需要重新设计输入插件的停止机制,因为stopDataInput方法已被移除
log.info("停止数据输入插件逻辑需要重新实现");
}
// 移除运行标记
runningTasks.remove(configId);
log.info("停止数据输入任务成功,配置ID: {}", configId);
} catch (Exception e) {
log.error("停止数据输入任务失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
throw e;
}
}
/** 检查任务是否正在运行 */
public Boolean isTaskRunning(Long configId) {
return runningTasks.getOrDefault(configId, false);
}
/** 启动定时拉取任务 */
@Async("dataBridgeExecutor")
public void startScheduledPullTask(
DataBridgeConfig config, ResourceConnection connection, DataInputPlugin plugin) {
Long configId = config.getId();
while (runningTasks.getOrDefault(configId, false)) {
try {
// 拉取数据
// TODO: 需要重新设计数据拉取机制,因为pullInputData方法已被移除
List<BaseUPRequest> data = null; // plugin.pullInputData(config, connection);
if (CollectionUtil.isNotEmpty(data)) {
processInputData(data, config);
}
// 等待下次拉取(可以从配置中读取间隔时间)
Thread.sleep(getScheduleInterval(config));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("定时拉取数据失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
// 记录错误日志
logInputExecution(config, 0, 0, 0, e.getMessage(), 0);
// 等待一段时间后重试
/**
* 启动数据输入任务
*/
public void startInputTask(Long configId) {
try {
Thread.sleep(30000); // 30秒后重试
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
DataBridgeConfig config = dataBridgeConfigService.getById(configId);
if (config == null) {
throw new RuntimeException("配置不存在");
}
// TODO: 需要实现新的输入配置判断逻辑,因为Direction枚举已被移除
/** 处理输入数据 */
private void processInputData(List<BaseUPRequest> data, DataBridgeConfig config) {
if (CollectionUtil.isEmpty(data)) {
return;
}
ResourceConnection connection = resourceConnectionService.getById(config.getTargetResourceId());
if (connection == null || connection.getStatus() != 1) {
throw new RuntimeException("资源连接不存在或已禁用");
}
long startTime = System.currentTimeMillis();
int successCount = 0;
int failedCount = 0;
String errorMessage = null;
DataInputPlugin plugin = inputPlugins.get(config.getBridgeType().name().toUpperCase());
if (plugin == null) {
throw new RuntimeException("未找到对应的输入插件: " + config.getBridgeType());
}
// 标记任务为运行状态
runningTasks.put(configId, true);
// 创建数据消费者回调
Consumer<List<BaseUPRequest>> dataConsumer = data -> processInputData(data, config);
// 启动数据输入
// TODO: 需要重新设计输入插件的启动机制,因为相关方法已被移除
log.info("数据输入任务启动逻辑需要重新实现,配置ID: {}", configId);
try {
// TODO: 这里需要将数据推送到IoT平台
// 可以通过消息队列或直接调用IoT平台的API
for (BaseUPRequest request : data) {
try {
// 推送到IoT平台的逻辑
pushToIoTPlatform(request, config);
successCount++;
} catch (Exception e) {
failedCount++;
log.error("推送数据到IoT平台失败: {}", e.getMessage(), e);
runningTasks.remove(configId);
log.error("启动数据输入任务失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
throw e;
}
}
log.debug(
"处理输入数据完成,配置: {}, 总数: {}, 成功: {}, 失败: {}",
config.getName(),
data.size(),
successCount,
failedCount);
} catch (Exception e) {
failedCount = data.size();
errorMessage = e.getMessage();
log.error("处理输入数据异常,配置: {}, 错误: {}", config.getName(), e.getMessage(), e);
} finally {
long executionTime = System.currentTimeMillis() - startTime;
logInputExecution(
config, data.size(), successCount, failedCount, errorMessage, executionTime);
}
}
/** 推送数据到IoT平台 */
private void pushToIoTPlatform(BaseUPRequest request, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug(
"推送数据到IoT平台: 设备={}, 产品={}",
request.getIoTDeviceDTO() != null ? request.getIoTDeviceDTO().getDeviceId() : "unknown",
config.getTargetResourceId());
}
/**
* 停止数据输入任务
*/
public void stopInputTask(Long configId) {
try {
DataBridgeConfig config = dataBridgeConfigService.getById(configId);
if (config == null) {
return;
}
/** 记录输入执行日志 */
private void logInputExecution(
DataBridgeConfig config,
int messageCount,
int successCount,
int failedCount,
String errorMessage,
long executionTime) {
try {
DataInputLog log =
DataInputLog.builder()
.configId(config.getId())
.configName(config.getName())
.sourceSystem(config.getBridgeType().name())
.messageCount(messageCount)
.successCount(successCount)
.failedCount(failedCount)
.errorMessage(errorMessage)
.executionTime(executionTime)
.status(failedCount > 0 ? DataInputLog.Status.FAILED : DataInputLog.Status.SUCCESS)
.createTime(LocalDateTime.now())
.createBy(config.getCreateBy())
.build();
ResourceConnection connection = resourceConnectionService.getById(config.getTargetResourceId());
if (connection == null) {
return;
}
dataInputLogService.save(log);
} catch (Exception e) {
log.error("记录输入执行日志失败: {}", e.getMessage(), e);
DataInputPlugin plugin = inputPlugins.get(config.getBridgeType().name().toUpperCase());
if (plugin != null) {
// TODO: 需要重新设计输入插件的停止机制,因为stopDataInput方法已被移除
log.info("停止数据输入插件逻辑需要重新实现");
}
// 移除运行标记
runningTasks.remove(configId);
log.info("停止数据输入任务成功,配置ID: {}", configId);
} catch (Exception e) {
log.error("停止数据输入任务失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
throw e;
}
}
}
/** 获取调度间隔时间(毫秒) */
private long getScheduleInterval(DataBridgeConfig config) {
// 从配置中读取间隔时间,默认60秒
// TODO: 可以从 inputConfig 中解析间隔时间
return 60000; // 60秒
}
/**
* 检查任务是否正在运行
*/
public Boolean isTaskRunning(Long configId) {
return runningTasks.getOrDefault(configId, false);
}
/**
* 启动定时拉取任务
*/
@Async("taskExecutor")
public void startScheduledPullTask(DataBridgeConfig config, ResourceConnection connection, DataInputPlugin plugin) {
Long configId = config.getId();
while (runningTasks.getOrDefault(configId, false)) {
try {
// 拉取数据
// TODO: 需要重新设计数据拉取机制,因为pullInputData方法已被移除
List<BaseUPRequest> data = null; // plugin.pullInputData(config, connection);
if (CollectionUtil.isNotEmpty(data)) {
processInputData(data, config);
}
// 等待下次拉取(可以从配置中读取间隔时间)
Thread.sleep(getScheduleInterval(config));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("定时拉取数据失败,配置ID: {}, 错误: {}", configId, e.getMessage(), e);
// 记录错误日志
logInputExecution(config, 0, 0, 0, e.getMessage(), 0);
// 等待一段时间后重试
try {
Thread.sleep(30000); // 30秒后重试
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
/**
* 处理输入数据
*/
private void processInputData(List<BaseUPRequest> data, DataBridgeConfig config) {
if (CollectionUtil.isEmpty(data)) {
return;
}
long startTime = System.currentTimeMillis();
int successCount = 0;
int failedCount = 0;
String errorMessage = null;
try {
// TODO: 这里需要将数据推送到IoT平台
// 可以通过消息队列或直接调用IoT平台的API
for (BaseUPRequest request : data) {
try {
// 推送到IoT平台的逻辑
pushToIoTPlatform(request, config);
successCount++;
} catch (Exception e) {
failedCount++;
log.error("推送数据到IoT平台失败: {}", e.getMessage(), e);
}
}
log.debug("处理输入数据完成,配置: {}, 总数: {}, 成功: {}, 失败: {}",
config.getName(), data.size(), successCount, failedCount);
} catch (Exception e) {
failedCount = data.size();
errorMessage = e.getMessage();
log.error("处理输入数据异常,配置: {}, 错误: {}", config.getName(), e.getMessage(), e);
} finally {
long executionTime = System.currentTimeMillis() - startTime;
logInputExecution(config, data.size(), successCount, failedCount, errorMessage, executionTime);
}
}
/**
* 推送数据到IoT平台
*/
private void pushToIoTPlatform(BaseUPRequest request, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug("推送数据到IoT平台: 设备={}, 产品={}",
request.getIoTDeviceDTO() != null ? request.getIoTDeviceDTO().getDeviceId() : "unknown",
config.getTargetResourceId());
}
/**
* 记录输入执行日志
*/
private void logInputExecution(DataBridgeConfig config, int messageCount, int successCount,
int failedCount, String errorMessage, long executionTime) {
try {
DataInputLog log = DataInputLog.builder()
.configId(config.getId())
.configName(config.getName())
.sourceSystem(config.getBridgeType().name())
.messageCount(messageCount)
.successCount(successCount)
.failedCount(failedCount)
.errorMessage(errorMessage)
.executionTime(executionTime)
.status(failedCount > 0 ? DataInputLog.Status.FAILED : DataInputLog.Status.SUCCESS)
.createTime(LocalDateTime.now())
.createBy(config.getCreateBy())
.build();
dataInputLogService.save(log);
} catch (Exception e) {
log.error("记录输入执行日志失败: {}", e.getMessage(), e);
}
}
/**
* 获取调度间隔时间(毫秒)
*/
private long getScheduleInterval(DataBridgeConfig config) {
// 从配置中读取间隔时间,默认60秒
// TODO: 可以从 inputConfig 中解析间隔时间
return 60000; // 60秒
}
}
@@ -26,58 +26,54 @@ import org.apache.ibatis.annotations.Param;
*/
public interface DataBridgeConfigMapper extends BaseMapper<DataBridgeConfig> {
/**
* 根据产品KEY获取活跃配置
*
* @param productKey 产品KEY
* @return 活跃配置列表
*/
List<DataBridgeConfig> selectActiveConfigsByProductKey(@Param("productKey") String productKey);
/**
* 根据产品KEY获取活跃配置
*
* @param productKey 产品KEY
* @return 活跃配置列表
*/
List<DataBridgeConfig> selectActiveConfigsByProductKey(@Param("productKey") String productKey);
/**
* 根据桥接类型获取配置
*
* @param bridgeType 桥接类型
* @return 配置列表
*/
List<DataBridgeConfig> selectConfigsByBridgeType(@Param("bridgeType") String bridgeType);
/**
* 根据桥接类型获取配置
*
* @param bridgeType 桥接类型
* @return 配置列表
*/
List<DataBridgeConfig> selectConfigsByBridgeType(@Param("bridgeType") String bridgeType);
/**
* 根据目标资源ID获取配置
*
* @param targetResourceId 目标资源ID
* @return 配置列表
*/
List<DataBridgeConfig> selectConfigsByTargetResourceId(
@Param("targetResourceId") Long targetResourceId);
/**
* 根据目标资源ID获取配置
*
* @param targetResourceId 目标资源ID
* @return 配置列表
*/
List<DataBridgeConfig> selectConfigsByTargetResourceId(@Param("targetResourceId") Long targetResourceId);
/**
* 批量更新配置状态
*
* @param ids 配置ID列表
* @param status 状态
* @param updateBy 更新者
* @return 更新数量
*/
int batchUpdateStatus(
@Param("ids") List<Long> ids,
@Param("status") Integer status,
@Param("updateBy") String updateBy);
/**
* 批量更新配置状态
*
* @param ids 配置ID列表
* @param status 状态
* @param updateBy 更新者
* @return 更新数量
*/
int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status, @Param("updateBy") String updateBy);
/**
* 根据名称查询配置(用于重名检查)
*
* @param name 配置名称
* @param excludeId 排除的ID(用于更新时检查)
* @return 配置
*/
DataBridgeConfig selectByName(@Param("name") String name, @Param("excludeId") Long excludeId);
/**
* 根据名称查询配置(用于重名检查)
*
* @param name 配置名称
* @param excludeId 排除的ID(用于更新时检查)
* @return 配置
*/
DataBridgeConfig selectByName(@Param("name") String name, @Param("excludeId") Long excludeId);
/**
* 根据创建者查询配置列表
*
* @param createBy 创建者
* @return 配置列表
*/
List<DataBridgeConfig> selectByCreateBy(@Param("createBy") String createBy);
/**
* 根据创建者查询配置列表
*
* @param createBy 创建者
* @return 配置列表
*/
List<DataBridgeConfig> selectByCreateBy(@Param("createBy") String createBy);
}
@@ -26,52 +26,48 @@ import org.apache.ibatis.annotations.Param;
*/
public interface ResourceConnectionMapper extends BaseMapper<ResourceConnection> {
/**
* 根据资源类型获取活跃连接
*
* @param type 资源类型
* @return 活跃连接列表
*/
List<ResourceConnection> selectActiveConnectionsByType(@Param("type") String type);
/**
* 根据资源类型获取活跃连接
*
* @param type 资源类型
* @return 活跃连接列表
*/
List<ResourceConnection> selectActiveConnectionsByType(@Param("type") String type);
/**
* 根据主机和端口查询连接
*
* @param host 主机地址
* @param port 端口号
* @param excludeId 排除的ID(用于更新时检查)
* @return 连接
*/
ResourceConnection selectByHostAndPort(
@Param("host") String host, @Param("port") Integer port, @Param("excludeId") Long excludeId);
/**
* 根据主机和端口查询连接
*
* @param host 主机地址
* @param port 端口号
* @param excludeId 排除的ID(用于更新时检查)
* @return 连接
*/
ResourceConnection selectByHostAndPort(@Param("host") String host, @Param("port") Integer port, @Param("excludeId") Long excludeId);
/**
* 根据名称查询连接(用于重名检查)
*
* @param name 连接名称
* @param excludeId 排除的ID(用于更新时检查)
* @return 连接
*/
ResourceConnection selectByName(@Param("name") String name, @Param("excludeId") Long excludeId);
/**
* 根据名称查询连接(用于重名检查)
*
* @param name 连接名称
* @param excludeId 排除的ID(用于更新时检查)
* @return 连接
*/
ResourceConnection selectByName(@Param("name") String name, @Param("excludeId") Long excludeId);
/**
* 批量更新连接状态
*
* @param ids 连接ID列表
* @param status 状态
* @param updateBy 更新者
* @return 更新数量
*/
int batchUpdateStatus(
@Param("ids") List<Long> ids,
@Param("status") Integer status,
@Param("updateBy") String updateBy);
/**
* 批量更新连接状态
*
* @param ids 连接ID列表
* @param status 状态
* @param updateBy 更新者
* @return 更新数量
*/
int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status, @Param("updateBy") String updateBy);
/**
* 测试连接可用性(通过查询系统表)
*
* @param id 连接ID
* @return 是否可用
*/
int testConnection(@Param("id") Long id);
/**
* 测试连接可用性(通过查询系统表)
*
* @param id 连接ID
* @return 是否可用
*/
int testConnection(@Param("id") Long id);
}
@@ -27,6 +27,7 @@ import jakarta.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -89,6 +90,7 @@ public class ResourceConnectionService {
/** 更新资源连接 */
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = {"data_bridage_resource_connection"},allEntries = true)
public void updateResourceConnection(ResourceConnection connection) {
// 1. 验证连接(使用ConfigValidator进行基本验证)
ConfigValidator.validateResourceConnection(connection);
@@ -171,6 +173,7 @@ public class ResourceConnectionService {
/** 删除连接 */
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = {"data_bridage_resource_connection"},allEntries = true)
public void deleteConnection(Long id) {
// 检查是否被桥接配置使用
List<DataBridgeConfig> configs = dataBridgeConfigService.getConfigsByTargetResourceId(id);
@@ -30,162 +30,170 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConfigValidator {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = new ObjectMapper();
// 名称正则表达式:字母、数字、下划线、中划线,长度1-100
private static final Pattern NAME_PATTERN =
Pattern.compile("^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{1,100}$");
// 名称正则表达式:字母、数字、下划线、中划线,长度1-100
private static final Pattern NAME_PATTERN = Pattern.compile("^[\\u4e00-\\u9fa5a-zA-Z0-9_-]{1,100}$");
// 产品KEY正则表达式:字母、数字、下划线,长度1-50
private static final Pattern PRODUCT_KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,50}$");
// 产品KEY正则表达式:字母、数字、下划线,长度1-50
private static final Pattern PRODUCT_KEY_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,50}$");
// 主机地址正则表达式
private static final Pattern HOST_PATTERN = Pattern.compile("^[a-zA-Z0-9.-]{1,255}$");
// 主机地址正则表达式
private static final Pattern HOST_PATTERN = Pattern.compile("^[a-zA-Z0-9.-]{1,255}$");
// 数据库名正则表达式:字母、数字、下划线,长度1-64
private static final Pattern DATABASE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,64}$");
// 数据库名正则表达式:字母、数字、下划线,长度1-64
private static final Pattern DATABASE_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,64}$");
/** 验证桥接配置 */
public static void validateDataBridgeConfig(DataBridgeConfig config) {
if (config == null) {
throw new DataBridgeException("CONFIG_NULL", "桥接配置不能为空");
/**
* 验证桥接配置
*/
public static void validateDataBridgeConfig(DataBridgeConfig config) {
if (config == null) {
throw new DataBridgeException("CONFIG_NULL", "桥接配置不能为空");
}
// 验证名称
validateName(config.getName(), "桥接配置名称");
// 验证源范围
if (config.getSourceScope() == null) {
throw new DataBridgeException("INVALID_SOURCE_SCOPE", "源范围不能为空");
}
// 验证目标资源ID
if (config.getTargetResourceId() == null || config.getTargetResourceId() <= 0) {
throw new DataBridgeException("INVALID_TARGET_RESOURCE_ID", "目标资源ID无效");
}
// 验证桥接类型
if (config.getBridgeType() == null) {
throw new DataBridgeException("INVALID_BRIDGE_TYPE", "桥接类型不能为空");
}
// 验证模板
if (config.getTemplate() == null || config.getTemplate().trim().isEmpty()) {
throw new DataBridgeException("INVALID_TEMPLATE", "模板内容不能为空");
}
// 验证统一配置JSON格式
if (config.getConfig() != null && !config.getConfig().trim().isEmpty()) {
validateJsonFormat(config.getConfig(), "统一配置");
}
// 验证Magic脚本
if (config.getMagicScript() != null && !config.getMagicScript().trim().isEmpty()) {
// Magic脚本内容验证可以在这里添加
}
// 验证状态
if (config.getStatus() != null && config.getStatus() != 0 && config.getStatus() != 1) {
throw new DataBridgeException("INVALID_STATUS", "状态值无效,只能为0或1");
}
// 验证描述长度
if (config.getDescription() != null && config.getDescription().length() > 500) {
throw new DataBridgeException("INVALID_DESCRIPTION", "描述长度不能超过500个字符");
}
}
// 验证名称
validateName(config.getName(), "桥接配置名称");
/**
* 验证资源连接
*/
public static void validateResourceConnection(ResourceConnection connection) {
if (connection == null) {
throw new DataBridgeException("CONNECTION_NULL", "资源连接不能为空");
}
// 验证源范围
if (config.getSourceScope() == null) {
throw new DataBridgeException("INVALID_SOURCE_SCOPE", "源范围不能为空");
// 验证名称
validateName(connection.getName(), "资源连接名称");
// 验证资源类型
if (connection.getType() == null) {
throw new DataBridgeException("INVALID_RESOURCE_TYPE", "资源类型不能为空");
}
// 验证数据流向
if (connection.getDataDirection() == null) {
throw new DataBridgeException("INVALID_DATA_DIRECTION", "数据流向不能为空");
}
// 验证扩展配置JSON格式(包含动态配置)
if (connection.getExtraConfig() != null && !connection.getExtraConfig().trim().isEmpty()) {
validateJsonFormat(connection.getExtraConfig(), "扩展配置");
}
// 验证状态
if (connection.getStatus() != null && connection.getStatus() != 0 && connection.getStatus() != 1) {
throw new DataBridgeException("INVALID_STATUS", "状态值无效,只能为0或1");
}
// 验证描述长度
if (connection.getDescription() != null && connection.getDescription().length() > 500) {
throw new DataBridgeException("INVALID_DESCRIPTION", "描述长度不能超过500个字符");
}
// 验证方向
if (connection.getDirection() == null) {
throw new DataBridgeException("INVALID_DIRECTION", "连接方向不能为空");
}
}
// 验证目标资源ID
if (config.getTargetResourceId() == null || config.getTargetResourceId() <= 0) {
throw new DataBridgeException("INVALID_TARGET_RESOURCE_ID", "目标资源ID无效");
/**
* 验证名称
*/
private static void validateName(String name, String fieldName) {
if (name == null || name.trim().isEmpty()) {
throw new DataBridgeException("INVALID_NAME", fieldName + "不能为空");
}
if (!NAME_PATTERN.matcher(name).matches()) {
throw new DataBridgeException("INVALID_NAME_FORMAT", fieldName + "格式无效,只能包含中文、字母、数字、下划线、中划线,长度1-100");
}
}
// 验证桥接类型
if (config.getBridgeType() == null) {
throw new DataBridgeException("INVALID_BRIDGE_TYPE", "桥接类型不能为空");
/**
* 验证产品KEY
*/
private static void validateProductKey(String productKey) {
if (productKey == null || productKey.trim().isEmpty()) {
throw new DataBridgeException("INVALID_PRODUCT_KEY", "产品KEY不能为空");
}
if (!PRODUCT_KEY_PATTERN.matcher(productKey).matches()) {
throw new DataBridgeException("INVALID_PRODUCT_KEY_FORMAT", "产品KEY格式无效,只能包含字母、数字、下划线,长度1-50");
}
}
// 验证模板
if (config.getTemplate() == null || config.getTemplate().trim().isEmpty()) {
throw new DataBridgeException("INVALID_TEMPLATE", "模板内容不能为空");
/**
* 验证主机地址
*/
private static void validateHost(String host) {
if (host == null || host.trim().isEmpty()) {
throw new DataBridgeException("INVALID_HOST", "主机地址不能为空");
}
if (!HOST_PATTERN.matcher(host).matches()) {
throw new DataBridgeException("INVALID_HOST_FORMAT", "主机地址格式无效");
}
}
// 验证统一配置JSON格式
if (config.getConfig() != null && !config.getConfig().trim().isEmpty()) {
validateJsonFormat(config.getConfig(), "统一配置");
/**
* 验证数据库名
*/
private static void validateDatabaseName(String databaseName) {
if (databaseName == null || databaseName.trim().isEmpty()) {
throw new DataBridgeException("INVALID_DATABASE_NAME", "数据库名不能为空");
}
if (!DATABASE_NAME_PATTERN.matcher(databaseName).matches()) {
throw new DataBridgeException("INVALID_DATABASE_NAME_FORMAT", "数据库名格式无效,只能包含字母、数字、下划线,长度1-64");
}
}
// 验证Magic脚本
if (config.getMagicScript() != null && !config.getMagicScript().trim().isEmpty()) {
// Magic脚本内容验证可以在这里添加
/**
* 验证JSON格式
*/
private static void validateJsonFormat(String json, String fieldName) {
try {
objectMapper.readTree(json);
} catch (JsonProcessingException e) {
throw new DataBridgeException("INVALID_JSON_FORMAT", fieldName + "JSON格式无效: " + e.getMessage());
}
}
// 验证状态
if (config.getStatus() != null && config.getStatus() != 0 && config.getStatus() != 1) {
throw new DataBridgeException("INVALID_STATUS", "状态值无效,只能为0或1");
}
// 验证描述长度
if (config.getDescription() != null && config.getDescription().length() > 500) {
throw new DataBridgeException("INVALID_DESCRIPTION", "描述长度不能超过500个字符");
}
}
/** 验证资源连接 */
public static void validateResourceConnection(ResourceConnection connection) {
if (connection == null) {
throw new DataBridgeException("CONNECTION_NULL", "资源连接不能为空");
}
// 验证名称
validateName(connection.getName(), "资源连接名称");
// 验证资源类型
if (connection.getType() == null) {
throw new DataBridgeException("INVALID_RESOURCE_TYPE", "资源类型不能为空");
}
// 验证数据流向
if (connection.getDataDirection() == null) {
throw new DataBridgeException("INVALID_DATA_DIRECTION", "数据流向不能为空");
}
// 验证扩展配置JSON格式(包含动态配置)
if (connection.getExtraConfig() != null && !connection.getExtraConfig().trim().isEmpty()) {
validateJsonFormat(connection.getExtraConfig(), "扩展配置");
}
// 验证状态
if (connection.getStatus() != null
&& connection.getStatus() != 0
&& connection.getStatus() != 1) {
throw new DataBridgeException("INVALID_STATUS", "状态值无效,只能为0或1");
}
// 验证描述长度
if (connection.getDescription() != null && connection.getDescription().length() > 500) {
throw new DataBridgeException("INVALID_DESCRIPTION", "描述长度不能超过500个字符");
}
// 验证方向
if (connection.getDirection() == null) {
throw new DataBridgeException("INVALID_DIRECTION", "连接方向不能为空");
}
}
/** 验证名称 */
private static void validateName(String name, String fieldName) {
if (name == null || name.trim().isEmpty()) {
throw new DataBridgeException("INVALID_NAME", fieldName + "不能为空");
}
if (!NAME_PATTERN.matcher(name).matches()) {
throw new DataBridgeException(
"INVALID_NAME_FORMAT", fieldName + "格式无效,只能包含中文、字母、数字、下划线、中划线,长度1-100");
}
}
/** 验证产品KEY */
private static void validateProductKey(String productKey) {
if (productKey == null || productKey.trim().isEmpty()) {
throw new DataBridgeException("INVALID_PRODUCT_KEY", "产品KEY不能为空");
}
if (!PRODUCT_KEY_PATTERN.matcher(productKey).matches()) {
throw new DataBridgeException("INVALID_PRODUCT_KEY_FORMAT", "产品KEY格式无效,只能包含字母、数字、下划线,长度1-50");
}
}
/** 验证主机地址 */
private static void validateHost(String host) {
if (host == null || host.trim().isEmpty()) {
throw new DataBridgeException("INVALID_HOST", "主机地址不能为空");
}
if (!HOST_PATTERN.matcher(host).matches()) {
throw new DataBridgeException("INVALID_HOST_FORMAT", "主机地址格式无效");
}
}
/** 验证数据库名 */
private static void validateDatabaseName(String databaseName) {
if (databaseName == null || databaseName.trim().isEmpty()) {
throw new DataBridgeException("INVALID_DATABASE_NAME", "数据库名不能为空");
}
if (!DATABASE_NAME_PATTERN.matcher(databaseName).matches()) {
throw new DataBridgeException(
"INVALID_DATABASE_NAME_FORMAT", "数据库名格式无效,只能包含字母、数字、下划线,长度1-64");
}
}
/** 验证JSON格式 */
private static void validateJsonFormat(String json, String fieldName) {
try {
objectMapper.readTree(json);
} catch (JsonProcessingException e) {
throw new DataBridgeException(
"INVALID_JSON_FORMAT", fieldName + "JSON格式无效: " + e.getMessage());
}
}
}
@@ -59,6 +59,14 @@ public class ConnectionTester {
switch (connection.getType()) {
case MYSQL:
return testMySQLConnection(connection);
case POSTGRESQL:
return testPostgreSQLConnection(connection);
case H2:
return testH2Connection(connection);
case ORACLE:
return testOracleConnection(connection);
case SQLSERVER:
return testSQLServerConnection(connection);
case KAFKA:
return testKafkaConnection(connection);
case MQTT:
@@ -112,6 +120,92 @@ public class ConnectionTester {
}
}
/** 测试PostgreSQL连接 */
private ConnectionTestResult testPostgreSQLConnection(ResourceConnection connection) {
try {
String jdbcUrl =
String.format(
"jdbc:postgresql://%s:%d/%s",
connection.getHost(), connection.getPort(), connection.getDatabaseName());
try (SimpleDataSource dataSource =
new SimpleDataSource(jdbcUrl, connection.getUsername(), connection.getPassword())) {
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().executeQuery("SELECT 1");
return ConnectionTestResult.success("PostgreSQL连接测试成功");
}
}
} catch (SQLException e) {
log.error("PostgreSQL连接测试失败", e);
return ConnectionTestResult.failure("PostgreSQL连接失败: " + e.getMessage());
}
}
/** 测试H2连接 */
private ConnectionTestResult testH2Connection(ResourceConnection connection) {
try {
String jdbcUrl =
String.format(
"jdbc:h2:tcp://%s:%d/%s",
connection.getHost(), connection.getPort(), connection.getDatabaseName());
try (SimpleDataSource dataSource =
new SimpleDataSource(jdbcUrl, connection.getUsername(), connection.getPassword())) {
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().executeQuery("SELECT 1");
return ConnectionTestResult.success("H2连接测试成功");
}
}
} catch (SQLException e) {
log.error("H2连接测试失败", e);
return ConnectionTestResult.failure("H2连接失败: " + e.getMessage());
}
}
/** 测试Oracle连接 */
private ConnectionTestResult testOracleConnection(ResourceConnection connection) {
try {
// Oracle使用SID或Service Name
String jdbcUrl =
String.format(
"jdbc:oracle:thin:@%s:%d:%s",
connection.getHost(), connection.getPort(), connection.getDatabaseName());
try (SimpleDataSource dataSource =
new SimpleDataSource(jdbcUrl, connection.getUsername(), connection.getPassword())) {
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().executeQuery("SELECT 1 FROM DUAL");
return ConnectionTestResult.success("Oracle连接测试成功");
}
}
} catch (SQLException e) {
log.error("Oracle连接测试失败", e);
return ConnectionTestResult.failure("Oracle连接失败: " + e.getMessage());
}
}
/** 测试SQL Server连接 */
private ConnectionTestResult testSQLServerConnection(ResourceConnection connection) {
try {
// SQL Server连接URL,添加encrypt=false和trustServerCertificate=true以避免SSL证书验证问题
String jdbcUrl =
String.format(
"jdbc:sqlserver://%s:%d;databaseName=%s;encrypt=false;trustServerCertificate=true;characterEncoding=UTF-8",
connection.getHost(), connection.getPort(), connection.getDatabaseName());
try (SimpleDataSource dataSource =
new SimpleDataSource(jdbcUrl, connection.getUsername(), connection.getPassword())) {
try (Connection conn = dataSource.getConnection()) {
conn.createStatement().executeQuery("SELECT 1");
return ConnectionTestResult.success("SQL Server连接测试成功");
}
}
} catch (SQLException e) {
log.error("SQL Server连接测试失败", e);
return ConnectionTestResult.failure("SQL Server连接失败: " + e.getMessage());
}
}
/** 测试Kafka连接 */
private ConnectionTestResult testKafkaConnection(ResourceConnection connection) {
Properties props = new Properties();
@@ -23,7 +23,8 @@ import javax.sql.DataSource;
import lombok.extern.slf4j.Slf4j;
/**
* 数据桥接连接池管理器 独立管理数据桥接的数据库连接池,避免与框架的tk.mybatis冲突
* 数据桥接连接池管理器
* 独立管理数据桥接的数据库连接池,避免与框架的tk.mybatis冲突
*
* @author gitee.com/NexIoT
* @version 1.0
@@ -32,384 +33,500 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DataBridgeConnectionManager {
// 连接池缓存 - 使用独立的命名空间避免冲突
private final ConcurrentMap<String, DataSource> connectionPoolCache = new ConcurrentHashMap<>();
// 保活调度器
private final ScheduledExecutorService keepAliveScheduler = Executors.newScheduledThreadPool(1);
// 连接池配置 - 针对数据桥接优化
private static final int MAX_POOL_SIZE = 3; // 数据桥接不需要太多连接
private static final int MIN_IDLE = 1;
private static final long CONNECTION_TIMEOUT = 20000L; // 20秒连接超时
private static final long IDLE_TIMEOUT = 300000L; // 5分钟空闲超时
private static final long MAX_LIFETIME = 1800000L; // 30分钟最大生命周期
private static final long VALIDATION_TIMEOUT = 2000L; // 2秒验证超时
// 保活配置
private static final long KEEP_ALIVE_INTERVAL = 300000L; // 5分钟保活间隔
public DataBridgeConnectionManager() {
// 启动保活任务
startKeepAliveTask();
}
/**
* 获取或创建数据源
*
* @param connection 连接配置
* @return 数据源
*/
public DataSource getOrCreateDataSource(ResourceConnection connection) {
String cacheKey = generateCacheKey(connection);
// 先检查缓存中是否已有连接池
DataSource existingDataSource = connectionPoolCache.get(cacheKey);
if (existingDataSource != null) {
// 检查连接池是否健康
if (isDataSourceHealthy(existingDataSource)) {
log.debug("复用现有数据桥接连接池: {}", cacheKey);
return existingDataSource;
} else {
log.warn("连接池不健康,移除并重新创建: {}", cacheKey);
removeDataSource(connection);
}
// 连接池缓存 - 使用独立的命名空间避免冲突
private final ConcurrentMap<String, DataSource> connectionPoolCache = new ConcurrentHashMap<>();
// 保活调度器
private final ScheduledExecutorService keepAliveScheduler = Executors.newScheduledThreadPool(1);
// 连接池配置 - 针对数据桥接优化
private static final int MAX_POOL_SIZE = 3; // 数据桥接不需要太多连接
private static final int MIN_IDLE = 1;
private static final long CONNECTION_TIMEOUT = 20000L; // 20秒连接超时
private static final long IDLE_TIMEOUT = 300000L; // 5分钟空闲超时
private static final long MAX_LIFETIME = 1800000L; // 30分钟最大生命周期
private static final long VALIDATION_TIMEOUT = 2000L; // 2秒验证超时
// 保活配置
private static final long KEEP_ALIVE_INTERVAL = 300000L; // 5分钟保活间隔
public DataBridgeConnectionManager() {
// 启动保活任务
startKeepAliveTask();
}
// 创建新的连接池
return connectionPoolCache.computeIfAbsent(
cacheKey,
key -> {
log.info("创建数据桥接连接池: {}", key);
return createDataBridgeDataSource(connection);
});
}
/**
* 创建数据桥接专用的数据源
*
* @param connection 连接配置
* @return 数据源
*/
private DataSource createDataBridgeDataSource(ResourceConnection connection) {
try {
log.info("创建数据桥接连接池: {}:{}", connection.getHost(), connection.getPort());
// 测试网络连接
testNetworkConnection(connection);
HikariConfig config = new HikariConfig();
// 基本连接信息
String driverClassName = getDriverClassName(connection.getType().name());
String jdbcUrl = buildConnectionUrl(connection);
config.setDriverClassName(driverClassName);
config.setJdbcUrl(jdbcUrl);
config.setUsername(connection.getUsername());
config.setPassword(connection.getPassword());
// 数据桥接专用连接池配置
config.setMaximumPoolSize(MAX_POOL_SIZE);
config.setMinimumIdle(MIN_IDLE);
config.setConnectionTimeout(CONNECTION_TIMEOUT);
config.setIdleTimeout(IDLE_TIMEOUT);
config.setMaxLifetime(MAX_LIFETIME);
config.setValidationTimeout(VALIDATION_TIMEOUT);
// 连接池名称 - 使用独立前缀避免冲突
config.setPoolName("DataBridge-" + connection.getId());
// 连接测试配置
config.setConnectionTestQuery("SELECT 1");
config.setConnectionInitSql("SET SESSION wait_timeout=28800");
// 数据桥接专用配置
config.setInitializationFailTimeout(15000); // 15秒初始化超时
config.setRegisterMbeans(false); // 关闭JMX监控,避免冲突
config.setLeakDetectionThreshold(30000); // 30秒连接泄漏检测
config.setAutoCommit(true);
config.setKeepaliveTime(30000); // 30秒保活
// MySQL连接属性 - 数据桥接优化
if ("MYSQL".equalsIgnoreCase(connection.getType().name())) {
addMySQLProperties(config);
}
// 创建数据源
HikariDataSource dataSource = new HikariDataSource(config);
// 测试连接
testDataSource(dataSource);
log.info("数据桥接连接池创建成功");
return dataSource;
} catch (Exception e) {
log.error(
"创建数据桥接连接池失败: {}:{} - {}", connection.getHost(), connection.getPort(), e.getMessage());
// 根据错误类型提供解决建议
if (e.getMessage().contains("Access denied")) {
log.error(
"解决建议: 检查MySQL用户权限,确保用户 '{}' 有访问数据库 '{}' 的权限",
connection.getUsername(),
getDatabaseName(connection));
} else if (e.getMessage().contains("Could not create connection")) {
log.error("解决建议: 检查网络连接和MySQL服务,确保端口 {} 可访问", connection.getPort());
}
throw new RuntimeException("数据桥接连接池创建失败: " + e.getMessage(), e);
}
}
/**
* 添加MySQL特定属性
*
* @param config HikariCP配置
*/
private void addMySQLProperties(HikariConfig config) {
config.addDataSourceProperty("useSSL", "false");
config.addDataSourceProperty("allowPublicKeyRetrieval", "true");
config.addDataSourceProperty("autoReconnect", "true");
config.addDataSourceProperty("failOverReadOnly", "false");
config.addDataSourceProperty("maxReconnects", "2"); // 减少重连次数
config.addDataSourceProperty("initialTimeout", "1");
config.addDataSourceProperty("connectTimeout", "20000");
config.addDataSourceProperty("socketTimeout", "20000");
config.addDataSourceProperty("useLocalSessionState", "true");
config.addDataSourceProperty("rewriteBatchedStatements", "true");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("useServerPrepStmts", "true");
config.addDataSourceProperty("maintainTimeStats", "false");
config.addDataSourceProperty("elideSetAutoCommits", "true");
config.addDataSourceProperty("useLocalTransactionState", "true");
config.addDataSourceProperty("alwaysSendSetIsolation", "false");
config.addDataSourceProperty("cacheCallableStmts", "true");
config.addDataSourceProperty("cacheResultSetMetadata", "true");
config.addDataSourceProperty("cacheServerConfiguration", "true");
}
/**
* 测试网络连接
*
* @param connection 连接配置
*/
private void testNetworkConnection(ResourceConnection connection) {
try {
java.net.Socket socket = new java.net.Socket();
socket.connect(
new java.net.InetSocketAddress(connection.getHost(), connection.getPort()), 5000);
socket.close();
} catch (Exception e) {
log.error("网络连接测试失败: {}:{} - {}", connection.getHost(), connection.getPort(), e.getMessage());
throw new RuntimeException("无法连接到数据库服务器: " + e.getMessage(), e);
}
}
/**
* 测试数据源连接
*
* @param dataSource 数据源
*/
private void testDataSource(DataSource dataSource) {
try (java.sql.Connection conn = dataSource.getConnection()) {
if (!conn.isValid(2)) {
throw new RuntimeException("连接无效");
}
} catch (Exception e) {
log.error("数据桥接连接测试失败: {}", e.getMessage());
throw new RuntimeException("数据桥接连接测试失败: " + e.getMessage(), e);
}
}
/**
* 检查数据源是否健康
*
* @param dataSource 数据源
* @return 是否健康
*/
private boolean isDataSourceHealthy(DataSource dataSource) {
try {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
// 检查连接池是否已关闭
if (hikariDataSource.isClosed()) {
return false;
}
// 检查连接池状态
try (java.sql.Connection conn = hikariDataSource.getConnection()) {
return conn.isValid(2);
}
}
// 对于非HikariCP数据源,尝试获取连接
try (java.sql.Connection conn = dataSource.getConnection()) {
return conn.isValid(2);
}
} catch (Exception e) {
return false;
}
}
/**
* 构建连接URL
*
* @param connection 连接配置
* @return 连接URL
*/
private String buildConnectionUrl(ResourceConnection connection) {
return String.format(
"jdbc:mysql://%s:%d/%s?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=2&initialTimeout=1&connectTimeout=20000&socketTimeout=20000&useLocalSessionState=true&rewriteBatchedStatements=true&cachePrepStmts=true&useServerPrepStmts=true&maintainTimeStats=false&elideSetAutoCommits=true&useLocalTransactionState=true&alwaysSendSetIsolation=false&cacheCallableStmts=true&cacheResultSetMetadata=true&cacheServerConfiguration=true",
connection.getHost(), connection.getPort(), getDatabaseName(connection));
}
/**
* 获取数据库名称
*
* @param connection 连接配置
* @return 数据库名称
*/
private String getDatabaseName(ResourceConnection connection) {
// 使用数据库名称字段,如果为空则使用默认值
String databaseName = connection.getDatabaseName();
return databaseName != null && !databaseName.trim().isEmpty() ? databaseName : "mysql";
}
/**
* 获取驱动类名
*
* @param dbType 数据库类型
* @return 驱动类名
*/
private String getDriverClassName(String dbType) {
return switch (dbType.toUpperCase()) {
case "MYSQL" -> "com.mysql.cj.jdbc.Driver";
case "POSTGRESQL" -> "org.postgresql.Driver";
case "ORACLE" -> "oracle.jdbc.driver.OracleDriver";
case "SQLSERVER" -> "com.microsoft.sqlserver.jdbc.SQLServerDriver";
default -> throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
};
}
/**
* 生成缓存键 - 使用独立前缀避免冲突
*
* @param connection 连接配置
* @return 缓存键
*/
private String generateCacheKey(ResourceConnection connection) {
return String.format(
"DataBridge_%s_%s_%s_%s_%s",
connection.getType().name(),
connection.getHost(),
connection.getPort(),
connection.getUsername(),
getDatabaseName(connection));
}
/**
* 移除指定连接池
*
* @param connection 连接配置
*/
public void removeDataSource(ResourceConnection connection) {
String cacheKey = generateCacheKey(connection);
DataSource dataSource = connectionPoolCache.remove(cacheKey);
if (dataSource instanceof HikariDataSource) {
try {
((HikariDataSource) dataSource).close();
log.info("已关闭数据桥接连接池: {}", cacheKey);
} catch (Exception e) {
log.warn("关闭数据桥接连接池失败: {} - {}", cacheKey, e.getMessage());
}
}
}
/** 启动保活任务 */
private void startKeepAliveTask() {
keepAliveScheduler.scheduleWithFixedDelay(
() -> {
try {
connectionPoolCache.forEach(
(key, dataSource) -> {
if (!isDataSourceHealthy(dataSource)) {
log.warn("检测到不健康的连接池: {}", key);
}
});
} catch (Exception e) {
log.error("连接池保活检查失败: {}", e.getMessage());
}
},
KEEP_ALIVE_INTERVAL,
KEEP_ALIVE_INTERVAL,
TimeUnit.MILLISECONDS);
log.info("数据桥接连接池保活任务已启动,检查间隔: {}秒", KEEP_ALIVE_INTERVAL / 1000);
}
/** 关闭所有连接池 */
public void shutdown() {
log.info("开始关闭所有数据桥接连接池...");
// 关闭保活调度器
keepAliveScheduler.shutdown();
try {
if (!keepAliveScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
keepAliveScheduler.shutdownNow();
}
} catch (InterruptedException e) {
keepAliveScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// 关闭所有连接池
connectionPoolCache.forEach(
(key, dataSource) -> {
try {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
/**
* 获取或创建数据源
*
* @param connection 连接配置
* @return 数据源
*/
public DataSource getOrCreateDataSource(ResourceConnection connection) {
String cacheKey = generateCacheKey(connection);
// 先检查缓存中是否已有连接池
DataSource existingDataSource = connectionPoolCache.get(cacheKey);
if (existingDataSource != null) {
// 检查连接池是否健康
if (isDataSourceHealthy(existingDataSource)) {
log.debug("复用现有数据桥接连接池: {}", cacheKey);
return existingDataSource;
} else {
log.warn("连接池不健康,移除并重新创建: {}", cacheKey);
removeDataSource(connection);
}
} catch (Exception e) {
log.warn("关闭数据桥接连接池失败: {} - {}", key, e.getMessage());
}
}
// 创建新的连接池
return connectionPoolCache.computeIfAbsent(cacheKey, key -> {
log.info("创建数据桥接连接池: {}", key);
return createDataBridgeDataSource(connection);
});
connectionPoolCache.clear();
log.info("所有数据桥接连接池已关闭");
}
}
/**
* 获取连接池状态
*
* @return 连接池状态信息
*/
public String getPoolStatus() {
StringBuilder status = new StringBuilder();
status.append("数据桥接连接池状态:\n");
connectionPoolCache.forEach(
(key, dataSource) -> {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
status.append(
String.format(
" %s: 活跃=%d, 空闲=%d, 等待=%d\n",
key,
hikariDataSource.getHikariPoolMXBean().getActiveConnections(),
hikariDataSource.getHikariPoolMXBean().getIdleConnections(),
hikariDataSource.getHikariPoolMXBean().getThreadsAwaitingConnection()));
}
/**
* 创建数据桥接专用的数据源
*
* @param connection 连接配置
* @return 数据源
*/
private DataSource createDataBridgeDataSource(ResourceConnection connection) {
try {
log.info("创建数据桥接连接池: {}:{}", connection.getHost(), connection.getPort());
// 测试网络连接
testNetworkConnection(connection);
HikariConfig config = new HikariConfig();
// 基本连接信息
String driverClassName = getDriverClassName(connection.getType().name());
String jdbcUrl = buildConnectionUrl(connection);
config.setDriverClassName(driverClassName);
config.setJdbcUrl(jdbcUrl);
config.setUsername(connection.getUsername());
config.setPassword(connection.getPassword());
// 数据桥接专用连接池配置
config.setMaximumPoolSize(MAX_POOL_SIZE);
config.setMinimumIdle(MIN_IDLE);
config.setConnectionTimeout(CONNECTION_TIMEOUT);
config.setIdleTimeout(IDLE_TIMEOUT);
config.setMaxLifetime(MAX_LIFETIME);
config.setValidationTimeout(VALIDATION_TIMEOUT);
// 连接池名称 - 使用独立前缀避免冲突
config.setPoolName("DataBridge-" + connection.getId());
// 连接测试配置 - 根据数据库类型设置
String dbType = connection.getType().name().toUpperCase();
config.setConnectionTestQuery(getConnectionTestQuery(dbType));
String initSql = getConnectionInitSql(dbType);
if (initSql != null && !initSql.isEmpty()) {
config.setConnectionInitSql(initSql);
}
// 数据桥接专用配置
config.setInitializationFailTimeout(15000); // 15秒初始化超时
config.setRegisterMbeans(false); // 关闭JMX监控,避免冲突
config.setLeakDetectionThreshold(30000); // 30秒连接泄漏检测
config.setAutoCommit(true);
config.setKeepaliveTime(30000); // 30秒保活
// 数据库特定属性配置
addDatabaseSpecificProperties(config, dbType);
// 创建数据源
HikariDataSource dataSource = new HikariDataSource(config);
// 测试连接
testDataSource(dataSource);
log.info("数据桥接连接池创建成功");
return dataSource;
} catch (Exception e) {
log.error("创建数据桥接连接池失败: {}:{} - {}",
connection.getHost(), connection.getPort(), e.getMessage());
// 根据错误类型提供解决建议
if (e.getMessage().contains("Access denied")) {
log.error("解决建议: 检查MySQL用户权限,确保用户 '{}' 有访问数据库 '{}' 的权限",
connection.getUsername(), getDatabaseName(connection));
} else if (e.getMessage().contains("Could not create connection")) {
log.error("解决建议: 检查网络连接和MySQL服务,确保端口 {} 可访问", connection.getPort());
}
throw new RuntimeException("数据桥接连接池创建失败: " + e.getMessage(), e);
}
}
/**
* 添加MySQL特定属性
*
* @param config HikariCP配置
*/
private void addMySQLProperties(HikariConfig config) {
config.addDataSourceProperty("useSSL", "false");
config.addDataSourceProperty("allowPublicKeyRetrieval", "true");
config.addDataSourceProperty("autoReconnect", "true");
config.addDataSourceProperty("failOverReadOnly", "false");
config.addDataSourceProperty("maxReconnects", "2"); // 减少重连次数
config.addDataSourceProperty("initialTimeout", "1");
config.addDataSourceProperty("connectTimeout", "20000");
config.addDataSourceProperty("socketTimeout", "20000");
config.addDataSourceProperty("useLocalSessionState", "true");
config.addDataSourceProperty("rewriteBatchedStatements", "true");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("useServerPrepStmts", "true");
config.addDataSourceProperty("maintainTimeStats", "false");
config.addDataSourceProperty("elideSetAutoCommits", "true");
config.addDataSourceProperty("useLocalTransactionState", "true");
config.addDataSourceProperty("alwaysSendSetIsolation", "false");
config.addDataSourceProperty("cacheCallableStmts", "true");
config.addDataSourceProperty("cacheResultSetMetadata", "true");
config.addDataSourceProperty("cacheServerConfiguration", "true");
}
/**
* 添加PostgreSQL特定属性
*
* @param config HikariCP配置
*/
private void addPostgreSQLProperties(HikariConfig config) {
config.addDataSourceProperty("tcpKeepAlive", "true");
config.addDataSourceProperty("socketTimeout", "20");
config.addDataSourceProperty("loginTimeout", "20");
}
/**
* 添加Oracle特定属性
*
* @param config HikariCP配置
*/
private void addOracleProperties(HikariConfig config) {
config.addDataSourceProperty("oracle.net.CONNECT_TIMEOUT", "20000");
config.addDataSourceProperty("oracle.jdbc.ReadTimeout", "20000");
}
/**
* 添加SQL Server特定属性
*
* @param config HikariCP配置
*/
private void addSQLServerProperties(HikariConfig config) {
config.addDataSourceProperty("loginTimeout", "20");
config.addDataSourceProperty("socketTimeout", "20000");
config.addDataSourceProperty("encrypt", "false");
config.addDataSourceProperty("trustServerCertificate", "true");
}
/**
* 添加H2特定属性
*
* @param config HikariCP配置
*/
private void addH2Properties(HikariConfig config) {
// H2数据库通常不需要特殊配置
config.addDataSourceProperty("AUTO_SERVER", "true");
}
/**
* 测试网络连接
*
* @param connection 连接配置
*/
private void testNetworkConnection(ResourceConnection connection) {
try {
java.net.Socket socket = new java.net.Socket();
socket.connect(new java.net.InetSocketAddress(connection.getHost(), connection.getPort()), 5000);
socket.close();
} catch (Exception e) {
log.error("网络连接测试失败: {}:{} - {}", connection.getHost(), connection.getPort(), e.getMessage());
throw new RuntimeException("无法连接到数据库服务器: " + e.getMessage(), e);
}
}
/**
* 测试数据源连接
*
* @param dataSource 数据源
*/
private void testDataSource(DataSource dataSource) {
try (java.sql.Connection conn = dataSource.getConnection()) {
if (!conn.isValid(2)) {
throw new RuntimeException("连接无效");
}
} catch (Exception e) {
log.error("数据桥接连接测试失败: {}", e.getMessage());
throw new RuntimeException("数据桥接连接测试失败: " + e.getMessage(), e);
}
}
/**
* 检查数据源是否健康
*
* @param dataSource 数据源
* @return 是否健康
*/
private boolean isDataSourceHealthy(DataSource dataSource) {
try {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
// 检查连接池是否已关闭
if (hikariDataSource.isClosed()) {
return false;
}
// 检查连接池状态
try (java.sql.Connection conn = hikariDataSource.getConnection()) {
return conn.isValid(2);
}
}
// 对于非HikariCP数据源,尝试获取连接
try (java.sql.Connection conn = dataSource.getConnection()) {
return conn.isValid(2);
}
} catch (Exception e) {
return false;
}
}
/**
* 构建连接URL
*
* @param connection 连接配置
* @return 连接URL
*/
private String buildConnectionUrl(ResourceConnection connection) {
String dbType = connection.getType().name().toUpperCase();
String host = connection.getHost();
Integer port = connection.getPort();
String databaseName = getDatabaseName(connection);
return switch (dbType) {
case "MYSQL" -> String.format(
"jdbc:mysql://%s:%d/%s?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=2&initialTimeout=1&connectTimeout=20000&socketTimeout=20000&useLocalSessionState=true&rewriteBatchedStatements=true&cachePrepStmts=true&useServerPrepStmts=true&maintainTimeStats=false&elideSetAutoCommits=true&useLocalTransactionState=true&alwaysSendSetIsolation=false&cacheCallableStmts=true&cacheResultSetMetadata=true&cacheServerConfiguration=true",
host, port, databaseName);
case "POSTGRESQL" -> String.format("jdbc:postgresql://%s:%d/%s", host, port, databaseName);
case "H2" -> String.format("jdbc:h2:tcp://%s:%d/%s", host, port, databaseName);
case "ORACLE" -> String.format("jdbc:oracle:thin:@%s:%d:%s", host, port, databaseName);
case "SQLSERVER" -> String.format("jdbc:sqlserver://%s:%d;databaseName=%s;characterEncoding=UTF-8", host, port, databaseName);
default -> throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
};
}
/**
* 获取数据库名称
*
* @param connection 连接配置
* @return 数据库名称
*/
private String getDatabaseName(ResourceConnection connection) {
// 使用数据库名称字段,如果为空则根据数据库类型使用默认值
String databaseName = connection.getDatabaseName();
if (databaseName != null && !databaseName.trim().isEmpty()) {
return databaseName;
}
// 根据数据库类型返回默认数据库名
String dbType = connection.getType().name().toUpperCase();
return switch (dbType) {
case "MYSQL" -> "mysql";
case "POSTGRESQL" -> "postgres";
case "H2" -> "test";
case "ORACLE" -> "ORCL";
case "SQLSERVER" -> "master";
default -> "mysql";
};
}
/**
* 获取驱动类名
*
* @param dbType 数据库类型
* @return 驱动类名
*/
private String getDriverClassName(String dbType) {
return switch (dbType.toUpperCase()) {
case "MYSQL" -> "com.mysql.cj.jdbc.Driver";
case "POSTGRESQL" -> "org.postgresql.Driver";
case "H2" -> "org.h2.Driver";
case "ORACLE" -> "oracle.jdbc.driver.OracleDriver";
case "SQLSERVER" -> "com.microsoft.sqlserver.jdbc.SQLServerDriver";
default -> throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
};
}
/**
* 获取连接测试SQL - 根据数据库类型返回不同的测试SQL
*
* @param dbType 数据库类型
* @return 测试SQL
*/
private String getConnectionTestQuery(String dbType) {
return switch (dbType) {
case "ORACLE" -> "SELECT 1 FROM DUAL";
case "SQLSERVER" -> "SELECT 1";
default -> "SELECT 1";
};
}
/**
* 获取连接初始化SQL - 根据数据库类型返回不同的初始化SQL
*
* @param dbType 数据库类型
* @return 初始化SQL,如果不需要则返回null
*/
private String getConnectionInitSql(String dbType) {
return switch (dbType) {
case "MYSQL" -> "SET SESSION wait_timeout=28800";
default -> null; // 其他数据库不需要初始化SQL
};
}
/**
* 添加数据库特定属性
*
* @param config HikariCP配置
* @param dbType 数据库类型
*/
private void addDatabaseSpecificProperties(HikariConfig config, String dbType) {
switch (dbType) {
case "MYSQL":
addMySQLProperties(config);
break;
case "POSTGRESQL":
addPostgreSQLProperties(config);
break;
case "ORACLE":
addOracleProperties(config);
break;
case "SQLSERVER":
addSQLServerProperties(config);
break;
case "H2":
addH2Properties(config);
break;
default:
// 其他数据库使用默认配置
break;
}
}
/**
* 生成缓存键 - 使用独立前缀避免冲突
*
* @param connection 连接配置
* @return 缓存键
*/
private String generateCacheKey(ResourceConnection connection) {
return String.format("DataBridge_%s_%s_%s_%s_%s",
connection.getType().name(),
connection.getHost(),
connection.getPort(),
connection.getUsername(),
getDatabaseName(connection));
}
/**
* 移除指定连接池
*
* @param connection 连接配置
*/
public void removeDataSource(ResourceConnection connection) {
String cacheKey = generateCacheKey(connection);
DataSource dataSource = connectionPoolCache.remove(cacheKey);
if (dataSource instanceof HikariDataSource) {
try {
((HikariDataSource) dataSource).close();
log.info("已关闭数据桥接连接池: {}", cacheKey);
} catch (Exception e) {
log.warn("关闭数据桥接连接池失败: {} - {}", cacheKey, e.getMessage());
}
}
}
/**
* 启动保活任务
*/
private void startKeepAliveTask() {
keepAliveScheduler.scheduleWithFixedDelay(() -> {
try {
connectionPoolCache.forEach((key, dataSource) -> {
if (!isDataSourceHealthy(dataSource)) {
log.warn("检测到不健康的连接池: {}", key);
}
});
} catch (Exception e) {
log.error("连接池保活检查失败: {}", e.getMessage());
}
}, KEEP_ALIVE_INTERVAL, KEEP_ALIVE_INTERVAL, TimeUnit.MILLISECONDS);
log.info("数据桥接连接池保活任务已启动,检查间隔: {}秒", KEEP_ALIVE_INTERVAL / 1000);
}
/**
* 关闭所有连接池
*/
public void shutdown() {
log.info("开始关闭所有数据桥接连接池...");
// 关闭保活调度器
keepAliveScheduler.shutdown();
try {
if (!keepAliveScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
keepAliveScheduler.shutdownNow();
}
} catch (InterruptedException e) {
keepAliveScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
// 关闭所有连接池
connectionPoolCache.forEach((key, dataSource) -> {
try {
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
}
} catch (Exception e) {
log.warn("关闭数据桥接连接池失败: {} - {}", key, e.getMessage());
}
});
return status.toString();
}
connectionPoolCache.clear();
log.info("所有数据桥接连接池已关闭");
}
/**
* 获取连接池状态
*
* @return 连接池状态信息
*/
public String getPoolStatus() {
StringBuilder status = new StringBuilder();
status.append("数据桥接连接池状态:\n");
connectionPoolCache.forEach((key, dataSource) -> {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
status.append(String.format(" %s: 活跃=%d, 空闲=%d, 等待=%d\n",
key,
hikariDataSource.getHikariPoolMXBean().getActiveConnections(),
hikariDataSource.getHikariPoolMXBean().getIdleConnections(),
hikariDataSource.getHikariPoolMXBean().getThreadsAwaitingConnection()));
}
});
return status.toString();
}
/**
* 获取连接池数量
*
* @return 连接池数量
*/
public int getPoolCount() {
return connectionPoolCache.size();
}
/**
* 获取连接池数量
*
* @return 连接池数量
*/
public int getPoolCount() {
return connectionPoolCache.size();
}
}
@@ -18,7 +18,8 @@ import java.util.regex.Pattern;
import lombok.extern.slf4j.Slf4j;
/**
* SQL模板处理器 智能处理SQL模板中的变量替换,避免引号冲突
* SQL模板处理器
* 智能处理SQL模板中的变量替换,避免引号冲突
*
* @author gitee.com/NexIoT
* @version 1.0
@@ -27,142 +28,142 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SqlTemplateProcessor {
/**
* 处理SQL模板,智能处理引号冲突
*
* @param template SQL模板
* @param variables 变量映射
* @return 处理后的SQL
*/
public static String processTemplate(String template, Map<String, Object> variables) {
if (template == null || template.trim().isEmpty()) {
return "";
/**
* 处理SQL模板,智能处理引号冲突
*
* @param template SQL模板
* @param variables 变量映射
* @return 处理后的SQL
*/
public static String processTemplate(String template, Map<String, Object> variables) {
if (template == null || template.trim().isEmpty()) {
return "";
}
// 使用正则表达式匹配所有 #{...} 占位符
Pattern pattern = Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
Matcher matcher = pattern.matcher(template);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String paramPath = matcher.group(1);
Object value = getNestedValue(variables, paramPath);
String replacement = convertToSqlValue(value, template, matcher.start());
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
// 使用正则表达式匹配所有 #{...} 占位符
Pattern pattern = Pattern.compile("#\\{([a-zA-Z0-9_.]+)\\}");
Matcher matcher = pattern.matcher(template);
/**
* 智能转换为SQL值,根据上下文选择合适的引号
*
* @param value 要转换的值
* @param template 完整模板
* @param position 当前位置
* @return SQL安全格式的字符串
*/
private static String convertToSqlValue(Object value, String template, int position) {
if (value == null) {
return "NULL";
}
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String paramPath = matcher.group(1);
Object value = getNestedValue(variables, paramPath);
String replacement = convertToSqlValue(value, template, matcher.start());
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
/**
* 智能转换为SQL值,根据上下文选择合适的引号
*
* @param value 要转换的值
* @param template 完整模板
* @param position 当前位置
* @return SQL安全格式的字符串
*/
private static String convertToSqlValue(Object value, String template, int position) {
if (value == null) {
return "NULL";
// 检查上下文,判断是否在JSON字符串中
boolean inJsonContext = isInJsonContext(template, position);
if (inJsonContext) {
// 在JSON上下文中,使用双引号
return convertToJsonValue(value);
} else {
// 在普通SQL上下文中,使用单引号
return SqlValueConverter.convertToSqlValue(value);
}
}
// 检查上下文,判断是否在JSON字符串中
boolean inJsonContext = isInJsonContext(template, position);
if (inJsonContext) {
// 在JSON上下文中,使用双引号
return convertToJsonValue(value);
} else {
// 在普通SQL上下文中,使用单引号
return SqlValueConverter.convertToSqlValue(value);
}
}
/**
* 检查当前位置是否在JSON字符串上下文中
*
* @param template 模板
* @param position 位置
* @return 是否在JSON上下文中
*/
private static boolean isInJsonContext(String template, int position) {
// 向前查找最近的引号
int singleQuotePos = template.lastIndexOf("'", position);
int doubleQuotePos = template.lastIndexOf("\"", position);
// 如果最近的是双引号,说明在JSON上下文中
if (doubleQuotePos > singleQuotePos) {
return true;
/**
* 检查当前位置是否在JSON字符串上下文中
*
* @param template 模板
* @param position 位置
* @return 是否在JSON上下文中
*/
private static boolean isInJsonContext(String template, int position) {
// 向前查找最近的引号
int singleQuotePos = template.lastIndexOf("'", position);
int doubleQuotePos = template.lastIndexOf("\"", position);
// 如果最近的是双引号,说明在JSON上下文中
if (doubleQuotePos > singleQuotePos) {
return true;
}
// 检查是否在JSON对象中(包含大括号)
String beforePosition = template.substring(0, position);
int lastOpenBrace = beforePosition.lastIndexOf("{");
int lastCloseBrace = beforePosition.lastIndexOf("}");
// 如果最近的大括号是开括号,说明在JSON对象中
if (lastOpenBrace > lastCloseBrace) {
return true;
}
return false;
}
// 检查是否在JSON对象中(包含大括号)
String beforePosition = template.substring(0, position);
int lastOpenBrace = beforePosition.lastIndexOf("{");
int lastCloseBrace = beforePosition.lastIndexOf("}");
// 如果最近的大括号是开括号,说明在JSON对象中
if (lastOpenBrace > lastCloseBrace) {
return true;
/**
* 转换为JSON值
*
* @param value 要转换的值
* @return JSON格式的字符串
*/
private static String convertToJsonValue(Object value) {
if (value == null) {
return "null";
}
if (value instanceof String) {
// 字符串:转义双引号
String strValue = (String) value;
return "\"" + strValue.replace("\"", "\\\"") + "\"";
} else if (value instanceof Number || value instanceof Boolean) {
// 数字和布尔类型直接转换
return value.toString();
} else {
// 其他类型:转为JSON字符串
return JSONUtil.toJsonStr(value);
}
}
return false;
}
/**
* 获取嵌套属性值
*
* @param variables 变量映射
* @param path 属性路径
* @return 属性值
*/
@SuppressWarnings("unchecked")
private static Object getNestedValue(Map<String, Object> variables, String path) {
if (path == null || path.trim().isEmpty()) {
return null;
}
/**
* 转换为JSON值
*
* @param value 要转换的值
* @return JSON格式的字符串
*/
private static String convertToJsonValue(Object value) {
if (value == null) {
return "null";
String[] keys = path.split("\\.");
Object current = variables;
for (String key : keys) {
if (current == null) {
return null;
}
if (current instanceof Map) {
current = ((Map<String, Object>) current).get(key);
} else {
return null;
}
}
return current;
}
if (value instanceof String) {
// 字符串:转义双引号
String strValue = (String) value;
return "\"" + strValue.replace("\"", "\\\"") + "\"";
} else if (value instanceof Number || value instanceof Boolean) {
// 数字和布尔类型直接转换
return value.toString();
} else {
// 其他类型:转为JSON字符串
return JSONUtil.toJsonStr(value);
}
}
/**
* 获取嵌套属性值
*
* @param variables 变量映射
* @param path 属性路径
* @return 属性值
*/
@SuppressWarnings("unchecked")
private static Object getNestedValue(Map<String, Object> variables, String path) {
if (path == null || path.trim().isEmpty()) {
return null;
}
String[] keys = path.split("\\.");
Object current = variables;
for (String key : keys) {
if (current == null) {
return null;
}
if (current instanceof Map) {
current = ((Map<String, Object>) current).get(key);
} else {
return null;
}
}
return current;
}
}
@@ -16,7 +16,8 @@ import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* SQL值转换工具类 提供将Java对象转换为SQL安全格式的通用方法
* SQL值转换工具类
* 提供将Java对象转换为SQL安全格式的通用方法
*
* @author gitee.com/NexIoT
* @version 1.0
@@ -26,8 +27,12 @@ import lombok.extern.slf4j.Slf4j;
public class SqlValueConverter {
/**
* 将变量值转换为SQL安全格式 1. 字符串类型:添加单引号,转义内部单引号 2. 数字/布尔类型:直接转换为字符串 3. null:转换为SQL的NULL关键字 4.
* 日期类型:转换为SQL标准格式 5. 其他类型:转为JSON字符串
* 将变量值转换为SQL安全格式
* 1. 字符串类型:添加单引号,转义内部单引号
* 2. 数字/布尔类型:直接转换为字符串
* 3. null:转换为SQL的NULL关键字
* 4. 日期类型:转换为SQL标准格式
* 5. 其他类型:转为JSON字符串
*
* @param value 要转换的值
* @return SQL安全格式的字符串
@@ -36,7 +41,7 @@ public class SqlValueConverter {
if (value == null) {
return "NULL";
}
if (value instanceof String) {
// 转义单引号(防止SQL注入和语法错误)
String strValue = (String) value;
@@ -64,7 +69,7 @@ public class SqlValueConverter {
if (values == null) {
return new String[0];
}
String[] result = new String[values.length];
for (int i = 0; i < values.length; i++) {
result[i] = convertToSqlValue(values[i]);
@@ -73,7 +78,8 @@ public class SqlValueConverter {
}
/**
* 将变量值转换为SQL安全格式(JSON字符串专用) 使用双引号包围JSON字符串,避免单引号冲突
* 将变量值转换为SQL安全格式(JSON字符串专用)
* 使用双引号包围JSON字符串,避免单引号冲突
*
* @param value 要转换的值
* @return SQL安全格式的字符串
@@ -82,7 +88,7 @@ public class SqlValueConverter {
if (value == null) {
return "NULL";
}
if (value instanceof String) {
// JSON字符串:使用双引号包围,转义内部双引号
String strValue = (String) value;
@@ -110,12 +116,12 @@ public class SqlValueConverter {
if (value == null) {
return false;
}
if (value instanceof String) {
String strValue = (String) value;
return strValue.contains("'") || strValue.contains("\\") || strValue.contains("\"");
}
return !(value instanceof Number || value instanceof Boolean);
}
}
@@ -1,99 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.universal.databridge.mapper.DataBridgeConfigMapper">
<resultMap id="BaseResultMap" type="cn.universal.databridge.entity.DataBridgeConfig">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="source_scope" jdbcType="VARCHAR" property="sourceScope"/>
<result column="source_product_keys" jdbcType="LONGVARCHAR" property="sourceProductKeys"/>
<result column="source_application_id" jdbcType="BIGINT" property="sourceApplicationId"/>
<result column="target_resource_id" jdbcType="BIGINT" property="targetResourceId"/>
<result column="bridge_type" jdbcType="VARCHAR" property="bridgeType"/>
<result column="template" jdbcType="LONGVARCHAR" property="template"/>
<result column="magic_script" jdbcType="LONGVARCHAR" property="magicScript"/>
<result column="config" jdbcType="LONGVARCHAR" property="config"/>
<result column="status" jdbcType="TINYINT" property="status"/>
<result column="description" jdbcType="VARCHAR" property="description"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
</resultMap>
<resultMap id="BaseResultMap" type="cn.universal.databridge.entity.DataBridgeConfig">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="source_scope" jdbcType="VARCHAR" property="sourceScope"/>
<result column="source_product_keys" jdbcType="LONGVARCHAR" property="sourceProductKeys"/>
<result column="source_application_id" jdbcType="BIGINT" property="sourceApplicationId"/>
<result column="target_resource_id" jdbcType="BIGINT" property="targetResourceId"/>
<result column="bridge_type" jdbcType="VARCHAR" property="bridgeType"/>
<result column="template" jdbcType="LONGVARCHAR" property="template"/>
<result column="magic_script" jdbcType="LONGVARCHAR" property="magicScript"/>
<result column="config" jdbcType="LONGVARCHAR" property="config"/>
<result column="status" jdbcType="TINYINT" property="status"/>
<result column="description" jdbcType="VARCHAR" property="description"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id
, name, source_scope, source_product_keys, source_application_id,
<sql id="Base_Column_List">
id
, name, source_scope, source_product_keys, source_application_id,
target_resource_id, bridge_type, template, magic_script, config,
status, description, create_by, create_time, update_by, update_time
</sql>
</sql>
<!-- 根据产品KEY获取活跃配置(兼容新结构:ALL_PRODUCTS 直接命中;SPECIFIC_PRODUCTS 判断JSON列表包含;APPLICATION 由上层按应用过滤) -->
<select id="selectActiveConfigsByProductKey" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE status = 1
AND (
source_scope = 'ALL_PRODUCTS'
OR (
source_scope = 'SPECIFIC_PRODUCTS'
AND JSON_CONTAINS(CAST(source_product_keys AS JSON), CONCAT('"', #{productKey}, '"'))
)
)
ORDER BY create_time DESC
</select>
<!-- 根据产品KEY获取活跃配置(兼容新结构:ALL_PRODUCTS 直接命中;SPECIFIC_PRODUCTS 判断JSON列表包含;APPLICATION 由上层按应用过滤) -->
<select id="selectActiveConfigsByProductKey" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE status = 1
AND (
source_scope = 'ALL_PRODUCTS'
OR (
source_scope = 'SPECIFIC_PRODUCTS'
AND JSON_CONTAINS(CAST(source_product_keys AS JSON), CONCAT('"', #{productKey}, '"'))
)
)
ORDER BY create_time DESC
</select>
<!-- 根据桥接类型获取配置 -->
<select id="selectConfigsByBridgeType" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE bridge_type = #{bridgeType}
ORDER BY create_time DESC
</select>
<!-- 根据桥接类型获取配置 -->
<select id="selectConfigsByBridgeType" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE bridge_type = #{bridgeType}
ORDER BY create_time DESC
</select>
<!-- 根据目标资源ID获取配置 -->
<select id="selectConfigsByTargetResourceId" parameterType="long" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE target_resource_id = #{targetResourceId}
ORDER BY create_time DESC
</select>
<!-- 根据目标资源ID获取配置 -->
<select id="selectConfigsByTargetResourceId" parameterType="long" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE target_resource_id = #{targetResourceId}
ORDER BY create_time DESC
</select>
<!-- 批量更新配置状态 -->
<update id="batchUpdateStatus">
UPDATE iot_data_bridge_config
SET status = #{status},
update_by = #{updateBy},
update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 批量更新配置状态 -->
<update id="batchUpdateStatus">
UPDATE iot_data_bridge_config
SET status = #{status},
update_by = #{updateBy},
update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 根据名称查询配置(用于重名检查) -->
<select id="selectByName" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE name = #{name}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 根据名称查询配置(用于重名检查) -->
<select id="selectByName" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE name = #{name}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 根据创建者查询配置列表 -->
<select id="selectByCreateBy" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE create_by = #{createBy}
ORDER BY create_time DESC
</select>
<!-- 根据创建者查询配置列表 -->
<select id="selectByCreateBy" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_data_bridge_config
WHERE create_by = #{createBy}
ORDER BY create_time DESC
</select>
</mapper>
@@ -1,39 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.universal.databridge.mapper.DataInputLogMapper">
<!-- 查询最近的日志 -->
<select id="selectRecentLogs" resultType="cn.universal.databridge.entity.DataInputLog">
SELECT *
FROM iot_data_input_log
WHERE config_id = #{configId}
ORDER BY create_time DESC
LIMIT #{limit}
</select>
<!-- 查询最近的日志 -->
<select id="selectRecentLogs" resultType="cn.universal.databridge.entity.DataInputLog">
SELECT *
FROM iot_data_input_log
WHERE config_id = #{configId}
ORDER BY create_time DESC
LIMIT #{limit}
</select>
<!-- 计算成功率 -->
<select id="calculateSuccessRate" resultType="java.lang.Double">
SELECT
CASE
WHEN SUM(message_count) = 0 THEN 0.0
ELSE ROUND(SUM(success_count) * 100.0 / SUM(message_count), 2)
END as success_rate
FROM iot_data_input_log
WHERE config_id = #{configId}
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time &lt;= #{endTime}
</if>
</select>
<!-- 计算成功率 -->
<select id="calculateSuccessRate" resultType="java.lang.Double">
SELECT
CASE
WHEN SUM(message_count) = 0 THEN 0.0
ELSE ROUND(SUM(success_count) * 100.0 / SUM(message_count), 2)
END as success_rate
FROM iot_data_input_log
WHERE config_id = #{configId}
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time &lt;= #{endTime}
</if>
</select>
<!-- 删除过期日志 -->
<delete id="deleteExpiredLogs">
DELETE
FROM iot_data_input_log
WHERE create_time &lt; #{expireTime}
</delete>
<!-- 删除过期日志 -->
<delete id="deleteExpiredLogs">
DELETE
FROM iot_data_input_log
WHERE create_time &lt; #{expireTime}
</delete>
</mapper>
@@ -1,85 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.universal.databridge.mapper.ResourceConnectionMapper">
<resultMap id="BaseResultMap" type="cn.universal.databridge.entity.ResourceConnection">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="type" jdbcType="VARCHAR" property="type"/>
<result column="host" jdbcType="VARCHAR" property="host"/>
<result column="port" jdbcType="INTEGER" property="port"/>
<result column="username" jdbcType="VARCHAR" property="username"/>
<result column="password" jdbcType="VARCHAR" property="password"/>
<result column="database_name" jdbcType="VARCHAR" property="databaseName"/>
<result column="extra_config" jdbcType="LONGVARCHAR" property="extraConfig"/>
<result column="status" jdbcType="TINYINT" property="status"/>
<result column="description" jdbcType="VARCHAR" property="description"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
</resultMap>
<resultMap id="BaseResultMap" type="cn.universal.databridge.entity.ResourceConnection">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="type" jdbcType="VARCHAR" property="type"/>
<result column="host" jdbcType="VARCHAR" property="host"/>
<result column="port" jdbcType="INTEGER" property="port"/>
<result column="username" jdbcType="VARCHAR" property="username"/>
<result column="password" jdbcType="VARCHAR" property="password"/>
<result column="database_name" jdbcType="VARCHAR" property="databaseName"/>
<result column="extra_config" jdbcType="LONGVARCHAR" property="extraConfig"/>
<result column="status" jdbcType="TINYINT" property="status"/>
<result column="description" jdbcType="VARCHAR" property="description"/>
<result column="create_by" jdbcType="VARCHAR" property="createBy"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="update_by" jdbcType="VARCHAR" property="updateBy"/>
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/>
</resultMap>
<sql id="Base_Column_List">
id
, name, type, host, port, username, password, database_name, extra_config,
<sql id="Base_Column_List">
id
, name, type, host, port, username, password, database_name, extra_config,
status, description, create_by, create_time, update_by, update_time
</sql>
</sql>
<!-- 根据资源类型获取活跃连接 -->
<select id="selectActiveConnectionsByType" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE type = #{type}
AND status = 1
ORDER BY create_time DESC
</select>
<!-- 根据资源类型获取活跃连接 -->
<select id="selectActiveConnectionsByType" parameterType="string" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE type = #{type}
AND status = 1
ORDER BY create_time DESC
</select>
<!-- 根据主机和端口查询连接 -->
<select id="selectByHostAndPort" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE host = #{host}
AND port = #{port}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 根据主机和端口查询连接 -->
<select id="selectByHostAndPort" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE host = #{host}
AND port = #{port}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 根据名称查询连接(用于重名检查) -->
<select id="selectByName" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE name = #{name}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 根据名称查询连接(用于重名检查) -->
<select id="selectByName" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM iot_resource_connection
WHERE name = #{name}
<if test="excludeId != null">
AND id != #{excludeId}
</if>
LIMIT 1
</select>
<!-- 批量更新连接状态 -->
<update id="batchUpdateStatus">
UPDATE iot_resource_connection
SET status = #{status},
update_by = #{updateBy},
update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 批量更新连接状态 -->
<update id="batchUpdateStatus">
UPDATE iot_resource_connection
SET status = #{status},
update_by = #{updateBy},
update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<!-- 测试连接可用性 -->
<select id="testConnection" parameterType="long" resultType="int">
SELECT COUNT(1)
FROM iot_resource_connection
WHERE id = #{id}
AND status = 1
</select>
<!-- 测试连接可用性 -->
<select id="testConnection" parameterType="long" resultType="int">
SELECT COUNT(1)
FROM iot_resource_connection
WHERE id = #{id}
AND status = 1
</select>
</mapper>
@@ -0,0 +1,255 @@
package cn.universal.databridge.engine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* ParamTemplateEngine 单元测试
*/
class ParamTemplateEngineTest {
private ParamTemplateEngine engine;
private Map<String, Object> variables;
@BeforeEach
void setUp() {
engine = new ParamTemplateEngine();
// 准备测试数据
variables = new HashMap<>();
variables.put("deviceId", "dev123");
variables.put("productKey", "pk001");
variables.put("deviceName", "sensor01");
variables.put("iotId", "iot456");
variables.put("deviceNode", "node1");
variables.put("messageType", "property");
// 嵌套属性
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 25.5);
properties.put("humidity", 60);
properties.put("illuminationDesc", "明亮");
variables.put("properties", properties);
}
@Test
void testMysqlTemplate() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
String template = "INSERT INTO device_data(\n" +
" device_id, product_key, device_name, iot_id,\n" +
" device_node, message_type, properties, raw_data\n" +
") VALUES(\n" +
" #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n" +
" #{deviceNode}, #{messageType}, #{properties}, \n" +
" ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n" +
");";
ParamSql result = engine.process(template, variables, adapter);
assertNotNull(result);
assertTrue(result.getSql().contains("JSON_OBJECT"));
assertTrue(result.getSql().contains("?"));
assertEquals(9, result.getParams().size());
assertEquals("dev123", result.getParams().get(0));
assertEquals("pk001", result.getParams().get(1));
assertEquals(25.5, result.getParams().get(3)); // csq value
assertEquals("明亮", result.getParams().get(4)); // tips value
}
@Test
void testPostgreSqlTemplate() {
SqlDialectAdapter adapter = new PostgreSqlDialectAdapter();
String template = "INSERT INTO device_data(\n" +
" device_id, product_key, device_name, iot_id,\n" +
" device_node, message_type, properties, raw_data\n" +
") VALUES(\n" +
" #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n" +
" #{deviceNode}, #{messageType}, #{properties}, \n" +
" ${json_text('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n" +
");";
ParamSql result = engine.process(template, variables, adapter);
assertNotNull(result);
assertTrue(result.getSql().contains("json_build_object"));
assertTrue(result.getSql().contains("::TEXT"));
assertTrue(result.getSql().contains("?"));
assertEquals(9, result.getParams().size());
assertEquals("dev123", result.getParams().get(0));
assertEquals("明亮", result.getParams().get(4));
}
@Test
void testOracleTemplate() {
SqlDialectAdapter adapter = new OracleDialectAdapter();
String template = "INSERT INTO device_data(\n" +
" device_id, product_key, device_name, iot_id,\n" +
" device_node, message_type, properties, raw_data\n" +
") VALUES(\n" +
" #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n" +
" #{deviceNode}, #{messageType}, #{properties}, \n" +
" ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n" +
");";
ParamSql result = engine.process(template, variables, adapter);
assertNotNull(result);
assertTrue(result.getSql().contains("JSON_OBJECT"));
assertTrue(result.getSql().contains("VALUE"));
assertTrue(result.getSql().contains("?"));
assertEquals(9, result.getParams().size());
}
@Test
void testSqlServerTemplate() {
SqlDialectAdapter adapter = new SqlServerDialectAdapter();
String template = "INSERT INTO device_data(\n" +
" device_id, product_key, device_name, iot_id,\n" +
" device_node, message_type, properties, raw_data\n" +
") VALUES(\n" +
" #{deviceId}, #{productKey}, #{deviceName}, #{iotId},\n" +
" #{deviceNode}, #{messageType}, #{properties}, \n" +
" ${json('csq', properties.temperature, 'tips', properties.illuminationDesc)}\n" +
");";
ParamSql result = engine.process(template, variables, adapter);
assertNotNull(result);
assertTrue(result.getSql().contains("JSON_OBJECT"));
assertTrue(result.getSql().contains("?"));
assertEquals(9, result.getParams().size());
}
@Test
void testNestedPropertyAccess() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
String template = "SELECT #{properties.temperature}, #{properties.humidity}";
ParamSql result = engine.process(template, variables, adapter);
assertEquals("SELECT ?, ?", result.getSql());
assertEquals(2, result.getParams().size());
assertEquals(25.5, result.getParams().get(0));
assertEquals(60, result.getParams().get(1));
}
@Test
void testDirectPropertiesAccess() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
String template = "INSERT INTO test(data) VALUES(#{properties})";
ParamSql result = engine.process(template, variables, adapter);
assertEquals("INSERT INTO test(data) VALUES(?)", result.getSql());
assertEquals(1, result.getParams().size());
// properties 应该被序列化为 JSON 字符串
assertTrue(result.getParams().get(0) instanceof String);
assertTrue(((String) result.getParams().get(0)).contains("temperature"));
}
@Test
void testMultipleJsonMacros() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
String template = "INSERT INTO test(col1, col2) VALUES(" +
"${json('a', deviceId)}, " +
"${json('b', productKey)}" +
")";
ParamSql result = engine.process(template, variables, adapter);
assertTrue(result.getSql().contains("JSON_OBJECT"));
assertEquals(2, result.getParams().size());
assertEquals("dev123", result.getParams().get(0));
assertEquals("pk001", result.getParams().get(1));
}
@Test
void testEmptyTemplate() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
ParamSql result = engine.process("", variables, adapter);
assertEquals("", result.getSql());
assertTrue(result.getParams().isEmpty());
}
@Test
void testNullTemplate() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
ParamSql result = engine.process(null, variables, adapter);
assertEquals("", result.getSql());
assertTrue(result.getParams().isEmpty());
}
@Test
void testNoVariablesTemplate() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
String template = "SELECT * FROM device_data WHERE id = 1";
ParamSql result = engine.process(template, variables, adapter);
assertEquals(template, result.getSql());
assertTrue(result.getParams().isEmpty());
}
@Test
void testMixedJsonAndJsonText() {
SqlDialectAdapter adapter = new PostgreSqlDialectAdapter();
String template = "INSERT INTO test(col1, col2) VALUES(" +
"${json('temp', properties.temperature)}, " +
"${json_text('desc', properties.illuminationDesc)}" +
")";
ParamSql result = engine.process(template, variables, adapter);
assertTrue(result.getSql().contains("json_build_object"));
assertTrue(result.getSql().contains("::TEXT"));
assertEquals(2, result.getParams().size());
assertEquals(25.5, result.getParams().get(0));
assertEquals("明亮", result.getParams().get(1));
}
@Test
void testChineseCharacters() {
SqlDialectAdapter adapter = new MySqlDialectAdapter();
variables.put("chineseValue", "测试中文");
String template = "INSERT INTO test(name) VALUES(#{chineseValue})";
ParamSql result = engine.process(template, variables, adapter);
assertEquals("INSERT INTO test(name) VALUES(?)", result.getSql());
assertEquals("测试中文", result.getParams().get(0));
}
@Test
void testSpecialCharactersInJson() {
SqlDialectAdapter adapter = new PostgreSqlDialectAdapter();
Map<String, Object> props = (Map<String, Object>) variables.get("properties");
props.put("specialDesc", "包含\"引号\"和'单引号'");
String template = "SELECT ${json_text('data', properties.specialDesc)}";
ParamSql result = engine.process(template, variables, adapter);
assertNotNull(result);
assertTrue(result.getParams().get(0).toString().contains("引号"));
}
}
@@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-http</name>
<description>HTTP数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-http</name>
<description>HTTP数据桥接插件</description>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
@@ -22,192 +22,181 @@ import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component("http")
@ConditionalOnProperty(
prefix = "databridge.plugins.http",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@ConditionalOnProperty(prefix = "databridge.plugins.http", name = "enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class HttpDataBridgePlugin extends AbstractDataOutputPlugin {
private final RestTemplate restTemplate = new RestTemplate();
private final RestTemplate restTemplate = new RestTemplate();
@Override
public PluginInfo getPluginInfo() {
return PluginInfo.builder()
.name("HTTP数据桥接插件")
.version("1.0.0")
.description("支持HTTP上报")
.author("gitee.com/NexIoT")
.pluginType("HTTP")
.supportedResourceTypes(List.of("HTTP"))
.dataDirection(PluginInfo.DataDirection.BIDIRECTIONAL)
.category("网络")
.icon("api")
.build();
}
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
}
@Override
protected void processProcessedData(
Object processedData,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = buildHeaders(config);
String payload;
if (processedData instanceof String) {
payload = (String) processedData;
} else {
// 如果Magic脚本返回的不是字符串,使用模板处理
Map<String, Object> variables = buildTemplateVariables(request, config);
payload = processTemplate(config.getTemplate(), variables);
}
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("HTTP请求成功: status={}, body={}", response.getStatusCode(), response.getBody());
} else {
log.warn("HTTP请求失败: status={}, body={}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
log.error("HTTP数据处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@Override
public PluginInfo getPluginInfo() {
return PluginInfo.builder()
.name("HTTP数据桥接插件")
.version("1.0.0")
.description("支持HTTP上报")
.author("gitee.com/NexIoT")
.pluginType("HTTP")
.supportedResourceTypes(List.of("HTTP"))
.dataDirection(PluginInfo.DataDirection.BIDIRECTIONAL)
.category("网络")
.icon("api")
.build();
}
}
@Override
protected void processTemplateResult(
String templateResult,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = buildHeaders(config);
HttpEntity<String> entity = new HttpEntity<>(templateResult, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("HTTP请求成功: status={}, body={}", response.getStatusCode(), response.getBody());
} else {
log.warn("HTTP请求失败: status={}, body={}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
log.error("HTTP模板处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS,
SourceScope.SPECIFIC_PRODUCTS,
SourceScope.APPLICATION
);
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>("{}", headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
log.error("HTTP连接测试失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return validateMagicScript(config.getMagicScript());
}
private String buildUrl(ResourceConnection connection) {
// 默认使用http协议,可以通过extraConfig配置SSL
String protocol = "http";
if (StrUtil.isNotBlank(connection.getExtraConfig())) {
try {
JSONObject extraConfig = JSONUtil.parseObj(connection.getExtraConfig());
if (extraConfig.getBool("ssl", false)) {
protocol = "https";
@Override
protected void processProcessedData(Object processedData, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = buildHeaders(config);
String payload;
if (processedData instanceof String) {
payload = (String) processedData;
} else {
// 如果Magic脚本返回的不是字符串,使用模板处理
Map<String, Object> variables = buildTemplateVariables(request, config);
payload = processTemplate(config.getTemplate(), variables);
}
HttpEntity<String> entity = new HttpEntity<>(payload, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("HTTP请求成功: status={}, body={}", response.getStatusCode(), response.getBody());
} else {
log.warn("HTTP请求失败: status={}, body={}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
log.error("HTTP数据处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
}
} catch (Exception e) {
log.warn("解析SSL配置失败: {}", e.getMessage());
}
}
String path = "/";
if (StrUtil.isNotBlank(connection.getExtraConfig())) {
try {
JSONObject extraConfig = JSONUtil.parseObj(connection.getExtraConfig());
path = extraConfig.getStr("path", "/");
} catch (Exception e) {
log.warn("解析路径配置失败: {}", e.getMessage());
}
}
return String.format(
"%s://%s:%d%s", protocol, connection.getHost(), connection.getPort(), path);
}
private HttpHeaders buildHeaders(DataBridgeConfig config) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 从统一配置中提取headers
if (StrUtil.isNotBlank(config.getConfig())) {
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
if (configJson.containsKey("headers")) {
JSONObject headersJson = configJson.getJSONObject("headers");
for (String key : headersJson.keySet()) {
headers.set(key, headersJson.getStr(key));
}
@Override
protected void processTemplateResult(String templateResult, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = buildHeaders(config);
HttpEntity<String> entity = new HttpEntity<>(templateResult, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.debug("HTTP请求成功: status={}, body={}", response.getStatusCode(), response.getBody());
} else {
log.warn("HTTP请求失败: status={}, body={}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
log.error("HTTP模板处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
}
} catch (Exception e) {
log.warn("解析HTTP配置失败: {}", e.getMessage());
}
}
return headers;
}
private Map<String, Object> buildTemplateVariables(
BaseUPRequest request, DataBridgeConfig config) {
Map<String, Object> variables = new HashMap<>();
// 设备信息
if (request.getIoTDeviceDTO() != null) {
variables.put("deviceKey", request.getIoTDeviceDTO().getDeviceId());
variables.put("productKey", request.getIoTDeviceDTO().getProductKey());
@Override
public Boolean testConnection(ResourceConnection connection) {
try {
String url = buildUrl(connection);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>("{}", headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
return response.getStatusCode().is2xxSuccessful();
} catch (Exception e) {
log.error("HTTP连接测试失败: {}", e.getMessage(), e);
return false;
}
}
// 消息信息
variables.put("messageType", request.getMessageType().name());
variables.put("timestamp", System.currentTimeMillis());
variables.put("properties", request.getProperties());
// 配置信息
if (StrUtil.isNotBlank(config.getConfig())) {
try {
variables.put("configJson", JSONUtil.parseObj(config.getConfig()));
} catch (Exception e) {
log.warn("解析配置JSON失败: {}", e.getMessage());
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return validateMagicScript(config.getMagicScript());
}
return variables;
}
}
private String buildUrl(ResourceConnection connection) {
// 默认使用http协议,可以通过extraConfig配置SSL
String protocol = "http";
if (StrUtil.isNotBlank(connection.getExtraConfig())) {
try {
JSONObject extraConfig = JSONUtil.parseObj(connection.getExtraConfig());
if (extraConfig.getBool("ssl", false)) {
protocol = "https";
}
} catch (Exception e) {
log.warn("解析SSL配置失败: {}", e.getMessage());
}
}
String path = "/";
if (StrUtil.isNotBlank(connection.getExtraConfig())) {
try {
JSONObject extraConfig = JSONUtil.parseObj(connection.getExtraConfig());
path = extraConfig.getStr("path", "/");
} catch (Exception e) {
log.warn("解析路径配置失败: {}", e.getMessage());
}
}
return String.format("%s://%s:%d%s", protocol, connection.getHost(), connection.getPort(), path);
}
private HttpHeaders buildHeaders(DataBridgeConfig config) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 从统一配置中提取headers
if (StrUtil.isNotBlank(config.getConfig())) {
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
if (configJson.containsKey("headers")) {
JSONObject headersJson = configJson.getJSONObject("headers");
for (String key : headersJson.keySet()) {
headers.set(key, headersJson.getStr(key));
}
}
} catch (Exception e) {
log.warn("解析HTTP配置失败: {}", e.getMessage());
}
}
return headers;
}
private Map<String, Object> buildTemplateVariables(BaseUPRequest request, DataBridgeConfig config) {
Map<String, Object> variables = new HashMap<>();
// 设备信息
if (request.getIoTDeviceDTO() != null) {
variables.put("deviceKey", request.getIoTDeviceDTO().getDeviceId());
variables.put("productKey", request.getIoTDeviceDTO().getProductKey());
}
// 消息信息
variables.put("messageType", request.getMessageType().name());
variables.put("timestamp", System.currentTimeMillis());
variables.put("properties", request.getProperties());
// 配置信息
if (StrUtil.isNotBlank(config.getConfig())) {
try {
variables.put("configJson", JSONUtil.parseObj(config.getConfig()));
} catch (Exception e) {
log.warn("解析配置JSON失败: {}", e.getMessage());
}
}
return variables;
}
}
@@ -1,54 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-influxdb</name>
<description>InfluxDB数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-influxdb</name>
<description>InfluxDB数据桥接插件</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Web (用于HTTP客户端) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Web (用于HTTP客户端) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- InfluxDB客户端 -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>6.10.0</version>
</dependency>
<!-- InfluxDB客户端 -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>6.10.0</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,55 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-iotdb</name>
<description>IoTDB数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-iotdb</name>
<description>IoTDB数据桥接插件</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- IoTDB JDBC驱动 -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-jdbc</artifactId>
<version>1.3.0</version>
</dependency>
<!-- IoTDB JDBC驱动 -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-jdbc</artifactId>
<version>1.3.0</version>
</dependency>
<!-- IoTDB Session API -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-session</artifactId>
<version>1.3.0</version>
</dependency>
<!-- IoTDB Session API -->
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>iotdb-session</artifactId>
<version>1.3.0</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -1,67 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-jdbc</name>
<description>JDBC数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-jdbc</name>
<description>JDBC数据桥接插件</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- JDBC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- PostgreSQL驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- PostgreSQL驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- H2数据库(用于测试) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- H2数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- Oracle数据库驱动 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.5.0.0</version>
</dependency>
<!-- SQL Server数据库驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>12.4.2.jre8</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -145,7 +145,7 @@ public class DefaultJdbcInputPlugin extends AbstractDataInputPlugin {
case "h2" -> String.format("jdbc:h2:tcp://%s:%d/%s", host, port, databaseName);
case "oracle" -> String.format("jdbc:oracle:thin:@%s:%d:%s", host, port, databaseName);
case "sqlserver" ->
String.format("jdbc:sqlserver://%s:%d;databaseName=%s", host, port, databaseName);
String.format("jdbc:sqlserver://%s:%d;databaseName=%s;characterEncoding=UTF-8", host, port, databaseName);
default -> throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
};
}
@@ -106,25 +106,40 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
try {
// 获取数据源
DataSource dataSource = connectionManager.getOrCreateDataSource(connection);
javax.sql.DataSource dataSource = connectionManager.getOrCreateDataSource(connection);
org.springframework.jdbc.core.JdbcTemplate jdbcTemplate = new org.springframework.jdbc.core.JdbcTemplate(dataSource);
// 使用Spring的JdbcTemplate执行SQL
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update(templateResult);
// 构建变量并进行参数化模板解析(避免引号问题,兼容多方言)
java.util.Map<String, Object> variables = buildTemplateVariables(request, parseConfig(config));
cn.universal.databridge.engine.ParamTemplateEngine engine = new cn.universal.databridge.engine.ParamTemplateEngine();
cn.universal.databridge.engine.SqlDialectAdapter adapter = getAdapter(connection.getType());
cn.universal.databridge.engine.ParamSql ps = engine.process(config.getTemplate(), variables, adapter);
jdbcTemplate.update(ps.getSql(), ps.getParams().toArray());
} catch (Exception e) {
log.error("执行SQL失败: {} - {}", templateResult, e.getMessage());
// 清除可能损坏的连接池
connectionManager.removeDataSource(connection);
throw new RuntimeException("SQL执行失败: " + e.getMessage(), e);
}
}
private cn.universal.databridge.engine.SqlDialectAdapter getAdapter(cn.universal.databridge.entity.ResourceConnection.ResourceType type) {
switch (type) {
case MYSQL:
case H2:
return new cn.universal.databridge.engine.dialect.MySqlDialectAdapter();
case POSTGRESQL:
return new cn.universal.databridge.engine.dialect.PostgresDialectAdapter();
case ORACLE:
return new cn.universal.databridge.engine.dialect.OracleDialectAdapter();
case SQLSERVER:
return new cn.universal.databridge.engine.dialect.SqlServerDialectAdapter();
default:
return new cn.universal.databridge.engine.dialect.MySqlDialectAdapter();
}
}
private List<String> generateExecutableSqlList(Object processedData, DataBridgeConfig config) {
List<String> sqlList = new ArrayList<>();
@@ -162,6 +177,7 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
}
// 使用正则表达式匹配所有 #{...} 占位符,支持嵌套属性
// 智能处理引号,兼容MySQL和PostgreSQL
protected String processTemplate(String template, Map<String, Object> variables) {
if (template == null || template.trim().isEmpty()) {
log.warn("模板为空,无法处理");
@@ -174,7 +190,25 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
while (matcher.find()) {
String paramPath = matcher.group(1); // 获取变量路径,如 "properties.csq"
Object value = getNestedValue(variables, paramPath);
String sqlValue = SqlValueConverter.convertToSqlJsonValue(value);
// 检测占位符在模板中的上下文,判断是否已经在引号内
int start = matcher.start();
int end = matcher.end();
boolean inSingleQuotes = isInQuotes(template, start, end, '\'');
boolean inDoubleQuotes = isInQuotes(template, start, end, '"');
String sqlValue;
if (inSingleQuotes) {
// 如果已经在单引号内,只替换值,不添加引号,但需要转义单引号
sqlValue = convertValueForQuotedContext(value, true);
} else if (inDoubleQuotes) {
// 如果已经在双引号内,只替换值,不添加引号,但需要转义双引号
sqlValue = convertValueForQuotedContext(value, false);
} else {
// 如果不在引号内,使用标准的SQL值转换(添加引号)
sqlValue = SqlValueConverter.convertToSqlValue(value);
}
matcher.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(sqlValue));
}
matcher.appendTail(sb);
@@ -182,4 +216,88 @@ public class DefaultJdbcOutPlugin extends AbstractDataOutputPlugin {
return result;
}
/**
* 检测占位符是否在引号内
* @param template 模板字符串
* @param start 占位符开始位置
* @param end 占位符结束位置
* @param quoteChar 引号字符(' 或 ")
* @return 是否在引号内
*/
private boolean isInQuotes(String template, int start, int end, char quoteChar) {
// 向前查找最近的未转义引号
int quoteBefore = -1;
for (int i = start - 1; i >= 0; i--) {
if (template.charAt(i) == quoteChar) {
// 检查是否被转义(考虑连续的反斜杠)
int backslashCount = 0;
for (int j = i - 1; j >= 0 && template.charAt(j) == '\\'; j--) {
backslashCount++;
}
// 如果反斜杠数量是偶数,说明引号未被转义
if (backslashCount % 2 == 0) {
quoteBefore = i;
break;
}
}
}
if (quoteBefore == -1) {
return false;
}
// 向后查找匹配的未转义引号
int quoteAfter = -1;
for (int i = end; i < template.length(); i++) {
if (template.charAt(i) == quoteChar) {
// 检查是否被转义
int backslashCount = 0;
for (int j = i - 1; j >= 0 && template.charAt(j) == '\\'; j--) {
backslashCount++;
}
// 如果反斜杠数量是偶数,说明引号未被转义
if (backslashCount % 2 == 0) {
quoteAfter = i;
break;
}
}
}
// 如果找到了匹配的引号对,说明在引号内
return quoteAfter != -1 && quoteBefore < start && quoteAfter >= end;
}
/**
* 为已在引号内的上下文转换值
* @param value 要转换的值
* @param isSingleQuote 是否是单引号上下文
* @return 转换后的值(不包含引号,但已转义)
*/
private String convertValueForQuotedContext(Object value, boolean isSingleQuote) {
if (value == null) {
return "NULL";
}
String quoteChar = isSingleQuote ? "'" : "\"";
String escapeChar = isSingleQuote ? "''" : "\\\"";
if (value instanceof String) {
String strValue = (String) value;
// 转义引号
return strValue.replace(quoteChar, escapeChar);
} else if (value instanceof Number || value instanceof Boolean) {
// 数字和布尔类型直接转换
return value.toString();
} else if (value instanceof java.util.Date) {
// 日期类型转换为SQL标准格式
return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(value);
} else {
// 其他类型:转为JSON字符串并转义引号
String jsonValue = cn.hutool.json.JSONUtil.toJsonStr(value);
return jsonValue.replace(quoteChar, escapeChar);
}
}
}
@@ -16,59 +16,53 @@ import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/** MySQL连接诊断工具 用于排查连接问题 */
/**
* MySQL连接诊断工具
* 用于排查连接问题
*/
public class ConnectionDiagnostic {
public static void main(String[] args) {
// 测试不同的连接配置
testConnection(
"jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true",
"root",
"password");
testConnection(
"jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true",
"root",
"password");
testConnection(
"jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true&useLocalSessionState=true",
"root",
"password");
}
private static void testConnection(String url, String username, String password) {
System.out.println("=== 测试连接配置 ===");
System.out.println("URL: " + url);
System.out.println("Username: " + username);
try {
// 设置连接属性
Properties props = new Properties();
props.setProperty("user", username);
props.setProperty("password", password);
props.setProperty("useSSL", "false");
props.setProperty("allowPublicKeyRetrieval", "true");
props.setProperty("autoReconnect", "true");
props.setProperty("failOverReadOnly", "false");
props.setProperty("maxReconnects", "3");
props.setProperty("connectTimeout", "30000");
props.setProperty("socketTimeout", "30000");
Connection conn = DriverManager.getConnection(url, props);
System.out.println("✅ 连接成功!");
// 测试查询
conn.createStatement().executeQuery("SELECT 1");
System.out.println("✅ 查询测试成功!");
conn.close();
System.out.println("✅ 连接关闭成功!");
} catch (SQLException e) {
System.out.println("❌ 连接失败: " + e.getMessage());
System.out.println("错误代码: " + e.getErrorCode());
System.out.println("SQL状态: " + e.getSQLState());
public static void main(String[] args) {
// 测试不同的连接配置
testConnection("jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true", "root", "password");
testConnection("jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true&autoReconnect=true", "root", "password");
testConnection("jdbc:mysql://192.168.31.194:3306/test?useSSL=false&allowPublicKeyRetrieval=true&useLocalSessionState=true", "root", "password");
}
System.out.println();
}
private static void testConnection(String url, String username, String password) {
System.out.println("=== 测试连接配置 ===");
System.out.println("URL: " + url);
System.out.println("Username: " + username);
try {
// 设置连接属性
Properties props = new Properties();
props.setProperty("user", username);
props.setProperty("password", password);
props.setProperty("useSSL", "false");
props.setProperty("allowPublicKeyRetrieval", "true");
props.setProperty("autoReconnect", "true");
props.setProperty("failOverReadOnly", "false");
props.setProperty("maxReconnects", "3");
props.setProperty("connectTimeout", "30000");
props.setProperty("socketTimeout", "30000");
Connection conn = DriverManager.getConnection(url, props);
System.out.println("✅ 连接成功!");
// 测试查询
conn.createStatement().executeQuery("SELECT 1");
System.out.println("✅ 查询测试成功!");
conn.close();
System.out.println("✅ 连接关闭成功!");
} catch (SQLException e) {
System.out.println("❌ 连接失败: " + e.getMessage());
System.out.println("错误代码: " + e.getErrorCode());
System.out.println("SQL状态: " + e.getSQLState());
}
System.out.println();
}
}
@@ -0,0 +1,305 @@
package cn.universal.databridge.plugin.jdbc;
import static org.junit.jupiter.api.Assertions.*;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* DefaultJdbcOutPlugin 智能引号处理测试
* 测试兼容MySQL和PostgreSQL的引号处理功能
*
* @author gitee.com/NexIoT
* @version 1.0
* @since 2025/1/15
*/
@DisplayName("JDBC插件智能引号处理测试")
public class DefaultJdbcOutPluginQuoteTest {
private DefaultJdbcOutPlugin plugin;
private Map<String, Object> variables;
@BeforeEach
void setUp() {
plugin = new DefaultJdbcOutPlugin();
// 准备测试数据
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 14.3);
properties.put("illuminationDesc", "bright");
properties.put("csq", 25);
variables = new HashMap<>();
variables.put("deviceId", "nex123321");
variables.put("productKey", "Ru871cfJjhoM");
variables.put("deviceName", "期望值验证");
variables.put("iotId", "Ru871cfJjhoMnex123321");
variables.put("deviceNode", "DEVICE");
variables.put("messageType", "PROPERTIES");
variables.put("properties", properties);
}
/**
* 使用反射调用受保护的processTemplate方法
*/
private String processTemplate(String template, Map<String, Object> variables) {
try {
java.lang.reflect.Method method = DefaultJdbcOutPlugin.class.getDeclaredMethod(
"processTemplate", String.class, Map.class);
method.setAccessible(true);
return (String) method.invoke(plugin, template, variables);
} catch (Exception e) {
throw new RuntimeException("调用processTemplate方法失败", e);
}
}
@Test
@DisplayName("测试占位符在单引号内的情况(MySQL/PostgreSQL标准)")
void testPlaceholderInSingleQuotes() {
// 测试场景:'#{deviceId}' 应该替换为 'nex123321',不产生双重引号
String template = "INSERT INTO device_data(device_id) VALUES('#{deviceId}');";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 应该只包含一对单引号,不应该有双重引号
assertEquals("INSERT INTO device_data(device_id) VALUES('nex123321');", result);
assertFalse(result.contains("''"), "不应该包含双重单引号");
}
@Test
@DisplayName("测试PostgreSQL json_build_object场景")
void testPostgreSQLJsonBuildObject() {
// 用户提供的实际场景
String template =
"INSERT INTO device_data(device_id, product_key, device_name, iot_id, " +
"device_node, message_type, properties, raw_data) VALUES(" +
"'#{deviceId}', '#{productKey}', '#{deviceName}', '#{iotId}', " +
"'#{deviceNode}', '#{messageType}', '#{properties}', " +
"json_build_object(" +
"'csq', #{properties.temperature}, " +
"'tips', #{properties.illuminationDesc}" +
")::TEXT);";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 验证单引号内的占位符被正确替换,不产生双重引号
assertTrue(result.contains("'nex123321'"), "deviceId应该被正确替换");
assertTrue(result.contains("'Ru871cfJjhoM'"), "productKey应该被正确替换");
assertTrue(result.contains("'期望值验证'"), "deviceName应该被正确替换");
// 验证json_build_object中的占位符(不在引号内)被正确替换
assertTrue(result.contains("14.3"), "temperature应该被替换为数字");
assertTrue(result.contains("bright"), "illuminationDesc应该被替换");
// 不应该有双重引号
assertFalse(result.contains("''nex123321''"), "不应该有双重单引号");
}
@Test
@DisplayName("测试占位符在双引号内的情况(PostgreSQL JSON)")
void testPlaceholderInDoubleQuotes() {
String template = "SELECT \"#{deviceId}\" as id;";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 应该只包含一对双引号,不应该有双重引号
assertEquals("SELECT \"nex123321\" as id;", result);
assertFalse(result.contains("\"\""), "不应该包含双重双引号");
}
@Test
@DisplayName("测试占位符不在引号内的情况")
void testPlaceholderNotInQuotes() {
String template = "SELECT #{deviceId} as id;";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 不在引号内时,应该添加单引号
assertEquals("SELECT 'nex123321' as id;", result);
}
@Test
@DisplayName("测试字符串值中包含单引号的情况")
void testStringWithSingleQuote() {
variables.put("deviceName", "O'Brien");
String template = "INSERT INTO device_data(device_name) VALUES('#{deviceName}');";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 单引号应该被转义为两个单引号
assertTrue(result.contains("O''Brien"), "单引号应该被转义");
assertEquals("INSERT INTO device_data(device_name) VALUES('O''Brien');", result);
}
@Test
@DisplayName("测试字符串值中包含双引号的情况")
void testStringWithDoubleQuote() {
variables.put("description", "He said \"Hello\"");
String template = "SELECT \"#{description}\" as desc;";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 双引号应该被转义
assertTrue(result.contains("\\\""), "双引号应该被转义");
}
@Test
@DisplayName("测试NULL值处理")
void testNullValue() {
variables.put("optionalField", null);
String template = "INSERT INTO device_data(optional_field) VALUES('#{optionalField}');";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// NULL值应该被替换为NULL关键字
assertEquals("INSERT INTO device_data(optional_field) VALUES('NULL');", result);
}
@Test
@DisplayName("测试数字值处理")
void testNumberValue() {
String template = "INSERT INTO device_data(temperature) VALUES(#{properties.temperature});";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 数字值不在引号内时,应该直接使用,不添加引号
assertTrue(result.contains("14.3"), "数字值应该被正确替换");
assertFalse(result.contains("'14.3'"), "数字值不应该被引号包围");
}
@Test
@DisplayName("测试数字值在引号内的情况")
void testNumberInQuotes() {
String template = "INSERT INTO device_data(temperature) VALUES('#{properties.temperature}');";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 数字值在引号内时,应该直接替换,不添加引号
assertEquals("INSERT INTO device_data(temperature) VALUES('14.3');", result);
}
@Test
@DisplayName("测试嵌套属性访问")
void testNestedPropertyAccess() {
String template =
"INSERT INTO device_data(csq, temp) VALUES(" +
"'#{properties.csq}', #{properties.temperature});";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
assertTrue(result.contains("'25'"), "嵌套属性csq应该被正确替换");
assertTrue(result.contains("14.3"), "嵌套属性temperature应该被正确替换");
}
@Test
@DisplayName("测试混合场景:部分在引号内,部分不在")
void testMixedQuotes() {
String template =
"INSERT INTO device_data(device_id, product_key, temperature) VALUES(" +
"'#{deviceId}', '#{productKey}', #{properties.temperature});";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 在引号内的应该只替换值
assertTrue(result.contains("'nex123321'"), "引号内的deviceId应该被正确替换");
assertTrue(result.contains("'Ru871cfJjhoM'"), "引号内的productKey应该被正确替换");
// 不在引号内的应该添加引号
assertTrue(result.contains("'14.3'"), "不在引号内的temperature应该被添加引号");
}
@Test
@DisplayName("测试复杂的PostgreSQL JSON场景")
void testComplexPostgreSQLJson() {
String template =
"INSERT INTO device_data(raw_data) VALUES(" +
"json_build_object(" +
"'deviceId', '#{deviceId}', " +
"'temperature', #{properties.temperature}, " +
"'description', '#{properties.illuminationDesc}'" +
")::TEXT);";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 验证JSON字符串中的占位符被正确替换
assertTrue(result.contains("'nex123321'"), "JSON中的deviceId应该被正确替换");
assertTrue(result.contains("14.3"), "JSON中的temperature应该被正确替换");
assertTrue(result.contains("'bright'"), "JSON中的description应该被正确替换");
// 不应该有双重引号
assertFalse(result.contains("''nex123321''"), "不应该有双重单引号");
}
@Test
@DisplayName("测试布尔值处理")
void testBooleanValue() {
variables.put("isActive", true);
String template = "INSERT INTO device_data(is_active) VALUES(#{isActive});";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 布尔值不在引号内时,应该直接使用
assertTrue(result.contains("true"), "布尔值应该被正确替换");
}
@Test
@DisplayName("测试空字符串处理")
void testEmptyString() {
variables.put("emptyField", "");
String template = "INSERT INTO device_data(empty_field) VALUES('#{emptyField}');";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 空字符串应该被正确处理
assertEquals("INSERT INTO device_data(empty_field) VALUES('');", result);
}
@Test
@DisplayName("测试多个占位符在同一引号内")
void testMultiplePlaceholdersInQuotes() {
// 注意:这种情况在实际SQL中不常见,但测试边界情况
String template = "SELECT '#{deviceId}_#{productKey}' as combined;";
String result = processTemplate(template, variables);
System.out.println("模板: " + template);
System.out.println("结果: " + result);
// 两个占位符都在同一个引号内,应该都被正确替换
assertTrue(result.contains("nex123321"), "第一个占位符应该被替换");
assertTrue(result.contains("Ru871cfJjhoM"), "第二个占位符应该被替换");
}
}
@@ -34,31 +34,29 @@ public class QuickTemplateTest {
try {
// 测试通用实现
AbstractDataBridgePlugin genericPlugin =
new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
AbstractDataBridgePlugin genericPlugin = new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
return null;
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
@Override
public Boolean testConnection(ResourceConnection connection) {
return null;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return null;
}
}
@Override
public List<SourceScope> getSupportedSourceScopes() {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of();
}
};
java.lang.reflect.Method genericMethod =
AbstractDataBridgePlugin.class.getDeclaredMethod(
"processTemplate", String.class, Map.class);
}
};
java.lang.reflect.Method genericMethod =
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
genericMethod.setAccessible(true);
String genericResult = (String) genericMethod.invoke(genericPlugin, template, variables);
@@ -66,7 +64,7 @@ public class QuickTemplateTest {
// 测试JDBC实现
DefaultJdbcOutPlugin jdbcPlugin = new DefaultJdbcOutPlugin();
java.lang.reflect.Method jdbcMethod =
java.lang.reflect.Method jdbcMethod =
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
jdbcMethod.setAccessible(true);
String jdbcResult = (String) jdbcMethod.invoke(jdbcPlugin, template, variables);
@@ -106,33 +104,31 @@ public class QuickTemplateTest {
String template = "Nested: #{root.level2.value}";
AbstractDataBridgePlugin plugin =
new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
AbstractDataBridgePlugin plugin = new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
return null;
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
@Override
public Boolean testConnection(ResourceConnection connection) {
return null;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return null;
}
}
@Override
public List<SourceScope> getSupportedSourceScopes() {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of();
}
};
}
};
try {
java.lang.reflect.Method method =
AbstractDataBridgePlugin.class.getDeclaredMethod(
"processTemplate", String.class, Map.class);
java.lang.reflect.Method method =
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
method.setAccessible(true);
String result = (String) method.invoke(plugin, template, variables);
@@ -29,10 +29,10 @@ public class TemplateDemo {
variables.put("properties", properties);
// 测试模板
String template =
"Device: #{deviceId}, Product: #{productKey}, Name: #{deviceName}, "
+ "CSQ: #{properties.csq}, Code: #{properties.code}, "
+ "City: #{properties.location.city}, Country: #{properties.location.country}";
String template =
"Device: #{deviceId}, Product: #{productKey}, Name: #{deviceName}, " +
"CSQ: #{properties.csq}, Code: #{properties.code}, " +
"City: #{properties.location.city}, Country: #{properties.location.country}";
System.out.println("原始模板:");
System.out.println(template);
@@ -41,31 +41,29 @@ public class TemplateDemo {
try {
// 1. 通用模板处理
System.out.println("1. 通用模板处理 (AbstractDataBridgePlugin):");
AbstractDataBridgePlugin genericPlugin =
new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
AbstractDataBridgePlugin genericPlugin = new AbstractDataBridgePlugin() {
@Override
public PluginInfo getPluginInfo() {
return null;
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
@Override
public Boolean testConnection(ResourceConnection connection) {
return null;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return null;
}
}
@Override
public List<SourceScope> getSupportedSourceScopes() {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of();
}
};
java.lang.reflect.Method genericMethod =
AbstractDataBridgePlugin.class.getDeclaredMethod(
"processTemplate", String.class, Map.class);
}
};
java.lang.reflect.Method genericMethod =
AbstractDataBridgePlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
genericMethod.setAccessible(true);
String genericResult = (String) genericMethod.invoke(genericPlugin, template, variables);
System.out.println("结果: " + genericResult);
@@ -75,7 +73,7 @@ public class TemplateDemo {
// 2. JDBC专用模板处理
System.out.println("2. JDBC专用模板处理 (DefaultJdbcOutPlugin):");
DefaultJdbcOutPlugin jdbcPlugin = new DefaultJdbcOutPlugin();
java.lang.reflect.Method jdbcMethod =
java.lang.reflect.Method jdbcMethod =
DefaultJdbcOutPlugin.class.getDeclaredMethod("processTemplate", String.class, Map.class);
jdbcMethod.setAccessible(true);
String jdbcResult = (String) jdbcMethod.invoke(jdbcPlugin, template, variables);
@@ -104,8 +102,7 @@ public class TemplateDemo {
System.out.println("- 文本模板: Device #{deviceId} is online");
System.out.println();
System.out.println("JDBC实现适用于:");
System.out.println(
"- SQL模板: INSERT INTO devices(id, name) VALUES(#{deviceId}, #{deviceName})");
System.out.println("- SQL模板: INSERT INTO devices(id, name) VALUES(#{deviceId}, #{deviceName})");
System.out.println("- 需要SQL注入防护的场景");
System.out.println();
@@ -22,14 +22,14 @@ public class TemplateProcessingTest {
variables.put("productKey", "product001");
variables.put("deviceName", "测试设备");
variables.put("iotId", "iot001");
variables.put("deviceNode", IoTConstant.DeviceNode.DEVICE.toString());
variables.put("deviceNode", IoTConstant.DeviceNode.DEVICE.toString());
variables.put("messageType", "PROPERTIES");
variables.put("properties", properties);
variables.put("data", "raw_data_content");
// 测试模板
String template =
"""
// 测试模板
String template =
"""
{
"deviceId": "#{deviceId}",
"location": {
@@ -91,10 +91,10 @@ public class TemplateProcessingTest {
variables.put("properties", properties);
variables.put("data", "raw_data_content");
// 测试模板
String template =
"INSERT INTO device_data(device_id,product_key,device_name,iot_id,device_node,message_type,csq,code,temperature,properties,raw_data) "
+ "VALUES(#{deviceId},#{productKey},#{deviceName},#{iotId},#{deviceNode},#{messageType},#{properties.csq},#{properties.code},#{properties.temperature},#{properties},#{data});";
// 测试模板
String template =
"INSERT INTO device_data(device_id,product_key,device_name,iot_id,device_node,message_type,csq,code,temperature,properties,raw_data) "
+ "VALUES(#{deviceId},#{productKey},#{deviceName},#{iotId},#{deviceNode},#{messageType},#{properties.csq},#{properties.code},#{properties.temperature},#{properties},#{data});";
// 创建插件实例进行测试
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
@@ -141,8 +141,8 @@ public class TemplateProcessingTest {
variables.put("deviceId", "device001");
variables.put("properties", properties);
String template =
"INSERT INTO test_table(csq,code,temperature) VALUES(#{properties.csq},#{properties.code},#{properties.temperature});";
String template =
"INSERT INTO test_table(csq,code,temperature) VALUES(#{properties.csq},#{properties.code},#{properties.temperature});";
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
@@ -178,8 +178,8 @@ public class TemplateProcessingTest {
variables.put("deviceId", "device001");
variables.put("properties", properties);
String template =
"INSERT INTO test_table(csq,non_existent) VALUES(#{properties.csq},#{properties.nonExistent});";
String template =
"INSERT INTO test_table(csq,non_existent) VALUES(#{properties.csq},#{properties.nonExistent});";
DefaultJdbcOutPlugin plugin = new DefaultJdbcOutPlugin();
@@ -1,53 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-kafka</name>
<description>Kafka数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-kafka</name>
<description>Kafka数据桥接插件</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -57,18 +57,17 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
SourceScope.ALL_PRODUCTS,
SourceScope.SPECIFIC_PRODUCTS,
SourceScope.APPLICATION
);
}
@Override
protected void processProcessedData(
Object processedData,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
protected void processProcessedData(Object processedData, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try (KafkaProducer<String, String> producer = createKafkaProducer(connection)) {
String topic = extractTopic(config);
String payload;
if (processedData instanceof String) {
payload = (String) processedData;
@@ -77,22 +76,17 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
Map<String, Object> variables = buildTemplateVariables(request, config);
payload = processTemplate(config.getTemplate(), variables);
}
ProducerRecord<String, String> record = new ProducerRecord<>(topic, payload);
producer.send(
record,
(metadata, exception) -> {
if (exception != null) {
log.error("Kafka消息发送失败: {}", exception.getMessage(), exception);
} else {
log.debug(
"Kafka消息发送成功: topic={}, partition={}, offset={}",
metadata.topic(),
metadata.partition(),
metadata.offset());
}
});
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Kafka消息发送失败: {}", exception.getMessage(), exception);
} else {
log.debug("Kafka消息发送成功: topic={}, partition={}, offset={}",
metadata.topic(), metadata.partition(), metadata.offset());
}
});
} catch (Exception e) {
log.error("Kafka数据处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@@ -100,29 +94,20 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
}
@Override
protected void processTemplateResult(
String templateResult,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
protected void processTemplateResult(String templateResult, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try (KafkaProducer<String, String> producer = createKafkaProducer(connection)) {
String topic = extractTopic(config);
ProducerRecord<String, String> record = new ProducerRecord<>(topic, templateResult);
producer.send(
record,
(metadata, exception) -> {
if (exception != null) {
log.error("Kafka消息发送失败: {}", exception.getMessage(), exception);
} else {
log.debug(
"Kafka消息发送成功: topic={}, partition={}, offset={}",
metadata.topic(),
metadata.partition(),
metadata.offset());
}
});
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Kafka消息发送失败: {}", exception.getMessage(), exception);
} else {
log.debug("Kafka消息发送成功: topic={}, partition={}, offset={}",
metadata.topic(), metadata.partition(), metadata.offset());
}
});
} catch (Exception e) {
log.error("Kafka模板处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@@ -143,13 +128,13 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return StrUtil.isNotBlank(extractTopic(config)) && validateMagicScript(config.getMagicScript());
return StrUtil.isNotBlank(extractTopic(config))
&& validateMagicScript(config.getMagicScript());
}
private KafkaProducer<String, String> createKafkaProducer(ResourceConnection connection) {
Properties props = new Properties();
props.put(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
String.format("%s:%d", connection.getHost(), connection.getPort()));
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
@@ -158,37 +143,32 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
if (StrUtil.isNotBlank(connection.getUsername())) {
props.put("security.protocol", "SASL_PLAINTEXT");
props.put("sasl.mechanism", "PLAIN");
props.put(
"sasl.jaas.config",
String.format(
"org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
props.put("sasl.jaas.config",
String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
connection.getUsername(), connection.getPassword()));
}
return new KafkaProducer<>(props);
}
private AdminClient createAdminClient(ResourceConnection connection) {
Properties props = new Properties();
props.put(
AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
String.format("%s:%d", connection.getHost(), connection.getPort()));
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5000);
if (StrUtil.isNotBlank(connection.getUsername())) {
props.put("security.protocol", "SASL_PLAINTEXT");
props.put("sasl.mechanism", "PLAIN");
props.put(
"sasl.jaas.config",
String.format(
"org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
props.put("sasl.jaas.config",
String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
connection.getUsername(), connection.getPassword()));
}
return AdminClient.create(props);
}
@@ -197,7 +177,7 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
if (StrUtil.isBlank(config.getConfig())) {
return "iot-device-data"; // 默认topic
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
return configJson.getStr("topic", "iot-device-data");
@@ -207,21 +187,20 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
}
}
private Map<String, Object> buildTemplateVariables(
BaseUPRequest request, DataBridgeConfig config) {
private Map<String, Object> buildTemplateVariables(BaseUPRequest request, DataBridgeConfig config) {
Map<String, Object> variables = new HashMap<>();
// 设备信息
if (request.getIoTDeviceDTO() != null) {
variables.put("deviceKey", request.getIoTDeviceDTO().getDeviceId());
variables.put("productKey", request.getIoTDeviceDTO().getProductKey());
}
// 消息信息
variables.put("messageType", request.getMessageType().name());
variables.put("timestamp", System.currentTimeMillis());
variables.put("properties", request.getProperties());
// 配置信息
if (StrUtil.isNotBlank(config.getConfig())) {
try {
@@ -230,7 +209,7 @@ public class KafkaDataBridgePlugin extends AbstractDataOutputPlugin {
log.warn("解析配置JSON失败: {}", e.getMessage());
}
}
return variables;
}
}
}
@@ -18,179 +18,174 @@ import org.apache.kafka.clients.admin.AdminClientConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/** Kafka数据输入插件 支持从Kafka消费数据 */
/**
* Kafka数据输入插件
* 支持从Kafka消费数据
*/
@Component("kafkaInput")
@ConditionalOnProperty(
prefix = "databridge.plugins.kafka-input",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@ConditionalOnProperty(prefix = "databridge.plugins.kafka-input", name = "enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class KafkaDataInputPlugin extends AbstractDataInputPlugin {
@Override
public PluginInfo getPluginInfo() {
return PluginInfo.builder()
.name("Kafka数据输入插件")
.version("1.0.0")
.description("支持从Kafka消费数据,实现数据输入功能")
.author("gitee.com/NexIoT")
.pluginType("KAFKA_INPUT")
.supportedResourceTypes(List.of("KAFKA"))
.dataDirection(PluginInfo.DataDirection.INPUT)
.category("消息队列")
.icon("message")
.build();
}
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
}
@Override
protected void sendToIoTPlatform(Object processedData, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug("推送数据到IoT平台: 配置ID={}", config.getId());
}
@Override
protected void processExternalDataWithDefaultLogic(
Object externalData, DataBridgeConfig config, ResourceConnection connection) {
try {
// 处理外部数据,转换为IoT平台格式
BaseUPRequest request = transformToIoTData(externalData, config);
if (request != null) {
sendToIoTPlatform(request, config);
}
} catch (Exception e) {
log.error("处理外部数据失败: {}", e.getMessage(), e);
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
try (AdminClient adminClient = createAdminClient(connection)) {
// 尝试列出topics来测试连接
adminClient.listTopics().names().get();
return true;
} catch (Exception e) {
log.error("Kafka连接测试失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
// 验证统一配置
if (StrUtil.isBlank(config.getConfig())) {
return false;
@Override
public PluginInfo getPluginInfo() {
return PluginInfo.builder()
.name("Kafka数据输入插件")
.version("1.0.0")
.description("支持从Kafka消费数据,实现数据输入功能")
.author("gitee.com/NexIoT")
.pluginType("KAFKA_INPUT")
.supportedResourceTypes(List.of("KAFKA"))
.dataDirection(PluginInfo.DataDirection.INPUT)
.category("消息队列")
.icon("message")
.build();
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// 验证必要的配置项
return configJson.containsKey("topic") && StrUtil.isNotBlank(configJson.getStr("topic"));
} catch (Exception e) {
log.error("验证Kafka输入配置失败: {}", e.getMessage(), e);
return false;
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS,
SourceScope.SPECIFIC_PRODUCTS,
SourceScope.APPLICATION
);
}
}
private BaseUPRequest transformToIoTData(Object externalData, DataBridgeConfig config) {
try {
if (externalData instanceof String) {
String payload = (String) externalData;
JSONObject data = JSONUtil.parseObj(payload);
@Override
protected void sendToIoTPlatform(Object processedData, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug("推送数据到IoT平台: 配置ID={}", config.getId());
}
// 构建BaseUPRequest
BaseUPRequest request = new BaseUPRequest();
request.setMessageType(MessageType.PROPERTIES);
request.setProperties(new HashMap<>());
// 设置设备信息
IoTDeviceDTO deviceDTO = new IoTDeviceDTO();
deviceDTO.setDeviceId(data.getStr("deviceId", "unknown"));
deviceDTO.setProductKey(data.getStr("productKey", "unknown"));
request.setIoTDeviceDTO(deviceDTO);
// 设置属性
if (data.containsKey("properties")) {
Object properties = data.get("properties");
if (properties instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) properties;
request.setProperties(props);
}
@Override
protected void processExternalDataWithDefaultLogic(Object externalData, DataBridgeConfig config, ResourceConnection connection) {
try {
// 处理外部数据,转换为IoT平台格式
BaseUPRequest request = transformToIoTData(externalData, config);
if (request != null) {
sendToIoTPlatform(request, config);
}
} catch (Exception e) {
log.error("处理外部数据失败: {}", e.getMessage(), e);
}
return request;
}
} catch (Exception e) {
log.error("转换外部数据为IoT格式失败: {}", e.getMessage(), e);
}
return null;
}
private AdminClient createAdminClient(ResourceConnection connection) {
Properties props = new Properties();
props.put(
AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
String.format("%s:%d", connection.getHost(), connection.getPort()));
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5000);
if (StrUtil.isNotBlank(connection.getUsername())) {
props.put("security.protocol", "SASL_PLAINTEXT");
props.put("sasl.mechanism", "PLAIN");
props.put(
"sasl.jaas.config",
String.format(
"org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
connection.getUsername(), connection.getPassword()));
}
return AdminClient.create(props);
}
@Override
public Boolean testConnection(ResourceConnection connection) {
try (AdminClient adminClient = createAdminClient(connection)) {
// 尝试列出topics来测试连接
adminClient.listTopics().names().get();
return true;
} catch (Exception e) {
log.error("Kafka连接测试失败: {}", e.getMessage(), e);
return false;
}
}
// TODO: Kafka消费者创建方法,用于数据输入
// private KafkaConsumer<String, String> createKafkaConsumer(ResourceConnection connection, String
// topic) {
// Properties props = new Properties();
// props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
// String.format("%s:%d", connection.getHost(), connection.getPort()));
// props.put(ConsumerConfig.GROUP_ID_CONFIG, "databridge-input-group");
// props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
// StringDeserializer.class.getName());
// props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
// StringDeserializer.class.getName());
// props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
// props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
//
// if (StrUtil.isNotBlank(connection.getUsername())) {
// props.put("security.protocol", "SASL_PLAINTEXT");
// props.put("sasl.mechanism", "PLAIN");
// props.put("sasl.jaas.config",
// String.format("org.apache.kafka.common.security.plain.PlainLoginModule required
// username=\"%s\" password=\"%s\";",
// connection.getUsername(), connection.getPassword()));
// }
//
// KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// consumer.subscribe(Collections.singletonList(topic));
// return consumer;
// }
@Override
public Boolean validateConfig(DataBridgeConfig config) {
// 验证统一配置
if (StrUtil.isBlank(config.getConfig())) {
return false;
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// 验证必要的配置项
return configJson.containsKey("topic") &&
StrUtil.isNotBlank(configJson.getStr("topic"));
} catch (Exception e) {
log.error("验证Kafka输入配置失败: {}", e.getMessage(), e);
return false;
}
}
// TODO: 提取topic的方法,用于Kafka订阅
// private String extractTopic(DataBridgeConfig config) {
// try {
// JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// return configJson.getStr("topic", "iot-input-data");
// } catch (Exception e) {
// log.warn("解析Kafka输入配置失败,使用默认topic: {}", e.getMessage());
// return "iot-input-data";
// }
// }
}
private BaseUPRequest transformToIoTData(Object externalData, DataBridgeConfig config) {
try {
if (externalData instanceof String) {
String payload = (String) externalData;
JSONObject data = JSONUtil.parseObj(payload);
// 构建BaseUPRequest
BaseUPRequest request = new BaseUPRequest();
request.setMessageType(MessageType.PROPERTIES);
request.setProperties(new HashMap<>());
// 设置设备信息
IoTDeviceDTO deviceDTO = new IoTDeviceDTO();
deviceDTO.setDeviceId(data.getStr("deviceId", "unknown"));
deviceDTO.setProductKey(data.getStr("productKey", "unknown"));
request.setIoTDeviceDTO(deviceDTO);
// 设置属性
if (data.containsKey("properties")) {
Object properties = data.get("properties");
if (properties instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) properties;
request.setProperties(props);
}
}
return request;
}
} catch (Exception e) {
log.error("转换外部数据为IoT格式失败: {}", e.getMessage(), e);
}
return null;
}
private AdminClient createAdminClient(ResourceConnection connection) {
Properties props = new Properties();
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,
String.format("%s:%d", connection.getHost(), connection.getPort()));
props.put(AdminClientConfig.REQUEST_TIMEOUT_MS_CONFIG, 5000);
if (StrUtil.isNotBlank(connection.getUsername())) {
props.put("security.protocol", "SASL_PLAINTEXT");
props.put("sasl.mechanism", "PLAIN");
props.put("sasl.jaas.config",
String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
connection.getUsername(), connection.getPassword()));
}
return AdminClient.create(props);
}
// TODO: Kafka消费者创建方法,用于数据输入
// private KafkaConsumer<String, String> createKafkaConsumer(ResourceConnection connection, String topic) {
// Properties props = new Properties();
// props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
// String.format("%s:%d", connection.getHost(), connection.getPort()));
// props.put(ConsumerConfig.GROUP_ID_CONFIG, "databridge-input-group");
// props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
// props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
// props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
// props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
//
// if (StrUtil.isNotBlank(connection.getUsername())) {
// props.put("security.protocol", "SASL_PLAINTEXT");
// props.put("sasl.mechanism", "PLAIN");
// props.put("sasl.jaas.config",
// String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";",
// connection.getUsername(), connection.getPassword()));
// }
//
// KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
// consumer.subscribe(Collections.singletonList(topic));
// return consumer;
// }
// TODO: 提取topic的方法,用于Kafka订阅
// private String extractTopic(DataBridgeConfig config) {
// try {
// JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// return configJson.getStr("topic", "iot-input-data");
// } catch (Exception e) {
// log.warn("解析Kafka输入配置失败,使用默认topic: {}", e.getMessage());
// return "iot-input-data";
// }
// }
}
@@ -1,48 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-plugin-mqtt</name>
<description>MQTT数据桥接插件</description>
<name>cn-universal-data-bridge-plugin-mqtt</name>
<description>MQTT数据桥接插件</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -44,18 +44,17 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
SourceScope.ALL_PRODUCTS,
SourceScope.SPECIFIC_PRODUCTS,
SourceScope.APPLICATION
);
}
@Override
protected void processProcessedData(
Object processedData,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
protected void processProcessedData(Object processedData, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try (MqttClient client = createMqttClient(connection)) {
client.connect(buildOptions(connection));
String payload;
if (processedData instanceof String) {
payload = (String) processedData;
@@ -64,11 +63,11 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
Map<String, Object> variables = buildTemplateVariables(request, config);
payload = processTemplate(config.getTemplate(), variables);
}
String topic = extractTopic(config);
client.publish(topic, new MqttMessage(payload.getBytes()));
client.disconnect();
} catch (Exception e) {
log.error("MQTT数据处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@@ -76,18 +75,14 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
}
@Override
protected void processTemplateResult(
String templateResult,
BaseUPRequest request,
DataBridgeConfig config,
ResourceConnection connection) {
protected void processTemplateResult(String templateResult, BaseUPRequest request, DataBridgeConfig config, ResourceConnection connection) {
try (MqttClient client = createMqttClient(connection)) {
client.connect(buildOptions(connection));
String topic = extractTopic(config);
client.publish(topic, new MqttMessage(templateResult.getBytes()));
client.disconnect();
} catch (Exception e) {
log.error("MQTT模板处理失败: {}", e.getMessage(), e);
throw new RuntimeException(e);
@@ -111,7 +106,8 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
@Override
public Boolean validateConfig(DataBridgeConfig config) {
return StrUtil.isNotBlank(extractTopic(config)) && validateMagicScript(config.getMagicScript());
return StrUtil.isNotBlank(extractTopic(config))
&& validateMagicScript(config.getMagicScript());
}
private MqttClient createMqttClient(ResourceConnection connection) throws Exception {
@@ -124,14 +120,14 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
options.setCleanSession(true);
options.setConnectionTimeout(30);
options.setKeepAliveInterval(60);
if (StrUtil.isNotBlank(connection.getUsername())) {
options.setUserName(connection.getUsername());
}
if (StrUtil.isNotBlank(connection.getPassword())) {
options.setPassword(connection.getPassword().toCharArray());
}
return options;
}
@@ -140,7 +136,7 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
if (StrUtil.isBlank(config.getConfig())) {
return "/iot/bridge"; // 默认topic
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
return configJson.getStr("topic", "/iot/bridge");
@@ -150,21 +146,20 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
}
}
private Map<String, Object> buildTemplateVariables(
BaseUPRequest request, DataBridgeConfig config) {
private Map<String, Object> buildTemplateVariables(BaseUPRequest request, DataBridgeConfig config) {
Map<String, Object> variables = new HashMap<>();
// 设备信息
if (request.getIoTDeviceDTO() != null) {
variables.put("deviceKey", request.getIoTDeviceDTO().getDeviceId());
variables.put("productKey", request.getIoTDeviceDTO().getProductKey());
}
// 消息信息
variables.put("messageType", request.getMessageType().name());
variables.put("timestamp", System.currentTimeMillis());
variables.put("properties", request.getProperties());
// 配置信息
if (StrUtil.isNotBlank(config.getConfig())) {
try {
@@ -173,7 +168,7 @@ public class MqttDataBridgePlugin extends AbstractDataOutputPlugin {
log.warn("解析配置JSON失败: {}", e.getMessage());
}
}
return variables;
}
}
}
@@ -17,18 +17,17 @@ import org.eclipse.paho.client.mqttv3.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
/** MQTT数据输入插件 支持从MQTT订阅数据 */
/**
* MQTT数据输入插件
* 支持从MQTT订阅数据
*/
@Component("mqttInput")
@ConditionalOnProperty(
prefix = "databridge.plugins.mqtt-input",
name = "enabled",
havingValue = "true",
matchIfMissing = true)
@ConditionalOnProperty(prefix = "databridge.plugins.mqtt-input", name = "enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class MqttDataInputPlugin extends AbstractDataInputPlugin {
// TODO: 存储正在运行的MQTT客户端,用于管理连接生命周期
// private final Map<Long, MqttClient> runningClients = new ConcurrentHashMap<>();
// TODO: 存储正在运行的MQTT客户端,用于管理连接生命周期
// private final Map<Long, MqttClient> runningClients = new ConcurrentHashMap<>();
@Override
public PluginInfo getPluginInfo() {
@@ -48,122 +47,125 @@ public class MqttDataInputPlugin extends AbstractDataInputPlugin {
@Override
public List<SourceScope> getSupportedSourceScopes() {
return List.of(
SourceScope.ALL_PRODUCTS, SourceScope.SPECIFIC_PRODUCTS, SourceScope.APPLICATION);
SourceScope.ALL_PRODUCTS,
SourceScope.SPECIFIC_PRODUCTS,
SourceScope.APPLICATION
);
}
@Override
protected void sendToIoTPlatform(Object processedData, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug("推送数据到IoT平台: 配置ID={}", config.getId());
}
@Override
protected void processExternalDataWithDefaultLogic(
Object externalData, DataBridgeConfig config, ResourceConnection connection) {
try {
// 处理外部数据,转换为IoT平台格式
BaseUPRequest request = transformToIoTData(externalData, config);
if (request != null) {
sendToIoTPlatform(request, config);
}
} catch (Exception e) {
log.error("处理外部数据失败: {}", e.getMessage(), e);
}
}
@Override
public Boolean testConnection(ResourceConnection connection) {
try {
String broker = String.format("tcp://%s:%d", connection.getHost(), connection.getPort());
MqttClient client = new MqttClient(broker, MqttClient.generateClientId());
MqttConnectOptions options = buildOptions(connection);
client.connect(options);
client.disconnect();
client.close();
return true;
} catch (Exception e) {
log.error("MQTT连接测试失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
// 验证统一配置
if (StrUtil.isBlank(config.getConfig())) {
return false;
@Override
protected void sendToIoTPlatform(Object processedData, DataBridgeConfig config) {
// TODO: 实现推送到IoT平台的逻辑
// 这里可以通过消息队列、HTTP API等方式将数据推送到IoT平台
log.debug("推送数据到IoT平台: 配置ID={}", config.getId());
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// 验证必要的配置项
return configJson.containsKey("topic") && StrUtil.isNotBlank(configJson.getStr("topic"));
} catch (Exception e) {
log.error("验证MQTT输入配置失败: {}", e.getMessage(), e);
return false;
}
}
private BaseUPRequest transformToIoTData(Object externalData, DataBridgeConfig config) {
try {
if (externalData instanceof String) {
String payload = (String) externalData;
JSONObject data = JSONUtil.parseObj(payload);
// 构建BaseUPRequest
BaseUPRequest request = new BaseUPRequest();
request.setMessageType(MessageType.PROPERTIES);
request.setProperties(new HashMap<>());
// 设置设备信息
IoTDeviceDTO deviceDTO = new IoTDeviceDTO();
deviceDTO.setDeviceId(data.getStr("deviceId", "unknown"));
deviceDTO.setProductKey(data.getStr("productKey", "unknown"));
request.setIoTDeviceDTO(deviceDTO);
// 设置属性
if (data.containsKey("properties")) {
Object properties = data.get("properties");
if (properties instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) properties;
request.setProperties(props);
}
@Override
protected void processExternalDataWithDefaultLogic(Object externalData, DataBridgeConfig config, ResourceConnection connection) {
try {
// 处理外部数据,转换为IoT平台格式
BaseUPRequest request = transformToIoTData(externalData, config);
if (request != null) {
sendToIoTPlatform(request, config);
}
} catch (Exception e) {
log.error("处理外部数据失败: {}", e.getMessage(), e);
}
return request;
}
} catch (Exception e) {
log.error("转换外部数据为IoT格式失败: {}", e.getMessage(), e);
}
return null;
}
private MqttConnectOptions buildOptions(ResourceConnection connection) {
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setConnectionTimeout(30);
options.setKeepAliveInterval(60);
if (StrUtil.isNotBlank(connection.getUsername())) {
options.setUserName(connection.getUsername());
}
if (StrUtil.isNotBlank(connection.getPassword())) {
options.setPassword(connection.getPassword().toCharArray());
}
return options;
}
@Override
public Boolean testConnection(ResourceConnection connection) {
try {
String broker = String.format("tcp://%s:%d", connection.getHost(), connection.getPort());
MqttClient client = new MqttClient(broker, MqttClient.generateClientId());
MqttConnectOptions options = buildOptions(connection);
client.connect(options);
client.disconnect();
client.close();
return true;
} catch (Exception e) {
log.error("MQTT连接测试失败: {}", e.getMessage(), e);
return false;
}
}
// TODO: 提取topic的方法,用于MQTT订阅
// private String extractTopic(DataBridgeConfig config) {
// try {
// JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// return configJson.getStr("topic", "/iot/input");
// } catch (Exception e) {
// log.warn("解析MQTT输入配置失败,使用默认topic: {}", e.getMessage());
// return "/iot/input";
// }
// }
}
@Override
public Boolean validateConfig(DataBridgeConfig config) {
// 验证统一配置
if (StrUtil.isBlank(config.getConfig())) {
return false;
}
try {
JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// 验证必要的配置项
return configJson.containsKey("topic") &&
StrUtil.isNotBlank(configJson.getStr("topic"));
} catch (Exception e) {
log.error("验证MQTT输入配置失败: {}", e.getMessage(), e);
return false;
}
}
private BaseUPRequest transformToIoTData(Object externalData, DataBridgeConfig config) {
try {
if (externalData instanceof String) {
String payload = (String) externalData;
JSONObject data = JSONUtil.parseObj(payload);
// 构建BaseUPRequest
BaseUPRequest request = new BaseUPRequest();
request.setMessageType(MessageType.PROPERTIES);
request.setProperties(new HashMap<>());
// 设置设备信息
IoTDeviceDTO deviceDTO = new IoTDeviceDTO();
deviceDTO.setDeviceId(data.getStr("deviceId", "unknown"));
deviceDTO.setProductKey(data.getStr("productKey", "unknown"));
request.setIoTDeviceDTO(deviceDTO);
// 设置属性
if (data.containsKey("properties")) {
Object properties = data.get("properties");
if (properties instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> props = (Map<String, Object>) properties;
request.setProperties(props);
}
}
return request;
}
} catch (Exception e) {
log.error("转换外部数据为IoT格式失败: {}", e.getMessage(), e);
}
return null;
}
private MqttConnectOptions buildOptions(ResourceConnection connection) {
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setConnectionTimeout(30);
options.setKeepAliveInterval(60);
if (StrUtil.isNotBlank(connection.getUsername())) {
options.setUserName(connection.getUsername());
}
if (StrUtil.isNotBlank(connection.getPassword())) {
options.setPassword(connection.getPassword().toCharArray());
}
return options;
}
// TODO: 提取topic的方法,用于MQTT订阅
// private String extractTopic(DataBridgeConfig config) {
// try {
// JSONObject configJson = JSONUtil.parseObj(config.getConfig());
// return configJson.getStr("topic", "/iot/input");
// } catch (Exception e) {
// log.warn("解析MQTT输入配置失败,使用默认topic: {}", e.getMessage());
// return "/iot/input";
// }
// }
}
@@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<packaging>pom</packaging>
<artifactId>cn-universal-data-bridge-plugins</artifactId>
<packaging>pom</packaging>
<name>cn-universal-data-bridge-plugins</name>
<description>数据桥接插件模块</description>
<name>cn-universal-data-bridge-plugins</name>
<description>数据桥接插件模块</description>
<modules>
<module>cn-universal-data-bridge-plugin-jdbc</module>
<module>cn-universal-data-bridge-plugin-kafka</module>
<module>cn-universal-data-bridge-plugin-mqtt</module>
<module>cn-universal-data-bridge-plugin-http</module>
<module>cn-universal-data-bridge-plugin-iotdb</module>
<module>cn-universal-data-bridge-plugin-influxdb</module>
</modules>
<modules>
<module>cn-universal-data-bridge-plugin-jdbc</module>
<module>cn-universal-data-bridge-plugin-kafka</module>
<module>cn-universal-data-bridge-plugin-mqtt</module>
<module>cn-universal-data-bridge-plugin-http</module>
<module>cn-universal-data-bridge-plugin-iotdb</module>
<module>cn-universal-data-bridge-plugin-influxdb</module>
</modules>
</project>
@@ -1,68 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-starter</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-starter</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-starter</name>
<description>数据桥接自动配置</description>
<name>cn-universal-data-bridge-starter</name>
<description>数据桥接自动配置</description>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>
@@ -11,14 +11,15 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* 数据桥接自动配置类
*
* <p>负责自动装配数据桥接相关的组件和插件
* <p>负责自动装配数据桥接相关的组件和插件</p>
*
* <p><b>注意:</b>@EnableAsync 已在主启动类中全局启用,此处无需重复配置
* <p><b>注意:</b>@EnableAsync 已在主启动类中全局启用,此处无需重复配置</p>
*
* @author NexIoT
* @since 1.0
@@ -1,74 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<parent>
<artifactId>cn-universal-data-bridge</artifactId>
<groupId>cn.universal.iot</groupId>
<version>1.5-SNAPSHOT</version>
</parent>
<artifactId>cn-universal-data-bridge-web</artifactId>
<packaging>jar</packaging>
<artifactId>cn-universal-data-bridge-web</artifactId>
<packaging>jar</packaging>
<name>cn-universal-data-bridge-web</name>
<description>数据桥接Web接口(预留)</description>
<name>cn-universal-data-bridge-web</name>
<description>数据桥接Web接口(预留)</description>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-starter</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 插件依赖 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-security</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-starter</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 插件依赖 -->
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-jdbc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-kafka</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-mqtt</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-iotdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-data-bridge-plugin-influxdb</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.universal.iot</groupId>
<artifactId>cn-universal-security</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

Some files were not shown because too many files have changed in this diff Show More