添加文件: intelligence_finance_v1_ga/client.go

This commit is contained in:
renzhiyuan 2026-08-17 16:13:11 +08:00
parent 8cd0138fed
commit 593169b85a
1 changed files with 176 additions and 0 deletions

View File

@ -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
}