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