From 593169b85a4ed194100dc2a6158d7520f6a4b9ad Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Mon, 17 Aug 2026 16:13:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20intelli?= =?UTF-8?q?gence=5Ffinance=5Fv1=5Fga/client.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- intelligence_finance_v1_ga/client.go | 176 +++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 intelligence_finance_v1_ga/client.go diff --git a/intelligence_finance_v1_ga/client.go b/intelligence_finance_v1_ga/client.go new file mode 100644 index 0000000..79ed263 --- /dev/null +++ b/intelligence_finance_v1_ga/client.go @@ -0,0 +1,176 @@ +package intelligence_finance_v1_ga + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// 接口路径常量。 +// 文档未提供具体路径,接口短码在对接时分配,请根据实际分配结果修改以下常量。 +const ( + // PathOrderInvoice 订单开票接口路径。 + PathOrderInvoice = "/invoice/order" + // PathQueryInvoiceStatus 开票状态查询接口路径。 + PathQueryInvoiceStatus = "/invoice/status" + // PathCreatePaymentOrder 创建付款单据接口路径。 + PathCreatePaymentOrder = "/payment/order" + // PathQueryPaymentStatus 支付状态查询接口路径。 + PathQueryPaymentStatus = "/payment/status" +) + +// Client 是业财连接接口的 SDK 客户端。 +type Client struct { + baseURL string + tenantID string + clientID string + clientSecret string + httpClient *http.Client +} + +// ClientOption 定义客户端配置选项。 +type ClientOption func(*Client) + +// WithHTTPClient 自定义 HTTP 客户端。 +func WithHTTPClient(hc *http.Client) ClientOption { + return func(c *Client) { c.httpClient = hc } +} + +// NewClient 创建业财连接接口客户端。 +// baseURL 为平台接口根地址;tenantID 为平台分配的租户唯一标识; +// clientID 为平台分配的应用标识(钉钉AI表格固定为 "dd-ai-table"); +// clientSecret 为平台分配的密钥,用于 HmacSHA256 签名。 +func NewClient(baseURL, tenantID, clientID, clientSecret string, opts ...ClientOption) *Client { + c := &Client{ + baseURL: strings.TrimRight(baseURL, "/"), + tenantID: tenantID, + clientID: clientID, + clientSecret: clientSecret, + httpClient: &http.Client{Timeout: 30 * time.Second}, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// do 发送 POST 请求,自动生成时间戳、随机数并完成 HmacSHA256 签名。 +// 若响应符合通用结构 {code, msg, data},则自动解包 data 到 respBody; +// 否则直接将整个响应体解析到 respBody。 +func (c *Client) do(ctx context.Context, path string, reqBody, respBody interface{}) error { + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return fmt.Errorf("marshal request body: %w", err) + } + + timestamp := GenerateTimestamp() + nonce, err := GenerateNonce(16) + if err != nil { + return fmt.Errorf("generate nonce: %w", err) + } + signature := HmacSHA256Base64(c.clientSecret, timestamp+nonce) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) + if err != nil { + return fmt.Errorf("new request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("tenant-id", c.tenantID) + req.Header.Set("client-id", c.clientID) + req.Header.Set("x-bfl-signature-timestamp", timestamp) + req.Header.Set("x-bfl-signature-nonce", nonce) + req.Header.Set("x-bfl-signature", signature) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("do request: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return &APIError{StatusCode: resp.StatusCode, Message: string(data)} + } + + // 尝试按通用响应结构 {code, msg, data} 解包 + var common struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(data, &common); err == nil && len(common.Data) > 0 { + if err := json.Unmarshal(common.Data, respBody); err == nil { + return nil + } + } + + // 直接解析整个响应体 + if err := json.Unmarshal(data, respBody); err != nil { + return fmt.Errorf("unmarshal response body: %w", err) + } + return nil +} + +// OrderInvoice 提交订单开票请求。 +func (c *Client) OrderInvoice(ctx context.Context, req *OrderInvoiceRequest) (*OrderInvoiceResponse, error) { + var resp OrderInvoiceResponse + if err := c.do(ctx, PathOrderInvoice, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// QueryInvoiceStatus 查询订单开票状态。 +func (c *Client) QueryInvoiceStatus(ctx context.Context, req *QueryInvoiceStatusRequest) (*QueryInvoiceStatusResponse, error) { + var resp QueryInvoiceStatusResponse + if err := c.do(ctx, PathQueryInvoiceStatus, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// CreatePaymentOrder 创建付款单据。 +func (c *Client) CreatePaymentOrder(ctx context.Context, req *CreatePaymentOrderRequest) (*CreatePaymentOrderResponse, error) { + var resp CreatePaymentOrderResponse + if err := c.do(ctx, PathCreatePaymentOrder, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// QueryPaymentStatus 查询支付状态。 +func (c *Client) QueryPaymentStatus(ctx context.Context, req *QueryPaymentStatusRequest) (*QueryPaymentStatusResponse, error) { + var resp QueryPaymentStatusResponse + if err := c.do(ctx, PathQueryPaymentStatus, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ParsePaymentNotify 解析支付完成通知(接口4)的请求体。 +// 客户在回调接口中调用此方法解析通知数据,解析成功后应返回 "SUCCESS",失败返回 "FAILED"。 +func ParsePaymentNotify(body []byte) (*PaymentNotifyData, error) { + var data PaymentNotifyData + if err := json.Unmarshal(body, &data); err != nil { + return nil, fmt.Errorf("parse payment notify: %w", err) + } + return &data, nil +} + +// ParseCommonNotify 解析通用通知(bizType/bizId/data)的请求体。 +func ParseCommonNotify(body []byte) (*CommonNotifyData, error) { + var data CommonNotifyData + if err := json.Unmarshal(body, &data); err != nil { + return nil, fmt.Errorf("parse common notify: %w", err) + } + return &data, nil +} \ No newline at end of file