intelligence_finance_v1_ga-.../intelligence_finance_v1_ga/client.go

237 lines
7.5 KiB
Go
Raw 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"
)
// 默认接口路径(实际路径以接口对接时分配的短码为准,可通过 Option 覆盖)。
const (
DefaultInvoiceOrderPath = "/invoice/order"
DefaultQueryInvoiceStatusPath = "/invoice/status/query"
DefaultCreatePaymentPath = "/payment/create"
DefaultQueryPaymentStatusPath = "/payment/status/query"
)
// Client 是业财连接接口的客户端。
type Client struct {
baseURL string
tenantID string
clientID string
clientSecret string
httpClient *http.Client
signEnabled bool
invoiceOrderPath string
queryInvoiceStatusPath string
createPaymentPath string
queryPaymentStatusPath string
}
// Option 定义客户端配置选项。
type Option func(*Client)
// WithHTTPClient 设置自定义 HTTP 客户端。
func WithHTTPClient(c *http.Client) Option {
return func(cli *Client) {
cli.httpClient = c
}
}
// WithTimeout 设置请求超时时间。
func WithTimeout(d time.Duration) Option {
return func(cli *Client) {
cli.httpClient.Timeout = d
}
}
// WithSignEnabled 设置是否启用签名。
// 客户应用需启用签名默认启用钉钉AI表格无需配置签名可设置为 false。
func WithSignEnabled(enabled bool) Option {
return func(cli *Client) {
cli.signEnabled = enabled
}
}
// WithInvoiceOrderPath 覆盖订单开票接口路径。
func WithInvoiceOrderPath(p string) Option {
return func(cli *Client) { cli.invoiceOrderPath = p }
}
// WithQueryInvoiceStatusPath 覆盖开票状态查询接口路径。
func WithQueryInvoiceStatusPath(p string) Option {
return func(cli *Client) { cli.queryInvoiceStatusPath = p }
}
// WithCreatePaymentPath 覆盖创建付款单据接口路径。
func WithCreatePaymentPath(p string) Option {
return func(cli *Client) { cli.createPaymentPath = p }
}
// WithQueryPaymentStatusPath 覆盖支付状态查询接口路径。
func WithQueryPaymentStatusPath(p string) Option {
return func(cli *Client) { cli.queryPaymentStatusPath = p }
}
// NewClient 创建一个新的业财连接接口客户端。
// baseURL 为平台接口地址tenantID 为平台分配的租户唯一标识,
// clientID 为平台分配的应用标识钉钉AI表格固定为 "dd-ai-table"
// clientSecret 为平台分配的密钥。
func NewClient(baseURL, tenantID, clientID, clientSecret string, opts ...Option) *Client {
c := &Client{
baseURL: strings.TrimRight(baseURL, "/"),
tenantID: tenantID,
clientID: clientID,
clientSecret: clientSecret,
httpClient: &http.Client{Timeout: 30 * time.Second},
signEnabled: true,
invoiceOrderPath: DefaultInvoiceOrderPath,
queryInvoiceStatusPath: DefaultQueryInvoiceStatusPath,
createPaymentPath: DefaultCreatePaymentPath,
queryPaymentStatusPath: DefaultQueryPaymentStatusPath,
}
for _, opt := range opts {
opt(c)
}
return c
}
// doRequest 发送 POST 请求并处理通用响应结构,返回 data 字段的原始字节。
func (c *Client) doRequest(ctx context.Context, path string, reqBody interface{}) (json.RawMessage, error) {
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("marshal request: %w", err)}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes))
if err != nil {
return nil, &SDKError{Op: "doRequest", Err: 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)
if c.signEnabled {
timestamp := GenerateTimestamp()
nonce, err := GenerateNonce(16)
if err != nil {
return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("generate nonce: %w", err)}
}
// 签名算法HmacSHA256密钥为 client-secret签名数据为 timestamp + nonce结果 Base64 编码。
signature := HmacSHA256Base64(c.clientSecret, timestamp+nonce)
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 nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("do request: %w", err)}
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("read response: %w", err)}
}
if resp.StatusCode != http.StatusOK {
return nil, &APIError{HTTPStatus: resp.StatusCode, Body: string(respBytes)}
}
var base BaseResponse
if err := json.Unmarshal(respBytes, &base); err != nil {
return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("unmarshal base response: %w", err)}
}
if base.Code != 0 && base.Code != 200 {
return nil, &APIError{Code: base.Code, Msg: base.Msg}
}
return base.Data, nil
}
// InvoiceOrder 提交订单开票请求。
func (c *Client) InvoiceOrder(ctx context.Context, req *InvoiceOrderRequest) (*InvoiceOrderData, error) {
data, err := c.doRequest(ctx, c.invoiceOrderPath, req)
if err != nil {
return nil, err
}
var out InvoiceOrderData
if len(data) > 0 {
if err := json.Unmarshal(data, &out); err != nil {
return nil, &SDKError{Op: "InvoiceOrder", Err: fmt.Errorf("unmarshal data: %w", err)}
}
}
return &out, nil
}
// QueryInvoiceStatus 查询订单开票状态。
func (c *Client) QueryInvoiceStatus(ctx context.Context, req *QueryInvoiceStatusRequest) (*QueryInvoiceStatusData, error) {
data, err := c.doRequest(ctx, c.queryInvoiceStatusPath, req)
if err != nil {
return nil, err
}
var out QueryInvoiceStatusData
if len(data) > 0 {
if err := json.Unmarshal(data, &out); err != nil {
return nil, &SDKError{Op: "QueryInvoiceStatus", Err: fmt.Errorf("unmarshal data: %w", err)}
}
}
return &out, nil
}
// CreatePayment 创建付款单据。
func (c *Client) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*CreatePaymentData, error) {
data, err := c.doRequest(ctx, c.createPaymentPath, req)
if err != nil {
return nil, err
}
var out CreatePaymentData
if len(data) > 0 {
if err := json.Unmarshal(data, &out); err != nil {
return nil, &SDKError{Op: "CreatePayment", Err: fmt.Errorf("unmarshal data: %w", err)}
}
}
return &out, nil
}
// QueryPaymentStatus 查询支付状态。
// 响应数据与支付完成通知的数据结构一致。
func (c *Client) QueryPaymentStatus(ctx context.Context, req *QueryPaymentStatusRequest) (*PaymentInfo, error) {
data, err := c.doRequest(ctx, c.queryPaymentStatusPath, req)
if err != nil {
return nil, err
}
var out PaymentInfo
if len(data) > 0 {
if err := json.Unmarshal(data, &out); err != nil {
return nil, &SDKError{Op: "QueryPaymentStatus", Err: fmt.Errorf("unmarshal data: %w", err)}
}
}
return &out, nil
}
// ParsePaymentNotify 解析支付完成通知请求体。
// 该接口由客户提供、平台调用,用于在支付完成后接收平台回调。
func ParsePaymentNotify(body []byte) (*PaymentInfo, error) {
var info PaymentInfo
if err := json.Unmarshal(body, &info); err != nil {
return nil, &SDKError{Op: "ParsePaymentNotify", Err: fmt.Errorf("unmarshal notify: %w", err)}
}
return &info, nil
}
// ParseNotify 解析通用通知请求体bizType / bizId / data
func ParseNotify(body []byte) (*NotifyRequest, error) {
var req NotifyRequest
if err := json.Unmarshal(body, &req); err != nil {
return nil, &SDKError{Op: "ParseNotify", Err: fmt.Errorf("unmarshal notify: %w", err)}
}
return &req, nil
}