# 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()` **现状**: ```go 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 // - 没有返回配置 } ``` **影响**: - 管理员审核通过后,申请人无法获得连接配置 - 组网断联情况下,新用户无法加入 - 审核流程形同虚设 **修复方案**: ```go 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()` **现状**: ```go 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 配置 - 设备管理功能残废 **根本原因**: ```go // model.Device 定义 type Device struct { ID uint64 PublicKey string // ✅ 存储公钥 PrivateKey string // ❌ 没有此字段! IPAddress string } ``` **解决方案 A: 添加 PrivateKey 字段**(不推荐) ```go type Device struct { PublicKey string PrivateKey string // ⚠️ 安全风险:服务端存储私钥 } ``` **解决方案 B: 创建时返回,之后不存储**(推荐) ```go // 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()` **现状**: ```go 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 获取配置 - 用户体验差 **修复方案**: ```go 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` **现状**: ```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 临时故障导致同步失败 - 用户不知道同步结果 - 数据不一致 **修复方案**: ```go 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 配置来源不明确 **位置**: 多处使用 **现状**: ```go // Core 层接收 STUN 服务器列表 stunServers := config.STUNServers // ❌ 从哪里来? ``` **可能来源**: 1. Service 表查询 (`type='STUN'`) 2. ExternalService 表查询 3. 硬编码默认值 4. 配置文件 **排查结果**: - ✅ Service 层有查询逻辑 - ⚠️ 未明确调用链 - ⚠️ 未传递给 Core 层 **修复建议**: ```go // 在 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` **现状**: ```javascript // 前端 WebSocket 连接 const ws = new WebSocket(url) ws.onclose = () => { // ❌ 没有自动重连 console.log('WebSocket 已关闭') } ``` **修复方案**: ```javascript 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 问题修复(今天) 3. **完善 Network 创建返回** - 添加关联配置查询 - 返回完整信息包 - 预计:1 小时 4. **添加 DDNS 重试机制** - 实现指数退避 - 添加状态记录 - 预计:1 小时 5. **明确 STUN/TURN 传递链** - 添加代码注释 - 确保配置传递 - 预计:0.5 小时 ### Phase 3: P2 优化(明天) 6. **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 问题修复