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  }