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