微信红包

This commit is contained in:
ziming 2025-11-05 18:25:57 +08:00
parent 7eed8473cc
commit 9cb8d100d8
29 changed files with 2363 additions and 82 deletions

View File

@ -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

View File

@ -2,7 +2,9 @@ package main
// main 这只是一个演示
func main() {
alipayOrderRedPack()
//wechatQueryCpn()
WechatRedPackV2Query()
//alipayOrderRedPack()
//zltxQuery()
//wechatQueryCpn()
//alipayQueryRedPack()

View File

@ -21,7 +21,8 @@ var wechatCpnConf = &manage.Config{
func getWechatCpnConf() []byte {
c := &wechat.Server{
MchID: "1605446142", // 证书所属商户
MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76",
MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76", // old
//MchCertificateSerialNumber: "46B64A9AF817BCE0425AB2ED003E7FC3C3DC48D9",
}
marshal, _ := json.Marshal(c)
return marshal

60
cmd/wechat_redpack_v2.go Normal file
View File

@ -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)
}

View File

@ -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
)

View File

@ -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=

View File

@ -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
}

View File

@ -0,0 +1,9 @@
package vo
type Code int
const CodeSuccess Code = 200
func (c Code) Value() int {
return int(c)
}

View File

@ -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": "商户员工(转账验密人)超时未验密",
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 // 必填,签名
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,9 @@
package srv
import (
"plugins/wechat_redpack_v2/internal/wechat/utils"
)
type Srv struct {
*utils.MchConfig
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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,
})
}

View File

@ -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=

View File

@ -16,7 +16,8 @@ func config() []byte {
c := &Config{
AppId: "25891",
AppKey: "83cc38e09560417ad7ea0feaaae9d171",
BaseUri: "http://211.137.105.198:17100",
//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}