From fae2b2ae606031721dff57ee1a877c631cecfe2c Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Mon, 17 Aug 2026 16:35:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6:=20intelli?= =?UTF-8?q?gence=5Ffinance=5Fv1=5Fga/valid.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- intelligence_finance_v1_ga/valid.md | 919 ++++++++++++++++++++++++++++ 1 file changed, 919 insertions(+) create mode 100644 intelligence_finance_v1_ga/valid.md diff --git a/intelligence_finance_v1_ga/valid.md b/intelligence_finance_v1_ga/valid.md new file mode 100644 index 0000000..89aae0f --- /dev/null +++ b/intelligence_finance_v1_ga/valid.md @@ -0,0 +1,919 @@ +// File: intelligence_finance_v1_ga/go.mod +```go +module intelligence_finance_v1_ga + +go 1.21 +``` + +// File: intelligence_finance_v1_ga/types.go +```go +package intelligence_finance_v1_ga + +import "encoding/json" + +// BaseResponse 是平台所有请求接口的通用响应结构。 +// code 为业务状态码(0 或 200 表示成功),msg 为提示信息,data 为具体业务数据。 +type BaseResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data json.RawMessage `json:"data"` +} + +// 发票类型枚举。 +const ( + InvoiceTypeSpecial = 1 // 专用发票 + InvoiceTypeNormal = 2 // 普通发票 + InvoiceTypeNormalElectronic = 3 // 普通发票(电子) + InvoiceTypeSpecialElectronic = 4 // 专用发票(电子) + InvoiceTypeDigitalSpecial = 8 // 数电专票 + InvoiceTypeDigitalNormal = 9 // 数电普票 +) + +// 开票状态枚举。 +const ( + InvoiceStatusNotIssued = 0 // 未开票 + InvoiceStatusIssuing = 1 // 开票中 + InvoiceStatusPartialFailed = 2 // 部分失败 + InvoiceStatusSuccess = 3 // 开票成功 + InvoiceStatusFailed = 4 // 开票失败 + InvoiceStatusPartialNotIssued = 5 // 部分未开 + InvoiceStatusNoDigitalAccount = 6 // 未配置数电账号 + InvoiceStatusNoAutoConfig = 7 // 未配置自动开票配置 +) + +// 支付状态枚举。 +const ( + PaymentStatusSuccess = "SUCCESS" // 支付成功 + PaymentStatusFail = "FAIL" // 支付失败 + PaymentStatusTerminate = "TERMINATE" // 支付取消 + PaymentStatusWaitPay = "WAIT_PAY" // 待支付 + PaymentStatusPaying = "PAYING" // 支付中 + PaymentStatusPartSuccess = "PART_SUCCESS" // 部分支付成功 + PaymentStatusRefund = "REFUND" // 退款 +) + +// 来源枚举。 +const ( + SourceApproval = "approval" // 审批单 + SourceOpenAPI = "openapi" // 开发接口 +) + +// 账户类型枚举。 +const ( + AccountTypeAlipay = "ALIPAY" // 支付宝 + AccountTypeBankCard = "BANKCARD" // 银行卡 + AccountTypeCorpBankCard = "CORP_BANK_CARD" // 对公银行卡 + AccountTypePersonalBankCard = "PERSONAL_BANK_CARD" // 对私银行卡 +) + +// 通知响应常量。 +const ( + NotifySuccess = "SUCCESS" // 通知处理成功 + NotifyFailed = "FAILED" // 通知处理失败 +) + +// InvoiceOrderRequest 是订单开票请求参数。 +type InvoiceOrderRequest struct { + CompanyCode string `json:"companyCode"` // 开票的企业主体编码,不传则默认主体开票 + OrderID string `json:"orderId"` // 订单唯一标识(需保证在贵方系统内唯一) + InvoiceType int `json:"invoiceType"` // 发票类型枚举 + Products []InvoiceProduct `json:"products"` // 货物/服务明细列表,至少一项 + Remark string `json:"remark"` // 订单备注(非发票备注) + Purchaser string `json:"purchaser"` // 购方企业名称 + TaxNum string `json:"taxnum"` // 购方纳税人识别号 + PurchaserAddress string `json:"purchaserAddress"` // 购方地址 + PurchaserTel string `json:"purchaserTel"` // 购方电话 + BankName string `json:"bankName"` // 购方开户行名称 + BankAccount string `json:"bankAccount"` // 购方银行账号 + Phone string `json:"phone"` // 收票人手机号(用于接收电票短信) + Email string `json:"email"` // 收票人邮箱(用于接收电票邮件) + ApplyPerson string `json:"applyPerson"` // 开票申请人名称 + Payee string `json:"payee"` // 收款人(发票票面) + Reviewer string `json:"reviewer"` // 复核人(发票票面) + InvoiceRemark string `json:"invoiceRemark"` // 发票备注栏内容 + NaturalPerson string `json:"naturalPerson"` // 购买方自然人标识:Y-是,N-否(默认N) + AdditionInfo string `json:"additionInfo"` // 附加信息(JSON数组字符串) +} + +// InvoiceProduct 是订单开票的货物/服务明细项。 +type InvoiceProduct struct { + ProductName string `json:"productName"` // 货物或服务名称 + RevenueCode string `json:"revenueCode"` // 19位税收分类编码 + AmountIncludeTax float64 `json:"amountIncludeTax"` // 单条明细含税总金额(单位:元) + Specs string `json:"specs"` // 规格型号 + Unit string `json:"unit"` // 计量单位(如:台、个、次) + Quantity float64 `json:"quantity"` // 数量 + Discount float64 `json:"discount"` // 折扣金额(无折扣传0) + TaxSign int `json:"taxSign"` // 是否含税:0-不含税;1-含税(默认建议传1) + TaxRate float64 `json:"taxRate"` // 税率(小数形式,如0.13表示13%) +} + +// InvoiceOrderData 是订单开票接口的响应数据。 +type InvoiceOrderData struct { + Status int `json:"status"` // 开票状态 + ErrorMsg string `json:"errorMsg"` // 错误信息(开票失败时返回原因) + DataList []InvoiceData `json:"dataList"` // 发票数据列表(一张订单可能对应多张发票) +} + +// InvoiceData 是单张发票的详细数据。 +type InvoiceData struct { + DeviceCode string `json:"deviceCode"` // 税控设备号 + Drawer string `json:"drawer"` // 开票人 + Email string `json:"email"` // 邮箱(购方邮箱) + InvoiceType string `json:"invoiceType"` // 发票类型 + IssueType string `json:"issueType"` // 开票类型:0-正数票;1-负数票(红冲) + ListFlag string `json:"listFlag"` // 清单标识:0-无清单;1-有清单 + Mobile string `json:"mobile"` // 手机号(购方手机) + OriginalInvCode string `json:"originalInvCode"` // 红冲时对应的原蓝票代码 + OriginalInvNo string `json:"originalInvNo"` // 红冲时对应的原蓝票号码 + AdditionInfo string `json:"additionInfo"` // 数电发票备注栏的附加信息部分 + Payee string `json:"payee"` // 收款人 + PurchaserAddress string `json:"purchaserAddress"` // 购方地址 + PurchaserBankAccount string `json:"purchaserBankAccount"` // 购方银行账号 + PurchaserBankName string `json:"purchaserBankName"` // 购方开户行 + PurchaserName string `json:"purchaserName"` // 购方名称 + PurchaserTaxNo string `json:"purchaserTaxNo"` // 购方税号 + PurchaserTel string `json:"purchaserTel"` // 购方电话 + NaturalPerson string `json:"naturalPerson"` // 购买方自然人标识:Y-是;N-否 + Remark string `json:"remark"` // 备注 + Reviewer string `json:"reviewer"` // 复核人 + SellerAddress string `json:"sellerAddress"` // 销方地址 + SellerBankAccount string `json:"sellerBankAccount"` // 销方开户账号 + SellerBankName string `json:"sellerBankName"` // 销方开户行 + SellerName string `json:"sellerName"` // 销方名称 + CheckCode string `json:"checkCode"` // 校验码 + CipherText string `json:"cipherText"` // 密码区 + DrewDate string `json:"drewDate"` // 开票日期(格式:yyyy-MM-dd HH:mm:ss) + InvoiceCode string `json:"invoiceCode"` // 发票代码 + InvoiceNo string `json:"invoiceNo"` // 发票号码 + InvoiceStatus string `json:"invoiceStatus"` // 发票状态:1-正常;2-已红冲;3-已作废 + LayoutFileURL string `json:"layoutFileUrl"` // 电子发票地址(PDF/OFD) + PDFURL string `json:"pdfUrl"` // 电子发票PDF地址 + OFDURL string `json:"ofdUrl"` // 电子发票OFD地址 + XMLURL string `json:"xmlUrl"` // 电子发票XML数据地址 + TotalExcludeTax string `json:"totalExcludeTax"` // 合计金额(不含税) + TotalIncludeTax string `json:"totalIncludeTax"` // 合计金额(含税) + TotalTaxAmount string `json:"totalTaxAmount"` // 合计税额 + LevyingType string `json:"levyingType"` // 征税方式 + Details []InvoiceDetail `json:"details"` // 商品明细列表 +} + +// InvoiceDetail 是发票中的商品明细。 +type InvoiceDetail struct { + Amount string `json:"amount"` // 金额 + Quantity string `json:"quantity"` // 数量 + DeductionAmount string `json:"deductionAmount"` // 扣除金额 + TaxAmount string `json:"taxAmount"` // 税额 + ItemTitle string `json:"itemTitle"` // 商品合并显示名称 + TaxCode string `json:"taxCode"` // 税收分类编码(19位) + ItemType string `json:"itemType"` // 商品行性质:0-正常行;1-折扣行;2-被折扣行 + ItemName string `json:"itemName"` // 商品简称 + Specs string `json:"specs"` // 商品规格型号 + TaxFreePolicy string `json:"taxFreePolicy"` // 免税政策:1-免税;2-不征税;3-普通零税率 + PreferentialPolicy string `json:"preferentialPolicy"` // 优惠政策类型 + TaxRate string `json:"taxRate"` // 税率(小数形式,如0.13) + TaxSign string `json:"taxSign"` // 是否含税:0-否;1-是 + Unit string `json:"unit"` // 计量单位(如:台、个、次) + UnitPrice string `json:"unitPrice"` // 单价 +} + +// QueryInvoiceStatusRequest 是开票状态查询请求参数。 +type QueryInvoiceStatusRequest struct { + OrderID string `json:"orderId"` // 订单唯一标识 +} + +// QueryInvoiceStatusData 是开票状态查询接口的响应数据。 +type QueryInvoiceStatusData struct { + Status int `json:"status"` // 开票状态 + Message string `json:"message"` // 状态描述(如"开票成功") + Data []InvoiceData `json:"data"` // 发票详细列表,结构与回调中的 data 字段一致 +} + +// CreatePaymentRequest 是创建付款单据请求参数。 +type CreatePaymentRequest struct { + Code string `json:"code"` // 单据编码 + YidaAppType string `json:"yidaAppType"` // 宜搭应用类型 + EmpAccountUserID string `json:"empAccountUserId"` // 员工账号用户ID + Department *Department `json:"department"` // 部门信息 + Usage string `json:"usage"` // 用途 + PaymentUserID string `json:"paymentUserId"` // 付款用户ID + Customer *Customer `json:"customer"` // 客户信息 + PrincipalID string `json:"principalId"` // 负责人ID + Remark string `json:"remark"` // 备注 + Supplier *Supplier `json:"supplier"` // 供应商信息 + Title string `json:"title"` // 标题 + Project *Project `json:"project"` // 项目信息 + PaymentUserIDListStr string `json:"paymentUserIdListStr"` // 付款用户ID列表字符串 + NeedPayment bool `json:"needPayment"` // 是否需要付款 + PaymentDetailListJsonStr string `json:"paymentDetailListJsonStr"` // 付款明细列表JSON字符串 + PaymentDetailList []PaymentDetail `json:"paymentDetailList"` // 付款明细列表 + Company *Company `json:"company"` // 企业主体信息 + Amount string `json:"amount"` // 金额 + RecipientAccountInfo *RecipientAccount `json:"recipientAccountInfo"` // 收款账户信息 + EnterpriseAccount *EnterpriseAccount `json:"enterpriseAccount"` // 企业账号信息 + Category []Category `json:"category"` // 收支类别信息 + UserID string `json:"userId"` // 用户ID + OccurDate int64 `json:"occurDate"` // 发生日期(时间戳) + Product *Product `json:"product"` // 商品信息 + YidaFormUUID string `json:"yidaFormUuid"` // 宜搭表单UUID + CanEditPaymentInfo bool `json:"canEditPaymentInfo"` // 是否可编辑付款信息 + PaymentUserIDList []string `json:"paymentUserIdList"` // 付款用户ID列表 + YidaProcInsID string `json:"yidaProcInsId"` // 宜搭流程实例ID + SyncPaymentOrder bool `json:"syncPaymentOrder"` // 是否同步付款单据 +} + +// Department 是部门信息。 +type Department struct { + Code string `json:"code"` // 部门编码 + Name string `json:"name"` // 部门名称 +} + +// Customer 是客户信息。 +type Customer struct { + Code string `json:"code"` // 客户编码 + Name string `json:"name"` // 客户名称 +} + +// Supplier 是供应商信息。 +type Supplier struct { + Code string `json:"code"` // 供应商编码 + Name string `json:"name"` // 供应商名称 +} + +// Project 是项目信息。 +type Project struct { + Code string `json:"code"` // 项目编码 + Name string `json:"name"` // 项目名称 +} + +// Category 是收支类别信息。 +type Category struct { + Code string `json:"code"` // 类别编码 + Name string `json:"name"` // 类别名称 +} + +// Product 是商品信息。 +type Product struct { + Code string `json:"code"` // 商品编码 + Name string `json:"name"` // 商品名称 +} + +// Company 是企业主体信息。 +type Company struct { + Code string `json:"code"` // 企业编码 + Name string `json:"name"` // 企业名称 +} + +// EnterpriseAccount 是企业账号信息。 +type EnterpriseAccount struct { + EnterpriseAccountCode string `json:"enterpriseAccountCode"` // 企业账号编码 + AccountCategory string `json:"accountCategory"` // 账户类别 + AccountType string `json:"accountType"` // 账户类型 + CardNo string `json:"cardNo"` // 卡号 + AccountName string `json:"accountName"` // 账户名称 + OfficialNumber string `json:"officialNumber"` // 对公账号 + OfficialName string `json:"officialName"` // 对公账户名称 + Name string `json:"name"` // 名称 + Code string `json:"code"` // 编码 + City string `json:"city"` // 城市 + Province string `json:"province"` // 省份 +} + +// RecipientAccount 是收款账户信息。 +type RecipientAccount struct { + AccountCategory string `json:"accountCategory"` // 账户类别 + AccountType string `json:"accountType"` // 账户类型 + CardNo string `json:"cardNo"` // 卡号 + AccountName string `json:"accountName"` // 账户名称 +} + +// PaymentDetail 是付款明细。 +type PaymentDetail struct { + Amount string `json:"amount"` // 金额 + InvoiceInfo *InvoiceInfo `json:"invoiceInfo"` // 发票信息 + ProductCode string `json:"productCode"` // 商品编码 + ProjectCode string `json:"projectCode"` // 项目编码 + Remark string `json:"remark"` // 备注 + PrincipalID string `json:"principalId"` // 负责人ID + Tax string `json:"tax"` // 税额 +} + +// InvoiceInfo 是付款明细中的发票信息。 +type InvoiceInfo struct { + InvoiceNo string `json:"invoiceNo"` // 发票号码 + InvoiceCode string `json:"invoiceCode"` // 发票代码 +} + +// CreatePaymentData 是创建付款单据接口的响应数据。 +type CreatePaymentData struct { + Code string `json:"code"` // 单据唯一编码 +} + +// QueryPaymentStatusRequest 是支付状态查询请求参数。 +type QueryPaymentStatusRequest struct { + Code string `json:"code"` // 单据编码 + UserID string `json:"userId"` // 用户ID +} + +// PaymentInfo 是支付信息,用于支付完成通知和支付状态查询。 +type PaymentInfo struct { + Code string `json:"code"` // 单据编码 + InstanceID string `json:"instanceId"` // 实例ID + CorpID string `json:"corpId"` // 企业ID + PaymentStatus string `json:"paymentStatus"` // 支付状态 + PaymentTime string `json:"paymentTime"` // 支付时间 + UserID string `json:"userId"` // 用户ID + FailReason string `json:"failReason"` // 失败原因 + PayerAccountInfo *PayerAccountInfo `json:"payerAccountInfo"` // 付款账户信息 + PayeeAccountInfo *PayeeAccountInfo `json:"payeeAccountInfo"` // 收款账户信息 + RelatedRowNumberList []string `json:"relatedRowNumberList"` // 关联行号列表 + Source string `json:"source"` // 来源 + Template string `json:"template"` // 模板 + Amount string `json:"amount"` // 金额 +} + +// PayerAccountInfo 是付款账户信息。 +type PayerAccountInfo struct { + BankOpenDTO *BankOpenDTO `json:"bankOpenDTO"` // 银行信息 + EnterpriseAccountCode string `json:"enterpriseAccountCode"` // 企业账号编码 + AccountType string `json:"accountType"` // 账户类型 +} + +// PayeeAccountInfo 是收款账户信息。 +type PayeeAccountInfo struct { + BankOpenDTO *BankOpenDTO `json:"bankOpenDTO"` // 银行信息 +} + +// BankOpenDTO 是银行信息。 +type BankOpenDTO struct { + BankCode string `json:"bankCode"` // 银行编码 + BankName string `json:"bankName"` // 银行名称 + BankBranchCode string `json:"bankBranchCode"` // 银行支行编码 + BankBranchName string `json:"bankBranchName"` // 银行支行名称 + AccountName string `json:"accountName"` // 账户名称 + BankCardNo string `json:"bankCardNo"` // 银行卡号 + Type string `json:"type"` // 账户类型 +} + +// NotifyRequest 是通用通知机制的通知数据。 +type NotifyRequest struct { + BizType string `json:"bizType"` // 业务类型 + BizID string `json:"bizId"` // 业务ID + Data string `json:"data"` // 业务数据 +} +``` + +// File: intelligence_finance_v1_ga/crypto.go +```go +package intelligence_finance_v1_ga + +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 生成指定长度的随机字符串(加密安全)。 +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 "", err + } + b[i] = charset[num.Int64()] + } + return string(b), nil +} + +// HmacSHA256Base64 使用 HmacSHA256 算法对 data 进行签名,并返回 Base64 编码结果。 +// 密钥为 client-secret,签名数据为 timestamp + nonce。 +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 方式1:字典序排序拼接。 +// 空值会被排除,且自动排除 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 方式2:固定顺序拼接。 +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_ga/errors.go +```go +package intelligence_finance_v1_ga + +import "fmt" + +// SDKError 表示 SDK 内部错误(网络、编解码等)。 +type SDKError struct { + Op string // 发生错误的操作名称 + Err error // 底层错误 +} + +// Error 实现 error 接口。 +func (e *SDKError) Error() string { + if e.Op != "" { + return fmt.Sprintf("intelligence_finance_v1_ga: %s: %v", e.Op, e.Err) + } + return fmt.Sprintf("intelligence_finance_v1_ga: %v", e.Err) +} + +// Unwrap 返回底层错误,支持 errors.Is / errors.As。 +func (e *SDKError) Unwrap() error { + return e.Err +} + +// APIError 表示平台返回的业务错误或非 200 的 HTTP 响应。 +type APIError struct { + HTTPStatus int // HTTP 状态码(非 200 时设置) + Code int // 平台业务错误码 + Msg string // 平台错误信息 + Body string // 原始响应体(HTTP 非 200 时设置) +} + +// Error 实现 error 接口。 +func (e *APIError) Error() string { + if e.HTTPStatus != 0 { + return fmt.Sprintf("intelligence_finance_v1_ga: http status %d: %s", e.HTTPStatus, e.Body) + } + return fmt.Sprintf("intelligence_finance_v1_ga: api error code=%d msg=%s", e.Code, e.Msg) +} +``` + +// File: intelligence_finance_v1_ga/client.go +```go +package intelligence_finance_v1_ga + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +// 默认接口路径(实际路径以接口对接时分配的短码为准,可通过 Option 覆盖)。 +const ( + DefaultInvoiceOrderPath = "/invoice/order" + DefaultQueryInvoiceStatusPath = "/invoice/status/query" + DefaultCreatePaymentPath = "/payment/create" + DefaultQueryPaymentStatusPath = "/payment/status/query" +) + +// Client 是业财连接接口的客户端。 +type Client struct { + baseURL string + tenantID string + clientID string + clientSecret string + httpClient *http.Client + signEnabled bool + + invoiceOrderPath string + queryInvoiceStatusPath string + createPaymentPath string + queryPaymentStatusPath string +} + +// Option 定义客户端配置选项。 +type Option func(*Client) + +// WithHTTPClient 设置自定义 HTTP 客户端。 +func WithHTTPClient(c *http.Client) Option { + return func(cli *Client) { + cli.httpClient = c + } +} + +// WithTimeout 设置请求超时时间。 +func WithTimeout(d time.Duration) Option { + return func(cli *Client) { + cli.httpClient.Timeout = d + } +} + +// WithSignEnabled 设置是否启用签名。 +// 客户应用需启用签名(默认启用);钉钉AI表格无需配置签名,可设置为 false。 +func WithSignEnabled(enabled bool) Option { + return func(cli *Client) { + cli.signEnabled = enabled + } +} + +// WithInvoiceOrderPath 覆盖订单开票接口路径。 +func WithInvoiceOrderPath(p string) Option { + return func(cli *Client) { cli.invoiceOrderPath = p } +} + +// WithQueryInvoiceStatusPath 覆盖开票状态查询接口路径。 +func WithQueryInvoiceStatusPath(p string) Option { + return func(cli *Client) { cli.queryInvoiceStatusPath = p } +} + +// WithCreatePaymentPath 覆盖创建付款单据接口路径。 +func WithCreatePaymentPath(p string) Option { + return func(cli *Client) { cli.createPaymentPath = p } +} + +// WithQueryPaymentStatusPath 覆盖支付状态查询接口路径。 +func WithQueryPaymentStatusPath(p string) Option { + return func(cli *Client) { cli.queryPaymentStatusPath = p } +} + +// 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, + httpClient: &http.Client{Timeout: 30 * time.Second}, + signEnabled: true, + invoiceOrderPath: DefaultInvoiceOrderPath, + queryInvoiceStatusPath: DefaultQueryInvoiceStatusPath, + createPaymentPath: DefaultCreatePaymentPath, + queryPaymentStatusPath: DefaultQueryPaymentStatusPath, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// doRequest 发送 POST 请求并处理通用响应结构,返回 data 字段的原始字节。 +func (c *Client) doRequest(ctx context.Context, path string, reqBody interface{}) (json.RawMessage, error) { + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("marshal request: %w", err)} + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) + if err != nil { + return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("new request: %w", err)} + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("tenant-id", c.tenantID) + req.Header.Set("client-id", c.clientID) + + if c.signEnabled { + timestamp := GenerateTimestamp() + nonce, err := GenerateNonce(16) + if err != nil { + return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("generate nonce: %w", err)} + } + // 签名算法:HmacSHA256,密钥为 client-secret,签名数据为 timestamp + nonce,结果 Base64 编码。 + 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, &SDKError{Op: "doRequest", Err: fmt.Errorf("do request: %w", err)} + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("read response: %w", err)} + } + + if resp.StatusCode != http.StatusOK { + return nil, &APIError{HTTPStatus: resp.StatusCode, Body: string(respBytes)} + } + + var base BaseResponse + if err := json.Unmarshal(respBytes, &base); err != nil { + return nil, &SDKError{Op: "doRequest", Err: fmt.Errorf("unmarshal base response: %w", err)} + } + if base.Code != 0 && base.Code != 200 { + return nil, &APIError{Code: base.Code, Msg: base.Msg} + } + return base.Data, nil +} + +// InvoiceOrder 提交订单开票请求。 +func (c *Client) InvoiceOrder(ctx context.Context, req *InvoiceOrderRequest) (*InvoiceOrderData, error) { + data, err := c.doRequest(ctx, c.invoiceOrderPath, req) + if err != nil { + return nil, err + } + var out InvoiceOrderData + if len(data) > 0 { + if err := json.Unmarshal(data, &out); err != nil { + return nil, &SDKError{Op: "InvoiceOrder", Err: fmt.Errorf("unmarshal data: %w", err)} + } + } + return &out, nil +} + +// QueryInvoiceStatus 查询订单开票状态。 +func (c *Client) QueryInvoiceStatus(ctx context.Context, req *QueryInvoiceStatusRequest) (*QueryInvoiceStatusData, error) { + data, err := c.doRequest(ctx, c.queryInvoiceStatusPath, req) + if err != nil { + return nil, err + } + var out QueryInvoiceStatusData + if len(data) > 0 { + if err := json.Unmarshal(data, &out); err != nil { + return nil, &SDKError{Op: "QueryInvoiceStatus", Err: fmt.Errorf("unmarshal data: %w", err)} + } + } + return &out, nil +} + +// CreatePayment 创建付款单据。 +func (c *Client) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*CreatePaymentData, error) { + data, err := c.doRequest(ctx, c.createPaymentPath, req) + if err != nil { + return nil, err + } + var out CreatePaymentData + if len(data) > 0 { + if err := json.Unmarshal(data, &out); err != nil { + return nil, &SDKError{Op: "CreatePayment", Err: fmt.Errorf("unmarshal data: %w", err)} + } + } + return &out, nil +} + +// QueryPaymentStatus 查询支付状态。 +// 响应数据与支付完成通知的数据结构一致。 +func (c *Client) QueryPaymentStatus(ctx context.Context, req *QueryPaymentStatusRequest) (*PaymentInfo, error) { + data, err := c.doRequest(ctx, c.queryPaymentStatusPath, req) + if err != nil { + return nil, err + } + var out PaymentInfo + if len(data) > 0 { + if err := json.Unmarshal(data, &out); err != nil { + return nil, &SDKError{Op: "QueryPaymentStatus", Err: fmt.Errorf("unmarshal data: %w", err)} + } + } + return &out, nil +} + +// ParsePaymentNotify 解析支付完成通知请求体。 +// 该接口由客户提供、平台调用,用于在支付完成后接收平台回调。 +func ParsePaymentNotify(body []byte) (*PaymentInfo, error) { + var info PaymentInfo + if err := json.Unmarshal(body, &info); err != nil { + return nil, &SDKError{Op: "ParsePaymentNotify", Err: fmt.Errorf("unmarshal notify: %w", err)} + } + return &info, nil +} + +// ParseNotify 解析通用通知请求体(bizType / bizId / data)。 +func ParseNotify(body []byte) (*NotifyRequest, error) { + var req NotifyRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, &SDKError{Op: "ParseNotify", Err: fmt.Errorf("unmarshal notify: %w", err)} + } + return &req, nil +} +``` + +// File: intelligence_finance_v1_ga/example_test.go +```go +package intelligence_finance_v1_ga + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +// TestInvoiceOrder 演示订单开票接口的调用。 +func TestInvoiceOrder(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if got := r.Header.Get("tenant-id"); got != "tenant-001" { + t.Errorf("tenant-id = %q", got) + } + if got := r.Header.Get("client-id"); got != "dd-ai-table" { + t.Errorf("client-id = %q", got) + } + if got := r.Header.Get("x-bfl-signature"); got == "" { + t.Error("missing x-bfl-signature header") + } + if got := r.Header.Get("x-bfl-signature-timestamp"); got == "" { + t.Error("missing x-bfl-signature-timestamp header") + } + if got := r.Header.Get("x-bfl-signature-nonce"); got == "" { + t.Error("missing x-bfl-signature-nonce header") + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"code":0,"msg":"ok","data":{"status":1,"errorMsg":"","dataList":[]}}`)) + })) + defer server.Close() + + client := NewClient(server.URL, "tenant-001", "dd-ai-table", "test-secret") + + resp, err := client.InvoiceOrder(context.Background(), &InvoiceOrderRequest{ + OrderID: "ORDER-001", + InvoiceType: InvoiceTypeDigitalNormal, + Purchaser: "测试购方企业", + TaxNum: "91310000MA1FL00000", + Email: "buyer@example.com", + Products: []InvoiceProduct{ + { + ProductName: "测试服务", + RevenueCode: "3040101000000000000", + AmountIncludeTax: 113, + Quantity: 1, + TaxSign: 1, + TaxRate: 0.13, + }, + }, + }) + if err != nil { + t.Fatalf("InvoiceOrder: %v", err) + } + if resp.Status != InvoiceStatusIssuing { + t.Errorf("status = %d, want %d", resp.Status, InvoiceStatusIssuing) + } +} + +// TestQueryInvoiceStatus 演示开票状态查询接口的调用。 +func TestQueryInvoiceStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"code":0,"msg":"ok","data":{"status":3,"message":"开票成功","data":[]}}`)) + })) + defer server.Close() + + client := NewClient(server.URL, "tenant-001", "dd-ai-table", "test-secret") + + resp, err := client.QueryInvoiceStatus(context.Background(), &QueryInvoiceStatusRequest{ + OrderID: "ORDER-001", + }) + if err != nil { + t.Fatalf("QueryInvoiceStatus: %v", err) + } + if resp.Status != InvoiceStatusSuccess { + t.Errorf("status = %d, want %d", resp.Status, InvoiceStatusSuccess) + } + if resp.Message != "开票成功" { + t.Errorf("message = %q", resp.Message) + } +} + +// TestCreatePayment 演示创建付款单据接口的调用。 +func TestCreatePayment(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"code":0,"msg":"ok","data":{"code":"PAY-001"}}`)) + })) + defer server.Close() + + client := NewClient(server.URL, "tenant-001", "dd-ai-table", "test-secret") + + resp, err := client.CreatePayment(context.Background(), &CreatePaymentRequest{ + Code: "PAY-001", + UserID: "user-001", + Title: "测试付款单", + Amount: "1000.00", + Department: &Department{ + Name: "财务部", + }, + Supplier: &Supplier{ + Name: "测试供应商", + }, + PaymentDetailList: []PaymentDetail{ + { + Amount: "1000.00", + InvoiceInfo: &InvoiceInfo{ + InvoiceNo: "12345678", + InvoiceCode: "011001900111", + }, + }, + }, + }) + if err != nil { + t.Fatalf("CreatePayment: %v", err) + } + if resp.Code != "PAY-001" { + t.Errorf("code = %q, want PAY-001", resp.Code) + } +} + +// TestQueryPaymentStatus 演示支付状态查询接口的调用。 +func TestQueryPaymentStatus(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"code":0,"msg":"ok","data":{"code":"PAY-001","instanceId":"inst-001","corpId":"corp-001","paymentStatus":"SUCCESS","paymentTime":"2024-01-01 10:00:00","userId":"user-001","source":"openapi","amount":"1000.00"}}`)) + })) + defer server.Close() + + client := NewClient(server.URL, "tenant-001", "dd-ai-table", "test-secret") + + resp, err := client.QueryPaymentStatus(context.Background(), &QueryPaymentStatusRequest{ + Code: "PAY-001", + UserID: "user-001", + }) + if err != nil { + t.Fatalf("QueryPaymentStatus: %v", err) + } + if resp.PaymentStatus != PaymentStatusSuccess { + t.Errorf("paymentStatus = %q, want %q", resp.PaymentStatus, PaymentStatusSuccess) + } +} + +// TestParsePaymentNotify 演示解析支付完成通知。 +func TestParsePaymentNotify(t *testing.T) { + body := []byte(`{"code":"PAY-001","instanceId":"inst-001","corpId":"corp-001","paymentStatus":"SUCCESS","paymentTime":"2024-01-01 10:00:00","userId":"user-001","source":"openapi","amount":"1000.00","payerAccountInfo":{"accountType":"BANKCARD","bankOpenDTO":{"bankName":"交通银行","bankCardNo":"6222000000000000"}}}`) + + info, err := ParsePaymentNotify(body) + if err != nil { + t.Fatalf("ParsePaymentNotify: %v", err) + } + if info.Code != "PAY-001" { + t.Errorf("code = %q", info.Code) + } + if info.PaymentStatus != PaymentStatusSuccess { + t.Errorf("paymentStatus = %q", info.PaymentStatus) + } + if info.PayerAccountInfo == nil || info.PayerAccountInfo.BankOpenDTO == nil { + t.Error("payerAccountInfo.bankOpenDTO is nil") + } +} + +// TestParseNotify 演示解析通用通知。 +func TestParseNotify(t *testing.T) { + body := []byte(`{"bizType":"invoice","bizId":"ORDER-001","data":"{\"status\":3}"}`) + + req, err := ParseNotify(body) + if err != nil { + t.Fatalf("ParseNotify: %v", err) + } + if req.BizType != "invoice" { + t.Errorf("bizType = %q", req.BizType) + } + if req.BizID != "ORDER-001" { + t.Errorf("bizId = %q", req.BizID) + } +} + +// TestHmacSHA256Base64 演示签名算法的正确性。 +func TestHmacSHA256Base64(t *testing.T) { + // 使用固定输入验证签名结果可复现。 + sig := HmacSHA256Base64("secret", "1700000000abc123") + if sig == "" { + t.Error("signature is empty") + } +} +``` \ No newline at end of file