This commit is contained in:
邹宗楠
2022-07-06 16:48:43 +08:00
parent 07373a7e49
commit c47a130bc0
15 changed files with 427 additions and 9 deletions

View File

@@ -0,0 +1,219 @@
package alipayapi
import (
"crypto/md5"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"git.rosy.net.cn/baseapi/utils"
"git.rosy.net.cn/gopay-main/pkg/util"
"io/ioutil"
"strings"
)
const (
AliPayPublicCertPath = "./config/alipayCertPublicKey_RSA2.crt" // 支付宝公钥证书文件路径
aliPayRootCertPath = "./config/alipayRootCert.crt" // 支付宝根证书文件路径
appCertPath = "./config/appCertPublicKey_2019110769024042.crt" // 应用公钥证书路径
)
// WithdrawalSystemParam 支付包创建提现操作,系统参数
type WithdrawalSystemParam struct {
AppId string `json:"app_id"` // 支付宝分配给开发者的应用ID
Method string `json:"method"` // 接口名称
Charset string `json:"charset"` // 请求使用的编码格式如utf-8,gbk,gb2312等
SignType string `json:"sign_type"` // 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2
Sign string `json:"sign"` // 商户请求参数的签名串,详见签名
Timestamp string `json:"timestamp"` // 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss"
Version string `json:"version"` // 调用的接口版本固定为1.0
BizContent string `json:"biz_content"` // 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
}
// WithdrawalParam 支付宝创建提现操作,请求参数
type WithdrawalParam struct {
// 必填
OutBizNo string `json:"out_biz_no"` // 商家侧唯一订单号,由商家自定义。对于不同转账请求,商家需保证该订单号在自身系统唯一。
TransAmount float64 `json:"trans_amount"` // 订单总金额,单位为元,不支持千位分隔符,精确到小数点后两位,取值范围[0.1,100000000]。
ProductCode string `json:"product_code"` // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD。
BizScene string `json:"biz_scene"` // 业务场景。单笔无密转账固定为 DIRECT_TRANSFER。
OrderTitle string `json:"order_title"` // 转账业务的标题,用于在支付宝用户的账单里显示。
PayeeInfo *PayeeInfoParam `json:"payee_info"` // 收款方信息
//AppCertSN string `json:"app_cert_sn"` // 支付宝应用证书
//AliPayPublicCertSN string `json:"ali_pay_public_cert_sn"` // 支付宝公钥证书
//AliPayRootCertSN string `json:"ali_pay_root_cert_sn"` // 支付宝根证书
// 可选参数
Remark string `json:"remark"` // 业务备注。
}
type PayeeInfoParam struct {
Identity string `json:"identity"` // 参与方的标识 ID。 当 identity_type=ALIPAY_USER_ID 时,填写支付宝用户 UID。示例值2088123412341234。 当 identity_type=ALIPAY_LOGON_ID 时填写支付宝登录号。示例值186xxxxxxxx。
IdentityType string `json:"identity_type"` // 参与方的标识类型,目前支持如下枚举: ALIPAY_USER_ID支付宝会员的用户 ID可通过 获取会员信息 能力获取。ALIPAY_LOGON_ID支付宝登录号支持邮箱和手机号格式
// 可选参数
Name string `json:"name"` // 参与方真实姓名。如果非空,将校验收款支付宝账号姓名一致性。 当 identity_type=ALIPAY_LOGON_ID 时,本字段必填。若传入该属性,则在支付宝回单中将会显示这个属性。
}
// Withdrawal4AliPay 单笔转账接口,公司转账给个人
func (a *API) Withdrawal4AliPay(param *WithdrawalParam) (map[string]interface{}, error) {
if param.PayeeInfo.Identity == "" || param.PayeeInfo.Name == "" {
return nil, errors.New("支付宝关联电话和用户正式姓名不能为空")
}
param.ProductCode = "TRANS_ACCOUNT_NO_PWD"
param.BizScene = "DIRECT_TRANSFER"
param.PayeeInfo.IdentityType = "ALIPAY_LOGON_ID"
// 获取证书
appCertSN, aliPayRootCertSN, aliPayPublicCertSN, err := SetCertSnByPath(appCertPath, aliPayRootCertPath, AliPayPublicCertPath)
if err != nil {
return nil, err
}
certSN := make(map[string]interface{}, 3)
certSN["app_cert_sn"] = appCertSN
certSN["alipay_public_cert_sn"] = aliPayPublicCertSN
certSN["alipay_root_cert_sn"] = aliPayRootCertSN
result, err := a.AccessAPI("alipay.fund.trans.uni.transfer", certSN, utils.Struct2FlatMap(param), false) // , "root_cert_content": RootCertContent
if err != nil {
return nil, err
}
return result, nil
}
// SetCertSnByPath 通过应用公钥证书路径设置 app_cert_sn、alipay_root_cert_sn、alipay_cert_sn
// appCertPath应用公钥证书路径
// aliPayRootCertPath支付宝根证书文件路径
// aliPayPublicCertPath支付宝公钥证书文件路径
func SetCertSnByPath(appCertPath, aliPayRootCertPath, aliPayPublicCertPath string) (string, string, string, error) {
appCertSn, err := GetCertSN(appCertPath)
if err != nil {
return "", "", "", fmt.Errorf("get app_cert_sn return err, but alse return alipay client. err: %w", err)
}
rootCertSn, err := GetRootCertSN(aliPayRootCertPath)
if err != nil {
return "", "", "", fmt.Errorf("get alipay_root_cert_sn return err, but alse return alipay client. err: %w", err)
}
publicCertSn, err := GetCertSN(aliPayPublicCertPath)
if err != nil {
return "", "", "", fmt.Errorf("get alipay_cert_sn return err, but alse return alipay client. err: %w", err)
}
return appCertSn, rootCertSn, publicCertSn, nil
}
/*
Q使用公钥证书签名方式下为什么开放平台网关的响应报文需要携带支付宝公钥证书SNalipay_cert_sn
**
A开发者上传自己的应用公钥证书后开放平台会为开发者应用自动签发支付宝公钥证书供开发者下载用来对开放平台网关响应报文做验签。
但是支付宝公钥证书可能因证书到期或者变更CA签发机构等原因可能会重新签发证书。在重新签发前开放平台会在门户上提前提醒开发者支付宝应用公钥证书变更时间。
但为避免开发者因未能及时感知支付宝公钥证书变更而导致验签失败,开放平台提供了一种支付宝公钥证书无感知升级机制,具体流程如下:
1开放平台网关在响应报文中会多返回支付宝公钥证书SN
2开放平台网关提供根据SN下载对应支付宝公钥证书的API接口
3开发者在验签过程中先比较本地使用的支付宝公钥证书SN与开放平台网关响应中SN是否一致。若不一致可调用支付宝公钥证书下载接口下载对应SN的支付宝公钥证书。
4对下载的支付宝公钥证书执行证书链校验若校验通过则用该证书验签。
基于该机制可实现支付宝公钥证书变更时开发者无感知当前开放平台提供的SDK已基于该机制实现对应功能。若开发者未通过SDK接入须自行实现该功能。
*/
// GetCertSN 获取证书序列号SN
// certPathOrData x509证书文件路径(appCertPublicKey.crt、alipayCertPublicKey_RSA2.crt) 或证书 buffer
// 返回 sn证书序列号(app_cert_sn、alipay_cert_sn)
// 返回 errerror 信息
func GetCertSN(certPathOrData interface{}) (sn string, err error) {
var certData []byte
switch pathOrData := certPathOrData.(type) {
case string:
certData, err = ioutil.ReadFile(pathOrData)
if err != nil {
return util.NULL, err
}
case []byte:
certData = pathOrData
default:
return util.NULL, errors.New("certPathOrData 证书类型断言错误")
}
block, _ := pem.Decode(certData)
if 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
}
// 允许进行 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,
}
// GetRootCertSN 获取root证书序列号SN
// rootCertPathOrData x509证书文件路径(alipayRootCert.crt) 或文件 buffer
// 返回 sn证书序列号(alipay_root_cert_sn)
// 返回 errerror 信息
func GetRootCertSN(rootCertPathOrData interface{}) (sn string, err error) {
var (
certData []byte
certEnd = `-----END CERTIFICATE-----`
)
switch pathOrData := rootCertPathOrData.(type) {
case string:
certData, err = ioutil.ReadFile(pathOrData)
if err != nil {
return util.NULL, err
}
case []byte:
certData = pathOrData
default:
return util.NULL, errors.New("rootCertPathOrData 断言异常")
}
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
}
// 退款签名