From c230624b6bae5678102e681ee78f1c76f7e0a2d8 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Mon, 17 Aug 2026 11:03:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20client.?= =?UTF-8?q?go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client.go | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 client.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..b4bfe55 --- /dev/null +++ b/client.go @@ -0,0 +1,298 @@ +package intelligence_finance + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// Default API paths - these can be customized after receiving the actual short codes. +const ( + defaultPathInvoiceCreate = "/api/invoice/create" + defaultPathInvoiceStatusQuery = "/api/invoice/status" + defaultPathPaymentCreate = "/api/payment/create" + defaultPathPaymentStatusQuery = "/api/payment/status" +) + +// Client is the intelligence finance API client. +type Client struct { + baseURL string + tenantID string + clientID string + clientSecret string + httpClient *http.Client + + // Paths for each API endpoint (configurable after onboarding). + pathInvoiceCreate string + pathInvoiceStatusQuery string + pathPaymentCreate string + pathPaymentStatusQuery string +} + +// ClientOption is a functional option for configuring the Client. +type ClientOption func(*Client) + +// WithHTTPClient sets a custom HTTP client. +func WithHTTPClient(httpClient *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = httpClient + } +} + +// WithInvoiceCreatePath sets a custom path for the invoice create API. +func WithInvoiceCreatePath(path string) ClientOption { + return func(c *Client) { + c.pathInvoiceCreate = path + } +} + +// WithInvoiceStatusQueryPath sets a custom path for the invoice status query API. +func WithInvoiceStatusQueryPath(path string) ClientOption { + return func(c *Client) { + c.pathInvoiceStatusQuery = path + } +} + +// WithPaymentCreatePath sets a custom path for the payment create API. +func WithPaymentCreatePath(path string) ClientOption { + return func(c *Client) { + c.pathPaymentCreate = path + } +} + +// WithPaymentStatusQueryPath sets a custom path for the payment status query API. +func WithPaymentStatusQueryPath(path string) ClientOption { + return func(c *Client) { + c.pathPaymentStatusQuery = path + } +} + +// NewClient creates a new intelligence finance API client. +// - baseURL: the base URL of the platform API (e.g., "https://api.example.com") +// - tenantID: the tenant ID assigned by the platform +// - clientID: the application client ID assigned by the platform +// - clientSecret: the application client secret assigned by the platform +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{}, + pathInvoiceCreate: defaultPathInvoiceCreate, + pathInvoiceStatusQuery: defaultPathInvoiceStatusQuery, + pathPaymentCreate: defaultPathPaymentCreate, + pathPaymentStatusQuery: defaultPathPaymentStatusQuery, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// doRequest sends an HTTP request with authentication headers and returns the raw response body. +func (c *Client) doRequest(ctx context.Context, path string, request interface{}) ([]byte, error) { + bodyBytes, err := json.Marshal(request) + if err != nil { + return nil, &ErrSigning{Message: "marshal request body", Err: err} + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, &ErrNetwork{Err: fmt.Errorf("create request: %w", err)} + } + + // Set standard headers + req.Header.Set("Content-Type", "application/json") + req.Header.Set("tenant-id", c.tenantID) + req.Header.Set("client-id", c.clientID) + + // Generate and set signature headers + timestamp := GenerateTimestamp() + nonce, err := GenerateNonce(16) + if err != nil { + return nil, &ErrSigning{Message: "generate nonce", Err: err} + } + + 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, &ErrNetwork{Err: fmt.Errorf("send request: %w", err)} + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &ErrNetwork{Err: fmt.Errorf("read response body: %w", err)} + } + + if resp.StatusCode != http.StatusOK { + return nil, &ErrInvalidResponse{ + Message: fmt.Sprintf("unexpected status code %d: %s", resp.StatusCode, string(respBody)), + } + } + + return respBody, nil +} + +// parseResponse parses the API response and extracts the data field. +func parseResponse(body []byte, target interface{}) error { + var apiResp APIResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal response: %v", err)} + } + + if apiResp.Code != 0 { + return &APIError{Code: ErrorCode(apiResp.Code), Message: apiResp.Msg} + } + + if target != nil && apiResp.Data != nil { + // Convert map to JSON then unmarshal into target + dataBytes, err := json.Marshal(apiResp.Data) + if err != nil { + return &ErrInvalidResponse{Message: fmt.Sprintf("marshal data: %v", err)} + } + if err := json.Unmarshal(dataBytes, target); err != nil { + return &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal data: %v", err)} + } + } + + return nil +} + +// parseResponseRaw parses the API response and returns the raw data field. +func parseResponseRaw(body []byte) (map[string]interface{}, error) { + var apiResp APIResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, &ErrInvalidResponse{Message: fmt.Sprintf("unmarshal response: %v", err)} + } + + if apiResp.Code != 0 { + return nil, &APIError{Code: ErrorCode(apiResp.Code), Message: apiResp.Msg} + } + + return apiResp.Data, nil +} + +// ============================================================ +// Interface 1: CreateInvoice - 订单开票接口 +// ============================================================ + +// CreateInvoice submits an invoice creation request for an order. +// It returns the invoice creation response containing status and invoice data. +func (c *Client) CreateInvoice(ctx context.Context, req *InvoiceCreateRequest) (*InvoiceCreateResponse, error) { + body, err := c.doRequest(ctx, c.pathInvoiceCreate, req) + if err != nil { + return nil, err + } + + var result InvoiceCreateResponse + if err := parseResponse(body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// Interface 2: QueryInvoiceStatus - 开票状态查询接口 +// ============================================================ + +// QueryInvoiceStatus queries the invoice status for a given order. +// It returns the invoice status response containing the current status and invoice data. +func (c *Client) QueryInvoiceStatus(ctx context.Context, req *InvoiceStatusQueryRequest) (*InvoiceStatusQueryResponse, error) { + body, err := c.doRequest(ctx, c.pathInvoiceStatusQuery, req) + if err != nil { + return nil, err + } + + var result InvoiceStatusQueryResponse + if err := parseResponse(body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// Interface 3: CreatePaymentOrder - 创建付款单据接口 +// ============================================================ + +// CreatePaymentOrder creates a payment order. +// It returns the response containing the unique payment order code. +func (c *Client) CreatePaymentOrder(ctx context.Context, req *CreatePaymentOrderRequest) (*CreatePaymentOrderResponse, error) { + body, err := c.doRequest(ctx, c.pathPaymentCreate, req) + if err != nil { + return nil, err + } + + var result CreatePaymentOrderResponse + if err := parseResponse(body, &result); err != nil { + return nil, err + } + return &result, nil +} + +// ============================================================ +// Interface 4: Payment Notification Handler (平台回调客户接口) +// ============================================================ + +// ParsePaymentNotification parses a payment notification request body +// sent by the platform. This is used when the platform calls back to +// the customer's system after a payment is completed. +// +// Usage example in an HTTP handler: +// +// func handler(w http.ResponseWriter, r *http.Request) { +// body, _ := io.ReadAll(r.Body) +// notification, err := ParsePaymentNotification(body) +// if err != nil { +// http.Error(w, "FAILED", http.StatusBadRequest) +// return +// } +// // Process the notification... +// w.Write([]byte("SUCCESS")) +// } +func ParsePaymentNotification(body []byte) (*PaymentNotificationData, error) { + var notification PaymentNotificationData + if err := json.Unmarshal(body, ¬ification); err != nil { + return nil, &ErrInvalidResponse{Message: fmt.Sprintf("parse payment notification: %v", err)} + } + return ¬ification, nil +} + +// ParseGenericNotification parses a generic notification request body +// sent by the platform. The generic notification contains bizType, bizId, and data. +func ParseGenericNotification(body []byte) (*NotificationRequest, error) { + var notification NotificationRequest + if err := json.Unmarshal(body, ¬ification); err != nil { + return nil, &ErrInvalidResponse{Message: fmt.Sprintf("parse notification: %v", err)} + } + return ¬ification, nil +} + +// ============================================================ +// Interface 5: QueryPaymentStatus - 支付状态查询接口 +// ============================================================ + +// QueryPaymentStatus queries the payment status for a given payment order. +// The response has the same structure as PaymentNotificationData. +func (c *Client) QueryPaymentStatus(ctx context.Context, req *PaymentStatusQueryRequest) (*PaymentStatusQueryResponse, error) { + body, err := c.doRequest(ctx, c.pathPaymentStatusQuery, req) + if err != nil { + return nil, err + } + + var result PaymentStatusQueryResponse + if err := parseResponse(body, &result); err != nil { + return nil, err + } + return &result, nil +} \ No newline at end of file