From 1aaae37bebee3fd815936e85694674a8e8914713 Mon Sep 17 00:00:00 2001 From: ziming Date: Wed, 24 Dec 2025 15:30:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=AF=E6=98=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 89 ++++++-- cmd/main.go | 3 +- cmd/wechat_cpn.go | 20 +- cmd/wechat_redpack_v2.go | 2 +- cmd/zltx_card.go | 51 ++++- plugins/qixing_alipay_redpack/go.mod | 39 ++++ plugins/qixing_alipay_redpack/go.sum | 82 +++++++ .../internal/po/notify.go | 10 + .../internal/po/order.go | 69 ++++++ .../qixing_alipay_redpack/internal/po/po.go | 60 +++++ .../internal/po/query.go | 48 ++++ .../internal/qixing_alipay_redpack.go | 135 +++++++++++ .../internal/qixing_alipay_redpack_test.go | 126 +++++++++++ .../internal/transform.go | 149 +++++++++++++ .../qixing_alipay_redpack/internal/util.go | 119 ++++++++++ .../qixing_alipay_redpack/internal/vo/code.go | 23 ++ .../internal/vo/status.go | 44 ++++ .../qixing_alipay_redpack/internal/vo/type.go | 41 ++++ plugins/qixing_alipay_redpack/main.go | 15 ++ plugins/qixing_wechat_redpack/go.mod | 39 ++++ plugins/qixing_wechat_redpack/go.sum | 82 +++++++ .../internal/po/notify.go | 25 +++ .../internal/po/order.go | 53 +++++ .../qixing_wechat_redpack/internal/po/po.go | 60 +++++ .../internal/po/query.go | 48 ++++ .../internal/qixing_wechat_redpack.go | 148 ++++++++++++ .../internal/qixing_wechat_redpack_test.go | 131 +++++++++++ .../internal/transform.go | 168 ++++++++++++++ .../qixing_wechat_redpack/internal/util.go | 118 ++++++++++ .../qixing_wechat_redpack/internal/vo/code.go | 23 ++ .../qixing_wechat_redpack/internal/vo/mode.go | 29 +++ .../internal/vo/status.go | 47 ++++ .../qixing_wechat_redpack/internal/vo/type.go | 47 ++++ plugins/qixing_wechat_redpack/main.go | 15 ++ .../wechat/srv/redpack/miniprogram/base.go | 14 ++ .../wechat/srv/redpack/miniprogram/model.go | 210 ++++++++++++++++++ .../wechat/srv/redpack/miniprogram/query.go | 77 +++++++ .../srv/redpack/miniprogram/query_test.go | 52 +++++ .../wechat/srv/redpack/miniprogram/send.go | 103 +++++++++ .../srv/redpack/miniprogram/send_biz_rp.go | 27 +++ .../srv/redpack/miniprogram/send_test.go | 112 ++++++++++ .../wechat/srv/redpack/official/base.go | 14 ++ .../wechat/srv/redpack/official/model.go | 192 ++++++++++++++++ .../wechat/srv/redpack/official/query.go | 77 +++++++ .../wechat/srv/redpack/official/query_test.go | 57 +++++ .../wechat/srv/redpack/official/send.go | 106 +++++++++ .../wechat/srv/redpack/official/send_test.go | 66 ++++++ .../internal/wechat/srv/redpack/redpack.go | 101 +++++++++ .../internal/wechat/utils/md5.go | 30 +++ .../internal/wechat/utils/wxpay_utility.go | 3 +- plugins/zltx_card_v1/go.mod | 2 +- plugins/zltx_card_v1/go.sum | 4 +- plugins/zltx_card_v1/internal/zltx_card_v1.go | 6 +- utils/request/request_test.go | 12 +- 54 files changed, 3367 insertions(+), 56 deletions(-) create mode 100644 plugins/qixing_alipay_redpack/go.mod create mode 100644 plugins/qixing_alipay_redpack/go.sum create mode 100644 plugins/qixing_alipay_redpack/internal/po/notify.go create mode 100644 plugins/qixing_alipay_redpack/internal/po/order.go create mode 100644 plugins/qixing_alipay_redpack/internal/po/po.go create mode 100644 plugins/qixing_alipay_redpack/internal/po/query.go create mode 100644 plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack.go create mode 100644 plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack_test.go create mode 100644 plugins/qixing_alipay_redpack/internal/transform.go create mode 100644 plugins/qixing_alipay_redpack/internal/util.go create mode 100644 plugins/qixing_alipay_redpack/internal/vo/code.go create mode 100644 plugins/qixing_alipay_redpack/internal/vo/status.go create mode 100644 plugins/qixing_alipay_redpack/internal/vo/type.go create mode 100644 plugins/qixing_alipay_redpack/main.go create mode 100644 plugins/qixing_wechat_redpack/go.mod create mode 100644 plugins/qixing_wechat_redpack/go.sum create mode 100644 plugins/qixing_wechat_redpack/internal/po/notify.go create mode 100644 plugins/qixing_wechat_redpack/internal/po/order.go create mode 100644 plugins/qixing_wechat_redpack/internal/po/po.go create mode 100644 plugins/qixing_wechat_redpack/internal/po/query.go create mode 100644 plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack.go create mode 100644 plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack_test.go create mode 100644 plugins/qixing_wechat_redpack/internal/transform.go create mode 100644 plugins/qixing_wechat_redpack/internal/util.go create mode 100644 plugins/qixing_wechat_redpack/internal/vo/code.go create mode 100644 plugins/qixing_wechat_redpack/internal/vo/mode.go create mode 100644 plugins/qixing_wechat_redpack/internal/vo/status.go create mode 100644 plugins/qixing_wechat_redpack/internal/vo/type.go create mode 100644 plugins/qixing_wechat_redpack/main.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/base.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/model.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_biz_rp.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/base.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/model.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send_test.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/srv/redpack/redpack.go create mode 100644 plugins/wechat_redpack_v2/internal/wechat/utils/md5.go diff --git a/Makefile b/Makefile index 3e434d8..14f9c9b 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,52 @@ +# 强制指定 Go 版本(需与主程序编译环境一致) +REQUIRED_GO_VERSION := 1.23.6 +# 目标平台配置(根据运行环境调整,如 arm64) +TARGET_GOOS := linux +TARGET_GOARCH := amd64 +# 交叉编译工具链(Mac 编译 Linux amd64 必需) +TARGET_CC := x86_64-linux-musl-gcc + +build-linux1: + @echo "===== 开始编译 Linux 插件 =====" + @echo "要求 Go 版本: $(REQUIRED_GO_VERSION)" + @echo "目标平台: $(TARGET_GOOS)/$(TARGET_GOARCH)" + + # 1. 检查 Go 版本 + @go version | grep -q $(REQUIRED_GO_VERSION) || ( \ + echo "错误:需要 Go $(REQUIRED_GO_VERSION),当前版本:$(shell go version)"; \ + exit 1 \ + ) + + # 2. 检查插件目录 + @[ -d "plugins/${name}" ] || ( \ + echo "错误:插件目录 plugins/${name} 不存在"; \ + exit 1 \ + ) + + # 3. 核心:所有命令写在同一行,确保环境变量作用域生效 + # 注意:&& 连接,且环境变量紧跟 cd 后,与 go build 同进程 + cd plugins/${name} && \ + CGO_ENABLED=1 \ + CC=$(TARGET_CC) \ + GOOS=$(TARGET_GOOS) \ + GOARCH=$(TARGET_GOARCH) \ + GOPROXY=https://goproxy.cn,direct \ + go build -buildmode=plugin -trimpath -o ../../pkg/linux/${name}.so . + + # 4. 验证编译结果 + @echo "===== 编译完成 =====" + @echo "插件路径: pkg/linux/${name}.so" + @echo "插件格式检测:" + @file pkg/linux/${name}.so + +build-linux: + cd plugins; \ + export GOOS=linux; \ + export GOARCH=amd64; \ + export CGO_ENABLED=0; \ + export GOPROXY=https://goproxy.cn,direct; \ + cd ${name} && go build -o ../../pkg/linux/${name}.so . + build-mac: cd plugins; \ export GOOS=darwin; \ @@ -15,73 +64,77 @@ build-win: export GOPROXY=https://goproxy.cn,direct; \ cd ${name} && go build -o ../../pkg/win/${name}.so . -build-linux: - cd plugins; \ - export GOOS=linux; \ - export GOARCH=amd64; \ - export CGO_ENABLED=0; \ - export GOPROXY=https://goproxy.cn,direct; \ - cd ${name} && go build -o ../../pkg/linux/${name}.so . - .PHONY: zltx_v1 zltx_v1: - make build-mac name=zltx_v1 && \ make build-linux name=zltx_v1 && \ + make build-mac name=zltx_v1 && \ make build-win name=zltx_v1 .PHONY: zltx_card_v1 zltx_card_v1: - make build-mac name=zltx_card_v1 && \ make build-linux name=zltx_card_v1 && \ + make build-mac name=zltx_card_v1 && \ make build-win name=zltx_card_v1 .PHONY: zltx_v2 zltx_v2: - make build-mac name=zltx_v2 && \ make build-linux name=zltx_v2 && \ + make build-mac name=zltx_v2 && \ make build-win name=zltx_v2 .PHONY: alipay_cpn alipay_cpn: - make build-mac name=alipay_cpn && \ make build-linux name=alipay_cpn && \ + make build-mac name=alipay_cpn && \ make build-win name=alipay_cpn .PHONY: alipay_redpack alipay_redpack: - make build-mac name=alipay_redpack && \ make build-linux name=alipay_redpack && \ + make build-mac name=alipay_redpack && \ make build-win name=alipay_redpack .PHONY: wechat_cpn wechat_cpn: - make build-mac name=wechat_cpn && \ make build-linux name=wechat_cpn && \ + make build-mac name=wechat_cpn && \ make build-win name=wechat_cpn .PHONY: wechat_redpack wechat_redpack: - make build-mac name=wechat_redpack && \ make build-linux name=wechat_redpack && \ + make build-mac 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-mac 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 && \ make build-linux name=union_pay_cpn && \ + make build-mac name=union_pay_cpn && \ make build-win name=union_pay_cpn .PHONY: union_pay_redpack union_pay_redpack: - make build-mac name=union_pay_redpack && \ make build-linux name=union_pay_redpack && \ + make build-mac name=union_pay_redpack && \ make build-win name=union_pay_redpack +.PHONY: qixing_alipay_redpack +qixing_alipay_redpack: + make build-linux name=qixing_alipay_redpack && \ + make build-mac name=qixing_alipay_redpack && \ + make build-win name=qixing_alipay_redpack + +.PHONY: qixing_wechat_redpack +qixing_wechat_redpack: + make build-linux name=qixing_wechat_redpack && \ + make build-mac name=qixing_wechat_redpack && \ + make build-win name=qixing_wechat_redpack + .PHONY: all all: zltx_v1 zltx_card_v1 zltx_v2 alipay_cpn alipay_redpack wechat_cpn wechat_redpack wechat_redpack_v2 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 67e540d..fc574c6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,11 +7,12 @@ func main() { //WechatRedPackV2Order() //WechatRedPackV2Query() - WechatRedPackV2Notify() + //WechatRedPackV2Notify() //alipayOrderRedPack() //alipayQueryRedPack() //zltxQuery() + zltxCardQuery() //zltxCardNotify() } diff --git a/cmd/wechat_cpn.go b/cmd/wechat_cpn.go index c3bfb25..9207f42 100644 --- a/cmd/wechat_cpn.go +++ b/cmd/wechat_cpn.go @@ -20,9 +20,9 @@ var wechatCpnConf = &manage.Config{ func getWechatCpnConf() []byte { c := &wechat.Server{ - MchID: "1605446142", // 证书所属商户 - MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76", // old - //MchCertificateSerialNumber: "46B64A9AF817BCE0425AB2ED003E7FC3C3DC48D9", + MchID: "1605446142", // 证书所属商户 + //MchCertificateSerialNumber: "4D081089DEB385316CBDCB55C070287E4920AC76", // old 过期 + MchCertificateSerialNumber: "46B64A9AF817BCE0425AB2ED003E7FC3C3DC48D9", // 新的 } marshal, _ := json.Marshal(c) return marshal @@ -37,12 +37,12 @@ func wechatOrderCpn() { request := &proto.OrderRequest{ Config: getWechatCpnConf(), Order: &proto.OrderRequest_Order{ - OrderNo: "2024112215282589700100009", - Account: "oknbq5tQyff_vCjjRjCJBElhF1og", - Extra: []byte(`{"app_id":"wx83fd6da8093f55b7","stock_creator_mchid":"1679625521"}`), + OrderNo: "202512051115219880010012", // {status:ING order_no:"202512051115219880010012" trade_no:"141771369565" message:"成功"} + Account: "oknbq5mAjfgPiCJV028g-ZFDx1WU", + Extra: []byte(`{"app_id":"wx83fd6da8093f55b7","stock_creator_mchid":"1652465541"}`), }, Product: &proto.OrderRequest_Product{ - ProductNo: "19519911", + ProductNo: "21144469", Extra: []byte(`{}`), }, } @@ -63,9 +63,9 @@ func wechatQueryCpn() { Config: getWechatCpnConf(), Order: &proto.QueryRequest_Order{ OrderNo: "", - TradeNo: "69445765514", - Account: "oO3vO5AxRWgTjmMD38FTvnB5Rq6o", - Extra: []byte(`{"app_id":"wx9ed74283ad25bca1"}`), + TradeNo: "141771369565", + Account: "oknbq5mAjfgPiCJV028g-ZFDx1WU", + Extra: []byte(`{"app_id":"wx83fd6da8093f55b7"}`), }, } resQuery, err := instance.Query(context.Background(), alpayRedConf.Tag, queryRequest) diff --git a/cmd/wechat_redpack_v2.go b/cmd/wechat_redpack_v2.go index 8643c6e..aaa67ca 100644 --- a/cmd/wechat_redpack_v2.go +++ b/cmd/wechat_redpack_v2.go @@ -95,7 +95,7 @@ func WechatRedPackV2Query() { request := &proto.QueryRequest{ Config: getWechatRedPackV2Conf(), Order: &proto.QueryRequest_Order{ - OrderNo: "19497351672832450564", + OrderNo: "19957295972489175049", TradeNo: "", Account: "", Extra: []byte(``), diff --git a/cmd/zltx_card.go b/cmd/zltx_card.go index fc4291a..484ae65 100644 --- a/cmd/zltx_card.go +++ b/cmd/zltx_card.go @@ -10,11 +10,11 @@ import ( ) var zltxCardConf = &manage.Config{ - Cmd: "pkg/mac/zltx_card_v1_1.so", - Tag: "zltx_card_v1_1", + Cmd: "pkg/mac/zltx_card_v1.so", + Tag: "zltx_card_v1", Version: 1, - CookieKey: "zltx_card_v1_1", - CookieValue: "zltx_card_v1_1", + CookieKey: "zltx_card_v1", + CookieValue: "zltx_card_v1", } func zlxtCardCf() []byte { @@ -39,6 +39,39 @@ func zlxtCardCf() []byte { //return marshal } +func zltxCardQuery() { + + err := manage.Add(zltxCardConf) + if err != nil { + log.Fatalln(err) + } + + defer func() { + fmt.Println("zltx_card_v1_1 close start") + manage.Close() + fmt.Println("zltx_card_v1_1 close end") + }() + + for i := 0; i < 1; i++ { + req := &proto.QueryRequest{ + Config: zlxtCardCf(), + Order: &proto.QueryRequest_Order{ + OrderNo: "test_plugin_zltx_v1_card_3", + TradeNo: "", + Account: "", + Extra: nil, + }, + } + res, err := instance.Query(context.Background(), zltxCardConf.Tag, req) + if err != nil { + log.Printf("query err:%+v i:%d", err, i) + } else { + log.Printf("query res:%+v i:%d", res, i) + } + } + +} + // main 这只是一个演示 func zltxCardNotify() { @@ -53,12 +86,6 @@ func zltxCardNotify() { fmt.Println("zltx_card_v1_1 close end") }() - //req := &proto.NotifyRequest{ - // Config: zlxtCardCf(), - // Queries: []byte(``), - // Headers: []byte(`{"Accept-Encoding":["gzip, deflate, br"],"Authorization":["MD5 appid=101,sign=1A821E1E6FA824C7099D7F17F58E1650"],"Connection":["close"],"Content-Length":["183"],"Content-Type":["application/json"],"Cookie":[""],"User-Agent":["GuzzleHttp/6.5.5 curl/7.69.1 PHP/7.2.34"],"X-Forwarded-For":["47.96.248.136"],"X-Real-Ip":["47.96.248.136"],"X-Remoteaddr":["172.18.0.1"]}`), - // Body: []byte(`{"merchantId":25943,"outTradeNo":"202506161700247580010056","tradeNo":"789175179564695553","status":"01","cardCode":"XPHrv0+uPVQOqfymz1jJAsLOOuGpfvgXi9RIF1m4tRCsDdvcZDDNY21M/22F56M1"`), - //} // transport: error while dialing: dial unix /tmp/plugin2702936918: connect: connection refused for i := 0; i < 5; i++ { req := &proto.NotifyRequest{ @@ -69,9 +96,9 @@ func zltxCardNotify() { } res, err := instance.Notify(context.Background(), zltxCardConf.Tag, req) if err != nil { - log.Printf("Order err:%+v i:%d", err, i) + log.Printf("notify err:%+v i:%d", err, i) } else { - log.Printf("Order res:%+v i:%d", res, i) + log.Printf("notify res:%+v i:%d", res, i) } } diff --git a/plugins/qixing_alipay_redpack/go.mod b/plugins/qixing_alipay_redpack/go.mod new file mode 100644 index 0000000..899ea0e --- /dev/null +++ b/plugins/qixing_alipay_redpack/go.mod @@ -0,0 +1,39 @@ +module plugins/qixing_alipay_redpack + +go 1.22.2 + +replace plugins/utils => ../../utils + +require ( + gitea.cdlsxd.cn/sdk/plugin v1.0.19 + github.com/go-playground/validator/v10 v10.22.0 + github.com/hashicorp/go-plugin v1.6.1 + github.com/stretchr/testify v1.9.0 + plugins/utils v0.0.0-00010101000000-000000000000 +) + +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/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.13 // indirect + github.com/mattn/go-isatty v0.0.20 // 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.26.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugins/qixing_alipay_redpack/go.sum b/plugins/qixing_alipay_redpack/go.sum new file mode 100644 index 0000000..3abbe24 --- /dev/null +++ b/plugins/qixing_alipay_redpack/go.sum @@ -0,0 +1,82 @@ +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= +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/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/qixing_alipay_redpack/internal/po/notify.go b/plugins/qixing_alipay_redpack/internal/po/notify.go new file mode 100644 index 0000000..8b867ef --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/po/notify.go @@ -0,0 +1,10 @@ +package po + +import "plugins/qixing_alipay_redpack/internal/vo" + +type Notify struct { + BatchId string `json:"batchId"` + BizNo string `json:"bizNo"` + Type vo.Type `json:"type"` + FailMsg string `json:"failMsg"` +} diff --git a/plugins/qixing_alipay_redpack/internal/po/order.go b/plugins/qixing_alipay_redpack/internal/po/order.go new file mode 100644 index 0000000..6a0e508 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/po/order.go @@ -0,0 +1,69 @@ +package po + +import ( + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" +) + +type OrderReq struct { + Amount float32 `validate:"required" json:"amount"` // 微信:0.3-200;支付宝:<400单位:元 + BatchId string `validate:"required" json:"batchId"` // 批次ID + BizNo string `validate:"required" json:"bizNo"` // 业务单号,需唯一 + + Name string `json:"name"` // 真实姓名 支付宝必填 + Phone string `json:"phone"` // 支付宝必填 + + //WxAppId string `json:"wxAppId"` // 公众号ID 微信必填 + //OpenId string `json:"openId"` // 微信openId 微信必填 + //Wishing string `json:"wishing"` // 红包祝福语,微信使用 + + //RedPacketName string `son:"redPacketName"` // 红包名称 默认"现金红包" + //Expand string `json:"expand"` // 拓展信息 + //Remark string `json:"remark"` // 红包备注 默认“现金红包” + //SendName string `json:"sendName"` // 发送名称 微信使用默认与redPacketName相同 + + NotifyUrl string `validate:"required" json:"notifyUrl"` + + Sign string `json:"sign"` // MD5签名 +} + +type OrderWechatReq struct { + Amount float32 `validate:"required" json:"amount"` // 微信:0.3-200;支付宝:<400单位:元 + BatchId string `validate:"required" json:"batchId"` // 批次ID + BizNo string `validate:"required" json:"bizNo"` // 业务单号,需唯一 + + WxAppId string `validate:"required" json:"wxAppId"` // 公众号ID 微信必填 + OpenId string `validate:"required" json:"openId"` // 微信openId 微信必填 + Wishing string `validate:"required" json:"wishing"` // 红包祝福语,微信使用 + + RedPacketName string `validate:"required" json:"redPacketName"` // 红包名称 默认"现金红包" + Expand string `json:"expand"` // 拓展信息 + Remark string `json:"remark"` // 红包备注 默认“现金红包” + SendName string `validate:"required" json:"sendName"` // 发送名称 微信使用默认与redPacketName相同 + + Sign string `json:"sign"` // MD5签名 +} + +type OrderResp struct { + Code any `json:"code"` + Msg string `json:"msg"` +} + +func (o *OrderResp) IsSuccess() bool { + strCode := fmt.Sprintf("%v", o.Code) + return strCode == "000000" +} + +func (o *OrderResp) GetMsg() string { + if o.IsSuccess() { + return o.Msg + } + return fmt.Sprintf("code:[%v],msg:[%s]", o.Code, o.Msg) +} + +func (o *OrderResp) GetOrderStatus() proto.Status { + if o.IsSuccess() { + return proto.Status_ING + } + return proto.Status_FAIL +} diff --git a/plugins/qixing_alipay_redpack/internal/po/po.go b/plugins/qixing_alipay_redpack/internal/po/po.go new file mode 100644 index 0000000..6384c17 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/po/po.go @@ -0,0 +1,60 @@ +package po + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" +) + +type Req interface { + Validate() error + ToJson() ([]byte, error) + SetSign(sign string) +} + +var _ Req = (*OrderReq)(nil) +var _ Req = (*QueryReq)(nil) + +func (req *OrderReq) Validate() error { + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + return nil +} + +func (req *OrderReq) ToJson() ([]byte, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + return b, nil +} + +func (req *OrderReq) SetSign(sign string) { + req.Sign = sign +} + +func (req *QueryReq) Validate() error { + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + return nil +} + +func (req *QueryReq) ToJson() ([]byte, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + return b, nil +} + +func (req *QueryReq) SetSign(sign string) { + +} diff --git a/plugins/qixing_alipay_redpack/internal/po/query.go b/plugins/qixing_alipay_redpack/internal/po/query.go new file mode 100644 index 0000000..757a3d5 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/po/query.go @@ -0,0 +1,48 @@ +package po + +import ( + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "plugins/qixing_alipay_redpack/internal/vo" +) + +type QueryReq struct { + BizNo string `json:"bizNo" validate:"required"` + BatchId string `json:"batchId" validate:"required"` +} + +type Data struct { + BatchId string `json:"batchId"` + WxAppId string `json:"wxAppId"` + OpenId string `json:"openId"` + Name string `json:"name"` + Phone string `json:"phone"` + Amount float32 `json:"amount"` + BizNo string `json:"bizNo"` + Status string `json:"status"` + FailMsg string `json:"failMsg"` + Expand string `json:"expand"` + MchId string `json:"mchId"` + SendName string `json:"sendName"` + Remark string `json:"remark"` +} + +type QueryResp struct { + Code any `json:"code"` + Msg string `json:"msg"` + Data *Data `json:"data"` +} + +func (o *QueryResp) IsSuccess() bool { + strCode := fmt.Sprintf("%v", o.Code) + return strCode == "000000" +} + +func (o *QueryResp) GetMsg() string { + code := vo.Code(fmt.Sprintf("%v", o.Code)) + return fmt.Sprintf("code[%s],msg[%s],text[%s]", code, o.Msg, code.GetText()) +} + +func (o *QueryResp) GetOrderStatus() proto.Status { + return vo.Status(o.Data.Status).GetOrderStatus() +} diff --git a/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack.go b/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack.go new file mode 100644 index 0000000..5bf37e3 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack.go @@ -0,0 +1,135 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "plugins/qixing_alipay_redpack/internal/po" +) + +// 插件通信信息,若不对应则会报错panic +const ( + Tag = "qixing_alipay_redpack" + Version = 1 + CookieKey = "qixing_alipay_redpack" + CookieValue = "qixing_alipay_redpack" +) + +const ( + orderMethod = "/send" + queryMethod = "/received" +) + +type RedPackService struct{} + +func (s *RedPackService) 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) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + poReq, err := c.orderReq(request.Order, request.Product) + if err != nil { + return nil, proto.ErrorParamFail(err.Error()) + } + + reqBody, err := req(c, poReq) + if err != nil { + return nil, err + } + + url := c.BasUrl + orderMethod + bodyBytes, _, err := Post(ctx, url, reqBody) + if err != nil { + return nil, err + } + + var response *po.OrderResp + if err = json.Unmarshal(bodyBytes, &response); err != nil { + return nil, proto.ErrorResponseFail(err.Error()) + } + + return orderResp(request, response, bodyBytes), nil +} + +func (s *RedPackService) 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) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + reqBody, err := getReq(c.queryReq(request.Order)) + if err != nil { + return nil, err + } + + url := c.BasUrl + queryMethod + bodyBytes, _, err := Get(ctx, url, reqBody) + if err != nil { + return nil, err + } + + var response *po.QueryResp + if err = json.Unmarshal(bodyBytes, &response); err != nil { + return nil, proto.ErrorResponseFail(err.Error()) + } + + if !response.IsSuccess() { + return nil, proto.ErrorRequestFail(response.GetMsg()) + } + + return queryResp(request, response, bodyBytes), nil +} + +func (s *RedPackService) Balance(ctx context.Context, request *proto.QueryRequest) (resp []byte, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + url := c.BasUrl + "/config?batchId=" + c.BatchId + bodyBytes, _, err := Get(ctx, url, nil) + if err != nil { + return nil, err + } + + return bodyBytes, nil +} + +func (s *RedPackService) Notify(_ context.Context, request *proto.NotifyRequest) (resp2 *proto.NotifyResponse, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + n, err := notifyReq(request) + if err != nil { + return nil, err + } + + return notifyResp(n, request.Body) +} diff --git a/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack_test.go b/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack_test.go new file mode 100644 index 0000000..6dd9a95 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/qixing_alipay_redpack_test.go @@ -0,0 +1,126 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/stretchr/testify/assert" + "testing" +) + +var server = &RedPackService{} + +// config +// https://ceshi.myviding.com/cdc-api/api/external/red-packet 测试 +// https://cdc.myviding.com/cdc-api/api/external/red-packet 生产 +func config() []byte { + c := &Config{ + NchName: "启星支付宝红包", + BasUrl: "https://cdc.myviding.com/cdc-api/api/external/red-packet", + AppKey: "ZU2Y2HHQK00N5TP4", + BatchId: "20964224", + NotifyUrl: "https://gateway.dev.cdlsxd.cn/yxh5api/v1/supplier/notify/52", + } + 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 Test_OrderAlipay(t *testing.T) { + request := &proto.OrderRequest{ + Config: config(), + Order: &proto.OrderRequest_Order{ + OrderNo: "lsxd20250929001", + Account: "18666173766", + Quantity: 1, + Amount: 0.01, + Extra: []byte(`{"name":"李子铭"}`), + }, + Product: &proto.OrderRequest_Product{ + ProductNo: "", + Price: 0.01, + Extra: []byte(`{"wishing":"支付宝测试红包"}`), + }, + } + t.Run("Test_OrderAlipay", func(t *testing.T) { + got, err := server.Order(context.Background(), request) + if err != nil { + t.Errorf("Order() error = %v", err) + return + } + fmt.Printf("%s", got.String()) + assert.Equal(t, int(proto.Status_ING), int(got.Result.Status)) + }) +} + +func Test_Query(t *testing.T) { + request := &proto.QueryRequest{ + Config: config(), + Order: &proto.QueryRequest_Order{ + OrderNo: "lsxd202306071545141532", + TradeNo: "", + Account: "", + Extra: []byte(``), + }, + } + t.Run("Test_Query", 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 Test_Balance(t *testing.T) { + request := &proto.QueryRequest{ + Config: config(), + Order: &proto.QueryRequest_Order{ + OrderNo: "lsxd202306071545141532", + TradeNo: "", + Account: "", + Extra: []byte(`{"product_no":"20976200"}`), + }, + } + t.Run("Test_Balance", func(t *testing.T) { + got, err := server.Balance(context.Background(), request) + if err != nil { + t.Errorf("Query() error = %v", err) + return + } + fmt.Printf("%s \n", string(got)) + }) +} + +func Test_Notify(t *testing.T) { + in := &proto.NotifyRequest{ + Config: config(), + Queries: []byte(``), + Headers: []byte(``), + Body: []byte(`{ + "batchId": "20300480", + "bizNo": "订单123456", + "type": "RECEIVED", + "failMsg": "失败原因" +}`), + } + t.Run("TestNotify", func(t *testing.T) { + got, err := server.Notify(context.Background(), in) + if err != nil { + t.Errorf("Notify() error = %v", err) + return + } + fmt.Printf("TestNotify : %+v \n", got) + assert.Equal(t, int(proto.Status_SUCCESS), int(got.Result.Status)) + }) +} diff --git a/plugins/qixing_alipay_redpack/internal/transform.go b/plugins/qixing_alipay_redpack/internal/transform.go new file mode 100644 index 0000000..6d885d5 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/transform.go @@ -0,0 +1,149 @@ +package internal + +import ( + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/go-playground/validator/v10" + "net/http" + "plugins/qixing_alipay_redpack/internal/po" + "plugins/utils/helper" +) + +type Config struct { + NchName string `validate:"required" json:"mch_name"` // 商户名称 + BasUrl string `validate:"required" json:"base_url"` // 请求地址 + AppKey string `validate:"required" json:"app_key"` // appKey密钥 + BatchId string `validate:"required" json:"batch_id"` // 批次账号 + NotifyUrl string `validate:"required" json:"notify_url"` // 回调地址 +} + +func (c *Config) validate() error { + + err := validator.New().Struct(c) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return proto.ErrorConfigFail(fmt.Sprintf("配置参数有误:%s", err.Error())) + } + } + + return nil +} + +func transConfig(config []byte) (*Config, error) { + + var c Config + + if err := json.Unmarshal(config, &c); err != nil { + return nil, proto.ErrorConfigFail(fmt.Sprintf("配置参数解析失败: %v", err)) + } + + if err := c.validate(); err != nil { + return nil, err + } + + return &c, nil +} + +func (c *Config) orderReq(order *proto.OrderRequest_Order, product *proto.OrderRequest_Product) (*po.OrderReq, error) { + + //var productExtra struct { + // Wishing string `json:"wishing"` + //} + //if err := json.Unmarshal(product.Extra, &productExtra); err != nil { + // return nil, proto.ErrorParamFail(fmt.Sprintf("启星product拓展参数解析失败: %v", err)) + //} + + if !helper.IsPhoneNumber(order.Account) && !helper.IsEmail(order.Account) { + return nil, proto.ErrorParamFail("启星支付宝红包只支持账号领取") + } + + var orderExtra struct { + Name string `json:"name"` + } + if err := json.Unmarshal(order.Extra, &orderExtra); err != nil { + return nil, proto.ErrorConfigFail(fmt.Sprintf("支付宝红包order拓展参数解析失败: %v", err)) + } + + return &po.OrderReq{ + Amount: order.Amount, + BatchId: c.BatchId, + BizNo: order.OrderNo, + + Name: orderExtra.Name, + Phone: order.Account, + + NotifyUrl: c.NotifyUrl, + + Sign: "", + }, nil +} + +func orderResp(request *proto.OrderRequest, resp *po.OrderResp, bodyBytes []byte) *proto.OrderResponse { + return &proto.OrderResponse{ + Result: &proto.Result{ + Status: resp.GetOrderStatus(), + OrderNo: request.Order.OrderNo, + TradeNo: "", + Message: resp.GetMsg(), + Data: bodyBytes, + }, + } +} + +func (c *Config) queryReq(in *proto.QueryRequest_Order) *po.QueryReq { + + return &po.QueryReq{ + BizNo: in.OrderNo, + BatchId: c.BatchId, + } +} + +func queryResp(request *proto.QueryRequest, resp *po.QueryResp, bodyBytes []byte) *proto.QueryResponse { + + return &proto.QueryResponse{ + Result: &proto.Result{ + OrderNo: request.Order.OrderNo, + TradeNo: "", + Status: resp.GetOrderStatus(), + Message: resp.Msg, + Data: bodyBytes, + }, + } +} + +func notifyReq(in *proto.NotifyRequest) (*po.Notify, error) { + + var n *po.Notify + if err := json.Unmarshal(in.Body, &n); err != nil { + return nil, proto.ErrorParamFail(fmt.Sprintf("notify拓展参数解析失败: %v", err)) + } + return n, nil +} + +func notifyResp(n *po.Notify, data []byte) (*proto.NotifyResponse, error) { + + pb := &proto.NotifyResponse{ + Result: &proto.Result{ + Status: n.Type.GetOrderType(), + OrderNo: n.BizNo, + TradeNo: "", + Message: n.FailMsg, + Data: data, + Extra: nil, + }, + Return: `{"code":"success"}`, + } + + headers := make(http.Header) + headers.Set("Content-Type", "application/json") + + headersBytes, err := json.Marshal(headers) + if err != nil { + return nil, err + } + + pb.Headers = string(headersBytes) + + return pb, nil +} diff --git a/plugins/qixing_alipay_redpack/internal/util.go b/plugins/qixing_alipay_redpack/internal/util.go new file mode 100644 index 0000000..3ff8f7b --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/util.go @@ -0,0 +1,119 @@ +package internal + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "gitea.cdlsxd.cn/sdk/plugin/utils" + "net/http" + "net/url" + "plugins/qixing_alipay_redpack/internal/po" + "plugins/utils/request" + "strings" +) + +func req(config *Config, req po.Req) ([]byte, error) { + + if err := req.Validate(); err != nil { + return nil, err + } + + var strToBeSigned strings.Builder + + kvRows := utils.SortStructJsonTag(req) + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) + } + s := strToBeSigned.String() + config.AppKey + + req.SetSign(MD5(s)) + + return req.ToJson() +} + +func getReq(req po.Req) (url.Values, error) { + + if err := req.Validate(); err != nil { + return nil, err + } + + //var strToBeSigned strings.Builder + uv := url.Values{} + kvRows := utils.SortStructJsonTag(req) + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + uv.Set(kv.Key, kv.Value) + //strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) + } + //s := strToBeSigned.String() + config.AppKey + //uv.Set("sign", MD5(s)) + + return uv, nil +} + +func Post(ctx context.Context, url string, reqBody []byte) ([]byte, http.Header, error) { + + //fmt.Printf("url: %s\n", url) + //fmt.Printf("reqBody: %s\n", reqBody) + + h := http.Header{ + "Content-Type": []string{"application/json"}, + } + + respHeader, respBody, err := request.Post(ctx, url, reqBody, request.WithHeaders(h)) + if err != nil { + return nil, nil, proto.ErrorRequestFail(err.Error()) + } + + return respBody, respHeader, nil +} + +func Get(ctx context.Context, url string, uv url.Values) ([]byte, http.Header, error) { + + requestUrl := url + if uv != nil { + requestUrl = url + "?" + uv.Encode() + } + + h := http.Header{ + "Content-Type": []string{"application/x-www-form-urlencoded"}, + } + respHeader, respBody, err := request.Get(ctx, requestUrl, request.WithHeaders(h)) + if err != nil { + return nil, nil, proto.ErrorRequestFail(err.Error()) + } + + return respBody, respHeader, nil +} + +func MD5(data string) string { + // 创建一个 MD5 哈希对象 + hash := md5.New() + // 写入待加密的数据 + hash.Write([]byte(data)) + // 获取 MD5 哈希值 + hashBytes := hash.Sum(nil) + // 将 MD5 哈希值转换为16进制字符串 + return hex.EncodeToString(hashBytes) +} + +func Verify(n *po.Notify, appKey string) (bool, error) { + var strToBeSigned strings.Builder + uv := url.Values{} + kvRows := utils.SortStructJsonTag(n) + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + uv.Set(kv.Key, kv.Value) + strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) + } + return false, nil +} diff --git a/plugins/qixing_alipay_redpack/internal/vo/code.go b/plugins/qixing_alipay_redpack/internal/vo/code.go new file mode 100644 index 0000000..4642512 --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/vo/code.go @@ -0,0 +1,23 @@ +package vo + +type Code string + +var CodeTextMap = map[Code]string{ + "000000": "成功", + "100000": "内部错误(联系技术支持)", + "100001": "验签错误(检查签名算法)", + "100010": "活动批次不存在(检查batchId)", + "100011": "活动预算不足(联系客服增加预算)", + "100012": "业务编号已存在(更换bizNo)", + "100014": "缺少请求参数(检查必填字段)", + "100015": "发放错误(检查金额/账户信息)", + "100088": "接口请求频繁(重新调用接口)", +} + +func (o Code) GetText() string { + msg, ok := CodeTextMap[o] + if !ok { + return "" + } + return msg +} diff --git a/plugins/qixing_alipay_redpack/internal/vo/status.go b/plugins/qixing_alipay_redpack/internal/vo/status.go new file mode 100644 index 0000000..5589abc --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/vo/status.go @@ -0,0 +1,44 @@ +package vo + +import "gitea.cdlsxd.cn/sdk/plugin/proto" + +type Status string + +const ( + StatusUnclaimed = "0" + StatusReceived = "1" + StatusExpired = "2" + StatusFailed = "3" +) + +var StatusTextMap = map[Status]string{ + StatusUnclaimed: "未领取", + StatusReceived: "已领取", + StatusExpired: "已过期", + StatusFailed: "发放失败", +} + +var StatusMap = map[Status]proto.Status{ + StatusUnclaimed: proto.Status_ING, + StatusReceived: proto.Status_SUCCESS, + StatusExpired: proto.Status_FAIL, + StatusFailed: proto.Status_FAIL, +} + +func (o Status) GetText() string { + msg, ok := StatusTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o Status) GetOrderStatus() proto.Status { + if o == "" { + return proto.Status_INVALID + } + if resultStatus, ok := StatusMap[o]; ok { + return resultStatus + } + return proto.Status_INVALID +} diff --git a/plugins/qixing_alipay_redpack/internal/vo/type.go b/plugins/qixing_alipay_redpack/internal/vo/type.go new file mode 100644 index 0000000..898d50b --- /dev/null +++ b/plugins/qixing_alipay_redpack/internal/vo/type.go @@ -0,0 +1,41 @@ +package vo + +import "gitea.cdlsxd.cn/sdk/plugin/proto" + +type Type string + +const ( + TypeReceived = "RECEIVED" + TypeRefund = "REFUND" + TypeFailed = "FAILED" +) + +var TypeTextMap = map[Type]string{ + TypeReceived: "领取成功", + TypeRefund: "已过期退回", + TypeFailed: "领取失败", +} + +var TypeMap = map[Type]proto.Status{ + TypeReceived: proto.Status_SUCCESS, + TypeRefund: proto.Status_FAIL, + TypeFailed: proto.Status_FAIL, +} + +func (o Type) GetText() string { + msg, ok := TypeTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o Type) GetOrderType() proto.Status { + if o == "" { + return proto.Status_INVALID + } + if resultType, ok := TypeMap[o]; ok { + return resultType + } + return proto.Status_INVALID +} diff --git a/plugins/qixing_alipay_redpack/main.go b/plugins/qixing_alipay_redpack/main.go new file mode 100644 index 0000000..75a7999 --- /dev/null +++ b/plugins/qixing_alipay_redpack/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "gitea.cdlsxd.cn/sdk/plugin/shared" + "github.com/hashicorp/go-plugin" + "plugins/qixing_alipay_redpack/internal" +) + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: shared.HandshakeConfig(internal.Version, internal.CookieKey, internal.CookieValue), + Plugins: shared.PluginSet(shared.NewPlugin(&internal.RedPackService{}, internal.Tag)), + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/qixing_wechat_redpack/go.mod b/plugins/qixing_wechat_redpack/go.mod new file mode 100644 index 0000000..e8d4f1a --- /dev/null +++ b/plugins/qixing_wechat_redpack/go.mod @@ -0,0 +1,39 @@ +module plugins/qixing_wechat_redpack + +go 1.22.2 + +replace plugins/utils => ../../utils + +require ( + gitea.cdlsxd.cn/sdk/plugin v1.0.19 + github.com/go-playground/validator/v10 v10.22.0 + github.com/hashicorp/go-plugin v1.6.1 + github.com/stretchr/testify v1.9.0 + plugins/utils v0.0.0-00010101000000-000000000000 +) + +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/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.13 // indirect + github.com/mattn/go-isatty v0.0.20 // 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.26.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/plugins/qixing_wechat_redpack/go.sum b/plugins/qixing_wechat_redpack/go.sum new file mode 100644 index 0000000..3abbe24 --- /dev/null +++ b/plugins/qixing_wechat_redpack/go.sum @@ -0,0 +1,82 @@ +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= +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/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/qixing_wechat_redpack/internal/po/notify.go b/plugins/qixing_wechat_redpack/internal/po/notify.go new file mode 100644 index 0000000..c4e6a9e --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/po/notify.go @@ -0,0 +1,25 @@ +package po + +import ( + "fmt" + "github.com/go-playground/validator/v10" + "plugins/qixing_wechat_redpack/internal/vo" +) + +type Notify struct { + BatchId string `json:"batchId" validate:"required"` + BizNo string `json:"bizNo" validate:"required"` + Type vo.Type `json:"type" validate:"required"` + Sign string `json:"sign" validate:"required"` + FailMsg string `json:"failMsg"` +} + +func (req *Notify) Validate() error { + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + return nil +} diff --git a/plugins/qixing_wechat_redpack/internal/po/order.go b/plugins/qixing_wechat_redpack/internal/po/order.go new file mode 100644 index 0000000..3a8b4cc --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/po/order.go @@ -0,0 +1,53 @@ +package po + +import ( + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" +) + +type OrderReq struct { + Amount float32 `validate:"required" json:"amount"` // 微信:0.3-200;支付宝:<400单位:元 + BatchId string `validate:"required" json:"batchId"` // 批次ID + BizNo string `validate:"required" json:"bizNo"` // 业务单号,需唯一 + + //Name string `json:"name"` // 支付宝必填 + //Phone string `json:"phone"` // 支付宝必填 + + WxAppId string `validate:"required" json:"wxAppId"` // 公众号ID 微信必填 + OpenId string `validate:"required" json:"openId"` // 微信openId 微信必填 + Wishing string `validate:"required" json:"wishing"` // 红包祝福语,微信使用 + + RedPacketName string `json:"redPacketName"` // 红包名称 默认"现金红包" + Remark string `json:"remark"` // 红包备注 默认“现金红包”-- 微信红包微信后台能导出每日的对账单里面有remark不给用户展示 + SendName string `json:"sendName"` // 发送名称 微信使用默认与redPacketName相同 + + NotifyUrl string `validate:"required" json:"notifyUrl"` + + //Expand string `json:"expand"` // 拓展信息 + Sign string `json:"sign"` // MD5签名 +} + +type OrderResp struct { + Code any `json:"code"` + Msg string `json:"msg"` +} + +func (o *OrderResp) IsSuccess() bool { + return fmt.Sprintf("%v", o.Code) == "000000" +} + +func (o *OrderResp) GetMsg() string { + + if o.IsSuccess() { + return o.Msg + } + return fmt.Sprintf("code:[%v],msg:[%s]", o.Code, o.Msg) +} + +func (o *OrderResp) GetOrderStatus() proto.Status { + + if o.IsSuccess() { + return proto.Status_ING + } + return proto.Status_FAIL +} diff --git a/plugins/qixing_wechat_redpack/internal/po/po.go b/plugins/qixing_wechat_redpack/internal/po/po.go new file mode 100644 index 0000000..6384c17 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/po/po.go @@ -0,0 +1,60 @@ +package po + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" +) + +type Req interface { + Validate() error + ToJson() ([]byte, error) + SetSign(sign string) +} + +var _ Req = (*OrderReq)(nil) +var _ Req = (*QueryReq)(nil) + +func (req *OrderReq) Validate() error { + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + return nil +} + +func (req *OrderReq) ToJson() ([]byte, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + return b, nil +} + +func (req *OrderReq) SetSign(sign string) { + req.Sign = sign +} + +func (req *QueryReq) Validate() error { + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + return nil +} + +func (req *QueryReq) ToJson() ([]byte, error) { + b, err := json.Marshal(req) + if err != nil { + return nil, err + } + return b, nil +} + +func (req *QueryReq) SetSign(sign string) { + +} diff --git a/plugins/qixing_wechat_redpack/internal/po/query.go b/plugins/qixing_wechat_redpack/internal/po/query.go new file mode 100644 index 0000000..12a94bd --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/po/query.go @@ -0,0 +1,48 @@ +package po + +import ( + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "plugins/qixing_wechat_redpack/internal/vo" +) + +type QueryReq struct { + BizNo string `json:"bizNo" validate:"required"` + BatchId string `json:"batchId" validate:"required"` +} + +type Data struct { + BatchId string `json:"batchId"` + WxAppId string `json:"wxAppId"` + OpenId string `json:"openId"` + Name string `json:"name"` + Phone string `json:"phone"` + Amount float32 `json:"amount"` + BizNo string `json:"bizNo"` + Status string `json:"status"` + FailMsg string `json:"failMsg"` + Expand string `json:"expand"` + MchId string `json:"mchId"` + SendName string `json:"sendName"` + Remark string `json:"remark"` +} + +type QueryResp struct { + Code any `json:"code"` + Msg string `json:"msg"` + Data *Data `json:"data"` +} + +func (o *QueryResp) IsSuccess() bool { + strCode := fmt.Sprintf("%v", o.Code) + return strCode == "000000" +} + +func (o *QueryResp) GetMsg() string { + code := vo.Code(fmt.Sprintf("%v", o.Code)) + return fmt.Sprintf("code[%s],msg[%s],text[%s]", code, o.Msg, code.GetText()) +} + +func (o *QueryResp) GetOrderStatus() proto.Status { + return vo.Status(o.Data.Status).GetOrderStatus() +} diff --git a/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack.go b/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack.go new file mode 100644 index 0000000..50420d8 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack.go @@ -0,0 +1,148 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "plugins/qixing_wechat_redpack/internal/po" +) + +// 插件通信信息,若不对应则会报错panic +const ( + Tag = "qixing_wechat_redpack" + Version = 1 + CookieKey = "qixing_wechat_redpack" + CookieValue = "qixing_wechat_redpack" +) + +const ( + orderMethod = "/send" + queryMethod = "/received" +) + +type QiXingWechatRedPackService struct{} + +func (s *QiXingWechatRedPackService) 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) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + poReq, err := c.orderReq(request.Order, request.Product) + if err != nil { + return nil, proto.ErrorParamFail(err.Error()) + } + + reqBody, err := req(c, poReq) + if err != nil { + return nil, err + } + + url := c.BasUrl + orderMethod + bodyBytes, _, err := Post(ctx, url, reqBody) + if err != nil { + return nil, err + } + + var response *po.OrderResp + if err = json.Unmarshal(bodyBytes, &response); err != nil { + return nil, proto.ErrorResponseFail(err.Error()) + } + + return orderResp(request, response, bodyBytes), nil +} + +func (s *QiXingWechatRedPackService) 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) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + reqBody, err := getReq(c.queryReq(request.Order)) + if err != nil { + return nil, err + } + + url := c.BasUrl + queryMethod + bodyBytes, _, err := Get(ctx, url, reqBody) + if err != nil { + return nil, err + } + + var response *po.QueryResp + if err = json.Unmarshal(bodyBytes, &response); err != nil { + return nil, proto.ErrorResponseFail(err.Error()) + } + + if !response.IsSuccess() { + return nil, proto.ErrorRequestFail(response.GetMsg()) + } + + return queryResp(request, response, bodyBytes), nil +} + +func (s *QiXingWechatRedPackService) Balance(ctx context.Context, request *proto.QueryRequest) (resp []byte, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + url := c.BasUrl + "/config?batchId=" + c.BatchId + bodyBytes, _, err := Get(ctx, url, nil) + if err != nil { + return nil, err + } + + return bodyBytes, nil +} + +func (s *QiXingWechatRedPackService) Notify(_ context.Context, request *proto.NotifyRequest) (resp2 *proto.NotifyResponse, respErr error) { + + defer func() { + if err := recover(); err != nil { + respErr = fmt.Errorf("panic: %v", err) + } + }() + + if request.Body == nil { + return nil, proto.ErrorParamFail("body is nil") + } + + n, err := notifyReq(request) + if err != nil { + return nil, err + } + + c, err := transConfig(request.Config) + if err != nil { + return nil, err + } + + if !Verify(n, c.AppKey) { + return nil, proto.ErrorSignFail("签名验证失败") + } + + return notifyResp(n, request.Body) +} diff --git a/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack_test.go b/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack_test.go new file mode 100644 index 0000000..4cbe8d2 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/qixing_wechat_redpack_test.go @@ -0,0 +1,131 @@ +package internal + +import ( + "context" + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/stretchr/testify/assert" + "testing" +) + +var server = &QiXingWechatRedPackService{} + +// config +// https://ceshi.myviding.com/cdc-api/api/external/red-packet 测试 +// https://cdc.myviding.com/cdc-api/api/external/red-packet 生产 +func config() []byte { + c := &Config{ + Mode: "wechat_redpack_mode", + NchName: "启星微信红包", + AppKey: "ZU2Y2HHQK00N5TP4", + BatchId: "20062389", + BasUrl: "https://cdc.myviding.com/cdc-api/api/external/red-packet", + NotifyUrl: "https://gateway.dev.cdlsxd.cn/yxh5api/v1/supplier/notify/51", + Official: Official{ + Name: "福建兴旺网络科技有限公司", + AppId: "wxe3bd59243545fa8a", + AppSecret: "4c9649cb998f71038e187b4c58f5fda0", + OauthKey: "bcee0c6753b2a31c792a91fe9f9f1666", + OauthUrl: "https://utils.85938.cn/utils/v1/wechat/oauth/fjxw", + }, + } + marshal, _ := json.Marshal(c) + return marshal +} + +func Test_Config(t *testing.T) { + t.Run("TestConfig", func(t *testing.T) { + c := config() + fmt.Printf("%s\n", string(c)) + assert.NotEmpty(t, c) + }) +} + +func Test_Order(t *testing.T) { + request := &proto.OrderRequest{ + Config: config(), + Order: &proto.OrderRequest_Order{ + OrderNo: "lsxd20250929004", + Account: "ojbqr6HpeWKFy9Sgdx8yCmmeVJiw", + Quantity: 1, + Amount: 0.3, + Extra: []byte(``), + }, + Product: &proto.OrderRequest_Product{ + ProductNo: "", + Price: 0.3, + Type: 0, + Extra: []byte(`{"batch_name":"batch name恭喜发财,红包拿来,红包祝福语", "batch_remark":"batch remark 恭喜发财,红包拿来,红包备注", "goods_name":"红包名称xx红包"}`), + }, + } + t.Run("Test_Order", func(t *testing.T) { + got, err := server.Order(context.Background(), request) + if err != nil { + t.Errorf("Order() error = %v", err) + return + } + fmt.Printf("响应报文=%s\n", got.Result.Data) + fmt.Printf("%s", got.String()) + assert.Equal(t, int(proto.Status_ING), int(got.Result.Status)) + }) +} + +func Test_Query(t *testing.T) { + request := &proto.QueryRequest{ + Config: config(), + Order: &proto.QueryRequest_Order{ + OrderNo: "lsxd20250929003", + TradeNo: "", + Account: "", + Extra: []byte(``), + }, + } + t.Run("Test_Query", 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 Test_Balance(t *testing.T) { + request := &proto.QueryRequest{ + Config: config(), + Order: &proto.QueryRequest_Order{ + OrderNo: "lsxd202306071545141532", + TradeNo: "", + Account: "", + Extra: []byte(``), + }, + } + t.Run("Test_Balance", func(t *testing.T) { + got, err := server.Balance(context.Background(), request) + if err != nil { + t.Errorf("Query() error = %v", err) + return + } + fmt.Printf("%s \n", string(got)) + }) +} + +func TestNotify(t *testing.T) { + in := &proto.NotifyRequest{ + Config: config(), + Queries: []byte(``), + Headers: []byte(``), + Body: []byte(`{"bizNo":"lsxd20250929003","sign":"e71e685d8d9544aad0f32e699fd98e06","batchId":"20062389","type":"SENT"}`), + } + t.Run("TestNotify", func(t *testing.T) { + got, err := server.Notify(context.Background(), in) + if err != nil { + t.Errorf("Notify() error = %v", err) + return + } + fmt.Printf("TestNotify : %+v \n", got) + assert.Equal(t, int(proto.Status_SUCCESS), int(got.Result.Status)) + }) +} diff --git a/plugins/qixing_wechat_redpack/internal/transform.go b/plugins/qixing_wechat_redpack/internal/transform.go new file mode 100644 index 0000000..5002494 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/transform.go @@ -0,0 +1,168 @@ +package internal + +import ( + "encoding/json" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "github.com/go-playground/validator/v10" + "net/http" + "plugins/qixing_wechat_redpack/internal/po" + "plugins/qixing_wechat_redpack/internal/vo" +) + +type Official struct { + Name string `json:"name"` // 公众号名称 + AppId string `validate:"required" json:"app_id"` + AppSecret string `json:"app_secret"` + OauthKey string `json:"oauth_key"` + OauthUrl string `json:"oauth_url"` +} + +type Config struct { + Mode vo.Mode `validate:"required" json:"mode"` // 商户名称 + NchName string `validate:"required" json:"mch_name"` // 商户名称 + AppKey string `validate:"required" json:"app_key"` // appKey密钥 + BatchId string `validate:"required" json:"batch_id"` // 批次账号 + BasUrl string `validate:"required" json:"base_url"` // 请求地址 + NotifyUrl string `validate:"required" json:"notify_url"` // 回调地址 + Official Official `validate:"required" json:"official"` // 公众号信息 +} + +func (c *Config) validate() error { + + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return proto.ErrorConfigFail(fmt.Sprintf("配置参数有误:%s", err.Error())) + } + } + + if c.Mode.IsRedPack() { + return nil + } + + return proto.ErrorConfigFail("微信发放红包模式不正确") +} + +func transConfig(config []byte) (*Config, error) { + + var c Config + + if err := json.Unmarshal(config, &c); err != nil { + return nil, proto.ErrorConfigFail(fmt.Sprintf("配置参数解析失败: %v", err)) + } + + if err := c.validate(); err != nil { + return nil, proto.ErrorConfigFail(err.Error()) + } + + return &c, nil +} + +func (c *Config) orderReq(order *proto.OrderRequest_Order, product *proto.OrderRequest_Product) (*po.OrderReq, error) { + + var productExtra struct { + BatchName string `json:"batch_name"` + BatchRemark string `json:"batch_remark"` + GoodsName string `json:"goods_name"` + } + if err := json.Unmarshal(product.Extra, &productExtra); err != nil { + return nil, proto.ErrorParamFail(fmt.Sprintf("product拓展参数解析失败: %v", err)) + } + + return &po.OrderReq{ + Amount: order.Amount, + BatchId: c.BatchId, + BizNo: order.OrderNo, + + WxAppId: c.Official.AppId, + OpenId: order.Account, + + SendName: productExtra.GoodsName, + RedPacketName: productExtra.GoodsName, + Wishing: productExtra.BatchName, + Remark: productExtra.BatchRemark, + + NotifyUrl: c.NotifyUrl, + + Sign: "", + }, nil +} + +func orderResp(request *proto.OrderRequest, resp *po.OrderResp, bodyBytes []byte) *proto.OrderResponse { + + return &proto.OrderResponse{ + Result: &proto.Result{ + Status: resp.GetOrderStatus(), + OrderNo: request.Order.OrderNo, + TradeNo: "", + Message: resp.GetMsg(), + Data: bodyBytes, + }, + } +} + +func (c *Config) queryReq(in *proto.QueryRequest_Order) *po.QueryReq { + + return &po.QueryReq{ + BizNo: in.OrderNo, + BatchId: c.BatchId, + } +} + +func queryResp(request *proto.QueryRequest, resp *po.QueryResp, bodyBytes []byte) *proto.QueryResponse { + + return &proto.QueryResponse{ + Result: &proto.Result{ + OrderNo: request.Order.OrderNo, + TradeNo: "", + Status: resp.GetOrderStatus(), + Message: resp.Msg, + Data: bodyBytes, + }, + } +} + +func notifyReq(in *proto.NotifyRequest) (*po.Notify, error) { + + var n *po.Notify + if err := json.Unmarshal(in.Body, &n); err != nil { + return nil, proto.ErrorParamFail(fmt.Sprintf("notify拓展参数解析失败: %v", err)) + } + + if err := n.Validate(); err != nil { + return nil, proto.ErrorParamFail(fmt.Sprintf("notify参数有误:%s", err.Error())) + } + + return n, nil +} + +func notifyResp(n *po.Notify, data []byte) (*proto.NotifyResponse, error) { + + pb := &proto.NotifyResponse{ + Result: &proto.Result{ + Status: n.Type.GetOrderType(), + OrderNo: n.BizNo, + TradeNo: "", + Message: n.FailMsg, + Data: data, + Extra: nil, + }, + Return: `{"code":"success"}`, + } + + if pb.Result.Status == proto.Status_SUCCESS { + pb.Result.Message = `成功` + } + + headers := make(http.Header) + headers.Set("Content-Type", "application/json") + + headersBytes, err := json.Marshal(headers) + if err != nil { + return nil, err + } + + pb.Headers = string(headersBytes) + + return pb, nil +} diff --git a/plugins/qixing_wechat_redpack/internal/util.go b/plugins/qixing_wechat_redpack/internal/util.go new file mode 100644 index 0000000..6c72c12 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/util.go @@ -0,0 +1,118 @@ +package internal + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + "gitea.cdlsxd.cn/sdk/plugin/proto" + "gitea.cdlsxd.cn/sdk/plugin/utils" + "net/http" + "net/url" + "plugins/qixing_wechat_redpack/internal/po" + "plugins/utils/request" + "strings" +) + +func req(config *Config, req po.Req) ([]byte, error) { + + if err := req.Validate(); err != nil { + return nil, err + } + + var strToBeSigned strings.Builder + + kvRows := utils.SortStructJsonTag(req) + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) + } + s := strToBeSigned.String() + config.AppKey + + req.SetSign(MD5(s)) + + return req.ToJson() +} + +func getReq(req po.Req) (url.Values, error) { + + if err := req.Validate(); err != nil { + return nil, proto.ErrorParamFail(err.Error()) + } + + uv := url.Values{} + kvRows := utils.SortStructJsonTag(req) + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + uv.Set(kv.Key, kv.Value) + } + + return uv, nil +} + +func Post(ctx context.Context, url string, reqBody []byte) ([]byte, http.Header, error) { + + h := http.Header{ + "Content-Type": []string{"application/json"}, + } + + respHeader, respBody, err := request.Post(ctx, url, reqBody, request.WithHeaders(h)) + if err != nil { + return nil, nil, proto.ErrorRequestFail(err.Error()) + } + + return respBody, respHeader, nil +} + +func Get(ctx context.Context, url string, uv url.Values) ([]byte, http.Header, error) { + + requestUrl := url + if uv != nil { + requestUrl = url + "?" + uv.Encode() + } + + h := http.Header{ + "Content-Type": []string{"application/x-www-form-urlencoded"}, + } + respHeader, respBody, err := request.Get(ctx, requestUrl, request.WithHeaders(h)) + if err != nil { + return nil, nil, proto.ErrorRequestFail(err.Error()) + } + + return respBody, respHeader, nil +} + +func MD5(data string) string { + // 创建一个 MD5 哈希对象 + hash := md5.New() + // 写入待加密的数据 + hash.Write([]byte(data)) + // 获取 MD5 哈希值 + hashBytes := hash.Sum(nil) + // 将 MD5 哈希值转换为16进制字符串 + return hex.EncodeToString(hashBytes) +} + +func Verify(n *po.Notify, appKey string) bool { + + var strToBeSigned strings.Builder + uv := url.Values{} + + kvRows := utils.SortStructJsonTag(n) + + for _, kv := range kvRows { + if kv.Key == "sign" || kv.Value == "" { + continue + } + uv.Set(kv.Key, kv.Value) + strToBeSigned.WriteString(fmt.Sprintf("%s=%s&", kv.Key, kv.Value)) + } + + s := strToBeSigned.String() + appKey + + return MD5(s) == n.Sign +} diff --git a/plugins/qixing_wechat_redpack/internal/vo/code.go b/plugins/qixing_wechat_redpack/internal/vo/code.go new file mode 100644 index 0000000..4642512 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/vo/code.go @@ -0,0 +1,23 @@ +package vo + +type Code string + +var CodeTextMap = map[Code]string{ + "000000": "成功", + "100000": "内部错误(联系技术支持)", + "100001": "验签错误(检查签名算法)", + "100010": "活动批次不存在(检查batchId)", + "100011": "活动预算不足(联系客服增加预算)", + "100012": "业务编号已存在(更换bizNo)", + "100014": "缺少请求参数(检查必填字段)", + "100015": "发放错误(检查金额/账户信息)", + "100088": "接口请求频繁(重新调用接口)", +} + +func (o Code) GetText() string { + msg, ok := CodeTextMap[o] + if !ok { + return "" + } + return msg +} diff --git a/plugins/qixing_wechat_redpack/internal/vo/mode.go b/plugins/qixing_wechat_redpack/internal/vo/mode.go new file mode 100644 index 0000000..b9c6e60 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/vo/mode.go @@ -0,0 +1,29 @@ +package vo + +type Mode string + +const ( + ModeRedPack = "wechat_redpack_mode" + ModeTransfer = "wechat_transfer_mode" +) + +var ModeTextMap = map[Mode]string{ + ModeRedPack: "微信红包模式", + ModeTransfer: "微信转账模式", +} + +func (o Mode) GetText() string { + msg, ok := ModeTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o Mode) GetMode() string { + return string(o) +} + +func (o Mode) IsRedPack() bool { + return o == ModeRedPack +} diff --git a/plugins/qixing_wechat_redpack/internal/vo/status.go b/plugins/qixing_wechat_redpack/internal/vo/status.go new file mode 100644 index 0000000..11ff1ba --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/vo/status.go @@ -0,0 +1,47 @@ +package vo + +import "gitea.cdlsxd.cn/sdk/plugin/proto" + +type Status string + +const ( + StatusUnclaimed = "0" + StatusReceived = "1" + StatusExpired = "2" + StatusFailed = "3" +) + +var StatusTextMap = map[Status]string{ + StatusUnclaimed: "未领取", + StatusReceived: "已领取", + StatusExpired: "已过期", + StatusFailed: "发放失败", +} + +var StatusMap = map[Status]proto.Status{ + StatusUnclaimed: proto.Status_ING, + StatusReceived: proto.Status_SUCCESS, + StatusExpired: proto.Status_FAIL, + StatusFailed: proto.Status_FAIL, +} + +func (o Status) GetText() string { + msg, ok := StatusTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o Status) GetOrderStatus() proto.Status { + + if o == "" { + return proto.Status_INVALID + } + + if resultStatus, ok := StatusMap[o]; ok { + return resultStatus + } + + return proto.Status_INVALID +} diff --git a/plugins/qixing_wechat_redpack/internal/vo/type.go b/plugins/qixing_wechat_redpack/internal/vo/type.go new file mode 100644 index 0000000..d13dca0 --- /dev/null +++ b/plugins/qixing_wechat_redpack/internal/vo/type.go @@ -0,0 +1,47 @@ +package vo + +import "gitea.cdlsxd.cn/sdk/plugin/proto" + +type Type string + +const ( + TypeSent = "SENT" + TypeReceived = "RECEIVED" + TypeRefund = "REFUND" + TypeFailed = "FAILED" +) + +var TypeTextMap = map[Type]string{ + TypeSent: "下单成功,领取中", + TypeReceived: "领取成功", + TypeRefund: "已过期退回", + TypeFailed: "领取失败", +} + +var TypeMap = map[Type]proto.Status{ + TypeSent: proto.Status_ING, + TypeReceived: proto.Status_SUCCESS, + TypeRefund: proto.Status_FAIL, + TypeFailed: proto.Status_FAIL, +} + +func (o Type) GetText() string { + msg, ok := TypeTextMap[o] + if !ok { + return "" + } + return msg +} + +func (o Type) GetOrderType() proto.Status { + + if o == "" { + return proto.Status_INVALID + } + + if resultType, ok := TypeMap[o]; ok { + return resultType + } + + return proto.Status_INVALID +} diff --git a/plugins/qixing_wechat_redpack/main.go b/plugins/qixing_wechat_redpack/main.go new file mode 100644 index 0000000..9f645a4 --- /dev/null +++ b/plugins/qixing_wechat_redpack/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "gitea.cdlsxd.cn/sdk/plugin/shared" + "github.com/hashicorp/go-plugin" + "plugins/qixing_wechat_redpack/internal" +) + +func main() { + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: shared.HandshakeConfig(internal.Version, internal.CookieKey, internal.CookieValue), + Plugins: shared.PluginSet(shared.NewPlugin(&internal.QiXingWechatRedPackService{}, internal.Tag)), + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/base.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/base.go new file mode 100644 index 0000000..ce1fed3 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/base.go @@ -0,0 +1,14 @@ +package miniprogram + +import ( + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" +) + +const ( + _sendUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendminiprogramhb" + _queryUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo" +) + +type MiniProgram struct { + *redpack.RedPack +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/model.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/model.go new file mode 100644 index 0000000..82d009b --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/model.go @@ -0,0 +1,210 @@ +package miniprogram + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "github.com/go-playground/validator/v10" +) + +type Str interface { + Str() (string, error) +} + +type Req interface { + Str + Validate() error + ToXML() ([]byte, error) +} + +var _ Req = (*SendRequest)(nil) +var _ Str = (*SendResponse)(nil) + +var _ Req = (*QueryRequest)(nil) +var _ Str = (*QueryResponse)(nil) + +// SendRequest 红包发送请求结构体(兼容 JSON/XML 双格式,XML 无 CDATA 包裹) +type SendRequest struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点为 ,JSON 忽略该字段 + NonceStr string `xml:"nonce_str" json:"nonce_str" validate:"required"` // 随机字符串(无 CDATA) + Sign string `xml:"sign" json:"sign"` // 签名(无 CDATA) + MchBillno string `xml:"mch_billno" json:"mch_billno" validate:"required"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id" json:"mch_id" validate:"required"` // 商户号(无 CDATA) + Wxappid string `xml:"wxappid" json:"wxappid" validate:"required"` // 小程序 appid(无 CDATA) + SendName string `xml:"send_name,omitempty" json:"send_name,omitempty" validate:"required"` // 发送者名称(无 CDATA,空值忽略) + ReOpenid string `xml:"re_openid" json:"re_openid" validate:"required"` // 用户 openid(无 CDATA) + TotalAmount int32 `xml:"total_amount" json:"total_amount" validate:"required"` // 付款金额(分,数值类型) + TotalNum int32 `xml:"total_num" json:"total_num" validate:"required"` // 发放人数(数值类型) + Wishing string `xml:"wishing" json:"wishing" validate:"required"` // 祝福语(无 CDATA) + ActName string `xml:"act_name" json:"act_name" validate:"required"` // 活动名称(无 CDATA) + Remark string `xml:"remark" json:"remark" validate:"required"` // 备注(无 CDATA) + NotifyWay string `xml:"notify_way,omitempty" json:"notify_way"` // 通知方式(无 CDATA,空值忽略) + SceneId string `xml:"scene_id,omitempty" json:"scene_id,omitempty"` // 场景 ID(无 CDATA,空值忽略) +} + +// SendResponse 红包发送响应结构体(兼容 JSON/XML 双格式,XML 无 CDATA 包裹) +type SendResponse struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点 + ReturnCode string `xml:"return_code,omitempty" json:"return_code,omitempty"` // 返回状态码(无 CDATA) + ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg,omitempty"` // 返回信息(无 CDATA) + // 以下字段在return_code为SUCCESS的时候有返回 + ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"` // 业务结果(无 CDATA) + ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` // 错误码(无 CDATA) + ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` // 错误描述(无 CDATA,修正为 string) + // 以下字段在return_code和result_code都为SUCCESS的时候有返回 + MchBillno string `xml:"mch_billno,omitempty" json:"mch_billno,omitempty"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"` // 商户号(无 CDATA) + Wxappid string `xml:"wxappid,omitempty" json:"wxappid,omitempty"` // 小程序 appid(无 CDATA) + ReOpenid string `xml:"re_openid,omitempty" json:"re_openid,omitempty"` // 用户 openid(无 CDATA) + TotalAmount string `xml:"total_amount,omitempty" json:"total_amount,omitempty"` // 付款金额(无 CDATA) + SendListid string `xml:"send_listid,omitempty" json:"send_listid,omitempty"` // 红包列表 ID(无 CDATA) + Package string `xml:"package,omitempty" json:"package,omitempty"` // 小程序跳转参数(无 CDATA) +} + +func (req *SendRequest) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +func (req *SendRequest) Validate() error { + + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + + return nil +} + +// ToXML 将 SendRequest 序列化为无 CDATA 的 XML 字节流(带 XML 声明) +func (req *SendRequest) ToXML() ([]byte, error) { + // 生成格式化 XML + data, err := xml.MarshalIndent(req, "", " ") + if err != nil { + return nil, fmt.Errorf("xml marshal failed: %w", err) + } + // 拼接 XML 声明(指定 UTF-8 编码) + xmlData := []byte(xml.Header + string(data)) + return xmlData, nil +} + +func (req *SendResponse) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +// QueryRequest . +type QueryRequest struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点为 ,JSON 忽略该字段 + NonceStr string `xml:"nonce_str" json:"nonce_str" validate:"required"` // 随机字符串(无 CDATA) + MchBillno string `xml:"mch_billno" json:"mch_billno" validate:"required"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id" json:"mch_id" validate:"required"` // 商户号(无 CDATA) + Appid string `xml:"appid" json:"appid" validate:"required"` // 小程序 appid(无 CDATA) + BillType string `xml:"bill_type" json:"bill_type" validate:"required"` // 场景 ID(无 CDATA,空值忽略) MCHT:通过商户订单号获取红包信息。 + Sign string `xml:"sign" json:"sign"` // 签名(无 CDATA) +} + +// QueryResponse . +type QueryResponse struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点 + ReturnCode string `xml:"return_code" json:"return_code"` // 返回状态码(无 CDATA) + ReturnMsg string `xml:"return_msg" json:"return_msg"` // 返回信息(无 CDATA) + // 以下字段在return_code为SUCCESS的时候有返回 + ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"` // 业务结果(无 CDATA) + ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` // 错误码(无 CDATA) + ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` // 错误描述(无 CDATA,修正为 string) + // 以下字段在return_code 和result_code都为SUCCESS的时候有返回 + MchBillno string `xml:"mch_billno,omitempty" json:"mch_billno,omitempty"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"` // 商户号(无 CDATA) + DetailId string `xml:"detail_id,omitempty" json:"detail_id,omitempty"` // 使用API发放现金红包时返回的红包单号 + Status string `xml:"status,omitempty" json:"status,omitempty"` // 红包状态 + SendType string `xml:"send_type,omitempty" json:"send_type,omitempty"` // 发放类型 + TotalNum int `xml:"total_num,omitempty" json:"total_num,omitempty"` // 红包个数 + TotalAmount int `xml:"total_amount,omitempty" json:"total_amount,omitempty"` // 小程序跳转参数(无 CDATA) + Reason string `xml:"reason,omitempty" json:"reason,omitempty"` // 失败原因 + SendTime string `xml:"send_time,omitempty" json:"send_time,omitempty"` // 红包发送时间 + RefundTime string `xml:"refund_time,omitempty" json:"refund_time,omitempty"` // 红包退款时间 + RefundAmount string `xml:"refund_amount,omitempty" json:"refund_amount,omitempty"` // 红包退款金额 + Wishing string `xml:"wishing,omitempty" json:"wishing,omitempty"` // 祝福语 + Remark string `xml:"remark,omitempty" json:"remark,omitempty"` // 活动描述,低版本微信可见 + ActName string `xml:"act_name,omitempty" json:"act_name,omitempty"` // 活动名称 + Hblist []Hbinfo `xml:"hblist,omitempty" json:"hblist,omitempty"` // 裂变红包领取列表 + Openid string `xml:"openid,omitempty" json:"openid,omitempty"` // 领取红包的Openid + Amount int `xml:"amount,omitempty" json:"amount,omitempty"` // 金额 + RcvTime string `xml:"rcv_time,omitempty" json:"rcv_time,omitempty"` // 接收时间 +} + +type Hbinfo struct { + Openid string `xml:"openid" json:"openid"` + Amount int `xml:"amount" json:"amount"` + RcvTime string `xml:"rcv_time" json:"rcv_time"` +} + +func (req *QueryRequest) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +func (req *QueryRequest) Validate() error { + + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + + return nil +} + +// ToXML 将 SendRequest 序列化为无 CDATA 的 XML 字节流(带 XML 声明) +func (req *QueryRequest) ToXML() ([]byte, error) { + // 生成格式化 XML + data, err := xml.MarshalIndent(req, "", " ") + if err != nil { + return nil, fmt.Errorf("xml marshal failed: %w", err) + } + // 拼接 XML 声明(指定 UTF-8 编码) + xmlData := []byte(xml.Header + string(data)) + return xmlData, nil +} + +func (req *QueryResponse) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +type SendBizRedPacket struct { + TimeStamp string `json:"timeStamp" validate:"required"` // 调用方生成的时间戳,需为字符串 + NonceStr string `json:"nonceStr" validate:"required"` // 调用方生成的随机字符串 + Package string `json:"package" validate:"required"` // 商户将红包信息组成该串,具体方案参见package的说明,package需要进行urlencode再传给页面 + SignType string `json:"signType" validate:"required"` // 按照文档中所示填入,目前仅支持MD5 + PaySign string `json:"paySign" validate:"required"` // 签名 +} + +func (req *SendBizRedPacket) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query.go new file mode 100644 index 0000000..8b535ef --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query.go @@ -0,0 +1,77 @@ +package miniprogram + +import ( + "context" + "encoding/xml" + "fmt" +) + +// Query 查询红包记录 +// @link https://pay.weixin.qq.com/doc/v2/merchant/4011937431 +func (srv *MiniProgram) Query(ctx context.Context, req *QueryRequest) (response *SendResponse, respBody []byte, err error) { + + reqBody, err := srv.queryReqBody(req) + if err != nil { + return + } + + respBody, err = srv.Post(ctx, _queryUrl, reqBody) + if err != nil { + return nil, nil, err + } + + response, err = srv.ParseXMLQueryResponse(respBody) + + return +} + +func (srv *MiniProgram) queryReqBody(req *QueryRequest) (reqBody []byte, err error) { + + req.BillType = "MCHT" + + if err = req.Validate(); err != nil { + return + } + + req.Sign = srv.generateQueryReqBodySign(req) + + reqBody, err = req.ToXML() + if err != nil { + return + } + + return +} + +func (srv *MiniProgram) generateQueryReqBodySign(req *QueryRequest) string { + + params := make(map[string]string) + if req.NonceStr != "" { + params["nonce_str"] = req.NonceStr + } + if req.MchBillno != "" { + params["mch_billno"] = req.MchBillno + } + if req.MchId != "" { + params["mch_id"] = req.MchId + } + if req.Appid != "" { + params["appid"] = req.Appid + } + if req.BillType != "" { + params["bill_type"] = req.BillType + } + + return srv.Sign(params) +} + +func (srv *MiniProgram) ParseXMLQueryResponse(xmlData []byte) (*SendResponse, error) { + + var v *SendResponse + + if err := xml.Unmarshal(xmlData, &v); err != nil { + return nil, fmt.Errorf("xml unmarshal failed: %w", err) + } + + return v, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query_test.go new file mode 100644 index 0000000..4616bba --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/query_test.go @@ -0,0 +1,52 @@ +package miniprogram + +import ( + "context" + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" + "testing" +) + +func TestRedPack_Query(t *testing.T) { + type args struct { + ctx context.Context + req *QueryRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "小程序查询红包", + args: args{ + ctx: context.Background(), + req: &QueryRequest{ + NonceStr: "123456", + MchBillno: "1652322442TEST001", + MchId: "1652322442", + Appid: "wx075c784fe71d4d04", + BillType: "", + Sign: "", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + b, err := redpack.NewRedPack(cf) + if err != nil { + panic(err) + } + srv := MiniProgram{b} + + gotResponse, bodyBytes, err := srv.Query(tt.args.ctx, tt.args.req) + if err != nil { + t.Errorf("Send() error = %v", err) + return + } + t.Errorf("Send() gotResponse = %v, bodyBytes = %s", gotResponse, string(bodyBytes)) + }) + } +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send.go new file mode 100644 index 0000000..f88fea7 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send.go @@ -0,0 +1,103 @@ +package miniprogram + +import ( + "context" + "encoding/xml" + "fmt" + "strconv" +) + +// Send 发送红包 +// @link https://pay.weixin.qq.com/doc/v2/merchant/4011937425 +func (srv *MiniProgram) Send(ctx context.Context, req *SendRequest) (response *SendResponse, respBody []byte, err error) { + + reqBody, err := srv.sendReqBody(req) + if err != nil { + return + } + + respBody, err = srv.Post(ctx, _sendUrl, reqBody) + if err != nil { + return + } + + response, err = srv.ParseXMLSendResponse(respBody) + if err != nil { + return + } + + return +} + +func (srv *MiniProgram) sendReqBody(req *SendRequest) (reqBody []byte, err error) { + + if err = req.Validate(); err != nil { + return + } + + req.Sign = srv.generateSendReqBodySign(req) + + reqBody, err = req.ToXML() + if err != nil { + return + } + + return +} + +func (srv *MiniProgram) generateSendReqBodySign(req *SendRequest) string { + // 1. 构造参数字典 + params := make(map[string]string) + if req.NonceStr != "" { + params["nonce_str"] = req.NonceStr + } + if req.MchBillno != "" { + params["mch_billno"] = req.MchBillno + } + if req.MchId != "" { + params["mch_id"] = req.MchId + } + if req.Wxappid != "" { + params["wxappid"] = req.Wxappid + } + if req.SendName != "" { + params["send_name"] = req.SendName + } + if req.ReOpenid != "" { + params["re_openid"] = req.ReOpenid + } + if req.TotalAmount != 0 { + params["total_amount"] = strconv.FormatInt(int64(req.TotalAmount), 10) + } + if req.TotalNum != 0 { + params["total_num"] = strconv.FormatInt(int64(req.TotalNum), 10) + } + if req.Wishing != "" { + params["wishing"] = req.Wishing + } + if req.ActName != "" { + params["act_name"] = req.ActName + } + if req.Remark != "" { + params["remark"] = req.Remark + } + if req.NotifyWay != "" { + params["notify_way"] = req.NotifyWay + } + if req.SceneId != "" { + params["scene_id"] = req.SceneId + } + + return srv.Sign(params) +} + +func (srv *MiniProgram) ParseXMLSendResponse(xmlData []byte) (*SendResponse, error) { + + var v *SendResponse + + if err := xml.Unmarshal(xmlData, &v); err != nil { + return nil, fmt.Errorf("xml unmarshal failed: %w", err) + } + + return v, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_biz_rp.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_biz_rp.go new file mode 100644 index 0000000..1ca232c --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_biz_rp.go @@ -0,0 +1,27 @@ +package miniprogram + +import "context" + +func (srv *MiniProgram) SendBizRedPacketSign(ctx context.Context, req *SendBizRedPacket) { + + req.PaySign = srv.generateSendBizRedPacketSignSign(req) + return +} + +func (srv *MiniProgram) generateSendBizRedPacketSignSign(req *SendBizRedPacket) string { + + params := make(map[string]string) + + if req.TimeStamp != "" { + params["timeStamp"] = req.TimeStamp + } + if req.NonceStr != "" { + params["nonce_str"] = req.NonceStr + } + if req.Package != "" { + params["package"] = req.Package + } + params["appid"] = "MD5" + + return srv.Sign(params) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_test.go new file mode 100644 index 0000000..5cc0d04 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/miniprogram/send_test.go @@ -0,0 +1,112 @@ +package miniprogram + +import ( + "context" + "fmt" + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" + "testing" + "time" +) + +var cf = &redpack.Config{ + MchId: "1652322442", + ApiV2Key: "bcee0c6753b2a31c792a91fe9f9f1666", + CertFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_cert.pem", + KeyFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_key.pem", +} + +func TestRedPack_Send(t *testing.T) { + type args struct { + ctx context.Context + req *SendRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "小程序发红包", + args: args{ + ctx: context.Background(), + req: &SendRequest{ + NonceStr: "wxe3bd59243545fa8a", + Sign: "", + MchBillno: "1652322442TEST001", + MchId: "1652322442", + Wxappid: "wx075c784fe71d4d04", + SendName: "福建兴旺", + ReOpenid: "odX1x1za5bIooAz7gG9Jx3lHSHxk", + TotalAmount: 10, + TotalNum: 1, + Wishing: "测试红包", + ActName: "测试活动红包", + Remark: "测试红包备注", + NotifyWay: "MINI_PROGRAM_JSAPI", + SceneId: "", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + b, err := redpack.NewRedPack(cf) + if err != nil { + panic(err) + } + srv := MiniProgram{b} + + gotResponse, bodyBytes, err := srv.Send(tt.args.ctx, tt.args.req) + if err != nil { + t.Errorf("Send() error = %v", err) + return + } + t.Errorf("Send() gotResponse = %v, bodyBytes = %s", gotResponse, string(bodyBytes)) + }) + } +} + +func TestRedPack_SendBizRedPacketSign(t *testing.T) { + type args struct { + ctx context.Context + req *SendBizRedPacket + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "小程序发红包", + args: args{ + ctx: context.Background(), + req: &SendBizRedPacket{ + TimeStamp: fmt.Sprintf("%d", time.Now().Unix()), + NonceStr: "wxe3bd59243545fa8a11", + Package: "sendid%3D242e8abd163d300019b2cae74ba8e8c06e3f0e51ab84d16b3c80decd22a5b672%26ver%3D8%26sign%3D4110d649a5aef52dd6b95654ddf91ca7d5411ac159ace4e1a766b7d3967a1c3dfe1d256811445a4abda2d9cfa4a9b377a829258bd00d90313c6c346f2349fe5d%26mchid%3D11475856%26appid%3Dwxd27ebc41b85ce36d", + SignType: "MD5", + PaySign: "", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + + t.Run(tt.name, func(t *testing.T) { + + b, err := redpack.NewRedPack(cf) + if err != nil { + panic(err) + } + srv := MiniProgram{b} + + srv.SendBizRedPacketSign(tt.args.ctx, tt.args.req) + + str, _ := tt.args.req.Str() + t.Logf("SendBizRedPacketSign() gotResponse = %s", str) + }) + } +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/base.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/base.go new file mode 100644 index 0000000..a323c02 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/base.go @@ -0,0 +1,14 @@ +package official + +import ( + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" +) + +const ( + _sendUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack" + _queryUrl = "https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo" +) + +type Official struct { + *redpack.RedPack +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/model.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/model.go new file mode 100644 index 0000000..d0c775d --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/model.go @@ -0,0 +1,192 @@ +package official + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "github.com/go-playground/validator/v10" +) + +type Str interface { + Str() (string, error) +} + +type Req interface { + Str + Validate() error + ToXML() ([]byte, error) +} + +var _ Req = (*SendRequest)(nil) +var _ Str = (*SendResponse)(nil) + +var _ Req = (*QueryRequest)(nil) +var _ Str = (*QueryResponse)(nil) + +// SendRequest 红包发送请求结构体(兼容 JSON/XML 双格式,XML 无 CDATA 包裹) +type SendRequest struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点为 ,JSON 忽略该字段 + NonceStr string `xml:"nonce_str" json:"nonce_str" validate:"required"` // 随机字符串(无 CDATA) + Sign string `xml:"sign" json:"sign"` // 签名(无 CDATA) + MchBillno string `xml:"mch_billno" json:"mch_billno" validate:"required"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id" json:"mch_id" validate:"required"` // 商户号(无 CDATA) + Wxappid string `xml:"wxappid" json:"wxappid" validate:"required"` // 小程序 appid(无 CDATA) + SendName string `xml:"send_name,omitempty" json:"send_name,omitempty" validate:"required"` // 发送者名称(无 CDATA,空值忽略) + ReOpenid string `xml:"re_openid" json:"re_openid" validate:"required"` // 用户 openid(无 CDATA) + TotalAmount int32 `xml:"total_amount" json:"total_amount" validate:"required"` // 付款金额(分,数值类型) + TotalNum int32 `xml:"total_num" json:"total_num" validate:"required"` // 发放人数(数值类型) + Wishing string `xml:"wishing" json:"wishing" validate:"required"` // 祝福语(无 CDATA) + ClientIp string `xml:"client_ip" json:"client_ip" validate:"required"` // 该IP同在商户平台设置的IP白名单中的IP没有关联,该IP可传用户端或者服务端的IP。 + ActName string `xml:"act_name" json:"act_name" validate:"required"` // 活动名称(无 CDATA) + Remark string `xml:"remark" json:"remark" validate:"required"` // 备注(无 CDATA) + SceneId string `xml:"scene_id,omitempty" json:"scene_id,omitempty"` // 场景 ID(无 CDATA,空值忽略) + RiskInfo string `xml:"risk_info,omitempty" json:"risk_info,omitempty"` // 活动信息 +} + +// SendResponse 红包发送响应结构体(兼容 JSON/XML 双格式,XML 无 CDATA 包裹) +type SendResponse struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点 + ReturnCode string `xml:"return_code,omitempty" json:"return_code"` // 返回状态码(无 CDATA) + ReturnMsg string `xml:"return_msg,omitempty" json:"return_msg"` // 返回信息(无 CDATA) + // 以下字段在return_code为SUCCESS的时候有返回 + ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"` // 业务结果(无 CDATA) + ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` // 错误码(无 CDATA) + ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` // 错误描述(无 CDATA,修正为 string) + // 以下字段在return_code和result_code都为SUCCESS的时候有返回 + MchBillno string `xml:"mch_billno,omitempty" json:"mch_billno,omitempty"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"` // 商户号(无 CDATA) + Wxappid string `xml:"wxappid,omitempty" json:"wxappid,omitempty"` // 小程序 appid(无 CDATA) + ReOpenid string `xml:"re_openid,omitempty" json:"re_openid,omitempty"` // 用户 openid(无 CDATA) + SendListid string `xml:"send_listid,omitempty" json:"send_listid,omitempty"` // 付款金额(无 CDATA) +} + +func (req *SendRequest) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +func (req *SendRequest) Validate() error { + + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + + return nil +} + +// ToXML 将 SendRequest 序列化为无 CDATA 的 XML 字节流(带 XML 声明) +func (req *SendRequest) ToXML() ([]byte, error) { + // 生成格式化 XML + data, err := xml.MarshalIndent(req, "", " ") + if err != nil { + return nil, fmt.Errorf("xml marshal failed: %w", err) + } + // 拼接 XML 声明(指定 UTF-8 编码) + xmlData := []byte(xml.Header + string(data)) + return xmlData, nil +} + +func (req *SendResponse) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +// QueryRequest . +type QueryRequest struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点为 ,JSON 忽略该字段 + NonceStr string `xml:"nonce_str" json:"nonce_str" validate:"required"` // 随机字符串(无 CDATA) + MchBillno string `xml:"mch_billno" json:"mch_billno" validate:"required"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id" json:"mch_id" validate:"required"` // 商户号(无 CDATA) + Appid string `xml:"appid" json:"appid" validate:"required"` // 小程序 appid(无 CDATA) + BillType string `xml:"bill_type" json:"bill_type" validate:"required"` // 场景 ID(无 CDATA,空值忽略) MCHT:通过商户订单号获取红包信息。 + Sign string `xml:"sign" json:"sign"` // 签名(无 CDATA) +} + +// QueryResponse . +type QueryResponse struct { + XMLName xml.Name `xml:"xml" json:"-"` // XML 根节点 + ReturnCode string `xml:"return_code" json:"return_code"` // 返回状态码(无 CDATA) + ReturnMsg string `xml:"return_msg" json:"return_msg"` // 返回信息(无 CDATA) + // 以下字段在return_code为SUCCESS的时候有返回 + ResultCode string `xml:"result_code,omitempty" json:"result_code,omitempty"` // 业务结果(无 CDATA) + ErrCode string `xml:"err_code,omitempty" json:"err_code,omitempty"` // 错误码(无 CDATA) + ErrCodeDes string `xml:"err_code_des,omitempty" json:"err_code_des,omitempty"` // 错误描述(无 CDATA,修正为 string) + // 以下字段在return_code 和result_code都为SUCCESS的时候有返回 + MchBillno string `xml:"mch_billno,omitempty" json:"mch_billno,omitempty"` // 商户订单号(无 CDATA) + MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"` // 商户号(无 CDATA) + DetailId string `xml:"detail_id,omitempty" json:"detail_id,omitempty"` // 使用API发放现金红包时返回的红包单号 + Status string `xml:"status,omitempty" json:"status,omitempty"` // 红包状态 + SendType string `xml:"send_type,omitempty" json:"send_type,omitempty"` // 发放类型 + TotalNum int `xml:"total_num,omitempty" json:"total_num,omitempty"` // 红包个数 + TotalAmount int `xml:"total_amount,omitempty" json:"total_amount,omitempty"` // 小程序跳转参数(无 CDATA) + Reason string `xml:"reason,omitempty" json:"reason,omitempty"` // 失败原因 + SendTime string `xml:"send_time,omitempty" json:"send_time,omitempty"` // 红包发送时间 + RefundTime string `xml:"refund_time,omitempty" json:"refund_time,omitempty"` // 红包退款时间 + RefundAmount string `xml:"refund_amount,omitempty" json:"refund_amount,omitempty"` // 红包退款金额 + Wishing string `xml:"wishing,omitempty" json:"wishing,omitempty"` // 祝福语 + Remark string `xml:"remark,omitempty" json:"remark,omitempty"` // 活动描述,低版本微信可见 + ActName string `xml:"act_name,omitempty" json:"act_name,omitempty"` // 活动名称 + Hblist []Hbinfo `xml:"hblist,omitempty" json:"hblist,omitempty"` // 裂变红包领取列表 + Openid string `xml:"openid,omitempty" json:"openid,omitempty"` // 领取红包的Openid + Amount int `xml:"amount,omitempty" json:"amount,omitempty"` // 金额 + RcvTime string `xml:"rcv_time,omitempty" json:"rcv_time,omitempty"` // 接收时间 +} + +type Hbinfo struct { + Openid string `xml:"openid" json:"openid"` + Amount int `xml:"amount" json:"amount"` + RcvTime string `xml:"rcv_time" json:"rcv_time"` +} + +func (req *QueryRequest) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} + +func (req *QueryRequest) Validate() error { + + err := validator.New().Struct(req) + if err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf("参数有误:" + err.Error()) + } + } + + return nil +} + +// ToXML 将 SendRequest 序列化为无 CDATA 的 XML 字节流(带 XML 声明) +func (req *QueryRequest) ToXML() ([]byte, error) { + // 生成格式化 XML + data, err := xml.MarshalIndent(req, "", " ") + if err != nil { + return nil, fmt.Errorf("xml marshal failed: %w", err) + } + // 拼接 XML 声明(指定 UTF-8 编码) + xmlData := []byte(xml.Header + string(data)) + return xmlData, nil +} + +func (req *QueryResponse) Str() (string, error) { + + b, err := json.Marshal(req) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query.go new file mode 100644 index 0000000..487a746 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query.go @@ -0,0 +1,77 @@ +package official + +import ( + "context" + "encoding/xml" + "fmt" +) + +// Query 查询红包记录 +// @link https://pay.weixin.qq.com/doc/v2/merchant/4011981612 +func (srv *Official) Query(ctx context.Context, req *QueryRequest) (response *SendResponse, respBody []byte, err error) { + + reqBody, err := srv.queryReqBody(req) + if err != nil { + return + } + + respBody, err = srv.Post(ctx, _queryUrl, reqBody) + if err != nil { + return nil, nil, err + } + + response, err = srv.ParseXMLQueryResponse(respBody) + + return +} + +func (srv *Official) queryReqBody(req *QueryRequest) (reqBody []byte, err error) { + + req.BillType = "MCHT" + + if err = req.Validate(); err != nil { + return + } + + req.Sign = srv.generateQueryReqBodySign(req) + + reqBody, err = req.ToXML() + if err != nil { + return + } + + return +} + +func (srv *Official) generateQueryReqBodySign(req *QueryRequest) string { + + params := make(map[string]string) + if req.NonceStr != "" { + params["nonce_str"] = req.NonceStr + } + if req.MchBillno != "" { + params["mch_billno"] = req.MchBillno + } + if req.MchId != "" { + params["mch_id"] = req.MchId + } + if req.Appid != "" { + params["appid"] = req.Appid + } + if req.BillType != "" { + params["bill_type"] = req.BillType + } + + return srv.Sign(params) +} + +func (srv *Official) ParseXMLQueryResponse(xmlData []byte) (*SendResponse, error) { + + var v *SendResponse + + if err := xml.Unmarshal(xmlData, &v); err != nil { + return nil, fmt.Errorf("xml unmarshal failed: %w", err) + } + + return v, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query_test.go new file mode 100644 index 0000000..afe7fcf --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/query_test.go @@ -0,0 +1,57 @@ +package official + +import ( + "context" + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" + "testing" +) + +func TestRedPack_Query(t *testing.T) { + type args struct { + ctx context.Context + req *QueryRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "小程序查询红包", + args: args{ + ctx: context.Background(), + req: &QueryRequest{ + NonceStr: "123456", + MchBillno: "1652322442TEST001", + MchId: "1652322442", + Appid: "wx075c784fe71d4d04", + BillType: "", + Sign: "", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + b, err := redpack.NewRedPack(&redpack.Config{ + MchId: "1652322442", + ApiV2Key: "bcee0c6753b2a31c792a91fe9f9f1666", + CertFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_cert.pem", + KeyFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_key.pem", + }) + if err != nil { + panic(err) + } + srv := Official{b} + + gotResponse, bodyBytes, err := srv.Query(tt.args.ctx, tt.args.req) + if err != nil { + t.Errorf("Send() error = %v", err) + return + } + t.Errorf("Send() gotResponse = %v, bodyBytes = %s", gotResponse, string(bodyBytes)) + }) + } +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send.go new file mode 100644 index 0000000..9dda419 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send.go @@ -0,0 +1,106 @@ +package official + +import ( + "context" + "encoding/xml" + "fmt" + "strconv" +) + +// Send 发送红包 +// @link https://pay.weixin.qq.com/doc/v2/merchant/4011974053 +func (srv *Official) Send(ctx context.Context, req *SendRequest) (response *SendResponse, respBody []byte, err error) { + + reqBody, err := srv.sendReqBody(req) + if err != nil { + return + } + + respBody, err = srv.Post(ctx, _sendUrl, reqBody) + if err != nil { + return + } + + response, err = srv.ParseXMLSendResponse(respBody) + if err != nil { + return + } + + return +} + +func (srv *Official) sendReqBody(req *SendRequest) (reqBody []byte, err error) { + + if err = req.Validate(); err != nil { + return + } + + req.Sign = srv.generateSendReqBodySign(req) + + reqBody, err = req.ToXML() + if err != nil { + return + } + + return +} + +func (srv *Official) generateSendReqBodySign(req *SendRequest) string { + // 1. 构造参数字典 + params := make(map[string]string) + if req.NonceStr != "" { + params["nonce_str"] = req.NonceStr + } + if req.MchBillno != "" { + params["mch_billno"] = req.MchBillno + } + if req.MchId != "" { + params["mch_id"] = req.MchId + } + if req.Wxappid != "" { + params["wxappid"] = req.Wxappid + } + if req.SendName != "" { + params["send_name"] = req.SendName + } + if req.ReOpenid != "" { + params["re_openid"] = req.ReOpenid + } + if req.TotalAmount != 0 { + params["total_amount"] = strconv.FormatInt(int64(req.TotalAmount), 10) + } + if req.TotalNum != 0 { + params["total_num"] = strconv.FormatInt(int64(req.TotalNum), 10) + } + if req.Wishing != "" { + params["wishing"] = req.Wishing + } + if req.ActName != "" { + params["act_name"] = req.ActName + } + if req.Remark != "" { + params["remark"] = req.Remark + } + if req.ClientIp != "" { + params["client_ip"] = req.ClientIp + } + if req.SceneId != "" { + params["scene_id"] = req.SceneId + } + if req.RiskInfo != "" { + params["risk_info"] = req.RiskInfo + } + + return srv.Sign(params) +} + +func (srv *Official) ParseXMLSendResponse(xmlData []byte) (*SendResponse, error) { + + var v *SendResponse + + if err := xml.Unmarshal(xmlData, &v); err != nil { + return nil, fmt.Errorf("xml unmarshal failed: %w", err) + } + + return v, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send_test.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send_test.go new file mode 100644 index 0000000..8fa9024 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/official/send_test.go @@ -0,0 +1,66 @@ +package official + +import ( + "context" + "plugins/wechat_redpack_v2/internal/wechat/srv/redpack" + "testing" +) + +func TestRedPack_Send(t *testing.T) { + type args struct { + ctx context.Context + req *SendRequest + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "小程序发红包", + args: args{ + ctx: context.Background(), + req: &SendRequest{ + NonceStr: "wxe3bd59243545fa8a", + Sign: "", + MchBillno: "1652322442TEST001", + MchId: "1652322442", + Wxappid: "wxe3bd59243545fa8a", + SendName: "福建兴旺", + ReOpenid: "ojbqr6HpeWKFy9Sgdx8yCmmeVJiw", + TotalAmount: 10, + TotalNum: 1, + ClientIp: "127.0.0.1", + Wishing: "测试公众号红包", + ActName: "测试公众号活动红包", + Remark: "测试公众号红包备注", + SceneId: "", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + b, err := redpack.NewRedPack(&redpack.Config{ + MchId: "1652322442", + ApiV2Key: "bcee0c6753b2a31c792a91fe9f9f1666", + CertFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_cert.pem", + KeyFile: "/Users/lsxd/code/go/lsxd/market/plugins//cert/wechat/1652322442_2/apiclient_key.pem", + }) + if err != nil { + panic(err) + } + + srv := Official{b} + + gotResponse, bodyBytes, err := srv.Send(tt.args.ctx, tt.args.req) + if err != nil { + t.Errorf("Send() error = %v", err) + return + } + t.Errorf("Send() gotResponse = %v, bodyBytes = %s", gotResponse, string(bodyBytes)) + }) + } +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/redpack.go b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/redpack.go new file mode 100644 index 0000000..70f652c --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/srv/redpack/redpack.go @@ -0,0 +1,101 @@ +package redpack + +import ( + "bytes" + "context" + "crypto/md5" + "crypto/tls" + "encoding/hex" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" +) + +type Config struct { + MchId string + ApiV2Key string + CertFile string // 证书路径 + KeyFile string // 私钥路径 +} + +type RedPack struct { + Config *Config + + transport *http.Transport +} + +func NewRedPack(config *Config) (*RedPack, error) { + + // 加载证书,配置双向 TLS + cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) + if err != nil { + return nil, fmt.Errorf("load cert failed: %w", err) + } + + // 关键修改:不创建新的 certPool,而是使用系统默认根证书池 + // 这样客户端能验证微信支付服务端的证书合法性 + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, // 仅添加商户证书(用于双向认证) + // 注释掉 RootCAs 配置,让 Go 使用系统默认根证书池 + // RootCAs: clientCertPool, + } + transport := &http.Transport{TLSClientConfig: tlsConfig} + + return &RedPack{Config: config, transport: transport}, nil +} + +func (srv *RedPack) Sign(params map[string]string) string { + + // 排序 + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + // 拼接字符串 + var buf strings.Builder + for _, k := range keys { + buf.WriteString(k) + buf.WriteString("=") + buf.WriteString(params[k]) + buf.WriteString("&") + } + // 拼接 API 密钥 + buf.WriteString("key=") + buf.WriteString(srv.Config.ApiV2Key) + + hash := md5.Sum([]byte(buf.String())) + sign := strings.ToUpper(hex.EncodeToString(hash[:])) + return sign +} + +func (srv *RedPack) Post(_ context.Context, url string, reqBody []byte) (respBytes []byte, err error) { + + // 发送 POST 请求 + httpReq, err := http.NewRequest("POST", url, bytes.NewReader(reqBody)) + if err != nil { + return nil, fmt.Errorf("create request failed: %w", err) + } + httpReq.Header.Set("Content-Type", "application/xml; charset=utf-8") + + client := &http.Client{Transport: srv.transport, Timeout: 20 * time.Second} + + // 执行请求 + httpResp, err := client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("send request failed: %w", err) + } + defer httpResp.Body.Close() + + // 解析响应 + respBytes, err = io.ReadAll(httpResp.Body) + if err != nil { + return nil, fmt.Errorf("read response failed: %w", err) + } + + return respBytes, nil +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/utils/md5.go b/plugins/wechat_redpack_v2/internal/wechat/utils/md5.go new file mode 100644 index 0000000..af59c99 --- /dev/null +++ b/plugins/wechat_redpack_v2/internal/wechat/utils/md5.go @@ -0,0 +1,30 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "strings" +) + +func MD5ToUpper(data string) string { + // 创建一个 MD5 哈希对象 + hash := md5.New() + // 写入待加密的数据 + hash.Write([]byte(data)) + // 获取 MD5 哈希值 + hashBytes := hash.Sum(nil) + // 将 MD5 哈希值转换为16进制字符串 + str := hex.EncodeToString(hashBytes) + return strings.ToUpper(str) +} + +func MD5(data string) string { + // 创建一个 MD5 哈希对象 + hash := md5.New() + // 写入待加密的数据 + hash.Write([]byte(data)) + // 获取 MD5 哈希值 + hashBytes := hash.Sum(nil) + // 将 MD5 哈希值转换为16进制字符串 + return hex.EncodeToString(hashBytes) +} diff --git a/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go index 0c2a4da..ad7f69c 100644 --- a/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go +++ b/plugins/wechat_redpack_v2/internal/wechat/utils/wxpay_utility.go @@ -480,8 +480,7 @@ func (srv *MchConfig) Request(host, method, path string, reqBody []byte) (respon respBody, err := ExtractResponseBody(httpResponse) if err != nil { - fmt.Printf("获取应答失败,Error: %v", err) - return nil, err + return nil, fmt.Errorf("获取应答失败:%w", err) } if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 { diff --git a/plugins/zltx_card_v1/go.mod b/plugins/zltx_card_v1/go.mod index 66006da..a07268c 100644 --- a/plugins/zltx_card_v1/go.mod +++ b/plugins/zltx_card_v1/go.mod @@ -3,7 +3,7 @@ module plugins/zltx_card_v1 go 1.22.2 require ( - gitea.cdlsxd.cn/sdk/plugin v1.0.24 + gitea.cdlsxd.cn/sdk/plugin v1.0.25 github.com/go-playground/validator/v10 v10.22.0 github.com/hashicorp/go-plugin v1.6.1 github.com/stretchr/testify v1.10.0 diff --git a/plugins/zltx_card_v1/go.sum b/plugins/zltx_card_v1/go.sum index 007223c..63fc516 100644 --- a/plugins/zltx_card_v1/go.sum +++ b/plugins/zltx_card_v1/go.sum @@ -1,5 +1,5 @@ -gitea.cdlsxd.cn/sdk/plugin v1.0.24 h1:B6sizQzFhb9Cin+iCYLKXiIBGyH/hCYQfl7CmkkF+ck= -gitea.cdlsxd.cn/sdk/plugin v1.0.24/go.mod h1:4fLMp/xB9GEBa3nJi62kXpHh7wnb9Lwjf0I8Vjaasx0= +gitea.cdlsxd.cn/sdk/plugin v1.0.25 h1:z7AnsIUiakqbcTg9/RAxicCLT4Y5NkayA1jLT+Ptw58= +gitea.cdlsxd.cn/sdk/plugin v1.0.25/go.mod h1:4fLMp/xB9GEBa3nJi62kXpHh7wnb9Lwjf0I8Vjaasx0= 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= diff --git a/plugins/zltx_card_v1/internal/zltx_card_v1.go b/plugins/zltx_card_v1/internal/zltx_card_v1.go index 3f10459..665e9ab 100644 --- a/plugins/zltx_card_v1/internal/zltx_card_v1.go +++ b/plugins/zltx_card_v1/internal/zltx_card_v1.go @@ -12,10 +12,10 @@ import ( // 插件通信信息,若不对应则会报错panic const ( - Tag = "zltx_card_v1_1" + Tag = "zltx_card_v1" Version = 1 - CookieKey = "zltx_card_v1_1" - CookieValue = "zltx_card_v1_1" + CookieKey = "zltx_card_v1" + CookieValue = "zltx_card_v1" ) type ZLTXCardV1Service struct{} diff --git a/utils/request/request_test.go b/utils/request/request_test.go index 4f13710..7482278 100644 --- a/utils/request/request_test.go +++ b/utils/request/request_test.go @@ -24,8 +24,8 @@ func Test_Get(t *testing.T) { return } - t.Logf("响应体:", string(respBody)) - t.Logf("响应头:", respHeader) + t.Logf("响应体:%s", string(respBody)) + t.Logf("响应头:%v", respHeader) } func Test_RequestHeaders(t *testing.T) { @@ -43,8 +43,8 @@ func Test_RequestHeaders(t *testing.T) { return } - t.Logf("响应体:", string(respBody)) - t.Logf("响应头:", respHeader) + t.Log("响应体:", string(respBody)) + t.Log("响应头:", respHeader) } func Test_RequestStatusCode(t *testing.T) { @@ -61,6 +61,6 @@ func Test_RequestStatusCode(t *testing.T) { return } - t.Logf("响应体:", string(respBody)) - t.Logf("响应头:", respHeader) + t.Log("响应体:", string(respBody)) + t.Log("响应头:", respHeader) }