Files
shop-crm-agent/internal/client/client.go
T
Shiny 340dd241c9 feat: 实现完整的本地配置管理与CLI重构
本次重构实现了标准化的本地配置系统,替换原有的硬编码环境变量读取逻辑:
1. 新增跨平台的原子化配置文件读写,支持Unix和Windows系统
2. 新增init命令用于安全初始化和更新本地凭据
3. 替换原有错误提示文案为更友好的中文提示
4. 更新文档说明新的配置流程和安全规范
5. 新增完整的配置相关测试用例
6. 添加必要的依赖包支持
2026-08-01 02:39:49 +08:00

167 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("CRM Agent API Base URL 必须是有效的 http/https URL")
}
if strings.TrimSpace(token) == "" {
return nil, errors.New("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{})
}