├── 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 |
--------------------------------------------------------------------------------