github.com/keysonZZZ/kmg@v0.0.0-20151121023212-05317bfd7d39/third/kmgAlipay/Oversea.go (about) 1 package kmgAlipay 2 3 import ( 4 "bytes" 5 "encoding/xml" 6 "fmt" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/bronze1man/kmg/kmgControllerRunner" 12 "github.com/bronze1man/kmg/kmgCrypto" 13 "github.com/bronze1man/kmg/kmgLog" 14 "github.com/bronze1man/kmg/kmgNet/kmgHttp" 15 "github.com/bronze1man/kmg/kmgStrconv" 16 ) 17 18 /* 19 境外收单接口系列 20 文档在此 https://download.alipay.com/ui/doc/global/cross-border_website_payment.zip 21 看英文版,中文版是旧的,少一些功能 22 已经出现的错误,及其可能原因: 23 * 不能重复创建交易,请返回订单页面重新付款,或重新登录支付宝付款。 同一个订单号,如果已经被支付成功了,不能再被创建支付了 24 25 */ 26 27 // 请调用Init初始化一下 28 // 请在进程初始化的时候进行Init.不要做懒加载. 29 type OverseaTrade struct { 30 PartnerId string 31 SecurityCode string 32 33 //自己网站的scheme和host,在支付回调处使用. 例如 https://www.abc.com ,注意最后不要加入 / 34 SelfSchemeAndHost string 35 // 支付处理回调, 支付成功必须处理,关闭可选处理. 36 // @deprecated 请使用 PayFinishCallback 和 PayCloseCallback ,这个接口容易丢掉 是成功还是关闭的处理. 37 PayCallback func(info OverseaTradeTransaction) (err error) 38 // 支付成功处理回调. 出现错误,请直接panic 39 PayFinishCallback func(info OverseaTradeTransaction) 40 // 交易关闭处理回调. 出现问题,请直接panic 41 PayCloseCallback func(info OverseaTradeTransaction) 42 //支付返回页面处理回调,此时已经调用过支付处理回调了. 43 PayReturnPageCallback func(info OverseaTradeTransaction, ctx *kmgHttp.Context) 44 } 45 46 // @deprecated 请使用 InitForPayCallback 或不初始化. 47 func (ot *OverseaTrade) Init() { 48 ot.InitForPayCallback() 49 } 50 51 // 请在进程初始化的时候进行Init.不要做懒加载.避免掉单. 52 func (ot *OverseaTrade) InitForPayCallback() { 53 if ot.SelfSchemeAndHost == "" { 54 panic("支付回调必须填写当前网站scheme和host 如: http://127.0.0.1") 55 } 56 if ot.PayReturnPageCallback == nil { 57 panic("支付回调必须处理同步回调.") 58 } 59 if ot.PayFinishCallback == nil && ot.PayCallback == nil { 60 panic("支付成功必须处理,必须加入支付成功回调.") 61 } 62 kmgControllerRunner.RegisterController(ot) 63 } 64 65 // 注意事项: 66 // 1.不管是使用RmbFee 还是 TotalFee 如果Currency的值是日元,日元金额不能少于1JPY. 67 // 2.同一个订单号,如果已经被支付成功了,不能再被创建支付了 68 // 3.同一个订单号,如果没有支付过,可以随意修改价钱,然后可以再重新提交. 69 type OverseaTradePayRequest struct { 70 Subject string 71 Body string //可选 72 OutTradeNo string // 最长64个字节 73 Currency string 74 //TotalFee 和 RmbFee 二选一,必须选一个 75 TotalFee float64 76 RmbFee float64 //选rmb也需要传入Currency参数,Currency依然填对应的货币,支付宝会反向再算出那个货币需要的金额. 77 Supplier string //可选 78 TimeoutRule string //可选 79 SpecifiedPayChannel string //可选 80 SellerId string //可选 81 SellerName string //可选 82 SellerIndustry string //可选 83 84 // (低级接口)回调url,可以添加query参数.在回调里面请先删除这些query参数, 这个会覆盖掉OverseaTrade里面的那个使用kmgControllerRunner的配置 85 NotifyUrl string 86 ReturnUrl string 87 } 88 89 //用户发起支付,请传入Request,然后redirect到这个函数返回的url里面去. 90 func (ot *OverseaTrade) Pay(req *OverseaTradePayRequest) (url string) { 91 query := map[string]string{ 92 "_input_charset": "utf-8", 93 "service": "create_forex_trade", 94 "partner": ot.PartnerId, 95 "notify_url": ot.SelfSchemeAndHost + "/?n=github.com.bronze1man.kmg.third.kmgAlipay.OverseaTrade.NotifyAction", 96 "return_url": ot.SelfSchemeAndHost + "/?n=github.com.bronze1man.kmg.third.kmgAlipay.OverseaTrade.ReturnPage", 97 "subject": req.Subject, 98 "body": req.Body, 99 "out_trade_no": req.OutTradeNo, 100 "currency": req.Currency, 101 "supplier": req.Supplier, 102 "timeout_rule": req.TimeoutRule, 103 "specified_pay_channel": req.SpecifiedPayChannel, 104 "seller_id": req.SellerId, 105 "seller_name": req.SellerName, 106 "seller_industry": req.SellerIndustry, 107 } 108 if req.NotifyUrl != "" { 109 query["notify_url"] = req.NotifyUrl 110 } 111 if req.ReturnUrl != "" { 112 query["return_url"] = req.ReturnUrl 113 } 114 if req.TotalFee != 0 { 115 if req.Currency == "JPY" { 116 //日元精度是整数,传入小数点会报错,这个文档上没有写,但是经过实验发现是这个样子的. 117 query["total_fee"] = kmgStrconv.FormatFloatPrec0(req.TotalFee) 118 } else { 119 query["total_fee"] = kmgStrconv.FormatFloatPrec2(req.TotalFee) 120 } 121 } 122 if req.RmbFee != 0 { 123 query["rmb_fee"] = kmgStrconv.FormatFloatPrec2(req.RmbFee) 124 } 125 ot.md5Sign(query) 126 kmgLog.Log("Alipay", "Oversea Pay", query) 127 //已经手动验证url里面的参数顺序无关紧要. 128 return kmgHttp.MustSetParameterMapToUrl("https://mapi.alipay.com/gateway.do", query) 129 } 130 131 // 请不要手动调用,这个是自动注册到 kmgControllerRunner里面的 132 func (ot *OverseaTrade) ReturnPage(ctx *kmgHttp.Context) { 133 ctx.DeleteInMap("n") 134 info := ot.MustReturnPage(ctx) 135 ot.payCallbackProceess(info) 136 ot.PayReturnPageCallback(info, ctx) 137 } 138 139 // 请不要手动调用,这个是自动注册到 kmgControllerRunner里面的 140 func (ot *OverseaTrade) NotifyAction(ctx *kmgHttp.Context) { 141 ctx.DeleteInMap("n") 142 ot.mustNotifyActionV2(ctx, ot.payCallbackProceess) 143 } 144 145 func (ot *OverseaTrade) payCallbackProceess(info OverseaTradeTransaction) { 146 if ot.PayCallback != nil { 147 err := ot.PayCallback(info) 148 if err != nil { 149 panic(err) 150 } 151 } 152 if info.TradeStatus == OverseaTradeStatusFinish && ot.PayFinishCallback != nil { 153 ot.PayFinishCallback(info) 154 } 155 if info.TradeStatus == OverseaTradeStatusClose && ot.PayCloseCallback != nil { 156 ot.PayCloseCallback(info) 157 } 158 } 159 160 type OverseaTradeStatus string 161 162 const ( 163 OverseaTradeStatusFinish OverseaTradeStatus = "TRADE_FINISHED" 164 OverseaTradeStatusClose OverseaTradeStatus = "TRADE_CLOSED" 165 ) 166 167 //一条交易信息 168 type OverseaTradeTransaction struct { 169 OutTradeNo string //用户传入的 OutTradeNo 170 Currency string 171 TotalFee float64 //此处是外币的价格,即使传入RmbFee,此处也是外币的价格 172 TradeStatus OverseaTradeStatus //一定是 OverseaTradeStatusFinish 173 TradeNo string //支付宝id 174 Subject string 175 } 176 177 // 同步回调 178 // 调用前请清除您自己的参数. 179 // @deprecated 请使用 OverseaTrade.PayFinishCallback 和 OverseaTrade.PayCloseCallback 180 func (ot *OverseaTrade) MustReturnPage(ctx *kmgHttp.Context) (info OverseaTradeTransaction) { 181 kmgLog.Log("Alipay", "Oversea PayReturnPage", ctx.GetInMap()) 182 var err error 183 info.OutTradeNo = ctx.MustInStr("out_trade_no") 184 info.Currency = ctx.MustInStr("currency") 185 info.TotalFee, err = kmgStrconv.ParseFloat64(ctx.MustInStr("total_fee")) 186 if err != nil { 187 panic(err) 188 } 189 info.TradeStatus = OverseaTradeStatus(ctx.MustInStr("trade_status")) 190 info.TradeNo = ctx.MustInStr("trade_no") 191 //这个也可以验证数据,只是文档上面没写. 192 err = ot.md5Verify(ctx.GetInMap()) 193 if err != nil { 194 panic(err) 195 } 196 // 向支付宝询问这个订单的情况 197 oInfo := ot.MustSingleTransactionQuery(info.OutTradeNo) 198 if oInfo.TradeStatus != info.TradeStatus { 199 panic("两次查询订单状态不一致") 200 } 201 info.Subject = oInfo.Subject 202 return info 203 } 204 205 // 异步回调,请不要在f中输出任何支付串.(字符串?) 206 // 调用前请清除您自己的参数. 207 // @deprecated 请使用 OverseaTrade.PayFinishCallback 和 OverseaTrade.PayCloseCallback 208 func (ot *OverseaTrade) MustNotifyAction(ctx *kmgHttp.Context, f func(info OverseaTradeTransaction) (err error)) { 209 ot.mustNotifyActionV2(ctx, func(info OverseaTradeTransaction) { 210 err := f(info) 211 if err != nil { 212 panic(err) 213 } 214 }) 215 } 216 217 func (ot *OverseaTrade) mustNotifyActionV2(ctx *kmgHttp.Context, f func(info OverseaTradeTransaction)) { 218 kmgLog.Log("Alipay", "Oversea PayNotifyAction", ctx.GetInMap()) 219 var err error 220 ctx.MustPost() 221 info := OverseaTradeTransaction{} 222 //info.NotifyId = ctx.MustInStr("notify_id") 这两项没有什么意义. 223 //info.NotifyTime = kmgTime.MustFromMysqlFormatInLocation(ctx.MustInStr("notify_time"), kmgTime.BeijingZone) 224 info.OutTradeNo = ctx.MustInStr("out_trade_no") 225 226 info.Currency = ctx.MustInStr("currency") 227 info.TotalFee, err = kmgStrconv.ParseFloat64(ctx.MustInStr("total_fee")) 228 if err != nil { 229 panic(err) 230 } 231 info.TradeStatus = OverseaTradeStatus(ctx.MustInStr("trade_status")) 232 info.TradeNo = ctx.MustInStr("trade_no") 233 err = ot.md5Verify(ctx.GetInMap()) 234 if err != nil { 235 panic(err) 236 } 237 err = ot.VerifyNotify(ctx.MustInStr("notify_id")) 238 if err != nil { 239 panic(err) 240 } 241 // 向支付宝询问这个订单的情况 242 oInfo := ot.MustSingleTransactionQuery(info.OutTradeNo) 243 if oInfo.TradeStatus != info.TradeStatus { 244 panic("两次查询订单状态不一致") 245 } 246 info.Subject = oInfo.Subject 247 f(info) 248 ctx.WriteString("success") 249 } 250 251 // 通知验证接口 252 // 和支付宝手机接口一模一样. 253 func (ot *OverseaTrade) VerifyNotify(NotifyId string) (err error) { 254 u := kmgHttp.MustSetParameterMapToUrl("https://mapi.alipay.com/gateway.do", map[string]string{ 255 "service": "notify_verify", 256 "partner": ot.PartnerId, 257 "notify_id": NotifyId, 258 }) 259 content, err := kmgHttp.UrlGetContent(u) 260 if err != nil { 261 return err 262 } 263 if !bytes.Equal(content, []byte(`true`)) { 264 return fmt.Errorf("notify_id verify fail") 265 } 266 return nil 267 } 268 269 type ExchangeRate struct { 270 Time time.Time 271 Currency string 272 Rate float64 273 } 274 275 //下载汇率文件接口 276 func (ot *OverseaTrade) MustGetExchangeRateList() (output []ExchangeRate) { 277 query := map[string]string{ 278 "service": "forex_rate_file", 279 "partner": ot.PartnerId, 280 } 281 ot.md5Sign(query) 282 content := kmgHttp.MustUrlGetContent(kmgHttp.MustSetParameterMapToUrl("https://mapi.alipay.com/gateway.do", query)) 283 284 lineList := strings.Split(string(content), "\n") 285 output = make([]ExchangeRate, 0, len(lineList)) 286 for _, line := range lineList { 287 line := strings.TrimSpace(line) 288 if line == "" { 289 continue 290 } 291 part := strings.Split(line, "|") 292 if len(part) < 4 { 293 panic(fmt.Errorf("[MustGetExchangeRateList] format error")) 294 } 295 t, err := time.Parse("20060102150405", part[0]+part[1]) //考虑别处基本不会使用,就直接写在这个地方了. 296 if err != nil { 297 panic(err) 298 } 299 rate, err := kmgStrconv.ParseFloat64(part[3]) 300 if err != nil { 301 panic(err) 302 } 303 output = append(output, ExchangeRate{ 304 Time: t, 305 Currency: part[2], 306 Rate: rate, 307 }) 308 } 309 return output 310 } 311 312 type overseaTradeTransactionQueryResponse struct { 313 XMLName xml.Name `xml:"alipay"` 314 IsSuccess string `xml:"is_success"` 315 TradeNo string `xml:"response>trade>trade_no"` 316 OutTradeNo string `xml:"response>trade>out_trade_no"` 317 Subject string `xml:"response>trade>subject"` 318 TradeStatus OverseaTradeStatus `xml:"response>trade>trade_status"` 319 Error string `xml:"error"` 320 } 321 322 func (ot *OverseaTrade) MustSingleTransactionQuery(outTradeId string) *OverseaTradeTransaction { 323 tran, err := ot.SingleTransactionQuery(outTradeId) 324 if err != nil { 325 panic(err) 326 } 327 return tran 328 } 329 330 // 给调用者mock用 331 type SingleTransactionQueryer func(outTradeId string) (tran *OverseaTradeTransaction, err error) 332 333 // 单条交易查询接口 334 func (ot *OverseaTrade) SingleTransactionQuery(outTradeId string) (tran *OverseaTradeTransaction, err error) { 335 query := map[string]string{ 336 "service": "single_trade_query", 337 "partner": ot.PartnerId, 338 "_input_charset": "utf-8", 339 "out_trade_no": outTradeId, 340 } 341 ot.md5Sign(query) 342 content := kmgHttp.MustUrlGetContent(kmgHttp.MustSetParameterMapToUrl("https://mapi.alipay.com/gateway.do", query)) 343 response := overseaTradeTransactionQueryResponse{} 344 err = xml.Unmarshal(content, &response) 345 if err != nil { 346 return nil, err 347 } 348 if response.IsSuccess != "T" { 349 return nil, fmt.Errorf("[支付宝单条交易查询接口错误] [%s]", response.Error) 350 } 351 return &OverseaTradeTransaction{ 352 TradeNo: response.TradeNo, 353 OutTradeNo: response.OutTradeNo, 354 Subject: response.Subject, 355 TradeStatus: response.TradeStatus, 356 }, nil 357 } 358 359 // TODO 批量退款 上传退款文件接口 360 // TODO 下载对账文件接口 361 // TODO 单笔退款接口 362 // TODO 退款撤销接口 363 // TODO 下载清算文件接口 364 // TODO 会员共享 ID 接口 365 366 type kv struct { 367 K string 368 V string 369 } 370 type kvSorter []kv 371 372 func (l kvSorter) Len() int { return len(l) } 373 func (l kvSorter) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 374 func (l kvSorter) Less(i, j int) bool { 375 if l[i].K == l[j].K { 376 return l[i].V < l[j].V 377 } 378 ret := l[i].K < l[j].K 379 return ret 380 } 381 382 //这个函数使用md5方式对query添加签名,没有数据的参数会被删除,并且直接更新输入的query数组. 383 // 和手机app版细节有所不同. 384 func (ot *OverseaTrade) md5Sign(query map[string]string) { 385 kvList := make([]kv, 0, len(query)) 386 for k, v := range query { 387 if v == "" { 388 delete(query, k) 389 continue 390 } 391 kvList = append(kvList, kv{ 392 K: k, 393 V: v, 394 }) 395 } 396 sort.Sort(kvSorter(kvList)) 397 398 toEncodeList := make([]string, len(kvList)) 399 for i, data := range kvList { 400 toEncodeList[i] = data.K + `=` + data.V 401 } 402 toSign := strings.Join(toEncodeList, "&") 403 //fmt.Println(kmgBase64.Base64EncodeStringToString(toSign)) 404 signed := kmgCrypto.Md5Hex([]byte(toSign + ot.SecurityCode)) 405 query["sign_type"] = "MD5" 406 query["sign"] = signed 407 return 408 } 409 410 func (ot *OverseaTrade) md5Verify(query map[string]string) (err error) { 411 signed := query["sign"] 412 delete(query, "sign") 413 delete(query, "sign_type") 414 ot.md5Sign(query) 415 if signed != query["sign"] { 416 return fmt.Errorf("[md5Verify] fail") 417 } 418 return nil 419 }