package plane_v2 import ( "encoding/json" "fmt" ) // PlaneService 通用 Plane API 封装(无绑定配置,workspace/project 通过参数传入) type PlaneService struct { Client *PlaneClient } // NewPlaneService 创建通用 PlaneService(须先调用 InitPlaneClient) func NewPlaneService() *PlaneService { if globalClient == nil { panic("[PlaneService] PlaneClient 未初始化,请先调用 InitPlaneClient") } return &PlaneService{Client: globalClient} } // ============ Workspace 方法 ============ func (s *PlaneService) ListWorkspaces() ([]byte, error) { resp, err := s.Client.Get("/api/users/me/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(workspaceSlug string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%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(workspaceSlug string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%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(workspaceSlug string) error { url := fmt.Sprintf("/api/workspaces/%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(workspaceSlug string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/", 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(workspaceSlug, projectID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%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(workspaceSlug string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/", 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(workspaceSlug, projectID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%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(workspaceSlug, projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%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(workspaceSlug, projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", 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(workspaceSlug, projectID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/archive/", 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(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", workspaceSlug, 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(workspaceSlug, projectId string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/", workspaceSlug, 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(workspaceSlug, projectId, issueID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/sub-issues/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/history/?activity_type=issue-property", workspaceSlug, 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 } // ============ Intake 方法 ============ func (s *PlaneService) CreateIntakeIssue(workspaceSlug, projectId string, issue IntakeIssueRequest) (*IntakeIssueResponse, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", workspaceSlug, 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(workspaceSlug, projectId, status string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/?expand=issue_inbox", workspaceSlug, 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 } func (s *PlaneService) UpdateIntakeIssue(workspaceSlug, projectId, issueID string, data map[string]any) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { return s.UpdateIntakeIssue(workspaceSlug, projectId, issueID, map[string]any{"status": 1}) } func (s *PlaneService) RejectIntakeIssue(workspaceSlug, projectId, issueID string) ([]byte, error) { return s.UpdateIntakeIssue(workspaceSlug, projectId, issueID, map[string]any{"status": -1}) } func (s *PlaneService) DeleteIntakeIssue(workspaceSlug, projectId, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/intake-issues/%s/", workspaceSlug, 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) UploadProjectAsset(workspaceSlug, projectId, fileName, mimeType string, fileData []byte, entityType string) (*AssetUploadResult, error) { // Step 1: 创建资源 createURL := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", workspaceSlug, 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/", workspaceSlug, projectId, result.AssetID) s.Client.Patch(confirmURL, map[string]any{}) return &result, nil } func (s *PlaneService) CreateProjectAsset(workspaceSlug, projectId, fileName, mimeType string, size int64, entityType string) (*AssetUploadResult, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/", workspaceSlug, 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(workspaceSlug, projectId, assetID string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", workspaceSlug, 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) GetProjectAssetURL(workspaceSlug, projectId, assetID string) (string, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", workspaceSlug, projectId, assetID) return s.Client.GetRedirectLocation(url) } func (s *PlaneService) GetProjectAsset(workspaceSlug, projectId, assetID string) ([]byte, error) { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", workspaceSlug, projectId, assetID) resp, err := s.Client.Get(url) if err != nil { return nil, err } return resp.Body, nil } func (s *PlaneService) DeleteProjectAsset(workspaceSlug, projectId, assetID string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/", workspaceSlug, 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(workspaceSlug, projectId, entityID string, assetIDs []string) error { url := fmt.Sprintf("/api/assets/v2/workspaces/%s/projects/%s/%s/bulk/", workspaceSlug, 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 } // ============ Cycle 方法 ============ func (s *PlaneService) ListCycles(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", workspaceSlug, 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(workspaceSlug, projectId string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string, issueIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/", workspaceSlug, 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(workspaceSlug, projectId, cycleID, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/cycle-issues/%s/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId, cycleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/cycles/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", workspaceSlug, 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(workspaceSlug, projectId string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string, issueIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/", workspaceSlug, 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(workspaceSlug, projectId, moduleID, issueID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/module-issues/%s/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId, moduleID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/modules/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", workspaceSlug, 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(workspaceSlug, projectId, labelID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, 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(workspaceSlug, projectId, name, color string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/", workspaceSlug, 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(workspaceSlug, projectId, labelID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, 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(workspaceSlug, projectId, labelID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issue-labels/%s/", workspaceSlug, 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(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", workspaceSlug, 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(workspaceSlug, projectId, stateID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", workspaceSlug, 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(workspaceSlug, projectId, name, color, group string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/", workspaceSlug, 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(workspaceSlug, projectId, stateID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", workspaceSlug, 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(workspaceSlug, projectId, stateID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/", workspaceSlug, 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(workspaceSlug, projectId, stateID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/states/%s/default/", workspaceSlug, 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 } // ============ Page 方法 ============ func (s *PlaneService) ListPages(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", workspaceSlug, 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(workspaceSlug, projectId, pageID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", workspaceSlug, 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(workspaceSlug, projectId, pageID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/description/", workspaceSlug, 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 } func (s *PlaneService) CreatePage(workspaceSlug, projectId string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/", workspaceSlug, 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(workspaceSlug, projectId, pageID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", workspaceSlug, 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(workspaceSlug, projectId, pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/", workspaceSlug, 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(workspaceSlug, projectId, pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", workspaceSlug, 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(workspaceSlug, projectId, pageID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/pages/%s/archive/", workspaceSlug, 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 } // ============ Comment 方法 ============ func (s *PlaneService) ListComments(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", workspaceSlug, 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(workspaceSlug, projectId, issueID, commentID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID, commentHTML string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/", workspaceSlug, 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(workspaceSlug, projectId, issueID, commentID, commentHTML string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID, commentID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/comments/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", workspaceSlug, 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(workspaceSlug, projectId, issueID, linkURL, title string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/", workspaceSlug, 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(workspaceSlug, projectId, issueID, linkID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID, linkID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/links/%s/", workspaceSlug, 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(workspaceSlug, projectId, issueID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", workspaceSlug, 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(workspaceSlug, projectId, issueID, relationType string, relatedIDs []string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/", workspaceSlug, 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(workspaceSlug, projectId, issueID, relationType, relatedID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/issues/%s/relations/%s/%s/", workspaceSlug, 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 } // ============ Member 方法 ============ func (s *PlaneService) ListWorkspaceMembers(workspaceSlug string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/", 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(workspaceSlug, memberID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/%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(workspaceSlug, memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/members/%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(workspaceSlug, memberID string) error { url := fmt.Sprintf("/api/workspaces/%s/members/%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(workspaceSlug, projectId string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", workspaceSlug, 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(workspaceSlug, projectId, memberID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", workspaceSlug, 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(workspaceSlug, projectId, memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/", workspaceSlug, 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(workspaceSlug, projectId, memberID string, role int) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", workspaceSlug, 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(workspaceSlug, projectId, memberID string) error { url := fmt.Sprintf("/api/workspaces/%s/projects/%s/members/%s/", workspaceSlug, 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 } // ============ Sticky 方法 ============ func (s *PlaneService) ListStickies(workspaceSlug string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/", 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(workspaceSlug, stickyID string) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/%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(workspaceSlug string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/", 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(workspaceSlug, stickyID string, data map[string]interface{}) ([]byte, error) { url := fmt.Sprintf("/api/workspaces/%s/stickies/%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(workspaceSlug, stickyID string) error { url := fmt.Sprintf("/api/workspaces/%s/stickies/%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 }