intelligence_finance_v1-202.../intelligence_finance_v1/valid.md

1230 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// File: intelligence_finance_v1/go.mod
```go
module intelligence_finance_v1
go 1.21
```
// File: intelligence_finance_v1/errors.go
```go
package intelligence_finance_v1
import "fmt"
// ErrorCode 表示 API 返回的错误码
type ErrorCode int
// APIError 表示 API 调用返回的业务错误
type APIError struct {
Code int `json:"code"`
Message string `json:"msg"`
}
// Error 实现 error 接口
func (e *APIError) Error() string {
return fmt.Sprintf("API error: code=%d, msg=%s", e.Code, e.Message)
}
// NewAPIError 创建一个新的 APIError
func NewAPIError(code int, msg string) *APIError {
return &APIError{
Code: code,
Message: msg,
}
}
// 开票状态常量
const (
InvoiceStatusNotInvoiced = 0 // 未开票
InvoiceStatusInvoicing = 1 // 开票中
InvoiceStatusPartiallyFailed = 2 // 部分失败
InvoiceStatusSuccess = 3 // 开票成功
InvoiceStatusFailed = 4 // 开票失败
InvoiceStatusPartiallyUnsent = 5 // 部分未开
InvoiceStatusNoAccount = 6 // 未配置数电账号
InvoiceStatusNoAutoConfig = 7 // 未配置自动开票配置
)
// 发票类型常量
const (
InvoiceTypeSpecial = 1 // 专用发票
InvoiceTypeNormal = 2 // 普通发票
InvoiceTypeNormalElectronic = 3 // 普通发票(电子)
InvoiceTypeSpecialElectronic = 4 // 专用发票(电子)
InvoiceTypeDigitalSpecial = 8 // 数电专票
InvoiceTypeDigitalNormal = 9 // 数电普票
)
// 支付状态常量
const (
PaymentStatusSuccess = "SUCCESS" // 支付成功
PaymentStatusFail = "FAIL" // 支付失败
PaymentStatusTerminate = "TERMINATE" // 支付取消
PaymentStatusWaitPay = "WAIT_PAY" // 待支付
PaymentStatusPaying = "PAYING" // 支付中
PaymentStatusPartSuccess = "PART_SUCCESS" // 部分支付成功
PaymentStatusRefund = "REFUND" // 退款
)
// 账户类型常量
const (
AccountTypeAlipay = "ALIPAY" // 支付宝
AccountTypeBankCard = "BANKCARD" // 银行卡
AccountTypeCorpBankCard = "CORP_BANK_CARD" // 对公银行卡
AccountTypePersonalBankCard = "PERSONAL_BANK_CARD" // 对私银行卡
)
// 通知响应常量
const (
NotificationSuccess = "SUCCESS"
NotificationFailed = "FAILED"
)
// 发票状态常量(发票自身状态)
const (
InvStatusNormal = "1" // 正常
InvStatusRedacted = "2" // 已红冲
InvStatusVoided = "3" // 已作废
)
// 开票类型
const (
IssueTypeNormal = "0" // 正数票
IssueTypeNegative = "1" // 负数票(红冲)
)
// 来源枚举
const (
SourceApproval = "approval" // 审批单
SourceOpenAPI = "openapi" // 开发接口
)
```
// File: intelligence_finance_v1/crypto.go
```go
package intelligence_finance_v1
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"math/big"
"sort"
"strings"
"time"
)
// GenerateTimestamp 生成秒级时间戳字符串
func GenerateTimestamp() string {
return fmt.Sprintf("%d", time.Now().Unix())
}
// GenerateTimestampMillis 生成毫秒级时间戳字符串
func GenerateTimestampMillis() string {
return fmt.Sprintf("%d", time.Now().UnixMilli())
}
// GenerateNonce 生成指定长度的加密安全随机字符串
// length 建议为 16-32 位
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
}
// HmacSHA256Sign 使用 HmacSHA256 算法对数据进行签名,返回 Base64 编码的签名字符串
// 密钥为 client-secret签名数据为 timestamp + nonce
func HmacSHA256Sign(secret string, data string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(data))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// HmacSHA256Verify 验证 HmacSHA256 签名
func HmacSHA256Verify(secret string, data string, signature string) bool {
expected := HmacSHA256Sign(secret, data)
return hmac.Equal([]byte(expected), []byte(signature))
}
// BuildSignString 按字典序排序拼接参数,格式为 key=value&key2=value2
// 排除空值、sign 和 signature 字段
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 按固定顺序拼接参数
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: intelligence_finance_v1/types.go
```go
package intelligence_finance_v1
// ============================================================================
// 通用结构
// ============================================================================
// commonResponse 通用 API 响应结构(内部使用)
type commonResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
// ============================================================================
// 订单开票(接口 1
// ============================================================================
// InvoiceRequest 订单开票请求参数
type InvoiceRequest struct {
// CompanyCode 开票的企业主体编码,不传则默认主体开票
CompanyCode string `json:"companyCode,omitempty"`
// OrderID 订单唯一标识(需保证在贵方系统内唯一)
OrderID string `json:"orderId"`
// InvoiceType 发票类型枚举1-专用发票2-普通发票3-普通发票(电子)4-专用发票(电子)8-数电专票9-数电普票
InvoiceType int `json:"invoiceType"`
// Products 货物/服务明细列表,至少一项
Products []ProductItem `json:"products"`
// Remark 订单备注(非发票备注)
Remark string `json:"remark,omitempty"`
// Purchaser 购方企业名称
Purchaser string `json:"purchaser"`
// Taxnum 购方纳税人识别号
Taxnum string `json:"taxnum,omitempty"`
// PurchaserAddress 购方地址
PurchaserAddress string `json:"purchaserAddress,omitempty"`
// PurchaserTel 购方电话
PurchaserTel string `json:"purchaserTel,omitempty"`
// BankName 购方开户行名称
BankName string `json:"bankName,omitempty"`
// BankAccount 购方银行账号
BankAccount string `json:"bankAccount,omitempty"`
// Phone 收票人手机号(用于接收电票短信)
Phone string `json:"phone,omitempty"`
// Email 收票人邮箱(用于接收电票邮件)
Email string `json:"email,omitempty"`
// ApplyPerson 开票申请人名称
ApplyPerson string `json:"applyPerson,omitempty"`
// Payee 收款人(发票票面)
Payee string `json:"payee,omitempty"`
// Reviewer 复核人(发票票面)
Reviewer string `json:"reviewer,omitempty"`
// InvoiceRemark 发票备注栏内容
InvoiceRemark string `json:"invoiceRemark,omitempty"`
// NaturalPerson 购买方自然人标识Y-是N-否默认N数电票可选传
NaturalPerson string `json:"naturalPerson,omitempty"`
// AdditionInfo 附加信息JSON数组字符串
AdditionInfo string `json:"additionInfo,omitempty"`
}
// ProductItem 货物/服务明细项
type ProductItem struct {
// ProductName 货物或服务名称
ProductName string `json:"productName"`
// RevenueCode 19位税收分类编码
RevenueCode string `json:"revenueCode"`
// AmountIncludeTax 单条明细含税总金额(单位:元)
AmountIncludeTax string `json:"amountIncludeTax"`
// Specs 规格型号
Specs string `json:"specs,omitempty"`
// Unit 计量单位(如:台、个、次)
Unit string `json:"unit,omitempty"`
// Quantity 数量
Quantity string `json:"quantity"`
// Discount 折扣金额无折扣传0
Discount string `json:"discount,omitempty"`
// TaxSign 是否含税0-不含税1-含税默认建议传1
TaxSign int `json:"taxSign,omitempty"`
// TaxRate 税率小数形式如0.13表示13%
TaxRate string `json:"taxRate,omitempty"`
}
// InvoiceResponse 订单开票响应数据
type InvoiceResponse struct {
// Status 开票状态0-未开票1-开票中2-部分失败3-开票成功4-开票失败5-部分未开6-未配置数电账号7-未配置自动开票配置
Status int `json:"status"`
// ErrorMsg 错误信息(开票失败时返回原因)
ErrorMsg string `json:"errorMsg,omitempty"`
// DataList 发票数据列表(一张订单可能对应多张发票)
DataList []InvoiceData `json:"dataList"`
}
// InvoiceData 发票数据
type InvoiceData struct {
// DeviceCode 税控设备号
DeviceCode string `json:"deviceCode"`
// Drawer 开票人
Drawer string `json:"drawer"`
// Email 邮箱(购方邮箱)
Email string `json:"email"`
// InvoiceType 发票类型1-专用发票2-普通发票3-普通发票电子4-专用发票电子8-数电专票9-数电普票
InvoiceType string `json:"invoiceType"`
// IssueType 开票类型0-正数票1-负数票(红冲)
IssueType string `json:"issueType"`
// ListFlag 清单标识0-无清单1-有清单
ListFlag string `json:"listFlag"`
// Mobile 手机号(购方手机)
Mobile string `json:"mobile"`
// OriginalInvCode 红冲时对应的原蓝票代码
OriginalInvCode string `json:"originalInvCode,omitempty"`
// OriginalInvNo 红冲时对应的原蓝票号码
OriginalInvNo string `json:"originalInvNo,omitempty"`
// AdditionInfo 数电发票备注栏的附加信息部分
AdditionInfo string `json:"additionInfo,omitempty"`
// Payee 收款人
Payee string `json:"payee"`
// PurchaserAddress 购方地址
PurchaserAddress string `json:"purchaserAddress"`
// PurchaserBankAccount 购方银行账号
PurchaserBankAccount string `json:"purchaserBankAccount"`
// PurchaserBankName 购方开户行
PurchaserBankName string `json:"purchaserBankName"`
// PurchaserName 购方名称
PurchaserName string `json:"purchaserName"`
// PurchaserTaxNo 购方税号
PurchaserTaxNo string `json:"purchaserTaxNo"`
// PurchaserTel 购方电话
PurchaserTel string `json:"purchaserTel"`
// NaturalPerson 购买方自然人标识Y-是N-否
NaturalPerson string `json:"naturalPerson,omitempty"`
// Remark 备注
Remark string `json:"remark"`
// Reviewer 复核人
Reviewer string `json:"reviewer"`
// SellerAddress 销方地址
SellerAddress string `json:"sellerAddress,omitempty"`
// SellerBankAccount 销方开户账号
SellerBankAccount string `json:"sellerBankAccount"`
// SellerBankName 销方开户行
SellerBankName string `json:"sellerBankName"`
// SellerName 销方名称
SellerName string `json:"sellerName"`
// CheckCode 校验码
CheckCode string `json:"checkCode"`
// CipherText 密码区
CipherText string `json:"cipherText"`
// DrewDate 开票日期格式yyyy-MM-dd HH:mm:ss
DrewDate string `json:"drewDate,omitempty"`
// InvoiceCode 发票代码
InvoiceCode string `json:"invoiceCode"`
// InvoiceNo 发票号码
InvoiceNo string `json:"invoiceNo"`
// InvoiceStatus 发票状态1-正常2-已红冲3-已作废
InvoiceStatus string `json:"invoiceStatus"`
// LayoutFileURL 电子发票地址PDF/OFD
LayoutFileURL string `json:"layoutFileUrl,omitempty"`
// PDFURL 电子发票PDF地址
PDFURL string `json:"pdfUrl,omitempty"`
// OFDURL 电子发票OFD地址
OFDURL string `json:"ofdUrl,omitempty"`
// XMLURL 电子发票XML数据地址
XMLURL string `json:"xmlUrl,omitempty"`
// TotalExcludeTax 合计金额(不含税)
TotalExcludeTax string `json:"totalExcludeTax"`
// TotalIncludeTax 合计金额(含税)
TotalIncludeTax string `json:"totalIncludeTax"`
// TotalTaxAmount 合计税额
TotalTaxAmount string `json:"totalTaxAmount"`
// LevyingType 征税方式
LevyingType string `json:"levyingType"`
// Details 商品明细列表
Details []InvoiceDetail `json:"details"`
}
// InvoiceDetail 发票商品明细
type InvoiceDetail struct {
// Amount 金额
Amount string `json:"amount,omitempty"`
// Quantity 数量
Quantity string `json:"quantity,omitempty"`
// DeductionAmount 扣除金额
DeductionAmount string `json:"deductionAmount,omitempty"`
// TaxAmount 税额
TaxAmount string `json:"taxAmount,omitempty"`
// ItemTitle 商品合并显示名称
ItemTitle string `json:"itemTitle"`
// TaxCode 税收分类编码19位
TaxCode string `json:"taxCode"`
// ItemType 商品行性质0-正常行1-折扣行2-被折扣行
ItemType string `json:"itemType"`
// ItemName 商品简称
ItemName string `json:"itemName"`
// Specs 商品规格型号
Specs string `json:"specs,omitempty"`
// TaxFreePolicy 免税政策1-免税2-不征税3-普通零税率
TaxFreePolicy string `json:"taxFreePolicy"`
// PreferentialPolicy 优惠政策类型
PreferentialPolicy string `json:"preferentialPolicy"`
// TaxRate 税率小数形式如0.13
TaxRate string `json:"taxRate"`
// TaxSign 是否含税0-否1-是
TaxSign string `json:"taxSign"`
// Unit 计量单位(如:台、个、次)
Unit string `json:"unit,omitempty"`
// UnitPrice 单价
UnitPrice string `json:"unitPrice,omitempty"`
}
// ============================================================================
// 开票状态查询(接口 2
// ============================================================================
// InvoiceStatusQueryRequest 开票状态查询请求参数
type InvoiceStatusQueryRequest struct {
// OrderID 订单唯一标识(需保证在贵方系统内唯一)
OrderID string `json:"orderId"`
}
// InvoiceStatusQueryResponse 开票状态查询响应数据
type InvoiceStatusQueryResponse struct {
// Status 开票状态0-未开票3-成功4-失败6-未配置数电账号7-未配置自动开票
Status int `json:"status"`
// Message 状态描述(如"开票成功"
Message string `json:"message"`
// Data 发票详细列表
Data []InvoiceData `json:"data"`
}
// ============================================================================
// 创建付款单据(接口 3
// ============================================================================
// PaymentDocumentRequest 创建付款单据请求参数
type PaymentDocumentRequest struct {
// Code 单据编码
Code string `json:"code"`
// YidaAppType 宜搭应用类型
YidaAppType string `json:"yidaAppType,omitempty"`
// EmpAccountUserID 员工账号用户ID
EmpAccountUserID string `json:"empAccountUserId,omitempty"`
// Department 部门信息
Department *Department `json:"department,omitempty"`
// Usage 用途
Usage string `json:"usage,omitempty"`
// PaymentUserID 付款人用户ID
PaymentUserID string `json:"paymentUserId,omitempty"`
// Customer 客户信息
Customer *Customer `json:"customer,omitempty"`
// PrincipalID 负责人ID
PrincipalID string `json:"principalId,omitempty"`
// Remark 备注
Remark string `json:"remark,omitempty"`
// Supplier 供应商信息
Supplier *Supplier `json:"supplier,omitempty"`
// Title 标题
Title string `json:"title,omitempty"`
// Project 项目信息
Project *Project `json:"project,omitempty"`
// PaymentUserIDListStr 付款人用户ID列表字符串
PaymentUserIDListStr string `json:"paymentUserIdListStr,omitempty"`
// NeedPayment 是否需要付款
NeedPayment *bool `json:"needPayment,omitempty"`
// PaymentDetailListJSONStr 付款明细列表JSON字符串
PaymentDetailListJSONStr string `json:"paymentDetailListJsonStr,omitempty"`
// PaymentDetailList 付款明细列表
PaymentDetailList []PaymentDetail `json:"paymentDetailList,omitempty"`
// Company 企业主体信息
Company *Company `json:"company,omitempty"`
// Amount 金额
Amount string `json:"amount,omitempty"`
// RecipientAccountInfo 收款账户信息
RecipientAccountInfo *RecipientAccount `json:"recipientAccountInfo,omitempty"`
// EnterpriseAccount 企业账号信息
EnterpriseAccount *EnterpriseAccount `json:"enterpriseAccount,omitempty"`
// Category 收支类别
Category []Category `json:"category,omitempty"`
// UserID 用户ID
UserID string `json:"userId"`
// OccurDate 发生日期(时间戳,毫秒)
OccurDate *int64 `json:"occurDate,omitempty"`
// Product 商品信息
Product *Product `json:"product,omitempty"`
// YidaFormUUID 宜搭表单UUID
YidaFormUUID string `json:"yidaFormUuid,omitempty"`
// CanEditPaymentInfo 是否可编辑付款信息
CanEditPaymentInfo *bool `json:"canEditPaymentInfo,omitempty"`
// PaymentUserIDList 付款人用户ID列表
PaymentUserIDList []string `json:"paymentUserIdList,omitempty"`
// YidaProcInsID 宜搭流程实例ID
YidaProcInsID string `json:"yidaProcInsId,omitempty"`
// SyncPaymentOrder 是否同步付款单
SyncPaymentOrder *bool `json:"syncPaymentOrder,omitempty"`
}
// Department 部门信息
type Department struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Customer 客户信息
type Customer struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Supplier 供应商信息
type Supplier struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Project 项目信息
type Project struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Category 收支类别信息
type Category struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Product 商品信息
type Product struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// Company 企业主体信息
type Company struct {
Code string `json:"code,omitempty"`
Name string `json:"name"`
}
// EnterpriseAccount 企业账号信息
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 收款账户信息
type RecipientAccount struct {
AccountCategory string `json:"accountCategory"`
AccountType string `json:"accountType,omitempty"`
CardNo string `json:"cardNo,omitempty"`
AccountName string `json:"accountName,omitempty"`
}
// PaymentDetail 付款明细
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 付款明细发票信息
type InvoiceInfo struct {
InvoiceNo string `json:"invoiceNo,omitempty"`
InvoiceCode string `json:"invoiceCode,omitempty"`
}
// PaymentDocumentResponse 创建付款单据响应数据
type PaymentDocumentResponse struct {
// Code 单据唯一标识
Code string `json:"code"`
}
// ============================================================================
// 支付完成通知(接口 4- 平台回调客户
// ============================================================================
// PaymentNotification 支付完成通知数据
type PaymentNotification struct {
// Code 单据编码
Code string `json:"code"`
// InstanceID 实例ID
InstanceID string `json:"instanceId"`
// CorpID 企业ID
CorpID string `json:"corpId"`
// PaymentStatus 支付状态
PaymentStatus string `json:"paymentStatus"`
// PaymentTime 支付时间
PaymentTime string `json:"paymentTime"`
// UserID 用户ID
UserID string `json:"userId"`
// FailReason 失败原因
FailReason string `json:"failReason,omitempty"`
// PayerAccountInfo 付款账户信息
PayerAccountInfo *PayerAccountInfo `json:"payerAccountInfo,omitempty"`
// PayeeAccountInfo 收款账户信息
PayeeAccountInfo *PayeeAccountInfo `json:"payeeAccountInfo,omitempty"`
// RelatedRowNumberList 关联行号列表
RelatedRowNumberList []string `json:"relatedRowNumberList,omitempty"`
// Source 来源
Source string `json:"source,omitempty"`
// Template 模板
Template string `json:"template,omitempty"`
// Amount 金额
Amount string `json:"amount,omitempty"`
}
// PayeeAccountInfo 收款账户信息
type PayeeAccountInfo struct {
BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,omitempty"`
}
// PayerAccountInfo 付款账户信息
type PayerAccountInfo struct {
BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,omitempty"`
EnterpriseAccountCode string `json:"enterpriseAccountCode,omitempty"`
AccountType string `json:"accountType,omitempty"`
}
// BankOpenDTO 银行信息
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"`
}
// ============================================================================
// 支付状态查询(接口 5
// ============================================================================
// PaymentStatusQueryRequest 支付状态查询请求参数
type PaymentStatusQueryRequest struct {
// Code 单据编码
Code string `json:"code"`
// UserID 用户ID
UserID string `json:"userId"`
}
// PaymentStatusQueryResponse 支付状态查询响应(与支付通知数据结构一致)
type PaymentStatusQueryResponse = PaymentNotification
// ============================================================================
// 通用通知(回调)
// ============================================================================
// GeneralNotification 通用通知数据
type GeneralNotification struct {
// BizType 业务类型
BizType string `json:"bizType"`
// BizID 业务ID
BizID string `json:"bizId"`
// Data 通知数据
Data string `json:"data,omitempty"`
}
```
// File: intelligence_finance_v1/client.go
```go
package intelligence_finance_v1
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
// DefaultTimeout 默认请求超时时间
DefaultTimeout = 30 * time.Second
// DefaultNonceLength 默认随机数长度
DefaultNonceLength = 16
// HeaderTenantID 租户ID请求头
HeaderTenantID = "tenant-id"
// HeaderClientID 应用标识请求头
HeaderClientID = "client-id"
// HeaderSignatureTimestamp 签名时间戳请求头
HeaderSignatureTimestamp = "x-bfl-signature-timestamp"
// HeaderSignatureNonce 签名随机数请求头
HeaderSignatureNonce = "x-bfl-signature-nonce"
// HeaderSignature 签名信息请求头
HeaderSignature = "x-bfl-signature"
// ClientIDDingTalk 钉钉AI表格的固定 client-id
ClientIDDingTalk = "dd-ai-table"
)
// Client 业财连接 SDK 客户端
// 用于调用平台提供的接口(订单开票、开票状态查询、创建付款单据、支付状态查询)
// 以及处理平台回调通知
type Client struct {
// BaseURL 平台接口基础地址
BaseURL string
// TenantID 平台分配的租户唯一标识
TenantID string
// ClientID 平台分配的应用标识
ClientID string
// ClientSecret 平台分配的密钥,用于签名
ClientSecret string
// HTTPClient HTTP 客户端
HTTPClient *http.Client
// NonceLength 随机数长度
NonceLength int
}
// ClientOption 客户端配置选项
type ClientOption func(*Client)
// WithHTTPClient 设置自定义 HTTP 客户端
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(c *Client) {
c.HTTPClient = httpClient
}
}
// WithNonceLength 设置随机数长度
func WithNonceLength(length int) ClientOption {
return func(c *Client) {
c.NonceLength = length
}
}
// NewClient 创建一个新的业财连接客户端
// baseURL: 平台接口基础地址(如 "https://api.example.com"
// tenantID: 平台分配的租户唯一标识
// clientID: 平台分配的应用标识钉钉AI表格固定为 "dd-ai-table"
// clientSecret: 平台分配的密钥
func NewClient(baseURL, tenantID, clientID, clientSecret string, opts ...ClientOption) *Client {
c := &Client{
BaseURL: baseURL,
TenantID: tenantID,
ClientID: clientID,
ClientSecret: clientSecret,
HTTPClient: &http.Client{
Timeout: DefaultTimeout,
},
NonceLength: DefaultNonceLength,
}
for _, opt := range opts {
opt(c)
}
return c
}
// NewDingTalkClient 创建一个钉钉AI表格专用的客户端
// baseURL: 平台接口基础地址
// tenantID: 平台分配的租户唯一标识
// clientSecret: 平台分配的密钥在钉钉AI表格中配置为 APPSecret
// 注意钉钉AI表格的 client-id 固定为 "dd-ai-table"签名由AI表格自动完成
func NewDingTalkClient(baseURL, tenantID, clientSecret string, opts ...ClientOption) *Client {
return NewClient(baseURL, tenantID, ClientIDDingTalk, clientSecret, opts...)
}
// ============================================================================
// 签名相关方法
// ============================================================================
// buildSignature 构建请求签名
// 签名算法HmacSHA256(client-secret, timestamp + nonce),结果 Base64 编码
func (c *Client) buildSignature(timestamp, nonce string) string {
data := timestamp + nonce
return HmacSHA256Sign(c.ClientSecret, data)
}
// setAuthHeaders 设置认证请求头
func (c *Client) setAuthHeaders(req *http.Request, timestamp, nonce string) {
req.Header.Set(HeaderTenantID, c.TenantID)
req.Header.Set(HeaderClientID, c.ClientID)
req.Header.Set(HeaderSignatureTimestamp, timestamp)
req.Header.Set(HeaderSignatureNonce, nonce)
req.Header.Set(HeaderSignature, c.buildSignature(timestamp, nonce))
req.Header.Set("Content-Type", "application/json")
}
// ============================================================================
// 内部 HTTP 请求方法
// ============================================================================
// doRequest 执行带签名的 POST 请求
func (c *Client) doRequest(ctx context.Context, path string, requestBody interface{}) (*commonResponse, error) {
bodyBytes, err := json.Marshal(requestBody)
if err != nil {
return nil, fmt.Errorf("marshal request body: %w", err)
}
url := c.BaseURL + path
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
timestamp := GenerateTimestamp()
nonce, err := GenerateNonce(c.NonceLength)
if err != nil {
return nil, fmt.Errorf("generate nonce: %w", err)
}
c.setAuthHeaders(req, timestamp, nonce)
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected HTTP status: %d, body: %s", resp.StatusCode, string(respBody))
}
var commonResp commonResponse
if err := json.Unmarshal(respBody, &commonResp); err != nil {
return nil, fmt.Errorf("unmarshal response: %w", err)
}
if commonResp.Code != 0 {
return nil, NewAPIError(commonResp.Code, commonResp.Msg)
}
return &commonResp, nil
}
// ============================================================================
// 接口 1订单开票
// ============================================================================
// CreateInvoice 提交订单开票请求
// path: 接口路径(接口短码,由平台对接时分配)
// req: 开票请求参数
// 返回开票响应,包含开票状态和发票数据列表
func (c *Client) CreateInvoice(ctx context.Context, path string, req *InvoiceRequest) (*InvoiceResponse, error) {
commonResp, err := c.doRequest(ctx, path, req)
if err != nil {
return nil, err
}
// 将 data 字段重新序列化后反序列化为 InvoiceResponse
dataBytes, err := json.Marshal(commonResp.Data)
if err != nil {
return nil, fmt.Errorf("marshal response data: %w", err)
}
var invoiceResp InvoiceResponse
if err := json.Unmarshal(dataBytes, &invoiceResp); err != nil {
return nil, fmt.Errorf("unmarshal invoice response: %w", err)
}
return &invoiceResp, nil
}
// ============================================================================
// 接口 2开票状态查询
// ============================================================================
// QueryInvoiceStatus 查询订单开票状态
// path: 接口路径(接口短码,由平台对接时分配)
// req: 查询请求参数
// 返回开票状态查询响应
func (c *Client) QueryInvoiceStatus(ctx context.Context, path string, req *InvoiceStatusQueryRequest) (*InvoiceStatusQueryResponse, error) {
commonResp, err := c.doRequest(ctx, path, req)
if err != nil {
return nil, err
}
dataBytes, err := json.Marshal(commonResp.Data)
if err != nil {
return nil, fmt.Errorf("marshal response data: %w", err)
}
var statusResp InvoiceStatusQueryResponse
if err := json.Unmarshal(dataBytes, &statusResp); err != nil {
return nil, fmt.Errorf("unmarshal invoice status response: %w", err)
}
return &statusResp, nil
}
// ============================================================================
// 接口 3创建付款单据
// ============================================================================
// CreatePaymentDocument 创建付款单据
// path: 接口路径(接口短码,由平台对接时分配)
// req: 付款单据请求参数
// 返回创建结果,包含单据唯一标识
func (c *Client) CreatePaymentDocument(ctx context.Context, path string, req *PaymentDocumentRequest) (*PaymentDocumentResponse, error) {
commonResp, err := c.doRequest(ctx, path, req)
if err != nil {
return nil, err
}
dataBytes, err := json.Marshal(commonResp.Data)
if err != nil {
return nil, fmt.Errorf("marshal response data: %w", err)
}
var paymentResp PaymentDocumentResponse
if err := json.Unmarshal(dataBytes, &paymentResp); err != nil {
return nil, fmt.Errorf("unmarshal payment document response: %w", err)
}
return &paymentResp, nil
}
// ============================================================================
// 接口 5支付状态查询
// ============================================================================
// QueryPaymentStatus 查询支付状态
// path: 接口路径(接口短码,由平台对接时分配)
// req: 查询请求参数
// 返回支付状态信息,结构与支付通知一致
func (c *Client) QueryPaymentStatus(ctx context.Context, path string, req *PaymentStatusQueryRequest) (*PaymentStatusQueryResponse, error) {
commonResp, err := c.doRequest(ctx, path, req)
if err != nil {
return nil, err
}
dataBytes, err := json.Marshal(commonResp.Data)
if err != nil {
return nil, fmt.Errorf("marshal response data: %w", err)
}
var paymentStatusResp PaymentStatusQueryResponse
if err := json.Unmarshal(dataBytes, &paymentStatusResp); err != nil {
return nil, fmt.Errorf("unmarshal payment status response: %w", err)
}
return &paymentStatusResp, nil
}
// ============================================================================
// 回调通知处理(接口 4支付完成通知
// ============================================================================
// ParsePaymentNotification 从 HTTP 请求中解析支付完成通知
// 该方法用于客户接收平台回调时使用
func ParsePaymentNotification(r *http.Request) (*PaymentNotification, error) {
if r.Method != http.MethodPost {
return nil, fmt.Errorf("invalid HTTP method: %s, expected POST", r.Method)
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("read notification body: %w", err)
}
defer r.Body.Close()
var notification PaymentNotification
if err := json.Unmarshal(body, &notification); err != nil {
return nil, fmt.Errorf("unmarshal payment notification: %w", err)
}
return &notification, nil
}
// ParseGeneralNotification 从 HTTP 请求中解析通用通知
func ParseGeneralNotification(r *http.Request) (*GeneralNotification, error) {
if r.Method != http.MethodPost {
return nil, fmt.Errorf("invalid HTTP method: %s, expected POST", r.Method)
}
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, fmt.Errorf("read notification body: %w", err)
}
defer r.Body.Close()
var notification GeneralNotification
if err := json.Unmarshal(body, &notification); err != nil {
return nil, fmt.Errorf("unmarshal general notification: %w", err)
}
return &notification, nil
}
// VerifyNotificationSignature 验证回调通知签名
// payload: 原始请求体
// signature: 请求中的签名
// secret: 客户在平台配置的验签密钥
// 使用 HmacSHA256 算法验证
func VerifyNotificationSignature(payload []byte, signature, secret string) bool {
return HmacSHA256Verify(secret, string(payload), signature)
}
// WriteNotificationResponse 写入通知响应
// success: true 返回 "SUCCESS"false 返回 "FAILED"
func WriteNotificationResponse(w http.ResponseWriter, success bool) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if success {
w.WriteHeader(http.StatusOK)
w.Write([]byte(NotificationSuccess))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(NotificationFailed))
}
}
```
// File: intelligence_finance_v1/example_test.go
```go
package intelligence_finance_v1_test
import (
"context"
"fmt"
"log"
"net/http"
"intelligence_finance_v1"
)
// ExampleClient_CreateInvoice 演示如何提交订单开票请求
func ExampleClient_CreateInvoice() {
// 创建客户端
client := intelligence_finance_v1.NewClient(
"https://api.example.com", // 平台基础地址
"your-tenant-id", // 租户ID
"your-client-id", // 应用标识
"your-client-secret", // 密钥
)
// 构建开票请求
req := &intelligence_finance_v1.InvoiceRequest{
OrderID: "ORDER-2024-001",
InvoiceType: intelligence_finance_v1.InvoiceTypeDigitalNormal, // 数电普票
Purchaser: "测试购方企业",
Taxnum: "91110000000000000X",
Products: []intelligence_finance_v1.ProductItem{
{
ProductName: "测试商品",
RevenueCode: "1090101000000000000",
AmountIncludeTax: "1130.00",
Quantity: "1",
Unit: "个",
TaxSign: 1,
TaxRate: "0.13",
Discount: "0",
},
},
Email: "test@example.com",
Phone: "13800138000",
}
// 调用接口
ctx := context.Background()
resp, err := client.CreateInvoice(ctx, "/api/invoice/create", req)
if err != nil {
log.Fatalf("创建发票失败: %v", err)
}
fmt.Printf("开票状态: %d\n", resp.Status)
if resp.Status == intelligence_finance_v1.InvoiceStatusSuccess {
for _, data := range resp.DataList {
fmt.Printf("发票号码: %s, 发票代码: %s\n", data.InvoiceNo, data.InvoiceCode)
}
}
}
// ExampleClient_QueryInvoiceStatus 演示如何查询开票状态
func ExampleClient_QueryInvoiceStatus() {
client := intelligence_finance_v1.NewClient(
"https://api.example.com",
"your-tenant-id",
"your-client-id",
"your-client-secret",
)
req := &intelligence_finance_v1.InvoiceStatusQueryRequest{
OrderID: "ORDER-2024-001",
}
ctx := context.Background()
resp, err := client.QueryInvoiceStatus(ctx, "/api/invoice/status", req)
if err != nil {
log.Fatalf("查询开票状态失败: %v", err)
}
fmt.Printf("状态: %d, 描述: %s\n", resp.Status, resp.Message)
for _, data := range resp.Data {
fmt.Printf("发票号码: %s, 发票状态: %s\n", data.InvoiceNo, data.InvoiceStatus)
}
}
// ExampleClient_CreatePaymentDocument 演示如何创建付款单据
func ExampleClient_CreatePaymentDocument() {
client := intelligence_finance_v1.NewClient(
"https://api.example.com",
"your-tenant-id",
"your-client-id",
"your-client-secret",
)
needPayment := true
req := &intelligence_finance_v1.PaymentDocumentRequest{
Code: "PAY-2024-001",
UserID: "user123",
Title: "测试付款单",
Amount: "10000.00",
Supplier: &intelligence_finance_v1.Supplier{
Name: "测试供应商",
},
Company: &intelligence_finance_v1.Company{
Name: "测试企业",
},
Department: &intelligence_finance_v1.Department{
Name: "财务部",
},
NeedPayment: &needPayment,
PaymentDetailList: []intelligence_finance_v1.PaymentDetail{
{
Amount: "10000.00",
Remark: "货款",
},
},
}
ctx := context.Background()
resp, err := client.CreatePaymentDocument(ctx, "/api/payment/create", req)
if err != nil {
log.Fatalf("创建付款单据失败: %v", err)
}
fmt.Printf("付款单据创建成功,单据编码: %s\n", resp.Code)
}
// ExampleClient_QueryPaymentStatus 演示如何查询支付状态
func ExampleClient_QueryPaymentStatus() {
client := intelligence_finance_v1.NewClient(
"https://api.example.com",
"your-tenant-id",
"your-client-id",
"your-client-secret",
)
req := &intelligence_finance_v1.PaymentStatusQueryRequest{
Code: "PAY-2024-001",
UserID: "user123",
}
ctx := context.Background()
resp, err := client.QueryPaymentStatus(ctx, "/api/payment/status", req)
if err != nil {
log.Fatalf("查询支付状态失败: %v", err)
}
fmt.Printf("支付状态: %s, 支付时间: %s\n", resp.PaymentStatus, resp.PaymentTime)
}
// Example_HandlePaymentNotification 演示如何处理平台支付完成回调
func Example_HandlePaymentNotification() {
// 这是一个 HTTP handler 示例,展示如何处理平台回调
http.HandleFunc("/callback/payment", func(w http.ResponseWriter, r *http.Request) {
// 解析通知数据
notification, err := intelligence_finance_v1.ParsePaymentNotification(r)
if err != nil {
log.Printf("解析通知失败: %v", err)
intelligence_finance_v1.WriteNotificationResponse(w, false)
return
}
// 根据支付状态处理业务逻辑
switch notification.PaymentStatus {
case intelligence_finance_v1.PaymentStatusSuccess:
log.Printf("支付成功: 单据 %s, 金额 %s, 时间 %s",
notification.Code, notification.Amount, notification.PaymentTime)
// 更新订单状态等业务处理...
case intelligence_finance_v1.PaymentStatusFail:
log.Printf("支付失败: 单据 %s, 原因: %s",
notification.Code, notification.FailReason)
case intelligence_finance_v1.PaymentStatusTerminate:
log.Printf("支付已取消: 单据 %s", notification.Code)
default:
log.Printf("支付状态: %s, 单据 %s", notification.PaymentStatus, notification.Code)
}
// 返回成功响应
intelligence_finance_v1.WriteNotificationResponse(w, true)
})
log.Println("回调服务启动在 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Example_VerifySignature 演示签名生成和验证
func Example_VerifySignature() {
secret := "your-client-secret"
timestamp := intelligence_finance_v1.GenerateTimestamp()
nonce, _ := intelligence_finance_v1.GenerateNonce(16)
// 生成签名
data := timestamp + nonce
signature := intelligence_finance_v1.HmacSHA256Sign(secret, data)
fmt.Printf("签名: %s\n", signature)
// 验证签名
valid := intelligence_finance_v1.HmacSHA256Verify(secret, data, signature)
fmt.Printf("签名验证: %v\n", valid)
}
// Example_DingTalkClient 演示钉钉AI表格客户端的使用
func Example_DingTalkClient() {
// 钉钉AI表格的 client-id 固定为 "dd-ai-table"
// 签名由AI表格自动完成但 SDK 仍会生成签名请求头
client := intelligence_finance_v1.NewDingTalkClient(
"https://api.example.com",
"your-tenant-id",
"your-app-secret", // 在钉钉AI表格中配置为 APPSecret
)
req := &intelligence_finance_v1.InvoiceStatusQueryRequest{
OrderID: "ORDER-2024-001",
}
ctx := context.Background()
resp, err := client.QueryInvoiceStatus(ctx, "/api/invoice/status", req)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
fmt.Printf("开票状态: %d\n", resp.Status)
}
```