package alipayapi import ( "crypto/md5" "crypto/x509" "encoding/hex" "encoding/pem" "errors" "fmt" "git.rosy.net.cn/baseapi/utils" "io/ioutil" "strings" ) const ( AliPayPublicCertPath = "./config/alipayCertPublicKey_RSA2.crt" // 支付宝公钥证书文件路径 AliPayRootCertPath = "./config/alipayRootCert.crt" // 支付宝根证书文件路径 AppCertPath = "./config/appCertPublicKey_2019110769024042.crt" // 应用公钥证书路径 MinWithdrawalMoney = 50 // 最小提现金额,不用审核 ) // 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"` // 收款方信息 // 可选参数 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 时,本字段必填。若传入该属性,则在支付宝回单中将会显示这个属性。 } type Withdrawal4AliPayRes struct { Code string `json:"code"` // code值 Msg string `json:"msg"` // 返回消息 OrderId string `json:"order_id"` // 支付宝订单Id OutBizNo string `json:"out_biz_no"` // 本地Id Status string `json:"status"` // 支付状态 TransDate string `json:"trans_date"` // 成功日期 } // Withdrawal4AliPay 单笔转账接口,公司转账给个人 func (a *API) Withdrawal4AliPay(param *WithdrawalParam) (*Withdrawal4AliPayRes, 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 } data := &Withdrawal4AliPayRes{} if err := utils.Map2StructByJson(result, data, false); err != nil { return nil, err } return data, 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:使用公钥证书签名方式下,为什么开放平台网关的响应报文需要携带支付宝公钥证书SN(alipay_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) // 返回 err:error 信息 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 "", err } case []byte: certData = pathOrData default: return "", errors.New("certPathOrData 证书类型断言错误") } block, _ := pem.Decode(certData) if block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", 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 == "" { return "", 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) // 返回 err:error 信息 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 "", err } case []byte: certData = pathOrData default: return "", 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 == "" { sn += hex.EncodeToString(h.Sum(nil)) } else { sn += "_" + hex.EncodeToString(h.Sum(nil)) } } } if sn == "" { return "", errors.New("failed to get sn,please check your cert") } return sn, nil } // 退款签名