package client import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "strings" "time" api "code.zhecent.com/open/shop-crm-agent/internal/api" "google.golang.org/protobuf/proto" ) const ( apiPath = "/api/shop/crm-agent/v1" maxResponseSize = 16 << 20 ) type Client struct { baseURL string token string httpClient *http.Client } func New(baseURL, token string, httpClient *http.Client) (*Client, error) { baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") parsed, err := url.Parse(baseURL) if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return nil, errors.New("LIGHTCORE_API_BASE_URL 必须是有效的 http/https URL") } if strings.TrimSpace(token) == "" { return nil, errors.New("LIGHTCORE_SHOP_CRM_AGENT_TOKEN 不能为空") } if httpClient == nil { httpClient = &http.Client{Timeout: 30 * time.Second} } return &Client{baseURL: baseURL, token: strings.TrimSpace(token), httpClient: httpClient}, nil } type responseWithHeader interface { proto.Message GetHeader() *api.ResponseHeader } func doRequest[Req proto.Message, Resp responseWithHeader](ctx context.Context, client *Client, path string, request Req, response Resp) (Resp, error) { data, err := proto.Marshal(request) if err != nil { return response, fmt.Errorf("编码 Protobuf 请求失败: %w", err) } httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+apiPath+path, bytes.NewReader(data)) if err != nil { return response, err } httpRequest.Header.Set("Content-Type", "application/x-protobuf") httpRequest.Header.Set("Accept", "application/x-protobuf") httpRequest.Header.Set("Authorization", "Bearer "+client.token) httpResponse, err := client.httpClient.Do(httpRequest) if err != nil { return response, err } defer httpResponse.Body.Close() body, err := io.ReadAll(io.LimitReader(httpResponse.Body, maxResponseSize+1)) if err != nil { return response, err } if len(body) > maxResponseSize { return response, errors.New("CRM Agent API 响应超过16 MiB限制") } if err := proto.Unmarshal(body, response); err != nil { return response, fmt.Errorf("解析 Protobuf 响应失败(HTTP %d): %w", httpResponse.StatusCode, err) } header := response.GetHeader() if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 { if header != nil && header.Message != "" { return response, fmt.Errorf("CRM Agent API HTTP %d: %s", httpResponse.StatusCode, header.Message) } return response, fmt.Errorf("CRM Agent API HTTP %d", httpResponse.StatusCode) } if header == nil { return response, errors.New("CRM Agent API 响应缺少 ResponseHeader") } if header.Code != 200 { return response, fmt.Errorf("CRM Agent API 错误 %d: %s", header.Code, header.Message) } return response, nil } func (c *Client) CustomerList(ctx context.Context, req *api.CustomerListRequest) (*api.CustomerListResponse, error) { return doRequest(ctx, c, "/customers/list", req, &api.CustomerListResponse{}) } func (c *Client) ProjectList(ctx context.Context, req *api.ProjectListRequest) (*api.ProjectListResponse, error) { return doRequest(ctx, c, "/projects/list", req, &api.ProjectListResponse{}) } func (c *Client) ProjectGet(ctx context.Context, req *api.ProjectGetRequest) (*api.ProjectResponse, error) { return doRequest(ctx, c, "/projects/get", req, &api.ProjectResponse{}) } func (c *Client) ProjectCreate(ctx context.Context, req *api.ProjectCreateRequest) (*api.ProjectResponse, error) { return doRequest(ctx, c, "/projects/create", req, &api.ProjectResponse{}) } func (c *Client) ProjectUpdate(ctx context.Context, req *api.ProjectUpdateRequest) (*api.ProjectResponse, error) { return doRequest(ctx, c, "/projects/update", req, &api.ProjectResponse{}) } func (c *Client) ProjectStatus(ctx context.Context, req *api.ProjectStatusRequest) (*api.ProjectResponse, error) { return doRequest(ctx, c, "/projects/status", req, &api.ProjectResponse{}) } func (c *Client) ProjectDeletePreview(ctx context.Context, req *api.ProjectDeletePreviewRequest) (*api.DeletePreviewResponse, error) { return doRequest(ctx, c, "/projects/delete/preview", req, &api.DeletePreviewResponse{}) } func (c *Client) ProjectDelete(ctx context.Context, req *api.ProjectDeleteRequest) (*api.DeletePreviewResponse, error) { return doRequest(ctx, c, "/projects/delete", req, &api.DeletePreviewResponse{}) } func (c *Client) QuotationList(ctx context.Context, req *api.QuotationListRequest) (*api.QuotationListResponse, error) { return doRequest(ctx, c, "/quotations/list", req, &api.QuotationListResponse{}) } func (c *Client) QuotationGet(ctx context.Context, req *api.QuotationGetRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/get", req, &api.QuotationResponse{}) } func (c *Client) QuotationCreate(ctx context.Context, req *api.QuotationCreateRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/create", req, &api.QuotationResponse{}) } func (c *Client) QuotationCopy(ctx context.Context, req *api.QuotationCopyRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/copy", req, &api.QuotationResponse{}) } func (c *Client) QuotationTitle(ctx context.Context, req *api.QuotationTitleRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/title", req, &api.QuotationResponse{}) } func (c *Client) QuotationStatus(ctx context.Context, req *api.QuotationStatusRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/status", req, &api.QuotationResponse{}) } func (c *Client) QuotationLock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/lock", req, &api.QuotationResponse{}) } func (c *Client) QuotationUnlock(ctx context.Context, req *api.QuotationLockRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotations/unlock", req, &api.QuotationResponse{}) } func (c *Client) QuotationDeletePreview(ctx context.Context, req *api.QuotationDeletePreviewRequest) (*api.DeletePreviewResponse, error) { return doRequest(ctx, c, "/quotations/delete/preview", req, &api.DeletePreviewResponse{}) } func (c *Client) QuotationDelete(ctx context.Context, req *api.QuotationDeleteRequest) (*api.DeletePreviewResponse, error) { return doRequest(ctx, c, "/quotations/delete", req, &api.DeletePreviewResponse{}) } func (c *Client) CatalogSearch(ctx context.Context, req *api.CatalogSearchRequest) (*api.CatalogSearchResponse, error) { return doRequest(ctx, c, "/catalog/search", req, &api.CatalogSearchResponse{}) } func (c *Client) EditBegin(ctx context.Context, req *api.QuotationEditBeginRequest) (*api.EditSessionResponse, error) { return doRequest(ctx, c, "/quotation/edit/begin", req, &api.EditSessionResponse{}) } func (c *Client) EditCommand(ctx context.Context, req *api.QuotationEditCommandRequest) (*api.QuotationEditCommandResponse, error) { return doRequest(ctx, c, "/quotation/edit/command", req, &api.QuotationEditCommandResponse{}) } func (c *Client) EditPreview(ctx context.Context, req *api.QuotationEditPreviewRequest) (*api.QuotationEditPreviewResponse, error) { return doRequest(ctx, c, "/quotation/edit/preview", req, &api.QuotationEditPreviewResponse{}) } func (c *Client) EditCommit(ctx context.Context, req *api.QuotationEditCommitRequest) (*api.QuotationResponse, error) { return doRequest(ctx, c, "/quotation/edit/commit", req, &api.QuotationResponse{}) } func (c *Client) EditDiscard(ctx context.Context, req *api.QuotationEditDiscardRequest) (*api.EditSessionResponse, error) { return doRequest(ctx, c, "/quotation/edit/discard", req, &api.EditSessionResponse{}) }