intelligence_finance_v1_ga-.../intelligence_finance_v1_ga/client.go

351 lines
12 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 (
BizTypeCreateSupplier = "create_supplier"
BizTypeQuerySupplierByName = "query_supplier_by_name"
BizTypeUpdateSupplier = "update_supplier"
BizTypeQuerySupplierByUserDefine = "query_supplier_by_userDefineCode"
)
// Client 业财连接接口客户端。
type Client struct {
baseURL string
tenantID string
clientID string
clientSecret string
signEnabled bool
httpClient *http.Client
// 各接口路径(可通过 Option 覆盖)
pathSubmitInvoiceApply string
pathQueryInvoiceStatus string
pathCreatePayment string
pathQueryPaymentStatus string
pathCreateSupplier string
pathQuerySupplierByName string
pathUpdateSupplier string
pathQuerySupplierByUserDefineCode string
}
// Option 客户端配置项。
type Option func(*Client)
// WithHTTPClient 自定义 http.Client。
func WithHTTPClient(c *http.Client) Option {
return func(cli *Client) { cli.httpClient = c }
}
// WithSignEnabled 是否启用请求签名钉钉AI表格场景可关闭签名由AI表格自动完成
func WithSignEnabled(enabled bool) Option {
return func(cli *Client) { cli.signEnabled = enabled }
}
// WithPathSubmitInvoiceApply 设置提交订单开票申请接口路径。
func WithPathSubmitInvoiceApply(path string) Option {
return func(cli *Client) { cli.pathSubmitInvoiceApply = path }
}
// WithPathQueryInvoiceStatus 设置开票状态查询接口路径。
func WithPathQueryInvoiceStatus(path string) Option {
return func(cli *Client) { cli.pathQueryInvoiceStatus = path }
}
// WithPathCreatePayment 设置创建付款单据接口路径。
func WithPathCreatePayment(path string) Option {
return func(cli *Client) { cli.pathCreatePayment = path }
}
// WithPathQueryPaymentStatus 设置支付状态查询接口路径。
func WithPathQueryPaymentStatus(path string) Option {
return func(cli *Client) { cli.pathQueryPaymentStatus = path }
}
// WithPathCreateSupplier 设置创建供应商接口路径。
func WithPathCreateSupplier(path string) Option {
return func(cli *Client) { cli.pathCreateSupplier = path }
}
// WithPathQuerySupplierByName 设置根据名称查询供应商接口路径。
func WithPathQuerySupplierByName(path string) Option {
return func(cli *Client) { cli.pathQuerySupplierByName = path }
}
// WithPathUpdateSupplier 设置更新供应商接口路径。
func WithPathUpdateSupplier(path string) Option {
return func(cli *Client) { cli.pathUpdateSupplier = path }
}
// WithPathQuerySupplierByUserDefineCode 设置根据用户自定义编码查询供应商接口路径。
func WithPathQuerySupplierByUserDefineCode(path string) Option {
return func(cli *Client) { cli.pathQuerySupplierByUserDefineCode = path }
}
// 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,
signEnabled: true,
httpClient: &http.Client{Timeout: 30 * time.Second},
pathSubmitInvoiceApply: "/invoice/apply",
pathQueryInvoiceStatus: "/invoice/status",
pathCreatePayment: "/payment/create",
pathQueryPaymentStatus: "/payment/status",
pathCreateSupplier: "/supplier/create",
pathQuerySupplierByName: "/supplier/queryByName",
pathUpdateSupplier: "/supplier/update",
pathQuerySupplierByUserDefineCode: "/supplier/queryByUserDefineCode",
}
for _, opt := range opts {
opt(c)
}
return c
}
// doRequest 发送 POST 请求并解析响应。
func (c *Client) doRequest(ctx context.Context, path string, reqBody, respBody interface{}) error {
body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("new request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("tenant-id", c.tenantID)
httpReq.Header.Set("client-id", c.clientID)
if c.signEnabled {
timestamp := GenerateTimestamp()
nonce, err := GenerateNonce(32)
if err != nil {
return fmt.Errorf("generate nonce: %w", err)
}
sign := HmacSHA256Base64(c.clientSecret, timestamp+nonce)
httpReq.Header.Set("x-bfl-signature-timestamp", timestamp)
httpReq.Header.Set("x-bfl-signature-nonce", nonce)
httpReq.Header.Set("x-bfl-signature", sign)
}
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("do request: %w", err)
}
defer httpResp.Body.Close()
respData, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
if httpResp.StatusCode != http.StatusOK {
return &APIError{StatusCode: httpResp.StatusCode, Body: string(respData)}
}
if err := json.Unmarshal(respData, respBody); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
}
return nil
}
// checkBusinessCode 校验通用响应结构中的业务 code。
func checkBusinessCode(code int, msg string) error {
if code != 0 && code != 200 {
return &BusinessError{Code: code, Msg: msg}
}
return nil
}
// SubmitInvoiceApply 提交订单开票申请接口1
func (c *Client) SubmitInvoiceApply(ctx context.Context, req *InvoiceApplyRequest) (*InvoiceApplyResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *InvoiceApplyResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathSubmitInvoiceApply, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// QueryInvoiceStatus 查询订单开票状态接口2
func (c *Client) QueryInvoiceStatus(ctx context.Context, req *InvoiceStatusQueryRequest) (*InvoiceStatusQueryResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *InvoiceStatusQueryResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathQueryInvoiceStatus, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// CreatePayment 创建付款单据接口3
func (c *Client) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*CreatePaymentResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *CreatePaymentResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathCreatePayment, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// QueryPaymentStatus 查询支付状态接口5响应数据与支付通知一致。
func (c *Client) QueryPaymentStatus(ctx context.Context, req *PaymentStatusQueryRequest) (*PaymentNotifyData, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *PaymentNotifyData `json:"data"`
}
if err := c.doRequest(ctx, c.pathQueryPaymentStatus, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// CreateSupplier 创建供应商接口6
func (c *Client) CreateSupplier(ctx context.Context, req *CreateSupplierRequest) (*CreateSupplierResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *CreateSupplierResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathCreateSupplier, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// QuerySupplierByName 根据名称查询供应商接口7
func (c *Client) QuerySupplierByName(ctx context.Context, req *QuerySupplierByNameRequest) (*QuerySupplierByNameResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *QuerySupplierByNameResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathQuerySupplierByName, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// UpdateSupplier 更新供应商接口8
func (c *Client) UpdateSupplier(ctx context.Context, req *UpdateSupplierRequest) (*UpdateSupplierResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *UpdateSupplierResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathUpdateSupplier, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// QuerySupplierByUserDefineCode 根据用户自定义编码查询供应商接口9
func (c *Client) QuerySupplierByUserDefineCode(ctx context.Context, req *QuerySupplierByUserDefineCodeRequest) (*QuerySupplierByUserDefineCodeResponse, error) {
var envelope struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *QuerySupplierByUserDefineCodeResponse `json:"data"`
}
if err := c.doRequest(ctx, c.pathQuerySupplierByUserDefineCode, req, &envelope); err != nil {
return nil, err
}
if err := checkBusinessCode(envelope.Code, envelope.Msg); err != nil {
return nil, err
}
return envelope.Data, nil
}
// ==================== 通知机制辅助 ====================
// ParseNotification 解析通用通知数据。
func ParseNotification(body []byte) (*Notification, error) {
var n Notification
if err := json.Unmarshal(body, &n); err != nil {
return nil, fmt.Errorf("parse notification: %w", err)
}
return &n, nil
}
// ParsePaymentNotify 解析支付完成通知数据接口4
func ParsePaymentNotify(body []byte) (*PaymentNotifyData, error) {
var n PaymentNotifyData
if err := json.Unmarshal(body, &n); err != nil {
return nil, fmt.Errorf("parse payment notify: %w", err)
}
return &n, nil
}
// VerifyNotificationSignature 校验通知签名。
//
// 平台使用客户提供的验签密钥对原始请求体进行 HmacSHA256 签名并 Base64 编码,
// 通过请求头 x-bfl-signature 传递。verifyKey 为客户在平台配置的验签密钥。
func VerifyNotificationSignature(verifyKey string, rawBody []byte, signature string) (bool, error) {
if signature == "" {
return false, &NotificationError{Reason: "empty signature"}
}
expected := HmacSHA256Base64(verifyKey, string(rawBody))
return hmacEqual(expected, signature), nil
}
// hmacEqual 常量时间比较两个 Base64 签名是否一致。
func hmacEqual(a, b string) bool {
if len(a) != len(b) {
return false
}
var v byte
for i := 0; i < len(a); i++ {
v |= a[i] ^ b[i]
}
return v == 0
}