本次重构实现了标准化的本地配置系统,替换原有的硬编码环境变量读取逻辑: 1. 新增跨平台的原子化配置文件读写,支持Unix和Windows系统 2. 新增init命令用于安全初始化和更新本地凭据 3. 替换原有错误提示文案为更友好的中文提示 4. 更新文档说明新的配置流程和安全规范 5. 新增完整的配置相关测试用例 6. 添加必要的依赖包支持
167 lines
7.6 KiB
Go
167 lines
7.6 KiB
Go
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{})
|
||
}
|