添加文件: intelligence_finance_v1_ga/generate.md
This commit is contained in:
parent
6a1ed50a7e
commit
754d43291c
|
|
@ -0,0 +1,921 @@
|
|||
// 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== SDK 生成完成 ===
|
||||
Loading…
Reference in New Issue