From 9cb8d100d8af5121d26501aaaa762260ea786aa0 Mon Sep 17 00:00:00 2001 From: ziming Date: Wed, 5 Nov 2025 18:25:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E7=BA=A2=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 8 +- cmd/main.go | 4 +- cmd/wechat_cpn.go | 5 +- cmd/wechat_redpack_v2.go | 60 ++ plugins/wechat_redpack_v2/go.mod | 41 ++ plugins/wechat_redpack_v2/go.sum | 89 +++ .../wechat_redpack_v2/internal/transform.go | 160 +++++ plugins/wechat_redpack_v2/internal/vo/code.go | 9 + .../internal/vo/fail_reason.go | 32 + .../internal/vo/query_status.go | 53 ++ .../internal/wechat/srv/authorize/mks.go | 133 ++++ .../internal/wechat/srv/authorize/model.go | 12 + .../internal/wechat/srv/authorize/token.go | 135 +++++ .../wechat/srv/authorize/token_test.go | 53 ++ .../internal/wechat/srv/srv.go | 9 + .../wechat/srv/transfer/fail_reason.go | 92 +++ .../internal/wechat/srv/transfer/model.go | 173 ++++++ .../internal/wechat/srv/transfer/transfer.go | 111 ++++ .../wechat/srv/transfer/transfer_test.go | 60 ++ .../internal/wechat/utils/aes.go | 87 +++ .../internal/wechat/utils/wxpay_utility.go | 572 ++++++++++++++++++ .../wechat/utils/wxpay_utility_test.go | 111 ++++ .../internal/wechat_redpack.go | 128 ++++ .../internal/wechat_redpack_test.go | 95 +++ plugins/wechat_redpack_v2/internal/wx.go | 87 +++ plugins/wechat_redpack_v2/internal/wx_test.go | 29 + plugins/wechat_redpack_v2/main.go | 15 + plugins/zltx_card_v1/go.sum | 75 --- plugins/zltx_v2/internal/zltx_v2_test.go | 7 +- 29 files changed, 2363 insertions(+), 82 deletions(-) create mode 100644 cmd/wechat_redpack_v2.go create mode 100644 plugins/wechat_redpack_v2/go.mod create mode 100644 plugins/wechat_redpack_v2/go.sum create mode 100644 plugins/wechat_redpack_v2/internal/transform.go create mode 100644 plugins/wechat_redpack_v2/internal/vo/code.go create mode 100644 plugins/wechat_redpack_v2/internal/vo/fail_reason.go create mode 100644 plugins/wechat_redpack_v2/internal/vo/query_status.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/authorize/mks.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/authorize/model.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/srv.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/transfer/fail_reason.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/transfer/model.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/utils/aes.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat_redpack.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat_redpack_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wx.go create mode 100644 plugins/wechat_redpack_v2/internal/wx_test.go create mode 100644 plugins/wechat_redpack_v2/main.go diff --git a/Makefile b/Makefile index 5ce05a5..3e434d8 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,12 @@ wechat_redpack: make build-linux name=wechat_redpack && \ make build-win name=wechat_redpack +.PHONY: wechat_redpack_v2 +wechat_redpack_v2: + make build-mac name=wechat_redpack_v2 && \ + make build-linux name=wechat_redpack_v2 && \ + make build-win name=wechat_redpack_v2 + .PHONY: union_pay_cpn union_pay_cpn: make build-mac name=union_pay_cpn && \ @@ -78,4 +84,4 @@ union_pay_redpack: make build-win name=union_pay_redpack .PHONY: all -all: zltx_v1 zltx_card_v1 zltx_v2 alipay_cpn alipay_redpack wechat_cpn wechat_redpack \ No newline at end of file +all: zltx_v1 zltx_card_v1 zltx_v2 alipay_cpn alipay_redpack wechat_cpn wechat_redpack wechat_redpack_v2 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 03d4e56..32fec06 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,7 +2,9 @@ package main // main 这只是一个演示 func main() { - alipayOrderRedPack() + //wechatQueryCpn() + WechatRedPackV2Query() + //alipayOrderRedPack() //zltxQuery() //wechatQueryCpn() //alipayQueryRedPack() diff --git a/cmd/wechat_cpn.go b/cmd/wechat_cpn.go index ec5236b..4810942 100644 --- a/cmd/wechat_cpn.go +++ b/cmd/wechat_cpn.go @@ -20,8 +20,9 @@ var wechatCpnConf = &manage.Config{ func getWechatCpnConf() []byte { c := &wechat.Server{ - MchID: "1605446142", // 证书所属商户 - MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76", + MchID: "1605446142", // 证书所属商户 + MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76", // old + //MchCertificateSerialNumber: "46B64A9AF817BCE0425AB2ED003E7FC3C3DC48D9", } marshal, _ := json.Marshal(c) return marshal diff --git a/cmd/wechat_redpack_v2.go b/cmd/wechat_redpack_v2.go new file mode 100644 index 0000000..79d7d38 --- /dev/null +++ b/cmd/wechat_redpack_v2.go @@ -0,0 +1,60 @@ +package main + +import ( + "context" + "encoding/json" + "gitea.cdlsxd.cn/sdk/plugin/instance" + "gitea.cdlsxd.cn/sdk/plugin/manage" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "log" +) + +var wechatRedpackConfig = &manage.Config{ + Cmd: "pkg/mac/wechat_redpack_v2.so", + Tag: "wechat_redpack_v2", + Version: 1, + CookieKey: "wechat_redpack_v2", + CookieValue: "wechat_redpack_v2", +} + +type Wechat struct { + MchID string `json:"mch_id"` + MchCertificateSerialNumber string `json:"mch_certificate_serial_number"` + WechatPayPublicKeyID string `json:"wechat_pay_public_key_id"` + MchApiV3Key string `json:"mch_api_v3_key"` +} + +func getWechatRedPackV2Conf() []byte { + c := &Wechat{ + MchID: "1652322442", // 证书所属商户 + MchCertificateSerialNumber: "2CE0C4F37E960878F354C986E6F1A5342558BABC", + WechatPayPublicKeyID: "PUB_KEY_ID_0116523224422025061800192371001800", + MchApiV3Key: "7e6eb4a5ebeed3cf61c693586b11d00b", + } + marshal, _ := json.Marshal(c) + return marshal +} + +func WechatRedPackV2Query() { + + err := manage.Add(wechatRedpackConfig) + if err != nil { + log.Fatalln(err) + } + defer manage.Close() + + queryRequest := &proto.QueryRequest{ + Config: getWechatRedPackV2Conf(), + Order: &proto.QueryRequest_Order{ + OrderNo: "19497351672832450564", + TradeNo: "", + Account: "", + Extra: []byte(``), + }, + } + resQuery, err := instance.Query(context.Background(), wechatRedpackConfig.Tag, queryRequest) + if err != nil { + log.Fatalln(err) + } + log.Printf("Query res:%+v", resQuery) +} diff --git a/plugins/wechat_redpack_v2/go.mod b/plugins/wechat_redpack_v2/go.mod new file mode 100644 index 0000000..9d545ce --- /dev/null +++ b/plugins/wechat_redpack_v2/go.mod @@ -0,0 +1,41 @@ +module plugins/wechat_redpack_v2 + +go 1.22.2 + +replace plugins/utils => ../../utils + +require ( + gitea.cdlsxd.cn/sdk/plugin v1.0.17 + github.com/carlmjohnson/requests v0.24.2 + github.com/hashicorp/go-plugin v1.6.1 + github.com/stretchr/testify v1.9.0 + github.com/wechatpay-apiv3/wechatpay-go v0.2.18 + plugins/utils v1.0.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-kratos/kratos/v2 v2.8.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/go-hclog v0.14.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugins/wechat_redpack_v2/go.sum b/plugins/wechat_redpack_v2/go.sum new file mode 100644 index 0000000..4b12dc7 --- /dev/null +++ b/plugins/wechat_redpack_v2/go.sum @@ -0,0 +1,89 @@ +gitea.cdlsxd.cn/sdk/plugin v1.0.17 h1:agk+9iA1ZI6fLVLtxEnuOWxcDzSq9QH7VBFvhlZZsbw= +gitea.cdlsxd.cn/sdk/plugin v1.0.17/go.mod h1:O/bYQWg1o9g/cBq9qNA3kLIpuPt7VDZqj1bPE6s04NM= +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/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/carlmjohnson/requests v0.24.2 h1:JDakhAmTIKL/qL/1P7Kkc2INGBJIkIFP6xUeUmPzLso= +github.com/carlmjohnson/requests v0.24.2/go.mod h1:duYA/jDnyZ6f3xbcF5PpZ9N8clgopubP2nK5i6MVMhU= +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/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-kratos/kratos/v2 v2.8.2 h1:EsEA7AmPQ2YQQ0FZrDWO2HgBNqeWM8z/mWKzS5UkQaQ= +github.com/go-kratos/kratos/v2 v2.8.2/go.mod h1:+Vfe3FzF0d+BfMdajA11jT0rAyJWublRE/seZQNZVxE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wechatpay-apiv3/wechatpay-go v0.2.18 h1:vj5tvSmnEIz3ZsnFNNUzg+3Z46xgNMJbrO4aD4wP15w= +github.com/wechatpay-apiv3/wechatpay-go v0.2.18/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/wechat_redpack_v2/internal/transform.go b/plugins/wechat_redpack_v2/internal/transform.go new file mode 100644 index 0000000..6512293 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/transform.go @@ -0,0 +1,160 @@ +package internal + +import ( + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "plugins/wechat_redpack_v2/internal/wechat/srv/transfer" + "plugins/wechat_redpack_v2/internal/wechat/utils" +) + +func transConfig(config []byte) (*Wechat, error) { + var c Wechat + + if err := json.Unmarshal(config, &c); err != nil { + return nil, proto.ErrorConfigFail(err.Error()) + } + + return &c, nil +} + +type OrderExtra struct { + Appid string `json:"app_id"` + NotifyUrl string `json:"notify_url"` +} + +type ProductExtra struct { + BatchName string `json:"batch_name"` + BatchRemark string `json:"batch_remark"` +} + +func orderReq(order *proto.OrderRequest_Order, product *proto.OrderRequest_Product) (*transfer.TransferToUserRequest, error) { + + var orderExtra OrderExtra + err := json.Unmarshal(order.Extra, &orderExtra) + if err != nil { + return nil, fmt.Errorf("order拓展参数 json unmarshal error[%+v]", err) + } + + var productExtra ProductExtra + if err := json.Unmarshal(product.Extra, &productExtra); err != nil { + return nil, fmt.Errorf("product拓展参数json unmarshal error:[%+v], extra[%s]", err, string(product.Extra)) + } + + transferSceneReportInfos := []transfer.TransferSceneReportInfo{ + { + InfoType: utils.String("活动名称"), + InfoContent: utils.String(productExtra.BatchName), // 商户自定义内容 + }, + { + InfoType: utils.String("奖励说明"), + InfoContent: utils.String(productExtra.BatchRemark), // 商户自定义内容 + }, + } + + transferAmount := int64(order.Amount * 100) + + return &transfer.TransferToUserRequest{ + Appid: utils.String(orderExtra.Appid), + OutBillNo: utils.String(order.OrderNo), + TransferSceneId: utils.String("1000"), + Openid: utils.String(order.Account), + //UserName: utils.String(orderExtra.Appid), + TransferAmount: utils.Int64(transferAmount), + TransferRemark: utils.String(productExtra.BatchRemark), + NotifyUrl: utils.String(orderExtra.NotifyUrl), + //UserRecvPerception: utils.String(""), + TransferSceneReportInfos: transferSceneReportInfos, + }, nil +} + +func orderResp(order *proto.OrderRequest_Order, response *transfer.TransferToUserResponse) (*proto.OrderResponse, error) { + + if *response.State != transfer.TRANSFERBILLSTATUS_WAIT_USER_CONFIRM { + return nil, fmt.Errorf("微信转账异常:%s", response.State.GetText()) + } + + // 返回出去 + data, _ := json.Marshal(response) + + return &proto.OrderResponse{ + Result: &proto.Result{ + Status: proto.Status_ING, + OrderNo: order.GetOrderNo(), + TradeNo: *response.OutBillNo, + Message: "成功", + Data: data, + Extra: nil, + }, + }, nil +} + +func queryReq(order *proto.QueryRequest_Order) (*transfer.GetTransferBillByOutNoRequest, error) { + if order.OrderNo == "" { + return nil, fmt.Errorf("商户订单号不能为空") + } + return &transfer.GetTransferBillByOutNoRequest{ + OutBillNo: core.String(order.OrderNo), + }, nil +} + +func queryResp(request *proto.QueryRequest, response *transfer.TransferBillEntity) (*proto.QueryResponse, error) { + + if response.FailReason != nil { + return nil, fmt.Errorf("%s-%s", response.State.GetText(), transfer.GetFailReasonMsg(*response.FailReason)) + } + + // 返回出去 + data, _ := json.Marshal(response) + + return &proto.QueryResponse{ + Result: &proto.Result{ + Status: statusResp(*response.State), + OrderNo: request.Order.GetOrderNo(), + TradeNo: *response.TransferBillNo, + Message: response.State.GetText(), + Data: data, + Extra: nil, + }, + }, nil +} + +func notifyResp(response *transfer.TransferBillEntity) *proto.NotifyResponse { + + // 返回出去 + data, _ := json.Marshal(response) + + return &proto.NotifyResponse{ + Result: &proto.Result{ + Status: statusResp(*response.State), + OrderNo: *response.OutBillNo, + TradeNo: *response.TransferBillNo, + Message: response.State.GetText(), + Data: data, + Extra: nil, + }, + } +} + +func statusResp(responseStatus transfer.TransferBillStatus) proto.Status { + + var status proto.Status + if responseStatus == transfer.TRANSFERBILLSTATUS_SUCCESS { + status = proto.Status_SUCCESS + } else if responseStatus == transfer.TRANSFERBILLSTATUS_PROCESSING { + status = proto.Status_ING + } else if responseStatus == transfer.TRANSFERBILLSTATUS_TRANSFERING { + status = proto.Status_ING + } else if responseStatus == transfer.TRANSFERBILLSTATUS_FAIL { + status = proto.Status_FAIL + } else if responseStatus == transfer.TRANSFERBILLSTATUS_CANCELING { + status = proto.Status_ING + } else if responseStatus == transfer.TRANSFERBILLSTATUS_CANCELLED { + status = proto.Status_FAIL + } else { + status = proto.Status_ING + } + + return status +} diff --git a/plugins/wechat_redpack_v2/internal/vo/code.go b/plugins/wechat_redpack_v2/internal/vo/code.go new file mode 100644 index 0000000..37123df --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/vo/code.go @@ -0,0 +1,9 @@ +package vo + +type Code int + +const CodeSuccess Code = 200 + +func (c Code) Value() int { + return int(c) +} diff --git a/plugins/wechat_redpack_v2/internal/vo/fail_reason.go b/plugins/wechat_redpack_v2/internal/vo/fail_reason.go new file mode 100644 index 0000000..bec6a2f --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/vo/fail_reason.go @@ -0,0 +1,32 @@ +package vo + +import "github.com/wechatpay-apiv3/wechatpay-go/services/transferbatch" + +var FailReasonMsg = map[transferbatch.FailReasonType]string{ + "ACCOUNT_FROZEN": "该用户账户被冻结", + "REAL_NAME_CHECK_FAIL": "收款人未实名认证,需要用户完成微信实名认证", + "NAME_NOT_CORRECT": "收款人姓名校验不通过,请核实信息", + "OPENID_INVALID": "Openid格式错误或者不属于商家公众账号", + "TRANSFER_QUOTA_EXCEED": "超过用户单笔收款额度,核实产品设置是否准确", + "DAY_RECEIVED_QUOTA_EXCEED": "超过用户单日收款额度,核实产品设置是否准确", + "MONTH_RECEIVED_QUOTA_EXCEED": "超过用户单月收款额度,核实产品设置是否准确", + "DAY_RECEIVED_COUNT_EXCEED": "超过用户单日收款次数,核实产品设置是否准确", + "PRODUCT_AUTH_CHECK_FAIL": "未开通该权限或权限被冻结,请核实产品权限状态", + "OVERDUE_CLOSE": "超过系统重试期,系统自动关闭", + "ID_CARD_NOT_CORRECT": "收款人身份证校验不通过,请核实信息", + "ACCOUNT_NOT_EXIST": "该用户账户不存在", + "TRANSFER_RISK": "该笔转账可能存在风险,已被微信拦截", + "OTHER_FAIL_REASON_TYPE": "其它失败原因", + "REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED": "用户账户收款受限,请引导用户在微信支付查看详情", + "RECEIVE_ACCOUNT_NOT_PERMMIT": "未配置该用户为转账收款人,请在产品设置中调整,添加该用户为收款人", + "PAYEE_ACCOUNT_ABNORMAL": "用户账户收款异常,请联系用户完善其在微信支付的身份信息以继续收款", + "PAYER_ACCOUNT_ABNORMAL": "商户账户付款受限,可前往商户平台获取解除功能限制指引", + "TRANSFER_SCENE_UNAVAILABLE": "该转账场景暂不可用,请确认转账场景ID是否正确", + "TRANSFER_SCENE_INVALID": "你尚未获取该转账场景,请确认转账场景ID是否正确", + "TRANSFER_REMARK_SET_FAIL": "转账备注设置失败,请调整后重新再试", + "RECEIVE_ACCOUNT_NOT_CONFIGURE": "请前往商户平台-商家转账到零钱-前往功能-转账场景中添加", + "BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH": "超出用户单月转账收款20w限额,本月不支持继续向该用户付款", + "BLOCK_B2C_USERLIMITAMOUNT_MONTH": "用户账户存在风险收款受限,本月不支持继续向该用户付款", + "MERCHANT_REJECT": "商户员工(转账验密人)已驳回转账", + "MERCHANT_NOT_CONFIRM": "商户员工(转账验密人)超时未验密", +} diff --git a/plugins/wechat_redpack_v2/internal/vo/query_status.go b/plugins/wechat_redpack_v2/internal/vo/query_status.go new file mode 100644 index 0000000..99b4820 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/vo/query_status.go @@ -0,0 +1,53 @@ +package vo + +import "gitea.cdlsxd.cn/sdk/plugin/proto" + +// QueryStatus +// INIT: 初始态。 系统转账校验中 +// WAIT_PAY: 待确认。待商户确认, 符合免密条件时, 系统会自动扭转为转账中 +// PROCESSING:转账中。正在处理中,转账结果尚未明确 +// SUCCESS:转账成功 +// FAIL:转账失败。需要确认失败原因后,再决定是否重新发起对该笔明细单的转账(并非整个转账批次单) +type QueryStatus string + +const ( + QueryStatusInit = "INIT" + QueryStatusWaitPay = "WAIT_PAY" + QueryStatusProcessing = "PROCESSING" + QueryStatusSuccess = "SUCCESS" + QueryStatusFail = "FAIL" +) + +var queryStatusTextMap = map[QueryStatus]string{ + QueryStatusInit: "初始态。 系统转账校验中", + QueryStatusWaitPay: "待确认。待商户确认, 符合免密条件时, 系统会自动扭转为转账中", + QueryStatusProcessing: "转账中。正在处理中,转账结果尚未明确", + QueryStatusSuccess: "转账成功", + QueryStatusFail: "转账失败。需要确认失败原因后,再决定是否重新发起对该笔明细单的转账(并非整个转账批次单)", +} + +var queryStatusMap = map[QueryStatus]proto.Status{ + QueryStatusInit: proto.Status_ING, + QueryStatusWaitPay: proto.Status_ING, + QueryStatusProcessing: proto.Status_ING, + QueryStatusSuccess: proto.Status_SUCCESS, + QueryStatusFail: proto.Status_FAIL, +} + +func (o QueryStatus) GetText() string { + msg, ok := queryStatusTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o QueryStatus) GetOrderStatus() proto.Status { + if o == "" { + return proto.Status_INVALID + } + if resultStatus, ok := queryStatusMap[o]; ok { + return resultStatus + } + return proto.Status_FAIL +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/mks.go b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/mks.go new file mode 100644 index 0000000..547a706 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/mks.go @@ -0,0 +1,133 @@ +package authorize + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + "transfer/internal/pkg/request" +) + +func GetTokenByMKS(appId, secret string) (accessToken string, expiresIn int64, err error) { + + uri := "http://marketapi.1688sup.com/mks/open/v1/wechat/getToken" + + body := struct { + AppId string `json:"app_id"` + Secret string `json:"secret"` + }{ + AppId: appId, + Secret: secret, + } + + bodyBytes, err := json.Marshal(body) + if err != nil { + return "", 0, err + } + + h := http.Header{ + "Content-Type": []string{"application/json"}, + } + + hc := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, // 最大空闲连接数 + MaxIdleConnsPerHost: 20, // 每个主机的最大空闲连接数 + IdleConnTimeout: 30 * time.Second, // 空闲连接超时时间 + }, + } + + isSuccess := func(code int) bool { + return code == http.StatusOK || code == http.StatusCreated + } + + srv := request.NewService(request.WithHttpClient(hc), request.WithStatusCodeFunc(isSuccess)) + + _, respBody, err := srv.POST(uri, h, bodyBytes) + if err != nil { + return "", 0, err + } + + var response struct { + Code int `json:"code"` + Data struct { + Token string `json:"token"` + Ttl string `json:"ttl"` + } `json:"data"` + Message string `json:"message"` + } + + if err2 := json.Unmarshal(respBody, &response); err2 != nil { + return "", 0, err2 + } + + ttl, err := strconv.ParseInt(response.Data.Ttl, 10, 64) + if err != nil { + return "", 0, err + } + + return response.Data.Token, ttl, nil +} + +func GetJsapiTicketByMKS(appId, secret string) (accessToken string, expiresIn int64, err error) { + + uri := "http://marketapi.1688sup.com/mks/open/v1/wechat/getJsapiTicket" + + body := struct { + AppId string `json:"app_id"` + Secret string `json:"secret"` + }{ + AppId: appId, + Secret: secret, + } + + bodyBytes, err := json.Marshal(body) + if err != nil { + return "", 0, err + } + + h := http.Header{ + "Content-Type": []string{"application/json"}, + } + + hc := &http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, // 最大空闲连接数 + MaxIdleConnsPerHost: 20, // 每个主机的最大空闲连接数 + IdleConnTimeout: 30 * time.Second, // 空闲连接超时时间 + }, + } + + isSuccess := func(code int) bool { + return code == http.StatusOK || code == http.StatusCreated + } + + srv := request.NewService(request.WithHttpClient(hc), request.WithStatusCodeFunc(isSuccess)) + + _, respBody, err := srv.POST(uri, h, bodyBytes) + if err != nil { + return "", 0, err + } + + var response struct { + Code int `json:"code"` + Data struct { + JsapiTicket string `json:"jsapi_ticket"` + Ttl string `json:"ttl"` + } `json:"data"` + Message string `json:"message"` + } + + if err2 := json.Unmarshal(respBody, &response); err2 != nil { + return "", 0, err2 + } + + ttl, err := strconv.ParseInt(response.Data.Ttl, 10, 64) + if err != nil { + return "", 0, err + } + + return response.Data.JsapiTicket, ttl, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/model.go b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/model.go new file mode 100644 index 0000000..c16d956 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/model.go @@ -0,0 +1,12 @@ +package authorize + +type TransferConfigRequest struct { + JsapiTicket string // 有效的jsapi_ticket, + Url string // 当前网页的URL,不包含#及其后面部分 +} + +type TransferConfigResponse struct { + Timestamp int64 // 必填,生成签名的时间戳 + NonceStr string // 必填,生成签名的随机串 + Signature string // 必填,签名 +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token.go b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token.go new file mode 100644 index 0000000..d4e977e --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token.go @@ -0,0 +1,135 @@ +package authorize + +import ( + "context" + "fmt" + "github.com/carlmjohnson/requests" + "net/url" + "time" + "transfer/internal/pkg/wechat/utils" +) + +func AuthorizeUrl(_ context.Context, appId, redirectUri string) (url string) { + + baseUrl := "https://open.weixin.qq.com/connect/oauth2/authorize" + + responseType := "code" + scope := "snsapi_base" + + return baseUrl + "?appid=" + appId + "&redirect_uri=" + redirectUri + "&response_type=" + responseType + "&scope=" + scope + "#wechat_redirect" +} + +// GetOpenId 获取openid https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/login/auth.code2Session.html +// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html +func GetOpenId(ctx context.Context, appId, secret, authCode string) (openid string, err error) { + + var uv = url.Values{} + + uv.Set("appid", appId) + uv.Set("secret", secret) + uv.Set("code", authCode) + uv.Set("grant_type", "authorization_code") + + var response struct { + //错误时 + ErrCode int64 `json:"errcode"` + ErrMsg string `json:"errmsg"` + //正常情况下 + Openid string `json:"openid"` + Unionid string `json:"unionid"` + SessionKey string `json:"session_key"` + } + + baseurl := "https://api.weixin.qq.com/sns/oauth2/access_token" + + if err = requests.URL(baseurl).Params(uv).ToJSON(&response).Fetch(ctx); err != nil { + return openid, fmt.Errorf("请求异常,msg:" + err.Error()) + } + + if response.ErrCode != 0 { + return openid, fmt.Errorf("请求错误,%d,%s", response.ErrCode, response.ErrMsg) + } + + return response.Openid, nil +} + +// GetToken 获取授权token https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html +func GetToken(ctx context.Context, appId, secret string) (accessToken string, expiresIn int64, err error) { + + baseurl := "https://api.weixin.qq.com/cgi-bin/token" + + var uv = url.Values{} + uv.Set("grant_type", "client_credential") + uv.Set("appid", appId) + uv.Set("secret", secret) + + type OrderResp struct { + //错误时 + ErrCode int64 `json:"errcode"` + ErrMsg string `json:"errmsg"` + //正常情况下 + ExpiresIn int64 `json:"expires_in"` + AccessToken string `json:"access_token"` + } + var response OrderResp + + err = requests.URL(baseurl).Post().Params(uv).ToJSON(&response).Fetch(ctx) + if err != nil { + return accessToken, expiresIn, fmt.Errorf("请求异常,msg:" + err.Error()) + } + + if response.ErrCode != 0 { + return accessToken, expiresIn, fmt.Errorf("请求错误,ErrCode[%d],ErrMsg[%s]", response.ErrCode, response.ErrMsg) + } + return response.AccessToken, response.ExpiresIn, nil +} + +// GetJsapiTicket https://developers.weixin.qq.com/doc/service/guide/h5/jssdk.html +func GetJsapiTicket(ctx context.Context, accessToken string) (ticket string, expiresIn int64, err error) { + + baseurl := "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + accessToken + "&type=jsapi" + + type OrderResp struct { + //错误时 + ErrCode int64 `json:"errcode"` + ErrMsg string `json:"errmsg"` + //正常情况下 + ExpiresIn int64 `json:"expires_in"` + Ticket string `json:"ticket"` + } + var response OrderResp + + err = requests.URL(baseurl).ToJSON(&response).Fetch(ctx) + if err != nil { + return accessToken, expiresIn, fmt.Errorf("请求异常,msg:" + err.Error()) + } + + if response.ErrCode != 0 { + return accessToken, expiresIn, fmt.Errorf("请求错误,ErrCode[%d],ErrMsg[%s]", response.ErrCode, response.ErrMsg) + } + return response.Ticket, response.ExpiresIn, nil +} + +// WxConfig 转账通知 @link https://developers.weixin.qq.com/doc/service/guide/h5/jssdk.html#62 +// 验证工具 https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign +func WxConfig(request *TransferConfigRequest) (response *TransferConfigResponse, err error) { + + nonceStr, err := utils.GenerateNonce() + if err != nil { + return nil, err + } + + timestamp := time.Now().Unix() + params := map[string]any{ + "noncestr": nonceStr, + "jsapi_ticket": request.JsapiTicket, + "timestamp": timestamp, + "url": request.Url, + } + + return &TransferConfigResponse{ + Timestamp: timestamp, + NonceStr: nonceStr, + Signature: utils.Sha1(utils.BuildSortedQueryString(params)), + }, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token_test.go new file mode 100644 index 0000000..70cdb82 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/authorize/token_test.go @@ -0,0 +1,53 @@ +package authorize + +import ( + "context" + "testing" +) + +func TestAuthorizeUrl(t *testing.T) { + // 公众号登录密码 fjxw1234 + ctx := context.Background() + appId := "wxe3bd59243545fa8a" + //redirectUri := "https://lsxdwx.access.86698.cn/" + redirectUri := "https://transferweb-pre.86698.cn/home/V8X7D5ydg5jpPE9kKZ" + url := AuthorizeUrl(ctx, appId, redirectUri) + + t.Logf("AuthorizeUrl() url = %s", url) +} + +func TestGetOpenId(t *testing.T) { + ctx := context.Background() + appId := "wxe3bd59243545fa8a" + secret := "4c9649cb998f71038e187b4c58f5fda0" + //ojbqr6HpeWKFy9Sgdx8yCmmeVJiws + openId, err := GetOpenId(ctx, appId, secret, "051d57Ha15I7ZJ0YJRGa1uDBEP1d57HM") + if err != nil { + t.Errorf("GetOpenId() error = %v", err) + return + } + t.Logf("GetOpenId() openId = %ss", openId) +} + +func TestGetToken(t *testing.T) { + appId := "wxe3bd59243545fa8a" + secret := "4c9649cb998f71038e187b4c58f5fda0" + gotAccessToken, gotExpiresIn, err := GetTokenByMKS(appId, secret) + if err != nil { + t.Errorf("GetToken() error = %v", err) + return + } + t.Logf("GetToken() gotAccessToken = %v, gotExpiresIn = %v", gotAccessToken, gotExpiresIn) +} + +func TestGetJsapiTicket(t *testing.T) { + appId := "wxe3bd59243545fa8a" + secret := "4c9649cb998f71038e187b4c58f5fda0" + ticket, gotExpiresIn, err := GetJsapiTicketByMKS(appId, secret) + if err != nil { + t.Errorf("GetToken() error = %v", err) + return + } + // O3SMpm8bG7kJnF36aXbe85Ex-xY8i_9qz8lzJov4TMdcDS_pIj_GV8TaZdrDvlvz9aoiI-8BJWGmGvRwq6RqrA + t.Logf("GetToken() ticket = %v, gotExpiresIn = %v", ticket, gotExpiresIn) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/srv.go b/plugins/wechat_redpack_v2/internal/wechat/srv/srv.go new file mode 100644 index 0000000..874f994 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/srv.go @@ -0,0 +1,9 @@ +package srv + +import ( + "plugins/wechat_redpack_v2/internal/wechat/utils" +) + +type Srv struct { + *utils.MchConfig +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/fail_reason.go b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/fail_reason.go new file mode 100644 index 0000000..98ace86 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/fail_reason.go @@ -0,0 +1,92 @@ +package transfer + +// 错误码常量定义 +const ( + ACCOUNT_FROZEN = "ACCOUNT_FROZEN" + ACCOUNT_NOT_EXIST = "ACCOUNT_NOT_EXIST" + BANK_CARD_ACCOUNT_ABNORMAL = "BANK_CARD_ACCOUNT_ABNORMAL" + BANK_CARD_BANK_INFO_WRONG = "BANK_CARD_BANK_INFO_WRONG" + BANK_CARD_CARD_INFO_WRONG = "BANK_CARD_CARD_INFO_WRONG" + BANK_CARD_COLLECTIONS_ABOVE_QUOTA = "BANK_CARD_COLLECTIONS_ABOVE_QUOTA" + BANK_CARD_PARAM_ERROR = "BANK_CARD_PARAM_ERROR" + BANK_CARD_STATUS_ABNORMAL = "BANK_CARD_STATUS_ABNORMAL" + BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH = "BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH" + BLOCK_B2C_USERLIMITAMOUNT_MONTH = "BLOCK_B2C_USERLIMITAMOUNT_MONTH" + DAY_RECEIVED_COUNT_EXCEED = "DAY_RECEIVED_COUNT_EXCEED" + DAY_RECEIVED_QUOTA_EXCEED = "DAY_RECEIVED_QUOTA_EXCEED" + EXCEEDED_ESTIMATED_AMOUNT = "EXCEEDED_ESTIMATED_AMOUNT" + ID_CARD_NOT_CORRECT = "ID_CARD_NOT_CORRECT" + MCH_CANCEL = "MCH_CANCEL" + MERCHANT_REJECT = "MERCHANT_REJECT" + MERCHANT_NOT_CONFIRM = "MERCHANT_NOT_CONFIRM" + NAME_NOT_CORRECT = "NAME_NOT_CORRECT" + OPENID_INVALID = "OPENID_INVALID" + OTHER_FAIL_REASON_TYPE = "OTHER_FAIL_REASON_TYPE" + OVERDUE_CLOSE = "OVERDUE_CLOSE" + PAYEE_ACCOUNT_ABNORMAL = "PAYEE_ACCOUNT_ABNORMAL" + PAYER_ACCOUNT_ABNORMAL = "PAYER_ACCOUNT_ABNORMAL" + PRODUCT_AUTH_CHECK_FAIL = "PRODUCT_AUTH_CHECK_FAIL" + REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED = "REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED" + REAL_NAME_CHECK_FAIL = "REAL_NAME_CHECK_FAIL" + RECEIVE_ACCOUNT_NOT_CONFIGURE = "RECEIVE_ACCOUNT_NOT_CONFIGURE" + RESERVATION_INFO_NOT_MATCH = "RESERVATION_INFO_NOT_MATCH" + RESERVATION_SCENE_NOT_MATCH = "RESERVATION_SCENE_NOT_MATCH" + RESERVATION_STATE_INVALID = "RESERVATION_STATE_INVALID" + TRANSFER_QUOTA_EXCEED = "TRANSFER_QUOTA_EXCEED" + TRANSFER_REMARK_SET_FAIL = "TRANSFER_REMARK_SET_FAIL" + TRANSFER_RISK = "TRANSFER_RISK" + TRANSFER_SCENE_INVALID = "TRANSFER_SCENE_INVALID" + TRANSFER_SCENE_UNAVAILABLE = "TRANSFER_SCENE_UNAVAILABLE" +) + +// 错误码到错误信息的映射 +var ErrorCodeMap = map[string]string{ + ACCOUNT_FROZEN: "该用户账户被冻结", + ACCOUNT_NOT_EXIST: "该用户账户不存在", + BANK_CARD_ACCOUNT_ABNORMAL: "银行卡已被销户、冻结、作废、挂失等致无法入账", + BANK_CARD_BANK_INFO_WRONG: "登记的银行名称或分支行信息有误", + BANK_CARD_CARD_INFO_WRONG: "银行卡户名或卡号有误", + BANK_CARD_COLLECTIONS_ABOVE_QUOTA: "银行卡属二/三类卡,达到收款限额无法入账", + BANK_CARD_PARAM_ERROR: "用户收款卡错误,请核实信息", + BANK_CARD_STATUS_ABNORMAL: "银行卡状态异常,无法入账", + BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH: "超出用户月转账收款限额,本月不支持继续向该用户付款", + BLOCK_B2C_USERLIMITAMOUNT_MONTH: "用户账户存在风险收款受限,本月不支持继续向该用户付款", + DAY_RECEIVED_COUNT_EXCEED: "超过用户日收款次数,核实产品设置是否准确", + DAY_RECEIVED_QUOTA_EXCEED: "超过用户日收款额度,核实产品设置是否准确", + EXCEEDED_ESTIMATED_AMOUNT: "转账金额超过预约金额范围,请检查", + ID_CARD_NOT_CORRECT: "收款人身份证校验不通过,请核实信息", + MCH_CANCEL: "商户撤销付款", + MERCHANT_REJECT: "商户员工(转账验密人)已驳回转账", + MERCHANT_NOT_CONFIRM: "商户员工(转账验密人)超时未验密", + NAME_NOT_CORRECT: "收款人姓名校验不通过,请核实信息", + OPENID_INVALID: "OpenID格式错误或者不属于商家公众账号", + OTHER_FAIL_REASON_TYPE: "其它失败原因", + OVERDUE_CLOSE: "超过系统重试期,系统自动关闭", + PAYEE_ACCOUNT_ABNORMAL: "用户账户收款异常,请联系用户完善其在微信支付的身份信息以继续收款", + PAYER_ACCOUNT_ABNORMAL: "商户账户付款受限,可前往商户平台获取解除功能限制指引", + PRODUCT_AUTH_CHECK_FAIL: "未开通该权限或权限被冻结,请核实产品权限状态", + REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED: "用户账户收款受限,请引导用户在微信支付查看详情", + REAL_NAME_CHECK_FAIL: "收款人未实名认证,需要用户完成微信实名认证", + RECEIVE_ACCOUNT_NOT_CONFIGURE: "请前往商户平台-商家转账-前往功能-转账场景中添加", + RESERVATION_INFO_NOT_MATCH: "转账信息,如用户OpenID等参数,与预约时传入的信息不一致,请检查", + RESERVATION_SCENE_NOT_MATCH: "该预约单的转账场景与发起转账时传入的不同,请检查", + RESERVATION_STATE_INVALID: "预约转账单状态异常,请检查", + TRANSFER_QUOTA_EXCEED: "超过用户单笔收款额度,核实产品设置是否准确", + TRANSFER_REMARK_SET_FAIL: "转账备注设置失败, 请调整后重新再试", + TRANSFER_RISK: "该笔转账可能存在风险,已被微信拦截", + TRANSFER_SCENE_INVALID: "你尚未获取该转账场景,请确认转账场景ID是否正确", + TRANSFER_SCENE_UNAVAILABLE: "该转账场景暂不可用,请确认转账场景ID是否正确", +} + +func GetFailReasonMsg(failReason string) string { + if msg, exists := ErrorCodeMap[failReason]; exists { + return msg + } + return failReason +} + +// IsReset https://pay.weixin.qq.com/doc/v3/merchant/4013774966 +func IsReset(failReason string) bool { + // 重置订单可再次申请转账, 超过系统重试期,系统自动关闭、商户员工(转账验密人)超时未验密 + return failReason == OVERDUE_CLOSE || failReason == MERCHANT_NOT_CONFIRM +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/model.go b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/model.go new file mode 100644 index 0000000..c6100be --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/model.go @@ -0,0 +1,173 @@ +package transfer + +import "encoding/json" + +type TransferBillStatus string + +func (e TransferBillStatus) Ptr() *TransferBillStatus { + return &e +} + +// ACCEPTED: 转账已受理 +// PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。 +// WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认 +// TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款 +// SUCCESS: 转账成功 +// FAIL: 转账失败 +// CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中 +// CANCELLED: 转账撤销完成 +const ( + TRANSFERBILLSTATUS_ACCEPTED TransferBillStatus = "ACCEPTED" + TRANSFERBILLSTATUS_PROCESSING TransferBillStatus = "PROCESSING" + TRANSFERBILLSTATUS_WAIT_USER_CONFIRM TransferBillStatus = "WAIT_USER_CONFIRM" + TRANSFERBILLSTATUS_TRANSFERING TransferBillStatus = "TRANSFERING" + TRANSFERBILLSTATUS_SUCCESS TransferBillStatus = "SUCCESS" + TRANSFERBILLSTATUS_FAIL TransferBillStatus = "FAIL" + TRANSFERBILLSTATUS_CANCELING TransferBillStatus = "CANCELING" + TRANSFERBILLSTATUS_CANCELLED TransferBillStatus = "CANCELLED" +) + +var TransferBillStatusMap = map[TransferBillStatus]string{ + TRANSFERBILLSTATUS_ACCEPTED: "转账已受理", + TRANSFERBILLSTATUS_PROCESSING: "转账锁定资金中", + TRANSFERBILLSTATUS_WAIT_USER_CONFIRM: "待收款用户确认", + TRANSFERBILLSTATUS_TRANSFERING: "转账中", + TRANSFERBILLSTATUS_SUCCESS: "转账成功", + TRANSFERBILLSTATUS_FAIL: "转账失败", + TRANSFERBILLSTATUS_CANCELING: "转账正在撤销中", + TRANSFERBILLSTATUS_CANCELLED: "转账撤销完成", +} + +func (s TransferBillStatus) GetValue() string { + return string(s) +} + +func (s TransferBillStatus) GetText() string { + if t, ok := TransferBillStatusMap[s]; ok { + return t + } + return "未知状态" +} + +type TransferToUserRequest struct { + Appid *string `json:"appid,omitempty"` + OutBillNo *string `json:"out_bill_no,omitempty"` + TransferSceneId *string `json:"transfer_scene_id,omitempty"` + Openid *string `json:"openid,omitempty"` + UserName *string `json:"user_name,omitempty"` + TransferAmount *int64 `json:"transfer_amount,omitempty"` + TransferRemark *string `json:"transfer_remark,omitempty"` + NotifyUrl *string `json:"notify_url,omitempty"` + UserRecvPerception *string `json:"user_recv_perception,omitempty"` + TransferSceneReportInfos []TransferSceneReportInfo `json:"transfer_scene_report_infos,omitempty"` +} + +type TransferSceneReportInfo struct { + InfoType *string `json:"info_type,omitempty"` + InfoContent *string `json:"info_content,omitempty"` +} + +type TransferToUserResponse struct { + OutBillNo *string `json:"out_bill_no,omitempty"` // 商户单号 + TransferBillNo *string `json:"transfer_bill_no,omitempty"` // 微信转账单号 string(64) + CreateTime *string `json:"create_time,omitempty"` // 单据创建时间 单据受理成功时返回 格式为yyyy-MM-DDThh:mm:ss+TIMEZONE + State *TransferBillStatus `json:"state,omitempty"` // 商家转账订单状态 + PackageInfo *string `json:"package_info,omitempty"` // 跳转领取页面的package信息 +} + +type CancelTransferResponse struct { + OutBillNo *string `json:"out_bill_no,omitempty"` + TransferBillNo *string `json:"transfer_bill_no,omitempty"` + State *string `json:"state,omitempty"` // 【单据状态】 CANCELING: 撤销中;CANCELLED:已撤销 + UpdateTime *string `json:"update_time,omitempty"` +} + +type CancelTransferRequest struct { + OutBillNo *string `json:"out_bill_no,omitempty"` +} + +func (o *CancelTransferRequest) MarshalJSON() ([]byte, error) { + type Alias CancelTransferRequest + a := &struct { + OutBillNo *string `json:"out_bill_no,omitempty"` + *Alias + }{ + // 序列化时移除非 Body 字段 + OutBillNo: nil, + Alias: (*Alias)(o), + } + return json.Marshal(a) +} + +type GetTransferBillByOutNoRequest struct { + OutBillNo *string `json:"out_bill_no,omitempty"` +} + +func (o *GetTransferBillByOutNoRequest) MarshalJSON() ([]byte, error) { + type Alias GetTransferBillByOutNoRequest + a := &struct { + OutBillNo *string `json:"out_bill_no,omitempty"` + *Alias + }{ + // 序列化时移除非 Body 字段 + OutBillNo: nil, + Alias: (*Alias)(o), + } + return json.Marshal(a) +} + +type TransferBillEntity struct { + MchId *string `json:"mch_id,omitempty"` // 商户号 + OutBillNo *string `json:"out_bill_no,omitempty"` // 商户单号 + TransferBillNo *string `json:"transfer_bill_no,omitempty"` // 微信单号 + Appid *string `json:"appid,omitempty"` // 应用id + State *TransferBillStatus `json:"state,omitempty"` // 商家转账订单状态 + TransferAmount *int64 `json:"transfer_amount,omitempty"` // 转账总金额,单位为“分” + TransferRemark *string `json:"transfer_remark,omitempty"` // 转账备注 + FailReason *string `json:"fail_reason,omitempty"` // 单已失败或者已退资金时,会返回订单失败原因 https://pay.weixin.qq.com/doc/v3/merchant/4013774966 + Openid *string `json:"openid,omitempty"` // 用户在商户appid下的唯一标识 + UserName *string `json:"user_name,omitempty"` // 收款用户姓名 + CreateTime *string `json:"create_time,omitempty"` // 单据创建时间 + UpdateTime *string `json:"update_time,omitempty"` // 最后一次状态变更时间 +} + +type GetTransferBillByNoRequest struct { + TransferBillNo *string `json:"transfer_bill_no,omitempty"` +} + +func (o *GetTransferBillByNoRequest) MarshalJSON() ([]byte, error) { + type Alias GetTransferBillByNoRequest + a := &struct { + TransferBillNo *string `json:"transfer_bill_no,omitempty"` + *Alias + }{ + // 序列化时移除非 Body 字段 + TransferBillNo: nil, + Alias: (*Alias)(o), + } + return json.Marshal(a) +} + +type TransferBillNotify struct { + OutBillNo *string `json:"out_bill_no,omitempty"` + TransferBillNo *string `json:"transfer_bill_no,omitempty"` + State *TransferBillStatus `json:"state,omitempty"` + MchId *string `json:"mch_id,omitempty"` + TransferAmount *int64 `json:"transfer_amount,omitempty"` + Openid *string `json:"openid,omitempty"` + FailReason *string `json:"fail_reason,omitempty"` + CreateTime *string `json:"create_time,omitempty"` + UpdateTime *string `json:"update_time,omitempty"` +} + +// TransferErr {"code":"INVALID_REQUEST","message":"对应单号已超出重试期;请查单确认后决定是否换单请求"} +type TransferErr struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +func BuildTransferErr(body []byte) (*TransferErr, error) { + ret := &TransferErr{} + err := json.Unmarshal(body, &ret) + return ret, err +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer.go b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer.go new file mode 100644 index 0000000..6496362 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer.go @@ -0,0 +1,111 @@ +package transfer + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "plugins/wechat_redpack_v2/internal/wechat/srv" + "plugins/wechat_redpack_v2/internal/wechat/utils" + "strings" +) + +const ( + transferToUserPath = "/v3/fund-app/mch-transfer/transfer-bills" + transferCancelPath = "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel" + transferQueryByBillByOutNoPath = "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}" + transferQueryByTransferBillNoPath = "/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}" +) + +type Transfer srv.Srv + +// TransferToUser 转账 @link https://pay.weixin.qq.com/doc/v3/merchant/4012716434 +func (srv *Transfer) TransferToUser(_ context.Context, req *TransferToUserRequest) (response *TransferToUserResponse, err error) { + + reqBody, err := json.Marshal(&req) + if err != nil { + return nil, err + } + + respBody, err := srv.Request(utils.Host, utils.MethodPOST, transferToUserPath, reqBody) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} + +// TransferCancel 取消转账 @link https://pay.weixin.qq.com/doc/v3/merchant/4012716458 +func (srv *Transfer) TransferCancel(_ context.Context, req *CancelTransferRequest) (response *CancelTransferResponse, err error) { + + reqBody, err := json.Marshal(req) + if err != nil { + return nil, err + } + + path := strings.Replace(transferCancelPath, "{out_bill_no}", url.PathEscape(*req.OutBillNo), -1) + + respBody, err := srv.Request(utils.Host, utils.MethodPOST, path, reqBody) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, response); err != nil { + return nil, err + } + + return response, nil +} + +// TransferQueryByBillByOutNo 商户单号查询转账单 @link https://pay.weixin.qq.com/doc/v3/merchant/4012716437 +func (srv *Transfer) TransferQueryByBillByOutNo(_ context.Context, req *GetTransferBillByOutNoRequest) (response *TransferBillEntity, err error) { + + path := strings.Replace(transferQueryByBillByOutNoPath, "{out_bill_no}", url.PathEscape(*req.OutBillNo), -1) + + respBody, err := srv.Request(utils.Host, utils.MethodGET, path, nil) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} + +// TransferQueryByTransferBillNo 微信单号查询转账单 @link https://pay.weixin.qq.com/doc/v3/merchant/4012716457 +func (srv *Transfer) TransferQueryByTransferBillNo(_ context.Context, req *GetTransferBillByNoRequest) (response *TransferBillEntity, err error) { + + path := strings.Replace(transferQueryByTransferBillNoPath, "{transfer_bill_no}", url.PathEscape(*req.TransferBillNo), -1) + + respBody, err := srv.Request(utils.Host, utils.MethodGET, path, nil) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(respBody, &response); err != nil { + return nil, err + } + + return response, nil +} + +// TransferNotify 转账通知 @link https://pay.weixin.qq.com/doc/v3/merchant/4012712115 +func (srv *Transfer) TransferNotify(_ context.Context, headers *http.Header, respBody []byte) (response *TransferBillEntity, err error) { + + bizStr, err := srv.Verify(headers, respBody) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(bizStr), &response); err != nil { + return nil, err + } + + return response, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer_test.go new file mode 100644 index 0000000..a548fa4 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/transfer/transfer_test.go @@ -0,0 +1,60 @@ +package transfer + +import ( + "context" + "fmt" + "os" + "plugins/wechat_redpack_v2/internal/wechat/utils" + "testing" +) + +func tr() *Transfer { + + dir, err := os.Getwd() + if err != nil { + panic(err) + } + + mchId := "1652322442" + certificateSerialNo := "7CCBCA9E6FA47845189FB8D212BA191EDE155613" + wechatPayPublicKeyId := "PUB_KEY_ID_0116523224422025061800192371001800" + + filePath := fmt.Sprintf("%s/cert/cert/%s", dir, mchId) + + c, err := utils.CreateMchConfig( + mchId, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + certificateSerialNo, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + fmt.Sprintf("%s/%s", filePath, "wechat_private_key.pem"), // 商户API证书私钥文件路径,本地文件路径 + wechatPayPublicKeyId, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + "7e6eb4a5ebeed3cf61c693586b11d00b", + ) + if err != nil { + panic(err) + } + + return &Transfer{c} +} + +func TestTransfer_Transfer(t *testing.T) { + + req := TransferToUserRequest{ + Appid: utils.String("wxe3bd59243545fa8a"), + OutBillNo: nil, + TransferSceneId: nil, + Openid: nil, + UserName: nil, + TransferAmount: nil, + TransferRemark: nil, + NotifyUrl: nil, + UserRecvPerception: nil, + TransferSceneReportInfos: nil, + } + gotResponse, err := tr().TransferToUser(context.Background(), &req) + if err != nil { + t.Errorf("Transfer.Transfer() error = %v", err) + return + } + + t.Logf("Transfer.Transfer() = %v", gotResponse) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/utils/aes.go b/plugins/wechat_redpack_v2/internal/wechat/utils/aes.go new file mode 100644 index 0000000..d95fb4c --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/utils/aes.go @@ -0,0 +1,87 @@ +package utils + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" +) + +const ( + keyLengthByte = 32 // AES-256 密钥长度(字节) + authTagLengthByte = 16 // GCM 认证标签长度(字节) +) + +// AesUtil 用于微信支付 AES-256-GCM 解密的工具类 +type AesUtil struct { + aesKey []byte // 32字节的AES密钥 +} + +// NewAesUtil 创建AesUtil实例,验证密钥长度 +func NewAesUtil(aesKey string) (*AesUtil, error) { + if len(aesKey) != keyLengthByte { + return nil, errors.New("无效的ApiV3Key,长度应为32个字节") + } + return &AesUtil{aesKey: []byte(aesKey)}, nil +} + +// DecryptToString 解密 AEAD_AES_256_GCM 加密的数据 +// associatedData: 附加认证数据 +// nonceStr: 随机数(12字节) +// ciphertext: 加密后的密文(Base64编码) +func (a *AesUtil) DecryptToString(associatedData, nonceStr, ciphertext string) (string, error) { + // 1. Base64解码密文 + cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("密文Base64解码失败: %w", err) + } + + // 2. 验证密文长度(需包含认证标签) + if len(cipherBytes) <= authTagLengthByte { + return "", errors.New("密文长度不足,无法解析认证标签") + } + + // 3. 分离密文和认证标签(GCM模式中,认证标签通常附加在密文末尾) + ctext := cipherBytes[:len(cipherBytes)-authTagLengthByte] + authTag := cipherBytes[len(cipherBytes)-authTagLengthByte:] + + // 4. 初始化AES-GCM加密器 + block, err := aes.NewCipher(a.aesKey) + if err != nil { + return "", fmt.Errorf("创建AES加密器失败: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("创建GCM模式失败: %w", err) + } + + // 5. 构建附加数据(GCM的AAD) + additionalData := []byte(associatedData) + + // 6. 解密(GCM模式会自动验证认证标签) + plaintext, err := gcm.Open(nil, []byte(nonceStr), append(ctext, authTag...), additionalData) + if err != nil { + return "", fmt.Errorf("解密失败(可能是密钥错误或数据被篡改): %w", err) + } + + return string(plaintext), nil +} + +type Resource struct { + OriginalType string `json:"original_type"` + Algorithm string `json:"algorithm"` + Ciphertext string `json:"ciphertext"` // 如支付通知中的 encrypted_data + AssociatedData string `json:"associated_data"` // 微信支付回调的附加数据 通常为空字符串或回调相关信息 + Nonce string `json:"nonce"` // 12字节字符串 +} + +type WxNotifyBody struct { + Id string `json:"id"` + CreateTime string `json:"create_time"` + ResourceType string `json:"resource_type"` + EventType string `json:"event_type"` + Summary string `json:"summary"` + Resource Resource `json:"resource"` +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go new file mode 100644 index 0000000..0c2a4da --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go @@ -0,0 +1,572 @@ +package utils + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "sort" + "strings" + "time" +) + +const ( + Host = "https://api.mch.weixin.qq.com" + MethodGET = "GET" + MethodPOST = "POST" +) + +// MchConfig 商户信息配置,用于调用商户API +// https://pay.weixin.qq.com/doc/v3/merchant/4012716434 +// +// 引用微信支付工具库 参考:https://pay.weixin.qq.com/doc/v3/merchant/4015119334 +type MchConfig struct { + mchId string // 商户号 + certificateSerialNo string // 商户API证书序列号 + privateKeyFilePath string // 商户API证书对应的私钥文件路径 + wechatPayPublicKeyId string // 微信支付公钥ID + wechatPayPublicKeyFilePath string // 微信支付公钥文件路径 + privateKey *rsa.PrivateKey // 商户API证书对应的私钥 + wechatPayPublicKey *rsa.PublicKey // 微信支付公钥 + aesKey string +} + +// MchId 商户号 +func (c *MchConfig) MchId() string { + return c.mchId +} + +// CertificateSerialNo 商户API证书序列号 +func (c *MchConfig) CertificateSerialNo() string { + return c.certificateSerialNo +} + +// PrivateKey 商户API证书对应的私钥 +func (c *MchConfig) PrivateKey() *rsa.PrivateKey { + return c.privateKey +} + +// WechatPayPublicKeyId 微信支付公钥ID +func (c *MchConfig) WechatPayPublicKeyId() string { + return c.wechatPayPublicKeyId +} + +// WechatPayPublicKey 微信支付公钥 +func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey { + return c.wechatPayPublicKey +} + +// CreateMchConfig MchConfig 构造函数 +func CreateMchConfig( + mchId string, + certificateSerialNo string, + privateKeyFilePath string, + wechatPayPublicKeyId string, + wechatPayPublicKeyFilePath string, + aesKey string, +) (*MchConfig, error) { + mchConfig := &MchConfig{ + mchId: mchId, + certificateSerialNo: certificateSerialNo, + privateKeyFilePath: privateKeyFilePath, + wechatPayPublicKeyId: wechatPayPublicKeyId, + wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath, + aesKey: aesKey, + } + privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath) + if err != nil { + return nil, err + } + mchConfig.privateKey = privateKey + wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath) + if err != nil { + return nil, err + } + mchConfig.wechatPayPublicKey = wechatPayPublicKey + return mchConfig, nil +} + +// LoadPrivateKey 通过私钥的文本内容加载私钥 +func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) { + block, _ := pem.Decode([]byte(privateKeyStr)) + if block == nil { + return nil, fmt.Errorf("decode private key err") + } + if block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse private key err:%s", err.Error()) + } + privateKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("not a RSA private key") + } + return privateKey, nil +} + +// LoadPublicKey 通过公钥的文本内容加载公钥 +func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) { + block, _ := pem.Decode([]byte(publicKeyStr)) + if block == nil { + return nil, errors.New("decode public key error") + } + if block.Type != "PUBLIC KEY" { + return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY") + } + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse public key err:%s", err.Error()) + } + publicKey, ok := key.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr) + } + return publicKey, nil +} + +// LoadPrivateKeyWithPath 通过私钥的文件路径内容加载私钥 +func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) { + privateKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read private pem file err:%s", err.Error()) + } + return LoadPrivateKey(string(privateKeyBytes)) +} + +// LoadPublicKeyWithPath 通过公钥的文件路径加载公钥 +func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) { + publicKeyBytes, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read certificate pem file err:%s", err.Error()) + } + return LoadPublicKey(string(publicKeyBytes)) +} + +// EncryptOAEPWithPublicKey 使用 OAEP padding方式用公钥进行加密 +func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) { + if publicKey == nil { + return "", fmt.Errorf("you should input *rsa.PublicKey") + } + ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil) + if err != nil { + return "", fmt.Errorf("encrypt message with public key err:%s", err.Error()) + } + ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte) + return ciphertext, nil +} + +// SignSHA256WithRSA 通过私钥对字符串以 SHA256WithRSA 算法生成签名信息 +func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) { + if privateKey == nil { + return "", fmt.Errorf("private key should not be nil") + } + h := crypto.Hash.New(crypto.SHA256) + _, err = h.Write([]byte(source)) + if err != nil { + return "", nil + } + hashed := h.Sum(nil) + signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signatureByte), nil +} + +// VerifySHA256WithRSA 通过公钥对字符串和签名结果以 SHA256WithRSA 验证签名有效性 +func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error { + if publicKey == nil { + return fmt.Errorf("public key should not be nil") + } + + sigBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return fmt.Errorf("verify failed: signature is not base64 encoded") + } + hashed := sha256.Sum256([]byte(source)) + err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes) + if err != nil { + return fmt.Errorf("verify signature with public key error:%s", err.Error()) + } + return nil +} + +// GenerateNonce 生成一个长度为 NonceLength 的随机字符串(只包含大小写字母与数字) +func GenerateNonce() (string, error) { + const ( + // NonceSymbols 随机字符串可用字符集 + NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + // NonceLength 随机字符串的长度 + NonceLength = 32 + ) + + bs := make([]byte, NonceLength) + _, err := rand.Read(bs) + if err != nil { + return "", err + } + symbolsByteLength := byte(len(NonceSymbols)) + for i, b := range bs { + bs[i] = NonceSymbols[b%symbolsByteLength] + } + return string(bs), nil +} + +// BuildAuthorization 构建请求头中的 Authorization 信息 +func BuildAuthorization( + mchid string, + certificateSerialNo string, + privateKey *rsa.PrivateKey, + method string, + canonicalURL string, + body []byte, +) (string, error) { + const ( + SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n" // 数字签名原文格式 + // HeaderAuthorizationFormat 请求头中的 Authorization 拼接格式 + HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"" + ) + + nonce, err := GenerateNonce() + if err != nil { + return "", err + } + timestamp := time.Now().Unix() + message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body) + signature, err := SignSHA256WithRSA(message, privateKey) + if err != nil { + return "", err + } + authorization := fmt.Sprintf( + HeaderAuthorizationFormat, + mchid, + nonce, + timestamp, + certificateSerialNo, + signature, + ) + return authorization, nil +} + +// ExtractResponseBody 提取应答报文的 Body +func ExtractResponseBody(response *http.Response) ([]byte, error) { + if response == nil { + return nil, nil + } + + if response.Body == nil { + return nil, nil + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("read response HttpBody err:[%s]", err.Error()) + } + response.Body = io.NopCloser(bytes.NewBuffer(body)) + return body, nil +} + +const ( + WechatPayTimestamp = "Wechatpay-Timestamp" // 微信支付回包时间戳 + WechatPayNonce = "Wechatpay-Nonce" // 微信支付回包随机字符串 + WechatPaySignature = "Wechatpay-Signature" // 微信支付回包签名信息 + WechatPaySerial = "Wechatpay-Serial" // 微信支付回包平台序列号 + RequestID = "Request-Id" // 微信支付回包请求ID +) + +// ValidateResponse 验证微信支付回包的签名信息 +func ValidateResponse( + wechatpayPublicKeyId string, + wechatpayPublicKey *rsa.PublicKey, + headers *http.Header, + body []byte, +) error { + requestID := headers.Get(RequestID) + timestampStr := headers.Get(WechatPayTimestamp) + serialNo := headers.Get(WechatPaySerial) + signature := headers.Get(WechatPaySignature) + nonce := headers.Get(WechatPayNonce) + + if timestampStr == "" { + return fmt.Errorf("header头请求时间戳未传递数据") + } + + if serialNo == "" { + return fmt.Errorf("header头请求平台序列号未传递数据") + } + + if nonce == "" { + return fmt.Errorf("header头请求随机字符串未传递数据") + } + + // 拒绝过期请求 + //timestamp, err := strconv.ParseInt(timestampStr, 10, 64) + //if err != nil { + // return fmt.Errorf("invalid timestamp: %v", err) + //} + + //if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute { + // return errors.New("invalid timestamp") + //} + + if serialNo != wechatpayPublicKeyId { + return fmt.Errorf( + "serial-no mismatch: got %s, expected %s, request-id: %s", + serialNo, + wechatpayPublicKeyId, + requestID, + ) + } + + message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body) + + if err2 := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err2 != nil { + return fmt.Errorf("invalid signature: %v, request-id: %s", err2, requestID) + } + + return nil +} + +// ApiException 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常 +type ApiException struct { + HttpStatusCode int // 应答报文的 HTTP 状态码 + HttpHeader http.Header // 应答报文的 Header 信息 + HttpBody []byte // 应答报文的 Body 原文 + ErrCode string // 微信支付回包的错误码 + ErrMessage string // 微信支付回包的错误信息 +} + +func (c *ApiException) Error() string { + buf := bytes.NewBuffer(nil) + buf.WriteString(fmt.Sprintf("srv error:[StatusCode: %d, Body: %s", c.HttpStatusCode, string(c.HttpBody))) + if len(c.HttpHeader) > 0 { + buf.WriteString(" Header: ") + for key, value := range c.HttpHeader { + buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value)) + } + buf.WriteString("\n") + } + buf.WriteString("]") + return buf.String() +} + +func (c *ApiException) StatusCode() int { + return c.HttpStatusCode +} + +func (c *ApiException) Header() http.Header { + return c.HttpHeader +} + +func (c *ApiException) Body() []byte { + return c.HttpBody +} + +func (c *ApiException) ErrorCode() string { + return c.ErrCode +} + +func (c *ApiException) ErrorMessage() string { + return c.ErrMessage +} + +func NewApiException(statusCode int, header http.Header, body []byte) error { + ret := &ApiException{ + HttpStatusCode: statusCode, + HttpHeader: header, + HttpBody: body, + } + + bodyObject := map[string]interface{}{} + if err := json.Unmarshal(body, &bodyObject); err == nil { + if val, ok := bodyObject["code"]; ok { + ret.ErrCode = val.(string) + } + if val, ok := bodyObject["message"]; ok { + ret.ErrMessage = val.(string) + } + } + + return ret +} + +// Time 复制 time.Time 对象,并返回复制体的指针 +func Time(t time.Time) *time.Time { + return &t +} + +// String 复制 string 对象,并返回复制体的指针 +func String(s string) *string { + return &s +} + +// Bool 复制 bool 对象,并返回复制体的指针 +func Bool(b bool) *bool { + return &b +} + +// Float64 复制 float64 对象,并返回复制体的指针 +func Float64(f float64) *float64 { + return &f +} + +// Float32 复制 float32 对象,并返回复制体的指针 +func Float32(f float32) *float32 { + return &f +} + +// Int64 复制 int64 对象,并返回复制体的指针 +func Int64(i int64) *int64 { + return &i +} + +// Int32 复制 int64 对象,并返回复制体的指针 +func Int32(i int32) *int32 { + return &i +} + +func (srv *MchConfig) Request(host, method, path string, reqBody []byte) (response []byte, err error) { + + reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path)) + if err != nil { + return nil, err + } + + httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody)) + + if err != nil { + return nil, err + } + + httpRequest.Header.Set("Accept", "application/json") + httpRequest.Header.Set("Wechatpay-Serial", srv.WechatPayPublicKeyId()) + httpRequest.Header.Set("Content-Type", "application/json") + + authorization, err := BuildAuthorization( + srv.MchId(), + srv.CertificateSerialNo(), + srv.PrivateKey(), + method, + reqUrl.Path, + reqBody, + ) + if err != nil { + return nil, err + } + + httpRequest.Header.Set("Authorization", authorization) + + client := &http.Client{} + httpResponse, err := client.Do(httpRequest) + if err != nil { + fmt.Printf("请求失败,Error: %v", err) + return nil, err + } + + respBody, err := ExtractResponseBody(httpResponse) + if err != nil { + fmt.Printf("获取应答失败,Error: %v", err) + return nil, err + } + + if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 { + // 2XX 成功,验证应答签名 + err = ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + &httpResponse.Header, + respBody, + ) + if err != nil { + fmt.Printf("验证应答签名失败,Error: %v", err) + return nil, err + } + + return respBody, nil + } + + return nil, &ApiException{ + HttpStatusCode: httpResponse.StatusCode, + HttpHeader: httpResponse.Header, + HttpBody: respBody, + } +} + +func (srv *MchConfig) Verify(headers *http.Header, respBody []byte) (string, error) { + + if respBody == nil { + return "", fmt.Errorf("request HttpBody is nil") + } + + err := ValidateResponse( + srv.WechatPayPublicKeyId(), + srv.WechatPayPublicKey(), + headers, + respBody, + ) + if err != nil { + return "", err + } + + var wxNotifyBody WxNotifyBody + if err = json.Unmarshal(respBody, &wxNotifyBody); err != nil { + return "", err + } + + aesUtil, err := NewAesUtil(srv.aesKey) + if err != nil { + return "", err + } + + return aesUtil.DecryptToString(wxNotifyBody.Resource.AssociatedData, wxNotifyBody.Resource.Nonce, wxNotifyBody.Resource.Ciphertext) +} + +// BuildSortedQueryString 函数接受一个 map,返回按照字段名排序后的 URL 键值对格式字符串 +func BuildSortedQueryString(params map[string]any) string { + // 创建一个字符串切片,用于保存所有的键名 + var keys []string + for key := range params { + keys = append(keys, key) + } + + // 对键名进行 ASCII 字典顺序排序 + sort.Strings(keys) + + // 构建一个 URL 键值对字符串 + var queryStrings []string + for _, key := range keys { + // 拼接 key=value + queryStrings = append(queryStrings, fmt.Sprintf("%s=%v", key, params[key])) + } + + // 使用 & 连接所有的 key=value 对 + return strings.Join(queryStrings, "&") +} + +func Sha1(data string) string { + // 创建一个 SHA-1 哈希对象 + hash := sha1.New() + // 写入数据 + hash.Write([]byte(data)) + // 计算并获取加密后的结果 + hashBytes := hash.Sum(nil) + // 将结果转换为十六进制字符串 + hashString := hex.EncodeToString(hashBytes) + // 打印加密后的 SHA-1 值 + return hashString +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility_test.go b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility_test.go new file mode 100644 index 0000000..7931489 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility_test.go @@ -0,0 +1,111 @@ +package utils + +import ( + "crypto/rsa" + "encoding/json" + "fmt" + "reflect" + "testing" +) + +func TestMchConfig_Request(t *testing.T) { + type fields struct { + mchId string + certificateSerialNo string + privateKeyFilePath string + wechatPayPublicKeyId string + wechatPayPublicKeyFilePath string + privateKey *rsa.PrivateKey + wechatPayPublicKey *rsa.PublicKey + } + type args struct { + host string + method string + path string + reqBody []byte + } + tests := []struct { + name string + fields fields + args args + wantResponse []byte + wantErr bool + }{ + { + name: "test", + fields: fields{ + mchId: "1652322442", + certificateSerialNo: "certificateSerialNo", + privateKeyFilePath: "privateKeyFilePath", + wechatPayPublicKeyId: "wechatPayPublicKeyId", + wechatPayPublicKeyFilePath: "wechatPayPublicKeyFilePath", + }, + args: args{ + host: "https://api.mch.weixin.qq.com", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := &MchConfig{ + mchId: tt.fields.mchId, + certificateSerialNo: tt.fields.certificateSerialNo, + privateKeyFilePath: tt.fields.privateKeyFilePath, + wechatPayPublicKeyId: tt.fields.wechatPayPublicKeyId, + wechatPayPublicKeyFilePath: tt.fields.wechatPayPublicKeyFilePath, + privateKey: tt.fields.privateKey, + wechatPayPublicKey: tt.fields.wechatPayPublicKey, + } + gotResponse, err := srv.Request(tt.args.host, tt.args.method, tt.args.path, tt.args.reqBody) + if (err != nil) != tt.wantErr { + t.Errorf("Request() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotResponse, tt.wantResponse) { + t.Errorf("Request() gotResponse = %v, want %v", gotResponse, tt.wantResponse) + } + }) + } +} + +func TestMDecryptToString(t *testing.T) { + + jsonStr := ` { + "id": "fd06376a-3e1b-5516-81f8-9b69cf1ba416", + "create_time": "2025-07-28T16:10:15+08:00", + "resource_type": "encrypt-resource", + "event_type": "MCHTRANSFER.BILL.FINISHED", + "summary": "商家转账单据终态通知", + "resource": { + "original_type": "mch_payment", + "algorithm": "AEAD_AES_256_GCM", + "ciphertext": "XJBIhrHgbe9NR5q/jLYmZKdT/3xuKm2x7EFu3T52Hj2hjPzarRSA2HCsGTxGojfD+CFyJHIULlL2adqLijAjpi3B6TaYKY4LqhtJ/RYSQtYNxYvBpWX1yLOWe8luJbWxmQvKZxIekFs8lGVgkPBUw0IfEAvJ6jHAGCcgxLIqxgOf6UtGUqxCCNp/V3xy8zCiHB0Mvlw8eXCTuG+ZESJIXvloVGNS79R6iNeqk4kNKRSaV86MNh1KQlmoBxZ4yEshD/vIlMulU3xEc+mM25y8vUS4Ot6pxEpUdUyjwcb9QTwTTnZzm6i+VWYymcItAVBQrvsKBMmqWnPtNXG8++13k3DeO1LyVKURmnWXXT1mImmGx/teN/1xPV5y6nChu/HTbcJGDQy2twuq6TPFbbYlTjZH047z/ZtozJNvGNeh", + "associated_data": "mch_payment", + "nonce": "YN3eW5H8mxLs" + } + }` + var wxNotifyBody WxNotifyBody + err := json.Unmarshal([]byte(jsonStr), &wxNotifyBody) + if err != nil { + fmt.Printf("JSON 解析失败: %v", err) + return + } + + // 微信支付 ApiV3 密钥(32字节) + apiV3Key := "7e6eb4a5ebeed3cf61c693586b11d00b" + + // 初始化解密工具 + aesUtil, err := NewAesUtil(apiV3Key) + if err != nil { + fmt.Printf("NewAesUtil err: %v", err) + return + } + + // 解密 + plaintext, err := aesUtil.DecryptToString(wxNotifyBody.Resource.AssociatedData, wxNotifyBody.Resource.Nonce, wxNotifyBody.Resource.Ciphertext) + if err != nil { + panic(fmt.Sprintf("解密失败: %v", err)) + } + + fmt.Println("解密结果:", plaintext) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat_redpack.go b/plugins/wechat_redpack_v2/internal/wechat_redpack.go new file mode 100644 index 0000000..0d34067 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat_redpack.go @@ -0,0 +1,128 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "net/http" + "plugins/wechat_redpack_v2/internal/wechat/srv/transfer" + "plugins/wechat_redpack_v2/internal/wechat/utils" +) + +// 插件通信信息,若不对应则会报错panic +const ( + Tag = "wechat_redpack_v2" + Version = 1 + CookieKey = "wechat_redpack_v2" + CookieValue = "wechat_redpack_v2" +) + +type WeChatRedPackService struct{} + +func (p *WeChatRedPackService) Order(ctx context.Context, request *proto.OrderRequest) (resp2 *proto.OrderResponse, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + config, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + req, err := orderReq(request.GetOrder(), request.GetProduct()) + if err != nil { + return nil, proto.ErrorParamFail(err.Error()) + } + + c, err := Client(config) + if err != nil { + return nil, proto.ErrorConfigFail(err.Error()) + } + + resp, err := c.TransferToUser(ctx, req) + if err != nil { + return nil, p.err(err) + } + + return orderResp(request.GetOrder(), resp) +} + +func (p *WeChatRedPackService) Query(ctx context.Context, request *proto.QueryRequest) (resp2 *proto.QueryResponse, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + config, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + req, err := queryReq(request.GetOrder()) + if err != nil { + return nil, proto.ErrorParamFail(err.Error()) + } + + c, err := Client(config) + if err != nil { + return nil, proto.ErrorConfigFail(err.Error()) + } + + resp, err := c.TransferQueryByBillByOutNo(ctx, req) + if err != nil { + return nil, p.err(err) + } + + return queryResp(request, resp) +} + +func (p *WeChatRedPackService) Notify(ctx context.Context, request *proto.NotifyRequest) (resp2 *proto.NotifyResponse, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + httpHeaders := make(http.Header) + if err := json.Unmarshal(request.Headers, &httpHeaders); err != nil { + return nil, proto.ErrorParamFail(fmt.Sprintf("headers Unmarshal err [%v]", err)) + } + + config, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + c, err := Client(config) + if err != nil { + return nil, proto.ErrorConfigFail(err.Error()) + } + + resp, err := c.TransferNotify(ctx, &httpHeaders, request.Body) + if err != nil { + return nil, p.err(err) + } + + return notifyResp(resp), nil +} + +func (p *WeChatRedPackService) err(err error) error { + if e, ok := err.(*utils.ApiException); ok { + + apiErr, err3 := transfer.BuildTransferErr(e.Body()) + if err3 != nil { + return err3 + } + + return fmt.Errorf("请求微信API错误: %s", apiErr.Message) + } + + return proto.ErrorRequestFail("请求微信返回错误:%v", err) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat_redpack_test.go b/plugins/wechat_redpack_v2/internal/wechat_redpack_test.go new file mode 100644 index 0000000..3013737 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat_redpack_test.go @@ -0,0 +1,95 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/stretchr/testify/assert" + "plugins/utils/wechat" + "testing" +) + +var server = &WeChatRedPackService{} + +func config() []byte { + c := &wechat.Server{ + MchID: "1629276485", + MchCertificateSerialNumber: "3C7E21B74C04BE6227A690EB44184F219D763F92", + } + marshal, _ := json.Marshal(c) + return marshal +} + +func TestConfig(t *testing.T) { + t.Run("TestConfig", func(t *testing.T) { + c := config() + fmt.Printf("%+s\n", string(c)) + assert.NotEmpty(t, c) + }) +} + +func TestOrder(t *testing.T) { + request := &proto.OrderRequest{ + Config: config(), + Order: &proto.OrderRequest_Order{ + OrderNo: "240403164049635931", + Account: "oO3vO5AxRWgTjmMD38FTvnB5Rq6o", + Quantity: 1, + Amount: 0.01, + Extra: []byte(`{"app_id":"13100720242", "out_detail_no":"stock_creator_mchid"}`), + }, + Product: &proto.OrderRequest_Product{ + ProductNo: "", + Extra: []byte(`{"batch_name":"13100720242", "batch_remark":"stock_creator_mchid"}`), + }, + } + t.Run("TestOrder", func(t *testing.T) { + got, err := server.Order(context.Background(), request) + if err != nil { + t.Errorf("Order() error = %v", err) + return + } + fmt.Printf("%+v", got) + assert.Equal(t, int(proto.Status_SUCCESS), int(got.Result.Status)) + }) +} + +func TestQuery(t *testing.T) { + request := &proto.QueryRequest{ + Config: config(), + Order: &proto.QueryRequest_Order{ + OrderNo: "202409141056275034200012", + TradeNo: "", + Account: "", + Extra: []byte(`{"out_detail_no":"0a2511525cc94a27bac18328771dc53e"}`), + }, + } + t.Run("TestQuery", func(t *testing.T) { + got, err := server.Query(context.Background(), request) + if err != nil { + t.Errorf("Query() error = %v", err) + return + } + fmt.Printf("%+v \n", got) + assert.Equal(t, int(proto.Status_SUCCESS), int(got.Result.Status)) + }) +} + +func TestNotify(t *testing.T) { + in := &proto.NotifyRequest{ + Config: config(), + Queries: []byte(``), + Headers: []byte(``), + Body: []byte(``), + } + t.Run("TestNotify", func(t *testing.T) { + got, err := server.Notify(context.Background(), in) + if err != nil { + t.Errorf("Notify() error = %v", err) + return + } + fmt.Printf("TestNotify : %+v \n", got) + assert.Equal(t, int(proto.Status_SUCCESS), int(got.Result.Status)) + }) +} diff --git a/plugins/wechat_redpack_v2/internal/wx.go b/plugins/wechat_redpack_v2/internal/wx.go new file mode 100644 index 0000000..3087a3c --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wx.go @@ -0,0 +1,87 @@ +package internal + +import ( + "fmt" + "os" + "plugins/utils/helper" + "plugins/wechat_redpack_v2/internal/wechat/srv/transfer" + "plugins/wechat_redpack_v2/internal/wechat/utils" + "sync" +) + +type manager struct { + once sync.Once + mutex sync.RWMutex + clients map[string]*transfer.Transfer +} + +var instance manager + +func init() { + instance = manager{ + clients: make(map[string]*transfer.Transfer), + } +} + +type Wechat struct { + MchID string `json:"mch_id"` + MchCertificateSerialNumber string `json:"mch_certificate_serial_number"` + WechatPayPublicKeyID string `json:"wechat_pay_public_key_id"` + MchApiV3Key string `json:"mch_api_v3_key"` +} + +func Client(wx *Wechat) (*transfer.Transfer, error) { + + if wx.MchID == "" { + return nil, fmt.Errorf("微信商户ID不能为空") + } + if wx.MchCertificateSerialNumber == "" { + return nil, fmt.Errorf("商户API证书序列号不能为空") + } + if wx.WechatPayPublicKeyID == "" { + return nil, fmt.Errorf("微信支付公钥ID不能为空") + } + if wx.MchApiV3Key == "" { + return nil, fmt.Errorf("微信apiv3key不能为空") + } + + if client, ok := instance.clients[wx.MchID]; ok { + return client, nil + } + + client, err := buildWx(wx) + if err != nil { + return nil, err + } + + instance.clients[wx.MchID] = client + + return client, nil +} + +func buildWx(wx *Wechat) (*transfer.Transfer, error) { + + dir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("商户ID[%s]获取目的地址有误[%v]", wx.MchID, err) + } + + filePath := fmt.Sprintf("%s/cert/%s", dir, wx.MchID) + if !helper.FileExists(filePath) { + panic(fmt.Sprintf("商户ID[%s]微信密钥证书信息不存在,请联系技术人员处理", wx.MchID)) + } + + cc, err := utils.CreateMchConfig( + wx.MchID, // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756 + wx.MchCertificateSerialNumber, // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053 + fmt.Sprintf("%s/%s", filePath, "apiclient_key.pem"), // 商户API证书私钥文件路径,本地文件路径 + wx.WechatPayPublicKeyID, // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816 + fmt.Sprintf("%s/%s", filePath, "pub_key.pem"), // 微信支付公钥文件路径,本地文件路径 + wx.MchApiV3Key, + ) + if err != nil { + return nil, fmt.Errorf("商户ID[%s]微信商户配置信息有误[%v]", wx.MchID, err) + } + + return &transfer.Transfer{MchConfig: cc}, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wx_test.go b/plugins/wechat_redpack_v2/internal/wx_test.go new file mode 100644 index 0000000..8b84ccb --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wx_test.go @@ -0,0 +1,29 @@ +package internal + +import ( + "testing" +) + +func TestNewWechat(t *testing.T) { + //c := &conf.Bootstrap{ + // Wechat: []*conf.Wechat{ + // { + // MchID: "123456", + // MchCertificateSerialNumber: "123456", + // WechatPayPublicKeyID: "123456", + // Name: "test", + // }, + // { + // MchID: "123456", + // MchCertificateSerialNumber: "123456", + // Name: "test", + // }, + // }, + //} + //got, err := NewWechat(c) + //if err != nil { + // t.Errorf("NewWechat() error = %v", err) + // return + //} + //t.Logf("NewWechat() = %v", got) +} diff --git a/plugins/wechat_redpack_v2/main.go b/plugins/wechat_redpack_v2/main.go new file mode 100644 index 0000000..027bb84 --- /dev/null +++ b/plugins/wechat_redpack_v2/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "gitea.cdlsxd.cn/sdk/plugin/shared" + "github.com/hashicorp/go-plugin" + "plugins/wechat_redpack_v2/internal" +) + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: shared.HandshakeConfig(internal.Version, internal.CookieKey, internal.CookieValue), + Plugins: shared.PluginSet(shared.NewPlugin(&internal.WeChatRedPackService{}, internal.Tag)), + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/zltx_card_v1/go.sum b/plugins/zltx_card_v1/go.sum index 77324b8..e69de29 100644 --- a/plugins/zltx_card_v1/go.sum +++ b/plugins/zltx_card_v1/go.sum @@ -1,75 +0,0 @@ -gitea.cdlsxd.cn/sdk/plugin v1.0.19 h1:j0Ifn3q+C7ibxSTfL1KbmnX1k/VO9e0XMDJSuPutixU= -gitea.cdlsxd.cn/sdk/plugin v1.0.19/go.mod h1:O/bYQWg1o9g/cBq9qNA3kLIpuPt7VDZqj1bPE6s04NM= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= -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/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/go-kratos/kratos/v2 v2.8.2 h1:EsEA7AmPQ2YQQ0FZrDWO2HgBNqeWM8z/mWKzS5UkQaQ= -github.com/go-kratos/kratos/v2 v2.8.2/go.mod h1:+Vfe3FzF0d+BfMdajA11jT0rAyJWublRE/seZQNZVxE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= -github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/zltx_v2/internal/zltx_v2_test.go b/plugins/zltx_v2/internal/zltx_v2_test.go index 49a1635..6a62766 100644 --- a/plugins/zltx_v2/internal/zltx_v2_test.go +++ b/plugins/zltx_v2/internal/zltx_v2_test.go @@ -14,9 +14,10 @@ var zltx = &ZLTXV2Service{} func config() []byte { c := &Config{ - AppId: "25891", - AppKey: "83cc38e09560417ad7ea0feaaae9d171", - BaseUri: "http://211.137.105.198:17100", + AppId: "25891", + AppKey: "83cc38e09560417ad7ea0feaaae9d171", + //BaseUri: "http://211.137.105.198:17100", + BaseUri: "http://117.175.169.61:17100", NotifyUrl: "https://gateway.dev.cdlsxd.cn/yxh5api/v1/order/direct/notify", } //{"app_id":"25891","app_key":"83cc38e09560417ad7ea0feaaae9d171","merchant_id":25891}