From 531d27171d6504861f0185d1564cb032fda55b4a Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Mon, 17 Aug 2026 17:18:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20intelli?= =?UTF-8?q?gence=5Ffinance=5Fv1/client.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- intelligence_finance_v1/client.go | 297 ++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 intelligence_finance_v1/client.go diff --git a/intelligence_finance_v1/client.go b/intelligence_finance_v1/client.go new file mode 100644 index 0000000..1ff3ca5 --- /dev/null +++ b/intelligence_finance_v1/client.go @@ -0,0 +1,297 @@ +package intelligence_finance_v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Client 业财连接SDK客户端 +type Client struct { + baseURL string + tenantID string + clientID string + clientSecret string + httpClient *http.Client + debug bool +} + +// ClientOption 客户端配置选项 +type ClientOption func(*Client) + +// WithHTTPClient 设置自定义HTTP客户端 +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = httpClient + } +} + +// WithDebug 开启调试模式 +func WithDebug(debug bool) ClientOption { + return func(c *Client) { + c.debug = debug + } +} + +// WithTimeout 设置请求超时时间 +func WithTimeout(timeout time.Duration) ClientOption { + return func(c *Client) { + c.httpClient.Timeout = timeout + } +} + +// NewClient 创建新的SDK客户端 +// - baseURL: API基础地址 +// - tenantID: 平台分配的租户唯一标识 +// - clientID: 平台分配的应用标识 +// - clientSecret: 平台分配的客户端密钥 +// - opts: 可选配置项 +func NewClient(baseURL, tenantID, clientID, clientSecret string, opts ...ClientOption) *Client { + c := &Client{ + baseURL: baseURL, + tenantID: tenantID, + clientID: clientID, + clientSecret: clientSecret, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// doRequest 发送HTTP请求,自动添加签名和认证头 +func (c *Client) doRequest(ctx context.Context, path string, request interface{}, response interface{}) error { + bodyBytes, err := json.Marshal(request) + if err != nil { + return NewSDKError(ErrCodeInvalidParam, "请求序列化失败", err) + } + + if c.debug { + fmt.Printf("[DEBUG] Request URL: %s%s\n", c.baseURL, path) + fmt.Printf("[DEBUG] Request Body: %s\n", string(bodyBytes)) + } + + // 生成时间戳和随机数 + timestamp := GenerateTimestamp() + nonce, err := GenerateNonce(16) + if err != nil { + return NewSDKError(ErrCodeSignatureFailed, "生成随机数失败", err) + } + + // 签名数据:timestamp + nonce + signData := timestamp + nonce + signature := HmacSHA256Base64(c.clientSecret, signData) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) + if err != nil { + return NewSDKError(ErrCodeNetworkError, "创建请求失败", 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) + + if c.debug { + fmt.Printf("[DEBUG] Headers: tenant-id=%s, client-id=%s, x-bfl-signature=%s\n", c.tenantID, c.clientID, signature) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return NewSDKError(ErrCodeNetworkError, "请求发送失败", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return NewSDKError(ErrCodeNetworkError, "读取响应失败", err) + } + + if c.debug { + fmt.Printf("[DEBUG] Response Status: %s\n", resp.Status) + fmt.Printf("[DEBUG] Response Body: %s\n", string(respBody)) + } + + // 解析通用响应 + var commonResp CommonResponse + if err := json.Unmarshal(respBody, &commonResp); err != nil { + return NewSDKError(ErrCodeUnknown, "响应解析失败", err) + } + + if commonResp.Code != 0 { + return NewSDKError(commonResp.Code, commonResp.Msg, nil) + } + + // 如果response不为nil,将commonResp.Data反序列化到response + if response != nil { + if err := json.Unmarshal(commonResp.Data, response); err != nil { + return NewSDKError(ErrCodeUnknown, "响应数据解析失败", err) + } + } + + return nil +} + +// ============================================================ +// 接口1:订单开票 +// ============================================================ + +// CreateInvoice 提交订单开票申请 +// 文档:接口1 - 提交订单开票申请 +func (c *Client) CreateInvoice(ctx context.Context, req *CreateInvoiceRequest) (*CreateInvoiceResponse, error) { + path := "/api/v1/invoice/create" + var resp CreateInvoiceResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口2:开票状态查询 +// ============================================================ + +// QueryInvoiceStatus 查询订单开票状态 +// 文档:接口2 - 查询订单开票状态 +func (c *Client) QueryInvoiceStatus(ctx context.Context, req *QueryInvoiceStatusRequest) (*QueryInvoiceStatusResponse, error) { + path := "/api/v1/invoice/query" + var resp QueryInvoiceStatusResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口3:创建付款单据 +// ============================================================ + +// CreatePayment 创建付款单据 +// 文档:接口3 - 创建付款单据 +func (c *Client) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*CreatePaymentResponse, error) { + path := "/api/v1/payment/create" + var resp CreatePaymentResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口4:支付完成通知(平台回调客户) +// 此接口由客户提供,平台调用。客户需实现HTTP Handler处理回调。 +// ============================================================ + +// HandlePaymentNotification 处理支付完成通知 +// 文档:接口4 - 支付完成通知回调处理 +// 该函数用于解析平台发起的支付完成通知请求,返回SUCCESS或FAILED +// 注意:平台回调时,请求体为 NotificationRequest 结构,其中 Data 字段为 PaymentNotification 的JSON字符串 +func HandlePaymentNotification(body []byte) (string, error) { + // 先解析外层通知结构 + var notificationReq NotificationRequest + if err := json.Unmarshal(body, ¬ificationReq); err != nil { + return "FAILED", NewSDKError(ErrCodeInvalidParam, "解析通知请求失败", err) + } + + // 根据bizType处理不同业务 + switch notificationReq.BizType { + case "payment_notification": + // 解析内部支付通知数据 + var paymentNotification PaymentNotification + if err := json.Unmarshal([]byte(notificationReq.Data), &paymentNotification); err != nil { + return "FAILED", NewSDKError(ErrCodeInvalidParam, "解析支付通知数据失败", err) + } + // 业务处理由调用方自行实现,此处仅返回成功 + // 调用方应在此函数基础上扩展业务逻辑 + _ = paymentNotification + default: + // 未知业务类型,记录日志后返回失败 + return "FAILED", NewSDKError(ErrCodeInvalidParam, "未知业务类型: "+notificationReq.BizType, nil) + } + + return "SUCCESS", nil +} + +// ============================================================ +// 接口5:支付状态查询 +// ============================================================ + +// QueryPaymentStatus 查询支付状态 +// 文档:接口5 - 查询支付状态 +func (c *Client) QueryPaymentStatus(ctx context.Context, req *QueryPaymentStatusRequest) (*QueryPaymentStatusResponse, error) { + path := "/api/v1/payment/query" + var resp QueryPaymentStatusResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口6:创建供应商 +// ============================================================ + +// CreateSupplier 创建供应商 +// 文档:接口6 - 创建供应商 +func (c *Client) CreateSupplier(ctx context.Context, req *CreateSupplierRequest) (*CreateSupplierResponse, error) { + path := "/api/v1/supplier/create" + var resp CreateSupplierResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口7:根据名称查询供应商 +// ============================================================ + +// QuerySupplierByName 根据供应商名称查询供应商 +// 文档:接口7 - 根据名称查询供应商 +func (c *Client) QuerySupplierByName(ctx context.Context, req *QuerySupplierByNameRequest) (*QuerySupplierByNameResponse, error) { + path := "/api/v1/supplier/queryByName" + var resp QuerySupplierByNameResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口8:更新供应商 +// ============================================================ + +// UpdateSupplier 更新供应商信息 +// 文档:接口8 - 更新供应商 +func (c *Client) UpdateSupplier(ctx context.Context, req *UpdateSupplierRequest) (*UpdateSupplierResponse, error) { + path := "/api/v1/supplier/update" + var resp UpdateSupplierResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// ============================================================ +// 接口9:根据用户自定义编码查询供应商 +// ============================================================ + +// QuerySupplierByUserDefineCode 根据用户自定义编码查询供应商 +// 文档:接口9 - 根据用户自定义编码查询供应商 +func (c *Client) QuerySupplierByUserDefineCode(ctx context.Context, req *QuerySupplierByUserDefineCodeRequest) (*QuerySupplierByUserDefineCodeResponse, error) { + path := "/api/v1/supplier/queryByCode" + var resp QuerySupplierByUserDefineCodeResponse + if err := c.doRequest(ctx, path, req, &resp); err != nil { + return nil, err + } + return &resp, nil +} \ No newline at end of file