Files
Meshray-Manager/docs/全面问题排查与修复清单.md
2026-06-30 15:14:37 +08:00

12 KiB

MeshRay 全面问题排查与修复清单

🔍 排查范围

排查时间: 2026-03-20
排查重点: 逻辑不完整、前后端不一致、功能缺失
排查方法: Handler → Service → Model 全链路审查


📋 问题清单总览

# 问题类别 严重程度 状态
1 PendingJoin 审核通过无后续动作 🔴 P0 待修复
2 DeviceService 生成配置缺少私钥 🔴 P0 待修复
3 Network 创建后未返回完整信息 🟡 P1 待确认
4 DDNS 同步缺少失败重试 🟡 P1 待修复
5 STUN/TURN 配置来源不明确 🟡 P1 待明确
6 WebSocket 断线重连机制 🟢 P2 优化

🔴 P0 - 严重问题

问题 1: PendingJoin 审核通过逻辑不完整

位置: internal/service/pending_join.go - ApproveJoin()

现状:

func (s *PendingJoinService) ApproveJoin(id uint) error {
    // 1. 查询记录
    var record model.PendingJoin
    s.store.DB().First(&record, id)
    
    // 2. 更新状态
    record.Status = "approved"
    record.ApprovedAt = &now
    
    // 3. 保存
    s.store.DB().Save(&record)
    
    // ❌ 缺失:
    // - 没有创建设备
    // - 没有生成密钥对
    // - 没有分配 IP
    // - 没有返回配置
}

影响:

  • 管理员审核通过后,申请人无法获得连接配置
  • 组网断联情况下,新用户无法加入
  • 审核流程形同虚设

修复方案:

type ApprovalResult struct {
    Device     *model.Device
    PrivateKey string
    Network    *model.Network
    ConfigText string  // WireGuard 配置文本
}

func (s *PendingJoinService) ApproveJoin(id uint) (*ApprovalResult, error) {
    // 1. 查询申请记录
    var record model.PendingJoin
    if err := s.store.DB().First(&record, id).Error; err != nil {
        return nil, err
    }
    
    // 2. 查询 MeshSeed 获取网络信息
    var meshSeed model.MeshSeed
    s.store.DB().Where("seed_id = ?", record.SeedID).First(&meshSeed)
    
    // 3. 查询网络详情
    var network model.Network
    s.store.DB().First(&network, meshSeed.NetworkID)
    
    // 4. 生成设备密钥对
    privateKey, publicKey := generateKeyPair()
    
    // 5. 分配 IP 地址
    ipAddress := s.allocateIP(network.SubnetIPv4)
    
    // 6. 创建设备记录
    device := &model.Device{
        Name:      record.DeviceName,
        NetworkID: network.ID,
        PublicKey: publicKey,
        IPAddress: ipAddress,
        Status:    "active",
    }
    s.store.DB().Create(device)
    
    // 7. 更新审核状态
    record.Status = "approved"
    record.ApprovedAt = &now
    s.store.DB().Save(&record)
    
    // 8. 生成配置文本
    configText := generateWireGuardConfig(device, network, privateKey)
    
    return &ApprovalResult{
        Device:     device,
        PrivateKey: privateKey,
        Network:    &network,
        ConfigText: configText,
    }, nil
}

修复优先级: 最高(阻塞性功能)


问题 2: DeviceService 生成配置缺少私钥

位置: internal/service/device.go - GenerateDeviceConfig()

现状:

func (s *DeviceService) GenerateDeviceConfig(deviceID uint64) (string, error) {
    // 1. 获取设备信息
    device := s.GetDevice(deviceID)
    
    // 2. 获取网络信息
    network := device.Network
    
    // 3. 生成配置
    var sb strings.Builder
    sb.WriteString("[Interface]\n")
    sb.WriteString("PrivateKey = <❌ 从哪里获取?>\n")  // ❌ 问题
    sb.WriteString(fmt.Sprintf("Address = %s\n", device.IPAddress))
    
    // ❌ 问题:设备表只存储了 PublicKey,没有 PrivateKey
}

