This commit is contained in:
ziming 2025-12-24 15:30:46 +08:00
parent cd6a66bdf3
commit 1aaae37beb
54 changed files with 3367 additions and 56 deletions

View File

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

View File

@ -7,11 +7,12 @@ func main() {
//WechatRedPackV2Order()
//WechatRedPackV2Query()
WechatRedPackV2Notify()
//WechatRedPackV2Notify()
//alipayOrderRedPack()
//alipayQueryRedPack()
//zltxQuery()
zltxCardQuery()
//zltxCardNotify()
}

View File

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

View File

@ -95,7 +95,7 @@ func WechatRedPackV2Query() {
request := &proto.QueryRequest{
Config: getWechatRedPackV2Conf(),
Order: &proto.QueryRequest_Order{
OrderNo: "19497351672832450564",
OrderNo: "19957295972489175049",
TradeNo: "",
Account: "",
Extra: []byte(``),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 根节点为 <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 根节点为 <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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 根节点为 <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 根节点为 <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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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