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 }