集群问题处理
This commit is contained in:
2
jeecg-boot/.gitignore
vendored
2
jeecg-boot/.gitignore
vendored
@@ -12,6 +12,8 @@ rebel.xml
|
|||||||
## backend
|
## backend
|
||||||
**/target
|
**/target
|
||||||
**/logs
|
**/logs
|
||||||
|
# 开发者本机钉钉 Stream 接收配置(从 application-dev-local.yml.example 复制)
|
||||||
|
**/application-dev-local.yml
|
||||||
|
|
||||||
## front
|
## front
|
||||||
**/*.lock
|
**/*.lock
|
||||||
|
|||||||
@@ -859,6 +859,26 @@ jeecgboot-vue3/src/views/xslmes/approval/integration/components/DingApprovalFore
|
|||||||
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
jeecgboot-vue3/src/views/xslmes/approval/integration/components/MesXslApprovalTraceDrawer.vue
|
||||||
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
jeecgboot-vue3/src/views/xslmes/approval/integration/MesXslApprovalTrace.data.ts
|
||||||
|
|
||||||
|
-- author:GHT---date:20260609--for: 【钉钉Stream开发】第三方配置页可视化Stream接收节点 -----
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_145__sys_third_app_config_stream_node.sql
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamNodeConfigService.java
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/DingTalkStreamConfigController.java
|
||||||
|
jeecgboot-vue3/src/views/system/appconfig/ThirdApp.data.ts
|
||||||
|
jeecgboot-vue3/src/views/system/appconfig/ThirdApp.api.ts
|
||||||
|
jeecgboot-vue3/src/views/system/appconfig/ThirdAppConfigModal.vue
|
||||||
|
jeecgboot-vue3/src/views/system/appconfig/ThirdAppDingTalkConfigForm.vue
|
||||||
|
|
||||||
|
-- author:GHT---date:20260609--for: 【钉钉Stream开发】本机白名单仅指定电脑接收回调 -----
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamProperties.java
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingApprovalReconcileScheduler.java
|
||||||
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamHealthMonitor.java
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
|
||||||
|
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev-local.yml.example
|
||||||
|
jeecg-boot/.gitignore
|
||||||
|
|
||||||
-- author:GHT---date:20260609--for: 【钉钉Stream集群】Redis选主单节点建连+存活监控 -----
|
-- author:GHT---date:20260609--for: 【钉钉Stream集群】Redis选主单节点建连+存活监控 -----
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamProperties.java
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamProperties.java
|
||||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamLeaderElection.java
|
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamLeaderElection.java
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.jeecg.modules.xslmes.dingtalk.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.jeecg.common.api.vo.Result;
|
||||||
|
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamNodeConfigService;
|
||||||
|
import org.jeecg.modules.xslmes.dingtalk.stream.DingTalkStreamSdkRunner;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钉钉 Stream 节点配置辅助接口(供「第三方配置-钉钉集成」页面展示本机信息)。
|
||||||
|
*/
|
||||||
|
@Tag(name = "钉钉Stream配置")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/xslmes/dingtalk/stream")
|
||||||
|
public class DingTalkStreamConfigController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页展示本机节点信息-----------
|
||||||
|
@Operation(summary = "获取本机 Stream 节点信息")
|
||||||
|
@GetMapping("/nodeInfo")
|
||||||
|
public Result<Map<String, Object>> nodeInfo() {
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>(nodeConfigService.buildNodeInfoSnapshot());
|
||||||
|
DingTalkStreamSdkRunner.ConnectionSnapshot snap = DingTalkStreamSdkRunner.snapshot();
|
||||||
|
data.put("streamRunning", snap.streamRunning());
|
||||||
|
data.put("totalEvents", snap.totalEventCount());
|
||||||
|
data.put("reconnectCount", snap.reconnectCount());
|
||||||
|
return Result.OK(data);
|
||||||
|
}
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页展示本机节点信息-----------
|
||||||
|
}
|
||||||
@@ -61,9 +61,15 @@ public class DingApprovalReconcileScheduler {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private DingBpmsEventProcessor eventProcessor;
|
private DingBpmsEventProcessor eventProcessor;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||||
|
|
||||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream补偿扫描】漏推回调自动修复-----
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream补偿扫描】漏推回调自动修复-----
|
||||||
@Scheduled(fixedDelay = SWEEP_INTERVAL_MS)
|
@Scheduled(fixedDelay = SWEEP_INTERVAL_MS)
|
||||||
public void reconcile() {
|
public void reconcile() {
|
||||||
|
if (!nodeConfigService.isThisNodeReceiver()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
long sweepStart = System.currentTimeMillis();
|
long sweepStart = System.currentTimeMillis();
|
||||||
Date cutoff = new Date(sweepStart - MIN_AGE_MS);
|
Date cutoff = new Date(sweepStart - MIN_AGE_MS);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
|||||||
private DingStreamCallbackLogHelper callbackLogHelper;
|
private DingStreamCallbackLogHelper callbackLogHelper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DingTalkStreamProperties streamProperties;
|
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DingTalkStreamLeaderElection leaderElection;
|
private DingTalkStreamLeaderElection leaderElection;
|
||||||
@@ -69,7 +69,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
|||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
stopStreamClient();
|
stopStreamClient();
|
||||||
if (streamProperties.isClusterMode()) {
|
if (nodeConfigService.isClusterMode()) {
|
||||||
leaderElection.release();
|
leaderElection.release();
|
||||||
}
|
}
|
||||||
log.info("{} Stream 客户端已停止", LOG_TAG);
|
log.info("{} Stream 客户端已停止", LOG_TAG);
|
||||||
@@ -85,12 +85,21 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
|||||||
|
|
||||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream集群】Redis选主+心跳重连生命周期管理-----
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream集群】Redis选主+心跳重连生命周期管理-----
|
||||||
private void initSdkClient() {
|
private void initSdkClient() {
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】非接收节点跳过建连-----------
|
||||||
|
if (!nodeConfigService.isThisNodeReceiver()) {
|
||||||
|
log.info("{} 本节点未启用钉钉 Stream 接收 host={} localIps={},"
|
||||||
|
+ "请在【系统管理-第三方配置-钉钉集成】配置 Stream 接收节点白名单",
|
||||||
|
LOG_TAG, nodeConfigService.resolveLocalHostName(), nodeConfigService.resolveLocalIpAddresses());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】非接收节点跳过建连-----------
|
||||||
|
|
||||||
String[] creds = resolveCredentials();
|
String[] creds = resolveCredentials();
|
||||||
if (creds == null) {
|
if (creds == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!streamProperties.isClusterMode()) {
|
if (!nodeConfigService.isClusterMode()) {
|
||||||
log.info("{} 单实例模式(cluster-mode=false),本节点直接建立 Stream 连接", LOG_TAG);
|
log.info("{} 单实例模式(cluster-mode=false),本节点直接建立 Stream 连接", LOG_TAG);
|
||||||
try {
|
try {
|
||||||
startStreamClient(creds);
|
startStreamClient(creds);
|
||||||
@@ -116,7 +125,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
|||||||
try {
|
try {
|
||||||
if (streamActive) {
|
if (streamActive) {
|
||||||
if (leaderElection.renew()) {
|
if (leaderElection.renew()) {
|
||||||
sleepQuietly(streamProperties.getLeaderRenewIntervalMs());
|
sleepQuietly(nodeConfigService.getLeaderRenewIntervalMs());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
log.warn("{} Leader 锁续期失败,断开 Stream 并降级为 Follower instanceId={} currentLeader={}",
|
log.warn("{} Leader 锁续期失败,断开 Stream 并降级为 Follower instanceId={} currentLeader={}",
|
||||||
@@ -124,25 +133,25 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
|||||||
stopStreamClient();
|
stopStreamClient();
|
||||||
streamActive = false;
|
streamActive = false;
|
||||||
leaderElection.release();
|
leaderElection.release();
|
||||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leaderElection.tryAcquire()) {
|
if (leaderElection.tryAcquire()) {
|
||||||
startStreamClient(creds);
|
startStreamClient(creds);
|
||||||
streamActive = true;
|
streamActive = true;
|
||||||
sleepQuietly(streamProperties.getLeaderRenewIntervalMs());
|
sleepQuietly(nodeConfigService.getLeaderRenewIntervalMs());
|
||||||
} else {
|
} else {
|
||||||
log.debug("{} 本节点为 Follower,等待 Leader 释放锁 instanceId={} currentLeader={}",
|
log.debug("{} 本节点为 Follower,等待 Leader 释放锁 instanceId={} currentLeader={}",
|
||||||
LOG_TAG, leaderElection.instanceId(), leaderElection.currentHolder());
|
LOG_TAG, leaderElection.instanceId(), leaderElection.currentHolder());
|
||||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("{} Stream 集群生命周期异常: {}", LOG_TAG, e.getMessage(), e);
|
log.error("{} Stream 集群生命周期异常: {}", LOG_TAG, e.getMessage(), e);
|
||||||
stopStreamClient();
|
stopStreamClient();
|
||||||
streamActive = false;
|
streamActive = false;
|
||||||
leaderElection.release();
|
leaderElection.release();
|
||||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class DingTalkStreamHealthMonitor {
|
|||||||
private static final String LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
private static final String LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DingTalkStreamProperties properties;
|
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DingTalkStreamLeaderElection leaderElection;
|
private DingTalkStreamLeaderElection leaderElection;
|
||||||
@@ -23,10 +23,15 @@ public class DingTalkStreamHealthMonitor {
|
|||||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream监控】定时输出存活与心跳状态-----------
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream监控】定时输出存活与心跳状态-----------
|
||||||
@Scheduled(fixedDelayString = "${jeecg.xslmes.dingtalk.stream.health-log-interval-ms:60000}")
|
@Scheduled(fixedDelayString = "${jeecg.xslmes.dingtalk.stream.health-log-interval-ms:60000}")
|
||||||
public void reportHealth() {
|
public void reportHealth() {
|
||||||
|
if (!nodeConfigService.isThisNodeReceiver()) {
|
||||||
|
log.debug("{} Stream存活状态 role=DISABLED host={}(本节点未配置为钉钉回调接收机)",
|
||||||
|
LOG_TAG, nodeConfigService.resolveLocalHostName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
DingTalkStreamSdkRunner.ConnectionSnapshot snap = DingTalkStreamSdkRunner.snapshot();
|
DingTalkStreamSdkRunner.ConnectionSnapshot snap = DingTalkStreamSdkRunner.snapshot();
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
boolean clusterMode = properties.isClusterMode();
|
boolean clusterMode = nodeConfigService.isClusterMode();
|
||||||
String role;
|
String role;
|
||||||
String leaderHolder;
|
String leaderHolder;
|
||||||
if (!clusterMode) {
|
if (!clusterMode) {
|
||||||
@@ -51,9 +56,9 @@ public class DingTalkStreamHealthMonitor {
|
|||||||
|
|
||||||
if ("LEADER".equals(role) && snap.streamRunning()
|
if ("LEADER".equals(role) && snap.streamRunning()
|
||||||
&& snap.lastEventAtMs() > 0
|
&& snap.lastEventAtMs() > 0
|
||||||
&& (now - snap.lastEventAtMs()) > properties.getIdleWarnSeconds() * 1000L) {
|
&& (now - snap.lastEventAtMs()) > nodeConfigService.getIdleWarnSeconds() * 1000L) {
|
||||||
log.warn("{} Stream长时间无推送(可能业务空闲或连接异常)idleSec={} thresholdSec={}",
|
log.warn("{} Stream长时间无推送(可能业务空闲或连接异常)idleSec={} thresholdSec={}",
|
||||||
LOG_TAG, lastEventAgoSec, properties.getIdleWarnSeconds());
|
LOG_TAG, lastEventAgoSec, nodeConfigService.getIdleWarnSeconds());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//update-end---author:GHT ---date:20260609 for:【钉钉Stream监控】定时输出存活与心跳状态-----------
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream监控】定时输出存活与心跳状态-----------
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
package org.jeecg.modules.xslmes.dingtalk.stream;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
|
import org.jeecg.modules.system.entity.SysThirdAppConfig;
|
||||||
|
import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 钉钉 Stream 接收节点运行时配置(DB 优先,YAML 兜底)。
|
||||||
|
* <p>
|
||||||
|
* 配置来源:第三方应用「钉钉集成」页面保存的 {@link SysThirdAppConfig}(stream_enabled=1 的记录)。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class DingTalkStreamNodeConfigService {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ThirdAppDingtalkServiceImpl dingtalkService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DingTalkStreamProperties yamlProperties;
|
||||||
|
|
||||||
|
private volatile ResolvedConfig cachedConfig;
|
||||||
|
private volatile long cachedAtMs;
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】DB配置优先于YAML控制接收节点-----------
|
||||||
|
public boolean isThisNodeReceiver() {
|
||||||
|
ResolvedConfig config = resolveConfig();
|
||||||
|
if (!config.receiverEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean hasHostRule = !config.designatedHosts.isEmpty();
|
||||||
|
boolean hasIpRule = !config.designatedIps.isEmpty();
|
||||||
|
if (!hasHostRule && !hasIpRule) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasHostRule && matchesHost(config.designatedHosts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasIpRule && matchesIp(config.designatedIps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClusterMode() {
|
||||||
|
return resolveConfig().clusterMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getLeaderRenewIntervalMs() {
|
||||||
|
return yamlProperties.getLeaderRenewIntervalMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFollowerRetryIntervalMs() {
|
||||||
|
return yamlProperties.getFollowerRetryIntervalMs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getIdleWarnSeconds() {
|
||||||
|
return yamlProperties.getIdleWarnSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveLocalHostName() {
|
||||||
|
return yamlProperties.resolveLocalHostName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> resolveLocalIpAddresses() {
|
||||||
|
return yamlProperties.resolveLocalIpAddresses();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 配置变更后由定时任务自动刷新;保存第三方配置后最多 30 秒内生效 */
|
||||||
|
@Scheduled(fixedDelay = 30_000L)
|
||||||
|
public void refreshCache() {
|
||||||
|
cachedConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResolvedConfig resolveConfig() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (cachedConfig != null && now - cachedAtMs < 25_000L) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
synchronized (this) {
|
||||||
|
if (cachedConfig != null && now - cachedAtMs < 25_000L) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
cachedConfig = loadFromDbAndYaml();
|
||||||
|
cachedAtMs = System.currentTimeMillis();
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResolvedConfig loadFromDbAndYaml() {
|
||||||
|
SysThirdAppConfig db = dingtalkService.getStreamMasterConfig();
|
||||||
|
boolean receiverEnabled = yamlProperties.isReceiverEnabled();
|
||||||
|
List<String> ips = new ArrayList<>(yamlProperties.getDesignatedIps());
|
||||||
|
List<String> hosts = new ArrayList<>(yamlProperties.getDesignatedHosts());
|
||||||
|
boolean clusterMode = yamlProperties.isClusterMode();
|
||||||
|
String source = "YAML";
|
||||||
|
|
||||||
|
if (db != null) {
|
||||||
|
if (db.getStreamReceiverEnabled() != null) {
|
||||||
|
receiverEnabled = db.getStreamReceiverEnabled() == 1;
|
||||||
|
}
|
||||||
|
// null 表示未在页面配置,沿用 YAML;空字符串表示明确清空白名单
|
||||||
|
if (db.getStreamDesignatedIps() != null) {
|
||||||
|
ips = splitCsv(db.getStreamDesignatedIps());
|
||||||
|
}
|
||||||
|
if (db.getStreamDesignatedHosts() != null) {
|
||||||
|
hosts = splitCsv(db.getStreamDesignatedHosts());
|
||||||
|
}
|
||||||
|
if (db.getStreamClusterMode() != null) {
|
||||||
|
clusterMode = db.getStreamClusterMode() == 1;
|
||||||
|
}
|
||||||
|
source = "DB";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("{} Stream节点配置已加载 source={} receiverEnabled={} ips={} hosts={} clusterMode={}",
|
||||||
|
LOG_TAG, source, receiverEnabled, ips, hosts, clusterMode);
|
||||||
|
return new ResolvedConfig(receiverEnabled, ips, hosts, clusterMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> splitCsv(String raw) {
|
||||||
|
if (oConvertUtils.isEmpty(raw)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Arrays.stream(raw.split("[,;\\s]+"))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesHost(List<String> allowedHosts) {
|
||||||
|
String host = resolveLocalHostName();
|
||||||
|
for (String allowed : allowedHosts) {
|
||||||
|
if (allowed.equalsIgnoreCase(host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesIp(List<String> allowedIps) {
|
||||||
|
List<String> localIps = resolveLocalIpAddresses();
|
||||||
|
for (String allowed : allowedIps) {
|
||||||
|
for (String localIp : localIps) {
|
||||||
|
if (allowed.equals(localIp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供第三方配置页展示:本机网络信息与当前是否接收 Stream。
|
||||||
|
*/
|
||||||
|
public java.util.Map<String, Object> buildNodeInfoSnapshot() {
|
||||||
|
ResolvedConfig config = resolveConfig();
|
||||||
|
java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
|
||||||
|
map.put("hostName", resolveLocalHostName());
|
||||||
|
map.put("localIps", resolveLocalIpAddresses());
|
||||||
|
map.put("thisNodeReceiver", isThisNodeReceiver());
|
||||||
|
map.put("receiverEnabled", config.receiverEnabled);
|
||||||
|
map.put("designatedIps", config.designatedIps);
|
||||||
|
map.put("designatedHosts", config.designatedHosts);
|
||||||
|
map.put("clusterMode", config.clusterMode);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ResolvedConfig {
|
||||||
|
private final boolean receiverEnabled;
|
||||||
|
private final List<String> designatedIps;
|
||||||
|
private final List<String> designatedHosts;
|
||||||
|
private final boolean clusterMode;
|
||||||
|
|
||||||
|
private ResolvedConfig(boolean receiverEnabled, List<String> designatedIps,
|
||||||
|
List<String> designatedHosts, boolean clusterMode) {
|
||||||
|
this.receiverEnabled = receiverEnabled;
|
||||||
|
this.designatedIps = designatedIps != null ? designatedIps : Collections.emptyList();
|
||||||
|
this.designatedHosts = designatedHosts != null ? designatedHosts : Collections.emptyList();
|
||||||
|
this.clusterMode = clusterMode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】DB配置优先于YAML控制接收节点-----------
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
package org.jeecg.modules.xslmes.dingtalk.stream;
|
package org.jeecg.modules.xslmes.dingtalk.stream;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.jeecg.common.util.oConvertUtils;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 钉钉 Stream 集群与监控配置。
|
* 钉钉 Stream 集群与监控配置。
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +17,24 @@ import org.springframework.stereotype.Component;
|
|||||||
@ConfigurationProperties(prefix = "jeecg.xslmes.dingtalk.stream")
|
@ConfigurationProperties(prefix = "jeecg.xslmes.dingtalk.stream")
|
||||||
public class DingTalkStreamProperties {
|
public class DingTalkStreamProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本节点是否参与钉钉 Stream 接收(含补偿扫描)。
|
||||||
|
* 共享 dev 环境建议默认 false,开发者在本机 application-dev-local.yml 中设为 true。
|
||||||
|
*/
|
||||||
|
private boolean receiverEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许接收钉钉回调的主机名白名单(不区分大小写,可选)。
|
||||||
|
* 与 designated-ips 二选一或同时配置:满足任一即视为本机接收节点。
|
||||||
|
*/
|
||||||
|
private List<String> designatedHosts = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许接收钉钉回调的本机 IP 白名单(推荐,比主机名更稳定)。
|
||||||
|
* 匹配本机网卡 IPv4/IPv6 地址,与 designated-hosts 满足其一即可。
|
||||||
|
*/
|
||||||
|
private List<String> designatedIps = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 集群模式:true 时通过 Redis 选主,仅 Leader 节点建立 Stream 长连接。
|
* 集群模式:true 时通过 Redis 选主,仅 Leader 节点建立 Stream 长连接。
|
||||||
* 多实例生产环境务必保持 true;单实例可设为 false 简化部署。
|
* 多实例生产环境务必保持 true;单实例可设为 false 简化部署。
|
||||||
@@ -29,4 +52,88 @@ public class DingTalkStreamProperties {
|
|||||||
|
|
||||||
/** 无事件空闲告警阈值(秒),Leader 且连接中超过该时间无推送则 warn */
|
/** 无事件空闲告警阈值(秒),Leader 且连接中超过该时间无推送则 warn */
|
||||||
private long idleWarnSeconds = 1800L;
|
private long idleWarnSeconds = 1800L;
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】本机白名单仅指定电脑接收回调-----------
|
||||||
|
/**
|
||||||
|
* 当前节点是否应接收钉钉 Stream 推送并执行补偿扫描。
|
||||||
|
*/
|
||||||
|
public boolean isThisNodeReceiver() {
|
||||||
|
if (!receiverEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean hasHostRule = designatedHosts != null && !designatedHosts.isEmpty();
|
||||||
|
boolean hasIpRule = designatedIps != null && !designatedIps.isEmpty();
|
||||||
|
if (!hasHostRule && !hasIpRule) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasHostRule && matchesDesignatedHost()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasIpRule && matchesDesignatedIp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesDesignatedHost() {
|
||||||
|
String host = resolveLocalHostName();
|
||||||
|
for (String allowed : designatedHosts) {
|
||||||
|
if (oConvertUtils.isNotEmpty(allowed) && allowed.equalsIgnoreCase(host)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesDesignatedIp() {
|
||||||
|
List<String> localIps = resolveLocalIpAddresses();
|
||||||
|
for (String allowed : designatedIps) {
|
||||||
|
if (oConvertUtils.isEmpty(allowed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String localIp : localIps) {
|
||||||
|
if (allowed.equals(localIp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveLocalHostName() {
|
||||||
|
try {
|
||||||
|
return InetAddress.getLocalHost().getHostName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "unknown-host";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 采集本机所有网卡 IP(含 127.0.0.1),供白名单匹配与启动日志展示。
|
||||||
|
*/
|
||||||
|
public List<String> resolveLocalIpAddresses() {
|
||||||
|
List<String> ips = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
java.util.Enumeration<java.net.NetworkInterface> interfaces = java.net.NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (interfaces != null && interfaces.hasMoreElements()) {
|
||||||
|
java.net.NetworkInterface ni = interfaces.nextElement();
|
||||||
|
java.util.Enumeration<InetAddress> addresses = ni.getInetAddresses();
|
||||||
|
while (addresses.hasMoreElements()) {
|
||||||
|
String ip = addresses.nextElement().getHostAddress();
|
||||||
|
if (oConvertUtils.isNotEmpty(ip) && !ips.contains(ip)) {
|
||||||
|
ips.add(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 回退到 LocalHost
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String localIp = InetAddress.getLocalHost().getHostAddress();
|
||||||
|
if (oConvertUtils.isNotEmpty(localIp) && !ips.contains(localIp)) {
|
||||||
|
ips.add(localIp);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
return ips;
|
||||||
|
}
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】本机白名单仅指定电脑接收回调-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ public class SysThirdAppConfig {
|
|||||||
private Integer streamEnabled;
|
private Integer streamEnabled;
|
||||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】Stream事件推送主配置标识-----
|
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】Stream事件推送主配置标识-----
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页Stream接收节点可视化-----------
|
||||||
|
@Schema(description = "是否限制仅指定节点接收Stream(0-否,1-是)")
|
||||||
|
private Integer streamReceiverEnabled;
|
||||||
|
@Schema(description = "允许接收Stream的IP白名单,逗号分隔")
|
||||||
|
private String streamDesignatedIps;
|
||||||
|
@Schema(description = "允许接收Stream的主机名白名单,逗号分隔")
|
||||||
|
private String streamDesignatedHosts;
|
||||||
|
@Schema(description = "Stream集群Redis选主(0-否,1-是)")
|
||||||
|
private Integer streamClusterMode;
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】第三方配置页Stream接收节点可视化-----------
|
||||||
|
|
||||||
/**创建日期*/
|
/**创建日期*/
|
||||||
@Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
@Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss")
|
||||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
|||||||
@@ -1228,6 +1228,29 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
|
|||||||
}
|
}
|
||||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】获取钉钉应用凭证(Stream模式专用)-----
|
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】获取钉钉应用凭证(Stream模式专用)-----
|
||||||
|
|
||||||
|
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream开发】获取Stream主配置(含节点白名单)-----------
|
||||||
|
/**
|
||||||
|
* 获取 Stream 主配置记录(stream_enabled=1 优先)。
|
||||||
|
*/
|
||||||
|
public SysThirdAppConfig getStreamMasterConfig() {
|
||||||
|
java.util.List<SysThirdAppConfig> all = configMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysThirdAppConfig>()
|
||||||
|
.eq(SysThirdAppConfig::getThirdType, THIRD_TYPE)
|
||||||
|
.eq(SysThirdAppConfig::getStatus, 1)
|
||||||
|
.orderByDesc(SysThirdAppConfig::getStreamEnabled)
|
||||||
|
.orderByDesc(SysThirdAppConfig::getTenantId));
|
||||||
|
if (all == null || all.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (SysThirdAppConfig c : all) {
|
||||||
|
if (c.getStreamEnabled() != null && c.getStreamEnabled() == 1) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all.get(0);
|
||||||
|
}
|
||||||
|
//update-end---author:GHT ---date:20260609 for:【钉钉Stream开发】获取Stream主配置(含节点白名单)-----------
|
||||||
|
|
||||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】后台线程专用AccessToken(绕过租户检查)-----
|
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】后台线程专用AccessToken(绕过租户检查)-----
|
||||||
/**
|
/**
|
||||||
* 后台线程专用:获取钉钉 AccessToken,不依赖 TenantContext。
|
* 后台线程专用:获取钉钉 AccessToken,不依赖 TenantContext。
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 复制为 application-dev-local.yml(同目录),仅本机生效,勿提交 Git
|
||||||
|
# 作用:只有你这台电脑接收钉钉 Stream 回调与补偿扫描,其他共用 dev 库的机器不会抢消息
|
||||||
|
#
|
||||||
|
# 1. 复制:application-dev-local.yml.example -> application-dev-local.yml
|
||||||
|
# 2. 查本机 IP(PowerShell: ipconfig,看 IPv4 地址)
|
||||||
|
# 3. 将 designated-ips 改成你的 IP(推荐,比主机名稳定)
|
||||||
|
# 4. 重启 JeecgBoot
|
||||||
|
#
|
||||||
|
# 启动后若未匹配,日志会打印 localIps=... 便于核对
|
||||||
|
|
||||||
|
jeecg:
|
||||||
|
xslmes:
|
||||||
|
dingtalk:
|
||||||
|
stream:
|
||||||
|
receiver-enabled: true
|
||||||
|
# 推荐:仅 IP 白名单(二选一或同时配置,满足其一即可)
|
||||||
|
designated-ips:
|
||||||
|
- 192.168.1.100
|
||||||
|
# 可选:主机名白名单(hostname 命令查看)
|
||||||
|
# designated-hosts:
|
||||||
|
# - LAPTOP-9LEM1NNJ
|
||||||
@@ -20,6 +20,9 @@ management:
|
|||||||
include: metrics,httpexchanges,jeecghttptrace
|
include: metrics,httpexchanges,jeecghttptrace
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
|
config:
|
||||||
|
# 开发者本机覆盖配置(不提交 Git),用于指定仅本机接收钉钉 Stream 回调
|
||||||
|
import: optional:classpath:application-dev-local.yml
|
||||||
# main:
|
# main:
|
||||||
# # 启动加速 (建议开发环境,开启后flyway自动升级失效)
|
# # 启动加速 (建议开发环境,开启后flyway自动升级失效)
|
||||||
# lazy-initialization: true
|
# lazy-initialization: true
|
||||||
@@ -214,6 +217,11 @@ jeecg:
|
|||||||
xslmes:
|
xslmes:
|
||||||
dingtalk:
|
dingtalk:
|
||||||
stream:
|
stream:
|
||||||
|
# 共享 dev 默认关闭;仅本机在 application-dev-local.yml 开启(见 application-dev-local.yml.example)
|
||||||
|
receiver-enabled: false
|
||||||
|
# 白名单二选一或同时配置(满足其一即可);推荐 designated-ips,比主机名稳定
|
||||||
|
designated-hosts: []
|
||||||
|
designated-ips: []
|
||||||
# 多实例部署务必 true:Redis 选主,仅 Leader 建 Stream 长连接
|
# 多实例部署务必 true:Redis 选主,仅 Leader 建 Stream 长连接
|
||||||
cluster-mode: true
|
cluster-mode: true
|
||||||
leader-renew-interval-ms: 10000
|
leader-renew-interval-ms: 10000
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- GHT 20260609 【钉钉Stream开发】第三方配置页可视化:Stream 接收节点白名单
|
||||||
|
ALTER TABLE sys_third_app_config
|
||||||
|
ADD COLUMN stream_receiver_enabled TINYINT(1) NULL DEFAULT NULL COMMENT '是否限制仅指定节点接收Stream(0-否,1-是,NULL-沿用YAML)' AFTER stream_enabled,
|
||||||
|
ADD COLUMN stream_designated_ips VARCHAR(1000) NULL DEFAULT NULL COMMENT '允许接收Stream的IP白名单(逗号分隔)' AFTER stream_receiver_enabled,
|
||||||
|
ADD COLUMN stream_designated_hosts VARCHAR(500) NULL DEFAULT NULL COMMENT '允许接收Stream的主机名白名单(逗号分隔)' AFTER stream_designated_ips,
|
||||||
|
ADD COLUMN stream_cluster_mode TINYINT(1) NULL DEFAULT NULL COMMENT 'Stream集群Redis选主(0-否,1-是,NULL-沿用YAML)' AFTER stream_designated_hosts;
|
||||||
@@ -12,6 +12,7 @@ enum Api {
|
|||||||
deleteThirdAccount = '/sys/thirdApp/deleteThirdAccount',
|
deleteThirdAccount = '/sys/thirdApp/deleteThirdAccount',
|
||||||
deleteThirdAppConfig = '/sys/thirdApp/deleteThirdAppConfig',
|
deleteThirdAppConfig = '/sys/thirdApp/deleteThirdAppConfig',
|
||||||
testKingdeeConnect = '/sys/thirdApp/testKingdeeConnect',
|
testKingdeeConnect = '/sys/thirdApp/testKingdeeConnect',
|
||||||
|
getDingTalkStreamNodeInfo = '/xslmes/dingtalk/stream/nodeInfo',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,3 +88,10 @@ export const deleteThirdAppConfig = (params, handleSuccess) => {
|
|||||||
export const testKingdeeConnect = (params) => {
|
export const testKingdeeConnect = (params) => {
|
||||||
return defHttp.get({ url: Api.testKingdeeConnect, params }, { isTransformResponse: false });
|
return defHttp.get({ url: Api.testKingdeeConnect, params }, { isTransformResponse: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本机钉钉 Stream 节点信息(主机名、IP、是否接收)
|
||||||
|
*/
|
||||||
|
export const getDingTalkStreamNodeInfo = () => {
|
||||||
|
return defHttp.get({ url: Api.getDingTalkStreamNodeInfo });
|
||||||
|
};
|
||||||
@@ -77,7 +77,57 @@ export const thirdAppFormSchema: FormSchema[] = [
|
|||||||
},
|
},
|
||||||
helpMessage: '开启后,此应用的 AppKey/AppSecret 将用于接收钉钉 Stream 事件推送(审批结果等)。同一企业只需开启一个',
|
helpMessage: '开启后,此应用的 AppKey/AppSecret 将用于接收钉钉 Stream 事件推送(审批结果等)。同一企业只需开启一个',
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
},{
|
},
|
||||||
|
{
|
||||||
|
label: '限制接收节点',
|
||||||
|
field: 'streamReceiverEnabled',
|
||||||
|
component: 'Switch',
|
||||||
|
ifShow: ({ values }) => values.thirdType === 'dingtalk' && values.streamEnabled === 1,
|
||||||
|
componentProps: {
|
||||||
|
checkedChildren: '已限制',
|
||||||
|
checkedValue: 1,
|
||||||
|
unCheckedChildren: '不限制',
|
||||||
|
unCheckedValue: 0,
|
||||||
|
},
|
||||||
|
helpMessage: '开启后,仅下方白名单内的服务器会建立 Stream 连接并处理审批回调(推荐用 IP)',
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '允许接收的IP',
|
||||||
|
field: 'streamDesignatedIps',
|
||||||
|
component: 'InputTextArea',
|
||||||
|
ifShow: ({ values }) =>
|
||||||
|
values.thirdType === 'dingtalk' && values.streamEnabled === 1 && values.streamReceiverEnabled === 1,
|
||||||
|
componentProps: {
|
||||||
|
rows: 2,
|
||||||
|
placeholder: '多个 IP 用逗号分隔,例如 192.168.1.74(可参考页面展示的本机 IP)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '允许接收的主机名',
|
||||||
|
field: 'streamDesignatedHosts',
|
||||||
|
component: 'Input',
|
||||||
|
ifShow: ({ values }) =>
|
||||||
|
values.thirdType === 'dingtalk' && values.streamEnabled === 1 && values.streamReceiverEnabled === 1,
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '可选,多个用逗号分隔(不如 IP 稳定)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '集群Redis选主',
|
||||||
|
field: 'streamClusterMode',
|
||||||
|
component: 'Switch',
|
||||||
|
ifShow: ({ values }) => values.thirdType === 'dingtalk' && values.streamEnabled === 1,
|
||||||
|
componentProps: {
|
||||||
|
checkedChildren: '开启',
|
||||||
|
checkedValue: 1,
|
||||||
|
unCheckedChildren: '关闭',
|
||||||
|
unCheckedValue: 0,
|
||||||
|
},
|
||||||
|
helpMessage: '多实例共用同一 AppKey 时务必开启,避免多台服务器同时建连抢消息',
|
||||||
|
defaultValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
label: '租户id',
|
label: '租户id',
|
||||||
field: 'tenantId',
|
field: 'tenantId',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<BasicModal @register="registerModal" :width="800" :title="title" @ok="handleSubmit">
|
<BasicModal @register="registerModal" :width="860" :title="title" @ok="handleSubmit">
|
||||||
|
<a-alert
|
||||||
|
v-if="showStreamNodeHint"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
:message="streamNodeHintTitle"
|
||||||
|
:description="streamNodeHintDesc"
|
||||||
|
/>
|
||||||
<BasicForm @register="registerForm" />
|
<BasicForm @register="registerForm" />
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
@@ -9,12 +17,15 @@
|
|||||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||||
import { useForm, BasicForm } from '/@/components/Form';
|
import { useForm, BasicForm } from '/@/components/Form';
|
||||||
import { thirdAppFormSchema } from './ThirdApp.data';
|
import { thirdAppFormSchema } from './ThirdApp.data';
|
||||||
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig } from './ThirdApp.api';
|
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig, getDingTalkStreamNodeInfo } from './ThirdApp.api';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ThirdAppConfigModal',
|
name: 'ThirdAppConfigModal',
|
||||||
components: { BasicModal, BasicForm },
|
components: { BasicModal, BasicForm },
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const title = ref<string>('钉钉配置');
|
const title = ref<string>('钉钉配置');
|
||||||
|
const showStreamNodeHint = ref(false);
|
||||||
|
const streamNodeHintTitle = ref('本机 Stream 节点信息');
|
||||||
|
const streamNodeHintDesc = ref('');
|
||||||
//表单配置
|
//表单配置
|
||||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||||
schemas: thirdAppFormSchema,
|
schemas: thirdAppFormSchema,
|
||||||
@@ -25,8 +36,23 @@
|
|||||||
//表单赋值
|
//表单赋值
|
||||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||||
setModalProps({ confirmLoading: true });
|
setModalProps({ confirmLoading: true });
|
||||||
|
showStreamNodeHint.value = false;
|
||||||
|
streamNodeHintDesc.value = '';
|
||||||
if (data.thirdType == 'dingtalk') {
|
if (data.thirdType == 'dingtalk') {
|
||||||
title.value = '钉钉配置';
|
title.value = '钉钉配置';
|
||||||
|
try {
|
||||||
|
const nodeInfo = await getDingTalkStreamNodeInfo();
|
||||||
|
if (nodeInfo) {
|
||||||
|
showStreamNodeHint.value = true;
|
||||||
|
const ips = Array.isArray(nodeInfo.localIps) ? nodeInfo.localIps.join(', ') : '';
|
||||||
|
const receiverText = nodeInfo.thisNodeReceiver ? '是(本机将接收回调)' : '否(本机不接收回调)';
|
||||||
|
streamNodeHintDesc.value =
|
||||||
|
`主机名:${nodeInfo.hostName || '-'}\n本机IP:${ips || '-'}\n当前是否接收:${receiverText}\n` +
|
||||||
|
`提示:开启「限制接收节点」后,请将本机局域网 IP 填入「允许接收的IP」`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 接口不可用时忽略
|
||||||
|
}
|
||||||
} else if (data.thirdType == 'kingdee') {
|
} else if (data.thirdType == 'kingdee') {
|
||||||
title.value = '金蝶配置';
|
title.value = '金蝶配置';
|
||||||
} else {
|
} else {
|
||||||
@@ -60,6 +86,9 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
showStreamNodeHint,
|
||||||
|
streamNodeHintTitle,
|
||||||
|
streamNodeHintDesc,
|
||||||
registerForm,
|
registerForm,
|
||||||
registerModal,
|
registerModal,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|||||||
@@ -53,6 +53,49 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="appConfigData.streamEnabled === 1">
|
||||||
|
<div class="flex-flow">
|
||||||
|
<div class="base-title">接收限制</div>
|
||||||
|
<div class="base-message" style="display:flex;align-items:center;min-height:50px;">
|
||||||
|
<a-tag :color="appConfigData.streamReceiverEnabled === 1 ? 'orange' : 'default'">
|
||||||
|
{{ appConfigData.streamReceiverEnabled === 1 ? '仅白名单节点' : '不限制节点' }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-flow" v-if="appConfigData.streamReceiverEnabled === 1">
|
||||||
|
<div class="base-title">允许IP</div>
|
||||||
|
<div class="base-message" style="min-height:50px;line-height:1.6;padding-top:12px;">
|
||||||
|
{{ appConfigData.streamDesignatedIps || '(未配置,所有节点均可接收)' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-flow">
|
||||||
|
<div class="base-title">集群选主</div>
|
||||||
|
<div class="base-message" style="display:flex;align-items:center;height:50px;">
|
||||||
|
<a-tag :color="appConfigData.streamClusterMode === 1 ? 'blue' : 'default'">
|
||||||
|
{{
|
||||||
|
appConfigData.streamClusterMode === 1
|
||||||
|
? 'Redis选主已开启'
|
||||||
|
: appConfigData.streamClusterMode === 0
|
||||||
|
? '单节点直连'
|
||||||
|
: '沿用YAML默认'
|
||||||
|
}}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-flow" v-if="streamNodeInfo.hostName">
|
||||||
|
<div class="base-title">本机信息</div>
|
||||||
|
<div class="base-message stream-node-info">
|
||||||
|
<div>主机名:{{ streamNodeInfo.hostName }}</div>
|
||||||
|
<div>本机IP:{{ (streamNodeInfo.localIps || []).join(', ') }}</div>
|
||||||
|
<div>
|
||||||
|
本机是否接收:
|
||||||
|
<a-tag :color="streamNodeInfo.thisNodeReceiver ? 'green' : 'default'" style="margin-left:4px">
|
||||||
|
{{ streamNodeInfo.thisNodeReceiver ? '是' : '否' }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div style="margin-top: 20px; width: 100%; text-align: right">
|
<div style="margin-top: 20px; width: 100%; text-align: right">
|
||||||
<a-button @click="dingEditClick">编辑</a-button>
|
<a-button @click="dingEditClick">编辑</a-button>
|
||||||
<a-button v-if="appConfigData.id" @click="cancelBindClick" danger style="margin-left: 10px">取消绑定</a-button>
|
<a-button v-if="appConfigData.id" @click="cancelBindClick" danger style="margin-left: 10px">取消绑定</a-button>
|
||||||
@@ -85,7 +128,12 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, h, inject, onMounted, reactive, ref, watch } from 'vue';
|
import { defineComponent, h, inject, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { getThirdConfigByTenantId, syncDingTalkDepartUserToLocal, deleteThirdAppConfig } from './ThirdApp.api';
|
import {
|
||||||
|
getThirdConfigByTenantId,
|
||||||
|
syncDingTalkDepartUserToLocal,
|
||||||
|
deleteThirdAppConfig,
|
||||||
|
getDingTalkStreamNodeInfo,
|
||||||
|
} from './ThirdApp.api';
|
||||||
import { useModal } from '/@/components/Modal';
|
import { useModal } from '/@/components/Modal';
|
||||||
import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
|
import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
|
||||||
import { Modal } from 'ant-design-vue';
|
import { Modal } from 'ant-design-vue';
|
||||||
@@ -109,6 +157,7 @@
|
|||||||
clientId: '',
|
clientId: '',
|
||||||
clientSecret: '',
|
clientSecret: '',
|
||||||
});
|
});
|
||||||
|
const streamNodeInfo = ref<any>({});
|
||||||
|
|
||||||
//企业微信钉钉配置modal
|
//企业微信钉钉配置modal
|
||||||
const [registerAppConfigModal, { openModal }] = useModal();
|
const [registerAppConfigModal, { openModal }] = useModal();
|
||||||
@@ -132,7 +181,12 @@
|
|||||||
if (values) {
|
if (values) {
|
||||||
appConfigData.value = values;
|
appConfigData.value = values;
|
||||||
} else {
|
} else {
|
||||||
appConfigData.value = "";
|
appConfigData.value = '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
streamNodeInfo.value = (await getDingTalkStreamNodeInfo()) || {};
|
||||||
|
} catch (e) {
|
||||||
|
streamNodeInfo.value = {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +306,7 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
appConfigData,
|
appConfigData,
|
||||||
|
streamNodeInfo,
|
||||||
collapseActiveKey,
|
collapseActiveKey,
|
||||||
registerAppConfigModal,
|
registerAppConfigModal,
|
||||||
dingEditClick,
|
dingEditClick,
|
||||||
@@ -331,4 +386,10 @@
|
|||||||
top: 2px
|
top: 2px
|
||||||
}
|
}
|
||||||
:deep(.ant-collapse-borderless >.ant-collapse-item:last-child) {border-bottom-width:1px;}
|
:deep(.ant-collapse-borderless >.ant-collapse-item:last-child) {border-bottom-width:1px;}
|
||||||
|
.stream-node-info {
|
||||||
|
line-height: 1.8;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: @text-color-secondary;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,9 +19,17 @@
|
|||||||
"settings": {
|
"settings": {
|
||||||
"java.compile.nullAnalysis.mode": "automatic",
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
"java.import.maven.enabled": true,
|
"java.import.maven.enabled": true,
|
||||||
|
"java.import.maven.recursive": true,
|
||||||
"java.configuration.updateBuildConfiguration": "automatic",
|
"java.configuration.updateBuildConfiguration": "automatic",
|
||||||
"java.autobuild.enabled": true,
|
"java.autobuild.enabled": true,
|
||||||
"java.import.maven.offline.enabled": false,
|
"java.import.maven.offline.enabled": false,
|
||||||
|
"java.configuration.runtimes": [
|
||||||
|
{
|
||||||
|
"name": "JavaSE-17",
|
||||||
|
"path": "C:\\Program Files\\Java\\jdk-17",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore",
|
"java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore",
|
||||||
"java.jdt.ls.java.home": "C:\\Program Files\\Java\\jdk-17",
|
"java.jdt.ls.java.home": "C:\\Program Files\\Java\\jdk-17",
|
||||||
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable",
|
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx4G -Xms100m -Xlog:disable",
|
||||||
|
|||||||
Reference in New Issue
Block a user