影响:

  • 管理员创建设备后,无法下载配置文件
  • 只能手动导入公钥,无法生成完整 WG 配置
  • 设备管理功能残废

根本原因:

// model.Device 定义
type Device struct {
    ID          uint64
    PublicKey   string  // ✅ 存储公钥
    PrivateKey  string  // ❌ 没有此字段!
    IPAddress   string
}

解决方案 A: 添加 PrivateKey 字段(不推荐)

type Device struct {
    PublicKey   string
    PrivateKey  string  // ⚠️ 安全风险:服务端存储私钥
}

解决方案 B: 创建时返回,之后不存储(推荐)

// CreateDevice 返回完整信息
type CreateDeviceResult struct {
    Device     *model.Device
    PrivateKey string  // 仅首次返回
    ConfigText string
}

func (s *DeviceService) CreateDevice(req *CreateDeviceRequest) (*CreateDeviceResult, error) {
    // 生成密钥对
    privateKey, publicKey := generateKeyPair()
    
    // 创建设备(只存公钥)
    device := &model.Device{
        PublicKey: publicKey,
        // ...
    }
    s.store.DB().Create(device)
    
    // 生成配置
    configText := generateConfig(device, privateKey)
    
    return &CreateDeviceResult{
        Device:     device,
        PrivateKey: privateKey,  // 仅此次返回
        ConfigText: configText,
    }, nil
}

修复优先级: 高(核心功能)


🟡 P1 - 重要问题

问题 3: Network 创建后未返回完整信息

位置: internal/api/handler/network.go - CreateNetwork()

现状:

func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
    network := h.networkService.CreateNetwork(req)
    
    // ❌ 只返回基础信息
    c.JSON(http.StatusOK, gin.H{
        "data": network,
    })
}

缺失信息:

  • STUN/TURN 服务器列表
  • DDNS Provider 配置
  • Server 公网 IP 和端口
  • WireGuard 密钥对(如果是管理员设备)

影响:

  • 创建网络后,无法立即使用
  • 需要额外调用多个 API 获取配置
  • 用户体验差

修复方案:

type CreateNetworkResponse struct {
    Network      *model.Network
    STUNServers  []model.Service
    TURNServers  []model.Service
    DDNSConfig   *DDNSConfig
    ServerInfo   *ServerInfo
}

func (h *NetworkHandler) CreateNetwork(c *gin.Context) {
    network := h.networkService.CreateNetwork(req)
    
    // 查询关联配置
    stunServers := s.getSTUNServers()
    turnServers := s.getTURNServers()
    ddnsConfig := s.getDDNSConfig(network.DDNSServiceID)
    
    resp := &CreateNetworkResponse{
        Network:      network,
        STUNServers:  stunServers,
        TURNServers:  turnServers,
        DDNSConfig:   ddnsConfig,
    }
    
    c.JSON(http.StatusOK, gin.H{"data": resp})
}

问题 4: DDNS 同步缺少失败重试

位置: internal/service/ddns_operation.go

现状:

func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string) error {
    // 1. 查询 DDNS 配置
    config := s.getDDNSConfig(networkID)
    
    // 2. 加密 MeshSeed
    encrypted := encrypt(seedString)
    
    // 3. 创建 TXT 记录
    err := s.provider.CreateTXTRecord(config.Domain, encrypted)
    
    // ❌ 没有重试机制
    // ❌ 没有错误处理
    // ❌ 没有状态记录
}

影响:

  • DNS API 临时故障导致同步失败
  • 用户不知道同步结果
  • 数据不一致

修复方案:

func (s *DDNSOperationService) SyncMeshSeedToDNS(networkID uint64, seedString string) error {
    // 最多重试 3 次
    for i := 0; i < 3; i++ {
        err := s.doSync(networkID, seedString)
        if err == nil {
            // 成功
            s.updateStatus(networkID, "success", "")
            return nil
        }
        
        // 记录失败
        if i < 2 {
            time.Sleep(time.Duration(i+1) * time.Second)  // 指数退避
        }
    }
    
    // 全部失败
    s.updateStatus(networkID, "failed", "重试 3 次失败")
    return fmt.Errorf("同步失败")
}

