1007 lines
34 KiB
Markdown
1007 lines
34 KiB
Markdown
// File: go.mod
|
|
```
|
|
module intelligence_finance
|
|
|
|
go 1.21
|
|
```
|
|
|
|
// File: crypto.go
|
|
```go
|
|
package intelligence_finance
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GenerateTimestamp generates a Unix timestamp in seconds as a string.
|
|
func GenerateTimestamp() string {
|
|
return fmt.Sprintf("%d", time.Now().Unix())
|
|
}
|
|
|
|
// GenerateTimestampMillis generates a Unix timestamp in milliseconds as a string.
|
|
func GenerateTimestampMillis() string {
|
|
return fmt.Sprintf("%d", time.Now().UnixMilli())
|
|
}
|
|
|
|
// GenerateNonce generates a cryptographically secure random string of the given length.
|
|
func GenerateNonce(length int) (string, error) {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
if err != nil {
|
|
return "", fmt.Errorf("generate nonce: %w", err)
|
|
}
|
|
b[i] = charset[num.Int64()]
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
// HmacSHA256Base64 computes HmacSHA256 of the given data using the secret key,
|
|
// and returns the result as a Base64-encoded string.
|
|
func HmacSHA256Base64(secret, data string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(data))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// BuildSignString sorts parameters by key in dictionary order and concatenates
|
|
// them as key=value&key2=value2, excluding empty values and the sign/signature fields.
|
|
func BuildSignString(params map[string]string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for k, v := range params {
|
|
if v != "" && k != "sign" && k != "signature" {
|
|
keys = append(keys, k)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var parts []string
|
|
for _, k := range keys {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
|
|
// BuildSignStringOrdered concatenates parameters in the specified key order,
|
|
// excluding empty values.
|
|
func BuildSignStringOrdered(params map[string]string, orderedKeys []string) string {
|
|
var parts []string
|
|
for _, k := range orderedKeys {
|
|
if v, ok := params[k]; ok && v != "" {
|
|
parts = append(parts, fmt.Sprintf("%s=%s", k, v))
|
|
}
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
```
|
|
|
|
// File: types.go
|
|
```go
|
|
package intelligence_finance
|
|
|
|
// ============================================================
|
|
// General API Response
|
|
// ============================================================
|
|
|
|
// APIResponse is the general response wrapper returned by the platform APIs.
|
|
type APIResponse struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Interface 1: 订单开票接口 (Invoice Create)
|
|
// ============================================================
|
|
|
|
// InvoiceCreateRequest is the request for creating an invoice order.
|
|
type InvoiceCreateRequest struct {
|
|
CompanyCode string `json:"companyCode,omitempty"`
|
|
OrderID string `json:"orderId"`
|
|
InvoiceType int `json:"invoiceType"`
|
|
Products []InvoiceProduct `json:"products"`
|
|
Remark string `json:"remark,omitempty"`
|
|
Purchaser string `json:"purchaser"`
|
|
TaxNum string `json:"taxnum,omitempty"`
|
|
PurchaserAddr string `json:"purchaserAddress,omitempty"`
|
|
PurchaserTel string `json:"purchaserTel,omitempty"`
|
|
BankName string `json:"bankName,omitempty"`
|
|
BankAccount string `json:"bankAccount,omitempty"`
|
|
Phone string `json:"phone,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
ApplyPerson string `json:"applyPerson,omitempty"`
|
|
Payee string `json:"payee,omitempty"`
|
|
Reviewer string `json:"reviewer,omitempty"`
|
|
InvoiceRemark string `json:"invoiceRemark,omitempty"`
|
|
NaturalPerson string `json:"naturalPerson,omitempty"`
|
|
AdditionInfo string `json:"additionInfo,omitempty"`
|
|
}
|
|
|
|
// InvoiceProduct represents a single product/service line item in an invoice.
|
|
type InvoiceProduct struct {
|
|
ProductName string `json:"productName"`
|
|
RevenueCode string `json:"revenueCode"`
|
|
AmountIncludeTax float64 `json:"amountIncludeTax"`
|
|
Specs string `json:"specs,omitempty"`
|
|
Unit string `json:"unit,omitempty"`
|
|
Quantity float64 `json:"quantity"`
|
|
Discount float64 `json:"discount,omitempty"`
|
|
TaxSign int `json:"taxSign,omitempty"`
|
|
TaxRate float64 `json:"taxRate,omitempty"`
|
|
}
|
|
|
|
// InvoiceCreateResponse is the response for creating an invoice order.
|
|
type InvoiceCreateResponse struct {
|
|
Status int `json:"status"`
|
|
ErrorMsg string `json:"errorMsg,omitempty"`
|
|
DataList []InvoiceData `json:"dataList"`
|
|
}
|
|
|
|
// InvoiceData contains detailed information about a single invoice.
|
|
type InvoiceData struct {
|
|
DeviceCode string `json:"deviceCode"`
|
|
Drawer string `json:"drawer"`
|
|
Email string `json:"email"`
|
|
InvoiceType string `json:"invoiceType"`
|
|
IssueType string `json:"issueType"`
|
|
ListFlag string `json:"listFlag"`
|
|
Mobile string `json:"mobile"`
|
|
OriginalInvCode string `json:"originalInvCode,omitempty"`
|
|
OriginalInvNo string `json:"originalInvNo,omitempty"`
|
|
AdditionInfo string `json:"additionInfo,omitempty"`
|
|
Payee string `json:"payee"`
|
|
PurchaserAddress string `json:"purchaserAddress"`
|
|
PurchaserBankAcct string `json:"purchaserBankAccount"`
|
|
PurchaserBankName string `json:"purchaserBankName"`
|
|
PurchaserName string `json:"purchaserName"`
|
|
PurchaserTaxNo string `json:"purchaserTaxNo"`
|
|
PurchaserTel string `json:"purchaserTel"`
|
|
NaturalPerson string `json:"naturalPerson,omitempty"`
|
|
Remark string `json:"remark"`
|
|
Reviewer string `json:"reviewer"`
|
|
SellerAddress string `json:"sellerAddress,omitempty"`
|
|
SellerBankAcct string `json:"sellerBankAccount"`
|
|
SellerBankName string `json:"sellerBankName"`
|
|
SellerName string `json:"sellerName"`
|
|
CheckCode string `json:"checkCode"`
|
|
CipherText string `json:"cipherText"`
|
|
DrewDate string `json:"drewDate,omitempty"`
|
|
InvoiceCode string `json:"invoiceCode"`
|
|
InvoiceNo string `json:"invoiceNo"`
|
|
InvoiceStatus string `json:"invoiceStatus"`
|
|
LayoutFileURL string `json:"layoutFileUrl,omitempty"`
|
|
PDFURL string `json:"pdfUrl,omitempty"`
|
|
OFDURL string `json:"ofdUrl,omitempty"`
|
|
XMLURL string `json:"xmlUrl,omitempty"`
|
|
TotalExcludeTax string `json:"totalExcludeTax"`
|
|
TotalIncludeTax string `json:"totalIncludeTax"`
|
|
TotalTaxAmount string `json:"totalTaxAmount"`
|
|
LevyingType string `json:"levyingType"`
|
|
Details []InvoiceDetail `json:"details"`
|
|
}
|
|
|
|
// InvoiceDetail represents a product detail line within an invoice.
|
|
type InvoiceDetail struct {
|
|
Amount string `json:"amount,omitempty"`
|
|
Quantity string `json:"quantity,omitempty"`
|
|
DeductionAmount string `json:"deductionAmount,omitempty"`
|
|
TaxAmount string `json:"taxAmount,omitempty"`
|
|
ItemTitle string `json:"itemTitle"`
|
|
TaxCode string `json:"taxCode"`
|
|
ItemType string `json:"itemType"`
|
|
ItemName string `json:"itemName"`
|
|
Specs string `json:"specs,omitempty"`
|
|
TaxFreePolicy string `json:"taxFreePolicy"`
|
|
PreferentialPolicy string `json:"preferentialPolicy"`
|
|
TaxRate string `json:"taxRate"`
|
|
TaxSign string `json:"taxSign"`
|
|
Unit string `json:"unit,omitempty"`
|
|
UnitPrice string `json:"unitPrice,omitempty"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Interface 2: 开票状态查询 (Invoice Status Query)
|
|
// ============================================================
|
|
|
|
// InvoiceStatusQueryRequest is the request for querying invoice status.
|
|
type InvoiceStatusQueryRequest struct {
|
|
OrderID string `json:"orderId"`
|
|
}
|
|
|
|
// InvoiceStatusQueryResponse is the response for querying invoice status.
|
|
type InvoiceStatusQueryResponse struct {
|
|
Status int `json:"status"`
|
|
Message string `json:"message,omitempty"`
|
|
Data []InvoiceData `json:"data"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Interface 3: 创建付款单据 (Create Payment Order)
|
|
// ============================================================
|
|
|
|
// CreatePaymentOrderRequest is the request for creating a payment order.
|
|
type CreatePaymentOrderRequest struct {
|
|
Code string `json:"code"`
|
|
YidaAppType string `json:"yidaAppType,omitempty"`
|
|
EmpAccountUserID string `json:"empAccountUserId,omitempty"`
|
|
Department *Department `json:"department,omitempty"`
|
|
Usage string `json:"usage,omitempty"`
|
|
PaymentUserID string `json:"paymentUserId,omitempty"`
|
|
Customer *Customer `json:"customer,omitempty"`
|
|
PrincipalID string `json:"principalId,omitempty"`
|
|
Remark string `json:"remark,omitempty"`
|
|
Supplier *Supplier `json:"supplier,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Project *Project `json:"project,omitempty"`
|
|
PaymentUserIDListStr string `json:"paymentUserIdListStr,omitempty"`
|
|
NeedPayment bool `json:"needPayment,omitempty"`
|
|
PaymentDetailListJSONStr string `json:"paymentDetailListJsonStr,omitempty"`
|
|
PaymentDetailList []PaymentDetail `json:"paymentDetailList,omitempty"`
|
|
Company *Company `json:"company,omitempty"`
|
|
Amount string `json:"amount,omitempty"`
|
|
RecipientAccountInfo *RecipientAccount `json:"recipientAccountInfo,omitempty"`
|
|
EnterpriseAccount *EnterpriseAccount `json:"enterpriseAccount,omitempty"`
|
|
Category []Category `json:"category,omitempty"`
|
|
UserID string `json:"userId"`
|
|
OccurDate int64 `json:"occurDate,omitempty"`
|
|
Product *Product `json:"product,omitempty"`
|
|
YidaFormUUID string `json:"yidaFormUuid,omitempty"`
|
|
CanEditPaymentInfo bool `json:"canEditPaymentInfo,omitempty"`
|
|
PaymentUserIDList []string `json:"paymentUserIdList,omitempty"`
|
|
YidaProcInsID string `json:"yidaProcInsId,omitempty"`
|
|
SyncPaymentOrder bool `json:"syncPaymentOrder,omitempty"`
|
|
}
|
|
|
|
// Department represents department information.
|
|
type Department struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Customer represents customer information.
|
|
type Customer struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Supplier represents supplier information.
|
|
type Supplier struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Project represents project information.
|
|
type Project struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Category represents income/expense category information.
|
|
type Category struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Product represents product information.
|
|
type Product struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// Company represents enterprise entity information.
|
|
type Company struct {
|
|
Code string `json:"code,omitempty"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// EnterpriseAccount represents enterprise account information.
|
|
type EnterpriseAccount struct {
|
|
EnterpriseAccountCode string `json:"enterpriseAccountCode,omitempty"`
|
|
AccountCategory string `json:"accountCategory"`
|
|
AccountType string `json:"accountType,omitempty"`
|
|
CardNo string `json:"cardNo,omitempty"`
|
|
AccountName string `json:"accountName,omitempty"`
|
|
OfficialNumber string `json:"officialNumber,omitempty"`
|
|
OfficialName string `json:"officialName,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
Province string `json:"province,omitempty"`
|
|
}
|
|
|
|
// RecipientAccount represents recipient account information.
|
|
type RecipientAccount struct {
|
|
AccountCategory string `json:"accountCategory"`
|
|
AccountType string `json:"accountType,omitempty"`
|
|
CardNo string `json:"cardNo,omitempty"`
|
|
AccountName string `json:"accountName,omitempty"`
|
|
}
|
|
|
|
// PaymentDetail represents a payment detail line item.
|
|
type PaymentDetail struct {
|
|
Amount string `json:"amount,omitempty"`
|
|
InvoiceInfo *InvoiceInfo `json:"invoiceInfo,omitempty"`
|
|
ProductCode string `json:"productCode,omitempty"`
|
|
ProjectCode string `json:"projectCode,omitempty"`
|
|
Remark string `json:"remark,omitempty"`
|
|
PrincipalID string `json:"principalId,omitempty"`
|
|
Tax string `json:"tax,omitempty"`
|
|
}
|
|
|
|
// InvoiceInfo represents invoice information within a payment detail.
|
|
type InvoiceInfo struct {
|
|
InvoiceNo string `json:"invoiceNo,omitempty"`
|
|
InvoiceCode string `json:"invoiceCode,omitempty"`
|
|
}
|
|
|
|
// CreatePaymentOrderResponse is the response for creating a payment order.
|
|
type CreatePaymentOrderResponse struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
// ============================================================
|
|
// Interface 4: 支付完成通知 (Payment Notification - Callback)
|
|
// ============================================================
|
|
|
|
// PaymentNotificationData represents the payment notification data sent by the platform.
|
|
type PaymentNotificationData struct {
|
|
Code string `json:"code"`
|
|
InstanceID string `json:"instanceId"`
|
|
CorpID string `json:"corpId"`
|
|
PaymentStatus string `json:"paymentStatus"`
|
|
PaymentTime string `json:"paymentTime"`
|
|
UserID string `json:"userId"`
|
|
FailReason string `json:"failReason,omitempty"`
|
|
PayerAccountInfo *PayerAccountInfo `json:"payerAccountInfo,omitempty"`
|
|
PayeeAccountInfo *PayeeAccountInfo `json:"payeeAccountInfo,omitempty"`
|
|
RelatedRowNumberList []string `json:"relatedRowNumberList,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
Template string `json:"template,omitempty"`
|
|
Amount string `json:"amount,omitempty"`
|
|
}
|
|
|
|
// PayerAccountInfo represents the payer's account information.
|
|
type PayerAccountInfo struct {
|
|
BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,omitempty"`
|
|
EnterpriseAccountCode string `json:"enterpriseAccountCode,omitempty"`
|
|
AccountType string `json:"accountType,omitempty"`
|
|
}
|
|
|
|
// PayeeAccountInfo represents the payee's account information.
|
|
type PayeeAccountInfo struct {
|
|
BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,omitempty"`
|
|
}
|
|
|
|
// BankOpenDTO represents bank account information.
|
|
type BankOpenDTO struct {
|
|
BankCode string `json:"bankCode,omitempty"`
|
|
BankName string `json:"bankName,omitempty"`
|
|
BankBranchCode string `json:"bankBranchCode,omitempty"`
|
|
BankBranchName string `json:"bankBranchName,omitempty"`
|
|
AccountName string `json:"accountName,omitempty"`
|
|
BankCardNo string `json:"bankCardNo,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
// PaymentStatus constants.
|
|
const (
|
|
PaymentStatusSuccess = "SUCCESS"
|
|
PaymentStatusFail = "FAIL"
|
|
PaymentStatusTerminate = "TERMINATE"
|
|
PaymentStatusWaitPay = "WAIT_PAY"
|
|
PaymentStatusPaying = "PAYING"
|
|
PaymentStatusPartSucc = "PART_SUCCESS"
|
|
PaymentStatusRefund = "REFUND"
|
|
)
|
|
|
|
// Source constants for payment notifications.
|
|
const (
|
|
SourceApproval = "approval"
|
|
SourceOpenAPI = "openapi"
|
|
)
|
|
|
|
// AccountType constants.
|
|
const (
|
|
AccountTypeAlipay = "ALIPAY"
|
|
AccountTypeBankCard = "BANKCARD"
|
|
AccountTypeCorpBankCard = "CORP_BANK_CARD"
|
|
AccountTypePersonalBankCard = "PERSONAL_BANK_CARD"
|
|
)
|
|
|
|
// ============================================================
|
|
// Interface 5: 支付状态查询 (Payment Status Query)
|
|
// ============================================================
|
|
|
|
// PaymentStatusQueryRequest is the request for querying payment status.
|
|
type PaymentStatusQueryRequest struct {
|
|
Code string `json:"code"`
|
|
UserID string `json:"userId"`
|
|
}
|
|
|
|
// PaymentStatusQueryResponse is the response for querying payment status,
|
|
// which has the same structure as PaymentNotificationData.
|
|
type PaymentStatusQueryResponse struct {
|
|
PaymentNotificationData
|
|
}
|
|
|
|
// ============================================================
|
|
// Generic Notification (Business Notification)
|
|
// ============================================================
|
|
|
|
// NotificationRequest represents a generic notification request from the platform.
|
|
type NotificationRequest struct {
|
|
BizType string `json:"bizType"`
|
|
BizID string `json:"bizId"`
|
|
Data string `json:"data,omitempty"`
|
|
}
|
|
```
|
|
|
|
// File: errors.go
|
|
```go
|
|
package intelligence_finance
|
|
|
|
import "fmt"
|
|
|
|
// ErrorCode represents a numeric error code from the API.
|
|
type ErrorCode int
|
|
|
|
// Predefined error codes.
|
|
const (
|
|
CodeSuccess ErrorCode = 0
|
|
)
|
|
|
|
// APIError represents an error returned by the platform API.
|
|
type APIError struct {
|
|
Code ErrorCode `json:"code"`
|
|
Message string `json:"msg,omitempty"`
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("api error: code=%d, msg=%s", e.Code, e.Message)
|
|
}
|
|
|
|
// ErrInvalidResponse is returned when the API response cannot be parsed.
|
|
type ErrInvalidResponse struct {
|
|
Message string
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *ErrInvalidResponse) Error() string {
|
|
return fmt.Sprintf("invalid response: %s", e.Message)
|
|
}
|
|
|
|
// ErrNetwork is returned when a network error occurs.
|
|
type ErrNetwork struct {
|
|
Err error
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *ErrNetwork) Error() string {
|
|
return fmt.Sprintf("network error: %v", e.Err)
|
|
}
|
|
|
|
// Unwrap returns the underlying error.
|
|
func (e *ErrNetwork) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// ErrSigning is returned when a signing error occurs.
|
|
type ErrSigning struct {
|
|
Message string
|
|
Err error
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *ErrSigning) Error() string {
|
|
if e.Err != nil {
|
|
return fmt.Sprintf("signing error: %s: %v", e.Message, e.Err)
|
|
}
|
|
return fmt.Sprintf("signing error: %s", e.Message)
|
|
}
|
|
|
|
// Unwrap returns the underlying error.
|
|
func (e *ErrSigning) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
```
|
|
|
|
// File: client.go
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
// File: example_test.go
|
|
```go
|
|
package intelligence_finance_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
"intelligence_finance"
|
|
)
|
|
|
|
// ExampleClient_CreateInvoice demonstrates how to create an invoice.
|
|
func ExampleClient_CreateInvoice() {
|
|
// Create a new client
|
|
client := intelligence_finance.NewClient(
|
|
"https://api.example.com",
|
|
"your-tenant-id",
|
|
"your-client-id",
|
|
"your-client-secret",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
// Build the invoice create request
|
|
req := &intelligence_finance.InvoiceCreateRequest{
|
|
OrderID: "ORD-2024-001",
|
|
InvoiceType: 9, // 数电普票
|
|
Purchaser: "某某科技有限公司",
|
|
TaxNum: "91110108MA01XXXXX",
|
|
Phone: "13800138000",
|
|
Email: "finance@example.com",
|
|
ApplyPerson: "张三",
|
|
Payee: "李四",
|
|
Reviewer: "王五",
|
|
Products: []intelligence_finance.InvoiceProduct{
|
|
{
|
|
ProductName: "云服务器ECS",
|
|
RevenueCode: "1010101010101010101",
|
|
AmountIncludeTax: 1180.00,
|
|
Unit: "台",
|
|
Quantity: 1,
|
|
TaxRate: 0.06,
|
|
TaxSign: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := client.CreateInvoice(ctx, req)
|
|
if err != nil {
|
|
log.Fatalf("CreateInvoice failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Invoice status: %d\n", resp.Status)
|
|
for _, inv := range resp.DataList {
|
|
fmt.Printf("Invoice No: %s, Amount: %s\n", inv.InvoiceNo, inv.TotalIncludeTax)
|
|
}
|
|
}
|
|
|
|
// ExampleClient_QueryInvoiceStatus demonstrates how to query invoice status.
|
|
func ExampleClient_QueryInvoiceStatus() {
|
|
client := intelligence_finance.NewClient(
|
|
"https://api.example.com",
|
|
"your-tenant-id",
|
|
"your-client-id",
|
|
"your-client-secret",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
req := &intelligence_finance.InvoiceStatusQueryRequest{
|
|
OrderID: "ORD-2024-001",
|
|
}
|
|
|
|
resp, err := client.QueryInvoiceStatus(ctx, req)
|
|
if err != nil {
|
|
log.Fatalf("QueryInvoiceStatus failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Status: %d, Message: %s\n", resp.Status, resp.Message)
|
|
for _, inv := range resp.Data {
|
|
fmt.Printf("Invoice No: %s\n", inv.InvoiceNo)
|
|
}
|
|
}
|
|
|
|
// ExampleClient_CreatePaymentOrder demonstrates how to create a payment order.
|
|
func ExampleClient_CreatePaymentOrder() {
|
|
client := intelligence_finance.NewClient(
|
|
"https://api.example.com",
|
|
"your-tenant-id",
|
|
"your-client-id",
|
|
"your-client-secret",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
req := &intelligence_finance.CreatePaymentOrderRequest{
|
|
Code: "PAY-2024-001",
|
|
UserID: "user-001",
|
|
Title: "采购付款-云服务器",
|
|
Amount: "1180.00",
|
|
Department: &intelligence_finance.Department{
|
|
Name: "技术部",
|
|
},
|
|
Supplier: &intelligence_finance.Supplier{
|
|
Name: "某某云服务商",
|
|
},
|
|
PaymentDetailList: []intelligence_finance.PaymentDetail{
|
|
{
|
|
Amount: "1180.00",
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := client.CreatePaymentOrder(ctx, req)
|
|
if err != nil {
|
|
log.Fatalf("CreatePaymentOrder failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Payment order code: %s\n", resp.Code)
|
|
}
|
|
|
|
// ExampleClient_QueryPaymentStatus demonstrates how to query payment status.
|
|
func ExampleClient_QueryPaymentStatus() {
|
|
client := intelligence_finance.NewClient(
|
|
"https://api.example.com",
|
|
"your-tenant-id",
|
|
"your-client-id",
|
|
"your-client-secret",
|
|
)
|
|
|
|
ctx := context.Background()
|
|
|
|
req := &intelligence_finance.PaymentStatusQueryRequest{
|
|
Code: "PAY-2024-001",
|
|
UserID: "user-001",
|
|
}
|
|
|
|
resp, err := client.QueryPaymentStatus(ctx, req)
|
|
if err != nil {
|
|
log.Fatalf("QueryPaymentStatus failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Payment status: %s\n", resp.PaymentStatus)
|
|
fmt.Printf("Payment time: %s\n", resp.PaymentTime)
|
|
}
|
|
|
|
// ExampleParsePaymentNotification demonstrates how to handle a payment notification
|
|
// callback from the platform.
|
|
func ExampleParsePaymentNotification() {
|
|
// This is the body received from the platform's callback
|
|
body := []byte(`{
|
|
"code": "PAY-2024-001",
|
|
"instanceId": "inst-001",
|
|
"corpId": "corp-123",
|
|
"paymentStatus": "SUCCESS",
|
|
"paymentTime": "2024-01-15 10:30:00",
|
|
"userId": "user-001",
|
|
"amount": "1180.00"
|
|
}`)
|
|
|
|
notification, err := intelligence_finance.ParsePaymentNotification(body)
|
|
if err != nil {
|
|
log.Fatalf("ParsePaymentNotification failed: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Payment code: %s, Status: %s\n", notification.Code, notification.PaymentStatus)
|
|
|
|
// In a real HTTP handler, respond with "SUCCESS" or "FAILED"
|
|
// w.Write([]byte("SUCCESS"))
|
|
}
|
|
|
|
// ExampleClient_WithCustomOptions demonstrates how to create a client with custom options.
|
|
func ExampleClient_WithCustomOptions() {
|
|
client := intelligence_finance.NewClient(
|
|
"https://api.example.com",
|
|
"your-tenant-id",
|
|
"your-client-id",
|
|
"your-client-secret",
|
|
intelligence_finance.WithInvoiceCreatePath("/custom/path/invoice/create"),
|
|
intelligence_finance.WithInvoiceStatusQueryPath("/custom/path/invoice/status"),
|
|
intelligence_finance.WithPaymentCreatePath("/custom/path/payment/create"),
|
|
intelligence_finance.WithPaymentStatusQueryPath("/custom/path/payment/status"),
|
|
)
|
|
|
|
_ = client // use the client as needed
|
|
}
|
|
``` |