12 KiB
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 // ❌ 从哪里来?
可能来源:
- Service 表查询 (
type='STUN') - ExternalService 表查询
- 硬编码默认值
- 配置文件
排查结果:
- ✅ 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 问题修复(立即)
-
修复 PendingJoin 审核逻辑
- 添加设备创建
- 添加密钥生成
- 添加配置返回
- 预计:2 小时
-
修复 DeviceService 配置生成
- 修改 CreateDevice 返回值
- 添加配置下载接口
- 预计:1 小时
Phase 2: P1 问题修复(今天)
-
完善 Network 创建返回
- 添加关联配置查询
- 返回完整信息包
- 预计:1 小时
-
添加 DDNS 重试机制
- 实现指数退避
- 添加状态记录
- 预计:1 小时
-
明确 STUN/TURN 传递链
- 添加代码注释
- 确保配置传递
- 预计:0.5 小时
Phase 3: P2 优化(明天)
- WebSocket 重连机制
- 前端实现重连
- 添加心跳检测
- 预计:1 小时
✅ 验收标准
P0 问题验收
PendingJoin 审核:
- ✅ 管理员审核通过后,能看到完整配置
- ✅ 可以复制配置发送给申请人
- ✅ 申请人导入配置即可连接
DeviceService 配置:
- ✅ 创建设备时能下载配置文件
- ✅ 配置文件格式正确
- ✅ 可直接导入 WireGuard 客户端
P1 问题验收
Network 创建:
- ✅ 创建后返回完整信息包
- ✅ 包含 STUN/TURN 配置
- ✅ 包含 DDNS 配置(如果启用)
DDNS 重试:
- ✅ 临时故障自动恢复
- ✅ 失败有明确提示
- ✅ 状态可查询
📝 总结
核心问题:
- ❌ 审核通过 ≠ 获得配置(逻辑断裂)
- ❌ 创建设备 ≠ 能下载配置(功能残废)
- ❌ 创建网络 ≠ 能用(信息不完整)
根本原因:
- 前后端沟通不足
- 服务边界不清晰
- 缺少端到端验证
改进方向:
- 加强全链路测试
- 建立验收标准
- 完善错误处理
- 增加日志记录
排查人员: AI Assistant
排查时间: 2026-03-20
下一步: 立即开始 P0 问题修复