问题 5: STUN/TURN 配置来源不明确

位置: 多处使用

现状:

// Core 层接收 STUN 服务器列表
stunServers := config.STUNServers  // ❌ 从哪里来?

可能来源:

  1. Service 表查询 (type='STUN')
  2. ExternalService 表查询
  3. 硬编码默认值
  4. 配置文件

排查结果:

  • Service 层有查询逻辑
  • ⚠️ 未明确调用链
  • ⚠️ 未传递给 Core 层

修复建议:

// 在 Ctr 层明确传递
func (c *Ctr) CreateNetwork(...) {
    // 查询 STUN/TURN 配置
    stunServers := s.getExternalServicesByType("STUN")
    turnServers := s.getExternalServicesByType("TURN")
    
    // 传递给 Core
    engine := c.coreInst.CreateEngine(..., stunServers, turnServers)
}

🟢 P2 - 优化建议

问题 6: WebSocket 断线重连机制

位置: web/src/utils/websocket.js

现状:

// 前端 WebSocket 连接
const ws = new WebSocket(url)

ws.onclose = () => {
    // ❌ 没有自动重连
    console.log('WebSocket 已关闭')
}

修复方案:

class WebSocketClient {
    constructor(url) {
        this.url = url
        this.reconnectAttempts = 0
        this.maxReconnectAttempts = 5
        this.connect()
    }
    
    connect() {
        this.ws = new WebSocket(this.url)
        
        this.ws.onclose = () => {
            if (this.reconnectAttempts < this.maxReconnectAttempts) {
                this.reconnectAttempts++
                setTimeout(() => this.connect(), 3000)
            }
        }
    }
}

📊 问题统计

按严重程度

级别 数量 说明
P0 2 阻塞性功能缺失
P1 3 重要功能不完整
P2 1 体验优化

按类别

类别 数量 说明
逻辑不完整 3 审核、配置生成、创建返回
错误处理 2 DDNS 重试、WebSocket 重连
配置管理 1 STUN/TURN 来源

🔧 修复计划

Phase 1: P0 问题修复(立即)

  1. 修复 PendingJoin 审核逻辑

    • 添加设备创建
    • 添加密钥生成
    • 添加配置返回
    • 预计:2 小时
  2. 修复 DeviceService 配置生成

    • 修改 CreateDevice 返回值
    • 添加配置下载接口
    • 预计:1 小时

Phase 2: P1 问题修复(今天)

  1. 完善 Network 创建返回

    • 添加关联配置查询
    • 返回完整信息包
    • 预计:1 小时
  2. 添加 DDNS 重试机制

    • 实现指数退避
    • 添加状态记录
    • 预计:1 小时
  3. 明确 STUN/TURN 传递链

    • 添加代码注释
    • 确保配置传递
    • 预计:0.5 小时

Phase 3: P2 优化(明天)

  1. WebSocket 重连机制
    • 前端实现重连
    • 添加心跳检测
    • 预计:1 小时

验收标准

P0 问题验收

PendingJoin 审核:

  • 管理员审核通过后,能看到完整配置
  • 可以复制配置发送给申请人
  • 申请人导入配置即可连接

DeviceService 配置:

  • 创建设备时能下载配置文件
  • 配置文件格式正确
  • 可直接导入 WireGuard 客户端

P1 问题验收

Network 创建:

  • 创建后返回完整信息包
  • 包含 STUN/TURN 配置
  • 包含 DDNS 配置(如果启用)

DDNS 重试:

  • 临时故障自动恢复
  • 失败有明确提示
  • 状态可查询

📝 总结

核心问题:

  1. 审核通过 ≠ 获得配置(逻辑断裂)
  2. 创建设备 ≠ 能下载配置(功能残废)
  3. 创建网络 ≠ 能用(信息不完整)

根本原因:

  • 前后端沟通不足
  • 服务边界不清晰
  • 缺少端到端验证

改进方向:

  1. 加强全链路测试
  2. 建立验收标准
  3. 完善错误处理
  4. 增加日志记录

排查人员: AI Assistant
排查时间: 2026-03-20
下一步: 立即开始 P0 问题修复