添加文件: client.go
This commit is contained in:
parent
a59d4f6f7e
commit
c230624b6b
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue