集群问题处理
This commit is contained in:
2
jeecg-boot/.gitignore
vendored
2
jeecg-boot/.gitignore
vendored
@@ -12,6 +12,8 @@ rebel.xml
|
||||
## backend
|
||||
**/target
|
||||
**/logs
|
||||
# 开发者本机钉钉 Stream 接收配置(从 application-dev-local.yml.example 复制)
|
||||
**/application-dev-local.yml
|
||||
|
||||
## front
|
||||
**/*.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/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选主单节点建连+存活监控 -----
|
||||
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
|
||||
|
||||
@@ -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
|
||||
private DingBpmsEventProcessor eventProcessor;
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream补偿扫描】漏推回调自动修复-----
|
||||
@Scheduled(fixedDelay = SWEEP_INTERVAL_MS)
|
||||
public void reconcile() {
|
||||
if (!nodeConfigService.isThisNodeReceiver()) {
|
||||
return;
|
||||
}
|
||||
long sweepStart = System.currentTimeMillis();
|
||||
Date cutoff = new Date(sweepStart - MIN_AGE_MS);
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
||||
private DingStreamCallbackLogHelper callbackLogHelper;
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamProperties streamProperties;
|
||||
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamLeaderElection leaderElection;
|
||||
@@ -69,7 +69,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
||||
public void stop() {
|
||||
running = false;
|
||||
stopStreamClient();
|
||||
if (streamProperties.isClusterMode()) {
|
||||
if (nodeConfigService.isClusterMode()) {
|
||||
leaderElection.release();
|
||||
}
|
||||
log.info("{} Stream 客户端已停止", LOG_TAG);
|
||||
@@ -85,12 +85,21 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
||||
|
||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream集群】Redis选主+心跳重连生命周期管理-----
|
||||
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();
|
||||
if (creds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!streamProperties.isClusterMode()) {
|
||||
if (!nodeConfigService.isClusterMode()) {
|
||||
log.info("{} 单实例模式(cluster-mode=false),本节点直接建立 Stream 连接", LOG_TAG);
|
||||
try {
|
||||
startStreamClient(creds);
|
||||
@@ -116,7 +125,7 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
||||
try {
|
||||
if (streamActive) {
|
||||
if (leaderElection.renew()) {
|
||||
sleepQuietly(streamProperties.getLeaderRenewIntervalMs());
|
||||
sleepQuietly(nodeConfigService.getLeaderRenewIntervalMs());
|
||||
continue;
|
||||
}
|
||||
log.warn("{} Leader 锁续期失败,断开 Stream 并降级为 Follower instanceId={} currentLeader={}",
|
||||
@@ -124,25 +133,25 @@ public class DingTalkStreamClient implements SmartLifecycle {
|
||||
stopStreamClient();
|
||||
streamActive = false;
|
||||
leaderElection.release();
|
||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
||||
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (leaderElection.tryAcquire()) {
|
||||
startStreamClient(creds);
|
||||
streamActive = true;
|
||||
sleepQuietly(streamProperties.getLeaderRenewIntervalMs());
|
||||
sleepQuietly(nodeConfigService.getLeaderRenewIntervalMs());
|
||||
} else {
|
||||
log.debug("{} 本节点为 Follower,等待 Leader 释放锁 instanceId={} currentLeader={}",
|
||||
LOG_TAG, leaderElection.instanceId(), leaderElection.currentHolder());
|
||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
||||
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("{} Stream 集群生命周期异常: {}", LOG_TAG, e.getMessage(), e);
|
||||
stopStreamClient();
|
||||
streamActive = false;
|
||||
leaderElection.release();
|
||||
sleepQuietly(streamProperties.getFollowerRetryIntervalMs());
|
||||
sleepQuietly(nodeConfigService.getFollowerRetryIntervalMs());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class DingTalkStreamHealthMonitor {
|
||||
private static final String LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamProperties properties;
|
||||
private DingTalkStreamNodeConfigService nodeConfigService;
|
||||
|
||||
@Autowired
|
||||
private DingTalkStreamLeaderElection leaderElection;
|
||||
@@ -23,10 +23,15 @@ public class DingTalkStreamHealthMonitor {
|
||||
//update-begin---author:GHT ---date:20260609 for:【钉钉Stream监控】定时输出存活与心跳状态-----------
|
||||
@Scheduled(fixedDelayString = "${jeecg.xslmes.dingtalk.stream.health-log-interval-ms:60000}")
|
||||
public void reportHealth() {
|
||||
if (!nodeConfigService.isThisNodeReceiver()) {
|
||||
log.debug("{} Stream存活状态 role=DISABLED host={}(本节点未配置为钉钉回调接收机)",
|
||||
LOG_TAG, nodeConfigService.resolveLocalHostName());
|
||||
return;
|
||||
}
|
||||
DingTalkStreamSdkRunner.ConnectionSnapshot snap = DingTalkStreamSdkRunner.snapshot();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
boolean clusterMode = properties.isClusterMode();
|
||||
boolean clusterMode = nodeConfigService.isClusterMode();
|
||||
String role;
|
||||
String leaderHolder;
|
||||
if (!clusterMode) {
|
||||
@@ -51,9 +56,9 @@ public class DingTalkStreamHealthMonitor {
|
||||
|
||||
if ("LEADER".equals(role) && snap.streamRunning()
|
||||
&& snap.lastEventAtMs() > 0
|
||||
&& (now - snap.lastEventAtMs()) > properties.getIdleWarnSeconds() * 1000L) {
|
||||
&& (now - snap.lastEventAtMs()) > nodeConfigService.getIdleWarnSeconds() * 1000L) {
|
||||
log.warn("{} Stream长时间无推送(可能业务空闲或连接异常)idleSec={} thresholdSec={}",
|
||||
LOG_TAG, lastEventAgoSec, properties.getIdleWarnSeconds());
|
||||
LOG_TAG, lastEventAgoSec, nodeConfigService.getIdleWarnSeconds());
|
||||
}
|
||||
}
|
||||
//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;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉 Stream 集群与监控配置。
|
||||
*/
|
||||
@@ -12,6 +17,24 @@ import org.springframework.stereotype.Component;
|
||||
@ConfigurationProperties(prefix = "jeecg.xslmes.dingtalk.stream")
|
||||
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;单实例可设为 false 简化部署。
|
||||
@@ -29,4 +52,88 @@ public class DingTalkStreamProperties {
|
||||
|
||||
/** 无事件空闲告警阈值(秒),Leader 且连接中超过该时间无推送则 warn */
|
||||
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 java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -72,6 +72,17 @@ public class SysThirdAppConfig {
|
||||
private Integer streamEnabled;
|
||||
//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")
|
||||
@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-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(绕过租户检查)-----
|
||||
/**
|
||||
* 后台线程专用:获取钉钉 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
|
||||
|
||||
spring:
|
||||
config:
|
||||
# 开发者本机覆盖配置(不提交 Git),用于指定仅本机接收钉钉 Stream 回调
|
||||
import: optional:classpath:application-dev-local.yml
|
||||
# main:
|
||||
# # 启动加速 (建议开发环境,开启后flyway自动升级失效)
|
||||
# lazy-initialization: true
|
||||
@@ -214,6 +217,11 @@ jeecg:
|
||||
xslmes:
|
||||
dingtalk:
|
||||
stream:
|
||||
# 共享 dev 默认关闭;仅本机在 application-dev-local.yml 开启(见 application-dev-local.yml.example)
|
||||
receiver-enabled: false
|
||||
# 白名单二选一或同时配置(满足其一即可);推荐 designated-ips,比主机名稳定
|
||||
designated-hosts: []
|
||||
designated-ips: []
|
||||
# 多实例部署务必 true:Redis 选主,仅 Leader 建 Stream 长连接
|
||||
cluster-mode: true
|
||||
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',
|
||||
deleteThirdAppConfig = '/sys/thirdApp/deleteThirdAppConfig',
|
||||
testKingdeeConnect = '/sys/thirdApp/testKingdeeConnect',
|
||||
getDingTalkStreamNodeInfo = '/xslmes/dingtalk/stream/nodeInfo',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,3 +88,10 @@ export const deleteThirdAppConfig = (params, handleSuccess) => {
|
||||
export const testKingdeeConnect = (params) => {
|
||||
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 事件推送(审批结果等)。同一企业只需开启一个',
|
||||
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',
|
||||
field: 'tenantId',
|
||||
component: 'Input',
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<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" />
|
||||
</BasicModal>
|
||||
</template>
|
||||
@@ -9,12 +17,15 @@
|
||||
import { BasicModal, useModalInner } from '/@/components/Modal';
|
||||
import { useForm, BasicForm } from '/@/components/Form';
|
||||
import { thirdAppFormSchema } from './ThirdApp.data';
|
||||
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig } from './ThirdApp.api';
|
||||
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig, getDingTalkStreamNodeInfo } from './ThirdApp.api';
|
||||
export default defineComponent({
|
||||
name: 'ThirdAppConfigModal',
|
||||
components: { BasicModal, BasicForm },
|
||||
setup(props, { emit }) {
|
||||
const title = ref<string>('钉钉配置');
|
||||
const showStreamNodeHint = ref(false);
|
||||
const streamNodeHintTitle = ref('本机 Stream 节点信息');
|
||||
const streamNodeHintDesc = ref('');
|
||||
//表单配置
|
||||
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
|
||||
schemas: thirdAppFormSchema,
|
||||
@@ -25,8 +36,23 @@
|
||||
//表单赋值
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
setModalProps({ confirmLoading: true });
|
||||
showStreamNodeHint.value = false;
|
||||
streamNodeHintDesc.value = '';
|
||||
if (data.thirdType == 'dingtalk') {
|
||||
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') {
|
||||
title.value = '金蝶配置';
|
||||
} else {
|
||||
@@ -60,6 +86,9 @@
|
||||
|
||||
return {
|
||||
title,
|
||||
showStreamNodeHint,
|
||||
streamNodeHintTitle,
|
||||
streamNodeHintDesc,
|
||||
registerForm,
|
||||
registerModal,
|
||||
handleSubmit,
|
||||
|
||||
@@ -53,6 +53,49 @@
|
||||
</a-tag>
|
||||
</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">
|
||||
<a-button @click="dingEditClick">编辑</a-button>
|
||||
<a-button v-if="appConfigData.id" @click="cancelBindClick" danger style="margin-left: 10px">取消绑定</a-button>
|
||||
@@ -85,7 +128,12 @@
|
||||
|
||||
<script lang="ts">
|
||||
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 ThirdAppConfigModal from './ThirdAppConfigModal.vue';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
@@ -109,6 +157,7 @@
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
});
|
||||
const streamNodeInfo = ref<any>({});
|
||||
|
||||
//企业微信钉钉配置modal
|
||||
const [registerAppConfigModal, { openModal }] = useModal();
|
||||
@@ -132,7 +181,12 @@
|
||||
if (values) {
|
||||
appConfigData.value = values;
|
||||
} else {
|
||||
appConfigData.value = "";
|
||||
appConfigData.value = '';
|
||||
}
|
||||
try {
|
||||
streamNodeInfo.value = (await getDingTalkStreamNodeInfo()) || {};
|
||||
} catch (e) {
|
||||
streamNodeInfo.value = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,6 +306,7 @@
|
||||
|
||||
return {
|
||||
appConfigData,
|
||||
streamNodeInfo,
|
||||
collapseActiveKey,
|
||||
registerAppConfigModal,
|
||||
dingEditClick,
|
||||
@@ -331,4 +386,10 @@
|
||||
top: 2px
|
||||
}
|
||||
: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>
|
||||
|
||||
@@ -19,9 +19,17 @@
|
||||
"settings": {
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.import.maven.enabled": true,
|
||||
"java.import.maven.recursive": true,
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"java.autobuild.enabled": true,
|
||||
"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.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",
|
||||
|
||||
Reference in New Issue
Block a user