添加文件: intelligence_finance_v1_ga/client.go
This commit is contained in:
parent
d37265a894
commit
75d380038b
|
|
@ -0,0 +1,237 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue