diff --git a/intelligence_finance_v1/generate.md b/intelligence_finance_v1/generate.md new file mode 100644 index 0000000..d5fd9ba --- /dev/null +++ b/intelligence_finance_v1/generate.md @@ -0,0 +1,981 @@ +。 + +现在我将根据文档生成完整的 Go SDK 代码工程。 + +## 分析 + +**认证机制**:客户应用使用 `HmacSHA256` 算法,密钥为 `client-secret`,签名数据为 `timestamp + nonce`,结果 Base64 编码。需要生成 timestamp 和 nonce。 + +**接口列表**: +1. 订单开票(POST) +2. 开票状态查询(POST) +3. 创建付款单据(POST) +4. 支付完成通知(回调,客户提供接口) +5. 支付状态查询(POST) + +**通知机制**:客户提供接口,平台调用,响应直接返回字符串 `SUCCESS` 或 `FAILED`。 + +现在开始生成所有文件: + +--- + +// 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" + +// Error 表示 SDK 返回的错误类型。 +type Error struct { + // Code 错误码 + Code int + // Msg 错误信息 + Msg string + // Err 底层错误 + Err error +} + +// Error 实现 error 接口。 +func (e *Error) Error() string { + if e.Err != nil { + return fmt.Sprintf("code=%d, msg=%s, err=%v", e.Code, e.Msg, e.Err) + } + return fmt.Sprintf("code=%d, msg=%s", e.Code, e.Msg) +} + +// Unwrap 返回底层错误,支持 errors.Is/As。 +func (e *Error) Unwrap() error { + return e.Err +} + +// NewError 创建一个新的 Error。 +func NewError(code int, msg string) *Error { + return &Error{Code: code, Msg: msg} +} + +// WrapError 包装底层错误。 +func WrapError(code int, msg string, err error) *Error { + return &Error{Code: code, Msg: msg, Err: err} +} + +// 预定义错误 +var ( + // ErrInvalidConfig 配置无效 + ErrInvalidConfig = NewError(1001, "invalid config") + // ErrNetwork 网络请求失败 + ErrNetwork = NewError(1002, "network error") + // ErrSignature 签名失败 + ErrSignature = NewError(1003, "signature error") + // ErrResponse 响应解析失败 + ErrResponse = NewError(1004, "response error") + // ErrBusiness 业务错误(code != 0) + ErrBusiness = NewError(1005, "business error") +) +``` + +--- + +// File: intelligence_finance_v1/crypto.go +```go +package intelligence_finance_v1 + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" + "math/big" + "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 进行签名,密钥为 secret,结果以 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)) +} + +// BuildSignature 根据文档认证机制生成签名。 +// +// 签名算法:HmacSHA256 +// 密钥:client-secret +// 签名数据:x-bfl-signature-timestamp + x-bfl-signature-nonce +// 结果编码:Base64 +func BuildSignature(clientSecret, timestamp, nonce string) string { + return HmacSHA256Base64(clientSecret, timestamp+nonce) +} +``` + +--- + +// File: intelligence_finance_v1/types.go +```go +package intelligence_finance_v1 + +// 发票类型枚举 +const ( + InvoiceTypeSpecial = 1 // 专用发票 + InvoiceTypeNormal = 2 // 普通发票 + InvoiceTypeNormalElectronic = 3 // 普通发票(电子) + InvoiceTypeSpecialElec = 4 // 专用发票(电子) + InvoiceTypeDigitalSpecial = 8 // 数电专票 + InvoiceTypeDigitalNormal = 9 // 数电普票 +) + +// 开票状态枚举 +const ( + InvoiceStatusNotInvoiced = 0 // 未开票 + InvoiceStatusInvoicing = 1 // 开票中 + InvoiceStatusPartialFailed = 2 // 部分失败 + InvoiceStatusSuccess = 3 // 开票成功 + InvoiceStatusFailed = 4 // 开票失败 + InvoiceStatusPartialNotDone = 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" // 对私银行卡 +) + +// CommonResponse 通用响应结构。 +type CommonResponse struct { + Code int `json:"code"` // 响应码 + Msg string `json:"msg"` // 响应信息 + Data any `json:"data"` // 响应数据 +} + +// InvoiceRequest 订单开票请求参数。 +type InvoiceRequest struct { + CompanyCode string `json:"companyCode"` // 开票的企业主体编码 + OrderID string `json:"orderId"` // 订单唯一标识 + InvoiceType int `json:"invoiceType"` // 发票类型枚举 + Products []InvoiceProduct `json:"products"` // 货物/服务明细列表 + Remark string `json:"remark,omitempty"` // 订单备注 + Purchaser string `json:"purchaser"` // 购方企业名称 + TaxNum string `json:"taxnum,omitempty"` // 购方纳税人识别号 + PurchaserAddr string `json:"purchaserAddress,omitempty"` // 购方地址 + PurchaserTel string `json:"purchaserTel,omitempty"` // 购方电话 + BankName string `json:"bankName,omitempty"` // 购方开户行名称 + BankAccount string `json:"bankAccount,omitempty"` // 购方银行账号 + Phone string `json:"phone,omitempty"` // 收票人手机号 + Email string `json:"email,omitempty"` // 收票人邮箱 + ApplyPerson string `json:"applyPerson,omitempty"` // 开票申请人名称 + Payee string `json:"payee,omitempty"` // 收款人 + Reviewer string `json:"reviewer,omitempty"` // 复核人 + InvoiceRemark string `json:"invoiceRemark,omitempty"` // 发票备注栏内容 + NaturalPerson string `json:"naturalPerson,omitempty"` // 购买方自然人标识 + AdditionInfo string `json:"additionInfo,omitempty"` // 附加信息(JSON数组字符串) +} + +// InvoiceProduct 订单开票货物/服务明细项。 +type InvoiceProduct struct { + ProductName string `json:"productName"` // 货物或服务名称 + RevenueCode string `json:"revenueCode"` // 19位税收分类编码 + AmountIncludeTax float64 `json:"amountIncludeTax"` // 单条明细含税总金额(元) + Specs string `json:"specs,omitempty"` // 规格型号 + Unit string `json:"unit,omitempty"` // 计量单位 + Quantity float64 `json:"quantity"` // 数量 + Discount float64 `json:"discount,omitempty"` // 折扣金额 + TaxSign int `json:"taxSign,omitempty"` // 是否含税:0-不含税;1-含税 + TaxRate float64 `json:"taxRate,omitempty"` // 税率(小数形式) +} + +// InvoiceResponse 订单开票响应参数。 +type InvoiceResponse 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"` // 开票类型 + ListFlag string `json:"listFlag"` // 清单标识 + Mobile string `json:"mobile"` // 手机号 + OriginalInvCode string `json:"originalInvCode,omitempty"` // 原蓝票代码 + OriginalInvNo string `json:"originalInvNo,omitempty"` // 原蓝票号码 + AdditionInfo string `json:"additionInfo,omitempty"` // 附加信息 + Payee string `json:"payee"` // 收款人 + PurchaserAddress string `json:"purchaserAddress"` // 购方地址 + PurchaserBankAccount string `json:"purchaserBankAccount"` // 购方银行账号 + PurchaserBankName string `json:"purchaserBankName"` // 购方开户行 + PurchaserName string `json:"purchaserName"` // 购方名称 + PurchaserTaxNo string `json:"purchaserTaxNo"` // 购方税号 + PurchaserTel string `json:"purchaserTel"` // 购方电话 + NaturalPerson string `json:"naturalPerson,omitempty"` // 自然人标识 + Remark string `json:"remark"` // 备注 + Reviewer string `json:"reviewer"` // 复核人 + SellerAddress string `json:"sellerAddress,omitempty"` // 销方地址 + SellerBankAccount string `json:"sellerBankAccount"` // 销方开户账号 + SellerBankName string `json:"sellerBankName"` // 销方开户行 + SellerName string `json:"sellerName"` // 销方名称 + CheckCode string `json:"checkCode"` // 校验码 + CipherText string `json:"cipherText"` // 密码区 + DrewDate string `json:"drewDate,omitempty"` // 开票日期 + InvoiceCode string `json:"invoiceCode"` // 发票代码 + InvoiceNo string `json:"invoiceNo"` // 发票号码 + InvoiceStatus string `json:"invoiceStatus"` // 发票状态 + LayoutFileURL string `json:"layoutFileUrl,omitempty"` // 电子发票地址 + PdfURL string `json:"pdfUrl,omitempty"` // 电子发票PDF地址 + OfdURL string `json:"ofdUrl,omitempty"` // 电子发票OFD地址 + XmlURL string `json:"xmlUrl,omitempty"` // 电子发票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,omitempty"` // 金额 + Quantity string `json:"quantity,omitempty"` // 数量 + DeductionAmount string `json:"deductionAmount,omitempty"` // 扣除金额 + TaxAmount string `json:"taxAmount,omitempty"` // 税额 + ItemTitle string `json:"itemTitle"` // 商品合并显示名称 + TaxCode string `json:"taxCode"` // 税收分类编码 + ItemType string `json:"itemType"` // 商品行性质 + ItemName string `json:"itemName"` // 商品简称 + Specs string `json:"specs,omitempty"` // 商品规格型号 + TaxFreePolicy string `json:"taxFreePolicy"` // 免税政策 + PreferentialPolicy string `json:"preferentialPolicy"` // 优惠政策类型 + TaxRate string `json:"taxRate"` // 税率 + TaxSign string `json:"taxSign"` // 是否含税 + Unit string `json:"unit,omitempty"` // 计量单位 + UnitPrice string `json:"unitPrice,omitempty"` // 单价 +} + +// InvoiceStatusQueryRequest 开票状态查询请求参数。 +type InvoiceStatusQueryRequest struct { + OrderID string `json:"orderId"` // 订单唯一标识 +} + +// InvoiceStatusQueryResponse 开票状态查询响应参数。 +type InvoiceStatusQueryResponse struct { + Status int `json:"status"` // 开票状态 + Message string `json:"message"` // 状态描述 + Data []InvoiceData `json:"data"` // 发票详细列表 +} + +// PaymentOrderRequest 创建付款单据请求参数。 +type PaymentOrderRequest struct { + Code string `json:"code"` // 单据编码 + YidaAppType string `json:"yidaAppType,omitempty"` // 宜搭应用类型 + EmpAccountUserID string `json:"empAccountUserId,omitempty"` // 员工账户用户ID + Department *Department `json:"department,omitempty"` // 部门信息 + Usage string `json:"usage,omitempty"` // 用途 + PaymentUserID string `json:"paymentUserId,omitempty"` // 付款用户ID + Customer *Customer `json:"customer,omitempty"` // 客户信息 + PrincipalID string `json:"principalId,omitempty"` // 负责人ID + Remark string `json:"remark,omitempty"` // 备注 + Supplier *Supplier `json:"supplier,omitempty"` // 供应商信息 + Title string `json:"title,omitempty"` // 标题 + Project *Project `json:"project,omitempty"` // 项目信息 + PaymentUserIDListStr string `json:"paymentUserIdListStr,omitempty"` // 付款用户ID列表字符串 + NeedPayment bool `json:"needPayment,omitempty"` // 是否需要付款 + PaymentDetailListJSONStr string `json:"paymentDetailListJsonStr,omitempty"` // 付款明细JSON字符串 + PaymentDetailList []PaymentDetail `json:"paymentDetailList,omitempty"` // 付款明细列表 + Company *Company `json:"company,omitempty"` // 企业主体信息 + Amount string `json:"amount,omitempty"` // 金额 + RecipientAccountInfo *RecipientAccount `json:"recipientAccountInfo,omitempty"` // 收款账户信息 + EnterpriseAccount *EnterpriseAccount `json:"enterpriseAccount,omitempty"` // 企业账号信息 + Category []Category `json:"category,omitempty"` // 收支类别信息 + UserID string `json:"userId"` // 用户ID + OccurDate int64 `json:"occurDate,omitempty"` // 发生日期(时间戳) + Product *Product `json:"product,omitempty"` // 商品信息 + YidaFormUUID string `json:"yidaFormUuid,omitempty"` // 宜搭表单UUID + CanEditPaymentInfo bool `json:"canEditPaymentInfo,omitempty"` // 是否可编辑付款信息 + PaymentUserIDList []string `json:"paymentUserIdList,omitempty"` // 付款用户ID列表 + YidaProcInsID string `json:"yidaProcInsId,omitempty"` // 宜搭流程实例ID + 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"` // 负责人ID + Tax string `json:"tax,omitempty"` // 税额 +} + +// InvoiceInfo 付款明细发票信息。 +type InvoiceInfo struct { + InvoiceNo string `json:"invoiceNo,omitempty"` // 发票号码 + InvoiceCode string `json:"invoiceCode,omitempty"` // 发票代码 +} + +// PaymentOrderResponse 创建付款单据响应参数。 +type PaymentOrderResponse struct { + Code string `json:"code"` // 单据唯一编码 +} + +// PaymentNotifyRequest 支付完成通知数据(接口4)。 +type PaymentNotifyRequest 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,omitempty"` // 失败原因 + PayerAccountInfo *PayerAccountInfo `json:"payerAccountInfo,omitempty"` // 付款账户信息 + PayeeAccountInfo *PayeeAccountInfo `json:"payeeAccountInfo,omitempty"` // 收款账户信息 + RelatedRowNumberList []string `json:"relatedRowNumberList,omitempty"` // 关联行号列表 + Source string `json:"source,omitempty"` // 来源 + Template string `json:"template,omitempty"` // 模板 + Amount string `json:"amount,omitempty"` // 金额 +} + +// PayerAccountInfo 付款账户信息。 +type PayerAccountInfo struct { + BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,omitempty"` // 银行信息 + EnterpriseAccountCode string `json:"enterpriseAccountCode,omitempty"` // 企业账号编码 + AccountType string `json:"accountType,omitempty"` // 账户类型 +} + +// PayeeAccountInfo 收款账户信息。 +type PayeeAccountInfo struct { + BankOpenDTO *BankOpenDTO `json:"bankOpenDTO,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"` // 账户类型 +} + +// PaymentStatusQueryRequest 支付状态查询请求参数。 +type PaymentStatusQueryRequest struct { + Code string `json:"code"` // 单据编码 + UserID string `json:"userId"` // 用户ID +} + +// PaymentStatusQueryResponse 支付状态查询响应参数(与支付通知数据结构一致)。 +type PaymentStatusQueryResponse 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,omitempty"` // 失败原因 + PayerAccountInfo *PayerAccountInfo `json:"payerAccountInfo,omitempty"` // 付款账户信息 + PayeeAccountInfo *PayeeAccountInfo `json:"payeeAccountInfo,omitempty"` // 收款账户信息 + RelatedRowNumberList []string `json:"relatedRowNumberList,omitempty"` // 关联行号列表 + Source string `json:"source,omitempty"` // 来源 + Template string `json:"template,omitempty"` // 模板 + Amount string `json:"amount,omitempty"` // 金额 +} + +// Notification 通用通知数据(bizType/bizId/data)。 +type Notification struct { + BizType string `json:"bizType"` // 业务类型 + BizID string `json:"bizId"` // 业务ID + Data string `json:"data"` // 业务数据 +} +``` + +--- + +// File: intelligence_finance_v1/client.go +```go +package intelligence_finance_v1 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// Client 是智能财务业财连接 SDK 的客户端。 +type Client struct { + // baseURL 平台接口基础地址 + baseURL string + // tenantID 平台分配的租户唯一标识 + tenantID string + // clientID 平台分配的应用标识 + clientID string + // clientSecret 平台分配的密钥 + clientSecret string + // httpClient HTTP 客户端 + httpClient *http.Client + // signEnabled 是否启用签名(钉钉AI表格无需配置签名,可关闭) + signEnabled bool +} + +// ClientOption 客户端配置选项。 +type ClientOption func(*Client) + +// WithHTTPClient 自定义 HTTP 客户端。 +func WithHTTPClient(hc *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = hc + } +} + +// WithSignEnabled 设置是否启用签名(默认启用)。 +func WithSignEnabled(enabled bool) ClientOption { + return func(c *Client) { + c.signEnabled = enabled + } +} + +// NewClient 创建一个新的智能财务业财连接客户端。 +// +// 参数: +// - baseURL:平台接口基础地址 +// - 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: 30 * time.Second}, + signEnabled: true, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// doRequest 发送 HTTP 请求并解析响应。 +func (c *Client) doRequest(ctx context.Context, path string, reqBody any, respBody any) error { + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return WrapError(ErrSignature.Code, "marshal request body failed", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(bodyBytes)) + if err != nil { + return WrapError(ErrNetwork.Code, "create request failed", err) + } + + // 设置公共请求头 + req.Header.Set("Content-Type", "application/json") + req.Header.Set("tenant-id", c.tenantID) + req.Header.Set("client-id", c.clientID) + + // 签名(钉钉AI表格无需配置,可通过 WithSignEnabled(false) 关闭) + if c.signEnabled { + timestamp := GenerateTimestamp() + nonce, err := GenerateNonce(16) + if err != nil { + return WrapError(ErrSignature.Code, "generate nonce failed", err) + } + req.Header.Set("x-bfl-signature-timestamp", timestamp) + req.Header.Set("x-bfl-signature-nonce", nonce) + req.Header.Set("x-bfl-signature", BuildSignature(c.clientSecret, timestamp, nonce)) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return WrapError(ErrNetwork.Code, "do request failed", err) + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return WrapError(ErrResponse.Code, "read response body failed", err) + } + + if resp.StatusCode != http.StatusOK { + return WrapError(ErrResponse.Code, fmt.Sprintf("unexpected http status: %d", resp.StatusCode), nil) + } + + if err := json.Unmarshal(respBytes, respBody); err != nil { + return WrapError(ErrResponse.Code, "unmarshal response body failed", err) + } + return nil +} + +// doRequestCommon 发送请求并解析通用响应结构(code/msg/data)。 +func (c *Client) doRequestCommon(ctx context.Context, path string, reqBody any, data any) error { + var common CommonResponse + if err := c.doRequest(ctx, path, reqBody, &common); err != nil { + return err + } + if common.Code != 0 { + return WrapError(common.Code, common.Msg, nil) + } + if data != nil && common.Data != nil { + // 将 data 重新序列化后反序列化到目标结构 + dataBytes, err := json.Marshal(common.Data) + if err != nil { + return WrapError(ErrResponse.Code, "marshal data failed", err) + } + if err := json.Unmarshal(dataBytes, data); err != nil { + return WrapError(ErrResponse.Code, "unmarshal data failed", err) + } + } + return nil +} + +// CreateInvoice 提交订单开票请求(接口1)。 +// +// 请求方法:POST +// 描述:提交订单开票请求。 +func (c *Client) CreateInvoice(ctx context.Context, req *InvoiceRequest) (*InvoiceResponse, error) { + var resp InvoiceResponse + if err := c.doRequestCommon(ctx, "/invoice/create", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// QueryInvoiceStatus 查询订单开票状态(接口2)。 +// +// 请求方法:POST +// 描述:查询订单开票状态。 +func (c *Client) QueryInvoiceStatus(ctx context.Context, req *InvoiceStatusQueryRequest) (*InvoiceStatusQueryResponse, error) { + var resp InvoiceStatusQueryResponse + if err := c.doRequestCommon(ctx, "/invoice/status", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// CreatePaymentOrder 创建付款单据(接口3)。 +// +// 请求方法:POST +// 描述:创建付款单据。 +func (c *Client) CreatePaymentOrder(ctx context.Context, req *PaymentOrderRequest) (*PaymentOrderResponse, error) { + var resp PaymentOrderResponse + if err := c.doRequestCommon(ctx, "/payment/order/create", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// QueryPaymentStatus 查询支付状态(接口5)。 +// +// 请求方法:POST +// 描述:查询支付状态,响应数据与支付通知一致。 +func (c *Client) QueryPaymentStatus(ctx context.Context, req *PaymentStatusQueryRequest) (*PaymentStatusQueryResponse, error) { + var resp PaymentStatusQueryResponse + if err := c.doRequestCommon(ctx, "/payment/status", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// HandlePaymentNotify 处理支付完成通知(接口4,平台回调客户接口)。 +// +// 该方法用于客户服务端接收平台回调,解析通知数据并返回响应字符串。 +// 响应 BODY 直接返回字符串:SUCCESS 表示成功,FAILED 表示失败。 +func HandlePaymentNotify(body []byte) (*PaymentNotifyRequest, error) { + var req PaymentNotifyRequest + if err := json.Unmarshal(body, &req); err != nil { + return nil, WrapError(ErrResponse.Code, "unmarshal payment notify failed", err) + } + return &req, nil +} + +// HandleNotification 处理通用通知(bizType/bizId/data)。 +// +// 该方法用于客户服务端接收平台通用通知,解析通知数据。 +func HandleNotification(body []byte) (*Notification, error) { + var n Notification + if err := json.Unmarshal(body, &n); err != nil { + return nil, WrapError(ErrResponse.Code, "unmarshal notification failed", err) + } + return &n, nil +} +``` + +--- + +// File: intelligence_finance_v1/example_test.go +```go +package intelligence_finance_v1 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" +) + +// ExampleNewClient 展示如何创建客户端并提交订单开票请求。 +func ExampleNewClient() { + // 创建客户端 + client := NewClient( + "https://api.example.com", + "your-tenant-id", + "dd-ai-table", + "your-client-secret", + ) + + // 构建订单开票请求 + req := &InvoiceRequest{ + OrderID: "ORDER20240101001", + InvoiceType: InvoiceTypeDigitalSpecial, // 数电专票 + Purchaser: "示例科技有限公司", + TaxNum: "91110000XXXXXXXXXX", + Email: "finance@example.com", + Phone: "13800000000", + Products: []InvoiceProduct{ + { + ProductName: "软件开发服务", + RevenueCode: "1090101000000000000", + AmountIncludeTax: 11300, + Quantity: 1, + TaxSign: 1, + TaxRate: 0.13, + }, + }, + } + + resp, err := client.CreateInvoice(context.Background(), req) + if err != nil { + fmt.Printf("create invoice failed: %v\n", err) + return + } + fmt.Printf("invoice status: %d\n", resp.Status) +} + +// ExampleClient_QueryInvoiceStatus 展示如何查询开票状态。 +func ExampleClient_QueryInvoiceStatus() { + client := NewClient( + "https://api.example.com", + "your-tenant-id", + "dd-ai-table", + "your-client-secret", + ) + + resp, err := client.QueryInvoiceStatus(context.Background(), &InvoiceStatusQueryRequest{ + OrderID: "ORDER20240101001", + }) + if err != nil { + fmt.Printf("query invoice status failed: %v\n", err) + return + } + fmt.Printf("status: %d, message: %s\n", resp.Status, resp.Message) +} + +// ExampleClient_CreatePaymentOrder 展示如何创建付款单据。 +func ExampleClient_CreatePaymentOrder() { + client := NewClient( + "https://api.example.com", + "your-tenant-id", + "dd-ai-table", + "your-client-secret", + ) + + req := &PaymentOrderRequest{ + Code: "PAY20240101001", + UserID: "user-001", + Title: "供应商货款", + Amount: "11300.00", + Supplier: &Supplier{ + Name: "示例供应商有限公司", + }, + Company: &Company{ + Name: "示例科技有限公司", + }, + PaymentDetailList: []PaymentDetail{ + { + Amount: "11300.00", + InvoiceInfo: &InvoiceInfo{ + InvoiceNo: "12345678", + InvoiceCode: "011002000111", + }, + }, + }, + } + + resp, err := client.CreatePaymentOrder(context.Background(), req) + if err != nil { + fmt.Printf("create payment order failed: %v\n", err) + return + } + fmt.Printf("payment order code: %s\n", resp.Code) +} + +// ExampleClient_QueryPaymentStatus 展示如何查询支付状态。 +func ExampleClient_QueryPaymentStatus() { + client := NewClient( + "https://api.example.com", + "your-tenant-id", + "dd-ai-table", + "your-client-secret", + ) + + resp, err := client.QueryPaymentStatus(context.Background(), &PaymentStatusQueryRequest{ + Code: "PAY20240101001", + UserID: "user-001", + }) + if err != nil { + fmt.Printf("query payment status failed: %v\n", err) + return + } + fmt.Printf("payment status: %s\n", resp.PaymentStatus) +} + +// ExampleHandlePaymentNotify 展示如何在客户服务端处理支付完成通知。 +func ExampleHandlePaymentNotify() { + // 模拟平台回调的请求体 + body := []byte(`{ + "code": "PAY20240101001", + "instanceId": "inst-001", + "corpId": "corp-001", + "paymentStatus": "SUCCESS", + "paymentTime": "2024-01-01 12:00:00", + "userId": "user-001", + "amount": "11300.00", + "source": "openapi" + }`) + + notify, err := HandlePaymentNotify(body) + if err != nil { + fmt.Printf("handle payment notify failed: %v\n", err) + return + } + + // 处理业务逻辑... + + // 返回响应字符串 + if notify.PaymentStatus == PaymentStatusSuccess { + fmt.Println("SUCCESS") + } else { + fmt.Println("FAILED") + } +} + +// TestSignature 验证签名算法。 +func TestSignature(t *testing.T) { + secret := "test-secret" + timestamp := "1700000000" + nonce := "abc123" + sig := BuildSignature(secret, timestamp, nonce) + if sig == "" { + t.Fatal("signature should not be empty") + } + t.Logf("signature: %s", sig) +} + +// TestHandlePaymentNotify 测试支付通知解析。 +func TestHandlePaymentNotify(t *testing.T) { + body := []byte(`{"code":"PAY001","instanceId":"i1","corpId":"c1","paymentStatus":"SUCCESS","paymentTime":"2024-01-01 12:00:00","userId":"u1"}`) + n, err := HandlePaymentNotify(body) + if err != nil { + t.Fatalf("handle payment notify failed: %v", err) + } + if n.Code != "PAY001" { + t.Fatalf("unexpected code: %s", n.Code) + } +} + +// TestHandleNotification 测试通用通知解析。 +func TestHandleNotification(t *testing.T) { + body := []byte(`{"bizType":"invoice","bizId":"ORDER001","data":"{\"status\":3}"}`) + n, err := HandleNotification(body) + if err != nil { + t.Fatalf("handle notification failed: %v", err) + } + if n.BizType != "invoice" { + t.Fatalf("unexpected bizType: %s", n.BizType) + } +} + +// TestJSONMarshal 测试请求体 JSON 序列化。 +func TestJSONMarshal(t *testing.T) { + req := &InvoiceRequest{ + OrderID: "ORDER001", + InvoiceType: 9, + Purchaser: "测试公司", + Products: []InvoiceProduct{ + {ProductName: "服务", RevenueCode: "1090101000000000000", AmountIncludeTax: 100, Quantity: 1}, + }, + } + b, err := json.Marshal(req) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + t.Logf("request json: %s", string(b)) +} + +// TestHTTPHandler 展示如何将通知处理接入 HTTP 服务。 +func TestHTTPHandler(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body []byte + _, _ = r.Body.Read(body) + notify, err := HandlePaymentNotify(body) + if err != nil { + w.Write([]byte("FAILED")) + return + } + _ = notify + w.Write([]byte("SUCCESS")) + }) + _ = handler +} +``` + +--- + +=== SDK 生成完成 === + +## 加密实现 + +以下是从加密工具获取的完整实现: + +### 加密实现 1 + +{ + "success": true, + "data": "\n### 参数拼接规范\n\n**常见拼接方式**:\n\n1. **字典序排序**:按key字典序排序后拼接 `key=value\u0026key2=value2`\n2. **固定顺序**:按文档指定顺序拼接\n3. **JSON字符串**:整个请求体作为签名串\n\n**代码模板**:\n\n```go\npackage crypto\n\nimport (\n \"fmt\"\n \"sort\"\n \"strings\"\n)\n\n// BuildSignString 方式1:字典序排序拼接\nfunc BuildSignString(params map[string]string) string {\n keys := make([]string, 0, len(params))\n for k, v := range params {\n if v != \"\" \u0026\u0026 k != \"sign\" \u0026\u0026 k != \"signature\" {\n keys = append(keys, k)\n }\n }\n sort.Strings(keys)\n \n var parts []string\n for _, k := range keys {\n parts = append(parts, fmt.Sprintf(\"%s=%s\", k, params[k]))\n }\n return strings.Join(parts, \"\u0026\")\n}\n\n// BuildSignStringOrdered 方式2:固定顺序拼接\nfunc BuildSignStringOrdered(params map[string]string, orderedKeys []string) string {\n var parts []string\n for _, k := range orderedKeys {\n if v, ok := params[k]; ok \u0026\u0026 v != \"\" {\n parts = append(parts, fmt.Sprintf(\"%s=%s\", k, v))\n }\n }\n return strings.Join(parts, \"\u0026\")\n}\n```\n\n**注意事项**:\n- 确认文档指定的排序规则(字典序/固定顺序)\n- 确认空值是否要包含(通常排除空值)\n- 确认是否需要URL编码\n- 注意排除签名字段本身\n", + "tool": "param_concat" +} + +### 加密实现 2 + +{ + "success": true, + "data": "\n### 时间戳和随机数生成规范\n\n**代码模板**:\n\n```go\npackage crypto\n\nimport (\n \"crypto/rand\"\n \"fmt\"\n \"math/big\"\n \"time\"\n)\n\n// GenerateTimestamp 生成秒级时间戳\nfunc GenerateTimestamp() string {\n return fmt.Sprintf(\"%d\", time.Now().Unix())\n}\n\n// GenerateTimestampMillis 生成毫秒级时间戳\nfunc GenerateTimestampMillis() string {\n return fmt.Sprintf(\"%d\", time.Now().UnixMilli())\n}\n\n// GenerateNonce 生成指定长度的随机字符串(加密安全)\nfunc GenerateNonce(length int) (string, error) {\n const charset = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\"\n b := make([]byte, length)\n for i := range b {\n num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n if err != nil {\n return \"\", err\n }\n b[i] = charset[num.Int64()]\n }\n return string(b), nil\n}\n```\n\n**注意事项**:\n- 确认文档要求的是秒级还是毫秒级时间戳\n- 确认nonce的长度要求(通常16-32位)\n- 生产环境建议使用加密安全的随机数生成器\n", + "tool": "nonce_timestamp" +} +