From 3d10a34b4bdd9f0c390bfb1ac63b1c6d8f35438d Mon Sep 17 00:00:00 2001 From: Rosbit Xu Date: Thu, 18 Aug 2022 18:19:59 +0800 Subject: [PATCH] add V3 transfer --- README.md | 112 ++++++++++++++++++++ conf/conf-gateway.go | 8 ++ go.mod | 7 +- go.sum | 21 +++- make.inc | 2 +- rest/rest-close-order.go | 12 ++- rest/v3-rest-query-transfer-detail.go | 80 ++++++++++++++ rest/v3-rest-query-transfer.go | 89 ++++++++++++++++ rest/v3-rest-transfer.go | 56 ++++++++++ router-gateway.go | 3 + v3/batch-transfer.go | 78 ++++++++++++++ v3/create-client.go | 48 +++++++++ v3/query-transfer-detail.go | 67 ++++++++++++ v3/query-transfer.go | 143 ++++++++++++++++++++++++++ wx-pay-api/close_order.go | 8 +- wxpay-gateway.conf.sample.json | 4 + 16 files changed, 721 insertions(+), 17 deletions(-) create mode 100644 rest/v3-rest-query-transfer-detail.go create mode 100644 rest/v3-rest-query-transfer.go create mode 100644 rest/v3-rest-transfer.go create mode 100644 v3/batch-transfer.go create mode 100644 v3/create-client.go create mode 100644 v3/query-transfer-detail.go create mode 100644 v3/query-transfer.go diff --git a/README.md b/README.md index f443da3..688e771 100644 --- a/README.md +++ b/README.md @@ -390,3 +390,115 @@ - 请求参数: 应用退款结果URL收到的POST Body内容 - 响应结果: - 参考校验支付支付结果 + + 1. V3版商家转账到零钱 + - 对应配置项: `v3-transfer` + - 访问方法: POST + - URI: 直接根据配置值访问,如`/v3/transfer` + - **[注意]** 该接口配置成一个内网可以访问的API + - 本接口只是“发出转账申请”,成功申请后需要在后台审核确认才能转出零钱 + - 请求参数 + + ```json + { + "payApp": "go-wxpay-gateway配置文件中的应用名", + "appId": "应用的appId", + "batchNo": "本次转账的批次,应用内唯一", + "batchName": "批次名称", + "batchRemark": "批次描述", + "details": [ + { + "tradeNo": "批次内唯一交易id", + "amount": 30, // 金额,单位"分", + "desc": "描述", + "openId": "收款人在appId内的openId", + "userName": "2000以上,必须给收款人实名" + }, + {...} + ] + } + ``` + + - 响应结果 + + ```json + { + "code": 200, + "msg": "OK", + "result":{ + "wxBatchId":"1030000040101068797782022081801027450232" // 微信批次id + } + } + ``` + + 1. V3版商家批次查询 + - 对应配置项: `v3-query-transfer` + - 访问方法: POST + - URI: 直接根据配置值访问,如`/v3/query-transfer` + - **[注意]** 该接口配置成一个内网可以访问的API + - 请求参数 + + ```json + { + "payApp": "go-wxpay-gateway配置文件中的应用名", + "status": "查询的状态值", // "ALL" | "SUCCESS" | "FAIL", 不给用ALL + "needDetail": true, // 是否展示详情 + "wxBatchId": "转账申请成功返回的微信批次id" + "------ or -----": "或者", + "batchNo": "转账的批次,应用内唯一" + } + ``` + + - 响应结果 + + ```json + { + "code": 200, + "msg": "OK", + "result":{ + "status":"FINISHED", + "total": 1, // 数目 + "details": [ + { + "detail_id":"1040000040101068797782022081801020503999", // 微信详情id + "out_detail_no":"tt202208170001", // 对应自己的tradeNo + "detail_status":"SUCCESS" // 详情状态 + } + ] + } + } + ``` + + 1. V3版商家转账详情查询 + - 对应配置项: `v3-query-transfer-detail` + - 访问方法: POST + - URI: 直接根据配置值访问,如`/v3/query-transfer-detail` + - **[注意]** 该接口配置成一个内网可以访问的API + - 请求参数 + + ```json + { + "payApp": "go-wxpay-gateway配置文件中的应用名", + + "wxBatchId": "转账申请成功返回的微信批次id", + "wxDetailId": "批次查询时返回的微信详情id", + "------ or -----": "或者", + "batchNo": "转账的批次,应用内唯一", + "tradeNo": "转账时详情的唯一id" + } + ``` + + - 响应结果 + + ```json + { + "code": 200, + "msg": "OK", + "result":{ + "detail_status":"SUCCESS", // 详情状态 + "amount": 30, // 金额,单位分 + "remark": "转账时的备注", + "reason": "失败原因。如果成功,返回空" + } + } + ``` diff --git a/conf/conf-gateway.go b/conf/conf-gateway.go index d0435a4..1fff8df 100644 --- a/conf/conf-gateway.go +++ b/conf/conf-gateway.go @@ -21,6 +21,9 @@ "close-order": "/close-order", "transfer": "/transfer", "query-transfer": "/query-transfer", + "v3-transfer": "/v3/transfer", + "v3-query-transfer": "/v3/query-transfer", + "v3-query-transfer-detail": "/v3/query-transfer-detail", "realname-auth-root": "/realname/auth -- will be replaced with ${realname-auth-root}/:op {url|identity|getinfo}", "verify-notify-pay": "/verify-notify-pay", "verify-notify-refund": "/verify-notify-refund" @@ -33,6 +36,7 @@ "mch-cert-pem-file": "your-cert-pem-file-name, only used when refunding", "mch-key-pem-file": "your-key-pem-file-name, only used when refunding", "mch-cert-serialno": "optional, only used when real-name getinfo", + "wxpay-v3-cert": "optional, only used for batch-transfer", "receipt": true } ], @@ -73,6 +77,9 @@ type EndpointConf struct { QueryOrder string `json:"query-order"` CloseOrder string `json:"close-order"` Transfer string `json:"transfer"` + V3Transfer string `json:"v3-transfer"` + V3QueryTransfer string `json:"v3-query-transfer"` + V3QueryTransferDetail string `json:"v3-query-transfer-detail"` QueryTransfer string `json:"query-transfer"` RealnameAuthRoot string `json:"realname-auth-root"` VerifyNotifyPay string `json:"verify-notify-pay"` @@ -86,6 +93,7 @@ type MerchantConf struct { MchCertPemFile string `json:"mch-cert-pem-file"` MchKeyPemFile string `json:"mch-key-pem-file"` MchCertSerialNo string `json:"mch-cert-serialno"` + WxpayV3Cert string `json:"wxpay-v3-cert"` Receipt bool `json:"receipt"` } diff --git a/go.mod b/go.mod index b58258f..313d395 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,8 @@ go 1.12 require ( github.com/beevik/etree v1.1.0 - github.com/rosbit/gnet v0.0.3 // indirect + github.com/rosbit/gnet v0.0.10 github.com/rosbit/go-aes v0.0.1 - github.com/rosbit/logmerger v0.0.1 - github.com/rosbit/mgin v0.0.1 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.0.0 + github.com/rosbit/mgin v0.0.1 + github.com/wechatpay-apiv3/wechatpay-go v0.2.14 ) diff --git a/go.sum b/go.sum index 6268627..cd26d86 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,11 @@ +github.com/agiledragon/gomonkey v2.0.2+incompatible h1:eXKi9/piiC3cjJD1658mEE2o3NjkJ5vDLgYjCQu0Xlw= +github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -14,18 +19,22 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mroth/weightedrand v0.4.1 h1:rHcbUBopmi/3x4nnrvwGJBhX9d0vk+KgoLUZeDP6YyI= github.com/mroth/weightedrand v0.4.1/go.mod h1:3p2SIcC8al1YMzGhAIoXD+r9olo/g/cdJgAD905gyNE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rosbit/gnet v0.0.3 h1:N4k8+NlbqnoI3JIuxdyx7Qjg+j2gkcuT/dcPCjc3y+g= -github.com/rosbit/gnet v0.0.3/go.mod h1:4p58JSylqipqD061GlEB7pSjeU6qJwhxHpzW832KMZI= +github.com/rosbit/gnet v0.0.10 h1:zBFcCxtG3WDR9TPywsWb3Yb7WbP8YoQR2KatMuQSEk8= +github.com/rosbit/gnet v0.0.10/go.mod h1:4p58JSylqipqD061GlEB7pSjeU6qJwhxHpzW832KMZI= github.com/rosbit/go-aes v0.0.1 h1:tmOiz5IO6pyQ35nrGClsSw0AAF8bCAZR5zIcK+Hd8uI= github.com/rosbit/go-aes v0.0.1/go.mod h1:CyPYFnM8SsrEw2Hvz5QtDIaAQnxy/n8rHv8NR/7Lm5Y= -github.com/rosbit/logmerger v0.0.1/go.mod h1:wzp/9K8Zu6+onB3moySMgBgXHNlXDj81vHWixaUHes0= github.com/rosbit/mgin v0.0.1 h1:jXtu0Bn7MrxLGKfhf1wVmuaeLZUN76w8e9jyUNvdh28= github.com/rosbit/mgin v0.0.1/go.mod h1:Cio++agodWrwVHBmV5I5lHZ+fcFP9PGYITYNFoz2yF4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/wechatpay-apiv3/wechatpay-go v0.2.14 h1:wLg8Yr4/LrCbUdRL2cVTNwCnkJB/7IXj8MKoiI+Q7DY= +github.com/wechatpay-apiv3/wechatpay-go v0.2.14/go.mod h1:jZzgos/NDEbnH1WFWOIa4wvcie3lqz1hZ4Z0O34Ntsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -37,7 +46,9 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/make.inc b/make.inc index 46fc690..1640aa3 100644 --- a/make.inc +++ b/make.inc @@ -2,7 +2,7 @@ build: @if [ "$o" == "macos" ]; then \ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'main.buildTime=`TZ=UTC-8 date '+%F %T'`' -X 'main.osInfo=`uname -sr`' -X 'main.goInfo=`go version`' -extldflags -static"; \ elif [ "$s" == "static" ]; then \ - go build -ldflags "-X 'main.buildTime=`TZ=UTC-8 date '+%F %T'`' -X 'main.osInfo=`uname -sr`' -X 'main.goInfo=`go version`' -linkmode external -extldflags -static" -tags $t -o go-wxpay-$t; \ + go build -ldflags "-X 'main.buildTime=`TZ=UTC-8 date '+%F %T'`' -X 'main.osInfo=`uname -sr`' -X 'main.goInfo=`go version`' -linkmode external -extldflags -static" -tags "$t timetzdata" -o go-wxpay-$t; \ else \ go build -ldflags "-X 'main.buildTime=`TZ=UTC-8 date '+%F %T'`' -X 'main.osInfo=`uname -sr`' -X 'main.goInfo=`go version`'"; \ fi diff --git a/rest/rest-close-order.go b/rest/rest-close-order.go index 2442cac..6264980 100644 --- a/rest/rest-close-order.go +++ b/rest/rest-close-order.go @@ -7,7 +7,7 @@ import ( "net/http" ) -// POST /queryorder +// POST /closeorder // POST Body: // { // "appId": "appId of mp/mini-prog", @@ -39,13 +39,19 @@ func CloseOrder(c *mgin.Context) { return } - sent, recv, err := wxpay.CloseOrder( + res, sent, recv, err := wxpay.CloseOrder( closeParam.AppId, mchConf.MchId, mchConf.MchApiKey, closeParam.OrderId, isSandbox, ) - sendResultWithMsg(c, closeParam.Debug, sent, recv, err) + if err != nil { + sendResultWithMsg(c, closeParam.Debug, sent, recv, err) + return + } + sendResultWithMsg(c, closeParam.Debug, sent, recv, nil, map[string]interface{}{ + "result": res, + }) } diff --git a/rest/v3-rest-query-transfer-detail.go b/rest/v3-rest-query-transfer-detail.go new file mode 100644 index 0000000..00bb13a --- /dev/null +++ b/rest/v3-rest-query-transfer-detail.go @@ -0,0 +1,80 @@ +package rest + +import ( + "github.com/rosbit/mgin" + "go-wxpay-gateway/v3" + "fmt" + "net/http" +) + +// POST /v3/query-transfer-detail +// POST Body: +// { +// "payApp": "name-of-app-in-wxpay-gateway", +// +// "wxBatchId": "wx-batchid", +// "wxDetailId": "wx-detailid" +// ---- or --- +// "batchNo": "unique-bach-no", +// "tradeNo": "unique-trade-no" +// } +func V3QueryTransferDetail(c *mgin.Context) { + var params struct { + PayApp string + + WxBatchId string + WxDetailId string + // ---- or ---- + BatchNo string + TradeNo string + } + + if code, err := c.ReadJSON(¶ms); err != nil { + c.Error(code, err.Error()) + return + } + if len(params.WxBatchId) == 0 && len(params.BatchNo) == 0 { + c.Error(http.StatusBadRequest, "wxBatchId or batchNo expected") + return + } + + var queryTransferDetail v3.FnQueryTransferDetail + var batchNo, tradeNo string + if len(params.WxBatchId) > 0 { + if len(params.WxDetailId) == 0 { + c.Error(http.StatusBadRequest, "wxDetailId expected") + return + } + queryTransferDetail, batchNo, tradeNo = v3.QueryTransferDetailByWxBatchId, params.WxBatchId, params.WxDetailId + } else { + if len(params.TradeNo) == 0 { + c.Error(http.StatusBadRequest, "tradeNo expected") + return + } + queryTransferDetail, batchNo, tradeNo = v3.QueryTransferDetailByBatchNo, params.BatchNo, params.TradeNo + } + + res, err := queryTransferDetail(params.PayApp, batchNo, tradeNo) + if err != nil { + sendResultWithMsg(c, false, nil, nil, err) + return + } + fmt.Printf("res: %v\n", res) + + c.JSON(http.StatusOK, map[string]interface{}{ + "code": http.StatusOK, + "msg": "OK", + "result": map[string]interface{}{ + "status": *res.DetailStatus, + "amount": *res.TransferAmount, + "remark": *res.TransferRemark, + "reason": func()string{ + if res.FailReason == nil { + return "" + } + return string(*res.FailReason) + }(), + }, + }) +} + diff --git a/rest/v3-rest-query-transfer.go b/rest/v3-rest-query-transfer.go new file mode 100644 index 0000000..a7152b5 --- /dev/null +++ b/rest/v3-rest-query-transfer.go @@ -0,0 +1,89 @@ +package rest + +import ( + "github.com/rosbit/mgin" + "go-wxpay-gateway/v3" + "strings" + "fmt" + "net/http" + "encoding/json" +) + +// POST /v3/query-transfer +// POST Body: +// { +// "payApp": "name-of-app-in-wxpay-gateway", +// "status": "" | "ALL" | "SUCCESS" | "FAIL" +// "needDetail": true|false, +// "wxBatchId": "wx-batchid" +// ---- or --- +// "batchNo": "unique-bach-no" +// } +func V3QueryTransfer(c *mgin.Context) { + var params struct { + PayApp string + Status string + NeedDetail bool + WxBatchId string + BatchNo string + } + + if code, err := c.ReadJSON(¶ms); err != nil { + c.Error(code, err.Error()) + return + } + if len(params.WxBatchId) == 0 && len(params.BatchNo) == 0 { + c.Error(http.StatusBadRequest, "wxBatchId or batchNo expected") + return + } + var status string = v3.ALL + switch strings.ToUpper(params.Status) { + case v3.SUCCESS: + status = v3.SUCCESS + case v3.FAIL: + status = v3.FAIL + default: + status = v3.ALL + } + + var queryTransfer v3.FnQueryTransfer + var batchNo string + if len(params.WxBatchId) > 0 { + queryTransfer, batchNo = v3.QueryTransferByWxBatchId, params.WxBatchId + } else { + queryTransfer, batchNo = v3.QueryTransferByBatchNo, params.BatchNo + } + + total, batchStatus, it, err := queryTransfer(params.PayApp, batchNo, params.NeedDetail, status) + if err != nil { + sendResultWithMsg(c, false, nil, nil, err) + return + } + + out := c.Response() + c.SetHeader("Content-Type", "application/json") + fmt.Fprintf(out, `{"code":%d,"msg":"OK","result":{"status":"%s","total":%d`, http.StatusOK, batchStatus, total) + if total == 0 { + fmt.Fprintf(out, "}}") + return + } + jOut := json.NewEncoder(out) + jOut.SetEscapeHTML(false) + i := 0 + fmt.Fprintf(out, `,"details":`) + for res := range it { + if i == 0 { + fmt.Fprintf(out, "[") + } else { + fmt.Fprintf(out, ",") + } + i += 1 + jOut.Encode(res) + } + if i == 0 { + fmt.Fprintf(out, "null}}") + } else { + fmt.Fprintf(out, "]}}") + } +} + diff --git a/rest/v3-rest-transfer.go b/rest/v3-rest-transfer.go new file mode 100644 index 0000000..32f60b4 --- /dev/null +++ b/rest/v3-rest-transfer.go @@ -0,0 +1,56 @@ +package rest + +import ( + "github.com/rosbit/mgin" + "go-wxpay-gateway/v3" + "net/http" +) + +// POST /v3/transfer +// POST Body: +// { +// "payApp": "name-of-app-in-wxpay-gateway", +// "appId": "appId of mp/mini-prog", +// "batchNo": "unique-bach-no", +// "batchName": "batch name", +// "batchRemark": "batch remark", +// "details": [ +// { +// "tradeNo": "unique-trade-no", +// "amount": xxx-in-fen, +// "desc": "description", +// "openId": "openid to be transfered", +// "userName": "real user name" +// }, +// {...} +// ] +// } +func V3Transfer(c *mgin.Context) { + var params struct { + PayApp string + AppId string + BatchNo string + BatchName string + BatchRemark string + Details []v3.TransferDetail + } + + if code, err := c.ReadJSON(¶ms); err != nil { + c.Error(code, err.Error()) + return + } + + wxBatchId, err := v3.BatchTransfer(params.PayApp, params.AppId, params.BatchNo, params.BatchName, params.BatchRemark, params.Details) + if err != nil { + sendResultWithMsg(c, false, nil, nil, err) + return + } + c.JSON(http.StatusOK, map[string]interface{}{ + "code": http.StatusOK, + "msg": "OK", + "result": map[string]interface{}{ + "wxBatchId": wxBatchId, + }, + }) +} + diff --git a/router-gateway.go b/router-gateway.go index b06611a..bba026b 100644 --- a/router-gateway.go +++ b/router-gateway.go @@ -35,6 +35,9 @@ func StartService() error { api.POST(endpoints.CloseOrder, rest.CloseOrder) api.POST(endpoints.Transfer, rest.Transfer) api.POST(endpoints.QueryTransfer,rest.QueryTransfer) + api.POST(endpoints.V3Transfer, rest.V3Transfer) + api.POST(endpoints.V3QueryTransfer, rest.V3QueryTransfer) + api.POST(endpoints.V3QueryTransferDetail,rest.V3QueryTransferDetail) api.POST(endpoints.VerifyNotifyPay, rest.VerifyNotifyPayment) api.POST(endpoints.VerifyNotifyRefund, rest.VerifyNotifyRefundment) if len(endpoints.RealnameAuthRoot) > 0 { diff --git a/v3/batch-transfer.go b/v3/batch-transfer.go new file mode 100644 index 0000000..2aaf5ac --- /dev/null +++ b/v3/batch-transfer.go @@ -0,0 +1,78 @@ +// 发起商家转账:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_1.shtml + +package v3 + +import ( + "github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "net/http" + "fmt" +) + +type TransferDetail struct { + SearialNo string `json:"tradeNo"` // 转账明细单的唯一标识 + Amount int64 `json:"amount"` // 转账金额,单位分 + Remark string `json:"desc"` // 备注,最多32个字符 + OpenId string `json:"openId"` // appid下的openid + UserName string `json:"userName"` // 收款方姓名 +} + +func BatchTransfer(appName string, appId string, batchNo string, name string, remark string, details []TransferDetail) (wxBatchId string, err error) { + fmt.Printf("appName: %s\n", appName) + fmt.Printf("appId: %s\n", appId) + fmt.Printf("batchNo: %s\n", batchNo) + fmt.Printf("name: %s\n", name) + fmt.Printf("remark: %s\n", remark) + // fmt.Printf("details: %v\n", details) + + if len(details) == 0 { + err = fmt.Errorf("details expected") + return + } + + var totalAmount int64 + totalNum := int64(len(details)) + transDetails := make([]transferbatch.TransferDetailInput, len(details)) + for i, _ := range details { + d := &details[i] + totalAmount += d.Amount + transDetails[i] = transferbatch.TransferDetailInput{ + OutDetailNo: core.String(d.SearialNo), + TransferAmount: core.Int64(d.Amount), + TransferRemark: core.String(d.Remark), + Openid: core.String(d.OpenId), + UserName: func()*string{ + if len(d.UserName) > 0 { + return core.String(d.UserName) + } + return nil + }(), + } + } + + ctx, client, e := createClient(appName) + if e != nil { + err = e + return + } + svc := transferbatch.TransferBatchApiService{Client: client} + resp, result, e := svc.InitiateBatchTransfer(ctx, transferbatch.InitiateBatchTransferRequest { + Appid: core.String(appId), + OutBatchNo: core.String(batchNo), + BatchName: core.String(name), + BatchRemark: core.String(remark), + TotalAmount: core.Int64(totalAmount), + TotalNum: core.Int64(totalNum), + TransferDetailList: transDetails, + }) + if e != nil { + err = e + return + } + if result.Response.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", result.Response.Status) + return + } + wxBatchId = *resp.BatchId + return +} diff --git a/v3/create-client.go b/v3/create-client.go new file mode 100644 index 0000000..0de17a4 --- /dev/null +++ b/v3/create-client.go @@ -0,0 +1,48 @@ +package v3 + +import ( + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/rosbit/gnet" + "go-wxpay-gateway/conf" + "fmt" + "context" + "crypto/x509" +) + +func createClient(appName string) (ctx context.Context, client *core.Client, err error) { + mchConf, ok := conf.GetAppAttrs(appName) + if !ok { + err = fmt.Errorf("conf not found for %s", appName) + return + } + if len(mchConf.MchCertSerialNo) == 0 { + err = fmt.Errorf("mch-cert-serialno not specified for %s", appName) + return + } + if len(mchConf.WxpayV3Cert) == 0 { + err = fmt.Errorf("wxpay-v3-cert not specified for %s", appName) + return + } + cert, e := utils.LoadCertificateWithPath(mchConf.WxpayV3Cert) + if e != nil { + err = e + return + } + + mchPrivateKey, e := utils.LoadPrivateKeyWithPath(mchConf.MchKeyPemFile) + if e != nil { + err = e + return + } + + ctx = context.Background() + // opt := option.WithWechatPayAutoAuthCipher(mchConf.MchId, mchConf.MchCertSerialNo, mchPrivateKey, mchConf.MchApiKey) + opts := []core.ClientOption{ + option.WithWechatPayAuthCipher(mchConf.MchId, mchConf.MchCertSerialNo, mchPrivateKey, []*x509.Certificate{cert}), + option.WithHTTPClient(gnet.NewHttpsRequest().GetClient()), + } + client, err = core.NewClient(ctx, opts...) + return +} diff --git a/v3/query-transfer-detail.go b/v3/query-transfer-detail.go new file mode 100644 index 0000000..a945c09 --- /dev/null +++ b/v3/query-transfer-detail.go @@ -0,0 +1,67 @@ +package v3 + +import ( + "github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "net/http" + "fmt" + "context" +) + +type FnQueryTransferDetail func(appName string, wxBatchId string, detailId string) (detail *transferbatch.TransferDetailEntity, err error) + +type fnQueryTranferDetail func(ctx context.Context, a *transferbatch.TransferDetailApiService) (detail *transferbatch.TransferDetailEntity, err error) + +// 微信明细单号查询明细单API: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_3.shtml +func QueryTransferDetailByWxBatchId(appName string, wxBatchId string, detailId string) (detail *transferbatch.TransferDetailEntity, err error) { + getDetail := func(ctx context.Context, a *transferbatch.TransferDetailApiService) (detail *transferbatch.TransferDetailEntity, err error) { + res, result, e := a.GetTransferDetailByNo(ctx, transferbatch.GetTransferDetailByNoRequest{ + BatchId: core.String(wxBatchId), + DetailId: core.String(detailId), + }) + if e != nil { + err = e + return + } + if result.Response.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", result.Response.Status) + return + } + detail = res + return + } + + return queryTransferDetail(appName, getDetail) +} + +// 商家明细单号查询明细单API: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_6.shtml +func QueryTransferDetailByBatchNo(appName string, batchNo string, tradeNo string) (detail *transferbatch.TransferDetailEntity, err error) { + getDetail := func(ctx context.Context, a *transferbatch.TransferDetailApiService) (detail *transferbatch.TransferDetailEntity, err error) { + res, result, e := a.GetTransferDetailByOutNo(ctx, transferbatch.GetTransferDetailByOutNoRequest{ + OutBatchNo: core.String(batchNo), + OutDetailNo: core.String(tradeNo), + }) + if e != nil { + err = e + return + } + if result.Response.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", result.Response.Status) + return + } + detail = res + return + } + + return queryTransferDetail(appName, getDetail) +} + +func queryTransferDetail(appName string, getDetail fnQueryTranferDetail) (detail *transferbatch.TransferDetailEntity, err error) { + ctx, client, e := createClient(appName) + if e != nil { + err = e + return + } + svc := &transferbatch.TransferDetailApiService{Client: client} + return getDetail(ctx, svc) +} diff --git a/v3/query-transfer.go b/v3/query-transfer.go new file mode 100644 index 0000000..401e4ad --- /dev/null +++ b/v3/query-transfer.go @@ -0,0 +1,143 @@ +package v3 + +import ( + "github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "net/http" + "fmt" + "os" + "context" +) + +type TransferQueryItem struct { + DetailId string `json:"detail_id"` + OutDetailNo string `json:"out_detail_no"` + DetailStatus string `json:"detail_status"` +} + +const ( + queryPageSize = int64(100) + + ALL = "ALL" + SUCCESS = "SUCCESS" + FAIL = "FAIL" +) + +type FnQueryTransfer func(appName string, wxBatchId string, needDetail bool, status string) (total int64, batchStatus string, it <-chan *TransferQueryItem, err error) + +type fnQueryTranferBatch func(ctx context.Context, a *transferbatch.TransferBatchApiService, pageNo int64) (resp *transferbatch.TransferBatchEntity, err error) + +// 微信批次单号查询批次单API: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_2.shtml +func QueryTransferByWxBatchId(appName string, wxBatchId string, needDetail bool, status string) (total int64, batchStatus string, it <-chan *TransferQueryItem, err error) { + getPage := func(ctx context.Context, a *transferbatch.TransferBatchApiService, pageNo int64) (resp *transferbatch.TransferBatchEntity, err error) { + res, result, e := a.GetTransferBatchByNo(ctx, transferbatch.GetTransferBatchByNoRequest{ + BatchId: core.String(wxBatchId), + NeedQueryDetail: core.Bool(needDetail), + Offset: core.Int64(pageNo * queryPageSize), + Limit: core.Int64(queryPageSize), + DetailStatus: core.String(status), + }) + if e != nil { + err = e + return + } + if result.Response.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", result.Response.Status) + return + } + resp = res + return + } + + return queryTransfer(appName, getPage) +} + +// 商家批次单号查询批次单API: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_3_5.shtml +func QueryTransferByBatchNo(appName string, batchNo string, needDetail bool, status string) (total int64, batchStatus string, it <-chan *TransferQueryItem, err error) { + getPage := func(ctx context.Context, a *transferbatch.TransferBatchApiService, pageNo int64) (resp *transferbatch.TransferBatchEntity, err error) { + res, result, e := a.GetTransferBatchByOutNo(ctx, transferbatch.GetTransferBatchByOutNoRequest{ + OutBatchNo: core.String(batchNo), + NeedQueryDetail: core.Bool(needDetail), + Offset: core.Int64(pageNo * queryPageSize), + Limit: core.Int64(queryPageSize), + DetailStatus: core.String(status), + }) + if e != nil { + err = e + return + } + if result.Response.StatusCode != http.StatusOK { + err = fmt.Errorf("%s", result.Response.Status) + return + } + resp = res + return + } + + return queryTransfer(appName, getPage) +} + +// 逐页查询明细 +func queryTransfer(appName string, getPage fnQueryTranferBatch) (total int64, batchStatus string, it <-chan *TransferQueryItem, err error) { + ctx, client, e := createClient(appName) + if e != nil { + err = e + return + } + svc := &transferbatch.TransferBatchApiService{Client: client} + + pageNo := int64(0) + res, e := getPage(ctx, svc, pageNo) + if e != nil { + err = e + return + } + batchRes := res.TransferBatch + batchStatus = *batchRes.BatchStatus + if total = *batchRes.TotalNum; total <= 0 { + return + } + + resPage := make(chan []transferbatch.TransferDetailCompact) + it = makeChanResult(resPage) + + go func() { + count := int64(0) + for { + resPage <- res.TransferDetailList + count += int64(len(res.TransferDetailList)) + if count >= total { + break + } + pageNo += 1 + + if res, e = getPage(ctx, svc, pageNo); e != nil { + fmt.Fprintf(os.Stderr, "error occurs when calling getPage(pageNo:%d): %v\n", pageNo, e) + break + } + } + close(resPage) + }() + + return +} + +func makeChanResult(resPage <-chan []transferbatch.TransferDetailCompact) (<-chan *TransferQueryItem) { + it := make(chan *TransferQueryItem) + go func() { + for res := range resPage { + for i, _ := range res { + r := &res[i] + it <- &TransferQueryItem{ + DetailId: *r.DetailId, + OutDetailNo: *r.OutDetailNo, + DetailStatus: *r.DetailStatus, + } + } + } + + close(it) + }() + + return it +} diff --git a/wx-pay-api/close_order.go b/wx-pay-api/close_order.go index cab32c4..9004c33 100644 --- a/wx-pay-api/close_order.go +++ b/wx-pay-api/close_order.go @@ -2,13 +2,13 @@ package wxpay -func postClose(orderId string, xml []byte, isSandbox bool, apiKey string) (recv []byte, err error) { +func postClose(orderId string, xml []byte, isSandbox bool, apiKey string) (res map[string]string, recv []byte, err error) { orderclose_url := _GetApiUrl(UT_ORDER_CLOSE, isSandbox) if recv, err = _CallWxAPI(orderclose_url, "POST", xml); err != nil { return } - _, err = parseXmlResult(recv, apiKey) + res, err = parseXmlResult(recv, apiKey) return } @@ -18,7 +18,7 @@ func CloseOrder( mchApiKey string, orderId string, isSandbox bool, -) (xmlstr, recv []byte, err error) { +) (res map[string]string, xmlstr, recv []byte, err error) { /* if isSandbox { var err error @@ -40,6 +40,6 @@ func CloseOrder( addTag(xml, params, "sign", signature, false) xmlstr = xml.ToXML() - recv, err = postClose(orderId, xmlstr, isSandbox, mchApiKey) + res, recv, err = postClose(orderId, xmlstr, isSandbox, mchApiKey) return } diff --git a/wxpay-gateway.conf.sample.json b/wxpay-gateway.conf.sample.json index acd1ef3..9a9f9f5 100644 --- a/wxpay-gateway.conf.sample.json +++ b/wxpay-gateway.conf.sample.json @@ -12,6 +12,9 @@ "close-order": "/wxpay/close-order", "transfer": "/transfer", "query-transfer": "/query-transfer", + "v3-transfer": "/v3/transfer", + "v3-query-transfer": "/v3/query-transfer", + "v3-query-transfer-detail": "/v3/query-transfer-detail", "realname-auth-root": "/realname/auth -- 实名认证根路径,会添加后缀 /:op op 可以是 {url|identity|getinfo}", "verify-notify-pay": "/verify-notify-pay", "verify-notify-refund": "/verify-notify-refund" @@ -24,6 +27,7 @@ "mch-cert-pem-file": "./路径指向/apiclient_cert.pem", "mch-key-pem-file": "./路径指向/apiclient_key.pem", "mch-cert-serialno": "证书序列号, 用于实名认证getinfo", + "wxpay-v3-cert": "optional, only used for batch-transfer", "receipt": false }, {