diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 7fd41174..4e956996 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -14,6 +14,7 @@ sidebar: false - 控制台接入中英双语(next-intl,无 URL 语言前缀):默认中文,可在顶栏或「外观设置」切换;选择写入 cookie 后刷新生效。 ### 🛠 修复 +- 修复 Cloudflare 指向分组引用的节点已被删除时,分组列表/详情接口整体返回「Cloudflare 资源不存在」的问题;现会跳过缺失节点并继续返回其余分组。 - 修复静态导出部署下访问 Cloudflare 指向分组详情(`/cloudflare/groups/{id}`,id 不为 1)会跳回首页并触发 React hydration 报错的问题。 - 修复在网站列表中删除已加入 Cloudflare 指向分组的域名后,访问 Cloudflare 指向分组详情报错「Cloudflare 资源不存在」的问题。 - 修复自定义 Webhook 推送在企业微信/钉钉返回 HTTP 200 但 `errcode` 非零时仍记为成功的问题;任务日志会记录上游响应体。 diff --git a/internal/apps/openflare/cloudflare/logics.go b/internal/apps/openflare/cloudflare/logics.go index 8d14935a..d6b2dbf9 100644 --- a/internal/apps/openflare/cloudflare/logics.go +++ b/internal/apps/openflare/cloudflare/logics.go @@ -255,11 +255,11 @@ func validEdgeNode(ctx context.Context, id uint, requireIPv4 bool) (*model.OpenF } func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupItem, error) { - primary, err := repository.GetOpenFlareNodeByID(ctx, group.PrimaryNodeID) + primary, err := lookupGroupNode(ctx, group.ID, group.PrimaryNodeID) if err != nil { return nil, err } - active, err := repository.GetOpenFlareNodeByID(ctx, group.ActiveNodeID) + active, err := lookupGroupNode(ctx, group.ID, group.ActiveNodeID) if err != nil { return nil, err } @@ -267,18 +267,34 @@ func buildGroupItem(ctx context.Context, group *model.CFPointingGroup) (*GroupIt if err != nil { return nil, err } - item := &GroupItem{ID: group.ID, Name: group.Name, PrimaryNode: nodeOption(primary), ActiveNode: nodeOption(active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt} + item := &GroupItem{ID: group.ID, Name: group.Name, PrimaryNode: nodeOptionForID(group.PrimaryNodeID, primary), ActiveNode: nodeOptionForID(group.ActiveNodeID, active), DefaultProxied: group.DefaultProxied, Enabled: group.Enabled, MemberCount: count, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt} if group.BackupNodeID != nil { - backup, backupErr := repository.GetOpenFlareNodeByID(ctx, *group.BackupNodeID) + backup, backupErr := lookupGroupNode(ctx, group.ID, *group.BackupNodeID) if backupErr != nil { return nil, backupErr } - option := nodeOption(backup) + option := nodeOptionForID(*group.BackupNodeID, backup) item.BackupNode = &option } return item, nil } +func lookupGroupNode(ctx context.Context, groupID, nodeID uint) (*model.OpenFlareNode, error) { + node, err := repository.GetOpenFlareNodeByID(ctx, nodeID) + if errors.Is(err, gorm.ErrRecordNotFound) { + logger.WarnF(ctx, "[Cloudflare] pointing group references missing node: group_id=%d node_id=%d", groupID, nodeID) + return nil, nil + } + return node, err +} + +func nodeOptionForID(id uint, node *model.OpenFlareNode) NodeOption { + if node == nil { + return NodeOption{ID: id} + } + return nodeOption(node) +} + func nodeOption(node *model.OpenFlareNode) NodeOption { return NodeOption{ID: node.ID, Name: node.Name, IP: node.IP} } diff --git a/internal/apps/openflare/cloudflare/routers_test.go b/internal/apps/openflare/cloudflare/routers_test.go index d945de53..f6adc344 100644 --- a/internal/apps/openflare/cloudflare/routers_test.go +++ b/internal/apps/openflare/cloudflare/routers_test.go @@ -4,12 +4,14 @@ package cloudflare import ( + "fmt" "net/http" "net/http/httptest" "strings" "testing" db "github.com/Rain-kl/Wavelet/internal/infra/persistence" + "github.com/Rain-kl/Wavelet/internal/model" "github.com/Rain-kl/Wavelet/internal/repository" "github.com/Rain-kl/Wavelet/internal/shared/response" "github.com/gin-gonic/gin" @@ -75,3 +77,56 @@ func TestGetGroupWithOrphanedMemberHealsAndSucceeds(t *testing.T) { t.Errorf("GetCFPointingMemberByID() should return not found after healing") } } + +func TestListGroupsHandlerWithMissingNodeStillSucceeds(t *testing.T) { + ctx, _ := setupCloudflareLogicDB(t) + if err := db.DB(ctx).Create(&model.CFPointingGroup{ + Name: "KR", + PrimaryNodeID: 12, + ActiveNodeID: 12, + Enabled: true, + }).Error; err != nil { + t.Fatalf("Create(missing-node group) error = %v", err) + } + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(response.ErrorHandlerMiddleware()) + router.GET("/groups", ListGroupsHandler) + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/groups", nil)) + if recorder.Code != http.StatusOK { + t.Fatalf("ListGroupsHandler status = %d, body = %s, want %d", recorder.Code, recorder.Body.String(), http.StatusOK) + } + body := recorder.Body.String() + if !strings.Contains(body, `"name":"KR"`) { + t.Fatalf("ListGroupsHandler body = %s, want group KR", body) + } + if !strings.Contains(body, `"name":"primary"`) { + t.Fatalf("ListGroupsHandler body = %s, want intact group primary", body) + } +} + +func TestGetGroupHandlerWithMissingNodeStillSucceeds(t *testing.T) { + ctx, _ := setupCloudflareLogicDB(t) + group := model.CFPointingGroup{Name: "KR", PrimaryNodeID: 12, ActiveNodeID: 12, Enabled: true} + if err := db.DB(ctx).Create(&group).Error; err != nil { + t.Fatalf("Create(missing-node group) error = %v", err) + } + + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(response.ErrorHandlerMiddleware()) + router.GET("/groups/:id", GetGroupHandler) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/groups/%d", group.ID), nil) + router.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("GetGroupHandler status = %d, body = %s, want %d", recorder.Code, recorder.Body.String(), http.StatusOK) + } + if !strings.Contains(recorder.Body.String(), `"name":"KR"`) { + t.Fatalf("GetGroupHandler body = %s, want group KR", recorder.Body.String()) + } +}