微信红包
This commit is contained in:
parent
7eed8473cc
commit
9cb8d100d8
8
Makefile
8
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
|
||||
all: zltx_v1 zltx_card_v1 zltx_v2 alipay_cpn alipay_redpack wechat_cpn wechat_redpack wechat_redpack_v2
|
||||
|
|
@ -2,7 +2,9 @@ package main
|
|||
|
||||
// main 这只是一个演示
|
||||
func main() {
|
||||
alipayOrderRedPack()
|
||||
//wechatQueryCpn()
|
||||
WechatRedPackV2Query()
|
||||
//alipayOrderRedPack()
|
||||
//zltxQuery()
|
||||
//wechatQueryCpn()
|
||||
//alipayQueryRedPack()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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=
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package vo
|
||||
|
||||
type Code int
|
||||
|
||||
const CodeSuccess Code = 200
|
||||
|
||||
func (c Code) Value() int {
|
||||
return int(c)
|
||||
}
|
||||
|
|
@ -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": "商户员工(转账验密人)超时未验密",
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 // 必填,签名
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package srv
|
||||
|
||||
import (
|
||||
"plugins/wechat_redpack_v2/internal/wechat/utils"
|
||||
)
|
||||
|
||||
type Srv struct {
|
||||
*utils.MchConfig
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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=
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue