// sdk v5.1.0 // https://open.unionpay.com/tjweb/api/list package unionpay import ( "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/hex" "errors" "fmt" "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 // 交易币种 NotifyURL string // 回调通知地址 } // 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 string `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 { fmt.Println("getPrivateCert", err) return } mi.Cert, _, err = getPublicCert(certPath) if err != nil { fmt.Println("getPublicCert", err) 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 } // V3Ok 请求返回是否成功(Version 3.0) func (res *ReqRespose) V3Ok() (ok bool, err error) { if res.RespCode == "00" { 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) // 交易币种 } if len(p.BackURL) > 10 { // http[s]:// 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 }