Initial commit

This commit is contained in:
2026-06-30 15:14:37 +08:00
commit 15dab96872
311 changed files with 95639 additions and 0 deletions
+502
View File
@@ -0,0 +1,502 @@
# 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 问题修复