|
@@ -0,0 +1,271 @@
|
|
|
|
+// sdk v5.1.0
|
|
|
|
+// https://open.unionpay.com/tjweb/api/list
|
|
|
|
+
|
|
|
|
+package unionpay
|
|
|
|
+
|
|
|
|
+import (
|
|
|
|
+ "crypto/rsa"
|
|
|
|
+ "crypto/sha256"
|
|
|
|
+ "crypto/x509"
|
|
|
|
+ "encoding/hex"
|
|
|
|
+ "errors"
|
|
|
|
+ "net/url"
|
|
|
|
+ "strconv"
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+// MerchantInfo 商家信息
|
|
|
|
+type MerchantInfo struct {
|
|
|
|
+ MerID string // 商户代码
|
|
|
|
+
|
|
|
|
+ Password string // 签名证书密码
|
|
|
|
+ Cert *x509.Certificate // 加密证书
|
|
|
|
+ PublicKey *rsa.PublicKey // 加密 Key
|
|
|
|
+ PrivateKey *rsa.PrivateKey // 签名 Key
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// OrderInfo 订单信息
|
|
|
|
+type OrderInfo struct {
|
|
|
|
+ TxnType string // 交易类型
|
|
|
|
+ TxnSubType string // 交易子类
|
|
|
|
+ OrderID string // 商户订单号,8-32位数字字母,不能含“-”或“_”
|
|
|
|
+ // TxnTime 订单发送时间,格式为YYYYMMDDhhmmss,取北京时间
|
|
|
|
+ TxnTime string
|
|
|
|
+ TxnAmt int // 交易金额, 单位: 分
|
|
|
|
+ CurrencyCode int // 交易币种
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// CustomerInfo 客户信息
|
|
|
|
+type CustomerInfo struct {
|
|
|
|
+ CardNo string `form:"cardNo" json:"cardNo" xml:"cardNo"` // 卡号
|
|
|
|
+ Phone string `form:"phone" json:"phone" xml:"phone"` // 手机号
|
|
|
|
+ CertifTp string `form:"certifTp" json:"certifTp" xml:"certifTp"` // 证件类型, 01 - 身份证
|
|
|
|
+ CertifID string `form:"certifId" json:"certifId" xml:"certifId"` // 证件号,15位身份证不校验尾号,18位会校验尾号
|
|
|
|
+ CustomerName string `form:"customerNm" json:"customerNm" xml:"customerNm"` // 姓名
|
|
|
|
+ Cvn2 string `form:"cvn2" json:"cvn2" xml:"cvn2"` // 信用卡安全码
|
|
|
|
+ Expired string `form:"expired" json:"expired" xml:"expired"` // 有效期,YYMM格式,持卡人卡面印的是MMYY的,请注意代码设置倒一下
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ReqParams 请求参数
|
|
|
|
+type ReqParams struct {
|
|
|
|
+ Version string `form:"version" json:"version" xml:"version"` // 版本号
|
|
|
|
+ Encoding string `form:"encoding" json:"encoding" xml:"encoding"` // 编码方式
|
|
|
|
+ SignMethod string `form:"signMethod" json:"signMethod" xml:"signMethod"` // 签名方法
|
|
|
|
+ TxnType string `form:"txnType" json:"txnType" xml:"txnType"` // 交易类型
|
|
|
|
+ TxnSubType string `form:"txnSubType" json:"txnSubType" xml:"txnSubType"` // 交易子类
|
|
|
|
+ BizType string `form:"bizType" json:"bizType" xml:"bizType"` // 业务类型
|
|
|
|
+ AccessType string `form:"accessType" json:"accessType" xml:"accessType"` // 接入类型
|
|
|
|
+ ChannelType string `form:"channelType" json:"channelType" xml:"channelType"` // 渠道类型
|
|
|
|
+ CurrencyCode int `form:"currencyCode" json:"currencyCode" xml:"currencyCode"` // 交易币种
|
|
|
|
+ CertID string `form:"certId" json:"certId" xml:"certId"` // 签名证书序列号
|
|
|
|
+ EncryptCertID string `form:"encryptCertId" json:"encryptCertId" xml:"encryptCertId"` // 验签证书序列号
|
|
|
|
+ MerID string `form:"merId" json:"merId" xml:"merId"` // 商户代码
|
|
|
|
+ OrderID string `form:"orderId" json:"orderId" xml:"orderId"` // 商户订单号
|
|
|
|
+ TxnTime string `form:"txnTime" json:"txnTime" xml:"txnTime"` // 订单发送时间
|
|
|
|
+ TxnAmt int `form:"txnAmt" json:"txnAmt" xml:"txnAmt"` // 交易金额,单位分
|
|
|
|
+ AccNo string `form:"accNo" json:"accNo" xml:"accNo"` // 卡号,敏感信息加密
|
|
|
|
+ Signature string `form:"signature" json:"signature" xml:"signature"` // 签名
|
|
|
|
+ BackURL string `form:"backUrl" json:"backUrl" xml:"backUrl"` // 后台通知地址
|
|
|
|
+ // 请求方保留域,
|
|
|
|
+ // 透传字段,查询、通知、对账文件中均会原样出现,如有需要请启用并修改自己希望透传的数据。
|
|
|
|
+ // 出现部分特殊字符时可能影响解析,请按下面建议的方式填写:
|
|
|
|
+ // 1. 如果能确定内容不会出现&={}[]"'等符号时,可以直接填写数据,建议的方法如下。
|
|
|
|
+ // 'reqReserved' =>'透传信息1|透传信息2|透传信息3',
|
|
|
|
+ // 2. 内容可能出现&={}[]"'符号时:
|
|
|
|
+ // 1) 如果需要对账文件里能显示,可将字符替换成全角&={}【】“‘字符(自己写代码,此处不演示);
|
|
|
|
+ // 2) 如果对账文件没有显示要求,可做一下base64(如下)。
|
|
|
|
+ // 注意控制数据长度,实际传输的数据长度不能超过1024位。
|
|
|
|
+ // 查询、通知等接口解析时使用base64_decode解base64后再对数据做后续解析。
|
|
|
|
+ // 'reqReserved' => base64_encode('任意格式的信息都可以'),
|
|
|
|
+ ReqReserved string `form:"reqReserved" json:"reqReserved" xml:"reqReserved"` // 透传字段
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ReqRespose 请求返回数据
|
|
|
|
+type ReqRespose struct {
|
|
|
|
+ Encoding string `form:"encoding" json:"encoding" xml:"encoding"`
|
|
|
|
+ Version string `form:"version" json:"version" xml:"version"`
|
|
|
|
+ RespCode int `form:"respCode" json:"respCode" xml:"respCode"` // 响应代码, 0 成功, > 0 错误
|
|
|
|
+ RespMsg string `form:"respMsg" json:"respMsg" xml:"respMsg"`
|
|
|
|
+ TxnType string `form:"txnType" json:"txnType" xml:"txnType"`
|
|
|
|
+ ChannelType string `form:"channelType" json:"channelType" xml:"channelType"`
|
|
|
|
+ CurrencyCode string `form:"currencyCode" json:"currencyCode" xml:"currencyCode"`
|
|
|
|
+ TxnSubType string `form:"txnSubType" json:"txnSubType" xml:"txnSubType"`
|
|
|
|
+ CustomerInfo string `form:"customerInfo" json:"customerInfo" xml:"customerInfo"`
|
|
|
|
+ TxnAmt string `form:"txnAmt" json:"txnAmt" xml:"txnAmt"`
|
|
|
|
+ SignPubKeyCert string `form:"signPubKeyCert" json:"signPubKeyCert" xml:"signPubKeyCert"`
|
|
|
|
+ SignMethod string `form:"signMethod" json:"signMethod" xml:"signMethod"`
|
|
|
|
+ AccNo string `form:"accNo" json:"accNo" xml:"accNo"`
|
|
|
|
+ BackURL string `form:"backUrl" json:"backUrl" xml:"backUrl"`
|
|
|
|
+ BizType string `form:"bizType" json:"bizType" xml:"bizType"`
|
|
|
|
+ Signature string `form:"signature" json:"signature" xml:"signature"`
|
|
|
|
+ OrderID string `form:"orderId" json:"orderId" xml:"orderId"`
|
|
|
|
+ TxnTime string `form:"txnTime" json:"txnTime" xml:"txnTime"`
|
|
|
|
+ AccessType int `form:"accessType" json:"accessType" xml:"accessType"`
|
|
|
|
+
|
|
|
|
+ //MerID string `form:"merId" json:"merId" xml:"merId"`
|
|
|
|
+ //CertID string `form:"certId" json:"certId" xml:"certId"`
|
|
|
|
+ //EncryptCertID string `form:"encryptCertId" json:"encryptCertId" xml:"encryptCertId"`
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// NewMerchantInfo 初始化商家信息
|
|
|
|
+func NewMerchantInfo(merID, certPath, keyPath, password string) (mi *MerchantInfo, err error) {
|
|
|
|
+ mi = &MerchantInfo{}
|
|
|
|
+ mi.MerID = merID
|
|
|
|
+ mi.Password = password
|
|
|
|
+
|
|
|
|
+ mi.PrivateKey, _, err = getPrivateCert(keyPath, password)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ mi.Cert, _, err = getPublicCert(certPath)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ mi.PublicKey = mi.Cert.PublicKey.(*rsa.PublicKey)
|
|
|
|
+
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func newParams(oi OrderInfo, ci CustomerInfo) *ReqParams {
|
|
|
|
+ args := &ReqParams{}
|
|
|
|
+
|
|
|
|
+ args.Version = "5.1.0"
|
|
|
|
+ args.Encoding = "utf-8"
|
|
|
|
+ args.SignMethod = "01" // 签名方法, 01 RSA签名
|
|
|
|
+
|
|
|
|
+ args.TxnType = oi.TxnType
|
|
|
|
+ args.TxnSubType = oi.TxnSubType
|
|
|
|
+
|
|
|
|
+ args.AccessType = "0" // 接入类型, 0:普通商户直连接入 1:收单机构接入 2:平台类商户接入
|
|
|
|
+ args.ChannelType = "07" // 渠道类型 07-PC
|
|
|
|
+
|
|
|
|
+ args.OrderID = oi.OrderID
|
|
|
|
+ args.TxnTime = oi.TxnTime
|
|
|
|
+
|
|
|
|
+ // 付款
|
|
|
|
+ if oi.TxnAmt > 0 {
|
|
|
|
+ args.TxnAmt = oi.TxnAmt
|
|
|
|
+ args.CurrencyCode = oi.CurrencyCode
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return args
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// SerialNumber 证书序列号
|
|
|
|
+func (mi *MerchantInfo) SerialNumber() string {
|
|
|
|
+ return mi.Cert.SerialNumber.String()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Encrypt 加密
|
|
|
|
+func (mi *MerchantInfo) Encrypt(src []byte) ([]byte, error) {
|
|
|
|
+ return encrypt(mi.PublicKey, src)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Decrypt 解密
|
|
|
|
+func (mi *MerchantInfo) Decrypt(src []byte) ([]byte, error) {
|
|
|
|
+ return decrypt(mi.PrivateKey, src)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Sign 签名
|
|
|
|
+func (mi *MerchantInfo) Sign(src []byte) ([]byte, error) {
|
|
|
|
+ hash := sha256.Sum256(src)
|
|
|
|
+ return sign(mi.PrivateKey, []byte(hex.EncodeToString(hash[:])))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Verify 签名验证
|
|
|
|
+func (mi *MerchantInfo) Verify(src, sig []byte) error {
|
|
|
|
+ return verify(mi.PublicKey, src, sig)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// EncryptCustomerInfo 持卡人身份信息,敏感信息加密
|
|
|
|
+func (mi *MerchantInfo) EncryptCustomerInfo(ci CustomerInfo) ([]byte, error) {
|
|
|
|
+ encryptedInfo := `phone=` + ci.Phone
|
|
|
|
+ if len(ci.Cvn2) > 0 {
|
|
|
|
+ encryptedInfo += `&cvn2=` + ci.Cvn2
|
|
|
|
+ }
|
|
|
|
+ if len(ci.Expired) > 0 {
|
|
|
|
+ encryptedInfo += `&expired=` + ci.Expired
|
|
|
|
+ }
|
|
|
|
+ encrypted, err := mi.Encrypt([]byte(encryptedInfo))
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, err
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return encrypted, nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (ci *CustomerInfo) String(encrypted []byte) string {
|
|
|
|
+ kvs := `encryptedInfo=` + base64Encode(encrypted)
|
|
|
|
+ kvs += `&certifTp=` + ci.CertifTp
|
|
|
|
+ kvs += `&certifId=` + ci.CertifID
|
|
|
|
+ kvs += `&customerNm=` + ci.CustomerName // `&cvn2=&=expired`
|
|
|
|
+
|
|
|
|
+ return base64Encode([]byte(`{` + kvs + `}`))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// Ok 请求返回是否成功
|
|
|
|
+func (res *ReqRespose) Ok() (ok bool, err error) {
|
|
|
|
+ if res.RespCode == 0 {
|
|
|
|
+ ok = true
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ err = errors.New(res.RespMsg)
|
|
|
|
+ return
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ToBody 请求参数转换为 HTTP POST 格式数据
|
|
|
|
+func (p *ReqParams) ToBody(mi *MerchantInfo, ci CustomerInfo) (body string, err error) {
|
|
|
|
+ var (
|
|
|
|
+ accNo []byte
|
|
|
|
+ encrypted []byte
|
|
|
|
+ signature []byte
|
|
|
|
+ values = &url.Values{}
|
|
|
|
+ args = make(map[string]string)
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ args["version"] = p.Version // 版本号
|
|
|
|
+ args["encoding"] = p.Encoding // 编码方式
|
|
|
|
+ args["signMethod"] = p.SignMethod // 签名方法
|
|
|
|
+ args["txnType"] = p.TxnType // 交易类型
|
|
|
|
+ args["txnSubType"] = p.TxnSubType // 交易子类
|
|
|
|
+ args["bizType"] = p.BizType // 业务类型
|
|
|
|
+ args["accessType"] = p.AccessType // 接入类型
|
|
|
|
+ args["channelType"] = p.ChannelType // 渠道类型
|
|
|
|
+ args["encryptCertId"] = mi.SerialNumber() // 验签证书序列号
|
|
|
|
+ args["certId"] = mi.SerialNumber() // 证书序列号
|
|
|
|
+ args["merId"] = mi.MerID // 商户代码
|
|
|
|
+ args["orderId"] = p.OrderID // 商户订单号,8-32位数字字母,不能含“-”或“_”
|
|
|
|
+ args["txnTime"] = p.TxnTime // 订单发送时间,格式为YYYYMMDDhhmmss,取北京时间
|
|
|
|
+
|
|
|
|
+ // 收款
|
|
|
|
+ if p.TxnAmt > 0 {
|
|
|
|
+ args["txnAmt"] = strconv.Itoa(p.TxnAmt) // 交易金额
|
|
|
|
+ args["currencyCode"] = strconv.Itoa(p.CurrencyCode) // 交易币种
|
|
|
|
+ args["backUrl"] = p.BackURL // 后台通知地址
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ accNo, err = mi.Encrypt([]byte(ci.CardNo))
|
|
|
|
+ if err != nil {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ args["accNo"] = base64Encode(accNo)
|
|
|
|
+
|
|
|
|
+ encrypted, err = mi.EncryptCustomerInfo(ci)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ args["customerInfo"] = ci.String(encrypted)
|
|
|
|
+
|
|
|
|
+ signature, err = mi.Sign([]byte(createLinkString(args, true, false)))
|
|
|
|
+ if err != nil {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ args["signature"] = base64Encode(signature) // 签名
|
|
|
|
+
|
|
|
|
+ for k, v := range args {
|
|
|
|
+ values.Add(k, v)
|
|
|
|
+ }
|
|
|
|
+ body = values.Encode()
|
|
|
|
+
|
|
|
|
+ return
|
|
|
|
+}
|