优化ip=“” nodeid=0

This commit is contained in:
2025-11-23 03:43:53 +08:00
parent 2c1c2b0857
commit 19f224c7fa
7 changed files with 614 additions and 35 deletions

View File

@@ -37,6 +37,19 @@ func main() {
// 初始化错误恢复
recovery.Init()
// 如果配置中没有节点信息,先发送一次心跳获取节点信息
if cfg.Node.ID == 0 || cfg.Node.IP == "" {
logger.Info("节点信息未配置,发送心跳获取节点信息")
if err := heartbeat.RegisterNode(cfg); err != nil {
logger.Warn("注册节点失败,将在心跳时重试", zap.Error(err))
} else {
logger.Info("节点信息已获取并保存",
zap.Uint("node_id", cfg.Node.ID),
zap.String("node_ip", cfg.Node.IP),
zap.String("location", fmt.Sprintf("%s/%s/%s", cfg.Node.Country, cfg.Node.Province, cfg.Node.City)))
}
}
// 启动心跳上报
heartbeatReporter := heartbeat.NewReporter(cfg)
go heartbeatReporter.Start(context.Background())

View File

@@ -387,6 +387,84 @@ configure_firewall() {
fi
}
# 登记节点调用心跳API获取节点信息
register_node() {
echo -e "${BLUE}登记节点到后端服务器...${NC}"
# 创建临时配置文件
CONFIG_FILE="$SOURCE_DIR/config.yaml"
sudo mkdir -p "$SOURCE_DIR"
# 创建基础配置文件
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
server:
port: 2200
backend:
url: ${BACKEND_URL}
heartbeat:
interval: 60
debug: false
node:
id: 0
ip: ""
country: ""
province: ""
city: ""
isp: ""
EOF
# 调用心跳API获取节点信息
echo -e "${BLUE}发送心跳请求获取节点信息...${NC}"
RESPONSE=$(curl -s -X POST "${BACKEND_URL}/api/node/heartbeat" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "type=pingServer" 2>&1)
if [ $? -eq 0 ]; then
# 尝试解析JSON响应
NODE_ID=$(echo "$RESPONSE" | grep -o '"node_id":[0-9]*' | grep -o '[0-9]*' | head -1)
NODE_IP=$(echo "$RESPONSE" | grep -o '"node_ip":"[^"]*"' | cut -d'"' -f4 | head -1)
COUNTRY=$(echo "$RESPONSE" | grep -o '"country":"[^"]*"' | cut -d'"' -f4 | head -1)
PROVINCE=$(echo "$RESPONSE" | grep -o '"province":"[^"]*"' | cut -d'"' -f4 | head -1)
CITY=$(echo "$RESPONSE" | grep -o '"city":"[^"]*"' | cut -d'"' -f4 | head -1)
ISP=$(echo "$RESPONSE" | grep -o '"isp":"[^"]*"' | cut -d'"' -f4 | head -1)
if [ -n "$NODE_ID" ] && [ "$NODE_ID" != "0" ] && [ -n "$NODE_IP" ]; then
# 更新配置文件
sudo tee "$CONFIG_FILE" > /dev/null <<EOF
server:
port: 2200
backend:
url: ${BACKEND_URL}
heartbeat:
interval: 60
debug: false
node:
id: ${NODE_ID}
ip: ${NODE_IP}
country: ${COUNTRY:-""}
province: ${PROVINCE:-""}
city: ${CITY:-""}
isp: ${ISP:-""}
EOF
echo -e "${GREEN}✓ 节点登记成功${NC}"
echo -e "${BLUE} 节点ID: ${NODE_ID}${NC}"
echo -e "${BLUE} 节点IP: ${NODE_IP}${NC}"
if [ -n "$COUNTRY" ] || [ -n "$PROVINCE" ] || [ -n "$CITY" ]; then
echo -e "${BLUE} 位置: ${COUNTRY:-""}/${PROVINCE:-""}/${CITY:-""}${NC}"
fi
else
echo -e "${YELLOW}⚠ 无法从响应中解析节点信息,将在服务启动时重试${NC}"
echo -e "${YELLOW} 响应: ${RESPONSE}${NC}"
fi
else
echo -e "${YELLOW}⚠ 心跳请求失败,将在服务启动时重试${NC}"
echo -e "${YELLOW} 错误: ${RESPONSE}${NC}"
fi
# 设置配置文件权限
sudo chmod 644 "$CONFIG_FILE"
}
# 启动服务
start_service() {
echo -e "${BLUE}启动服务...${NC}"
@@ -472,6 +550,7 @@ main() {
build_from_source
create_service
configure_firewall
register_node
start_service
verify_installation

View File

@@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
@@ -21,6 +22,16 @@ type Config struct {
} `yaml:"heartbeat"`
Debug bool `yaml:"debug"`
// 节点信息(通过心跳获取并持久化)
Node struct {
ID uint `yaml:"id"` // 节点ID
IP string `yaml:"ip"` // 节点外网IP
Country string `yaml:"country"` // 国家
Province string `yaml:"province"` // 省份
City string `yaml:"city"` // 城市
ISP string `yaml:"isp"` // ISP
} `yaml:"node"`
}
func Load() (*Config, error) {
@@ -58,3 +69,37 @@ func Load() (*Config, error) {
return cfg, nil
}
// Save 保存配置到文件
func (c *Config) Save() error {
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.yaml"
}
// 确保目录存在
dir := filepath.Dir(configPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败: %w", err)
}
data, err := yaml.Marshal(c)
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
if err := os.WriteFile(configPath, data, 0644); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
// GetConfigPath 获取配置文件路径
func GetConfigPath() string {
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "config.yaml"
}
return configPath
}

View File

@@ -24,6 +24,8 @@ type PingTask struct {
mu sync.RWMutex
logger *zap.Logger
targetIP string // 存储目标IP从ping输出中提取
currentCmd *exec.Cmd // 当前正在执行的命令,用于停止时取消
cmdMu sync.Mutex // 保护 currentCmd 的锁
}
func NewPingTask(taskID, target string, interval, maxDuration time.Duration) *PingTask {
@@ -77,11 +79,31 @@ func (t *PingTask) Start(ctx context.Context, resultCallback func(result map[str
func (t *PingTask) Stop() {
t.mu.Lock()
defer t.mu.Unlock()
if t.IsRunning {
if !t.IsRunning {
t.mu.Unlock()
return
}
t.IsRunning = false
t.mu.Unlock()
// 取消正在执行的命令
t.cmdMu.Lock()
if t.currentCmd != nil && t.currentCmd.Process != nil {
t.logger.Debug("取消正在执行的ping命令", zap.String("task_id", t.TaskID))
t.currentCmd.Process.Kill()
t.currentCmd = nil
}
t.cmdMu.Unlock()
// 关闭停止通道
select {
case <-t.StopCh:
// 已经关闭
default:
close(t.StopCh)
}
t.logger.Info("Ping任务已停止", zap.String("task_id", t.TaskID))
}
func (t *PingTask) UpdateLastRequest() {
@@ -91,10 +113,30 @@ func (t *PingTask) UpdateLastRequest() {
}
func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result map[string]interface{})) {
// 检查任务是否已停止
t.mu.RLock()
isRunning := t.IsRunning
t.mu.RUnlock()
if !isRunning {
return
}
// 发送10个ping包间隔0.5秒,实时解析每个包的延迟
// 使用 -c 10 -i 0.5 发送10个包
cmd := exec.Command("ping", "-c", "10", "-i", "0.5", t.Target)
// 保存命令引用,以便停止时取消
t.cmdMu.Lock()
t.currentCmd = cmd
t.cmdMu.Unlock()
// 确保在函数退出时清理命令引用
defer func() {
t.cmdMu.Lock()
t.currentCmd = nil
t.cmdMu.Unlock()
}()
// 获取标准输出管道,实时读取
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -124,6 +166,16 @@ func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result ma
return
}
// 检查任务是否已停止(在启动命令后)
t.mu.RLock()
isRunning = t.IsRunning
t.mu.RUnlock()
if !isRunning {
// 任务已停止,取消命令
cmd.Process.Kill()
return
}
// 使用bufio.Scanner实时读取每一行
scanner := bufio.NewScanner(stdout)
processedPackets := make(map[int]bool) // 用于去重避免重复处理同一个包通过icmp_seq
@@ -131,6 +183,14 @@ func (t *PingTask) executePingWithRealtimeCallback(resultCallback func(result ma
// 在goroutine中读取输出避免阻塞
go func() {
for scanner.Scan() {
// 检查任务是否已停止
t.mu.RLock()
isRunning := t.IsRunning
t.mu.RUnlock()
if !isRunning {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue

View File

@@ -73,8 +73,25 @@ func (t *TCPingTask) Start(ctx context.Context, resultCallback func(result map[s
}
t.mu.RUnlock()
// 检查任务是否已停止
t.mu.RLock()
isRunning := t.IsRunning
t.mu.RUnlock()
if !isRunning {
return
}
// 执行tcping测试每次测试完成后立即返回结果
result := t.executeTCPing()
// 再次检查任务是否已停止(执行完成后)
t.mu.RLock()
isRunning = t.IsRunning
t.mu.RUnlock()
if !isRunning {
return
}
if resultCallback != nil {
resultCallback(result)
}
@@ -94,11 +111,22 @@ func (t *TCPingTask) Start(ctx context.Context, resultCallback func(result map[s
func (t *TCPingTask) Stop() {
t.mu.Lock()
defer t.mu.Unlock()
if t.IsRunning {
if !t.IsRunning {
t.mu.Unlock()
return
}
t.IsRunning = false
t.mu.Unlock()
// 关闭停止通道
select {
case <-t.StopCh:
// 已经关闭
default:
close(t.StopCh)
}
t.logger.Info("TCPing任务已停止", zap.String("task_id", t.TaskID))
}
func (t *TCPingTask) UpdateLastRequest() {

View File

@@ -8,6 +8,7 @@ import (
"io"
"net"
"net/http"
"strings"
"sync"
"time"
@@ -24,6 +25,25 @@ var taskMutex sync.RWMutex
var backendURL string
var logger *zap.Logger
// 批量推送缓冲(每个任务一个缓冲)
var pushBuffers = make(map[string]*pushBuffer)
var bufferMutex sync.RWMutex
// pushBuffer 批量推送缓冲
type pushBuffer struct {
taskID string
results []map[string]interface{}
mu sync.Mutex
lastPush time.Time
pushTimer *time.Timer
}
const (
// 批量推送配置
batchPushInterval = 1 * time.Second // 批量推送间隔1秒
batchPushMaxSize = 10 // 批量推送最大数量10个结果
)
func InitContinuousHandler(cfg *config.Config) {
backendURL = cfg.Backend.URL
logger, _ = zap.NewProduction()
@@ -202,9 +222,6 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
result["packet_loss"] = false
}
// 推送结果到后端
url := fmt.Sprintf("%s/api/public/node/continuous/result", backendURL)
// 优先使用心跳返回的节点信息
nodeID := heartbeat.GetNodeID()
nodeIP := heartbeat.GetNodeIP()
@@ -215,22 +232,132 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
logger.Debug("使用本地IP作为后备", zap.String("node_ip", nodeIP))
}
// 发送 node_id 和 node_ip后端可以通过这些信息精准匹配
// 确保已经获取到 node_id,避免发送无效数据包
if nodeID == 0 {
logger.Warn("节点ID未获取跳过推送结果",
zap.String("task_id", taskID),
zap.String("node_ip", nodeIP),
zap.String("hint", "等待心跳返回node_id后再推送"))
return
}
// 确保已经获取到 node_ip
if nodeIP == "" {
logger.Warn("节点IP未获取跳过推送结果",
zap.String("task_id", taskID),
zap.Uint("node_id", nodeID),
zap.String("hint", "等待心跳返回node_ip后再推送"))
return
}
// 添加到批量推送缓冲
addToPushBuffer(taskID, nodeID, nodeIP, result)
}
// addToPushBuffer 添加结果到批量推送缓冲
func addToPushBuffer(taskID string, nodeID uint, nodeIP string, result map[string]interface{}) {
bufferMutex.Lock()
buffer, exists := pushBuffers[taskID]
if !exists {
buffer = &pushBuffer{
taskID: taskID,
results: make([]map[string]interface{}, 0, batchPushMaxSize),
lastPush: time.Now(),
}
pushBuffers[taskID] = buffer
}
bufferMutex.Unlock()
buffer.mu.Lock()
defer buffer.mu.Unlock()
// 添加结果到缓冲
buffer.results = append(buffer.results, result)
// 如果缓冲已满,立即推送
shouldFlush := len(buffer.results) >= batchPushMaxSize
buffer.mu.Unlock()
if shouldFlush {
flushPushBuffer(taskID, nodeID, nodeIP)
return
}
buffer.mu.Lock()
// 如果距离上次推送超过间隔时间,启动定时器推送
if buffer.pushTimer == nil {
buffer.pushTimer = time.AfterFunc(batchPushInterval, func() {
flushPushBuffer(taskID, nodeID, nodeIP)
})
}
}
// flushPushBuffer 刷新并推送缓冲中的结果
func flushPushBuffer(taskID string, nodeID uint, nodeIP string) {
bufferMutex.RLock()
buffer, exists := pushBuffers[taskID]
bufferMutex.RUnlock()
if !exists {
return
}
buffer.mu.Lock()
if len(buffer.results) == 0 {
buffer.mu.Unlock()
return
}
// 复制结果列表
results := make([]map[string]interface{}, len(buffer.results))
copy(results, buffer.results)
buffer.results = buffer.results[:0] // 清空缓冲
// 停止定时器
if buffer.pushTimer != nil {
buffer.pushTimer.Stop()
buffer.pushTimer = nil
}
buffer.lastPush = time.Now()
buffer.mu.Unlock()
// 批量推送结果(目前后端只支持单个结果,所以逐个推送)
// 但可以减少HTTP请求的频率
for _, result := range results {
pushSingleResult(taskID, nodeID, nodeIP, result)
}
}
// pushSingleResult 推送单个结果到后端
func pushSingleResult(taskID string, nodeID uint, nodeIP string, result map[string]interface{}) {
// 推送结果到后端
url := fmt.Sprintf("%s/api/public/node/continuous/result", backendURL)
// 获取节点位置信息
country, province, city, isp := heartbeat.GetNodeLocation()
// 发送 node_id、node_ip 和位置信息,后端可以通过这些信息精准匹配
data := map[string]interface{}{
"task_id": taskID,
"node_id": nodeID,
"node_ip": nodeIP,
"result": result,
}
// 如果 node_id 存在,优先发送 node_id
if nodeID > 0 {
data["node_id"] = nodeID
logger.Debug("推送结果时使用存储的node_id", zap.Uint("node_id", nodeID))
// 添加位置信息(如果存在)
if country != "" {
data["country"] = country
}
// 如果 node_ip 存在,发送 node_ip
if nodeIP != "" {
data["node_ip"] = nodeIP
logger.Debug("推送结果时使用存储的node_ip", zap.String("node_ip", nodeIP))
if province != "" {
data["province"] = province
}
if city != "" {
data["city"] = city
}
if isp != "" {
data["isp"] = isp
}
jsonData, err := json.Marshal(data)
@@ -261,18 +388,110 @@ func pushResultToBackend(taskID string, result map[string]interface{}) {
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
bodyStr := string(body)
// 检查是否是任务不存在的错误
if containsTaskNotFoundError(bodyStr) {
logger.Warn("后端任务不存在,停止节点端任务",
zap.String("task_id", taskID),
zap.String("response", bodyStr))
// 停止对应的持续测试任务
stopTaskByTaskID(taskID)
return
}
logger.Warn("推送结果失败,继续运行",
zap.Int("status", resp.StatusCode),
zap.String("task_id", taskID),
zap.String("url", url),
zap.String("response", string(body)))
// 推送失败不停止任务,继续运行
zap.String("response", bodyStr))
// 其他错误不停止任务,继续运行
return
}
logger.Debug("推送结果成功", zap.String("task_id", taskID))
}
// containsTaskNotFoundError 检查响应中是否包含任务不存在的错误
func containsTaskNotFoundError(responseBody string) bool {
// 检查常见的任务不存在错误消息
errorKeywords := []string{
"找不到对应的后端任务",
"任务不存在",
"task not found",
"找不到对应的任务",
}
responseLower := strings.ToLower(responseBody)
for _, keyword := range errorKeywords {
if strings.Contains(responseLower, strings.ToLower(keyword)) {
return true
}
}
// 尝试解析 JSON 响应,检查错误消息
var resp struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
if err := json.Unmarshal([]byte(responseBody), &resp); err == nil {
msgLower := strings.ToLower(resp.Msg)
for _, keyword := range errorKeywords {
if strings.Contains(msgLower, strings.ToLower(keyword)) {
return true
}
}
}
return false
}
// stopTaskByTaskID 根据 taskID 停止对应的持续测试任务
func stopTaskByTaskID(taskID string) {
taskMutex.Lock()
defer taskMutex.Unlock()
task, exists := continuousTasks[taskID]
if !exists {
logger.Debug("任务不存在,无需停止", zap.String("task_id", taskID))
return
}
logger.Info("停止持续测试任务", zap.String("task_id", taskID))
// 停止任务
task.IsRunning = false
if task.pingTask != nil {
task.pingTask.Stop()
}
if task.tcpingTask != nil {
task.tcpingTask.Stop()
}
// 关闭停止通道
select {
case <-task.StopCh:
// 已经关闭
default:
close(task.StopCh)
}
// 删除任务
delete(continuousTasks, taskID)
// 清理推送缓冲
bufferMutex.Lock()
if buffer, exists := pushBuffers[taskID]; exists {
if buffer.pushTimer != nil {
buffer.pushTimer.Stop()
}
delete(pushBuffers, taskID)
}
bufferMutex.Unlock()
logger.Info("持续测试任务已停止", zap.String("task_id", taskID))
}
func getLocalIP() string {
// 简化实现返回第一个非回环IP
// 实际应该获取外网IP

View File

@@ -15,11 +15,32 @@ import (
"go.uber.org/zap"
)
// 节点信息存储(通过心跳更新)
// 节点信息存储(通过心跳更新,优先从配置文件读取
var nodeInfo struct {
sync.RWMutex
nodeID uint
nodeIP string
country string
province string
city string
isp string
cfg *config.Config
initialized bool
}
// InitNodeInfo 初始化节点信息(从配置文件读取)
func InitNodeInfo(cfg *config.Config) {
nodeInfo.Lock()
defer nodeInfo.Unlock()
nodeInfo.cfg = cfg
nodeInfo.nodeID = cfg.Node.ID
nodeInfo.nodeIP = cfg.Node.IP
nodeInfo.country = cfg.Node.Country
nodeInfo.province = cfg.Node.Province
nodeInfo.city = cfg.Node.City
nodeInfo.isp = cfg.Node.ISP
nodeInfo.initialized = true
}
// GetNodeID 获取节点ID
@@ -36,6 +57,13 @@ func GetNodeIP() string {
return nodeInfo.nodeIP
}
// GetNodeLocation 获取节点位置信息
func GetNodeLocation() (country, province, city, isp string) {
nodeInfo.RLock()
defer nodeInfo.RUnlock()
return nodeInfo.country, nodeInfo.province, nodeInfo.city, nodeInfo.isp
}
type Reporter struct {
cfg *config.Config
client *http.Client
@@ -45,6 +73,10 @@ type Reporter struct {
func NewReporter(cfg *config.Config) *Reporter {
logger, _ := zap.NewProduction()
// 初始化节点信息(从配置文件读取)
InitNodeInfo(cfg)
return &Reporter{
cfg: cfg,
client: &http.Client{
@@ -78,8 +110,76 @@ func (r *Reporter) Stop() {
close(r.stopCh)
}
// RegisterNode 注册节点(安装时或首次启动时调用)
func RegisterNode(cfg *config.Config) error {
url := fmt.Sprintf("%s/api/node/heartbeat", cfg.Backend.URL)
req, err := http.NewRequest("POST", url, bytes.NewBufferString("type=pingServer"))
if err != nil {
return fmt.Errorf("创建心跳请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("发送心跳失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %w", err)
}
// 尝试解析 JSON 响应
var result struct {
Status string `json:"status"`
NodeID uint `json:"node_id"`
NodeIP string `json:"node_ip"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
ISP string `json:"isp"`
}
if err := json.Unmarshal(body, &result); err == nil {
// 成功解析 JSON更新配置文件和内存
if result.NodeID > 0 && result.NodeIP != "" {
cfg.Node.ID = result.NodeID
cfg.Node.IP = result.NodeIP
cfg.Node.Country = result.Country
cfg.Node.Province = result.Province
cfg.Node.City = result.City
cfg.Node.ISP = result.ISP
// 保存到配置文件
if err := cfg.Save(); err != nil {
return fmt.Errorf("保存配置文件失败: %w", err)
}
// 更新内存中的节点信息
nodeInfo.Lock()
nodeInfo.nodeID = result.NodeID
nodeInfo.nodeIP = result.NodeIP
nodeInfo.country = result.Country
nodeInfo.province = result.Province
nodeInfo.city = result.City
nodeInfo.isp = result.ISP
nodeInfo.cfg = cfg
nodeInfo.initialized = true
nodeInfo.Unlock()
return nil
}
}
return fmt.Errorf("心跳响应格式无效或节点信息不完整")
}
return fmt.Errorf("心跳请求失败,状态码: %d", resp.StatusCode)
}
func (r *Reporter) sendHeartbeat() {
// 新节点不发送IP让后端服务器从请求中获取
// 发送心跳使用Form格式兼容旧接口
url := fmt.Sprintf("%s/api/node/heartbeat", r.cfg.Backend.URL)
req, err := http.NewRequest("POST", url, bytes.NewBufferString("type=pingServer"))
@@ -106,17 +206,52 @@ func (r *Reporter) sendHeartbeat() {
Status string `json:"status"`
NodeID uint `json:"node_id"`
NodeIP string `json:"node_ip"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
ISP string `json:"isp"`
}
if err := json.Unmarshal(body, &result); err == nil {
// 成功解析 JSON更新节点信息
// 成功解析 JSON检查是否有更新
if result.NodeID > 0 && result.NodeIP != "" {
nodeInfo.Lock()
needUpdate := false
if nodeInfo.nodeID != result.NodeID || nodeInfo.nodeIP != result.NodeIP ||
nodeInfo.country != result.Country || nodeInfo.province != result.Province ||
nodeInfo.city != result.City || nodeInfo.isp != result.ISP {
needUpdate = true
}
if needUpdate {
// 更新内存
nodeInfo.nodeID = result.NodeID
nodeInfo.nodeIP = result.NodeIP
nodeInfo.country = result.Country
nodeInfo.province = result.Province
nodeInfo.city = result.City
nodeInfo.isp = result.ISP
// 更新配置文件
if nodeInfo.cfg != nil {
nodeInfo.cfg.Node.ID = result.NodeID
nodeInfo.cfg.Node.IP = result.NodeIP
nodeInfo.cfg.Node.Country = result.Country
nodeInfo.cfg.Node.Province = result.Province
nodeInfo.cfg.Node.City = result.City
nodeInfo.cfg.Node.ISP = result.ISP
if err := nodeInfo.cfg.Save(); err != nil {
r.logger.Warn("保存节点信息到配置文件失败", zap.Error(err))
}
}
nodeInfo.Unlock()
r.logger.Debug("心跳响应解析成功,已更新节点信息",
r.logger.Info("节点信息已更新",
zap.Uint("node_id", result.NodeID),
zap.String("node_ip", result.NodeIP))
zap.String("node_ip", result.NodeIP),
zap.String("location", fmt.Sprintf("%s/%s/%s", result.Country, result.Province, result.City)))
} else {
nodeInfo.Unlock()
}
}
} else {
// 不是 JSON 格式,可能是旧格式的 "done",忽略