diff --git a/README.md b/README.md index 142031d4..d4aa2553 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,10 @@ client.SetLocation(). // 设置时区,不设置或出错 SetAppAuthToken(). // 设置第三方应用授权 SetAuthToken() // 设置个人信息授权 +// 证书路径 err := client.SetCertSnByPath("appCertPublicKey.crt", "alipayRootCert.crt", "alipayCertPublicKey_RSA2.crt") +// 证书内容 +err := client.SetCertSnByContent("appCertPublicKey bytes", "alipayRootCert bytes", "alipayCertPublicKey_RSA2 bytes") ``` ## 2、初始化并赋值BodyMap(client的方法所需的入参) @@ -548,12 +551,15 @@ import ( aliRsp, err := client.TradePay(bm) // 支付宝同步返回验签 // 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 -// aliPayPublicKey:支付宝公钥 +// aliPayPublicKey:支付宝平台获取的支付宝公钥 // signData:待验签参数,aliRsp.SignData // sign:待验签sign,aliRsp.Sign // 返回参数ok:是否验签通过 // 返回参数err:错误信息 ok, err := alipay.VerifySyncSign(aliPayPublicKey, aliRsp.SignData, aliRsp.Sign) +// aliPayPublicKeyCert:支付宝公钥证书存放路径 alipayCertPublicKey_RSA2.crt 或文件内容[]byte +ok, err := alipay.VerifySyncSignWithCert(aliPayPublicKeyCert, aliRsp.SignData, aliRsp.Sign) + // ====异步通知参数解析和验签Sign==== // 解析异步通知的参数 diff --git a/alipay/client.go b/alipay/client.go index 11236023..da733702 100644 --- a/alipay/client.go +++ b/alipay/client.go @@ -2,7 +2,6 @@ package alipay import ( "encoding/json" - "errors" "fmt" "strings" "sync" @@ -61,628 +60,6 @@ func (a *Client) PostAliPayAPISelf(bm gopay.BodyMap, method string, aliRsp inter return nil } -// alipay.trade.fastpay.refund.query(统一收单交易退款查询) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.fastpay.refund.query -func (a *Client) TradeFastPayRefundQuery(bm gopay.BodyMap) (aliRsp *TradeFastpayRefundQueryResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - err = bm.CheckEmptyError("out_request_no") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.fastpay.refund.query"); err != nil { - return nil, err - } - aliRsp = new(TradeFastpayRefundQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.order.settle(统一收单交易结算接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.order.settle -func (a *Client) TradeOrderSettle(bm gopay.BodyMap) (aliRsp *TradeOrderSettleResponse, err error) { - err = bm.CheckEmptyError("out_request_no", "trade_no", "royalty_parameters") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.order.settle"); err != nil { - return nil, err - } - aliRsp = new(TradeOrderSettleResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.create(统一收单交易创建接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.create -func (a *Client) TradeCreate(bm gopay.BodyMap) (aliRsp *TradeCreateResponse, err error) { - err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.create"); err != nil { - return nil, err - } - aliRsp = new(TradeCreateResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.close(统一收单交易关闭接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.close -func (a *Client) TradeClose(bm gopay.BodyMap) (aliRsp *TradeCloseResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.close"); err != nil { - return nil, err - } - aliRsp = new(TradeCloseResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.cancel(统一收单交易撤销接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.cancel -func (a *Client) TradeCancel(bm gopay.BodyMap) (aliRsp *TradeCancelResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.cancel"); err != nil { - return nil, err - } - aliRsp = new(TradeCancelResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.refund(统一收单交易退款接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.refund -func (a *Client) TradeRefund(bm gopay.BodyMap) (aliRsp *TradeRefundResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - err = bm.CheckEmptyError("refund_amount") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.refund"); err != nil { - return nil, err - } - aliRsp = new(TradeRefundResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.page.refund(统一收单退款页面接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.page.refund -func (a *Client) TradePageRefund(bm gopay.BodyMap) (aliRsp *TradePageRefundResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - err = bm.CheckEmptyError("out_request_no", "refund_amount") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.page.refund"); err != nil { - return nil, err - } - aliRsp = new(TradePageRefundResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.precreate(统一收单线下交易预创建) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.precreate -func (a *Client) TradePrecreate(bm gopay.BodyMap) (aliRsp *TradePrecreateResponse, err error) { - err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.precreate"); err != nil { - return nil, err - } - aliRsp = new(TradePrecreateResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - if aliRsp.NullResponse != nil { - info := aliRsp.NullResponse - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.pay(统一收单交易支付接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.pay -func (a *Client) TradePay(bm gopay.BodyMap) (aliRsp *TradePayResponse, err error) { - err = bm.CheckEmptyError("out_trade_no", "scene", "auth_code", "subject") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.pay"); err != nil { - return nil, err - } - aliRsp = new(TradePayResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.query(统一收单线下交易查询) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.query -func (a *Client) TradeQuery(bm gopay.BodyMap) (aliRsp *TradeQueryResponse, err error) { - if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { - return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.query"); err != nil { - return nil, err - } - aliRsp = new(TradeQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.app.pay(app支付接口2.0) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.app.pay -func (a *Client) TradeAppPay(bm gopay.BodyMap) (payParam string, err error) { - err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") - if err != nil { - return util.NULL, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.app.pay"); err != nil { - return util.NULL, err - } - payParam = string(bs) - return payParam, nil -} - -// alipay.trade.wap.pay(手机网站支付接口2.0) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay -func (a *Client) TradeWapPay(bm gopay.BodyMap) (payUrl string, err error) { - bm.Set("product_code", "QUICK_WAP_WAY") - err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") - if err != nil { - return util.NULL, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.wap.pay"); err != nil { - return util.NULL, err - } - payUrl = string(bs) - return payUrl, nil -} - -// alipay.trade.page.pay(统一收单下单并支付页面接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay -func (a *Client) TradePagePay(bm gopay.BodyMap) (payUrl string, err error) { - bm.Set("product_code", "FAST_INSTANT_TRADE_PAY") - err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") - if err != nil { - return util.NULL, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.trade.page.pay"); err != nil { - return util.NULL, err - } - payUrl = string(bs) - return payUrl, nil -} - -// alipay.fund.trans.toaccount.transfer(单笔转账到支付宝账户接口) -// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.toaccount.transfer -// 注意:此接口官方以升级替换为 alipay.fund.trans.uni.transfer -func (a *Client) FundTransToaccountTransfer(bm gopay.BodyMap) (aliRsp *FundTransToaccountTransferResponse, err error) { - if bm.Get("out_biz_no") == util.NULL { - return nil, errors.New("out_biz_no is not allowed to be null") - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.fund.trans.toaccount.transfer"); err != nil { - return - } - aliRsp = new(FundTransToaccountTransferResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.fund.trans.uni.transfer(单笔转账接口) -// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer -func (a *Client) FundTransUniTransfer(bm gopay.BodyMap) (aliRsp *FundTransUniTransferResponse, err error) { - err = bm.CheckEmptyError("out_biz_no", "trans_amount", "product_code", "payee_info") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.fund.trans.uni.transfer"); err != nil { - return nil, err - } - aliRsp = new(FundTransUniTransferResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.fund.trans.common.query(转账业务单据查询接口) -// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.common.query -func (a *Client) FundTransCommonQuery(bm gopay.BodyMap) (aliRsp *FundTransCommonQueryResponse, err error) { - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.fund.trans.common.query"); err != nil { - return nil, err - } - aliRsp = new(FundTransCommonQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.fund.account.query(支付宝资金账户资产查询接口) -// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.account.query -func (a *Client) FundAccountQuery(bm gopay.BodyMap) (aliRsp *FundAccountQueryResponse, err error) { - err = bm.CheckEmptyError("alipay_user_id") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.fund.account.query"); err != nil { - return nil, err - } - aliRsp = new(FundAccountQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.trade.orderinfo.sync(支付宝订单信息同步接口) -// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.orderinfo.sync -func (a *Client) TradeOrderinfoSync(body gopay.BodyMap) { - -} - -// alipay.system.oauth.token(换取授权访问令牌) -// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token -func (a *Client) SystemOauthToken(bm gopay.BodyMap) (aliRsp *SystemOauthTokenResponse, err error) { - if bm.Get("code") == util.NULL && bm.Get("refresh_token") == util.NULL { - return nil, errors.New("code and refresh_token are not allowed to be null at the same time") - } - err = bm.CheckEmptyError("grant_type") - if err != nil { - return nil, err - } - - if a.AppCertSN != util.NULL { - a.mu.RLock() - bm.Set("app_cert_sn", a.AppCertSN) - a.mu.RUnlock() - } - if a.AliPayRootCertSN != util.NULL { - a.mu.RLock() - bm.Set("alipay_root_cert_sn", a.AliPayRootCertSN) - a.mu.RUnlock() - } - - var bs []byte - if bs, err = systemOauthToken(a.AppId, a.PrivateKeyType, a.PrivateKey, bm, "alipay.system.oauth.token", a.IsProd, a.SignType); err != nil { - return nil, err - } - aliRsp = new(SystemOauthTokenResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.ErrorResponse != nil { - info := aliRsp.ErrorResponse - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.user.info.share(支付宝会员授权信息查询接口) -// body:此接口无需body参数 -// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.info.share -func (a *Client) UserInfoShare() (aliRsp *UserInfoShareResponse, err error) { - var bs []byte - if bs, err = a.doAliPay(nil, "alipay.user.info.share"); err != nil { - return nil, err - } - aliRsp = new(UserInfoShareResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.open.auth.token.app(换取应用授权令牌) -// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.open.auth.token.app -func (a *Client) OpenAuthTokenApp(bm gopay.BodyMap) (aliRsp *OpenAuthTokenAppResponse, err error) { - if bm.Get("code") == util.NULL && bm.Get("refresh_token") == util.NULL { - return nil, errors.New("code and refresh_token are not allowed to be null at the same time") - } - err = bm.CheckEmptyError("grant_type") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.open.auth.token.app"); err != nil { - return nil, err - } - aliRsp = new(OpenAuthTokenAppResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// zhima.credit.score.get(芝麻分) -// 文档地址:https://opendocs.alipay.com/apis/api_8/zhima.credit.score.get -func (a *Client) ZhimaCreditScoreGet(bm gopay.BodyMap) (aliRsp *ZhimaCreditScoreGetResponse, err error) { - if bm.Get("product_code") == util.NULL { - bm.Set("product_code", "w1010100100000000001") - } - err = bm.CheckEmptyError("transaction_id") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "zhima.credit.score.get"); err != nil { - return nil, err - } - aliRsp = new(ZhimaCreditScoreGetResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.user.certify.open.initialize(身份认证初始化服务) -// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.initialize -func (a *Client) UserCertifyOpenInit(bm gopay.BodyMap) (aliRsp *UserCertifyOpenInitResponse, err error) { - err = bm.CheckEmptyError("outer_order_no", "biz_code", "identity_param", "merchant_config") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.user.certify.open.initialize"); err != nil { - return nil, err - } - aliRsp = new(UserCertifyOpenInitResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.user.certify.open.certify(身份认证开始认证) -// API文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.certify -// 产品文档地址:https://opendocs.alipay.com/open/20181012100420932508/quickstart -func (a *Client) UserCertifyOpenCertify(bm gopay.BodyMap) (certifyUrl string, err error) { - err = bm.CheckEmptyError("certify_id") - if err != nil { - return util.NULL, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.user.certify.open.certify"); err != nil { - return util.NULL, err - } - certifyUrl = string(bs) - return certifyUrl, nil -} - -// alipay.user.certify.open.query(身份认证记录查询) -// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.query -func (a *Client) UserCertifyOpenQuery(bm gopay.BodyMap) (aliRsp *UserCertifyOpenQueryResponse, err error) { - err = bm.CheckEmptyError("certify_id") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.user.certify.open.query"); err != nil { - return nil, err - } - aliRsp = new(UserCertifyOpenQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.user.info.auth(用户登陆授权) -// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.user.info.auth -func (a *Client) UserInfoAuth(bm gopay.BodyMap) (aliRsp *UserInfoAuthResponse, err error) { - err = bm.CheckEmptyError("scopes", "state") - if err != nil { - return nil, err - } - - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.user.info.auth"); err != nil { - return nil, err - } - if strings.Contains(string(bs), "") { - return nil, errors.New(string(bs)) - } - aliRsp = new(UserInfoAuthResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.data.bill.balance.query(支付宝商家账户当前余额查询) -// 文档地址:https://opendocs.alipay.com/apis/api_15/alipay.data.bill.balance.query -func (a *Client) DataBillBalanceQuery(bm gopay.BodyMap) (aliRsp *DataBillBalanceQueryResponse, err error) { - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.data.bill.balance.query"); err != nil { - return nil, err - } - aliRsp = new(DataBillBalanceQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - -// alipay.data.dataservice.bill.downloadurl.query(查询对账单下载地址) -// 文档地址:https://opendocs.alipay.com/apis/api_15/alipay.data.dataservice.bill.downloadurl.query -func (a *Client) DataBillDownloadUrlQuery(bm gopay.BodyMap) (aliRsp *DataBillDownloadUrlQueryResponse, err error) { - err = bm.CheckEmptyError("bill_type", "bill_date") - if err != nil { - return nil, err - } - var bs []byte - if bs, err = a.doAliPay(bm, "alipay.data.dataservice.bill.downloadurl.query"); err != nil { - return nil, err - } - aliRsp = new(DataBillDownloadUrlQueryResponse) - if err = json.Unmarshal(bs, aliRsp); err != nil { - return nil, err - } - if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { - info := aliRsp.Response - return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) - } - aliRsp.SignData = getSignData(bs) - return aliRsp, nil -} - // 向支付宝发送请求 func (a *Client) doAliPay(bm gopay.BodyMap, method string) (bs []byte, err error) { var ( diff --git a/alipay/client_test.go b/alipay/client_test.go index b55eb633..c7d32f28 100644 --- a/alipay/client_test.go +++ b/alipay/client_test.go @@ -34,11 +34,11 @@ func TestMain(m *testing.M) { // SetReturnUrl("https://www.gopay.ink"). // SetNotifyUrl("https://www.gopay.ink") - // err := client.SetCertSnByPath("cert/appCertPublicKey.crt", "cert/alipayRootCert.crt", "cert/alipayCertPublicKey_RSA2.crt") - // if err != nil { - // xlog.Debug("SetCertSnByPath:", err) - // return - // } + err := client.SetCertSnByPath("cert/appCertPublicKey.crt", "cert/alipayRootCert.crt", "cert/alipayCertPublicKey_RSA2.crt") + if err != nil { + xlog.Debug("SetCertSnByPath:", err) + return + } os.Exit(m.Run()) } @@ -313,26 +313,6 @@ func TestClient_TradeFastPayRefundQuery(t *testing.T) { xlog.Debug("aliRsp:", *aliRsp) } -func TestClient_FundTransToaccountTransfer(t *testing.T) { - // 请求参数 - bm := make(gopay.BodyMap) - bm.Set("out_biz_no", util.GetRandomString(32)) - bm.Set("payee_type", "ALIPAY_LOGONID") - bm.Set("payee_account", "otmdfd2378@sandbox.com") - bm.Set("amount", "1000") - bm.Set("payer_show_name", "发钱人名字") - bm.Set("payee_real_name", "沙箱环境") - bm.Set("remark", "转账测试") - - // 转账 - aliRsp, err := client.FundTransToaccountTransfer(bm) - if err != nil { - xlog.Errorf("client.FundTransToaccountTransfer(%+v),error:%+v", bm, err) - return - } - xlog.Debug("aliRsp:", *aliRsp) -} - func TestClient_UserCertifyOpenInit(t *testing.T) { // 请求参数 bm := make(gopay.BodyMap) diff --git a/alipay/common_api.go b/alipay/common_api.go new file mode 100644 index 00000000..53814b23 --- /dev/null +++ b/alipay/common_api.go @@ -0,0 +1,270 @@ +package alipay + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/iGoogle-ink/gopay" + xaes "github.com/iGoogle-ink/gopay/pkg/aes" + "github.com/iGoogle-ink/gopay/pkg/util" + "github.com/iGoogle-ink/gopay/pkg/xhttp" +) + +// FormatPrivateKey 格式化 普通应用秘钥 +func FormatPrivateKey(privateKey string) (pKey string) { + var buffer strings.Builder + buffer.WriteString("-----BEGIN RSA PRIVATE KEY-----\n") + rawLen := 64 + keyLen := len(privateKey) + raws := keyLen / rawLen + temp := keyLen % rawLen + if temp > 0 { + raws++ + } + start := 0 + end := start + rawLen + for i := 0; i < raws; i++ { + if i == raws-1 { + buffer.WriteString(privateKey[start:]) + } else { + buffer.WriteString(privateKey[start:end]) + } + buffer.WriteByte('\n') + start += rawLen + end = start + rawLen + } + buffer.WriteString("-----END RSA PRIVATE KEY-----\n") + pKey = buffer.String() + return +} + +// FormatPublicKey 格式化 普通支付宝公钥 +func FormatPublicKey(publicKey string) (pKey string) { + var buffer strings.Builder + buffer.WriteString("-----BEGIN PUBLIC KEY-----\n") + rawLen := 64 + keyLen := len(publicKey) + raws := keyLen / rawLen + temp := keyLen % rawLen + if temp > 0 { + raws++ + } + start := 0 + end := start + rawLen + for i := 0; i < raws; i++ { + if i == raws-1 { + buffer.WriteString(publicKey[start:]) + } else { + buffer.WriteString(publicKey[start:end]) + } + buffer.WriteByte('\n') + start += rawLen + end = start + rawLen + } + buffer.WriteString("-----END PUBLIC KEY-----\n") + pKey = buffer.String() + return +} + +// 格式化请求URL参数 +func FormatURLParam(body gopay.BodyMap) (urlParam string) { + v := url.Values{} + for key, value := range body { + v.Add(key, value.(string)) + } + return v.Encode() +} + +// DecryptOpenDataToStruct 解密支付宝开放数据到 结构体 +// encryptedData:包括敏感数据在内的完整用户信息的加密数据 +// secretKey:AES密钥,支付宝管理平台配置 +// beanPtr:需要解析到的结构体指针 +// 文档:https://opendocs.alipay.com/mini/introduce/aes +// 文档:https://opendocs.alipay.com/open/common/104567 +func DecryptOpenDataToStruct(encryptedData, secretKey string, beanPtr interface{}) (err error) { + if encryptedData == util.NULL || secretKey == util.NULL { + return errors.New("encryptedData or secretKey is null") + } + beanValue := reflect.ValueOf(beanPtr) + if beanValue.Kind() != reflect.Ptr { + return errors.New("传入参数类型必须是以指针形式") + } + if beanValue.Elem().Kind() != reflect.Struct { + return errors.New("传入interface{}必须是结构体") + } + var ( + block cipher.Block + blockMode cipher.BlockMode + originData []byte + ) + aesKey, _ := base64.StdEncoding.DecodeString(secretKey) + ivKey := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + secretData, _ := base64.StdEncoding.DecodeString(encryptedData) + if block, err = aes.NewCipher(aesKey); err != nil { + return fmt.Errorf("aes.NewCipher:%w", err) + } + if len(secretData)%len(aesKey) != 0 { + return errors.New("encryptedData is error") + } + blockMode = cipher.NewCBCDecrypter(block, ivKey) + originData = make([]byte, len(secretData)) + blockMode.CryptBlocks(originData, secretData) + if len(originData) > 0 { + originData = xaes.PKCS5UnPadding(originData) + } + if err = json.Unmarshal(originData, beanPtr); err != nil { + return fmt.Errorf("json.Unmarshal(%s):%w", string(originData), err) + } + return nil +} + +// DecryptOpenDataToBodyMap 解密支付宝开放数据到 BodyMap +// encryptedData:包括敏感数据在内的完整用户信息的加密数据 +// secretKey:AES密钥,支付宝管理平台配置 +// 文档:https://opendocs.alipay.com/mini/introduce/aes +// 文档:https://opendocs.alipay.com/open/common/104567 +func DecryptOpenDataToBodyMap(encryptedData, secretKey string) (bm gopay.BodyMap, err error) { + if encryptedData == util.NULL || secretKey == util.NULL { + return nil, errors.New("encryptedData or secretKey is null") + } + var ( + aesKey, originData []byte + ivKey = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + block cipher.Block + blockMode cipher.BlockMode + ) + aesKey, _ = base64.StdEncoding.DecodeString(secretKey) + secretData, _ := base64.StdEncoding.DecodeString(encryptedData) + if block, err = aes.NewCipher(aesKey); err != nil { + return nil, fmt.Errorf("aes.NewCipher:%w", err) + } + if len(secretData)%len(aesKey) != 0 { + return nil, errors.New("encryptedData is error") + } + blockMode = cipher.NewCBCDecrypter(block, ivKey) + originData = make([]byte, len(secretData)) + blockMode.CryptBlocks(originData, secretData) + if len(originData) > 0 { + originData = xaes.PKCS5UnPadding(originData) + } + bm = make(gopay.BodyMap) + if err = json.Unmarshal(originData, &bm); err != nil { + return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(originData), err) + } + return +} + +// SystemOauthToken 换取授权访问令牌(默认使用utf-8,RSA2) +// appId:应用ID +// t:支付宝私钥类型,alipay.PKCS1 或 alipay.PKCS8,默认 PKCS1 +// privateKey:应用私钥 +// grantType:值为 authorization_code 时,代表用code换取;值为 refresh_token 时,代表用refresh_token换取,传空默认code换取 +// codeOrToken:支付宝授权码或refresh_token +// signType:签名方式 RSA 或 RSA2,默认 RSA2 +// 文档:https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token +func SystemOauthToken(appId string, t PKCSType, privateKey, grantType, codeOrToken, signType string) (rsp *SystemOauthTokenResponse, err error) { + var bs []byte + bm := make(gopay.BodyMap) + + switch grantType { + case "authorization_code": + bm.Set("grant_type", "authorization_code") + bm.Set("code", codeOrToken) + case "refresh_token": + bm.Set("grant_type", "refresh_token") + bm.Set("refresh_token", codeOrToken) + default: + bm.Set("grant_type", "authorization_code") + bm.Set("code", codeOrToken) + } + + if bs, err = systemOauthToken(appId, t, privateKey, bm, "alipay.system.oauth.token", true, signType); err != nil { + return + } + rsp = new(SystemOauthTokenResponse) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) + } + if rsp.Response.AccessToken == "" { + return nil, errors.New("access_token is NULL") + } + return +} + +// systemOauthToken 向支付宝发送请求 +func systemOauthToken(appId string, t PKCSType, privateKey string, bm gopay.BodyMap, method string, isProd bool, signType string) (bs []byte, err error) { + bm.Set("app_id", appId) + bm.Set("method", method) + bm.Set("format", "JSON") + bm.Set("charset", "utf-8") + if signType == util.NULL { + bm.Set("sign_type", RSA2) + } else { + bm.Set("sign_type", signType) + } + bm.Set("timestamp", time.Now().Format(util.TimeLayout)) + bm.Set("version", "1.0") + var ( + sign string + baseUrl = baseUrlUtf8 + ) + if sign, err = GetRsaSign(bm, bm.Get("sign_type"), t, privateKey); err != nil { + return nil, err + } + bm.Set("sign", sign) + if !isProd { + baseUrl = sandboxBaseUrlUtf8 + } + _, bs, errs := xhttp.NewClient().Type(xhttp.TypeForm).Post(baseUrl).SendString(FormatURLParam(bm)).EndBytes() + if len(errs) > 0 { + return nil, errs[0] + } + return bs, nil +} + +// monitor.heartbeat.syn(验签接口) +// appId:应用ID +// privateKey:应用私钥,支持PKCS1和PKCS8 +// signType:签名方式 alipay.RSA 或 alipay.RSA2,默认 RSA2 +// bizContent:验签时该参数不做任何处理,{任意值},此参数具体看文档 +// 文档:https://opendocs.alipay.com/apis/api_9/monitor.heartbeat.syn +func MonitorHeartbeatSyn(appId string, t PKCSType, privateKey, signType, bizContent string) (rsp *MonitorHeartbeatSynResponse, err error) { + var bs []byte + bm := make(gopay.BodyMap) + bm.Set("biz_content", bizContent) + bm.Set("app_id", appId) + bm.Set("method", "monitor.heartbeat.syn") + bm.Set("format", "JSON") + bm.Set("charset", "utf-8") + if signType == util.NULL { + bm.Set("sign_type", RSA2) + } else { + bm.Set("sign_type", signType) + } + bm.Set("timestamp", time.Now().Format(util.TimeLayout)) + bm.Set("version", "1.0") + + sign, err := GetRsaSign(bm, bm.Get("sign_type"), t, privateKey) + if err != nil { + return nil, err + } + bm.Set("sign", sign) + + _, bs, errs := xhttp.NewClient().Type(xhttp.TypeForm).Post(baseUrlUtf8).SendString(FormatURLParam(bm)).EndBytes() + if len(errs) > 0 { + return nil, errs[0] + } + rsp = new(MonitorHeartbeatSynResponse) + if err = json.Unmarshal(bs, rsp); err != nil { + return nil, err + } + return rsp, nil +} diff --git a/alipay/service_api_test.go b/alipay/common_api_test.go similarity index 100% rename from alipay/service_api_test.go rename to alipay/common_api_test.go diff --git a/alipay/data_api.go b/alipay/data_api.go new file mode 100644 index 00000000..a091f870 --- /dev/null +++ b/alipay/data_api.go @@ -0,0 +1,51 @@ +package alipay + +import ( + "encoding/json" + "fmt" + + "github.com/iGoogle-ink/gopay" +) + +// Deprecated +// alipay.data.bill.balance.query(支付宝商家账户当前余额查询) +// 文档地址:https://opendocs.alipay.com/apis/api_15/alipay.data.bill.balance.query +func (a *Client) DataBillBalanceQuery(bm gopay.BodyMap) (aliRsp *DataBillBalanceQueryResponse, err error) { + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.data.bill.balance.query"); err != nil { + return nil, err + } + aliRsp = new(DataBillBalanceQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.data.dataservice.bill.downloadurl.query(查询对账单下载地址) +// 文档地址:https://opendocs.alipay.com/apis/api_15/alipay.data.dataservice.bill.downloadurl.query +func (a *Client) DataBillDownloadUrlQuery(bm gopay.BodyMap) (aliRsp *DataBillDownloadUrlQueryResponse, err error) { + err = bm.CheckEmptyError("bill_type", "bill_date") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.data.dataservice.bill.downloadurl.query"); err != nil { + return nil, err + } + aliRsp = new(DataBillDownloadUrlQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} diff --git a/alipay/funds_api.go b/alipay/funds_api.go new file mode 100644 index 00000000..47c3caf4 --- /dev/null +++ b/alipay/funds_api.go @@ -0,0 +1,73 @@ +package alipay + +import ( + "encoding/json" + "fmt" + + "github.com/iGoogle-ink/gopay" +) + +// alipay.fund.trans.uni.transfer(单笔转账接口) +// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.uni.transfer +func (a *Client) FundTransUniTransfer(bm gopay.BodyMap) (aliRsp *FundTransUniTransferResponse, err error) { + err = bm.CheckEmptyError("out_biz_no", "trans_amount", "product_code", "payee_info") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.fund.trans.uni.transfer"); err != nil { + return nil, err + } + aliRsp = new(FundTransUniTransferResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.fund.account.query(支付宝资金账户资产查询接口) +// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.account.query +func (a *Client) FundAccountQuery(bm gopay.BodyMap) (aliRsp *FundAccountQueryResponse, err error) { + err = bm.CheckEmptyError("alipay_user_id") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.fund.account.query"); err != nil { + return nil, err + } + aliRsp = new(FundAccountQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.fund.trans.common.query(转账业务单据查询接口) +// 文档地址:https://opendocs.alipay.com/apis/api_28/alipay.fund.trans.common.query +func (a *Client) FundTransCommonQuery(bm gopay.BodyMap) (aliRsp *FundTransCommonQueryResponse, err error) { + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.fund.trans.common.query"); err != nil { + return nil, err + } + aliRsp = new(FundTransCommonQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} diff --git a/alipay/member_api.go b/alipay/member_api.go new file mode 100644 index 00000000..b5aaa37e --- /dev/null +++ b/alipay/member_api.go @@ -0,0 +1,91 @@ +package alipay + +import ( + "encoding/json" + "fmt" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// alipay.user.info.share(支付宝会员授权信息查询接口) +// body:此接口无需body参数 +// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.info.share +func (a *Client) UserInfoShare() (aliRsp *UserInfoShareResponse, err error) { + var bs []byte + if bs, err = a.doAliPay(nil, "alipay.user.info.share"); err != nil { + return nil, err + } + aliRsp = new(UserInfoShareResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.user.certify.open.initialize(身份认证初始化服务) +// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.initialize +func (a *Client) UserCertifyOpenInit(bm gopay.BodyMap) (aliRsp *UserCertifyOpenInitResponse, err error) { + err = bm.CheckEmptyError("outer_order_no", "biz_code", "identity_param", "merchant_config") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.user.certify.open.initialize"); err != nil { + return nil, err + } + aliRsp = new(UserCertifyOpenInitResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.user.certify.open.certify(身份认证开始认证) +// API文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.certify +// 产品文档地址:https://opendocs.alipay.com/open/20181012100420932508/quickstart +func (a *Client) UserCertifyOpenCertify(bm gopay.BodyMap) (certifyUrl string, err error) { + err = bm.CheckEmptyError("certify_id") + if err != nil { + return util.NULL, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.user.certify.open.certify"); err != nil { + return util.NULL, err + } + certifyUrl = string(bs) + return certifyUrl, nil +} + +// alipay.user.certify.open.query(身份认证记录查询) +// 文档地址:https://opendocs.alipay.com/apis/api_2/alipay.user.certify.open.query +func (a *Client) UserCertifyOpenQuery(bm gopay.BodyMap) (aliRsp *UserCertifyOpenQueryResponse, err error) { + err = bm.CheckEmptyError("certify_id") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.user.certify.open.query"); err != nil { + return nil, err + } + aliRsp = new(UserCertifyOpenQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} diff --git a/alipay/notify.go b/alipay/notify.go new file mode 100644 index 00000000..cc19e9c4 --- /dev/null +++ b/alipay/notify.go @@ -0,0 +1,111 @@ +package alipay + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// 解析支付宝支付异步通知的参数到BodyMap +// req:*http.Request +// 返回参数bm:Notify请求的参数 +// 返回参数err:错误信息 +// 文档:https://opendocs.alipay.com/open/203/105286 +func ParseNotifyToBodyMap(req *http.Request) (bm gopay.BodyMap, err error) { + if err = req.ParseForm(); err != nil { + return nil, err + } + var form map[string][]string = req.Form + bm = make(gopay.BodyMap, len(form)+1) + for k, v := range form { + if len(v) == 1 { + bm.Set(k, v[0]) + } + } + return +} + +// 通过 url.Values 解析支付宝支付异步通知的参数到Struct +// value:url.Values +// 返回参数notifyReq:Notify请求的参数 +// 返回参数err:错误信息 +// 文档:https://opendocs.alipay.com/open/203/105286 +func ParseNotifyByURLValues(value url.Values) (bm gopay.BodyMap, err error) { + bm = make(gopay.BodyMap, len(value)+1) + for k, v := range value { + if len(v) == 1 { + bm.Set(k, v[0]) + } + } + return +} + +// Deprecated +// 解析支付宝支付异步通知的参数到Struct +// req:*http.Request +// 返回参数notifyReq:Notify请求的参数 +// 返回参数err:错误信息 +// 文档:https://opendocs.alipay.com/open/203/105286 +func ParseNotifyResult(req *http.Request) (notifyReq *NotifyRequest, err error) { + notifyReq = new(NotifyRequest) + if err = req.ParseForm(); err != nil { + return + } + notifyReq.NotifyTime = req.Form.Get("notify_time") + notifyReq.NotifyType = req.Form.Get("notify_type") + notifyReq.NotifyId = req.Form.Get("notify_id") + notifyReq.AppId = req.Form.Get("app_id") + notifyReq.Charset = req.Form.Get("charset") + notifyReq.Version = req.Form.Get("version") + notifyReq.SignType = req.Form.Get("sign_type") + notifyReq.Sign = req.Form.Get("sign") + notifyReq.AuthAppId = req.Form.Get("auth_app_id") + notifyReq.TradeNo = req.Form.Get("trade_no") + notifyReq.OutTradeNo = req.Form.Get("out_trade_no") + notifyReq.OutBizNo = req.Form.Get("out_biz_no") + notifyReq.BuyerId = req.Form.Get("buyer_id") + notifyReq.BuyerLogonId = req.Form.Get("buyer_logon_id") + notifyReq.SellerId = req.Form.Get("seller_id") + notifyReq.SellerEmail = req.Form.Get("seller_email") + notifyReq.TradeStatus = req.Form.Get("trade_status") + notifyReq.TotalAmount = req.Form.Get("total_amount") + notifyReq.ReceiptAmount = req.Form.Get("receipt_amount") + notifyReq.InvoiceAmount = req.Form.Get("invoice_amount") + notifyReq.BuyerPayAmount = req.Form.Get("buyer_pay_amount") + notifyReq.PointAmount = req.Form.Get("point_amount") + notifyReq.RefundFee = req.Form.Get("refund_fee") + notifyReq.Subject = req.Form.Get("subject") + notifyReq.Body = req.Form.Get("body") + notifyReq.GmtCreate = req.Form.Get("gmt_create") + notifyReq.GmtPayment = req.Form.Get("gmt_payment") + notifyReq.GmtRefund = req.Form.Get("gmt_refund") + notifyReq.GmtClose = req.Form.Get("gmt_close") + notifyReq.PassbackParams = req.Form.Get("passback_params") + + billList := req.Form.Get("fund_bill_list") + if billList != util.NULL { + bills := make([]*FundBillListInfo, 0) + if err = json.Unmarshal([]byte(billList), &bills); err != nil { + return nil, fmt.Errorf(`"fund_bill_list" xml.Unmarshal(%s):%w`, billList, err) + } + notifyReq.FundBillList = bills + } else { + notifyReq.FundBillList = nil + } + + detailList := req.Form.Get("voucher_detail_list") + if detailList != util.NULL { + details := make([]*VoucherDetailListInfo, 0) + if err = json.Unmarshal([]byte(detailList), &details); err != nil { + return nil, fmt.Errorf(`"voucher_detail_list" xml.Unmarshal(%s):%w`, detailList, err) + } + notifyReq.VoucherDetailList = details + } else { + notifyReq.VoucherDetailList = nil + } + return +} diff --git a/alipay/param.go b/alipay/param.go index b77daadd..6f50f79e 100644 --- a/alipay/param.go +++ b/alipay/param.go @@ -1,22 +1,10 @@ package alipay import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "errors" "fmt" - "hash" "log" - "net/url" "time" - "github.com/iGoogle-ink/gopay" "github.com/iGoogle-ink/gopay/pkg/util" ) @@ -91,7 +79,7 @@ func (a *Client) SetAliPayRootCertSN(aliPayRootCertSN string) (client *Client) { return a } -// 设置 app_cert_sn、alipay_root_cert_sn、alipay_cert_sn 通过应用公钥证书路径 +// 通过应用公钥证书路径设置 app_cert_sn、alipay_root_cert_sn、alipay_cert_sn // appCertPath:应用公钥证书路径 // aliPayRootCertPath:支付宝根证书文件路径 // aliPayPublicCertPath:支付宝公钥证书文件路径 @@ -116,6 +104,31 @@ func (a *Client) SetCertSnByPath(appCertPath, aliPayRootCertPath, aliPayPublicCe return nil } +// 通过应用公钥证书内容设置 app_cert_sn、alipay_root_cert_sn、alipay_cert_sn +// appCertContent:应用公钥证书文件内容 +// aliPayRootCertContent:支付宝根证书文件内容 +// aliPayPublicCertContent:支付宝公钥证书文件内容 +func (a *Client) SetCertSnByContent(appCertContent, aliPayRootCertContent, aliPayPublicCertContent []byte) (err error) { + appCertSn, err := GetCertSN(appCertContent) + if err != nil { + return fmt.Errorf("get app_cert_sn return err, but alse return alipay client. err: %w", err) + } + rootCertSn, err := GetRootCertSN(aliPayRootCertContent) + if err != nil { + return fmt.Errorf("get alipay_root_cert_sn return err, but alse return alipay client. err: %w", err) + } + publicCertSn, err := GetCertSN(aliPayPublicCertContent) + if err != nil { + return fmt.Errorf("get alipay_cert_sn return err, but alse return alipay client. err: %w", err) + } + a.mu.Lock() + a.AppCertSN = appCertSn + a.AliPayRootCertSN = rootCertSn + a.AliPayPublicCertSN = publicCertSn + a.mu.Unlock() + return nil +} + // 设置支付后的ReturnUrl func (a *Client) SetReturnUrl(url string) (client *Client) { a.mu.Lock() @@ -171,73 +184,3 @@ func (a *Client) SetAuthToken(authToken string) (client *Client) { a.mu.Unlock() return a } - -// 获取支付宝参数签名 -// bm:签名参数 -// signType:签名类型,alipay.RSA 或 alipay.RSA2 -// t:私钥类型,alipay.PKCS1 或 alipay.PKCS1,默认 PKCS1 -// privateKey:应用私钥,支持PKCS1和PKCS8 -func GetRsaSign(bm gopay.BodyMap, signType string, t PKCSType, privateKey string) (sign string, err error) { - var ( - block *pem.Block - h hash.Hash - key *rsa.PrivateKey - hashs crypto.Hash - encryptedBytes []byte - ) - pk := FormatPrivateKey(privateKey) - - if block, _ = pem.Decode([]byte(pk)); block == nil { - return util.NULL, errors.New("pem.Decode:privateKey decode error") - } - - switch t { - case PKCS1: - if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { - return util.NULL, err - } - case PKCS8: - pkcs8Key, err := x509.ParsePKCS8PrivateKey(block.Bytes) - if err != nil { - return util.NULL, err - } - pk8, ok := pkcs8Key.(*rsa.PrivateKey) - if !ok { - return util.NULL, errors.New("parse PKCS8 key error") - } - key = pk8 - default: - if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { - return util.NULL, err - } - } - - switch signType { - case RSA: - h = sha1.New() - hashs = crypto.SHA1 - case RSA2: - h = sha256.New() - hashs = crypto.SHA256 - default: - h = sha256.New() - hashs = crypto.SHA256 - } - if _, err = h.Write([]byte(bm.EncodeAliPaySignParams())); err != nil { - return - } - if encryptedBytes, err = rsa.SignPKCS1v15(rand.Reader, key, hashs, h.Sum(nil)); err != nil { - return - } - sign = base64.StdEncoding.EncodeToString(encryptedBytes) - return -} - -// 格式化请求URL参数 -func FormatURLParam(body gopay.BodyMap) (urlParam string) { - v := url.Values{} - for key, value := range body { - v.Add(key, value.(string)) - } - return v.Encode() -} diff --git a/alipay/payment_api.go b/alipay/payment_api.go new file mode 100644 index 00000000..fc9629ce --- /dev/null +++ b/alipay/payment_api.go @@ -0,0 +1,303 @@ +package alipay + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// alipay.trade.pay(统一收单交易支付接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.pay +func (a *Client) TradePay(bm gopay.BodyMap) (aliRsp *TradePayResponse, err error) { + err = bm.CheckEmptyError("out_trade_no", "scene", "auth_code", "subject") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.pay"); err != nil { + return nil, err + } + aliRsp = new(TradePayResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.precreate(统一收单线下交易预创建) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.precreate +func (a *Client) TradePrecreate(bm gopay.BodyMap) (aliRsp *TradePrecreateResponse, err error) { + err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.precreate"); err != nil { + return nil, err + } + aliRsp = new(TradePrecreateResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + if aliRsp.NullResponse != nil { + info := aliRsp.NullResponse + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.app.pay(app支付接口2.0) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.app.pay +func (a *Client) TradeAppPay(bm gopay.BodyMap) (payParam string, err error) { + err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") + if err != nil { + return util.NULL, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.app.pay"); err != nil { + return util.NULL, err + } + payParam = string(bs) + return payParam, nil +} + +// alipay.trade.wap.pay(手机网站支付接口2.0) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay +func (a *Client) TradeWapPay(bm gopay.BodyMap) (payUrl string, err error) { + bm.Set("product_code", "QUICK_WAP_WAY") + err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") + if err != nil { + return util.NULL, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.wap.pay"); err != nil { + return util.NULL, err + } + payUrl = string(bs) + return payUrl, nil +} + +// alipay.trade.page.pay(统一收单下单并支付页面接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.page.pay +func (a *Client) TradePagePay(bm gopay.BodyMap) (payUrl string, err error) { + bm.Set("product_code", "FAST_INSTANT_TRADE_PAY") + err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") + if err != nil { + return util.NULL, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.page.pay"); err != nil { + return util.NULL, err + } + payUrl = string(bs) + return payUrl, nil +} + +// alipay.trade.create(统一收单交易创建接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.create +func (a *Client) TradeCreate(bm gopay.BodyMap) (aliRsp *TradeCreateResponse, err error) { + err = bm.CheckEmptyError("out_trade_no", "total_amount", "subject") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.create"); err != nil { + return nil, err + } + aliRsp = new(TradeCreateResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.query(统一收单线下交易查询) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.query +func (a *Client) TradeQuery(bm gopay.BodyMap) (aliRsp *TradeQueryResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.query"); err != nil { + return nil, err + } + aliRsp = new(TradeQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.cancel(统一收单交易撤销接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.cancel +func (a *Client) TradeCancel(bm gopay.BodyMap) (aliRsp *TradeCancelResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.cancel"); err != nil { + return nil, err + } + aliRsp = new(TradeCancelResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.close(统一收单交易关闭接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.close +func (a *Client) TradeClose(bm gopay.BodyMap) (aliRsp *TradeCloseResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.close"); err != nil { + return nil, err + } + aliRsp = new(TradeCloseResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.refund(统一收单交易退款接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.refund +func (a *Client) TradeRefund(bm gopay.BodyMap) (aliRsp *TradeRefundResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + err = bm.CheckEmptyError("refund_amount") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.refund"); err != nil { + return nil, err + } + aliRsp = new(TradeRefundResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.page.refund(统一收单退款页面接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.page.refund +func (a *Client) TradePageRefund(bm gopay.BodyMap) (aliRsp *TradePageRefundResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + err = bm.CheckEmptyError("out_request_no", "refund_amount") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.page.refund"); err != nil { + return nil, err + } + aliRsp = new(TradePageRefundResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.fastpay.refund.query(统一收单交易退款查询) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.fastpay.refund.query +func (a *Client) TradeFastPayRefundQuery(bm gopay.BodyMap) (aliRsp *TradeFastpayRefundQueryResponse, err error) { + if bm.Get("out_trade_no") == util.NULL && bm.Get("trade_no") == util.NULL { + return nil, errors.New("out_trade_no and trade_no are not allowed to be null at the same time") + } + err = bm.CheckEmptyError("out_request_no") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.fastpay.refund.query"); err != nil { + return nil, err + } + aliRsp = new(TradeFastpayRefundQueryResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.order.settle(统一收单交易结算接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.order.settle +func (a *Client) TradeOrderSettle(bm gopay.BodyMap) (aliRsp *TradeOrderSettleResponse, err error) { + err = bm.CheckEmptyError("out_request_no", "trade_no", "royalty_parameters") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.trade.order.settle"); err != nil { + return nil, err + } + aliRsp = new(TradeOrderSettleResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.trade.orderinfo.sync(支付宝订单信息同步接口) +// 文档地址:https://opendocs.alipay.com/apis/api_1/alipay.trade.orderinfo.sync +func (a *Client) TradeOrderinfoSync(body gopay.BodyMap) { + // TODO:to finish this +} diff --git a/alipay/service_api.go b/alipay/service_api.go deleted file mode 100644 index 9b2ef1c0..00000000 --- a/alipay/service_api.go +++ /dev/null @@ -1,677 +0,0 @@ -package alipay - -import ( - "crypto" - "crypto/aes" - "crypto/cipher" - "crypto/md5" - "crypto/rsa" - "crypto/x509" - "encoding/base64" - "encoding/hex" - "encoding/json" - "encoding/pem" - "errors" - "fmt" - "hash" - "io/ioutil" - "net/http" - "net/url" - "reflect" - "strings" - "time" - - "github.com/iGoogle-ink/gopay" - xaes "github.com/iGoogle-ink/gopay/pkg/aes" - "github.com/iGoogle-ink/gopay/pkg/util" - "github.com/iGoogle-ink/gopay/pkg/xhttp" -) - -// 允许进行 sn 提取的证书签名算法 -var allowSignatureAlgorithm = map[string]bool{ - "MD2-RSA": true, - "MD5-RSA": true, - "SHA1-RSA": true, - "SHA256-RSA": true, - "SHA384-RSA": true, - "SHA512-RSA": true, - "SHA256-RSAPSS": true, - "SHA384-RSAPSS": true, - "SHA512-RSAPSS": true, -} - -// 解析支付宝支付异步通知的参数到BodyMap -// req:*http.Request -// 返回参数bm:Notify请求的参数 -// 返回参数err:错误信息 -// 文档:https://opendocs.alipay.com/open/203/105286 -func ParseNotifyToBodyMap(req *http.Request) (bm gopay.BodyMap, err error) { - if err = req.ParseForm(); err != nil { - return nil, err - } - var form map[string][]string = req.Form - bm = make(gopay.BodyMap, len(form)+1) - for k, v := range form { - if len(v) == 1 { - bm.Set(k, v[0]) - } - } - return -} - -// 通过 url.Values 解析支付宝支付异步通知的参数到Struct -// value:url.Values -// 返回参数notifyReq:Notify请求的参数 -// 返回参数err:错误信息 -// 文档:https://opendocs.alipay.com/open/203/105286 -func ParseNotifyByURLValues(value url.Values) (bm gopay.BodyMap, err error) { - bm = make(gopay.BodyMap, len(value)+1) - for k, v := range value { - if len(v) == 1 { - bm.Set(k, v[0]) - } - } - return -} - -// Deprecated -// 解析支付宝支付异步通知的参数到Struct -// req:*http.Request -// 返回参数notifyReq:Notify请求的参数 -// 返回参数err:错误信息 -// 文档:https://opendocs.alipay.com/open/203/105286 -func ParseNotifyResult(req *http.Request) (notifyReq *NotifyRequest, err error) { - notifyReq = new(NotifyRequest) - if err = req.ParseForm(); err != nil { - return - } - notifyReq.NotifyTime = req.Form.Get("notify_time") - notifyReq.NotifyType = req.Form.Get("notify_type") - notifyReq.NotifyId = req.Form.Get("notify_id") - notifyReq.AppId = req.Form.Get("app_id") - notifyReq.Charset = req.Form.Get("charset") - notifyReq.Version = req.Form.Get("version") - notifyReq.SignType = req.Form.Get("sign_type") - notifyReq.Sign = req.Form.Get("sign") - notifyReq.AuthAppId = req.Form.Get("auth_app_id") - notifyReq.TradeNo = req.Form.Get("trade_no") - notifyReq.OutTradeNo = req.Form.Get("out_trade_no") - notifyReq.OutBizNo = req.Form.Get("out_biz_no") - notifyReq.BuyerId = req.Form.Get("buyer_id") - notifyReq.BuyerLogonId = req.Form.Get("buyer_logon_id") - notifyReq.SellerId = req.Form.Get("seller_id") - notifyReq.SellerEmail = req.Form.Get("seller_email") - notifyReq.TradeStatus = req.Form.Get("trade_status") - notifyReq.TotalAmount = req.Form.Get("total_amount") - notifyReq.ReceiptAmount = req.Form.Get("receipt_amount") - notifyReq.InvoiceAmount = req.Form.Get("invoice_amount") - notifyReq.BuyerPayAmount = req.Form.Get("buyer_pay_amount") - notifyReq.PointAmount = req.Form.Get("point_amount") - notifyReq.RefundFee = req.Form.Get("refund_fee") - notifyReq.Subject = req.Form.Get("subject") - notifyReq.Body = req.Form.Get("body") - notifyReq.GmtCreate = req.Form.Get("gmt_create") - notifyReq.GmtPayment = req.Form.Get("gmt_payment") - notifyReq.GmtRefund = req.Form.Get("gmt_refund") - notifyReq.GmtClose = req.Form.Get("gmt_close") - notifyReq.PassbackParams = req.Form.Get("passback_params") - - billList := req.Form.Get("fund_bill_list") - if billList != util.NULL { - bills := make([]*FundBillListInfo, 0) - if err = json.Unmarshal([]byte(billList), &bills); err != nil { - return nil, fmt.Errorf(`"fund_bill_list" xml.Unmarshal(%s):%w`, billList, err) - } - notifyReq.FundBillList = bills - } else { - notifyReq.FundBillList = nil - } - - detailList := req.Form.Get("voucher_detail_list") - if detailList != util.NULL { - details := make([]*VoucherDetailListInfo, 0) - if err = json.Unmarshal([]byte(detailList), &details); err != nil { - return nil, fmt.Errorf(`"voucher_detail_list" xml.Unmarshal(%s):%w`, detailList, err) - } - notifyReq.VoucherDetailList = details - } else { - notifyReq.VoucherDetailList = nil - } - return -} - -/* -Q:使用公钥证书签名方式下,为什么开放平台网关的响应报文需要携带支付宝公钥证书SN(alipay_cert_sn)? -** -A:开发者上传自己的应用公钥证书后,开放平台会为开发者应用自动签发支付宝公钥证书供开发者下载,用来对开放平台网关响应报文做验签。 - -但是支付宝公钥证书可能因证书到期或者变更CA签发机构等原因,可能会重新签发证书。在重新签发前,开放平台会在门户上提前提醒开发者支付宝应用公钥证书变更时间。 - -但为避免开发者因未能及时感知支付宝公钥证书变更而导致验签失败,开放平台提供了一种支付宝公钥证书无感知升级机制,具体流程如下: -1)开放平台网关在响应报文中会多返回支付宝公钥证书SN -2)开放平台网关提供根据SN下载对应支付宝公钥证书的API接口 -3)开发者在验签过程中,先比较本地使用的支付宝公钥证书SN与开放平台网关响应中SN是否一致。若不一致,可调用支付宝公钥证书下载接口下载对应SN的支付宝公钥证书。 -4)对下载的支付宝公钥证书执行证书链校验,若校验通过,则用该证书验签。 - -基于该机制可实现支付宝公钥证书变更时开发者无感知,当前开放平台提供的SDK已基于该机制实现对应功能。若开发者未通过SDK接入,须自行实现该功能。 -*/ - -// VerifySyncSign 支付宝同步返回验签 -// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 -// aliPayPublicKey:支付宝公钥 -// signData:待验签参数,aliRsp.SignData -// sign:待验签sign,aliRsp.Sign -// 返回参数ok:是否验签通过 -// 返回参数err:错误信息 -// 验签文档:https://opendocs.alipay.com/open/200/106120 -func VerifySyncSign(aliPayPublicKey, signData, sign string) (ok bool, err error) { - // 支付宝公钥验签 - pKey := FormatPublicKey(aliPayPublicKey) - if err = verifySign(signData, sign, RSA2, pKey); err != nil { - return false, err - } - return true, nil -} - -// VerifySign 支付宝异步通知验签 -// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 -// aliPayPublicKey:支付宝公钥 -// bean:此参数为异步通知解析的结构体或BodyMap:notifyReq 或 bm,推荐通 BodyMap 验签 -// 返回参数ok:是否验签通过 -// 返回参数err:错误信息 -// 验签文档:https://opendocs.alipay.com/open/200/106120 -func VerifySign(aliPayPublicKey string, bean interface{}) (ok bool, err error) { - if aliPayPublicKey == util.NULL { - return false, errors.New("aliPayPublicKey is null") - } - if bean == nil { - return false, errors.New("bean is nil") - } - var ( - bodySign string - bodySignType string - signData string - bm = make(gopay.BodyMap) - ) - if reflect.ValueOf(bean).Kind() == reflect.Map { - if bm, ok = bean.(gopay.BodyMap); ok { - bodySign = bm.Get("sign") - bodySignType = bm.Get("sign_type") - bm.Remove("sign") - bm.Remove("sign_type") - signData = bm.EncodeAliPaySignParams() - } - } else { - bs, err := json.Marshal(bean) - if err != nil { - return false, fmt.Errorf("json.Marshal:%w", err) - } - if err = json.Unmarshal(bs, &bm); err != nil { - return false, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) - } - bodySign = bm.Get("sign") - bodySignType = bm.Get("sign_type") - bm.Remove("sign") - bm.Remove("sign_type") - signData = bm.EncodeAliPaySignParams() - } - pKey := FormatPublicKey(aliPayPublicKey) - if err = verifySign(signData, bodySign, bodySignType, pKey); err != nil { - return false, err - } - return true, nil -} - -// VerifySignWithCert 支付宝异步通知验签 -// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 -// aliPayPublicKey:支付宝公钥存放路径 alipayCertPublicKey_RSA2.crt 或文件 buffer -// bean:此参数为异步通知解析的结构体或BodyMap:notifyReq 或 bm,推荐通 BodyMap 验签 -// 返回参数ok:是否验签通过 -// 返回参数err:错误信息 -// 验签文档:https://opendocs.alipay.com/open/200/106120 -func VerifySignWithCert(aliPayPublicKey, bean interface{}) (ok bool, err error) { - if bean == nil || aliPayPublicKey == nil { - return false, errors.New("aliPayPublicKey or bean is nil") - } - switch aliPayPublicKey.(type) { - case string: - if aliPayPublicKey == util.NULL { - return false, errors.New("aliPayPublicKeyPath is null") - } - case []byte: - default: - return false, errors.New("aliPayPublicKeyPath type assert error") - } - var ( - bodySign string - bodySignType string - signData string - bm = make(gopay.BodyMap) - ) - if reflect.ValueOf(bean).Kind() == reflect.Map { - if bm, ok = bean.(gopay.BodyMap); ok { - bodySign = bm.Get("sign") - bodySignType = bm.Get("sign_type") - bm.Remove("sign") - bm.Remove("sign_type") - signData = bm.EncodeAliPaySignParams() - } - } else { - bs, err := json.Marshal(bean) - if err != nil { - return false, fmt.Errorf("json.Marshal:%w", err) - } - if err = json.Unmarshal(bs, &bm); err != nil { - return false, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) - } - bodySign = bm.Get("sign") - bodySignType = bm.Get("sign_type") - bm.Remove("sign") - bm.Remove("sign_type") - signData = bm.EncodeAliPaySignParams() - } - if err = verifySignCert(signData, bodySign, bodySignType, aliPayPublicKey); err != nil { - return false, err - } - return true, nil -} - -func verifySign(signData, sign, signType, aliPayPublicKey string) (err error) { - var ( - h hash.Hash - hashs crypto.Hash - block *pem.Block - pubKey interface{} - publicKey *rsa.PublicKey - ok bool - ) - signBytes, _ := base64.StdEncoding.DecodeString(sign) - if block, _ = pem.Decode([]byte(aliPayPublicKey)); block == nil { - return errors.New("支付宝公钥Decode错误") - } - if pubKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { - return fmt.Errorf("x509.ParsePKIXPublicKey:%w", err) - } - if publicKey, ok = pubKey.(*rsa.PublicKey); !ok { - return errors.New("支付宝公钥转换错误") - } - switch signType { - case RSA: - hashs = crypto.SHA1 - case RSA2: - hashs = crypto.SHA256 - default: - hashs = crypto.SHA256 - } - h = hashs.New() - h.Write([]byte(signData)) - return rsa.VerifyPKCS1v15(publicKey, hashs, h.Sum(nil), signBytes) -} - -func verifySignCert(signData, sign, signType string, aliPayPublicKey interface{}) (err error) { - var ( - h hash.Hash - hashs crypto.Hash - block *pem.Block - pubKey *x509.Certificate - publicKey *rsa.PublicKey - ok bool - bytes []byte - ) - if v, ok := aliPayPublicKey.(string); ok { - if bytes, err = ioutil.ReadFile(v); err != nil { - return fmt.Errorf("支付宝公钥文件读取失败: %w", err) - } - } else { - bytes, ok = aliPayPublicKey.([]byte) - if !ok { - return fmt.Errorf("支付宝公钥读取失败: %w", err) - } - } - signBytes, _ := base64.StdEncoding.DecodeString(sign) - if block, _ = pem.Decode(bytes); block == nil { - return errors.New("支付宝公钥Decode错误") - } - if pubKey, err = x509.ParseCertificate(block.Bytes); err != nil { - return fmt.Errorf("x509.ParseCertificate:%w", err) - } - if publicKey, ok = pubKey.PublicKey.(*rsa.PublicKey); !ok { - return errors.New("支付宝公钥转换错误") - } - switch signType { - case RSA: - hashs = crypto.SHA1 - case RSA2: - hashs = crypto.SHA256 - default: - hashs = crypto.SHA256 - } - h = hashs.New() - h.Write([]byte(signData)) - return rsa.VerifyPKCS1v15(publicKey, hashs, h.Sum(nil), signBytes) -} - -// FormatPrivateKey 格式化 普通应用秘钥 -func FormatPrivateKey(privateKey string) (pKey string) { - var buffer strings.Builder - buffer.WriteString("-----BEGIN RSA PRIVATE KEY-----\n") - rawLen := 64 - keyLen := len(privateKey) - raws := keyLen / rawLen - temp := keyLen % rawLen - if temp > 0 { - raws++ - } - start := 0 - end := start + rawLen - for i := 0; i < raws; i++ { - if i == raws-1 { - buffer.WriteString(privateKey[start:]) - } else { - buffer.WriteString(privateKey[start:end]) - } - buffer.WriteByte('\n') - start += rawLen - end = start + rawLen - } - buffer.WriteString("-----END RSA PRIVATE KEY-----\n") - pKey = buffer.String() - return -} - -// FormatPublicKey 格式化 普通支付宝公钥 -func FormatPublicKey(publicKey string) (pKey string) { - var buffer strings.Builder - buffer.WriteString("-----BEGIN PUBLIC KEY-----\n") - rawLen := 64 - keyLen := len(publicKey) - raws := keyLen / rawLen - temp := keyLen % rawLen - if temp > 0 { - raws++ - } - start := 0 - end := start + rawLen - for i := 0; i < raws; i++ { - if i == raws-1 { - buffer.WriteString(publicKey[start:]) - } else { - buffer.WriteString(publicKey[start:end]) - } - buffer.WriteByte('\n') - start += rawLen - end = start + rawLen - } - buffer.WriteString("-----END PUBLIC KEY-----\n") - pKey = buffer.String() - return -} - -// GetCertSN 获取证书序列号SN -// certPathOrData.509证书文件路径(appCertPublicKey.crt、alipayCertPublicKey_RSA2.crt) 或证书 buffer -// 返回 sn:证书序列号(app_cert_sn、alipay_cert_sn) -// 返回 err:error 信息 -func GetCertSN(certPathOrData interface{}) (sn string, err error) { - var certData []byte - switch certPathOrData.(type) { - case string: - certData, err = ioutil.ReadFile(certPathOrData.(string)) - case []byte: - certData = certPathOrData.([]byte) - default: - return util.NULL, errors.New("certPathOrData 证书类型断言错误") - } - if err != nil { - return util.NULL, err - } - - if block, _ := pem.Decode(certData); block != nil { - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return util.NULL, err - } - name := cert.Issuer.String() - serialNumber := cert.SerialNumber.String() - h := md5.New() - h.Write([]byte(name)) - h.Write([]byte(serialNumber)) - sn = hex.EncodeToString(h.Sum(nil)) - } - if sn == util.NULL { - return util.NULL, errors.New("failed to get sn,please check your cert") - } - return sn, nil -} - -// GetRootCertSN 获取root证书序列号SN -// rootCertPathOrData.509证书文件路径(alipayRootCert.crt) 或文件 buffer -// 返回 sn:证书序列号(alipay_root_cert_sn) -// 返回 err:error 信息 -func GetRootCertSN(rootCertPathOrData interface{}) (sn string, err error) { - var certData []byte - var certEnd = `-----END CERTIFICATE-----` - switch rootCertPathOrData.(type) { - case string: - certData, err = ioutil.ReadFile(rootCertPathOrData.(string)) - case []byte: - certData = rootCertPathOrData.([]byte) - default: - return util.NULL, errors.New("rootCertPathOrData 断言异常") - } - if err != nil { - return util.NULL, err - } - - pems := strings.Split(string(certData), certEnd) - for _, c := range pems { - if block, _ := pem.Decode([]byte(c + certEnd)); block != nil { - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - continue - } - if !allowSignatureAlgorithm[cert.SignatureAlgorithm.String()] { - continue - } - name := cert.Issuer.String() - serialNumber := cert.SerialNumber.String() - h := md5.New() - h.Write([]byte(name)) - h.Write([]byte(serialNumber)) - if sn == util.NULL { - sn += hex.EncodeToString(h.Sum(nil)) - } else { - sn += "_" + hex.EncodeToString(h.Sum(nil)) - } - } - } - if sn == util.NULL { - return util.NULL, errors.New("failed to get sn,please check your cert") - } - return sn, nil -} - -// DecryptOpenDataToStruct 解密支付宝开放数据到 结构体 -// encryptedData:包括敏感数据在内的完整用户信息的加密数据 -// secretKey:AES密钥,支付宝管理平台配置 -// beanPtr:需要解析到的结构体指针 -// 文档:https://opendocs.alipay.com/mini/introduce/aes -// 文档:https://opendocs.alipay.com/open/common/104567 -func DecryptOpenDataToStruct(encryptedData, secretKey string, beanPtr interface{}) (err error) { - if encryptedData == util.NULL || secretKey == util.NULL { - return errors.New("encryptedData or secretKey is null") - } - beanValue := reflect.ValueOf(beanPtr) - if beanValue.Kind() != reflect.Ptr { - return errors.New("传入参数类型必须是以指针形式") - } - if beanValue.Elem().Kind() != reflect.Struct { - return errors.New("传入interface{}必须是结构体") - } - var ( - block cipher.Block - blockMode cipher.BlockMode - originData []byte - ) - aesKey, _ := base64.StdEncoding.DecodeString(secretKey) - ivKey := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - secretData, _ := base64.StdEncoding.DecodeString(encryptedData) - if block, err = aes.NewCipher(aesKey); err != nil { - return fmt.Errorf("aes.NewCipher:%w", err) - } - if len(secretData)%len(aesKey) != 0 { - return errors.New("encryptedData is error") - } - blockMode = cipher.NewCBCDecrypter(block, ivKey) - originData = make([]byte, len(secretData)) - blockMode.CryptBlocks(originData, secretData) - if len(originData) > 0 { - originData = xaes.PKCS5UnPadding(originData) - } - if err = json.Unmarshal(originData, beanPtr); err != nil { - return fmt.Errorf("json.Unmarshal(%s):%w", string(originData), err) - } - return nil -} - -// DecryptOpenDataToBodyMap 解密支付宝开放数据到 BodyMap -// encryptedData:包括敏感数据在内的完整用户信息的加密数据 -// secretKey:AES密钥,支付宝管理平台配置 -// 文档:https://opendocs.alipay.com/mini/introduce/aes -// 文档:https://opendocs.alipay.com/open/common/104567 -func DecryptOpenDataToBodyMap(encryptedData, secretKey string) (bm gopay.BodyMap, err error) { - if encryptedData == util.NULL || secretKey == util.NULL { - return nil, errors.New("encryptedData or secretKey is null") - } - var ( - aesKey, originData []byte - ivKey = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - block cipher.Block - blockMode cipher.BlockMode - ) - aesKey, _ = base64.StdEncoding.DecodeString(secretKey) - secretData, _ := base64.StdEncoding.DecodeString(encryptedData) - if block, err = aes.NewCipher(aesKey); err != nil { - return nil, fmt.Errorf("aes.NewCipher:%w", err) - } - if len(secretData)%len(aesKey) != 0 { - return nil, errors.New("encryptedData is error") - } - blockMode = cipher.NewCBCDecrypter(block, ivKey) - originData = make([]byte, len(secretData)) - blockMode.CryptBlocks(originData, secretData) - if len(originData) > 0 { - originData = xaes.PKCS5UnPadding(originData) - } - bm = make(gopay.BodyMap) - if err = json.Unmarshal(originData, &bm); err != nil { - return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(originData), err) - } - return -} - -// SystemOauthToken 换取授权访问令牌(默认使用utf-8,RSA2) -// appId:应用ID -// t:支付宝私钥类型,alipay.PKCS1 或 alipay.PKCS8,默认 PKCS1 -// privateKey:应用私钥 -// grantType:值为 authorization_code 时,代表用code换取;值为 refresh_token 时,代表用refresh_token换取,传空默认code换取 -// codeOrToken:支付宝授权码或refresh_token -// signType:签名方式 RSA 或 RSA2,默认 RSA2 -// 文档:https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token -func SystemOauthToken(appId string, t PKCSType, privateKey, grantType, codeOrToken, signType string) (rsp *SystemOauthTokenResponse, err error) { - var bs []byte - bm := make(gopay.BodyMap) - - switch grantType { - case "authorization_code": - bm.Set("grant_type", "authorization_code") - bm.Set("code", codeOrToken) - case "refresh_token": - bm.Set("grant_type", "refresh_token") - bm.Set("refresh_token", codeOrToken) - default: - bm.Set("grant_type", "authorization_code") - bm.Set("code", codeOrToken) - } - - if bs, err = systemOauthToken(appId, t, privateKey, bm, "alipay.system.oauth.token", true, signType); err != nil { - return - } - rsp = new(SystemOauthTokenResponse) - if err = json.Unmarshal(bs, rsp); err != nil { - return nil, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) - } - if rsp.Response.AccessToken == "" { - return nil, errors.New("access_token is NULL") - } - return -} - -// systemOauthToken 向支付宝发送请求 -func systemOauthToken(appId string, t PKCSType, privateKey string, bm gopay.BodyMap, method string, isProd bool, signType string) (bs []byte, err error) { - bm.Set("app_id", appId) - bm.Set("method", method) - bm.Set("format", "JSON") - bm.Set("charset", "utf-8") - if signType == util.NULL { - bm.Set("sign_type", RSA2) - } else { - bm.Set("sign_type", signType) - } - bm.Set("timestamp", time.Now().Format(util.TimeLayout)) - bm.Set("version", "1.0") - var ( - sign string - baseUrl = baseUrlUtf8 - ) - if sign, err = GetRsaSign(bm, bm.Get("sign_type"), t, privateKey); err != nil { - return nil, err - } - bm.Set("sign", sign) - if !isProd { - baseUrl = sandboxBaseUrlUtf8 - } - _, bs, errs := xhttp.NewClient().Type(xhttp.TypeForm).Post(baseUrl).SendString(FormatURLParam(bm)).EndBytes() - if len(errs) > 0 { - return nil, errs[0] - } - return bs, nil -} - -// monitor.heartbeat.syn(验签接口) -// appId:应用ID -// privateKey:应用私钥,支持PKCS1和PKCS8 -// signType:签名方式 alipay.RSA 或 alipay.RSA2,默认 RSA2 -// bizContent:验签时该参数不做任何处理,{任意值},此参数具体看文档 -// 文档:https://opendocs.alipay.com/apis/api_9/monitor.heartbeat.syn -func MonitorHeartbeatSyn(appId string, t PKCSType, privateKey, signType, bizContent string) (rsp *MonitorHeartbeatSynResponse, err error) { - var bs []byte - bm := make(gopay.BodyMap) - bm.Set("biz_content", bizContent) - bm.Set("app_id", appId) - bm.Set("method", "monitor.heartbeat.syn") - bm.Set("format", "JSON") - bm.Set("charset", "utf-8") - if signType == util.NULL { - bm.Set("sign_type", RSA2) - } else { - bm.Set("sign_type", signType) - } - bm.Set("timestamp", time.Now().Format(util.TimeLayout)) - bm.Set("version", "1.0") - - sign, err := GetRsaSign(bm, bm.Get("sign_type"), t, privateKey) - if err != nil { - return nil, err - } - bm.Set("sign", sign) - - _, bs, errs := xhttp.NewClient().Type(xhttp.TypeForm).Post(baseUrlUtf8).SendString(FormatURLParam(bm)).EndBytes() - if len(errs) > 0 { - return nil, errs[0] - } - rsp = new(MonitorHeartbeatSynResponse) - if err = json.Unmarshal(bs, rsp); err != nil { - return nil, err - } - return rsp, nil -} diff --git a/alipay/sign.go b/alipay/sign.go new file mode 100644 index 00000000..e965df93 --- /dev/null +++ b/alipay/sign.go @@ -0,0 +1,413 @@ +package alipay + +import ( + "crypto" + "crypto/md5" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "hash" + "io/ioutil" + "reflect" + "strings" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// 允许进行 sn 提取的证书签名算法 +var allowSignatureAlgorithm = map[string]bool{ + "MD2-RSA": true, + "MD5-RSA": true, + "SHA1-RSA": true, + "SHA256-RSA": true, + "SHA384-RSA": true, + "SHA512-RSA": true, + "SHA256-RSAPSS": true, + "SHA384-RSAPSS": true, + "SHA512-RSAPSS": true, +} + +/* +Q:使用公钥证书签名方式下,为什么开放平台网关的响应报文需要携带支付宝公钥证书SN(alipay_cert_sn)? +** +A:开发者上传自己的应用公钥证书后,开放平台会为开发者应用自动签发支付宝公钥证书供开发者下载,用来对开放平台网关响应报文做验签。 + +但是支付宝公钥证书可能因证书到期或者变更CA签发机构等原因,可能会重新签发证书。在重新签发前,开放平台会在门户上提前提醒开发者支付宝应用公钥证书变更时间。 + +但为避免开发者因未能及时感知支付宝公钥证书变更而导致验签失败,开放平台提供了一种支付宝公钥证书无感知升级机制,具体流程如下: +1)开放平台网关在响应报文中会多返回支付宝公钥证书SN +2)开放平台网关提供根据SN下载对应支付宝公钥证书的API接口 +3)开发者在验签过程中,先比较本地使用的支付宝公钥证书SN与开放平台网关响应中SN是否一致。若不一致,可调用支付宝公钥证书下载接口下载对应SN的支付宝公钥证书。 +4)对下载的支付宝公钥证书执行证书链校验,若校验通过,则用该证书验签。 + +基于该机制可实现支付宝公钥证书变更时开发者无感知,当前开放平台提供的SDK已基于该机制实现对应功能。若开发者未通过SDK接入,须自行实现该功能。 +*/ + +// GetCertSN 获取证书序列号SN +// certPathOrData.509证书文件路径(appCertPublicKey.crt、alipayCertPublicKey_RSA2.crt) 或证书 buffer +// 返回 sn:证书序列号(app_cert_sn、alipay_cert_sn) +// 返回 err:error 信息 +func GetCertSN(certPathOrData interface{}) (sn string, err error) { + var certData []byte + switch certPathOrData.(type) { + case string: + certData, err = ioutil.ReadFile(certPathOrData.(string)) + case []byte: + certData = certPathOrData.([]byte) + default: + return util.NULL, errors.New("certPathOrData 证书类型断言错误") + } + if err != nil { + return util.NULL, err + } + + if block, _ := pem.Decode(certData); block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return util.NULL, err + } + name := cert.Issuer.String() + serialNumber := cert.SerialNumber.String() + h := md5.New() + h.Write([]byte(name)) + h.Write([]byte(serialNumber)) + sn = hex.EncodeToString(h.Sum(nil)) + } + if sn == util.NULL { + return util.NULL, errors.New("failed to get sn,please check your cert") + } + return sn, nil +} + +// GetRootCertSN 获取root证书序列号SN +// rootCertPathOrData.509证书文件路径(alipayRootCert.crt) 或文件 buffer +// 返回 sn:证书序列号(alipay_root_cert_sn) +// 返回 err:error 信息 +func GetRootCertSN(rootCertPathOrData interface{}) (sn string, err error) { + var certData []byte + var certEnd = `-----END CERTIFICATE-----` + switch rootCertPathOrData.(type) { + case string: + certData, err = ioutil.ReadFile(rootCertPathOrData.(string)) + case []byte: + certData = rootCertPathOrData.([]byte) + default: + return util.NULL, errors.New("rootCertPathOrData 断言异常") + } + if err != nil { + return util.NULL, err + } + + pems := strings.Split(string(certData), certEnd) + for _, c := range pems { + if block, _ := pem.Decode([]byte(c + certEnd)); block != nil { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } + if !allowSignatureAlgorithm[cert.SignatureAlgorithm.String()] { + continue + } + name := cert.Issuer.String() + serialNumber := cert.SerialNumber.String() + h := md5.New() + h.Write([]byte(name)) + h.Write([]byte(serialNumber)) + if sn == util.NULL { + sn += hex.EncodeToString(h.Sum(nil)) + } else { + sn += "_" + hex.EncodeToString(h.Sum(nil)) + } + } + } + if sn == util.NULL { + return util.NULL, errors.New("failed to get sn,please check your cert") + } + return sn, nil +} + +// 获取支付宝参数签名 +// bm:签名参数 +// signType:签名类型,alipay.RSA 或 alipay.RSA2 +// t:私钥类型,alipay.PKCS1 或 alipay.PKCS1,默认 PKCS1 +// privateKey:应用私钥,支持PKCS1和PKCS8 +func GetRsaSign(bm gopay.BodyMap, signType string, t PKCSType, privateKey string) (sign string, err error) { + var ( + block *pem.Block + h hash.Hash + key *rsa.PrivateKey + hashs crypto.Hash + encryptedBytes []byte + ) + pk := FormatPrivateKey(privateKey) + + if block, _ = pem.Decode([]byte(pk)); block == nil { + return util.NULL, errors.New("pem.Decode:privateKey decode error") + } + + switch t { + case PKCS1: + if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + return util.NULL, err + } + case PKCS8: + pkcs8Key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return util.NULL, err + } + pk8, ok := pkcs8Key.(*rsa.PrivateKey) + if !ok { + return util.NULL, errors.New("parse PKCS8 key error") + } + key = pk8 + default: + if key, err = x509.ParsePKCS1PrivateKey(block.Bytes); err != nil { + return util.NULL, err + } + } + + switch signType { + case RSA: + h = sha1.New() + hashs = crypto.SHA1 + case RSA2: + h = sha256.New() + hashs = crypto.SHA256 + default: + h = sha256.New() + hashs = crypto.SHA256 + } + if _, err = h.Write([]byte(bm.EncodeAliPaySignParams())); err != nil { + return + } + if encryptedBytes, err = rsa.SignPKCS1v15(rand.Reader, key, hashs, h.Sum(nil)); err != nil { + return + } + sign = base64.StdEncoding.EncodeToString(encryptedBytes) + return +} + +// VerifySyncSign 支付宝同步返回验签 +// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 +// aliPayPublicKey:支付宝平台获取的支付宝公钥 +// signData:待验签参数,aliRsp.SignData +// sign:待验签sign,aliRsp.Sign +// 返回参数ok:是否验签通过 +// 返回参数err:错误信息 +// 验签文档:https://opendocs.alipay.com/open/200/106120 +func VerifySyncSign(aliPayPublicKey, signData, sign string) (ok bool, err error) { + // 支付宝公钥验签 + pKey := FormatPublicKey(aliPayPublicKey) + if err = verifySign(signData, sign, RSA2, pKey); err != nil { + return false, err + } + return true, nil +} + +// VerifySyncSignWithCert 支付宝同步返回验签 +// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 +// aliPayPublicKeyCert:支付宝公钥证书存放路径 alipayCertPublicKey_RSA2.crt 或文件内容[]byte +// signData:待验签参数,aliRsp.SignData +// sign:待验签sign,aliRsp.Sign +// 返回参数ok:是否验签通过 +// 返回参数err:错误信息 +// 验签文档:https://opendocs.alipay.com/open/200/106120 +func VerifySyncSignWithCert(aliPayPublicKeyCert interface{}, signData, sign string) (ok bool, err error) { + switch aliPayPublicKeyCert.(type) { + case string: + if aliPayPublicKeyCert == util.NULL { + return false, errors.New("aliPayPublicKeyPath is null") + } + case []byte: + default: + return false, errors.New("aliPayPublicKeyCert type assert error") + } + if err = verifySignCert(signData, sign, RSA2, aliPayPublicKeyCert); err != nil { + return false, err + } + return true, nil +} + +// VerifySign 支付宝异步通知验签 +// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 +// aliPayPublicKey:支付宝平台获取的支付宝公钥 +// notifyBean:此参数为异步通知解析的结构体或BodyMap:notifyReq 或 bm,推荐通 BodyMap 验签 +// 返回参数ok:是否验签通过 +// 返回参数err:错误信息 +// 验签文档:https://opendocs.alipay.com/open/200/106120 +func VerifySign(aliPayPublicKey string, notifyBean interface{}) (ok bool, err error) { + if aliPayPublicKey == util.NULL || notifyBean == nil { + return false, errors.New("aliPayPublicKey or notifyBean is nil") + } + var ( + bodySign string + bodySignType string + signData string + bm = make(gopay.BodyMap) + ) + if reflect.ValueOf(notifyBean).Kind() == reflect.Map { + if bm, ok = notifyBean.(gopay.BodyMap); ok { + bodySign = bm.Get("sign") + bodySignType = bm.Get("sign_type") + bm.Remove("sign") + bm.Remove("sign_type") + signData = bm.EncodeAliPaySignParams() + } + } else { + bs, err := json.Marshal(notifyBean) + if err != nil { + return false, fmt.Errorf("json.Marshal:%w", err) + } + if err = json.Unmarshal(bs, &bm); err != nil { + return false, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) + } + bodySign = bm.Get("sign") + bodySignType = bm.Get("sign_type") + bm.Remove("sign") + bm.Remove("sign_type") + signData = bm.EncodeAliPaySignParams() + } + pKey := FormatPublicKey(aliPayPublicKey) + if err = verifySign(signData, bodySign, bodySignType, pKey); err != nil { + return false, err + } + return true, nil +} + +// VerifySignWithCert 支付宝异步通知验签 +// 注意:APP支付,手机网站支付,电脑网站支付 暂不支持同步返回验签 +// aliPayPublicKeyCert:支付宝公钥证书存放路径 alipayCertPublicKey_RSA2.crt 或文件内容[]byte +// notifyBean:此参数为异步通知解析的结构体或BodyMap:notifyReq 或 bm,推荐通 BodyMap 验签 +// 返回参数ok:是否验签通过 +// 返回参数err:错误信息 +// 验签文档:https://opendocs.alipay.com/open/200/106120 +func VerifySignWithCert(aliPayPublicKeyCert, notifyBean interface{}) (ok bool, err error) { + if notifyBean == nil || aliPayPublicKeyCert == nil { + return false, errors.New("aliPayPublicKeyCert or notifyBean is nil") + } + switch aliPayPublicKeyCert.(type) { + case string: + if aliPayPublicKeyCert == util.NULL { + return false, errors.New("aliPayPublicKeyPath is null") + } + case []byte: + default: + return false, errors.New("aliPayPublicKeyCert type assert error") + } + var ( + bodySign string + bodySignType string + signData string + bm = make(gopay.BodyMap) + ) + if reflect.ValueOf(notifyBean).Kind() == reflect.Map { + if bm, ok = notifyBean.(gopay.BodyMap); ok { + bodySign = bm.Get("sign") + bodySignType = bm.Get("sign_type") + bm.Remove("sign") + bm.Remove("sign_type") + signData = bm.EncodeAliPaySignParams() + } + } else { + bs, err := json.Marshal(notifyBean) + if err != nil { + return false, fmt.Errorf("json.Marshal:%w", err) + } + if err = json.Unmarshal(bs, &bm); err != nil { + return false, fmt.Errorf("json.Unmarshal(%s):%w", string(bs), err) + } + bodySign = bm.Get("sign") + bodySignType = bm.Get("sign_type") + bm.Remove("sign") + bm.Remove("sign_type") + signData = bm.EncodeAliPaySignParams() + } + if err = verifySignCert(signData, bodySign, bodySignType, aliPayPublicKeyCert); err != nil { + return false, err + } + return true, nil +} + +func verifySign(signData, sign, signType, aliPayPublicKey string) (err error) { + var ( + h hash.Hash + hashs crypto.Hash + block *pem.Block + pubKey interface{} + publicKey *rsa.PublicKey + ok bool + ) + signBytes, _ := base64.StdEncoding.DecodeString(sign) + if block, _ = pem.Decode([]byte(aliPayPublicKey)); block == nil { + return errors.New("支付宝公钥Decode错误") + } + if pubKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + return fmt.Errorf("x509.ParsePKIXPublicKey:%w", err) + } + if publicKey, ok = pubKey.(*rsa.PublicKey); !ok { + return errors.New("支付宝公钥转换错误") + } + switch signType { + case RSA: + hashs = crypto.SHA1 + case RSA2: + hashs = crypto.SHA256 + default: + hashs = crypto.SHA256 + } + h = hashs.New() + h.Write([]byte(signData)) + return rsa.VerifyPKCS1v15(publicKey, hashs, h.Sum(nil), signBytes) +} + +func verifySignCert(signData, sign, signType string, aliPayPublicKeyCert interface{}) (err error) { + var ( + h hash.Hash + hashs crypto.Hash + block *pem.Block + pubKey *x509.Certificate + publicKey *rsa.PublicKey + ok bool + bytes []byte + ) + if v, ok := aliPayPublicKeyCert.(string); ok { + if bytes, err = ioutil.ReadFile(v); err != nil { + return fmt.Errorf("支付宝公钥文件读取失败: %w", err) + } + } else { + bytes, ok = aliPayPublicKeyCert.([]byte) + if !ok { + return fmt.Errorf("支付宝公钥读取失败: %w", err) + } + } + signBytes, _ := base64.StdEncoding.DecodeString(sign) + if block, _ = pem.Decode(bytes); block == nil { + return errors.New("支付宝公钥Decode错误") + } + if pubKey, err = x509.ParseCertificate(block.Bytes); err != nil { + return fmt.Errorf("x509.ParseCertificate:%w", err) + } + if publicKey, ok = pubKey.PublicKey.(*rsa.PublicKey); !ok { + return errors.New("支付宝公钥转换错误") + } + switch signType { + case RSA: + hashs = crypto.SHA1 + case RSA2: + hashs = crypto.SHA256 + default: + hashs = crypto.SHA256 + } + h = hashs.New() + h.Write([]byte(signData)) + return rsa.VerifyPKCS1v15(publicKey, hashs, h.Sum(nil), signBytes) +} diff --git a/alipay/util_api.go b/alipay/util_api.go new file mode 100644 index 00000000..0c8a3dea --- /dev/null +++ b/alipay/util_api.go @@ -0,0 +1,102 @@ +package alipay + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// alipay.user.info.auth(用户登陆授权) +// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.user.info.auth +func (a *Client) UserInfoAuth(bm gopay.BodyMap) (aliRsp *UserInfoAuthResponse, err error) { + err = bm.CheckEmptyError("scopes", "state") + if err != nil { + return nil, err + } + + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.user.info.auth"); err != nil { + return nil, err + } + if strings.Contains(string(bs), "") { + return nil, errors.New(string(bs)) + } + aliRsp = new(UserInfoAuthResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.system.oauth.token(换取授权访问令牌) +// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.system.oauth.token +func (a *Client) SystemOauthToken(bm gopay.BodyMap) (aliRsp *SystemOauthTokenResponse, err error) { + if bm.Get("code") == util.NULL && bm.Get("refresh_token") == util.NULL { + return nil, errors.New("code and refresh_token are not allowed to be null at the same time") + } + err = bm.CheckEmptyError("grant_type") + if err != nil { + return nil, err + } + + if a.AppCertSN != util.NULL { + a.mu.RLock() + bm.Set("app_cert_sn", a.AppCertSN) + a.mu.RUnlock() + } + if a.AliPayRootCertSN != util.NULL { + a.mu.RLock() + bm.Set("alipay_root_cert_sn", a.AliPayRootCertSN) + a.mu.RUnlock() + } + + var bs []byte + if bs, err = systemOauthToken(a.AppId, a.PrivateKeyType, a.PrivateKey, bm, "alipay.system.oauth.token", a.IsProd, a.SignType); err != nil { + return nil, err + } + aliRsp = new(SystemOauthTokenResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.ErrorResponse != nil { + info := aliRsp.ErrorResponse + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} + +// alipay.open.auth.token.app(换取应用授权令牌) +// 文档地址:https://opendocs.alipay.com/apis/api_9/alipay.open.auth.token.app +func (a *Client) OpenAuthTokenApp(bm gopay.BodyMap) (aliRsp *OpenAuthTokenAppResponse, err error) { + if bm.Get("code") == util.NULL && bm.Get("refresh_token") == util.NULL { + return nil, errors.New("code and refresh_token are not allowed to be null at the same time") + } + err = bm.CheckEmptyError("grant_type") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "alipay.open.auth.token.app"); err != nil { + return nil, err + } + aliRsp = new(OpenAuthTokenAppResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} diff --git a/alipay/zhima_api.go b/alipay/zhima_api.go new file mode 100644 index 00000000..85a6352b --- /dev/null +++ b/alipay/zhima_api.go @@ -0,0 +1,35 @@ +package alipay + +import ( + "encoding/json" + "fmt" + + "github.com/iGoogle-ink/gopay" + "github.com/iGoogle-ink/gopay/pkg/util" +) + +// zhima.credit.score.get(查询芝麻用户的芝麻分) +// 文档地址:https://opendocs.alipay.com/apis/api_8/zhima.credit.score.get +func (a *Client) ZhimaCreditScoreGet(bm gopay.BodyMap) (aliRsp *ZhimaCreditScoreGetResponse, err error) { + if bm.Get("product_code") == util.NULL { + bm.Set("product_code", "w1010100100000000001") + } + err = bm.CheckEmptyError("transaction_id") + if err != nil { + return nil, err + } + var bs []byte + if bs, err = a.doAliPay(bm, "zhima.credit.score.get"); err != nil { + return nil, err + } + aliRsp = new(ZhimaCreditScoreGetResponse) + if err = json.Unmarshal(bs, aliRsp); err != nil { + return nil, err + } + if aliRsp.Response != nil && aliRsp.Response.Code != "10000" { + info := aliRsp.Response + return nil, fmt.Errorf(`{"code":"%s","msg":"%s","sub_code":"%s","sub_msg":"%s"}`, info.Code, info.Msg, info.SubCode, info.SubMsg) + } + aliRsp.SignData = getSignData(bs) + return aliRsp, nil +} diff --git a/body_map.go b/body_map.go index 96bcfc1d..a8301665 100644 --- a/body_map.go +++ b/body_map.go @@ -12,7 +12,17 @@ import ( type BodyMap map[string]interface{} -var mu sync.RWMutex +type xmlMapMarshal struct { + XMLName xml.Name + Value interface{} `xml:",cdata"` +} + +type xmlMapUnmarshal struct { + XMLName xml.Name + Value string `xml:",cdata"` +} + +var mu = new(sync.RWMutex) // 设置参数 func (bm BodyMap) Set(key string, value interface{}) BodyMap { @@ -50,14 +60,25 @@ func (bm BodyMap) Get(key string) string { return v } +// 删除参数 +func (bm BodyMap) Remove(key string) { + mu.Lock() + delete(bm, key) + mu.Unlock() +} + // 置空BodyMap func (bm BodyMap) Reset() { + mu.Lock() for k := range bm { delete(bm, k) } + mu.Unlock() } func (bm BodyMap) JsonBody() (jb string) { + mu.Lock() + defer mu.Unlock() bs, err := json.Marshal(bm) if err != nil { return "" @@ -66,38 +87,6 @@ func (bm BodyMap) JsonBody() (jb string) { return jb } -func convertToString(v interface{}) (str string) { - if v == nil { - return NULL - } - var ( - bs []byte - err error - ) - if bs, err = json.Marshal(v); err != nil { - return NULL - } - str = string(bs) - return -} - -// 删除参数 -func (bm BodyMap) Remove(key string) { - mu.Lock() - delete(bm, key) - mu.Unlock() -} - -type xmlMapMarshal struct { - XMLName xml.Name - Value interface{} `xml:",cdata"` -} - -type xmlMapUnmarshal struct { - XMLName xml.Name - Value string `xml:",cdata"` -} - func (bm BodyMap) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) { if len(bm) == 0 { return nil @@ -211,3 +200,18 @@ func (bm BodyMap) CheckEmptyError(keys ...string) error { } return nil } + +func convertToString(v interface{}) (str string) { + if v == nil { + return NULL + } + var ( + bs []byte + err error + ) + if bs, err = json.Marshal(v); err != nil { + return NULL + } + str = string(bs) + return +} diff --git a/constant.go b/constant.go index 66408050..d5699800 100644 --- a/constant.go +++ b/constant.go @@ -7,7 +7,7 @@ const ( OK = "OK" DebugOff = 0 DebugOn = 1 - Version = "1.5.27" + Version = "1.5.28" ) type DebugSwitch int8 diff --git a/examples/alipay/alipay_FundTransToaccountTransfer.go b/examples/alipay/alipay_FundTransToaccountTransfer.go deleted file mode 100644 index c72c52c3..00000000 --- a/examples/alipay/alipay_FundTransToaccountTransfer.go +++ /dev/null @@ -1,39 +0,0 @@ -package alipay - -import ( - "github.com/iGoogle-ink/gopay" - "github.com/iGoogle-ink/gopay/alipay" - "github.com/iGoogle-ink/gopay/pkg/util" - "github.com/iGoogle-ink/gopay/pkg/xlog" -) - -func FundTransToaccountTransfer() { - privateKey := "MIIEogIBAAKCAQEAy+CRzKw4krA2RzCDTqg5KJg92XkOY0RN3pW4sYInPqnGtHV7YDHu5nMuxY6un+dLfo91OFOEg+RI+WTOPoM4xJtsOaJwQ1lpjycoeLq1OyetGW5Q8wO+iLWJASaMQM/t/aXR/JHaguycJyqlHSlxANvKKs/tOHx9AhW3LqumaCwz71CDF/+70scYuZG/7wxSjmrbRBswxd1Sz9KHdcdjqT8pmieyPqnM24EKBexHDmQ0ySXvLJJy6eu1dJsPIz+ivX6HEfDXmSmJ71AZVqZyCI1MhK813R5E7XCv5NOtskTe3y8uiIhgGpZSdB77DOyPLcmVayzFVLAQ3AOBDmsY6wIDAQABAoIBAHjsNq31zAw9FcR9orQJlPVd7vlJEt6Pybvmg8hNESfanO+16rpwg2kOEkS8zxgqoJ1tSzJgXu23fgzl3Go5fHcoVDWPAhUAOFre9+M7onh2nPXDd6Hbq6v8OEmFapSaf2b9biHnBHq5Chk08v/r74l501w3PVVOiPqulJrK1oVb+0/YmCvVFpGatBcNaefKUEcA+vekWPL7Yl46k6XeUvRfTwomCD6jpYLUhsAKqZiQJhMGoaLglZvkokQMF/4G78K7FbbVLMM1+JDh8zJ/DDVdY2vHREUcCGhl4mCVQtkzIbpxG++vFg7/g/fDI+PquG22hFILTDdtt2g2fV/4wmkCgYEA6goRQYSiM03y8Tt/M4u1Mm7OWYCksqAsU7rzQllHekIN3WjD41Xrjv6uklsX3sTG1syo7Jr9PGE1xQgjDEIyO8h/3lDQyLyycYnyUPGNNMX8ZjmGwcM51DQ/QfIrY/CXjnnW+MVpmNclAva3L33KXCWjw20VsROV1EA8LCL94BUCgYEA3wH4ANpzo7NqXf+2WlPPMuyRrF0QPIRGlFBNtaKFy0mvoclkREPmK7+N4NIGtMf5JNODS5HkFRgmU4YNdupA2I8lIYpD+TsIobZxGUKUkYzRZYZ1m1ttL69YYvCVz9Xosw/VoQ+RrW0scS5yUKqFMIUOV2R/Imi//c5TdKx6VP8CgYAnJ1ADugC4vI2sNdvt7618pnT3HEJxb8J6r4gKzYzbszlGlURQQAuMfKcP7RVtO1ZYkRyhmLxM4aZxNA9I+boVrlFWDAchzg+8VuunBwIslgLHx0/4EoUWLzd1/OGtco6oU1HXhI9J9pRGjqfO1iiIifN/ujwqx7AFNknayG/YkQKBgD6yNgA/ak12rovYzXKdp14Axn+39k2dPp6J6R8MnyLlB3yruwW6NSbNhtzTD1GZ+wCQepQvYvlPPc8zm+t3tl1r+Rtx3ORf5XBZc3iPkGdPOLubTssrrAnA+U9vph61W+OjqwLJ9sHUNK9pSHhHSIS4k6ycM2YAHyIC9NGTgB0PAoGAJjwd1DgMaQldtWnuXjvohPOo8cQudxXYcs6zVRbx6vtjKe2v7e+eK1SSVrR5qFV9AqxDfGwq8THenRa0LC3vNNplqostuehLhkWCKE7Y75vXMR7N6KU1kdoVWgN4BhXSwuRxmHMQfSY7q3HG3rDGz7mzXo1FVMr/uE4iDGm0IXY=" - //初始化支付宝客户端 - // appId:应用ID - // privateKey:应用私钥,支持PKCS1和PKCS8 - // isProd:是否是正式环境 - client := alipay.NewClient("2016091200494382", privateKey, false) - //配置公共参数 - client.SetCharset("utf-8"). - SetSignType(alipay.RSA2). - SetPrivateKeyType(alipay.PKCS1) - - //请求参数 - body := make(gopay.BodyMap) - out_biz_no := util.GetRandomString(32) - body.Set("out_biz_no", out_biz_no) - body.Set("payee_type", "ALIPAY_LOGONID") - body.Set("payee_account", "otmdfd2378@sandbox.com") - body.Set("amount", "1000") - body.Set("payer_show_name", "发钱人名字") - body.Set("payee_real_name", "沙箱环境") - body.Set("remark", "转账测试") - //转账 - aliRsp, err := client.FundTransToaccountTransfer(body) - if err != nil { - xlog.Error("err:", err) - return - } - xlog.Debug("aliRsp:", *aliRsp) -} diff --git a/qq/client.go b/qq/client.go index 4d2486ba..75702f7e 100644 --- a/qq/client.go +++ b/qq/client.go @@ -2,7 +2,6 @@ package qq import ( "crypto/tls" - "crypto/x509" "encoding/json" "encoding/xml" "errors" @@ -21,8 +20,7 @@ type Client struct { ApiKey string IsProd bool DebugSwitch gopay.DebugSwitch - certificate tls.Certificate - certPool *x509.CertPool + certificate *tls.Certificate mu sync.RWMutex } @@ -143,7 +141,7 @@ func (q *Client) CloseOrder(bm gopay.BodyMap) (qqRsp *CloseOrderResponse, err er // 注意:如已使用client.AddCertFilePath()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传空字符串 nil,否则,3证书Path均不可空 // 文档地址:https://qpay.qq.com/buss/wiki/38/1207 func (q *Client) Refund(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath interface{}) (qqRsp *RefundResponse, err error) { - if err = checkCertFilePath(certFilePath, keyFilePath, pkcs12FilePath); err != nil { + if err = checkCertFilePathOrContent(certFilePath, keyFilePath, pkcs12FilePath); err != nil { return nil, err } err = bm.CheckEmptyError("nonce_str", "out_refund_no", "refund_fee", "op_user_id", "op_user_passwd") @@ -228,22 +226,17 @@ func (q *Client) AccRoll(bm gopay.BodyMap) (qqRsp string, err error) { // 向QQ发送请求 func (q *Client) doQQ(bm gopay.BodyMap, url string, tlsConfig *tls.Config) (bs []byte, err error) { - func() { - q.mu.RLock() - defer q.mu.RUnlock() - - if bm.Get("mch_id") == util.NULL { - bm.Set("mch_id", q.MchId) - } - if bm.Get("fee_type") == util.NULL { - bm.Set("fee_type", "CNY") - } + if bm.Get("mch_id") == util.NULL { + bm.Set("mch_id", q.MchId) + } + if bm.Get("fee_type") == util.NULL { + bm.Set("fee_type", "CNY") + } - if bm.Get("sign") == util.NULL { - sign := getReleaseSign(q.ApiKey, bm.Get("sign_type"), bm) - bm.Set("sign", sign) - } - }() + if bm.Get("sign") == util.NULL { + sign := getReleaseSign(q.ApiKey, bm.Get("sign_type"), bm) + bm.Set("sign", sign) + } httpClient := xhttp.NewClient() if tlsConfig != nil { diff --git a/qq/param.go b/qq/param.go index ba09716e..1f3e7e38 100644 --- a/qq/param.go +++ b/qq/param.go @@ -5,8 +5,8 @@ import ( "crypto/md5" "crypto/sha256" "crypto/tls" - "crypto/x509" "encoding/hex" + "encoding/pem" "encoding/xml" "errors" "fmt" @@ -16,6 +16,7 @@ import ( "github.com/iGoogle-ink/gopay" "github.com/iGoogle-ink/gopay/pkg/util" + "golang.org/x/crypto/pkcs12" ) // 添加QQ证书 Path 路径 @@ -23,52 +24,67 @@ import ( // keyFilePath:apiclient_key.pem 路径 // pkcs12FilePath:apiclient_cert.p12 路径 // 返回err -func (w *Client) AddCertFilePath(certFilePath, keyFilePath, pkcs12FilePath interface{}) (err error) { - if err = checkCertFilePath(certFilePath, keyFilePath, pkcs12FilePath); err != nil { +func (q *Client) AddCertFilePath(certFilePath, keyFilePath, pkcs12FilePath interface{}) (err error) { + if err = checkCertFilePathOrContent(certFilePath, keyFilePath, pkcs12FilePath); err != nil { return err } - cert, err := ioutil.ReadFile(certFilePath.(string)) - if err != nil { - return fmt.Errorf("ioutil.ReadFile:%w", err) - } - key, err := ioutil.ReadFile(keyFilePath.(string)) - if err != nil { - return fmt.Errorf("ioutil.ReadFile:%w", err) - } - pkcs, err := ioutil.ReadFile(pkcs12FilePath.(string)) - if err != nil { - return fmt.Errorf("ioutil.ReadFile:%w", err) - } - certificate, err := tls.X509KeyPair(cert, key) - if err != nil { - return fmt.Errorf("tls.LoadX509KeyPair:%w", err) + var config *tls.Config + if config, err = q.addCertConfig(certFilePath, keyFilePath, pkcs12FilePath); err != nil { + return } - pkcsPool := x509.NewCertPool() - pkcsPool.AppendCertsFromPEM(pkcs) - w.mu.Lock() - w.certificate = certificate - w.certPool = pkcsPool - w.mu.Unlock() + q.mu.Lock() + q.certificate = &config.Certificates[0] + q.mu.Unlock() return nil } -func checkCertFilePath(certFilePath, keyFilePath, pkcs12FilePath interface{}) error { - if certFilePath != nil && keyFilePath != nil && pkcs12FilePath != nil { - if v, ok := certFilePath.(string); !ok || v == util.NULL { - return errors.New("certFilePath not string type or is null string") - } - if v, ok := keyFilePath.(string); !ok || v == util.NULL { - return errors.New("keyFilePath not string type or is null string") +// 添加QQ证书内容 +// certFileContent:apiclient_cert.pem 内容 +// keyFileContent:apiclient_key.pem 内容 +// pkcs12FileContent:apiclient_cert.p12 内容 +// 返回err +func (q *Client) AddCertFileContent(certFileContent, keyFileContent, pkcs12FileContent []byte) (err error) { + return q.AddCertFilePath(certFileContent, keyFileContent, pkcs12FileContent) +} + +func checkCertFilePathOrContent(certFile, keyFile, pkcs12File interface{}) error { + if certFile == nil && keyFile == nil && pkcs12File == nil { + return nil + } + if certFile != nil && keyFile != nil { + files := map[string]interface{}{"certFile": certFile, "keyFile": keyFile} + for varName, v := range files { + switch v.(type) { + case string: + if v.(string) == util.NULL { + return fmt.Errorf("%s is empty", varName) + } + case []byte: + if len(v.([]byte)) == 0 { + return fmt.Errorf("%s is empty", varName) + } + default: + return fmt.Errorf("%s type error", varName) + } } - if v, ok := pkcs12FilePath.(string); !ok || v == util.NULL { - return errors.New("pkcs12FilePath not string type or is null string") + return nil + } else if pkcs12File != nil { + switch pkcs12File.(type) { + case string: + if pkcs12File.(string) == util.NULL { + return errors.New("pkcs12File is empty") + } + case []byte: + if len(pkcs12File.([]byte)) == 0 { + return errors.New("pkcs12File is empty") + } + default: + return errors.New("pkcs12File type error") } return nil + } else { + return errors.New("certFile keyFile must all nil or all not nil") } - if !(certFilePath == nil && keyFilePath == nil && pkcs12FilePath == nil) { - return errors.New("cert paths must all nil or all not nil") - } - return nil } // 生成请求XML的Body体 @@ -92,44 +108,65 @@ func getReleaseSign(apiKey string, signType string, bm gopay.BodyMap) (sign stri return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) } -func (q *Client) addCertConfig(certFilePath, keyFilePath, pkcs12FilePath interface{}) (tlsConfig *tls.Config, err error) { - if certFilePath == nil && keyFilePath == nil && pkcs12FilePath == nil { +func (q *Client) addCertConfig(certFile, keyFile, pkcs12File interface{}) (tlsConfig *tls.Config, err error) { + if certFile == nil && keyFile == nil && pkcs12File == nil { q.mu.RLock() defer q.mu.RUnlock() - if q.certPool != nil { + if q.certificate != nil { tlsConfig = &tls.Config{ - Certificates: []tls.Certificate{q.certificate}, - RootCAs: q.certPool, + Certificates: []tls.Certificate{*q.certificate}, InsecureSkipVerify: true, } return tlsConfig, nil } + return nil, errors.New("cert parse failed") } - if certFilePath != nil && keyFilePath != nil && pkcs12FilePath != nil { - cert, err := ioutil.ReadFile(certFilePath.(string)) - if err != nil { - return nil, fmt.Errorf("ioutil.ReadFile:%w", err) + var ( + certPem, keyPem []byte + certificate tls.Certificate + ) + if certFile != nil && keyFile != nil { + if _, ok := certFile.([]byte); ok { + certPem = certFile.([]byte) + } else { + certPem, err = ioutil.ReadFile(certFile.(string)) } - key, err := ioutil.ReadFile(keyFilePath.(string)) - if err != nil { - return nil, fmt.Errorf("ioutil.ReadFile:%w", err) + if _, ok := keyFile.([]byte); ok { + keyPem = keyFile.([]byte) + } else { + keyPem, err = ioutil.ReadFile(keyFile.(string)) } - pkcs, err := ioutil.ReadFile(pkcs12FilePath.(string)) if err != nil { return nil, fmt.Errorf("ioutil.ReadFile:%w", err) } - pkcsPool := x509.NewCertPool() - pkcsPool.AppendCertsFromPEM(pkcs) - certificate, err := tls.X509KeyPair(cert, key) + } else if pkcs12File != nil { + var pfxData []byte + if _, ok := pkcs12File.([]byte); ok { + pfxData = pkcs12File.([]byte) + } else { + if pfxData, err = ioutil.ReadFile(pkcs12File.(string)); err != nil { + return nil, fmt.Errorf("ioutil.ReadFile:%w", err) + } + } + blocks, err := pkcs12.ToPEM(pfxData, q.MchId) if err != nil { + return nil, fmt.Errorf("pkcs12.ToPEM:%w", err) + } + for _, b := range blocks { + keyPem = append(keyPem, pem.EncodeToMemory(b)...) + } + certPem = keyPem + } + if certPem != nil && keyPem != nil { + if certificate, err = tls.X509KeyPair(certPem, keyPem); err != nil { return nil, fmt.Errorf("tls.LoadX509KeyPair:%w", err) } tlsConfig = &tls.Config{ Certificates: []tls.Certificate{certificate}, - RootCAs: pkcsPool, - InsecureSkipVerify: true} + InsecureSkipVerify: true, + } return tlsConfig, nil } - return nil, errors.New("cert paths must all nil or all not nil") + return nil, errors.New("cert files must all nil or all not nil") } diff --git a/qq/red.go b/qq/red.go index e885e142..afe9ea08 100644 --- a/qq/red.go +++ b/qq/red.go @@ -17,7 +17,7 @@ import ( // // 注意:如已使用client.AddCertFilePath()添加过证书,参数certFilePath、keyFilePath、pkcs12FilePath全传 nil,否则,3证书Path均不可空 // 文档:https://qpay.qq.com/buss/wiki/221/1220 func (q *Client) SendCashRed(bm gopay.BodyMap, certFilePath, keyFilePath, pkcs12FilePath interface{}) (qqRsp *SendCashRedResponse, err error) { - if err = checkCertFilePath(certFilePath, keyFilePath, pkcs12FilePath); err != nil { + if err = checkCertFilePathOrContent(certFilePath, keyFilePath, pkcs12FilePath); err != nil { return nil, err } err = bm.CheckEmptyError("charset", "nonce_str", "mch_billno", "mch_name", "re_openid", diff --git a/release_note.txt b/release_note.txt index d6a68392..98e1e2af 100644 --- a/release_note.txt +++ b/release_note.txt @@ -1,3 +1,12 @@ +版本号:Release 1.5.28 +发布时间:2021/02/19 18:48 +修改记录: + (1) QQ:新增 client.AddCertFileContent(),解决无证书文件,只有证书内容的问题 + (2) 支付宝:新增 alipay.VerifySyncSignWithCert(),同步证书验签 + (3) 支付宝:新增 client.SetCertSnByContent(),通过应用公钥证书内容设置 app_cert_sn、alipay_root_cert_sn、alipay_cert_sn + (4) 支付宝:删除废弃接口 client.FundTransToaccountTransfer() + (5) fix BodyMap 的部分方法 + 版本号:Release 1.5.27 发布时间:2021/02/03 18:50 修改记录: diff --git a/wechat/payment_api.go b/wechat/payment_api.go index b8d00306..1e37f0dd 100644 --- a/wechat/payment_api.go +++ b/wechat/payment_api.go @@ -306,7 +306,7 @@ func GetH5PaySign(appId, nonceStr, packages, signType, timeStamp, apiKey string) // signType:此处签名方式,务必与统一下单时用的签名方式一致 // timeStamp:时间 // ApiKey:API秘钥值 -// APP支付官方文档:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12 +// APP支付官方文档:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12&index=2 func GetAppPaySign(appid, partnerid, noncestr, prepayid, signType, timestamp, apiKey string) (paySign string) { var ( buffer strings.Builder