添加文件: intelligence_finance_v1_ga/client.go
This commit is contained in:
parent
8cd0138fed
commit
593169b85a
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue