intelligence_finance_v1_ga-.../intelligence_finance_v1_ga/client.go

176 lines
5.6 KiB
Go
Raw Permalink 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 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
}