package plane_v2 import ( "encoding/json" "fmt" ) // PlaneService 便捷封装(绑定固定的 workspace/project,共享全局 PlaneClient) type PlaneService struct { Client *PlaneClient WorkspaceSlug string ProjectId string LabelBugId string LabelFeatureId string LabelFeedbackId string } // NewPlaneService 创建 PlaneService(须先调用 InitPlaneClient) func NewPlaneService(workspaceSlug, projectId, labelBugId, labelFeatureId, labelFeedbackId string) *PlaneService { if globalClient == nil { panic("[PlaneService] PlaneClient 未初始化,请先调用 InitPlaneClient") } return &PlaneService{ Client: globalClient, WorkspaceSlug: workspaceSlug, ProjectId: projectId, LabelBugId: labelBugId, LabelFeatureId: labelFeatureId, LabelFeedbackId: labelFeedbackId, } } // ============ Intake 便捷方法 ============ func (s *PlaneService) CreateIntakeIssue(issue IntakeIssueRequest) (*IntakeIssueResponse, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", s.WorkspaceSlug, s.ProjectId) payload := map[string]any{"source": "IN_APP", "issue": issue} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateIntakeIssue 失败: %s", resp.Status) } var result IntakeIssueResponse if err := json.Unmarshal(resp.Body, &result); err != nil { return nil, err } return &result, nil } func (s *PlaneService) ListIntakeIssues(status string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", s.WorkspaceSlug, s.ProjectId) if status != "" { url += "?status=" + status } resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListIntakeIssues 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetIntakeIssue(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/?expand=issue_inbox", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetIntakeIssue 失败: %s", resp.Status) } return resp.Body, nil } // ============ Asset 便捷方法 ============ func (s *PlaneService) UploadProjectAsset(fileName, mimeType string, fileData []byte, entityType string) (*AssetUploadResult, error) { // Step 1: 创建资源 createURL := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", s.WorkspaceSlug, s.ProjectId) createPayload := map[string]any{ "name": fileName, "type": mimeType, "size": len(fileData), "entity_type": entityType, "entity_identifier": "", } resp, err := s.Client.Post(createURL, createPayload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateProjectAsset 失败: %s", resp.Status) } var result AssetUploadResult if err := json.Unmarshal(resp.Body, &result); err != nil { return nil, err } // Step 2: 上传到 S3 if err := s.Client.PostS3(result.UploadData.URL, result.UploadData.Fields, fileData, fileName); err != nil { return nil, err } // Step 3: 确认 confirmURL := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, result.AssetID) s.Client.Patch(confirmURL, map[string]any{}) return &result, nil } func (s *PlaneService) GetProjectAssetURL(assetID string) (string, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID) return s.Client.GetRedirectLocation(url) } func (s *PlaneService) DeleteProjectAsset(assetID string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteProjectAsset 失败: %s", resp.Status) } return nil } func (s *PlaneService) BindAssetsToEntity(entityID string, assetIDs []string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/bulk/", s.WorkspaceSlug, s.ProjectId, entityID) resp, err := s.Client.Post(url, map[string]any{"asset_ids": assetIDs}) if err != nil { return err } if resp.StatusCode != 204 && !IsSuccess(resp.StatusCode) { return fmt.Errorf("BindAssetsToEntity 失败: %s", resp.Status) } return nil } // ============ Page 便捷方法 ============ func (s *PlaneService) ListPages() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListPages 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetPage(pageID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetPage 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetPageDescription(pageID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/description/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetPageDescription 失败: %s", resp.Status) } return resp.Body, nil } // ============ Workspace 便捷方法 ============ func (s *PlaneService) ListWorkspaces() ([]byte, error) { resp, err := s.Client.Get("/api/workspaces/") if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListWorkspaces 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetWorkspace() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetWorkspace 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateWorkspace(name, slug string) ([]byte, error) { payload := map[string]interface{}{"name": name, "slug": slug} resp, err := s.Client.Post("/api/workspaces/", payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateWorkspace 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateWorkspace(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateWorkspace 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteWorkspace() error { url := fmt.Sprintf("/api/workspaces/%s/", s.WorkspaceSlug) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteWorkspace 失败: %s", resp.Status) } return nil } // ============ Project 便捷方法 ============ func (s *PlaneService) ListProjects() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/", s.WorkspaceSlug) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListProjects 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetProject(projectID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetProject 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateProject(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/", s.WorkspaceSlug) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateProject 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateProject(projectID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateProject 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteProject(projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/", s.WorkspaceSlug, projectID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteProject 失败: %s", resp.Status) } return nil } func (s *PlaneService) ArchiveProject(projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", s.WorkspaceSlug, projectID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("ArchiveProject 失败: %s", resp.Status) } return nil } func (s *PlaneService) UnarchiveProject(projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", s.WorkspaceSlug, projectID) resp, err := s.Client.Delete(url) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("UnarchiveProject 失败: %s", resp.Status) } return nil } // ============ Issue 便捷方法 ============ func (s *PlaneService) ListIssues() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListIssues 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetIssue(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetIssue 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateIssue(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateIssue 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateIssue(issueID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateIssue 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteIssue(issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteIssue 失败: %s", resp.Status) } return nil } func (s *PlaneService) GetSubIssues(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/sub-issues/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetSubIssues 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) ArchiveIssue(issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("ArchiveIssue 失败: %s", resp.Status) } return nil } func (s *PlaneService) UnarchiveIssue(issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Delete(url) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("UnarchiveIssue 失败: %s", resp.Status) } return nil } func (s *PlaneService) GetIssueActivity(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/history/?activity_type=issue-property", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetIssueActivity 失败: %s", resp.Status) } return resp.Body, nil } // ============ Cycle 便捷方法 ============ func (s *PlaneService) ListCycles() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListCycles 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetCycle(cycleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetCycle 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateCycle(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateCycle 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateCycle(cycleID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateCycle 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteCycle(cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteCycle 失败: %s", resp.Status) } return nil } func (s *PlaneService) ListCycleIssues(cycleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListCycleIssues 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) AddIssuesToCycle(cycleID string, issueIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", s.WorkspaceSlug, s.ProjectId, cycleID) payload := map[string]interface{}{"issues": issueIDs} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("AddIssuesToCycle 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) RemoveIssueFromCycle(cycleID, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", s.WorkspaceSlug, s.ProjectId, cycleID, issueID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("RemoveIssueFromCycle 失败: %s", resp.Status) } return nil } func (s *PlaneService) ArchiveCycle(cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("ArchiveCycle 失败: %s", resp.Status) } return nil } func (s *PlaneService) UnarchiveCycle(cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", s.WorkspaceSlug, s.ProjectId, cycleID) resp, err := s.Client.Delete(url) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("UnarchiveCycle 失败: %s", resp.Status) } return nil } // ============ Module 便捷方法 ============ func (s *PlaneService) ListModules() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListModules 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetModule(moduleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetModule 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateModule(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateModule 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateModule(moduleID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateModule 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteModule(moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteModule 失败: %s", resp.Status) } return nil } func (s *PlaneService) ListModuleIssues(moduleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListModuleIssues 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) AddIssuesToModule(moduleID string, issueIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", s.WorkspaceSlug, s.ProjectId, moduleID) payload := map[string]interface{}{"issues": issueIDs} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("AddIssuesToModule 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) RemoveIssueFromModule(moduleID, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/%s/", s.WorkspaceSlug, s.ProjectId, moduleID, issueID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("RemoveIssueFromModule 失败: %s", resp.Status) } return nil } func (s *PlaneService) ArchiveModule(moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("ArchiveModule 失败: %s", resp.Status) } return nil } func (s *PlaneService) UnarchiveModule(moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", s.WorkspaceSlug, s.ProjectId, moduleID) resp, err := s.Client.Delete(url) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("UnarchiveModule 失败: %s", resp.Status) } return nil } // ============ Label 便捷方法 ============ func (s *PlaneService) ListLabels() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListLabels 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetLabel(labelID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetLabel 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateLabel(name, color string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/", s.WorkspaceSlug, s.ProjectId) payload := map[string]interface{}{"name": name, "color": color} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateLabel 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateLabel(labelID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateLabel 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteLabel(labelID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/labels/%s/", s.WorkspaceSlug, s.ProjectId, labelID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteLabel 失败: %s", resp.Status) } return nil } // ============ State 便捷方法 ============ func (s *PlaneService) ListStates() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListStates 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetState(stateID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetState 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateState(name, color, group string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", s.WorkspaceSlug, s.ProjectId) payload := map[string]interface{}{"name": name, "color": color, "group": group} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateState 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateState(stateID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateState 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteState(stateID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", s.WorkspaceSlug, s.ProjectId, stateID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteState 失败: %s", resp.Status) } return nil } func (s *PlaneService) MarkStateDefault(stateID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/default/", s.WorkspaceSlug, s.ProjectId, stateID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("MarkStateDefault 失败: %s", resp.Status) } return nil } // ============ Comment 便捷方法 ============ func (s *PlaneService) ListComments(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListComments 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetComment(issueID, commentID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetComment 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateComment(issueID, commentHTML string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", s.WorkspaceSlug, s.ProjectId, issueID) payload := map[string]interface{}{"comment_html": commentHTML} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateComment 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateComment(issueID, commentID, commentHTML string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID) payload := map[string]interface{}{"comment_html": commentHTML} resp, err := s.Client.Patch(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateComment 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteComment(issueID, commentID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", s.WorkspaceSlug, s.ProjectId, issueID, commentID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteComment 失败: %s", resp.Status) } return nil } // ============ Link 便捷方法 ============ func (s *PlaneService) ListIssueLinks(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListIssueLinks 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateIssueLink(issueID, linkURL, title string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", s.WorkspaceSlug, s.ProjectId, issueID) payload := map[string]interface{}{"url": linkURL, "title": title} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateIssueLink 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateIssueLink(issueID, linkID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", s.WorkspaceSlug, s.ProjectId, issueID, linkID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateIssueLink 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteIssueLink(issueID, linkID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", s.WorkspaceSlug, s.ProjectId, issueID, linkID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteIssueLink 失败: %s", resp.Status) } return nil } // ============ Relation 便捷方法 ============ func (s *PlaneService) ListIssueRelations(issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListIssueRelations 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateIssueRelation(issueID, relationType string, relatedIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", s.WorkspaceSlug, s.ProjectId, issueID) payload := map[string]interface{}{"relation_type": relationType, "related_issues": relatedIDs} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateIssueRelation 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) RemoveIssueRelation(issueID, relationType, relatedID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/%s/%s/", s.WorkspaceSlug, s.ProjectId, issueID, relationType, relatedID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("RemoveIssueRelation 失败: %s", resp.Status) } return nil } // ============ Sticky 便捷方法 ============ func (s *PlaneService) ListStickies() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/", s.WorkspaceSlug) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListStickies 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetSticky(stickyID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetSticky 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) CreateSticky(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/", s.WorkspaceSlug) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateSticky 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateSticky(stickyID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateSticky 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeleteSticky(stickyID string) error { url := fmt.Sprintf("/api/workspaces/%s/stickies/%s/", s.WorkspaceSlug, stickyID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteSticky 失败: %s", resp.Status) } return nil } // ============ Intake 补充方法 ============ func (s *PlaneService) UpdateIntakeIssue(issueID string, data map[string]any) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateIntakeIssue 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) AcceptIntakeIssue(issueID string) ([]byte, error) { return s.UpdateIntakeIssue(issueID, map[string]any{"status": 1}) } func (s *PlaneService) RejectIntakeIssue(issueID string) ([]byte, error) { return s.UpdateIntakeIssue(issueID, map[string]any{"status": -1}) } func (s *PlaneService) DeleteIntakeIssue(issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", s.WorkspaceSlug, s.ProjectId, issueID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeleteIntakeIssue 失败: %s", resp.Status) } return nil } // ============ Asset 补充方法 ============ func (s *PlaneService) CreateProjectAsset(fileName, mimeType string, size int64, entityType string) (*AssetUploadResult, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", s.WorkspaceSlug, s.ProjectId) payload := map[string]interface{}{ "name": fileName, "type": mimeType, "size": size, "entity_type": entityType, "entity_identifier": "", } resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreateProjectAsset 失败: %s", resp.Status) } var result AssetUploadResult if err := json.Unmarshal(resp.Body, &result); err != nil { return nil, fmt.Errorf("解析响应失败: %w", err) } return &result, nil } func (s *PlaneService) ConfirmProjectAssetUpload(assetID string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID) resp, err := s.Client.Patch(url, map[string]interface{}{}) if err != nil { return err } if resp.StatusCode != 204 && !IsSuccess(resp.StatusCode) { return fmt.Errorf("ConfirmProjectAssetUpload 失败: %s", resp.Status) } return nil } func (s *PlaneService) GetProjectAsset(assetID string) ([]byte, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", s.WorkspaceSlug, s.ProjectId, assetID) resp, err := s.Client.Get(url) if err != nil { return nil, err } return resp.Body, nil } // ============ Page 补充方法 ============ func (s *PlaneService) CreatePage(data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Post(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("CreatePage 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdatePage(pageID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Patch(url, data) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdatePage 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) DeletePage(pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("DeletePage 失败: %s", resp.Status) } return nil } func (s *PlaneService) ArchivePage(pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Post(url, nil) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("ArchivePage 失败: %s", resp.Status) } return nil } func (s *PlaneService) UnarchivePage(pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", s.WorkspaceSlug, s.ProjectId, pageID) resp, err := s.Client.Delete(url) if err != nil { return err } if !IsSuccess(resp.StatusCode) { return fmt.Errorf("UnarchivePage 失败: %s", resp.Status) } return nil } // ============ Member 便捷方法 ============ func (s *PlaneService) ListWorkspaceMembers() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/", s.WorkspaceSlug) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListWorkspaceMembers 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetWorkspaceMember(memberID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetWorkspaceMember 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) UpdateWorkspaceMemberRole(memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID) payload := map[string]interface{}{"role": role} resp, err := s.Client.Patch(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateWorkspaceMemberRole 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) RemoveWorkspaceMember(memberID string) error { url := fmt.Sprintf("/api/workspaces/%s/members/%s/", s.WorkspaceSlug, memberID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("RemoveWorkspaceMember 失败: %s", resp.Status) } return nil } func (s *PlaneService) ListProjectMembers() ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", s.WorkspaceSlug, s.ProjectId) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("ListProjectMembers 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) GetProjectMember(memberID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID) resp, err := s.Client.Get(url) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("GetProjectMember 失败: %s", resp.Status) } return resp.Body, nil } func (s *PlaneService) AddProjectMember(memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", s.WorkspaceSlug, s.ProjectId) payload := map[string]interface{}{"member": memberID, "role": role} resp, err := s.Client.Post(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("AddProjectMember 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) UpdateProjectMemberRole(memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID) payload := map[string]interface{}{"role": role} resp, err := s.Client.Patch(url, payload) if err != nil { return nil, err } if !IsSuccess(resp.StatusCode) { return nil, fmt.Errorf("UpdateProjectMemberRole 失败: %s, body=%s", resp.Status, string(resp.Body)) } return resp.Body, nil } func (s *PlaneService) RemoveProjectMember(memberID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", s.WorkspaceSlug, s.ProjectId, memberID) resp, err := s.Client.Delete(url) if err != nil { return err } if resp.StatusCode != 204 { return fmt.Errorf("RemoveProjectMember 失败: %s", resp.Status) } return nil }