package plane_v1 import ( "bytes" "encoding/json" "fmt" "fwh-toolbox-server/config" "io" "net/http" "strings" "time" "go.uber.org/zap" ) // 官方API手册 // https://developers.plane.so/api-reference/introduction const ( timeout time.Duration = 60 * time.Second // Plane请求超时60s ) var ( planeHost string planeApiKey string planeWorkspaceSlug string planeProjectId string planeLabelBugId string planeLabelFeatureId string ) // PlaneConfig Plane配置结构 type PlaneConfig struct { Host string ApiKey string WorkspaceSlug string ProjectId string LabelBugId string LabelFeatureId string } // PlaneHttpInit 初始化Plane HTTP客户端配置 func PlaneHttpInit(cfg *config.PlaneApiConfig) { planeHost = strings.TrimRight(cfg.Host, "/") planeApiKey = cfg.ApiKey planeWorkspaceSlug = cfg.WorkspaceSlug planeProjectId = cfg.ProjectId planeLabelBugId = cfg.LabelBugId planeLabelFeatureId = cfg.LabelFeatureId } // GetLabelBugId 获取Bug标签ID func GetLabelBugId() string { return planeLabelBugId } // GetLabelFeatureId 获取Feature标签ID func GetLabelFeatureId() string { return planeLabelFeatureId } // planeResponse 封装Plane HTTP响应 type planeResponse struct { StatusCode int Status string Body []byte } // planeHttpGet 发送GET请求到Plane API func planeHttpGet(relativeURL string) (*planeResponse, error) { url := planeHost + relativeURL zap.L().Debug("[Plane] HttpGet", zap.String("url", url)) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("X-API-Key", planeApiKey) req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } return &planeResponse{ StatusCode: resp.StatusCode, Status: resp.Status, Body: body, }, nil } // planeHttpPost 发送POST请求到Plane API func planeHttpPost(relativeURL string, reqBody any) (*planeResponse, error) { url := planeHost + relativeURL zap.L().Debug("[Plane] HttpPost", zap.String("url", url)) jsonData, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("序列化请求体失败: %w", err) } req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(jsonData)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("X-API-Key", planeApiKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } return &planeResponse{ StatusCode: resp.StatusCode, Status: resp.Status, Body: body, }, nil } // planeHttpPatch 发送PATCH请求到Plane API func planeHttpPatch(relativeURL string, reqBody any) (*planeResponse, error) { url := planeHost + relativeURL zap.L().Debug("[Plane] HttpPatch", zap.String("url", url)) jsonData, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("序列化请求体失败: %w", err) } req, err := http.NewRequest(http.MethodPatch, url, bytes.NewReader(jsonData)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("X-API-Key", planeApiKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } return &planeResponse{ StatusCode: resp.StatusCode, Status: resp.Status, Body: body, }, nil } // planeHttpPut 发送PUT请求(用于上传文件到presigned URL,不带API Key) func planeHttpPut(url string, data []byte, contentType string) (*planeResponse, error) { zap.L().Debug("[Plane] HttpPut", zap.String("url", url)) req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("Content-Type", contentType) client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } return &planeResponse{ StatusCode: resp.StatusCode, Status: resp.Status, Body: body, }, nil } // isSuccess 判断HTTP状态码是否表示成功(2xx) func isSuccess(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } // planeHttpDelete 发送DELETE请求到Plane API func planeHttpDelete(relativeURL string) (*planeResponse, error) { url := planeHost + relativeURL zap.L().Debug("[Plane] HttpDelete", zap.String("url", url)) req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { return nil, fmt.Errorf("创建请求失败: %w", err) } req.Header.Set("X-API-Key", planeApiKey) req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("读取响应失败: %w", err) } return &planeResponse{ StatusCode: resp.StatusCode, Status: resp.Status, Body: body, }, nil }