├── README.md ├── sign.go ├── refund.go ├── example └── main.go ├── alipay.go ├── mobile_payment.go ├── LICENSE └── instant_credit.go /README.md: -------------------------------------------------------------------------------- 1 | # alipay 2 | 支付宝即时到帐,支付宝移动支付 Golang实现 3 | 4 | ## 快速开始 5 | ### 获取安装 6 | go get -u github.com/shima-park/alipay 7 | 8 | ### 推荐使用localtunnel测试回调通知 9 | 可以先安装一个[localtunnel](https://localtunnel.github.io/www/) 10 | 可以方便快捷的实现你的本地web服务通过外网访问,无需修改DNS和防火墙设置 11 | 12 | ```console 13 | $ npm install -g localtunnel 14 | ``` 15 | 16 | ## 示例 17 | 18 | #### 通过localtunnel获取外网地址: 19 | 20 | ```console 21 | $ lt --port 9090 22 | your url is: http://eygytquvvu.localtunnel.me 23 | ``` 24 | 25 | #### 修改示例代码中的配置: 26 | 记得修改示例中的对应的partner, key, email配置, 27 | 如果需要使用app支付记得添加public key path和private key path 28 | 29 | ```golang 30 | var ( 31 | partner = "your pid" 32 | key = "your key" 33 | email = "your email" 34 | 35 | publicKeyPath = "your rsa pubKey path" // "xxx/rsa_public_key.pem" 36 | privateKeyPath = "your rsa priKey path" // "xxx/rsa_private_key.pem" 37 | 38 | a = alipay.NewPayment(partner, key, email) 39 | // app 支付需要加入rsa公钥密钥 40 | // a.InitRSA(publicKeyPath, privateKeyPath) 41 | 42 | // 示例监听的端口 43 | port = ":9090" 44 | 45 | // 通过 lt --port 9090 获取的外网地址 46 | localTunnel = "http://eygytquvvu.localtunnel.me" 47 | ... 48 | ) 49 | ``` 50 | 51 | #### 启动示例程序: 52 | 53 | ```console 54 | $ go run example/main.go 55 | ``` 56 | 57 | #### 在浏览器中访问本地服务: 58 | [http://localhost:9090/index](http://localhost:9090/index) 59 | 60 | 具体如何使用请查看[example/main.go](https://github.com/shima-park/alipay/blob/master/example/main.go) 61 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | package alipay 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | type KVpair struct { 14 | K, V string 15 | } 16 | 17 | type KVpairs []KVpair 18 | 19 | func (t KVpairs) Less(i, j int) bool { 20 | return t[i].K < t[j].K 21 | } 22 | 23 | func (t KVpairs) Swap(i, j int) { 24 | t[i], t[j] = t[j], t[i] 25 | } 26 | 27 | func (t KVpairs) Len() int { 28 | return len(t) 29 | } 30 | 31 | func (t KVpairs) Sort() KVpairs { 32 | sort.Sort(t) 33 | return t 34 | } 35 | 36 | func (t KVpairs) RemoveEmpty() KVpairs { 37 | for i := 0; i < len(t); i++ { 38 | if t[i].V == "" { 39 | t = append(t[:i], t[i+1:]...) 40 | i-- 41 | } 42 | } 43 | return t 44 | } 45 | 46 | func (t KVpairs) Join(sep string) string { 47 | var strs []string 48 | for _, kv := range t { 49 | strs = append(strs, kv.K+"="+kv.V) 50 | } 51 | return strings.Join(strs, sep) 52 | } 53 | 54 | func MD5(strs ...string) string { 55 | h := md5.New() 56 | for _, str := range strs { 57 | io.WriteString(h, str) 58 | } 59 | return fmt.Sprintf("%x", h.Sum(nil)) 60 | } 61 | 62 | func SHA1(b []byte) []byte { 63 | h := sha1.New() 64 | h.Write(b) 65 | return h.Sum(nil) 66 | } 67 | 68 | func GenKVpairs(paramsKeyMap map[string]bool, initParams map[string]string, ignoreKey ...string) (kvs KVpairs, err error) { 69 | kvs = make(KVpairs, 0) 70 | for key, isMust := range paramsKeyMap { 71 | val, ok := initParams[key] 72 | if ok && val != "" { 73 | kvs = append(kvs, KVpair{K: key, V: val}) 74 | } else { 75 | // sign 参数需要签名后才会生成这里跳过 76 | if isMust && !Contains(ignoreKey, key) { 77 | err = errors.New("must param is empty:" + key) 78 | return 79 | } 80 | } 81 | } 82 | return 83 | } 84 | 85 | func Contains(strs []string, key string) bool { 86 | for _, v := range strs { 87 | if v == key { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /refund.go: -------------------------------------------------------------------------------- 1 | package alipay 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type RefundDetailData struct { 12 | AlipayTransID string 13 | Amount float64 14 | RefundReason string 15 | } 16 | 17 | func filterRefundReason(s string) string { 18 | s = strings.Replace(s, "^", "", -1) 19 | s = strings.Replace(s, "|", "", -1) 20 | s = strings.Replace(s, "$", "", -1) 21 | s = strings.Replace(s, "#", "", -1) 22 | return s 23 | } 24 | 25 | /* 26 | service 接口名称 String 接口名称。 不可空 refund_fastpay_by_platform_pwd 27 | partner 合作者身份ID String(16) 签约的支付宝账号对应的支付宝唯一用户号。以2088开头的16位纯数字组成。 不可空 2088101008267254 28 | _input_charset 参数编码字符集 String 商户网站使用的编码格式,如utf-8、gbk、gb2312等。 不可空 GBK 29 | sign_type 签名方式 String DSA、RSA、MD5三个值可选,必须大写。 不可空 MD5 30 | sign 签名 String 请参见签名。 不可空 tphoyf4aoio5e6zxoaydjevem2c1s1zo 31 | notify_url 服务器异步通知页面路径 String(200) 支付宝服务器主动通知商户网站里指定的页面http路径。 可空 http://api.test.alipay.net/atinterface/receive_notify.htm 32 | seller_email 卖家支付宝账号 String 如果卖家Id已填,则此字段可为空。 不可空 Jier1105@alitest.com 33 | seller_user_id 卖家用户ID String 卖家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。登录时,seller_email和seller_user_id两者必填一个。如果两者都填,以seller_user_id为准。 不可空 2088101008267254 34 | refund_date 退款请求时间 String 退款请求的当前时间。格式为:yyyy-MM-dd hh:mm:ss。 不可空 2011-01-12 11:21:00 35 | batch_no 退款批次号 String 每进行一次即时到账批量退款,都需要提供一个批次号,通过该批次号可以查询这一批次的退款交易记录,对于每一个合作伙伴,传递的每一个批次号都必须保证唯一性。格式为:退款日期(8位)+流水号(3~24位)。不可重复,且退款日期必须是当天日期。流水号可以接受数字或英文字符,建议使用数字,但不可接受“000”。 不可空 201101120001 36 | batch_num 总笔数 String 即参数detail_data的值中,“#”字符出现的数量加1,最大支持1000笔(即“#”字符出现的最大数量为999个)。 不可空 1 37 | detail_data 单笔数据集 String 退款请求的明细数据。格式详情参见下面的“单笔数据集参数说明”。 不可空 2011011201037066^5.00^协商退款 38 | */ 39 | func (a *Alipay) Refund(batchNo string, detailDatas []RefundDetailData, notifyURL string) (refundURL string, err error) { 40 | if batchNo == "" { 41 | err = fmt.Errorf("%s batch_no:Required parameter missing", LogPrefix) 42 | return 43 | } 44 | 45 | if len(detailDatas) == 0 { 46 | err = fmt.Errorf("%s detail_data:Required parameter missing", LogPrefix) 47 | return 48 | } 49 | 50 | var datas []string 51 | for _, v := range detailDatas { 52 | reason := filterRefundReason(v.RefundReason) 53 | datas = append(datas, fmt.Sprintf("%s^%.2f^%s", v.AlipayTransID, v.Amount, reason)) 54 | } 55 | 56 | kvs := KVpairs{} 57 | kvs = append(kvs, KVpair{K: "service", V: "refund_fastpay_by_platform_pwd"}) 58 | kvs = append(kvs, KVpair{K: "partner", V: a.partner}) 59 | kvs = append(kvs, KVpair{K: "_input_charset", V: "utf-8"}) 60 | kvs = append(kvs, KVpair{K: "notify_url", V: notifyURL}) 61 | kvs = append(kvs, KVpair{K: "seller_email", V: a.email}) 62 | kvs = append(kvs, KVpair{K: "seller_user_id", V: a.partner}) 63 | kvs = append(kvs, KVpair{K: "refund_date", V: time.Now().Format("2006-01-02 15:04:05")}) 64 | kvs = append(kvs, KVpair{K: "batch_no", V: batchNo}) 65 | kvs = append(kvs, KVpair{K: "batch_num", V: fmt.Sprint(len(detailDatas))}) 66 | kvs = append(kvs, KVpair{K: "detail_data", V: strings.Join(datas, "#")}) 67 | 68 | signStr := MD5(kvs.RemoveEmpty().Sort().Join("&"), a.key) 69 | 70 | kvs = append(kvs, KVpair{K: "sign", V: signStr}) 71 | kvs = append(kvs, KVpair{K: "sign_type", V: "MD5"}) 72 | 73 | vals := url.Values{} 74 | for _, v := range kvs { 75 | vals.Set(v.K, v.V) 76 | } 77 | 78 | refundURL = AlipayGateway + vals.Encode() 79 | return 80 | } 81 | 82 | type RefundNotifyResult struct { 83 | NotifyTime string // 通知时间 Date 通知发送的时间。格式为:yyyy-MM-dd HH:mm:ss。 不可空 2009-08-12 11:08:32 84 | NotifyType string // 通知类型 String 通知的类型。 不可空 batch_refund_notify 85 | NotifyID string // 通知校验ID String 通知校验ID。 不可空 70fec0c2730b27528665af4517c27b95 86 | SignType string // 签名方式 String DSA、RSA、MD5三个值可选,必须大写。 不可空 MD5 87 | Sign string // 签名 String 请参见签名验证。 不可空 b7baf9af3c91b37bef4261849aa76281 88 | BatchNo string // 退款批次号 String 原请求退款批次号。 不可空 20060702001 89 | SuccessNum string // 退款成功总数 String 退交易成功的笔数。0<= success_num<= 总退款笔数。 不可空 2 90 | ResultDetails string // 退款结果明细 String 退款结果明细:退手续费结果返回格式:交易号^退款金额^处理结果\$退费账号^退费账户ID^退费金额^处理结果;不退手续费结果返回格式:交易号^退款金额^处理结果。若退款申请提交成功,处理结果会返回“SUCCESS”。若提交失败,退款的处理结果中会有报错码,参见即时到账批量退款业务错误码。 可空 2010031906272929^80^SUCCESS$jax_chuanhang@alipay.com^2088101003147483^0.01^SUCCESS 91 | } 92 | 93 | func (a *Alipay) RefundNotify(req *http.Request) (rnr *RefundNotifyResult, err error) { 94 | vals, err := parsePostData(req) 95 | if len(vals) == 0 { 96 | err = ErrNotifyDataIsEmpty 97 | return 98 | } 99 | 100 | var fields = []string{ 101 | "notify_time", 102 | "notify_type", 103 | "notify_id", 104 | "sign_type", 105 | "sign", 106 | "batch_no", 107 | "success_num", 108 | "result_details", 109 | } 110 | 111 | err = a.verify(vals, fields) 112 | if err != nil { 113 | return 114 | } 115 | 116 | rnr = &RefundNotifyResult{ 117 | NotifyTime: vals.Get("notify_time"), 118 | NotifyType: vals.Get("notify_type"), 119 | NotifyID: vals.Get("notify_id"), 120 | SignType: vals.Get("sign_type"), 121 | Sign: vals.Get("sign"), 122 | BatchNo: vals.Get("batch_no"), 123 | SuccessNum: vals.Get("success_num"), 124 | ResultDetails: vals.Get("result_details"), 125 | } 126 | 127 | return 128 | } 129 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "strings" 9 | "time" 10 | 11 | "strconv" 12 | 13 | "net/http/httputil" 14 | 15 | "github.com/shima-park/alipay" 16 | ) 17 | 18 | var ( 19 | partner = "your pid" 20 | key = "your key" 21 | email = "your email" 22 | 23 | publicKeyPath = "your rsa pubKey path" // "xxx/rsa_public_key.pem" 24 | privateKeyPath = "your rsa priKey path" // "xxx/rsa_private_key.pem" 25 | 26 | a = alipay.NewPayment(partner, key, email) 27 | // app 支付需要加入rsa公钥密钥 28 | // a.InitRSA(publicKeyPath, privateKeyPath) 29 | 30 | // 示例监听的端口 31 | port = ":9090" 32 | 33 | // 通过 lt --port 9090 获取的外网地址 34 | localTunnel = "http://eqfssupbgz.localtunnel.me" 35 | 36 | returnURL = fmt.Sprintf("%s/%s", localTunnel, "alipay/return") 37 | notifyURL = fmt.Sprintf("%s/%s", localTunnel, "alipay/notify") 38 | returnNotifyURL = fmt.Sprintf("%s/%s", localTunnel, "alipay/return-notify") 39 | ) 40 | 41 | type MyServeMux struct { 42 | *http.ServeMux 43 | } 44 | 45 | func NewServeMux() *MyServeMux { return &MyServeMux{http.NewServeMux()} } 46 | 47 | func (mux *MyServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | dump, _ := httputil.DumpRequest(r, true) 49 | log.Println(string(dump)) 50 | if r.RequestURI == "*" { 51 | if r.ProtoAtLeast(1, 1) { 52 | w.Header().Set("Connection", "close") 53 | } 54 | w.WriteHeader(http.StatusBadRequest) 55 | return 56 | } 57 | h, _ := mux.Handler(r) 58 | h.ServeHTTP(w, r) 59 | } 60 | 61 | func main() { 62 | mux := NewServeMux() 63 | mux.HandleFunc("/hello", HelloServer) 64 | mux.HandleFunc("/index", IndexServer) 65 | mux.HandleFunc("/alipay/payment-web", PaymentWebServer) // 支付宝网页支付 66 | mux.HandleFunc("/alipay/payment-mobile", PaymentAPPServer) // 支付宝app支付 67 | mux.HandleFunc("/alipay/return", ReturnWebServer) // 支付宝网页支付返回处理 68 | mux.HandleFunc("/alipay/notify", NotifyWebServer) // 支付宝支付通知 69 | mux.HandleFunc("/alipay/refund-notify", RefundNotifyServer) // 支付宝退款通知 70 | mux.HandleFunc("/alipay/refund", RefundServer) // 支付宝退款 71 | 72 | log.Println("Listen", port) 73 | log.Fatal(http.ListenAndServe(port, mux)) 74 | } 75 | 76 | // hello world, the web server 77 | func HelloServer(w http.ResponseWriter, req *http.Request) { 78 | io.WriteString(w, "hello, world!\n") 79 | } 80 | 81 | func IndexServer(w http.ResponseWriter, req *http.Request) { 82 | w.Header().Set("content-type", "text/html; charset=utf-8") 83 | var html = ` 84 |

