From b537faeb9974e2035c7d6b52abe71881776df033 Mon Sep 17 00:00:00 2001
From: wjh <1693415692@qq.com>
Date: Fri, 9 Oct 2026 09:34:37 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=BC=80=E6=94=BE?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=BD=91=E5=85=B3=E5=BE=AE=E6=9C=8D=E5=8A=A1?=
=?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=B7=A5=E7=A8=8B=20(OAuth2.0=20+?=
=?UTF-8?q?=20MCP=20+=20OpenAPI=20+=20Webhook)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 38 ++++
Jenkinsfile | 49 ++++++
README.md | 13 ++
pom.xml | 123 +++++++++++++
.../iot/gateway/IotGatewayApplication.java | 14 ++
.../admin/OpenClientAdminController.java | 129 ++++++++++++++
.../admin/OpenGatewayInfoController.java | 54 ++++++
.../admin/OpenWebhookAdminController.java | 108 ++++++++++++
.../com/iot/gateway/common/BizException.java | 18 ++
.../common/GlobalExceptionHandler.java | 37 ++++
.../java/com/iot/gateway/common/Result.java | 35 ++++
.../com/iot/gateway/entity/OpenApiLog.java | 29 ++++
.../com/iot/gateway/entity/OpenClient.java | 37 ++++
.../com/iot/gateway/entity/OpenWebhook.java | 32 ++++
.../iot/gateway/mapper/OpenApiLogMapper.java | 9 +
.../iot/gateway/mapper/OpenClientMapper.java | 9 +
.../iot/gateway/mapper/OpenWebhookMapper.java | 9 +
.../com/iot/gateway/mcp/McpSseController.java | 163 ++++++++++++++++++
.../com/iot/gateway/mcp/McpToolRegistry.java | 121 +++++++++++++
.../gateway/oauth/GatewaySecurityContext.java | 43 +++++
.../iot/gateway/oauth/JwtTokenProvider.java | 72 ++++++++
.../oauth/OAuth2AuthenticationFilter.java | 82 +++++++++
.../gateway/oauth/OAuth2TokenController.java | 95 ++++++++++
.../com/iot/gateway/oauth/SecurityConfig.java | 45 +++++
.../gateway/openapi/OpenAlarmController.java | 51 ++++++
.../openapi/OpenCommandController.java | 40 +++++
.../gateway/openapi/OpenDeviceController.java | 65 +++++++
.../openapi/OpenTelemetryController.java | 63 +++++++
.../webhook/WebhookDispatchService.java | 70 ++++++++
.../webhook/WebhookReceiveController.java | 35 ++++
src/main/resources/application-prod.yml | 12 ++
src/main/resources/application.yml | 44 +++++
32 files changed, 1744 insertions(+)
create mode 100644 .gitignore
create mode 100644 Jenkinsfile
create mode 100644 README.md
create mode 100644 pom.xml
create mode 100644 src/main/java/com/iot/gateway/IotGatewayApplication.java
create mode 100644 src/main/java/com/iot/gateway/admin/OpenClientAdminController.java
create mode 100644 src/main/java/com/iot/gateway/admin/OpenGatewayInfoController.java
create mode 100644 src/main/java/com/iot/gateway/admin/OpenWebhookAdminController.java
create mode 100644 src/main/java/com/iot/gateway/common/BizException.java
create mode 100644 src/main/java/com/iot/gateway/common/GlobalExceptionHandler.java
create mode 100644 src/main/java/com/iot/gateway/common/Result.java
create mode 100644 src/main/java/com/iot/gateway/entity/OpenApiLog.java
create mode 100644 src/main/java/com/iot/gateway/entity/OpenClient.java
create mode 100644 src/main/java/com/iot/gateway/entity/OpenWebhook.java
create mode 100644 src/main/java/com/iot/gateway/mapper/OpenApiLogMapper.java
create mode 100644 src/main/java/com/iot/gateway/mapper/OpenClientMapper.java
create mode 100644 src/main/java/com/iot/gateway/mapper/OpenWebhookMapper.java
create mode 100644 src/main/java/com/iot/gateway/mcp/McpSseController.java
create mode 100644 src/main/java/com/iot/gateway/mcp/McpToolRegistry.java
create mode 100644 src/main/java/com/iot/gateway/oauth/GatewaySecurityContext.java
create mode 100644 src/main/java/com/iot/gateway/oauth/JwtTokenProvider.java
create mode 100644 src/main/java/com/iot/gateway/oauth/OAuth2AuthenticationFilter.java
create mode 100644 src/main/java/com/iot/gateway/oauth/OAuth2TokenController.java
create mode 100644 src/main/java/com/iot/gateway/oauth/SecurityConfig.java
create mode 100644 src/main/java/com/iot/gateway/openapi/OpenAlarmController.java
create mode 100644 src/main/java/com/iot/gateway/openapi/OpenCommandController.java
create mode 100644 src/main/java/com/iot/gateway/openapi/OpenDeviceController.java
create mode 100644 src/main/java/com/iot/gateway/openapi/OpenTelemetryController.java
create mode 100644 src/main/java/com/iot/gateway/webhook/WebhookDispatchService.java
create mode 100644 src/main/java/com/iot/gateway/webhook/WebhookReceiveController.java
create mode 100644 src/main/resources/application-prod.yml
create mode 100644 src/main/resources/application.yml
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1e7daf5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Log and Temp ###
+*.log
+*.tmp
+*.zip
+*.tar.gz
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..a4cb55a
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,49 @@
+pipeline {
+ agent any
+
+ tools {
+ maven 'Maven-3.8'
+ jdk 'JDK17'
+ }
+
+ environment {
+ DEPLOY_PORT = '18082'
+ APP_NAME = 'iot-open-gateway'
+ DEPLOY_DIR = '/data/iot/gateway'
+ JENKINS_NODE_COOKIE = 'dontKillMe'
+ BUILD_ID = 'dontKillMe'
+ }
+
+ stages {
+ stage('Deploy & Run') {
+ steps {
+ sh '''
+ echo "=== 启动 IOT 开放网关微服务 ==="
+ mkdir -p "${DEPLOY_DIR}"
+
+ if [ -f "/data/iot/gateway/source/target/iot-open-gateway-1.0.0.jar" ]; then
+ cp -f "/data/iot/gateway/source/target/iot-open-gateway-1.0.0.jar" "${DEPLOY_DIR}/${APP_NAME}.jar"
+ fi
+
+ PID=$(lsof -ti:${DEPLOY_PORT} || true)
+ if [ -n "$PID" ]; then
+ echo "停止旧进程 $PID"
+ kill -9 $PID || true
+ sleep 2
+ fi
+
+ export JENKINS_NODE_COOKIE=dontKillMe
+ export BUILD_ID=dontKillMe
+ nohup java -jar -Dspring.profiles.active=prod "${DEPLOY_DIR}/${APP_NAME}.jar" > "${DEPLOY_DIR}/${APP_NAME}.log" 2>&1 &
+
+ echo "后台启动中..."
+ sleep 6
+ lsof -i:${DEPLOY_PORT} || true
+ echo "=== 网关健康检查 ==="
+ curl -s http://127.0.0.1:${DEPLOY_PORT}/open/gateway/health || true
+ echo ""
+ '''
+ }
+ }
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e61d2c1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# IOT-open-gateway 统一开放平台网关
+
+基于 Spring Boot 3.2.11 + Java 17 研发的独立开放网关服务,与主业务后端 `IOT-backend` 完全物理隔离解耦,专门负责:
+1. **OAuth 2.0 授权服务器 (Client Credentials)**:客户端通过 `/oauth2/token` 凭借 `client_id` + `client_secret` 申请并获取强绑定租户的 JWT 访问令牌;
+2. **MCP Server 协议适配层 (面向 AI 智能体生态)**:提供标准 HTTP+SSE 端点 `/mcp/sse` 与 `/mcp/messages`,对外暴露 `iot_device_list`、`iot_telemetry_latest`、`iot_device_control`、`iot_alarm_query` 工业级物联能力;
+3. **REST OpenAPI 业务适配层 (面向传统三方企业系统)**:对外暴露 `/open/v1/devices/**`、`/open/v1/telemetry/**`、`/open/v1/commands`、`/open/v1/alarms`;
+4. **三方双向集成与 Webhook 调度**:支持向企业微信、钉钉、飞书群机器人主动推送告警与事件,支持接收外部系统事件回调;
+5. **解耦高可用保活**:主后端 `IOT-backend` 发版重启期间,网关与外部所有的 MCP 长连接会话保持不中断。
+
+## 启动与运行
+- 本地开发:端口 `8082`
+- 生产环境:端口 `18082`(激活 `prod` profile)
+- Swagger 接口文档:`http://localhost:8082/swagger-ui.html`
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b364eac
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,123 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.11
+
+
+
+ com.iot
+ iot-open-gateway
+ 1.0.0
+ iot-open-gateway
+ IOT 物联网平台统一开放网关与三方集成中台 (OAuth 2.0 + MCP + OpenAPI + Webhook)
+
+
+ 17
+ 3.5.7
+ 2.5.0
+ 0.11.5
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ ${mybatis-plus.version}
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ ${jjwt.version}
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ ${jjwt.version}
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ ${jjwt.version}
+ runtime
+
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
diff --git a/src/main/java/com/iot/gateway/IotGatewayApplication.java b/src/main/java/com/iot/gateway/IotGatewayApplication.java
new file mode 100644
index 0000000..d599781
--- /dev/null
+++ b/src/main/java/com/iot/gateway/IotGatewayApplication.java
@@ -0,0 +1,14 @@
+package com.iot.gateway;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@MapperScan("com.iot.gateway.mapper")
+public class IotGatewayApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(IotGatewayApplication.class, args);
+ }
+}
diff --git a/src/main/java/com/iot/gateway/admin/OpenClientAdminController.java b/src/main/java/com/iot/gateway/admin/OpenClientAdminController.java
new file mode 100644
index 0000000..1b432e4
--- /dev/null
+++ b/src/main/java/com/iot/gateway/admin/OpenClientAdminController.java
@@ -0,0 +1,129 @@
+package com.iot.gateway.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.iot.gateway.common.BizException;
+import com.iot.gateway.common.Result;
+import com.iot.gateway.entity.OpenClient;
+import com.iot.gateway.mapper.OpenClientMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.UUID;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
+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.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;
+
+@Slf4j
+@Tag(name = "开放平台管理 - 三方应用凭据管理")
+@RestController
+@RequestMapping("/api/open/client")
+public class OpenClientAdminController {
+
+ @Autowired
+ private OpenClientMapper clientMapper;
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ @Operation(summary = "三方应用凭据分页列表")
+ @GetMapping("/page")
+ public Result> page(
+ @RequestParam(defaultValue = "1") Integer pageNum,
+ @RequestParam(defaultValue = "10") Integer pageSize,
+ @RequestParam(required = false) Long tenantId,
+ @RequestParam(required = false) String keyword) {
+
+ Page page = new Page<>(pageNum, pageSize);
+ LambdaQueryWrapper qw = new LambdaQueryWrapper<>();
+ if (tenantId != null) {
+ qw.eq(OpenClient::getTenantId, tenantId);
+ }
+ if (StringUtils.hasText(keyword)) {
+ qw.and(w -> w.like(OpenClient::getClientName, keyword)
+ .or().like(OpenClient::getClientId, keyword));
+ }
+ qw.orderByDesc(OpenClient::getCreateTime);
+ return Result.success(clientMapper.selectPage(page, qw));
+ }
+
+ @Operation(summary = "获取凭据详情")
+ @GetMapping("/{id}")
+ public Result getById(@PathVariable Long id) {
+ OpenClient client = clientMapper.selectById(id);
+ if (client == null) throw new BizException("客户端凭据不存在");
+ return Result.success(client);
+ }
+
+ @Operation(summary = "新建三方应用凭据 (自动生成 ClientId 与 Secret)")
+ @PostMapping
+ public Result create(@RequestBody OpenClient req) {
+ req.setId(null);
+ req.setClientId("client_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12));
+ req.setClientSecret("sec_" + UUID.randomUUID().toString().replace("-", "") + Long.toHexString(RANDOM.nextLong()));
+ if (req.getTenantId() == null) req.setTenantId(1L);
+ if (!StringUtils.hasText(req.getScopes())) req.setScopes("device.read");
+ if (req.getStatus() == null) req.setStatus(1);
+ if (req.getRateLimit() == null) req.setRateLimit(60);
+
+ clientMapper.insert(req);
+ log.info("新建开放应用凭据: id={}, clientId={}", req.getId(), req.getClientId());
+ return Result.success(req);
+ }
+
+ @Operation(summary = "修改三方应用凭据")
+ @PutMapping("/{id}")
+ public Result update(@PathVariable Long id, @RequestBody OpenClient req) {
+ OpenClient client = clientMapper.selectById(id);
+ if (client == null) throw new BizException("客户端凭据不存在");
+
+ client.setClientName(req.getClientName());
+ if (req.getScopes() != null) client.setScopes(req.getScopes());
+ client.setIpWhitelist(req.getIpWhitelist());
+ if (req.getRateLimit() != null) client.setRateLimit(req.getRateLimit());
+ if (req.getStatus() != null) client.setStatus(req.getStatus());
+ client.setExpireTime(req.getExpireTime());
+ client.setRemark(req.getRemark());
+
+ clientMapper.updateById(client);
+ return Result.success();
+ }
+
+ @Operation(summary = "删除三方应用凭据")
+ @DeleteMapping("/{id}")
+ public Result delete(@PathVariable Long id) {
+ clientMapper.deleteById(id);
+ return Result.success();
+ }
+
+ @Operation(summary = "重置客户端密钥 (Secret)")
+ @PostMapping("/{id}/reset-secret")
+ public Result