From a79da7d031b9dadc9ec779993b6515b2a0af5d7a Mon Sep 17 00:00:00 2001 From: LarryLiu Date: Fri, 13 Oct 2023 05:50:07 -0500 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=89=AB=E5=91=97=E6=94=AF?= =?UTF-8?q?=E4=BB=98=20(#357)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 扫呗支付 --------- Co-authored-by: reed --- README.md | 1 + doc/saobei.md | 23 +++++++ saobei/account.go | 3 + saobei/api.go | 14 +++++ saobei/cert/cert.go | 9 +++ saobei/client.go | 95 ++++++++++++++++++++++++++++ saobei/client_test.go | 33 ++++++++++ saobei/consts.go | 41 +++++++++++++ saobei/error.go | 32 ++++++++++ saobei/merchant.go | 1 + saobei/model.go | 140 ++++++++++++++++++++++++++++++++++++++++++ saobei/pay.go | 111 +++++++++++++++++++++++++++++++++ saobei/pay_test.go | 115 ++++++++++++++++++++++++++++++++++ saobei/sign.go | 35 +++++++++++ 14 files changed, 653 insertions(+) create mode 100644 doc/saobei.md create mode 100644 saobei/account.go create mode 100644 saobei/api.go create mode 100644 saobei/cert/cert.go create mode 100644 saobei/client.go create mode 100644 saobei/client_test.go create mode 100644 saobei/consts.go create mode 100644 saobei/error.go create mode 100644 saobei/merchant.go create mode 100644 saobei/model.go create mode 100644 saobei/pay.go create mode 100644 saobei/pay_test.go create mode 100644 saobei/sign.go diff --git a/README.md b/README.md index 21772a5a..f1ef5220 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ func main() { * #### [拉卡拉支付](https://github.com/go-pay/gopay/blob/main/doc/lakala.md) * #### [Paypal支付](https://github.com/go-pay/gopay/blob/main/doc/paypal.md) * #### [Apple支付校验](https://github.com/go-pay/gopay/blob/main/doc/apple.md) +* #### [扫呗支付](https://github.com/go-pay/gopay/blob/main/doc/saobei.md) --- diff --git a/doc/saobei.md b/doc/saobei.md new file mode 100644 index 00000000..b1c644b4 --- /dev/null +++ b/doc/saobei.md @@ -0,0 +1,23 @@ +## 扫呗支付 API + + +- 扫呗支付:[官方文档中心](https://help.lcsw.cn/xrmpic/q6imdiojes7iq5y1/qg52lx) + + + +> 具体API使用介绍,请参考`gopay/saobei/client_test.go` + + +### 支付2.0接口 +> 请参考`gopay/saobei/pay_test.go`, +* 小程序支付接口(暂无账号为测试可用性):`client.MiniPay()` +* 付款码支付 `client.BarcodePay()` +* 支付查询 `client.Query()` +* 退款申请 `client.Refund()` +* 退款订单查询 `client.QueryRefund()` + +### 资金接口 +> 请参考`gopay/saobei/merchant_test.go`, + +### CBK企业钱包分账 +> 请参考`gopay/saobei/account_test.go`, \ No newline at end of file diff --git a/saobei/account.go b/saobei/account.go new file mode 100644 index 00000000..ee3f3663 --- /dev/null +++ b/saobei/account.go @@ -0,0 +1,3 @@ +package saobei + +//CBK企业钱包分账 diff --git a/saobei/api.go b/saobei/api.go new file mode 100644 index 00000000..dfc00169 --- /dev/null +++ b/saobei/api.go @@ -0,0 +1,14 @@ +package saobei + +const ( + // payPath 小程序支付接口 + miniPayPath = "/pay/open/minipay" + // barcodePayPath 付款码支付(扫码支付) + barcodePayPath = "/pay/open/barcodepay" + // queryPath 支付查询 + queryPath = "/pay/open/query" + // refundPath 退款申请 + refundPath = "/pay/open/refund" + // queryRefundPath 退款订单查询 + queryRefundPath = "/pay/open/queryrefund" +) diff --git a/saobei/cert/cert.go b/saobei/cert/cert.go new file mode 100644 index 00000000..ab101a0f --- /dev/null +++ b/saobei/cert/cert.go @@ -0,0 +1,9 @@ +package cert + +const ( + Key = "" // 机构秘钥,扫呗分配 + AccessToken = "8ee19b194b504b9c89b88a68b4cdf623" // 支付秘钥,扫呗分配 + InstNo = "" // 机构号,扫呗分配 + MerchantNo = "858104816000177" // 商户号,扫呗分配 + TerminalId = "44350591" // 终端号,扫呗分配 +) diff --git a/saobei/client.go b/saobei/client.go new file mode 100644 index 00000000..55d01a92 --- /dev/null +++ b/saobei/client.go @@ -0,0 +1,95 @@ +package saobei + +import ( + "context" + "crypto/md5" + "fmt" + "hash" + "sync" + + "github.com/go-pay/gopay/pkg/util" + "github.com/go-pay/gopay/pkg/xlog" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/pkg/xhttp" +) + +type Client struct { + instNo string //商户系统机构号inst_no + key string // 商户系统令牌 + merchantNo string // 支付系统:商户号 + terminalId string // 支付系统:商户号终端号 + accessToken string // 支付系统: 令牌 + isProd bool // 是否正式环境 + payVer string //版本号 当前201 + serviceId string //接口类型,当前类型015 + hc *xhttp.Client + mu sync.Mutex + md5Hash hash.Hash +} + +// NewClient 初始化扫呗客户端 +// instNo string //商户系统机构号inst_no +// key string // 商户系统令牌 +// merchantNo string // 支付系统:商户号 +// terminalId string // 支付系统:商户号终端号 +// accessToken string // 支付系统: 令牌 +// isProd:是否是正式环境 +func NewClient(instNo, key, merchantNo, terminalId, accessToken string, isProd bool) (*Client, error) { + return &Client{ + instNo: instNo, + key: key, + merchantNo: merchantNo, + terminalId: terminalId, + accessToken: accessToken, + isProd: isProd, + hc: xhttp.NewClient(), + md5Hash: md5.New(), + payVer: "201", + serviceId: "015", + }, nil +} + +// pubParamsHandle 公共参数处理 +func (c *Client) pubParamsHandle(bm gopay.BodyMap) gopay.BodyMap { + if ver := bm.GetString("pay_ver"); ver == util.NULL { + bm.Set("pay_ver", c.payVer) + } + if v := bm.GetString("service_id"); v == util.NULL { + bm.Set("service_id", c.serviceId) + } + if v := bm.GetString("merchant_no"); v == util.NULL { + bm.Set("merchant_no", c.merchantNo) + } + if v := bm.GetString("terminal_id"); v == util.NULL { + bm.Set("terminal_id", c.terminalId) + } + sign := c.getRsaSign(bm) + + bm.Set("key_sign", sign) + + return bm +} + +// doPost 发起请求 +func (c *Client) doPost(ctx context.Context, path string, bm gopay.BodyMap) (bs []byte, err error) { + param := c.pubParamsHandle(bm) + if err != nil { + return nil, err + } + + xlog.Debugf("saobeiParam:%+v", param.JsonBody()) + + url := baseUrl + if !c.isProd { + url = sandboxBaseUrl + } + res, bs, err := c.hc.Req(xhttp.TypeJSON).Post(url + path).SendBodyMap(param).EndBytes(ctx) + if err != nil { + return nil, err + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("HTTP Request Error, StatusCode = %d", res.StatusCode) + } + return bs, nil +} diff --git a/saobei/client_test.go b/saobei/client_test.go new file mode 100644 index 00000000..2f605dd6 --- /dev/null +++ b/saobei/client_test.go @@ -0,0 +1,33 @@ +package saobei + +import ( + "context" + "os" + "testing" + + "github.com/go-pay/gopay/pkg/xlog" + "github.com/go-pay/gopay/saobei/cert" +) + +var ( + ctx = context.Background() + client *Client + err error +) + +func TestMain(m *testing.M) { + // 初始化通联客户端 + // instNo string //商户系统机构号inst_no + // key string // 商户系统令牌 + // merchantNo string // 支付系统:商户号 + // terminalId string // 支付系统:商户号终端号 + // accessToken string // 支付系统: 令牌 + // isProd:是否是正式环境 + client, err = NewClient(cert.InstNo, cert.Key, cert.MerchantNo, cert.TerminalId, cert.AccessToken, false) + if err != nil { + xlog.Error(err) + return + } + + os.Exit(m.Run()) +} diff --git a/saobei/consts.go b/saobei/consts.go new file mode 100644 index 00000000..fa381292 --- /dev/null +++ b/saobei/consts.go @@ -0,0 +1,41 @@ +package saobei + +const ( + baseUrl = "https://pay.lcsw.cn/lcsw" + sandboxBaseUrl = "http://test2.lcsw.cn:8117/lcsw" + + //PayTypeWX 支付方式:微信 + PayTypeWX = "010" + //PayTypeAli 支付方式:支付宝 + PayTypeAli = "020" + //PayTypeQQ 支付方式:QQ钱包 + PayTypeQQ = "060" + //PayTypeYi 支付方式:翼支付 + PayTypeYi = "100" + //PayTypeYL 支付方式:银联二维码 + PayTypeYL = "110" + + //ResultCodeSuccess 业务结果:01 成功 + ResultCodeSuccess = "01" + //ResultCodeFail 业务结果:02 失败 + ResultCodeFail = "02" + //ResultCodePaying 业务结果:03 支付中 + ResultCodePaying = "03" + + //TradeStatusSuccess 交易订单状态:支付成功 + TradeStatusSuccess = "SUCCESS" + //TradeStatusRefund 交易订单状态:转入退款 + TradeStatusRefund = "REFUND" + //TradeStatusNotPay 交易订单状态:未支付 + TradeStatusNotPay = "NOTPAY" + //TradeStatusClosed 交易订单状态:已关闭 + TradeStatusClosed = "CLOSED" + //TradeStatusUserPaying 交易订单状态:用户支付中 + TradeStatusUserPaying = "USERPAYING" + //TradeStatusRevoked 交易订单状态:已撤销 + TradeStatusRevoked = "REVOKED" + //TradeStatusNoPay 交易订单状态:未支付支付超时 + TradeStatusNoPay = "NOPAY" + //TradeStatusPayError 交易订单状态:支付失败 + TradeStatusPayError = "PAYERROR" +) diff --git a/saobei/error.go b/saobei/error.go new file mode 100644 index 00000000..6fc7420c --- /dev/null +++ b/saobei/error.go @@ -0,0 +1,32 @@ +package saobei + +import ( + "fmt" +) + +// BizErr 用于判断通联的业务逻辑是否有错误 +type BizErr struct { + Code string `json:"code"` + Msg string `json:"msg"` +} + +// bizErrCheck 检查返回码是否为SUCCESS 否则返回一个BizErr +func bizErrCheck(resp RspBase) error { + if resp.ReturnCode != "01" { + return &BizErr{ + Code: resp.ReturnCode, + Msg: resp.ReturnMsg, + } + } + //if resp.ResultCode != "01" { + // return &BizErr{ + // Code: resp.ResultCode, + // Msg: resp.ReturnMsg, + // } + //} + return nil +} + +func (e *BizErr) Error() string { + return fmt.Sprintf(`[%s]%s`, e.Code, e.Msg) +} diff --git a/saobei/merchant.go b/saobei/merchant.go new file mode 100644 index 00000000..5307f025 --- /dev/null +++ b/saobei/merchant.go @@ -0,0 +1 @@ +package saobei diff --git a/saobei/model.go b/saobei/model.go new file mode 100644 index 00000000..f5785b88 --- /dev/null +++ b/saobei/model.go @@ -0,0 +1,140 @@ +package saobei + +type RspBase struct { + ReturnCode string `json:"return_code"` //响应码:01成功 ,02失败,响应码仅代表通信状态,不代表业务结果 + ReturnMsg string `json:"return_msg"` //返回信息提示,“退款成功”、“订单不存在”等 + KeySign string `json:"key_sign"` //签名串《2.4签名算法》 签名测试页 + ResultCode string `json:"result_code"` //业务结果:01成功 ,02失败 +} + +// MiniPayRsp 小程序支付响应 +type MiniPayRsp struct { + RspBase + PayType string `json:"pay_type"` //支付方式,010微信,020支付宝 + MerchantName string `json:"merchant_name"` //商户名称 + MerchantNo string `json:"merchant_no"` //商户号 + TerminalId string `json:"terminal_id"` //终端号 + DeviceNo string `json:"device_no"` //商户终端设备号(商户自定义,如门店编号),必须在平台已配置过 + TerminalTrace string `json:"terminal_trace"` //终端流水号,商户系统的订单号,系统原样返回 + TerminalTime string `json:"terminal_time"` //终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 + + TotalFee string `json:"total_fee"` //金额,单位分 + OutTradeNo string `json:"out_trade_no"` //平台唯一订单号 + AppId string `json:"appId"` //微信小程序支付返回字段,公众号id + TimeStamp string `json:"timeStamp"` //微信小程序支付返回字段,时间戳,示例:1414561699,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。 + NonceStr string `json:"nonceStr"` //微信小程序支付返回字段,随机字符串 + PackageStr string `json:"package_str"` //微信小程序支付返回字段,订单详情扩展字符串,示例:prepay_id=123456789,统一下单接口返回的prepay_id参数值,提交格式如:prepay_id= + SignType string `json:"signType"` //微信小程序支付返回字段,签名方式,示例:MD5,RSA + PaySign string `json:"paySign"` //微信小程序支付返回字段,签名 + AliTradeNo string `json:"ali_trade_no"` //支付宝小程序支付返回字段用于调起支付宝小程序 +} + +// BarcodePayRsp 付款码支付(扫码支付)响应 +type BarcodePayRsp struct { + RspBase + PayType string `json:"pay_type"` //支付方式,010微信,020支付宝 + MerchantName string `json:"merchant_name"` //商户名称 + MerchantNo string `json:"merchant_no"` //商户号 + TerminalId string `json:"terminal_id"` //终端号 + DeviceNo string `json:"device_no"` //商户终端设备号(商户自定义,如门店编号),必须在平台已配置过 + TerminalTrace string `json:"terminal_trace"` //终端流水号,商户系统的订单号,系统原样返回 + TerminalTime string `json:"terminal_time"` //终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 + + TotalFee string `json:"total_fee"` //金额,单位分 + BuyerPayFee string `json:"buyer_pay_fee"` // 买家实付金额(分)pay_ver为202时返回 + PlatformDiscountFee string `json:"platform_discount_fee"` // 平台优惠金额(分)pay_ver为202时返回 + MerchantDiscountFee string `json:"merchant_discount_fee"` // 商家优惠金额(分)pay_ver为202时返回 + EndTime string `json:"end_time"` // 支付完成时间,yyyyMMddHHmmss,全局统一时间格式 + OutTradeNo string `json:"out_trade_no"` //平台唯一订单号 + ChannelTradeNo string `json:"channel_trade_no"` //通道订单号,微信订单号、支付宝订单号等 + ChannelOrderNo string `json:"channel_order_no"` //银行渠道订单号,微信支付时显示在支付成功页面的条码,可用作扫码查询和扫码退款时匹配 + UserId string `json:"user_id"` //付款方用户id,“微信openid”、“支付宝账户” + Attach string `json:"attach"` //附加数据,原样返回 + ReceiptFee string `json:"receipt_fee"` //商家应结算金额,单位分 + BankType string `json:"bank_type"` //银行类型,采用字符串类型的银行标识 + PromotionDetail string `json:"promotion_detail"` //官方营销详情,pay_ver=202时返回. 本交易支付时使用的所有优惠券信息 ,单品优惠功能字段,详情见 + OrderBody string `json:"order_body"` //订单标题描述 + SubOpenid string `json:"sub_openid"` //微信子商户sub_appid对应的用户标识 +} + +// QueryRsp 支付查询 +type QueryRsp struct { + RspBase + PayType string `json:"pay_type"` //支付方式,010微信,020支付宝 + MerchantName string `json:"merchant_name"` //商户名称 + MerchantNo string `json:"merchant_no"` //商户号 + TerminalId string `json:"terminal_id"` //终端号 + DeviceNo string `json:"device_no"` //商户终端设备号(商户自定义,如门店编号),必须在平台已配置过 + TerminalTrace string `json:"terminal_trace"` //终端流水号,商户系统的订单号,系统原样返回 + TerminalTime string `json:"terminal_time"` //终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 + + TotalFee string `json:"total_fee"` //金额,单位分 + BuyerPayFee string `json:"buyer_pay_fee"` //买家实付金额(分)pay_ver为202时返回 + PlatformDiscountFee string `json:"platform_discount_fee"` //平台优惠金额(分)pay_ver为202时返回 + MerchantDiscountFee string `json:"merchant_discount_fee"` //商家优惠金额(分)pay_ver为202时返回 + SubOpenid string `json:"sub_openid"` //微信子商户sub_appid对应的用户标识 + OrderBody string `json:"order_body"` //订单标题描述 + EndTime string `json:"end_time"` //支付完成时间,yyyyMMddHHmmss,全局统一时间格式 + OutTradeNo string `json:"out_trade_no"` //平台唯一订单号 + TradeState string `json:"trade_state"` //交易订单状态,SUCCESS支付成功,REFUND转入退款,NOTPAY未支付,CLOSED已关闭,USERPAYING用户支付中,REVOKED已撤销,NOPAY未支付支付超时,PAYERROR支付失败 + ChannelTradeNo string `json:"channel_trade_no"` //通道订单号,微信订单号、支付宝订单号等 + ChannelOrderNo string `json:"channel_order_no"` //银行渠道订单号,微信支付时显示在支付成功页面的条码,可用作扫码查询和扫码退款时匹配 + UserId string `json:"user_id"` //付款方用户id,“微信openid”、“支付宝账户” + Attach string `json:"attach"` //附加数据,原样返回 + ReceiptFee string `json:"receipt_fee"` //商家应结算金额,单位分 + PayTrace string `json:"pay_trace"` //当前支付终端流水号 + PayTime string `json:"pay_time"` //当前支付终端交易时间,yyyyMMddHHmmss,全局统一时间格式 + BankType string `json:"bank_type"` //银行类型,采用字符串类型的银行标识 + PromotionDetail string `json:"promotion_detail"` //官方营销详情,pay_ver=202时返回. 本交易支付时使用的所有优惠券信息 ,单品优惠功能字段,详情见 +} + +// RefundRsp 退款申请 +type RefundRsp struct { + RspBase + PayType string `json:"pay_type"` //支付方式,010微信,020支付宝 + MerchantName string `json:"merchant_name"` //商户名称 + MerchantNo string `json:"merchant_no"` //商户号 + TerminalId string `json:"terminal_id"` //终端号 + DeviceNo string `json:"device_no"` //商户终端设备号(商户自定义,如门店编号),必须在平台已配置过 + TerminalTrace string `json:"terminal_trace"` //终端流水号,商户系统的订单号,系统原样返回 + TerminalTime string `json:"terminal_time"` //终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 + + RefundFee string `json:"refund_fee"` //退款金额,单位分 + RefundReceiptFee string `json:"refund_receipt_fee"` //退商家应结算金额,单位分 + RefundBuyerPayFee string `json:"refund_buyer_pay_fee"` //退买家实付金额(分) + RefundPlatformDiscountFee string `json:"refund_platform_discount_fee"` //退平台优惠金额(分) + RefundMerchantDiscountFee string `json:"refund_merchant_discount_fee"` //退商家优惠金额(分) + RefundPromotionDetail string `json:"refund_promotion_detail"` //退优惠明细,详情见 + EndTime string `json:"end_time"` //退款完成时间,yyyyMMddHHmmss,全局统一时间格式 + OutTradeNo string `json:"out_trade_no"` //平台唯一订单号 + OutRefundNo string `json:"out_refund_no"` //平台唯一退款单号 +} + +// QueryRefundRsp 退款订单查询 +type QueryRefundRsp struct { + RspBase + PayType string `json:"pay_type"` //支付方式,010微信,020支付宝 + MerchantName string `json:"merchant_name"` //商户名称 + MerchantNo string `json:"merchant_no"` //商户号 + TerminalId string `json:"terminal_id"` //终端号 + DeviceNo string `json:"device_no"` //商户终端设备号(商户自定义,如门店编号),必须在平台已配置过 + TerminalTrace string `json:"terminal_trace"` //终端流水号,商户系统的订单号,系统原样返回 + TerminalTime string `json:"terminal_time"` //终端交易时间,yyyyMMddHHmmss,全局统一时间格式,系统原样返回 + + RefundFee string `json:"refund_fee"` //退款金额,单位分 + RefundReceiptFee string `json:"refund_receipt_fee"` //退商家应结算金额,单位分 + RefundBuyerPayFee string `json:"refund_buyer_pay_fee"` //退买家实付金额(分) + RefundPlatformDiscountFee string `json:"refund_platform_discount_fee"` //退平台优惠金额(分) + RefundMerchantDiscountFee string `json:"refund_merchant_discount_fee"` //退商家优惠金额(分) + RefundPromotionDetail string `json:"refund_promotion_detail"` //退优惠明细,详情见 + EndTime string `json:"end_time"` //退款完成时间,yyyyMMddHHmmss,全局统一时间格式 + OutRefundNo string `json:"out_refund_no"` //平台唯一退款单号 + OutTradeNo string `json:"out_trade_no"` //平台唯一订单号 + TradeState string `json:"trade_state"` //交易订单状态,SUCCESS支付成功,REFUND转入退款,NOTPAY未支付,CLOSED已关闭,USERPAYING用户支付中,REVOKED已撤销,NOPAY未支付支付超时,PAYERROR支付失败 + ChannelTradeNo string `json:"channel_trade_no"` //通道订单号,微信订单号、支付宝订单号等 + ChannelOrderNo string `json:"channel_order_no"` //银行渠道订单号,微信支付时显示在支付成功页面的条码,可用作扫码查询和扫码退款时匹配 + UserId string `json:"user_id"` //退款方用户id,“微信openid”、“支付宝账户”、“qq号”等 + Attach string `json:"attach"` //附加数据,原样返回 + PayTrace string `json:"pay_trace"` //退款终端流水号 + PayTime string `json:"pay_time"` //退款终端交易时间 +} diff --git a/saobei/pay.go b/saobei/pay.go new file mode 100644 index 00000000..6e2c85a8 --- /dev/null +++ b/saobei/pay.go @@ -0,0 +1,111 @@ +package saobei + +// 支付2.0接口 + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/go-pay/gopay" +) + +// MiniPay 小程序支付接口 https://help.lcsw.cn/xrmpic/tisnldchblgxohfl/rinsc3#title-node17 +func (c *Client) MiniPay(ctx context.Context, bm gopay.BodyMap) (rsp *MiniPayRsp, err error) { + err = bm.CheckEmptyError("pay_type", "terminal_ip", "terminal_trace", "terminal_time", "total_fee", "sub_appid", "open_id") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = c.doPost(ctx, miniPayPath, bm); err != nil { + return nil, err + } + rsp = new(MiniPayRsp) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) + } + if err := bizErrCheck(rsp.RspBase); err != nil { + return nil, err + } + return rsp, c.verifySign(bs) +} + +// BarcodePay 付款码支付(扫码支付) https://help.lcsw.cn/xrmpic/tisnldchblgxohfl/rinsc3#title-node14 +func (c *Client) BarcodePay(ctx context.Context, bm gopay.BodyMap) (rsp *BarcodePayRsp, err error) { + err = bm.CheckEmptyError("pay_type", "terminal_ip", "terminal_trace", "terminal_time", "total_fee", "auth_no") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = c.doPost(ctx, barcodePayPath, bm); err != nil { + return nil, err + } + rsp = new(BarcodePayRsp) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) + } + if err := bizErrCheck(rsp.RspBase); err != nil { + return nil, err + } + return rsp, nil +} + +// Query 支付查询 https://help.lcsw.cn/xrmpic/tisnldchblgxohfl/rinsc3#title-node18 +func (c *Client) Query(ctx context.Context, bm gopay.BodyMap) (rsp *QueryRsp, err error) { + err = bm.CheckEmptyError("pay_type", "terminal_trace", "terminal_time") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = c.doPost(ctx, queryPath, bm); err != nil { + return nil, err + } + rsp = new(QueryRsp) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) + } + if err := bizErrCheck(rsp.RspBase); err != nil { + return nil, err + } + return rsp, c.verifySign(bs) +} + +// Refund 退款申请 https://help.lcsw.cn/xrmpic/tisnldchblgxohfl/rinsc3#title-node19 +func (c *Client) Refund(ctx context.Context, bm gopay.BodyMap) (rsp *RefundRsp, err error) { + err = bm.CheckEmptyError("pay_type", "terminal_trace", "terminal_time", "refund_fee") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = c.doPost(ctx, refundPath, bm); err != nil { + return nil, err + } + rsp = new(RefundRsp) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) + } + if err := bizErrCheck(rsp.RspBase); err != nil { + return nil, err + } + return rsp, c.verifySign(bs) +} + +// QueryRefund 退款订单查询 https://help.lcsw.cn/xrmpic/tisnldchblgxohfl/rinsc3#title-node22 +func (c *Client) QueryRefund(ctx context.Context, bm gopay.BodyMap) (rsp *QueryRefundRsp, err error) { + err = bm.CheckEmptyError("pay_type", "terminal_trace", "terminal_time") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = c.doPost(ctx, queryRefundPath, bm); err != nil { + return nil, err + } + rsp = new(QueryRefundRsp) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("[%w], bytes: %s", gopay.UnmarshalErr, string(bs)) + } + if err := bizErrCheck(rsp.RspBase); err != nil { + return nil, err + } + return rsp, c.verifySign(bs) +} diff --git a/saobei/pay_test.go b/saobei/pay_test.go new file mode 100644 index 00000000..d3502bb9 --- /dev/null +++ b/saobei/pay_test.go @@ -0,0 +1,115 @@ +package saobei + +import ( + "testing" + "time" + + "github.com/go-pay/gopay" + "github.com/go-pay/gopay/pkg/xlog" +) + +// 小程序支付接口 +func TestClient_MiniPay(t *testing.T) { + // 请求参数 + bm := make(gopay.BodyMap) + bm.Set("pay_type", "010"). + Set("terminal_ip", "127.0.0.1"). + Set("terminal_trace", "larry01"). + Set("terminal_time", time.Now().Format("20060102150405")). + Set("total_fee", "1"). + Set("sub_appid", "wx91b9fee6ce0135c9"). + Set("open_id", "oXJQK5paQaKRhgrXm_ZzF_8azJj0") + + resp, err := client.MiniPay(ctx, bm) + xlog.Debugf("saobeiRsp:%+v", resp) + if err != nil { + xlog.Errorf("%+v", err) + return + } +} + +// 付款码支付(扫码支付) +func TestClient_BarcodePay(t *testing.T) { + + terminalTrace := "larry02456" // 终端流水号,填写商户系统的支付订单号,不可重复 + terminalTime := time.Now().Format("20060102150405") // 终端交易时间,yyyyMMddHHmmss,全局统一时间格式 + + // 请求参数 + bm := make(gopay.BodyMap) + bm.Set("pay_type", "010"). + Set("terminal_ip", "127.0.0.1"). + Set("terminal_trace", terminalTrace). + Set("terminal_time", terminalTime). + Set("total_fee", "1"). + Set("auth_no", "132038911197761804") + + resp, err := client.BarcodePay(ctx, bm) + xlog.Debugf("saobeiRsp:%+v", resp) + if err != nil { + xlog.Errorf("%+v", err) + return + } + + xlog.Debugf("terminal_trace:%s", terminalTrace) + xlog.Debugf("terminal_time:%s", terminalTime) +} + +// 支付查询 +func TestClient_Query(t *testing.T) { + // out_trade_no 和 pay_trace|pay_time 两种方式二选一 + + // 请求参数 + bm := make(gopay.BodyMap) + bm.Set("pay_type", "010"). + Set("terminal_trace", "larry02456"). + Set("terminal_time", "20231008133303"). + Set("out_trade_no", "443505910021123100813330300001") + //Set("pay_trace", "larry02456"). // terminal_trace + //Set("pay_time", "20231008133303") // terminal_time + + resp, err := client.Query(ctx, bm) + xlog.Debugf("saobeiRsp:%+v", resp) + if err != nil { + xlog.Errorf("%+v", err) + return + } +} + +// 退款申请 +func TestClient_Refund(t *testing.T) { + // out_trade_no 和 pay_trace|pay_time 两种方式二选一 + + // 请求参数 + bm := make(gopay.BodyMap) + bm.Set("pay_type", "010"). + Set("terminal_trace", "larry02456"). + Set("terminal_time", "20231008133303"). + Set("refund_fee", "1"). + Set("out_trade_no", "443505910021123100813330300001") + //Set("pay_trace", "larry02456"). // terminal_trace + //Set("pay_time", "20231008133303") // terminal_time + + resp, err := client.Refund(ctx, bm) + xlog.Debugf("saobeiRsp:%+v", resp) + if err != nil { + xlog.Errorf("%+v", err) + return + } +} + +// 退款订单查询 +func TestClient_QueryRefund(t *testing.T) { + // 请求参数 + bm := make(gopay.BodyMap) + bm.Set("pay_type", "010"). + Set("terminal_trace", "larry02456"). + Set("terminal_time", "20231008133303"). + Set("out_refund_no", "1111111") + + resp, err := client.QueryRefund(ctx, bm) + xlog.Debugf("saobeiRsp:%+v", resp) + if err != nil { + xlog.Errorf("%+v", err) + return + } +} diff --git a/saobei/sign.go b/saobei/sign.go new file mode 100644 index 00000000..1d7df6de --- /dev/null +++ b/saobei/sign.go @@ -0,0 +1,35 @@ +package saobei + +import ( + "encoding/json" + "fmt" + + "github.com/go-pay/gopay" +) + +// getRsaSign 获取签名字符串 +func (c *Client) getRsaSign(bm gopay.BodyMap) (sign string) { + signParams := bm.EncodeAliPaySignParams() + c.mu.Lock() + defer func() { + c.md5Hash.Reset() + c.mu.Unlock() + }() + c.md5Hash.Write([]byte(signParams + "&access_token=" + c.accessToken)) + return fmt.Sprintf("%x", c.md5Hash.Sum(nil)) +} + +// verifySign 验证响应签名 +func (c *Client) verifySign(bs []byte) (err error) { + bm := gopay.BodyMap{} + if err = json.Unmarshal(bs, &bm); err != nil { + return err + } + sign := bm.Get("key_sign") + bm.Remove("key_sign") + s := c.getRsaSign(bm) + if s != sign { + return fmt.Errorf("[%w]: %v", gopay.VerifySignatureErr, "验签失败") + } + return nil +}