package plane_v2 import ( "encoding/json" "fmt" "fwh-toolbox-server/config" ) // PlaneService 便捷封装(绑定固定的 workspace/project,共享全局 PlaneClient) type PlaneService struct { Client *PlaneClient WorkspaceSlug string ProjectId string LabelBugId string LabelFeatureId string LabelFeedbackId string } // NewPlaneService 创建 PlaneService(须先调用 InitPlaneClient) func NewPlaneService(cfg *config.PlaneWebApiConfig) *PlaneService { if globalClient == nil { panic("[PlaneService] PlaneClient 未初始化,请先调用 InitPlaneClient") } return &PlaneService{ Client: globalClient, WorkspaceSlug: cfg.WorkspaceSlug, ProjectId: cfg.ProjectId, LabelBugId: cfg.LabelBugId, LabelFeatureId: cfg.LabelFeatureId, LabelFeedbackId: cfg.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 }