Oh!!! It works!

85 |

支付宝订单

86 |

移动支付

87 | ` 88 | fmt.Fprintf(w, html) 89 | return 90 | } 91 | 92 | func PaymentWebServer(w http.ResponseWriter, req *http.Request) { 93 | var ( 94 | outTradeNo = time.Now().Format("20060102150405999") 95 | subject = "test_subject" 96 | totalFee = 0.01 97 | extraParams = map[string]string{ 98 | "body": "test body", 99 | "return_url": returnURL, 100 | "notify_url": notifyURL, 101 | } 102 | ) 103 | checkourURL, err := a.InstantCredit(outTradeNo, subject, totalFee, extraParams) 104 | if err != nil { 105 | fmt.Fprintf(w, "Error:%s", err.Error()) 106 | return 107 | } 108 | http.Redirect(w, req, checkourURL, http.StatusFound) 109 | return 110 | } 111 | 112 | func PaymentAPPServer(w http.ResponseWriter, req *http.Request) { 113 | var ( 114 | outTradeNo = time.Now().Format("20060102150405999") 115 | subject = "test_subject" 116 | totalFee = 0.01 117 | extraParams = map[string]string{ 118 | "body": "test body", 119 | "return_url": returnURL, 120 | "notify_url": notifyURL, 121 | } 122 | ) 123 | 124 | paymentParams, err := a.MobilePayment(outTradeNo, subject, totalFee, notifyURL, extraParams) 125 | if err != nil { 126 | fmt.Fprintf(w, "Error:%s", err.Error()) 127 | return 128 | } 129 | fmt.Fprintf(w, "%s", paymentParams) 130 | return 131 | } 132 | 133 | func ReturnWebServer(w http.ResponseWriter, req *http.Request) { 134 | r, err := a.InstantCreditReturn(req) 135 | if err != nil { 136 | fmt.Fprintf(w, "Error:%s", err.Error()) 137 | return 138 | } 139 | 140 | w.Header().Set("content-type", "text/html; charset=utf-8") 141 | 142 | var html = fmt.Sprintf(` 143 | result:%+v
144 | 退款
145 | `, r, r.TradeNo, r.TotalFee) 146 | fmt.Fprintf(w, html) 147 | return 148 | } 149 | 150 | func RefundServer(w http.ResponseWriter, req *http.Request) { 151 | var ( 152 | tradeNo = req.URL.Query().Get("trade_no") 153 | reason = req.URL.Query().Get("reason") 154 | refundAmount, _ = strconv.ParseFloat(req.URL.Query().Get("trade_amount"), 64) 155 | outRefundNo = time.Now().Format("20060102150405000000") 156 | detailDatas = []alipay.RefundDetailData{ 157 | alipay.RefundDetailData{ 158 | AlipayTransID: tradeNo, 159 | Amount: refundAmount, 160 | RefundReason: reason, 161 | }, 162 | } 163 | ) 164 | refundURL, err := a.Refund(outRefundNo, detailDatas, notifyURL) 165 | if err != nil { 166 | fmt.Fprintf(w, "Error:%s", err.Error()) 167 | return 168 | } 169 | http.Redirect(w, req, refundURL, http.StatusFound) 170 | return 171 | } 172 | 173 | func RefundNotifyServer(w http.ResponseWriter, req *http.Request) { 174 | r, err := a.RefundNotify(req) 175 | if err != nil { 176 | fmt.Fprintf(w, "Error:%s", err.Error()) 177 | return 178 | } 179 | 180 | var notifyResult = parseAlipayNotify(r.ResultDetails) 181 | for _, nr := range notifyResult { 182 | if nr.Result == "SUCCESS" { 183 | // do something... 184 | } 185 | } 186 | 187 | fmt.Fprintf(w, "%s", "success") 188 | return 189 | } 190 | 191 | type AlipayRefundNotifyResult struct { 192 | TradeNo string 193 | Amount float64 194 | Result string 195 | } 196 | 197 | func parseAlipayNotify(resultDetails string) (results []AlipayRefundNotifyResult) { 198 | var trades = strings.Split(resultDetails, "$") 199 | 200 | for _, trade := range trades { 201 | tradeInfo := strings.Split(trade, "^") 202 | if len(tradeInfo) == 3 { 203 | amount, _ := strconv.ParseFloat(tradeInfo[1], 64) 204 | results = append(results, AlipayRefundNotifyResult{ 205 | TradeNo: tradeInfo[0], 206 | Amount: amount, 207 | Result: tradeInfo[2], 208 | }) 209 | } 210 | } 211 | return 212 | } 213 | 214 | func NotifyWebServer(w http.ResponseWriter, req *http.Request) { 215 | r, err := a.InstantCreditNotify(req) 216 | if err != nil { 217 | fmt.Fprintf(w, "Error:%s", err.Error()) 218 | return 219 | } 220 | 221 | if r.TradeStatus != "TRADE_SUCCESS" && r.TradeStatus != "TRADE_FINISHED" { 222 | fmt.Fprintf(w, "%s", "fail") 223 | return 224 | } 225 | 226 | fmt.Fprintf(w, "%s", "success") 227 | return 228 | } 229 | -------------------------------------------------------------------------------- /alipay.go: -------------------------------------------------------------------------------- 1 | package alipay 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha1" 8 | "crypto/tls" 9 | "crypto/x509" 10 | "encoding/base64" 11 | "encoding/pem" 12 | "errors" 13 | "fmt" 14 | "io" 15 | "io/ioutil" 16 | "log" 17 | "net/http" 18 | "net/url" 19 | "strings" 20 | ) 21 | 22 | // http://doc.open.alipay.com/doc2/detail?spm=0.0.0.0.1LPUGt&treeId=63&articleId=103758&docType=1 23 | 24 | var AlipayGateway = "https://mapi.alipay.com/gateway.do?" 25 | var LogPrefix = "[Alipay]" 26 | 27 | var ErrNotFoundNotifyID = errors.New("not found notify_id") 28 | var ErrReturnDataIsEmpty = errors.New("return data is empty") 29 | var ErrNotifyDataIsEmpty = errors.New("notify data is empty") 30 | 31 | type Alipay struct { 32 | partner string 33 | key string 34 | email string 35 | 36 | publicKey *rsa.PublicKey 37 | privateKey *rsa.PrivateKey 38 | } 39 | 40 | func NewPayment(partner, key, email string) *Alipay { 41 | a := &Alipay{ 42 | partner: partner, 43 | key: key, 44 | email: email, 45 | } 46 | 47 | return a 48 | } 49 | 50 | func (a *Alipay) InitRSA(pubPath, priPath string) *Alipay { 51 | var ( 52 | err error 53 | publicKey *rsa.PublicKey 54 | privateKey *rsa.PrivateKey 55 | ) 56 | 57 | publicKey, err = newPublicKey(pubPath) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | privateKey, err = newPrivateKey(priPath) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | a.publicKey = publicKey 68 | a.privateKey = privateKey 69 | 70 | return a 71 | } 72 | 73 | func newPublicKey(path string) (pub *rsa.PublicKey, err error) { 74 | // Read the verify sign certification key 75 | pemData, err := ioutil.ReadFile(path) 76 | if err != nil { 77 | return 78 | } 79 | 80 | // Extract the PEM-encoded data block 81 | block, _ := pem.Decode(pemData) 82 | if block == nil { 83 | err = fmt.Errorf("%s bad key data: %s", LogPrefix, "not PEM-encoded") 84 | return 85 | } 86 | // if got, want := block.Type, "CERTIFICATE"; got != want { 87 | // err = fmt.Errorf("%s unknown key type %q, want %q", LogPrefix, got, want) 88 | // return 89 | // } 90 | 91 | // Decode the certification 92 | //cert, err = x509.ParseCertificate(block.Bytes) 93 | 94 | pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes) 95 | if err != nil { 96 | err = fmt.Errorf("%s bad public key: %s", LogPrefix, err) 97 | return 98 | } 99 | pub = pubInterface.(*rsa.PublicKey) 100 | return 101 | } 102 | 103 | func newPrivateKey(path string) (priKey *rsa.PrivateKey, err error) { 104 | // Read the private key 105 | pemData, err := ioutil.ReadFile(path) 106 | if err != nil { 107 | err = fmt.Errorf("%s read key file: %s", LogPrefix, err) 108 | return 109 | } 110 | 111 | // Extract the PEM-encoded data block 112 | block, _ := pem.Decode(pemData) 113 | if block == nil { 114 | err = fmt.Errorf("%s bad key data: %s", LogPrefix, "not PEM-encoded") 115 | return 116 | } 117 | if got, want := block.Type, "RSA PRIVATE KEY"; got != want { 118 | err = fmt.Errorf("%s unknown key type %q, want %q", LogPrefix, got, want) 119 | return 120 | } 121 | 122 | // Decode the RSA private key 123 | priKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) 124 | if err != nil { 125 | err = fmt.Errorf("%s bad private key: %s", LogPrefix, err) 126 | return 127 | } 128 | 129 | return 130 | } 131 | 132 | func (a *Alipay) rsaSign(kvs KVpairs) (sig string, err error) { 133 | h := sha1.New() 134 | io.WriteString(h, kvs.RemoveEmpty().Sort().Join("&")) 135 | hashed := h.Sum(nil) 136 | 137 | rsaSign, err := rsa.SignPKCS1v15(rand.Reader, a.privateKey, crypto.SHA1, hashed) 138 | if err != nil { 139 | return 140 | } 141 | 142 | sig = base64.StdEncoding.EncodeToString(rsaSign) 143 | return 144 | } 145 | 146 | func (a *Alipay) rsaVerify(vals url.Values, fields []string) (err error) { 147 | var signature, notifyID string 148 | kvs := KVpairs{} 149 | for key := range vals { 150 | if len(fields) > 0 && !Contains(fields, key) { 151 | continue 152 | } 153 | 154 | if key == "sign" { 155 | signature, _ = url.QueryUnescape(vals.Get(key)) 156 | continue 157 | } 158 | 159 | if key == "sign_type" { 160 | continue 161 | } 162 | 163 | if key == "notify_id" { 164 | notifyID, _ = url.QueryUnescape(vals.Get(key)) 165 | } 166 | 167 | var k, v string 168 | k, err = url.QueryUnescape(key) 169 | if err != nil { 170 | return 171 | } 172 | 173 | v, err = url.QueryUnescape(vals.Get(key)) 174 | if err != nil { 175 | return 176 | } 177 | kvs = append(kvs, KVpair{K: k, V: v}) 178 | } 179 | 180 | hashed := SHA1([]byte(kvs.RemoveEmpty().Sort().Join("&"))) 181 | 182 | var inSign []byte 183 | inSign, err = base64.StdEncoding.DecodeString(signature) 184 | if err != nil { 185 | return 186 | } 187 | 188 | err = rsa.VerifyPKCS1v15(a.publicKey, crypto.SHA1, hashed, inSign) 189 | if err != nil { 190 | return 191 | } 192 | 193 | err = a.checkNotify(notifyID) 194 | if err != nil { 195 | return 196 | } 197 | return 198 | } 199 | 200 | // verify 判断过来的参数是否有效 201 | // TODO 只把支付非空参数加入验证 它把我传的参数也带过来了!!!!!!!!!!! 202 | func (a *Alipay) verify(vals url.Values, fields []string) (err error) { 203 | var signature, notifyID string 204 | kvs := KVpairs{} 205 | for key := range vals { 206 | if len(fields) > 0 && !Contains(fields, key) { 207 | continue 208 | } 209 | 210 | val := vals.Get(key) 211 | 212 | if key == "sign" { 213 | signature = val 214 | continue 215 | } 216 | 217 | if key == "sign_type" { 218 | continue 219 | } 220 | 221 | if key == "notify_id" { 222 | notifyID = val 223 | } 224 | /* 225 | var v string 226 | v, err = url.QueryUnescape(val) 227 | if err != nil { 228 | return 229 | } 230 | */kvs = append(kvs, KVpair{K: key, V: val}) 231 | } 232 | 233 | signStr := MD5(kvs.RemoveEmpty().Sort().Join("&"), a.key) 234 | if signStr != signature { 235 | err = fmt.Errorf("%s illegal signature, want %s, got %s", LogPrefix, signature, signStr) 236 | return 237 | } 238 | 239 | err = a.checkNotify(notifyID) 240 | if err != nil { 241 | return 242 | } 243 | return 244 | } 245 | 246 | /* 247 | checkNotify 直接访问支付宝借口判断请求是否有效 248 | 249 | 得到的处理结果有两种: 250 | 成功时:true 251 | 不成功时:报对应错误 252 | */ 253 | func (a *Alipay) checkNotify(notifyID string) (err error) { 254 | if notifyID == "" { 255 | err = ErrNotFoundNotifyID 256 | return 257 | } 258 | 259 | vals := url.Values{} 260 | vals.Set("service", "notify_verify") 261 | vals.Set("partner", a.partner) 262 | vals.Set("notify_id", notifyID) 263 | 264 | r, err := http.NewRequest("GET", AlipayGateway+vals.Encode(), nil) 265 | if err != nil { 266 | return 267 | } 268 | 269 | client := &http.Client{ 270 | Transport: &http.Transport{ 271 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 272 | DisableCompression: true, 273 | }, 274 | } 275 | 276 | resp, err := client.Do(r) 277 | if err != nil { 278 | return 279 | } 280 | defer resp.Body.Close() 281 | 282 | var body []byte 283 | body, err = ioutil.ReadAll(resp.Body) 284 | if err != nil { 285 | return 286 | } 287 | 288 | if string(body) != "true" { 289 | err = fmt.Errorf("%s illegal notify.%s got %s", LogPrefix, AlipayGateway+vals.Encode(), string(body)) 290 | return 291 | } 292 | return 293 | } 294 | 295 | func parsePostData(req *http.Request) (vals url.Values, err error) { 296 | var formStr = []byte(req.Form.Encode()) 297 | if len(formStr) > 0 { 298 | var fields []string 299 | fields = strings.Split(string(formStr), "&") 300 | 301 | vals = url.Values{} 302 | data := map[string]string{} 303 | for _, field := range fields { 304 | f := strings.SplitN(field, "=", 2) 305 | key, val := f[0], f[1] 306 | data[key] = val 307 | vals.Set(key, val) 308 | } 309 | } 310 | return 311 | } 312 | -------------------------------------------------------------------------------- /mobile_payment.go: -------------------------------------------------------------------------------- 1 | package alipay 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "net/url" 9 | ) 10 | 11 | var initMobileParamMap = map[string]bool{ 12 | "service": true, // service 接口名称 String 接口名称,固定值。 不可空 mobile.securitypay.pay 13 | "partner": true, // partner 合作者身份ID String(16) 签约的支付宝账号对应的支付宝唯一用户号。以2088开头的16位纯数字组成。 不可空 2088101568358171 14 | "_input_charset": true, // _input_charset 参数编码字符集 String 商户网站使用的编码格式,固定为utf-8。 不可空 utf-8 15 | "sign_type": true, // sign_type 签名方式 String 签名类型,目前仅支持RSA。 不可空 RSA 16 | "sign": true, // sign 签名 String 请参见签名。 不可空 lBBK%2F0w5LOajrMrji7DUgEqNjIhQbidR13GovA5r3TgIbNqv231yC1NksLdw%2Ba3JnfHXoXuet6XNNHtn7VE%2BeCoRO1O%2BR1KugLrQEZMtG5jmJI 17 | "notify_url": true, // notify_url 服务器异步通知页面路径 String(200) 支付宝服务器主动通知商户网站里指定的页面http路径。 不可空 http://notify.msp.hk/notify.htm 18 | "app_id": false, // app_id 客户端号 String 标识客户端。 可空 external 19 | "appenv": false, // appenv 客户端来源 String 标识客户端来源。参数值内容约定如下:appenv=”system=客户端平台名^version=业务系统版本” 可空 appenv=”system=android^version=3.0.1.2” 20 | "out_trade_no": true, // out_trade_no 商户网站唯一订单号 String(64) 支付宝合作商户网站唯一订单号。 不可空 0819145412-6177 21 | "subject": true, // subject 商品名称 String(128) 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字。 不可空 测试 22 | "payment_type": true, // payment_type 支付类型 String(4) 支付类型。默认值为:1(商品购买)。 不可空 1 23 | "seller_id": true, // seller_id 卖家支付宝账号 String(16) 卖家支付宝账号(邮箱或手机号码格式)或其对应的支付宝唯一用户号(以2088开头的纯16位数字)。 不可空 xxx@alipay.com 24 | "total_fee": true, // total_fee 总金额 Number 该笔订单的资金总额,单位为RMB-Yuan。取值范围为[0.01,100000000.00],精确到小数点后两位。 不可空 0.01 25 | "body": true, // body 商品详情 String(512) 对一笔交易的具体描述信息。如果是多种商品,请将商品描述字符串累加传给body。 不可空 测试测试 26 | "goods_type": false, // goods_type 商品类型 String(1) 具体区分本地交易的商品类型。 1:实物交易; 0:虚拟交易。 默认为1(实物交易)。 可空 1 27 | "rn_check": false, // rn_check 是否发起实名校验 String(1) T:发起实名校验; F:不发起实名校验。 可空 T 28 | "it_b_pay": false, // it_b_pay 未付款交易的超时时间 String 设置未付款交易的超时时间,一旦超时,该笔交易就会自动被关闭。当用户输入支付密码、点击确认付款后(即创建支付宝交易后)开始计时。取值范围:1m~15d,或者使用绝对时间(示例格式:2014-06-13 16:00:00)。m-分钟,h-小时,d-天,1c-当天(1c-当天的情况下,无论交易何时创建,都在0点关闭)。该参数数值不接受小数点,如1.5h,可转换为90m。 可空 30m 29 | "extern_token": false, // extern_token 授权令牌 String(32) 开放平台返回的包含账户信息的token(授权令牌,商户在一定时间内对支付宝某些服务的访问权限)。通过授权登录后获取的alipay_open_id,作为该参数的value,登录授权账户即会为支付账户。 可空 1b258b84ed2faf3e88b4d979ed9fd4db 30 | "out_context": false, // out_context 商户业务扩展参数 String(128) 业务扩展参数,支付宝特定的业务需要添加该字段,json格式。 商户接入时和支付宝协商确定。 可空 {“ccode”:“shanghai”,“no”:“2014052600006128”} 31 | 32 | } 33 | 34 | type MobilePaymentNotify struct { 35 | NotifyTime string // notify_time 通知时间 Date 通知的发送时间。格式为yyyy-MM-dd HH:mm:ss。 不可空 2013-08-22 14:45:24 36 | NotifyType string // notify_type 通知类型 String 通知的类型。 不可空 trade_status_sync 37 | NotifyID string // notify_id 通知校验ID String 通知校验ID。 不可空 64ce1b6ab92d00ede0ee56ade98fdf2f4c 38 | SignType string // sign_type 签名方式 String 固定取值为RSA。 不可空 RSA 39 | Sign string // sign 签名 String 请参见签名机制。 不可空 lBBK%2F0w5LOajrMrji7DUgEqNjIhQbidR13GovA5r3TgIbNqv231yC1NksLdw%2Ba3JnfHXoXuet6XNNHtn7VE%2BeCoRO1O%2BR1KugLrQEZMtG5jmJI 40 | OutTradeNo string // out_trade_no 商户网站唯一订单号 String(64) 对应商户网站的订单系统中的唯一订单号,非支付宝交易号。需保证在商户网站中的唯一性。是请求时对应的参数,原样返回。 可空 082215222612710 41 | Subject string // subject 商品名称 String(128) 商品的标题/交易标题/订单标题/订单关键字等。它在支付宝的交易明细中排在第一列,对于财务对账尤为重要。是请求时对应的参数,原样通知回来。 可空 测试 42 | PaymentType string // payment_type 支付类型 String(4) 支付类型。默认值为:1(商品购买)。 可空 1 43 | TradeNo string // trade_no 支付宝交易号 String(64) 该交易在支付宝系统中的交易流水号。最短16位,最长64位。 不可空 2013082244524842 44 | TradeStatus string // trade_status 交易状态 String 交易状态,取值范围请参见“交易状态”。 不可空 TRADE_SUCCESS 45 | SellerID string // seller_id 卖家支付宝用户号 String(30) 卖家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 不可空 2088501624816263 46 | SellerEmail string // seller_email 卖家支付宝账号 String(100) 卖家支付宝账号,可以是email和手机号码。 不可空 xxx@alipay.com 47 | BuyerID string // buyer_id 买家支付宝用户号 String(30) 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 不可空 2088602315385429 48 | BuyerEmail string // buyer_email 买家支付宝账号 String(100) 买家支付宝账号,可以是Email或手机号码。 不可空 dlwdgl@gmail.com 49 | TotalFee float64 // total_fee 交易金额 Number 该笔订单的总金额。请求时对应的参数,原样通知回来。 不可空 1.00 50 | Quantity int64 // quantity 购买数量 Number 购买数量,固定取值为1(请求时使用的是total_fee)。 可空 1 51 | Price float64 // price 商品单价 Number price等于total_fee(请求时使用的是total_fee)。 可空 1.00 52 | Body string // body 商品描述 String(512) 该笔订单的备注、描述、明细等。对应请求时的body参数,原样通知回来。 可空 测试测试 53 | GMTCreate string // gmt_create 交易创建时间 Date 该笔交易创建的时间。格式为yyyy-MM-dd HH:mm:ss。 可空 2013-08-22 14:45:23 54 | GMTPayment string // gmt_payment 交易付款时间 Date 该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:ss。 可空 2013-08-22 14:45:24 55 | IsTotalFeeAdjust string // is_total_fee_adjust 是否调整总价 String(1) 该交易是否调整过价格。 可空 N 56 | UseCoupon string // use_coupon 是否使用红包买家 String(1) 是否在交易过程中使用了红包。 可空 N 57 | Discount float64 // discount 折扣 String 支付宝系统会把discount的值加到交易金额上,如果有折扣,本参数为负数,单位为元。 可空 0.00 58 | RefundStatus string // refund_status 退款状态 String 取值范围请参见“退款状态”。 可空 REFUND_SUCCESS 59 | GMTRefund string // gmt_refund 退款时间 Date 卖家退款的时间,退款通知时会发送。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-29 19:38:25 60 | } 61 | 62 | func (a *Alipay) MobilePayment(outTradeNo, subject string, totalFee float64, notifyURL string, extraParams map[string]string) (s string, err error) { 63 | if outTradeNo == "" { 64 | err = fmt.Errorf("%s out_trade_no : Required parameter missing", LogPrefix) 65 | return 66 | } 67 | 68 | if subject == "" { 69 | err = fmt.Errorf("%s subject is required parameter", LogPrefix) 70 | return 71 | } 72 | 73 | if notifyURL == "" { 74 | err = fmt.Errorf("%s notify_url is required parameter", LogPrefix) 75 | return 76 | } 77 | 78 | if totalFee == 0 { 79 | err = fmt.Errorf("%s total_fee is required parameter", LogPrefix) 80 | return 81 | } 82 | 83 | if a.privateKey == nil { 84 | err = fmt.Errorf("%s rsa private key is not init", LogPrefix) 85 | return 86 | } 87 | 88 | params := a.initParams(outTradeNo, subject, notifyURL, totalFee, extraParams) 89 | kvs, err := GenKVpairs(initMobileParamMap, params, "sign", "sign_type") 90 | if err != nil { 91 | return 92 | } 93 | 94 | for i, kv := range kvs { 95 | kvs[i] = KVpair{K: kv.K, V: fmt.Sprintf(`"%s"`, kv.V)} 96 | } 97 | 98 | var sig string 99 | sig, err = a.rsaSign(kvs) 100 | if err != nil { 101 | return 102 | } 103 | 104 | kvs = append(kvs, KVpair{K: "sign", V: fmt.Sprintf(`"%s"`, url.QueryEscape(sig))}) 105 | kvs = append(kvs, KVpair{K: "sign_type", V: `"RSA"`}) 106 | 107 | s = kvs.Join("&") 108 | return 109 | } 110 | 111 | func (a *Alipay) initParams(outTradeNo, subject, notifyURL string, totalFee float64, extraParams map[string]string) (params map[string]string) { 112 | params = make(map[string]string) 113 | 114 | params["service"] = "mobile.securitypay.pay" 115 | params["_input_charset"] = "utf-8" 116 | params["payment_type"] = "1" 117 | 118 | params["partner"] = a.partner 119 | params["seller_id"] = a.partner 120 | 121 | params["notify_url"] = notifyURL 122 | params["out_trade_no"] = outTradeNo 123 | params["total_fee"] = strconv.FormatFloat(totalFee, 'f', 2, 64) 124 | params["subject"] = subject 125 | params["body"] = subject 126 | 127 | if extraParams != nil { 128 | for k, v := range extraParams { 129 | _, ok := instantCreditParamMap[k] 130 | if ok { 131 | params[k] = v 132 | } 133 | } 134 | } 135 | return 136 | } 137 | 138 | func (a *Alipay) MobilePaymentNotify(req *http.Request) (result *MobilePaymentNotify, err error) { 139 | vals, err := parsePostData(req) 140 | if err != nil { 141 | return 142 | } 143 | 144 | if len(vals) == 0 { 145 | err = ErrNotifyDataIsEmpty 146 | return 147 | } 148 | 149 | var fields = []string{ 150 | "notify_time", 151 | "notify_type", 152 | "notify_id", 153 | "sign_type", 154 | "sign", 155 | "out_trade_no", 156 | "subject", 157 | "payment_type", 158 | "trade_no", 159 | "trade_status", 160 | "seller_id", 161 | "seller_email", 162 | "buyer_id", 163 | "buyer_email", 164 | "total_fee", 165 | "quantity", 166 | "price", 167 | "body", 168 | "gmt_create", 169 | "gmt_payment", 170 | "is_total_fee_adjust", 171 | "use_coupon", 172 | "discount", 173 | "refund_status", 174 | "gmt_refund", 175 | "gmt_close", 176 | } 177 | 178 | err = a.rsaVerify(vals, fields) 179 | if err != nil { 180 | return 181 | } 182 | 183 | var price, totalFee, discount float64 184 | price, _ = strconv.ParseFloat(vals.Get("price"), 64) 185 | totalFee, _ = strconv.ParseFloat(vals.Get("total_fee"), 64) 186 | discount, _ = strconv.ParseFloat(vals.Get("discount"), 64) 187 | 188 | var quantity int64 189 | quantity, _ = strconv.ParseInt(vals.Get("quantity"), 10, 64) 190 | 191 | result = &MobilePaymentNotify{ 192 | NotifyTime: vals.Get("notify_time"), 193 | NotifyType: vals.Get("notify_type"), 194 | NotifyID: vals.Get("notify_id"), 195 | SignType: vals.Get("sign_type"), 196 | Sign: vals.Get("sign"), 197 | OutTradeNo: vals.Get("out_trade_no"), 198 | Subject: vals.Get("subject"), 199 | PaymentType: vals.Get("payment_type"), 200 | TradeNo: vals.Get("trade_no"), 201 | TradeStatus: vals.Get("trade_status"), 202 | SellerID: vals.Get("seller_id"), 203 | SellerEmail: vals.Get("seller_email"), 204 | BuyerID: vals.Get("buyer_id"), 205 | BuyerEmail: vals.Get("buyer_email"), 206 | TotalFee: totalFee, 207 | Quantity: quantity, 208 | Price: price, 209 | Body: vals.Get("body"), 210 | GMTCreate: vals.Get("gmt_create"), 211 | GMTPayment: vals.Get("gmt_payment"), 212 | IsTotalFeeAdjust: vals.Get("is_total_fee_adjust"), 213 | UseCoupon: vals.Get("use_coupon"), 214 | Discount: discount, 215 | RefundStatus: vals.Get("refund_status"), 216 | GMTRefund: vals.Get("gmt_refund"), 217 | } 218 | 219 | return 220 | } 221 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /instant_credit.go: -------------------------------------------------------------------------------- 1 | package alipay 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strconv" 8 | ) 9 | 10 | var instantCreditParamMap = map[string]bool{ 11 | "service": true, // 接口名称 String 接口名称。 不可空 create_direct_pay_by_user 12 | "partner": true, // 合作者身份ID String(16) 签约的支付宝账号对应的支付宝唯一用户号。以2088开头的16位纯数字组成。 不可空 2088101011913539 13 | "_input_charset": true, // 参数编码字符集 String 商户网站使用的编码格式,如utf-8、gbk、gb2312等。 不可空 gbk 14 | "sign_type": true, // 签名方式 String DSA、RSA、MD5三个值可选,必须大写。 不可空 MD5 15 | "sign": true, // 签名 String 请参见签名 不可空 7d314d22efba4f336fb187697793b9d2 16 | "notify_url": false, // 服务器异步通知页面路径 String(190) 支付宝服务器主动通知商户网站里指定的页面http路径。 可空 http://api.test.alipay.net/atinterface/receive_return.htm 17 | "return_url": false, // 页面跳转同步通知页面路径 String(200) 支付宝处理完请求后,当前页面自动跳转到商户网站里指定页面的http路径。 可空 http://api.test.alipay.net/atinterface/receive_return.htm 18 | "error_notify_url": false, // 请求出错时的通知页面路径 String(200) 当商户通过该接口发起请求时,如果出现提示报错,支付宝会根据请求出错时的通知错误码通过异步的方式发送通知给商户。该功能需要联系支付宝开通。 可空 http://api.test.alipay.net/atinterface/receive_return.htm 19 | "out_trade_no": true, // 商户网站唯一订单号 String(64) 支付宝合作商户网站唯一订单号。 不可空 6843192280647118 20 | "subject": true, // 商品名称 String(256) 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字。 不可空 贝尔金护腕式 21 | "payment_type": true, // 支付类型 String(4) 取值范围请参见附录收款类型”。默认值为:1(商品购买)。注意:支付类型为“47”时,公共业务扩展参数(extend_param)中必须包含凭证号(evoucheprod_evouche_id)参数名和参数值。 不可空 1 22 | "total_fee": true, // 交易金额 Number 该笔订单的资金总额,单位为RMB-Yuan。取值范围为[0.01,100000000.00],精确到小数点后两位。 不可空 100 23 | "seller_id": true, // 卖家支付宝用户号 String(16) 卖家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 不可空 2088002007018966 24 | "buyer_id": false, // 买家支付宝用户号 String(16) 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088002007018955 25 | "seller_email": false, // 卖家支付宝账号 String(100) 卖家支付宝账号,格式为邮箱或手机号。 可空 alipay-test01@alipay.com 26 | "buyer_email": false, // 买家支付宝账号 String(100) 买家支付宝账号,格式为邮箱或手机号。 可空 tstable01@alipay.com 27 | "seller_account_name": false, // 卖家别名支付宝账号 String(100) 卖家别名支付宝账号。卖家信息优先级:seller_id>seller_account_name>seller_email。 可空 tstable02@alipay.com 28 | "buyer_account_name": false, // 买家别名支付宝账号 String(100) 买家别名支付宝账号。买家信息优先级:buyer_id>buyer_account_name>buyer_email。 可空 tstable03@alipay.com 29 | "price": false, // 商品单价 Number 单位为:RMB Yuan。取值范围为[0.01,100000000.00],精确到小数点后两位。此参数为单价。规则:price、quantity能代替total_fee。即存在total_fee,就不能存在price和quantity;存在price、quantity,就不能存在total_fee。 可空 10.00 30 | "quantity": false, // 购买数量 Number price、quantity能代替total_fee。即存在total_fee,就不能存在price和quantity;存在price、quantity,就不能存在total_fee。 可空 1 31 | "body": false, // 商品描述 String(1000) 对一笔交易的具体描述信息。如果是多种商品,请将商品描述字符串累加传给body。 可空 美国专业护腕鼠标垫,舒缓式凝胶软垫模拟手腕的自然曲线和运动,创造和缓的GelFlex舒适地带! 32 | "show_url": false, // 商品展示网址 String(400) 收银台页面上,商品展示的超链接。 可空 http://www.360buy.com/product/113714.html 33 | "paymethod": false, // 默认支付方式 String 取值范围:creditPay(信用支付);directPay(余额支付)。如果不设置,默认识别为余额支付。说明:必须注意区分大小写。 可空 directPay 34 | "enable_paymethod": false, // 支付渠道 String 用于控制收银台支付渠道显示,该值的取值范围请参见支付渠道。可支持多种支付渠道显示,以“^”分隔。 可空 directPay^bankPay^cartoon^cash 35 | "need_ctu_check": false, // 网银支付时是否做CTU校验 String 商户在配置了支持CTU(支付宝风险稽查系统)校验权限的前提下,可以选择本次交易是否需要经过CTU校验。Y:做CTU校验;N:不做CTU校验。 可空 Y 36 | "anti_phishing_key": false, // 防钓鱼时间戳 String 通过时间戳查询接口获取的加密支付宝系统时间戳。如果已申请开通防钓鱼时间戳验证,则此字段必填。 可空 587FE3D2858E6B01E30104656E7805E2 37 | "exter_invoke_ip": false, // 客户端IP String(15) 用户在创建交易时,该用户当前所使用机器的IP。如果商户申请后台开通防钓鱼IP地址检查选项,此字段必填,校验用。 可空 128.214.222.111 38 | "extra_common_param": false, // 公用回传参数 String(100) 如果用户请求时传递了该参数,则返回给商户时会回传该参数。 可空 你好,这是测试商户的广告。 39 | "extend_param": false, // 公用业务扩展参数 String 用于商户的特定业务信息的传递,只有商户与支付宝约定了传递此参数且约定了参数含义,此参数才有效。参数格式:参数名1^参数值1|参数名2^参数值2|…… 多条数据用“|”间隔。支付类型(payment_type)为47(电子卡券)时,需要包含凭证号(evoucheprod_evouche_id)参数名和参数值。 可空 pnr^MFGXDW|start_ticket_no^123|end_ticket_no^234|b2b_login_name^abc 40 | "it_b_pay": false, // 超时时间 String 设置未付款交易的超时时间,一旦超时,该笔交易就会自动被关闭。取值范围:1m~15d。m-分钟,h-小时,d-天,1c-当天(无论交易何时创建,都在0点关闭)。该参数数值不接受小数点,如1.5h,可转换为90m。该功能需要联系支付宝配置关闭时间。 可空 1h 41 | "default_login": false, // 自动登录标识 String 用于标识商户是否使用自动登录的流程。如果和参数buyer_email一起使用时,就不会再让用户登录支付宝,即在收银台中不会出现登录页面。取值有以下情况:Y代表使用;N代表不使用。该功能需要联系支付宝配置。 可空 Y 42 | "product_type": false, // 商户申请的产品类型 String(50) 用于针对不同的产品,采取不同的计费策略。如果开通了航旅垂直搜索平台产品,请填写CHANNEL_FAST_PAY;如果没有,则为空。 可空 CHANNEL_FAST_PAY 43 | "token": false, // 快捷登录授权令牌 String(40) 如果开通了快捷登录产品,则需要填写;如果没有开通,则为空。 可空 201103290c9f9f2c03db4267a4c8e1bfe3adfd52 44 | "sign_id_ext": false, // 商户买家签约号 String(50) 用于唯一标识商户买家。如果本参数不为空,则sign_name_ext不能为空。 可空 ZHANGSAN 45 | "sign_name_ext": false, // 商户买家签约名 String(128) 商户买家唯一标识对应的名字。 可空 张三 46 | "qr_pay_mode": false, // 扫码支付方式 String(1) 扫码支付的方式,支持前置模式和跳转模式。 47 | //前置模式是将二维码前置到商户的订单确认页的模式。需要商户在自己的页面中以iframe方式请求支付宝页面。具体分为以下3种: 48 | //0:订单码-简约前置模式,对应iframe宽度不能小于600px,高度不能小于300px; 49 | //1:订单码-前置模式,对应iframe宽度不能小于300px,高度不能小于600px; 50 | //3:订单码-迷你前置模式,对应iframe宽度不能小于75px,高度不能小于75px。 51 | //跳转模式下,用户的扫码界面是由支付宝生成的,不在商户的域名下。 52 | //2:订单码-跳转模式 可空 1 53 | } 54 | 55 | type InstantCreditReturn struct { 56 | IsSuccess string // 成功标识 String(1) 表示接口调用是否成功,并不表明业务处理结果。 不可空 T 57 | SignType string // 签名方式 String DSA、RSA、MD5三个值可选,必须大写。 不可空 MD5 58 | Sign string // 签名 String(32) 请参见签名验证 不可空 b1af584504b8e845ebe40b8e0e733729 59 | OutTradeNo string // 商户网站唯一订单号 String(64) 对应商户网站的订单系统中的唯一订单号,非支付宝交易号。需保证在商户网站中的唯一性。是请求时对应的参数,原样返回。 可空 6402757654153618 60 | Subject string // 商品名称 String(256) 商品的标题/交易标题/订单标题/订单关键字等。 可空 手套 61 | PaymentType string // 支付类型 String(4) 对应请求时的payment_type参数,原样返回。 可空 1 62 | Exterface string // 接口名称 String 标志调用哪个接口返回的链接。 可空 create_direct_pay_by_user 63 | TradeNo string // 支付宝交易号 String(64) 该交易在支付宝系统中的交易流水号。最长64位。 可空 2014040311001004370000361525 64 | TradeStatus string // 交易状态 String 交易目前所处的状态。成功状态的值只有两个:TRADE_FINISHED(普通即时到账的交易成功状态);TRADE_SUCCESS(开通了高级即时到账或机票分销产品后的交易成功状态) 可空 TRADE_FINISHED 65 | NotifyID string // 通知校验ID String 支付宝通知校验ID,商户可以用这个流水号询问支付宝该条通知的合法性。 可空 RqPnCoPT3K9%2Fvwbh3I%2BODmZS9o4qChHwPWbaS7UMBJpUnBJlzg42y9A8gQlzU6m3fOhG 66 | NotifyTime string // 通知时间 Date 通知时间(支付宝时间)。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-23 13:17:39 67 | NotifyType string // 通知类型 String 返回通知类型。 可空 trade_status_sync 68 | SellerEmail string // 卖家支付宝账号 String(100) 卖家支付宝账号,可以是Email或手机号码。 可空 chao.chenc1@alipay.com 69 | BuyerEmail string // 买家支付宝账号 String(100) 买家支付宝账号,可以是Email或手机号码。 可空 tstable01@alipay.com 70 | SellerID string // 卖家支付宝账户号 String(30) 卖家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088002007018916 71 | BuyerID string // 买家支付宝账户号 String(30) 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088101000082594 72 | TotalFee float64 // 交易金额 Number 该笔订单的资金总额,单位为RMB-Yuan。取值范围为[0.01,100000000.00],精确到小数点后两位。 可空 10.00 73 | Body string // 商品描述 String(400) 对一笔交易的具体描述信息。如果是多种商品,请将商品描述字符串累加传给body。 可空 Hello 74 | ExtraCommonParam string // 公用回传参数 String 用于商户回传参数,该值不能包含“=”、“&”等特殊字符。如果用户请求时传递了该参数,则返回给商户时会回传该参数。 可空 你好,这是测试商户的广告。 75 | AgentUserID string // 信用支付购票员的代理人ID String 本参数用于信用支付。它代表执行支付操作的操作员账号所属的代理人的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088101000071628 76 | } 77 | 78 | type InstantCreditNotify struct { 79 | NotifyTime string // 通知时间 Date 通知的发送时间。格式为yyyy-MM-dd HH:mm:ss。 不可空 2009-08-12 11:08:32 80 | NotifyType string // 通知类型 String 通知的类型。 不可空 trade_status_sync 81 | NotifyID string // 通知校验ID String 通知校验ID。 不可空 70fec0c2730b27528665af4517c27b95 82 | SignType string // 签名方式 String DSA、RSA、MD5三个值可选,必须大写。 不可空 DSA 83 | Sign string // 签名 String 请参见签名验证。 不可空 _p_w_l_h_j0b_gd_aejia7n_ko4_m%2Fu_w_jd3_nx_s_k_mxus9_hoxg_y_r_lunli_pmma29_t_q%3D 84 | OutTradeNo string // 商户网站唯一订单号 String(64) 对应商户网站的订单系统中的唯一订单号,非支付宝交易号。需保证在商户网站中的唯一性。是请求时对应的参数,原样返回。 可空 3618810634349901 85 | Subject string // 商品名称 String(256) 商品的标题/交易标题/订单标题/订单关键字等。它在支付宝的交易明细中排在第一列,对于财务对账尤为重要。是请求时对应的参数,原样通知回来。 可空 phone手机 86 | PaymentType string // 支付类型 String(4) 取值范围请参见收款类型。 可空 1 87 | TradeNo string // 支付宝交易号 String(64) 该交易在支付宝系统中的交易流水号。最长64位。 可空 2014040311001004370000361525 88 | TradeStatus string // 交易状态 String 取值范围请参见交易状态。 可空 TRADE_FINISHED 89 | GMTCreate string // 交易创建时间 Date 该笔交易创建的时间。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-22 20:49:31 90 | GMTPayment string // 交易付款时间 Date 该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-22 20:49:50 91 | GMTClose string // 交易关闭时间 Date 交易关闭时间。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-22 20:49:46 92 | RefundStatus string // 退款状态 String 取值范围请参见退款状态。 可空 REFUND_SUCCESS 93 | GMTRefund string // 退款时间 Date 卖家退款的时间,退款通知时会发送。格式为yyyy-MM-dd HH:mm:ss。 可空 2008-10-29 19:38:25 94 | SellerEmail string // 卖家支付宝账号 String(100) 卖家支付宝账号,可以是email和手机号码。 可空 chao.chenc1@alipay.com 95 | BuyerEmail string // 买家支付宝账号 String(100) 买家支付宝账号,可以是Email或手机号码。 可空 13758698870 96 | SellerID string // 卖家支付宝账户号 String(30) 卖家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088002007018916 97 | BuyerID string // 买家支付宝账户号 String(30) 买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字。 可空 2088002007013600 98 | Price float64 // 商品单价 Number 如果请求时使用的是total_fee,那么price等于total_fee;如果请求时使用的是price,那么对应请求时的price参数,原样通知回来。 可空 10.00 99 | TotalFee float64 // 交易金额 Number 该笔订单的总金额。请求时对应的参数,原样通知回来。 可空 10.00 100 | Quantity int64 // 购买数量 Number 如果请求时使用的是total_fee,那么quantity等于1;如果请求时使用的是quantity,那么对应请求时的quantity参数,原样通知回来。 可空 1 101 | Body string // 商品描述 String(400) 该笔订单的备注、描述、明细等。对应请求时的body参数,原样通知回来。 可空 Hello 102 | Discount float64 // 折扣 Number 支付宝系统会把discount的值加到交易金额上,如果需要折扣,本参数为负数。 可空 -5 103 | IsTotalFeeAdjust string // 是否调整总价 String(1) 该交易是否调整过价格。 可空 N 104 | UseCoupon string // 是否使用红包买家 String(1) 是否在交易过程中使用了红包。 可空 N 105 | ExtraCommonParam string // 公用回传参数 String 用于商户回传参数,该值不能包含“=”、“&”等特殊字符。如果用户请求时传递了该参数,则返回给商户时会回传该参数。 可空 你好,这是测试商户的广告。 106 | OutChannelType string // 支付渠道组合信息 String 该笔交易所使用的支付渠道。格式为:渠道1|渠道2|…,如果有多个渠道,用“|”隔开。取值范围请参见渠道类型说明与币种列表。 可空 OPTIMIZED_MOTO|BALANCE 107 | OutChannelAmount string // 支付金额组合信息 String 该笔交易通过使用各支付渠道所支付的金额。格式为:金额1|金额2|…,如果有多个支付渠道,各渠道所支付金额用“|”隔开。 可空 90.00|10.00 108 | OutChannelInst string // 实际支付渠道 String 该交易支付时实际使用的银行渠道。格式为:支付渠道1|支付渠道2|…,如果有多个支付渠道,用“|”隔开。取值范围请参见实际支付渠道列表。该参数需要联系支付宝开通。 可空 ICBC 109 | BusinessScene string // 是否扫码支付 String 回传给商户此标识为qrpay时,表示对应交易为扫码支付。目前只有qrpay一种回传值。非扫码支付方式下,目前不会返回该参数。 可空 qrpay 110 | } 111 | 112 | func (a *Alipay) InstantCredit(outTradeNo, subject string, totalFee float64, extraParams map[string]string) (checkoutURL string, err error) { 113 | if outTradeNo == "" { 114 | err = fmt.Errorf("%s out_trade_no : Required parameter missing", LogPrefix) 115 | return 116 | } 117 | 118 | if subject == "" { 119 | err = fmt.Errorf("%s subject is required parameter", LogPrefix) 120 | return 121 | } 122 | 123 | if totalFee == 0 { 124 | err = fmt.Errorf("%s total_fee is required parameter", LogPrefix) 125 | return 126 | } 127 | 128 | params := a.initInstantCreditParams(outTradeNo, subject, totalFee, extraParams) 129 | kvs, err := GenKVpairs(instantCreditParamMap, params, "sign", "sign_type") 130 | if err != nil { 131 | return 132 | } 133 | 134 | signStr := MD5(kvs.RemoveEmpty().Sort().Join("&"), a.key) 135 | 136 | vals := url.Values{} 137 | for _, v := range kvs { 138 | vals.Set(v.K, v.V) 139 | } 140 | vals.Set("sign", signStr) 141 | vals.Set("sign_type", "MD5") 142 | 143 | checkoutURL = AlipayGateway + vals.Encode() 144 | return 145 | } 146 | 147 | func (a *Alipay) initInstantCreditParams(outTradeNo, subject string, totalFee float64, extraParams map[string]string) (params map[string]string) { 148 | params = make(map[string]string) 149 | 150 | params["service"] = "create_direct_pay_by_user" 151 | params["_input_charset"] = "utf-8" 152 | params["payment_type"] = "1" 153 | 154 | params["partner"] = a.partner 155 | params["key"] = a.key 156 | params["seller_id"] = a.partner 157 | params["seller_email"] = a.email 158 | 159 | params["out_trade_no"] = outTradeNo 160 | params["total_fee"] = strconv.FormatFloat(totalFee, 'f', 2, 64) 161 | params["subject"] = subject 162 | 163 | if extraParams != nil { 164 | for k, v := range extraParams { 165 | _, ok := instantCreditParamMap[k] 166 | if ok { 167 | params[k] = v 168 | } 169 | } 170 | } 171 | return 172 | } 173 | 174 | /* 175 | 商户需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号, 176 | 并判断total_fee是否确实为该订单的实际金额(即商户订单创建时的金额), 177 | 同时需要校验通知中的seller_id(或者seller_email) 178 | 是否为out_trade_no这笔单据的对应的操作方(有的时候, 179 | 一个商户可能有多个seller_id/seller_email), 180 | 上述有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。 181 | 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。 182 | 在支付宝的业务通知中,只有交易通知状态为TRADE_SUCCESS或TRADE_FINISHED时,支付宝才会认定为买家付款成功。 183 | 如果商户需要对同步返回的数据做验签,必须通过服务端的签名验签代码逻辑来实现。如果商户未正确处理业务通知,存在潜在的风险,商户自行承担因此而产生的所有损失。 184 | 185 | 交易状态TRADE_SUCCESS的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功; 186 | 交易状态TRADE_FINISHED的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限; 187 | 交易成功之后,商户(高级即时到账或机票平台商)可调用批量退款接口,系统会发送退款通知给商户,具体内容请参见批量退款接口文档; 188 | 当商户使用站内退款时,系统会发送包含refund_status和gmt_refund字段的通知给商户。 189 | */ 190 | func (a *Alipay) InstantCreditReturn(req *http.Request) (result *InstantCreditReturn, err error) { 191 | vals := req.URL.Query() 192 | if len(vals) == 0 { 193 | err = ErrReturnDataIsEmpty 194 | return 195 | } 196 | 197 | var fields = []string{ 198 | "is_success", 199 | "sign_type", 200 | "sign", 201 | "out_trade_no", 202 | "subject", 203 | "payment_type", 204 | "exterface", 205 | "trade_no", 206 | "trade_status", 207 | "notify_id", 208 | "notify_time", 209 | "notify_type", 210 | "seller_email", 211 | "buyer_email", 212 | "seller_id", 213 | "buyer_id", 214 | "total_fee", 215 | "body", 216 | "extra_common_param", 217 | "agent_user_id", 218 | } 219 | 220 | err = a.verify(vals, fields) 221 | if err != nil { 222 | return 223 | } 224 | 225 | totalFee, _ := strconv.ParseFloat(vals.Get("total_fee"), 64) 226 | 227 | result = &InstantCreditReturn{ 228 | IsSuccess: vals.Get("is_success"), 229 | SignType: vals.Get("sign_type"), 230 | Sign: vals.Get("sign"), 231 | OutTradeNo: vals.Get("out_trade_no"), 232 | Subject: vals.Get("subject"), 233 | PaymentType: vals.Get("payment_type"), 234 | Exterface: vals.Get("exterface"), 235 | TradeNo: vals.Get("trade_no"), 236 | TradeStatus: vals.Get("trade_status"), 237 | NotifyID: vals.Get("notify_id"), 238 | NotifyTime: vals.Get("notify_time"), 239 | NotifyType: vals.Get("notify_type"), 240 | SellerEmail: vals.Get("seller_email"), 241 | BuyerEmail: vals.Get("buyer_email"), 242 | SellerID: vals.Get("seller_id"), 243 | BuyerID: vals.Get("buyer_id"), 244 | TotalFee: totalFee, 245 | Body: vals.Get("body"), 246 | ExtraCommonParam: vals.Get("extra_common_param"), 247 | AgentUserID: vals.Get("agent_user_id"), 248 | } 249 | return 250 | } 251 | 252 | func (a *Alipay) InstantCreditNotify(req *http.Request) (result *InstantCreditNotify, err error) { 253 | vals, err := parsePostData(req) 254 | if len(vals) == 0 { 255 | err = ErrNotifyDataIsEmpty 256 | return 257 | } 258 | 259 | var fields = []string{ 260 | "notify_time", 261 | "notify_type", 262 | "notify_id", 263 | "sign_type", 264 | "sign", 265 | "out_trade_no", 266 | "subject", 267 | "payment_type", 268 | "trade_no", 269 | "trade_status", 270 | "gmt_create", 271 | "gmt_payment", 272 | "gmt_close", 273 | "refund_status", 274 | "gmt_refund", 275 | "seller_email", 276 | "buyer_email", 277 | "seller_id", 278 | "buyer_id", 279 | "price", 280 | "total_fee", 281 | "quantity", 282 | "body", 283 | "discount", 284 | "is_total_fee_adjust", 285 | "use_coupon", 286 | "extra_common_param", 287 | "out_channel_type", 288 | "out_channel_amount", 289 | "out_channel_inst", 290 | "business_scene", 291 | } 292 | 293 | err = a.verify(vals, fields) 294 | if err != nil { 295 | return 296 | } 297 | 298 | var price, totalFee, discount float64 299 | price, _ = strconv.ParseFloat(vals.Get("price"), 64) 300 | totalFee, _ = strconv.ParseFloat(vals.Get("total_fee"), 64) 301 | discount, _ = strconv.ParseFloat(vals.Get("discount"), 64) 302 | 303 | var quantity int64 304 | quantity, _ = strconv.ParseInt(vals.Get("quantity"), 10, 64) 305 | 306 | result = &InstantCreditNotify{ 307 | NotifyTime: vals.Get("notify_time"), 308 | NotifyType: vals.Get("notify_type"), 309 | NotifyID: vals.Get("notify_id"), 310 | SignType: vals.Get("sign_type"), 311 | Sign: vals.Get("sign"), 312 | OutTradeNo: vals.Get("out_trade_no"), 313 | Subject: vals.Get("subject"), 314 | PaymentType: vals.Get("payment_type"), 315 | TradeNo: vals.Get("trade_no"), 316 | TradeStatus: vals.Get("trade_status"), 317 | GMTCreate: vals.Get("gmt_create"), 318 | GMTPayment: vals.Get("gmt_payment"), 319 | GMTClose: vals.Get("gmt_close"), 320 | RefundStatus: vals.Get("refund_status"), 321 | GMTRefund: vals.Get("gmt_refund"), 322 | SellerEmail: vals.Get("seller_email"), 323 | BuyerEmail: vals.Get("buyer_email"), 324 | SellerID: vals.Get("seller_id"), 325 | BuyerID: vals.Get("buyer_id"), 326 | Price: price, 327 | TotalFee: totalFee, 328 | Quantity: quantity, 329 | Body: vals.Get("body"), 330 | Discount: discount, 331 | IsTotalFeeAdjust: vals.Get("is_total_fee_adjust"), 332 | UseCoupon: vals.Get("use_coupon"), 333 | ExtraCommonParam: vals.Get("extra_common_param"), 334 | OutChannelType: vals.Get("out_channel_type"), 335 | OutChannelAmount: vals.Get("out_channel_amount"), 336 | OutChannelInst: vals.Get("out_channel_inst"), 337 | BusinessScene: vals.Get("business_scene"), 338 | } 339 | 340 | return 341 | } 342 | --------------------------------------------------------------------------------